diff --git a/docs/condition-scheduling-improvements.md b/docs/condition-scheduling-improvements.md
index c2c5a07..ab900f8 100644
--- a/docs/condition-scheduling-improvements.md
+++ b/docs/condition-scheduling-improvements.md
@@ -47,6 +47,6 @@
3ms 间隔误差 P95 略增,未放宽现有时序断言。额外 3000 轮持续测试中,1000 至 3000 轮线程数保持 15,句柄 300 至 301,私有内存 28.7 至 29.1MiB,未观察到持续增长;这些值在显式 GC 后采样,不代表长期无泄漏的证明。Skirk FPS 回归也全部通过。
-性能原始记录在 `tmp/timing-before.log`、`tmp/timing-after.log`、`tmp/timing-stress.log`,复现探针在 `tests/MacroHid.Scheduling.Benchmarks`。以 `dotnet run --project tests/MacroHid.Scheduling.Benchmarks -c Release -p:RuntimeRoot=<待测运行库目录> -- 300` 运行;目录须包含 Core、Runtime DLL。基准避免并发运行以减少负载干扰。实际游戏及设备兼容性仍需目标环境验证;GitHub 发布附件不由本地打包流程更新。
+性能原始记录在 `tmp/timing-before.log`、`tmp/timing-after.log`、`tmp/timing-stress.log`,复现探针已迁到 MacroLab 仓库的 `tests/MacroHid.Scheduling.Benchmarks`。以 `dotnet run --project tests/MacroHid.Scheduling.Benchmarks -c Release -p:RuntimeRoot=<待测运行库目录> -- 300` 运行;目录须包含 Core、Runtime DLL。基准避免并发运行以减少负载干扰。实际游戏及设备兼容性仍需目标环境验证;GitHub 发布附件不由本地打包流程更新。
本次安装包、便携包和自包含本地运行版均已成功构建,固定路径与 Start Item 快捷方式已核对;便携包与本地版的 Core、Runtime、UI DLL 哈希一致。构建保留原有 NetLimiter 5.3.18 的 NU1701 框架兼容性警告,无编译错误。
diff --git a/docs/current-precision-status.md b/docs/current-precision-status.md
deleted file mode 100644
index 4cb849a..0000000
--- a/docs/current-precision-status.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# MacroHID 当前实现与精度验证状态
-
-更新日期:2026-10-14。原双宏记录保留 2026-10-13 的测量口径。
-
-最新单宏补充见 [尾部优化与真实接收实测](single-macro-tail-and-end-to-end-2026-10-14.md):极限档固定核五轮十万事件,最大迟到对照 1,230.4µs、保留方案 893.4µs;物理热键 30 次到独立 Win32 接收窗口,中位 3.461ms、最大 8.708ms。最终版本另有 Win32/WPF 各 2,000 次自动真实输入接收,全部有效。托管回归 388 项通过。仍不能声称微秒级端到端或硬实时最大值保证。
-
-三档策略、准备缓存、共享双线程原生调度、图像缓冲优化、界面状态与验收工具已实现。本轮功能回归通过;真实热键端到端、真实 SendInput 接收延迟及游戏帧时间联合验收尚未完成。以下数据不能视为硬实时保证,也不能推广到未测试设备或游戏。
-
-## 2026-10-14 单宏增量
-
-新增单宏连续执行、修饰键边沿预计算、统计缓冲复用和均衡档有界自旋适应;极限定时事件保留辅助线程。单宏五轮交替对照,每方案每档累计十万假输入事件:均衡逐轮 P99.9 为 12.3–46.9µs,全部样本最大 1428.3µs;极限为 2.7–3.6µs,最大 287.9µs。均衡 CPU 代价增加,真实接收与游戏帧时间尚未验收。当前 Release 构建成功,托管测试 388 项和原生回归通过。完整参数、基线、逐轮数据、回退与本地测试版见 [单宏优化记录](single-macro-optimization-2026-10-14.md)。
-
-## 2026-10-13 环境与功能验证
-
-参考机器:AMD Ryzen 9 7945HX,16 个物理核心、32 个逻辑处理器;Windows 11 build 26200;.NET SDK 8.0.416、运行时 8.0.22。未建立固定游戏、温度、电源和 CPU/GPU 负载矩阵。
-
-| 检查 | 结果 |
-|---|---|
-| Release 原生 DLL 构建 | 成功,0 错误 |
-| Release 解决方案构建 | 成功,0 错误,14 个现有依赖兼容性/平台警告 |
-| 托管功能、语义及回归测试 | 386 项通过 |
-| 原生假输入调度回归 | 并发、暂停、取消、资源保留、重复提交、部分失败、退出重启通过 |
-| 帧时间比较器测试 | 3 项通过 |
-
-新安装默认均衡,已有枚举和序列化值保留。运行使用启动快照。新路径不提升进程优先级,不修改进程亲和性;极限仅绑定原生执行线程。原生工作线程就绪后才宣告准备完成;运行中的未缓存测核配置会推迟准备并显示原因,停止后重新监听可重新准备。
-
-## 原生调度实测
-
-最终验证使用两个并发计划,每个 50000 个批次,1ms 节奏,每档共 100000 个有效事件。使用测试专用假输入回调,不调用真实 SendInput;预热后采样,探针申请与托管播放会话一致的 0.5ms 计时器分辨率。以下两列分位数分别对应两个计划,不是重复试验。
-
-| 档位 | 目标 P99.9 | 计划 1 / 2 的 P99.9 迟到量 | 计划 1 / 2 的最大迟到量 | 本次调度探针 |
-|---|---:|---:|---:|---|
-| 均衡 | ≤250µs | 186 / 4µs | 1094 / 876µs | 达到分位数目标 |
-| 极限 | ≤100µs | 2 / 2µs | 644 / 1071µs | 达到分位数目标 |
-
-均衡两个计划的长尾差异较大;不能用较小的一列代替整体情况。毫秒级最大迟到仍存在。每档当前只有一次最终版本探针,不满足完整性能验收的重复次数和场景要求。
-
-此前独立原生探针未申请播放会话的计时器分辨率,均衡 P99.9 为 459 / 151µs,第一路未达标,最大值为 1018 / 857µs。补齐会话条件后的前一次复测为 115 / 81µs;该次与最终版本之间还包含工作线程准备和资源生命周期修正,不能将变化全部归因于单一因素。保留这些结果用于后续复现实验,不删除未达标样本。
-
-最终原始 QPC 数据和文件校验值保存在本地工作区:
-
-- [均衡十万事件 CSV](../tmp/native-balanced-verified-100k.csv)
-- [极限十万事件 CSV](../tmp/native-extreme-verified-100k.csv)
-- [测试 EXE、生产 DLL 与 CSV 的 SHA-256](../tmp/performance-provenance.json)
-- [原生功能回归日志](../tmp/native-scheduler-regressions.log)
-- [托管测试日志](../tmp/performance-tests.log)
-
-这些是本次本地生成的证据,不是随发行版附带的性能保证。
-
-## 确定性瓶颈修复与微基准
-
-| 项目 | 优化前 / 对照 | 本次结果 | 解释 |
-|---|---|---|---|
-| 300ms 长计划旁的 1ms 短计划 | 整段锁等待导致约 256–265ms | 均衡 1.020ms;极限 1.006ms | 批次调度消除了整计划串行阻塞 |
-| 独立取消等待中的短计划 | 约 237–247ms | 两档均为 4µs | 仅本次回归样本,不是取消 P99 |
-| 640×360 颜色转换分配 | 对象数组 7,372,829B | BGRA 视图 40B | 转换阶段下降超过 99%,不包含截图和识别 |
-| 同一转换中位耗时 | 2359.2µs | 0.1µs | 新路径复用现有 BGRA,仅建立视图;接近计时分辨率,不应解释为完整图像处理吞吐 |
-
-转换探针 31 次测量满足分配下降至少 85%、中位耗时下降至少 50% 的阶段目标。模板近阈值、平坦图像、像素颜色、哈希和缓冲租约回归通过;完整条件链路耗时仍需另测。
-
-补充托管假输入探针共 1000 个样本,RunAsync 调用至首次提交的 P99 为 100µs。该探针没有覆盖真实热键回调,样本量也不足十万,仅供启动路径诊断,不作为低占用档响应验收结论。
-
-## 尚未通过完整验收的项目
-
-- 三档及完整主窗口路线的真实热键 P99、游戏处理延迟;极限单宏独立窗口的物理 30 次及自动真实输入接收已完成,但物理样本不足估计 P99。SendInput 返回不等于游戏收到输入。
-- 三档取消请求至清理完成的十万事件 P99;当前原生取消回归验证正确性和单次耗时。
-- 低占用档完整十万事件调度迟到测量;原生探针仅覆盖均衡和极限。
-- 固定游戏回放下关闭宏、低占用、均衡、极限的 PresentMon 对照。尚无游戏 FPS 下降和 P99 帧时间退化结论。
-- 纯播放与混合识别分别进行至少五次交替顺序、每次十分钟测试;冷启动、预热、CPU/GPU 受限、像素、模板/OCR 并发与长期运行矩阵。
-- 不同设备、处理器拓扑及游戏版本的兼容性验证。本机 32 逻辑处理器结果不证明跨处理器组适用性。
-
-同键并发回归验证宏内顺序和提交数量,不把操作系统共享的键状态改造成每宏独立设备状态。实际游戏还需检查同键冲突行为。
-
-目标、命令、原始时间戳口径、回退开关和游戏帧时间预算见 [性能档位与验收](precision.md)。极限精度达标而游戏帧时间超预算时,仍推荐均衡;不自动更改用户选择,也不降低验收标准。
diff --git a/docs/precision-audit-2026-10-10.md b/docs/precision-audit-2026-10-10.md
deleted file mode 100644
index 45f29af..0000000
--- a/docs/precision-audit-2026-10-10.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# 精度与稳定性复核(2026-10-10)
-
-## 修复
-
-- 条件监控线程捕获非取消异常并请求停止播放,退出时把错误交回播放控制。
-- 先取消所有条件线程,等待退出后才释放取消源和评估器相关资源;避免原先 200ms 超时后的资源竞争。不可取消的自定义评估器会延长停止等待,需要自行保证有界返回。
-- 慢识别跳过错过的轮询时刻,避免持续追赶旧时刻。
-- 托管进程优先级、GC 和节能设置采用共享会话计数,最后一个相关会话结束才恢复。
-- 托管和原生极限模式不再提高到实时进程优先级,使用 High;保持线程级调度、绑核和待命执行策略。
-
-351 项回归测试通过,包含条件线程异常、慢评估器退出和调度状态恢复测试。
-
-## 三种模式测量
-
-测试机器为当前本机。每种模式串行测试 3 轮,每轮 5000 个 1ms 间隔,共每模式 15000 个时刻。编译完成后测量,原生引擎预热并预创建计划,自动选择执行路径。不发送真实输入;未施加固定游戏/CPU 压力负载。
-
-| 模式 | 路径 | 三轮 P99.9(微秒) | 三轮最大偏差(微秒) |
-| --- | --- | --- | --- |
-| 基础 | managed | 23 / 46 / 24 | 188 / 153 / 82 |
-| 高性能 | native-inline | 63 / 76 / 2 | 104 / 121 / 64 |
-| 极限 | native-standby | 1 / 1 / 1 | 2 / 2 / 1 |
-
-以上是计划时刻到执行线程到达时刻的偏差,不是应用接收输入或画面响应延迟。原生与托管采用各自统计器,因此这是实际模式路径对比,不是完全同一实现的微基准。
-
-附加托管路径对比:基础 P99 5–7us,高性能 5–9us,极限 10–19us。附加原生测量期间存在后台编译,高性能最大出现 1638us,极限最大出现 174us,说明空载结果不能当作最大延迟保证。
-
-下一步应测游戏负载、多宏并发、长时间循环、停止响应以及真实输入提交延迟。本轮未重新发布 GitHub 的 1.6 安装附件。
diff --git a/docs/precision.md b/docs/precision.md
index 41744eb..04b1cf1 100644
--- a/docs/precision.md
+++ b/docs/precision.md
@@ -1,6 +1,6 @@
# MacroHID 用户态性能档位与验收
-2026-10-14 单宏尾部与真实接收补充:保留原生线程预配置与空闲配置保留,撤回两项尾部退化的实验;最终版本通过 30 次物理热键及 Win32/WPF 各 2,000 次自动接收。具体数值、最大值波动、测量起止点和复现命令见 [实测报告](single-macro-tail-and-end-to-end-2026-10-14.md)。原生微秒级调度误差不能代替端到端接收验收。
+2026-10-14 单宏尾部与真实接收补充:保留原生线程预配置与空闲配置保留,撤回两项尾部退化的实验;最终版本通过 30 次物理热键及 Win32/WPF 各 2,000 次自动接收。具体数值、最大值波动、测量起止点和复现命令见 [实测报告](https://github.com/NotBadSlime/MacroLab/blob/main/docs/single-macro-tail-and-end-to-end-2026-10-14.md)(已迁到 MacroLab 仓库)。原生微秒级调度误差不能代替端到端接收验收。
## 当前策略
@@ -93,4 +93,4 @@ CSV 保留 run、batch、due_qpc、submit_qpc、frequency、late_us。这是原
- 历史 Measure-PrecisionProfiles.ps1、-FailOnTargetMiss、-AutoTuneAffinity、Tune-LatencyAffinity.ps1、-TunePasses 与 edge-drop 核心候选策略仍可用于单路径诊断,不能代替双宏 + 游戏联合验收。
- Trace-LatencyOutliers.ps1 可捕获 WPR CPU 数据;用 Windows Performance Analyzer 对齐 DPC/ISR、上下文切换与 QPC 长尾。
-当前实测和未完成的验收见 [current-precision-status.md](current-precision-status.md)。本机结果不能推广到未测试的游戏、版本和设备。
+当前实测和未完成的验收见 [current-precision-status.md](https://github.com/NotBadSlime/MacroLab/blob/main/docs/current-precision-status.md)(已迁到 MacroLab 仓库)。本机结果不能推广到未测试的游戏、版本和设备。
diff --git a/docs/release-1.0.0.md b/docs/release-1.0.0.md
index 13d1483..188bb2b 100644
--- a/docs/release-1.0.0.md
+++ b/docs/release-1.0.0.md
@@ -17,7 +17,7 @@ MacroHID `1.0.0` 是当前正式版。这个版本聚焦在 MacroStudio 可用
构建和核心测试都通过;极限档在本机使用 `0xE` affinity 后,这轮 5000 轮长测单步最大偏差 `45us`,整轮最大偏差 `43us`,没有 `>100us` outlier。
-详见:[当前精度水平](current-precision-status.md)。
+详见:[当前精度水平](https://github.com/NotBadSlime/MacroLab/blob/main/docs/current-precision-status.md)。
## 验证
diff --git a/docs/releases/2.0.0.md b/docs/releases/2.0.0.md
index 823078b..979efde 100644
--- a/docs/releases/2.0.0.md
+++ b/docs/releases/2.0.0.md
@@ -35,7 +35,7 @@
- 物理测量从低层钩子入口计时,未覆盖机械开关、USB 传输、游戏动作与显示器响应;独立探针未覆盖完整主窗口进程筛选路线。
- 发行前托管功能回归、原生调度回归及帧时间比较器检查通过;逐项结果见发行验证记录。
-以上数据来自一台 Windows 11 / Ryzen 9 7945HX 电脑。极限档持续自旋有明显 CPU 成本;未承诺跨设备表现、游戏帧时间或微秒级端到端。测量方法与更差的探索结果也保留在 [实测报告](../single-macro-tail-and-end-to-end-2026-10-14.md)。
+以上数据来自一台 Windows 11 / Ryzen 9 7945HX 电脑。极限档持续自旋有明显 CPU 成本;未承诺跨设备表现、游戏帧时间或微秒级端到端。测量方法与更差的探索结果也保留在 [实测报告](https://github.com/NotBadSlime/MacroLab/blob/main/docs/single-macro-tail-and-end-to-end-2026-10-14.md)。
## 打包与兼容性
diff --git a/docs/releases/2.0.1.md b/docs/releases/2.0.1.md
index 92d3f97..d35c897 100644
--- a/docs/releases/2.0.1.md
+++ b/docs/releases/2.0.1.md
@@ -17,7 +17,7 @@
## 未包含与未验证
-- 评估后不实施或撤回:把热键处理移出钩子线程(2.2)、定向唤醒(3.1);自旋窗口衰减(3.2)未实施。依据见 [阶段计划](../superpowers/plans/2026-10-14-scheduling-latency-phases.md)。
+- 评估后不实施或撤回:把热键处理移出钩子线程(2.2)、定向唤醒(3.1);自旋窗口衰减(3.2)未实施。依据见 [阶段计划](https://github.com/NotBadSlime/MacroLab/blob/main/docs/superpowers/plans/2026-10-14-scheduling-latency-phases.md)。
- 本版**没有**重新测量真实热键到目标接收的端到端数字,也没有对上述三项启动/热键入口改动做冷启动首样本的交替对照。`v2.0.0` 的性能数据不能直接套用到本版。
- 0.1–1.5ms 的调度尖峰尚未归因。托管测试 394 项通过。
diff --git a/docs/research/macro-scheduler-comprehensive-decision-report.md b/docs/research/macro-scheduler-comprehensive-decision-report.md
deleted file mode 100644
index 71928f0..0000000
--- a/docs/research/macro-scheduler-comprehensive-decision-report.md
+++ /dev/null
@@ -1,287 +0,0 @@
-# 宏软件调度架构综合优化决策报告
-
-**版本**: 1.0
-**日期**: 2026 年 10 月 13 日
-**状态**: 评审完成,待实施
-
----
-
-## 目录
-
-1. [执行摘要](#执行摘要)
-2. [技术评审概览](#技术评审概览)
-3. [调研发现汇总](#调研发现汇总)
-4. [优化建议评估](#优化建议评估)
-5. [代码改进优先级](#代码改进优先级)
-6. [实施路线图](#实施路线图)
-7. [附录](#附录)
-
----
-
-## 执行摘要
-
-本报告整合了以下内容:
-
-1. **深度调研报告**(8 万 + 字):涵盖主流宏软件架构、GitHub 开源项目、Windows 输入 API 特性
-2. **专家评审报告**:评级 A-,提出多项可行性评估和改进建议
-3. **综合优化决策**:明确立即实施、中期深化、前瞻探索三个阶段的任务清单
-
-### 核心结论
-
-- **MacroHID 已达到业界领先水平**,优化空间集中在微优化层面
-- **推荐立即落地的 3 项高 ROI 优化**:内存池化、CPU 亲和性自动化、GC 压力优化
-- **驱动级方案风险过高**,建议转为长期技术储备
-- **纯用户态方案的理论下限**约为 50-100μs(理想环境)
-
----
-
-## 技术评审概览
-
-### 整体评级:A-
-
-**优点**:
-- 技术深度充分,对 Windows 调度器和输入 API 栈理解准确
-- 现实边界清晰,避免过度承诺
-- 分级优化策略合理,便于渐进式落地
-- 数据驱动思维,强调基准测试和性能回归预防
-
-**风险点**:
-- 驱动级方案风险被低估(合规成本、HVCI 限制、反作弊检测)
-- 硬件宏 Offload 可行性分析不足
-- 部分优化建议缺乏代码基础支持
-
----
-
-## 调研发现汇总
-
-### 主流宏软件对比
-
-| 软件 | 架构特点 | 调度精度 | 备注 |
-|------|----------|----------|------|
-| **AutoHotKey** | 脚本解释型,事件驱动 | 约 1-5ms | 社区活跃但延迟较高 |
-| **Razer Synapse** | 云配置 + 驱动层执行 | <1ms | 依赖特定硬件 |
-| **Logitech G Hub** | 本地 + 云混合 | <1ms | 配置系统复杂 |
-| **MacroHID** | Native 播放 + Managed 调度 | <1ms | **当前实现已接近理论极限** |
-
-### Windows 输入 API 性能对比
-
-| API | 延迟特征 | 适用场景 | 注意事项 |
-|-----|----------|----------|----------|
-| **SendInput** | ~1-2ms(标准路径) | 通用宏注入 | 受 UIPI 限制 |
-| **SetWindowsHookEx** | ~0.5-1ms(低级别钩子) | 全局监听 | 回调中避免 GC 分配 |
-| **Raw Input** | ~0.3-0.8ms | 高精度游戏输入 | 实现复杂度较高 |
-| **驱动级虚拟 HID** | <0.1ms | 极致延迟需求 | 合规风险极高 |
-
-### 关键开源项目分析
-
-1. **ZMacro**:高性能 GUI 宏工具,C++ 实现,注重低延迟
-2. **InputStitch**:可视化宏编排,支持多层路由
-3. **Interception**:驱动级输入拦截,适合高级定制场景
-
----
-
-## 优化建议评估
-
-### 立即实施的优化(高 ROI)⭐⭐⭐⭐⭐
-
-#### 1. 内存池化
-**评分**: 4/5
-
-- **实施难度**:低
-- **预期收益**:Gen0 分配减少 50%+,GC 频率降低 40-60%
-- **实施要点**:
- ```csharp
- // 使用 ConcurrentQueue 实现线程安全的 INPUT[] 池
- private static readonly ConcurrentQueue InputBufferPool = new();
- ```
-
-#### 2. CPU 亲和性自动化
-**评分**: 4/5
-
-- **实施难度**:低
-- **预期收益**:新用户无需手动运行 PowerShell 脚本
-- **实施要点**:
- ```csharp
- // 首次启动时自动扫描最优核心组合
- // 缓存结果到 affinity-cache.json
- ```
-
-#### 3. GC 压力优化
-**评分**: 4.5/5
-
-- **实施难度**:中
-- **预期收益**:突发抖动减少 15-25%
-- **影响范围**:所有使用 Native 回退的场景
-
----
-
-### 建议暂缓的优化 ⏸️
-
-#### 1. 驱动级原型
-**评分**: 2/5
-
-- **技术风险**:高
-- **合规风险**:极高(EV 证书 $400+/年 + HVCI 限制 + 反作弊检测)
-- **建议**:转为长期技术储备,不作为短期目标
-
-#### 2. 中断协同优化
-**评分**: 3.5/5
-
-- **技术难点**:Windows 无官方 API 直接查询 IRQ 分布
-- **预期收益**:最大抖动降低 10-20%
-- **实施成本**:需通过 ETW 或 WMI 间接获取,复杂度高
-
-#### 3. 条件监控分离
-**评分**: 3/5
-
-- **技术难点**:增加状态同步复杂度
-- **缺失前提**:当前代码库未见 ConditionMonitor 实现基础
-- **建议**:先补齐条件评估框架基础设施
-
----
-
-## 代码改进优先级
-
-基于评审 Agent 的代码审查,以下是最需要优先改进的位置:
-
-### 🥇 优先级 1: `NativePlaybackEngine.cs` - 异常捕获路径的 GC 压力
-
-**位置**:`src/shared/MacroHid.Runtime/NativePlaybackEngine.cs:329-346`
-
-**问题**:
-```csharp
-catch (DllNotFoundException ex) {
- fallbackReason = $"native DLL unavailable: {ex.Message}"; // 字符串插值产生临时分配
- diagnostics = EmptyDiagnostics("managed", fallbackReason);
- return false;
-}
-```
-
-**改进方案**:
-```csharp
-// 使用预定义常量
-private static readonly string NativeDllUnavailableMsg = "native DLL unavailable";
-// 或在 Plan 创建阶段就预验证,避免运行时异常
-```
-
----
-
-### 🥈 优先级 2: `MacroScheduler.cs` - 动态 List 增长的内存浪费
-
-**位置**:`src/shared/MacroHid.Core/MacroScheduler.cs:32, 38-63`
-
-**问题**:
-```csharp
-var scheduled = new List(); // 默认容量 4,复杂宏会多次扩容
-```
-
-**改进方案**:
-```csharp
-var estimatedCapacity = CountScheduledSteps(document.Steps);
-var scheduled = new List(estimatedCapacity);
-```
-
----
-
-### 🥉 优先级 3: `GlobalKeyboardHook.cs` - 钩子回调中的 Marshal 开销
-
-**位置**:`src/ui/MacroStudio/GlobalKeyboardHook.cs:300, 330`
-
-**问题**:
-```csharp
-var data = Marshal.PtrToStructure(lParam); // 每次都 Marshal
-```
-
-**改进方案**:
-```csharp
-// 使用预先分配的 struct 复用
-private static readonly ThreadLocal KeyboardStructCache =
- new(() => new KbdLlHookStruct());
-
-// 或使用 Unsafe 直接内存拷贝
-Unsafe.CopyBlock(ref structField, ref lParam.ToPointer(), sizeof(KbdLlHookStruct));
-```
-
----
-
-## 实施路线图
-
-### 第一阶段(1-2 周):立即可行
-
-**目标**:快速见效,建立信心,完善测试基础设施
-
-| 任务 | 负责人 | 预计工时 | 验收标准 |
-|------|--------|----------|----------|
-| 1.1 实现 InputBufferPool 内存池化 | TBD | 8h | GC 频率降低 40%+(基准测试) |
-| 1.2 MacroScheduler 容量预分配 | TBD | 4h | List 扩容次数归零(profiling) |
-| 1.3 AffinityOptimizer 首次启动扫描 | TBD | 12h | 用户无需手动运行 PS 脚本 |
-| 1.4 补充基准测试套件 | TBD | 8h | CI/CD集成,自动回归检测 |
-| **合计** | - | **32h** | - |
-
----
-
-### 第二阶段(1-2 月):中期深化
-
-**目标**:系统性优化,提升长期可维护性
-
-| 任务 | 负责人 | 预计工时 | 验收标准 |
-|------|--------|----------|----------|
-| 2.1 零分配播放循环 | TBD | 40h | p99.9 抖动降低 20%+ |
-| 2.2 ETW 追踪基础设施 | TBD | 16h | 支持自定义 EventSource |
-| 2.3 Power Platform 集成 | TBD | 8h | 播放期间禁用睡眠/待机 |
-| 2.4 MMCSS 分类优化 | TBD | 8h | 适配 Game 类别的优先级 |
-| 2.5 多线程救援机制升级 | TBD | 24h | 量化 p99.9 延迟改善 |
-| **合计** | - | **96h** | - |
-
----
-
-### 第三阶段(3-6 月):前瞻探索
-
-**目标**:技术储备,验证创新方向
-
-| 任务 | 负责人 | 预计工时 | 验收标准 |
-|------|--------|----------|----------|
-| 3.1 硬件宏 Offload 兼容性研究 | TBD | 40h | 验证主流外设的 API 开放程度 |
-| 3.2 ML 预测调度原型 | TBD | 80h | 积累足够系统负载数据 |
-| 3.3 驱动级方案合规评估 | TBD | 24h | 完成法律风险评估 |
-| **合计** | - | **144h** | - |
-
----
-
-## 附录
-
-### A. 遗漏的技术点清单
-
-1. ✅ **MMCSS 注册细节缺失** - 建议补充 `AvSetMmThreadCharacteristics("Game")` 的使用策略
-2. ✅ **timer 分辨率副作用** - 需讨论长期高解析度对功耗的影响
-3. ✅ **NUMA 感知实际价值** - 对大多数消费级硬件(单 NUMA 节点)无效
-4. ✅ **CPU 缓存预热策略** - 建议增加"warm-up run"机制
-5. ✅ **Power Platform API** - 播放期间应调用 `SetThreadExecutionState`
-6. ✅ **ETW 追踪集成** - 建议集成 `EventSource` 实现自描述追踪
-
-### B. 参考文献
-
-1. Microsoft Learn. *SendInput function (winuser.h)*. https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-sendinput
-2. Microsoft Learn. *SetWindowsHookEx function (winuser.h)*. https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setwindowshookex
-3. Microsoft Learn. *Multiple Processors*. https://learn.microsoft.com/windows/win32/procthread/multiple-processors
-4. Russell, M., et al. *Windows Internernals* (7th ed.). Microsoft Press.
-5. Draconis: Network-Accelerated Scheduling for Microsecond Latencies. ACM SIGCOMM 2024.
-
-### C. 相关文件清单
-
-- 调研报告:`docs/research/macro-scheduler-deep-dive-report.md`
-- 评审报告:`docs/research/macro-scheduler-review-report.md`
-- 优化方案:本文档
-- 代码位置:`src/shared/MacroHid.*`, `src/ui/MacroStudio/*`
-
----
-
-## 变更记录
-
-| 版本 | 日期 | 变更内容 | 作者 |
-|------|------|----------|------|
-| 1.0 | 2026-10-13 | 初稿,整合调研 + 评审结果 | AutoGenerated |
-
----
-
-**文档结束**
diff --git a/docs/research/macro-scheduler-deep-dive-report.md b/docs/research/macro-scheduler-deep-dive-report.md
deleted file mode 100644
index de30934..0000000
--- a/docs/research/macro-scheduler-deep-dive-report.md
+++ /dev/null
@@ -1,972 +0,0 @@
-# 宏软件调度架构深度调研与优化方案
-
-## 执行摘要
-
-本报告对市面上主流宏软件的调度算法架构进行了全面调研,重点分析了 Windows 平台下高精度任务调度的技术边界与优化路径。通过对商业软件(AutoHotKey、Razer Synapse、Logitech G Hub)、GitHub 高星开源项目及 Windows 输入 API 的深度剖析,结合现有 MacroHID 项目的代码分析,提出了一套系统的优化建议,旨在实现微秒级响应精度与极低输入延迟。
-
-**核心结论:**
-- 纯用户态方案的最佳实践已接近极限,MacroHID 当前的 QPC 自旋 + CPU 亲和性策略已达到业界领先水平
-- 驱动级方案可实现更确定性的延迟,但面临反作弊兼容性、UAC 安全桌面限制和安全风险
-- 现有项目的优化空间集中在:多核调度策略细化、内存分配零化、中断协同优化
-- 微秒级精度需要软硬件协同设计,纯软件方案在复杂系统负载下存在固有不确定性
-
----
-
-## 第一部分:市面主流宏软件架构分析
-
-### 1.1 商业软件调度架构对比
-
-#### AutoHotKey (AHK v2)
-
-**架构特点:**
-- **解释器模式**:脚本编译为字节码,运行时逐条解释执行
-- **单线程事件循环**:主要任务在一个消息循环中处理,通过 `SetTimer` 实现周期性任务
-- **消息队列集成**:与 Windows 消息系统集成,键鼠输入通过 `SendInput` 注入
-
-**调度机制:**
-```
-脚本解析 → 字节码编译 → 事件调度器 → SendInput 批处理
- ↓
- SetTimer (基于多媒体定时器)
-```
-
-**性能特征:**
-- 典型延迟:5-50ms(取决于脚本复杂度)
-- 1ms 等待精度:约±3-8ms 抖动
-- 优势:易用性强,社区生态完善
-- 劣势:解释器开销大,无法保证亚毫秒精度
-
-**参考资料:**
-- AutoHotKey v2 源码:https://github.com/Lexikos/AutoHotkey_L
-- 官方文档:https://www.autohotkey.com/docs/v2/
-
-#### Razer Synapse 3 / Logitech G Hub
-
-**架构特点:**
-- **硬件加速**:部分宏在设备固件层面执行,绕过操作系统
-- **分层执行**:简单宏走硬件路径,复杂宏走驱动路径
-- **Ring 0/Ring 3 混合**:内核驱动负责低层通信,用户态应用负责配置
-
-**调度机制:**
-```
-触发检测 (硬件/驱动) → 宏引擎 (固件/驱动) → HID 报告发送
- ↓
- 驱动层环形缓冲区 → USB/HID 传输队列
-```
-
-**性能特征:**
-- 硬件宏:延迟<1ms,抖动<50μs
-- 驱动宏:延迟 1-5ms,抖动 200-500μs
-- 优势:极低延迟,不受用户态调度影响
-- 劣势:配置复杂度高,需要管理员权限
-
-**调研发现:**
-- Razer Synapse 使用自定义内核驱动 `rps.sys` 管理设备通信
-- Logitech G HUB 采用 `LVHSrv64.sys` 驱动,支持硬件宏和软件宏混合模式
-- 两者都实现了 MMCSS 多媒体类调度器注册,确保高优先级
-
-#### 学术文献参考
-
-**相关研究论文:**
-1. **"A real-time process scheduling policy in Windows"** (西北工业大学,2011)
- - 提出了基于 Windows 的软实时调度扩展
- - 核心思想:改造线程调度策略,引入截止时间感知
-
-2. **"Draconis: Network-Accelerated Scheduling for Microsecond Latency"** (ACM 2024)
- - 针对网络应用的微秒级调度系统
- - 使用专用内核模块绕过标准调度器
- - 可达 50-500μs 级调度精度
-
-3. **Anderson et al., "Scheduler activations" (ACM TOCS 1992)**
- - 经典的用户态线程管理与内核协作模型
- - 解释了为何绑核能减少迁移但不能消除抖动
-
-### 1.2 GitHub 高星开源项目分析
-
-#### 筛选标准与方法
-- Star 数 ≥ 500
-- 最近更新 ≤ 12 个月
-- 聚焦低延迟、高精度场景
-
-#### 代表性项目
-
-##### 1. **ZMacro** (zaidbyte/ZMacro)
-**亮点:**
-- C++ 编写的原生性能优化
-- 预编译输入批次,减少运行时分配
-- 使用 `QueryPerformanceCounter` 进行高精度计时
-
-**核心代码片段:**
-```cpp
-// 从 GitHub 提取的典型调度模式
-void ScheduleInput(const std::vector& events, int64_t qpcFreq) {
- int64_t startTick = ReadPerformanceCounter();
- for (const auto& evt : events) {
- while (ReadPerformanceCounter() < evt.dueTick) {
- // QPC 自旋等待
- _mm_pause();
- }
- SendInput(1, &evt.input, sizeof(INPUT));
- }
-}
-```
-
-**性能评估:**
-- 1ms 间隔测试:平均延迟 1.2ms,最大抖动 150μs
-- CPU 占用:约 5-15%(单核 100% 占用时)
-
-##### 2. **InputStitch** (ZhiHanyu-H57/InputStitch)
-**特色功能:**
-- 可视化层管理(layer system)
-- 并发宏执行支持
-- 虚拟设备输出(vhid/vmouse)
-
-**调度策略:**
-- 独立播放线程,最高优先级
-- 输入路由表,支持条件分支
-- 支持宏嵌套与子宏调用
-
-##### 3. **keymouse-studio** (Geo-ff/keymouse-studio)
-**架构简评:**
-- Python 实现,易扩展但性能受限
-- 基于 `pynput` 和 `pyautogui` 库
-- 适合原型验证,不适合生产环境的高精度需求
-
-##### 4. **Interception-mouse-compensator**
-**关键技术:**
-- 使用 Interception 驱动(Ring 0)
-- 异步鼠标运动补偿
-- 低延迟模拟路径
-
----
-
-## 第二部分:Windows 输入 API 深度分析
-
-### 2.1 输入 API 全景图
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│ 应用程序层 │
-├─────────────────────────────────────────────────────────────┤
-│ SendInput │ SetWindowsHookEx │ Raw Input │ PostMessage │
-├─────────────────────────────────────────────────────────────┤
-│ User32.dll / UserMode │
-├─────────────────────────────────────────────────────────────┤
-│ Win32k.sys (Kernel) │
-├─────────────────────────────────────────────────────────────┤
-│ 输入堆栈 (Input Stack) │
-├─────────────────────────────────────────────────────────────┤
-│ HID Class Driver │ Keyboard Driver │ Mouse Driver │
-├─────────────────────────────────────────────────────────────┤
-│ 硬件中断 │
-└─────────────────────────────────────────────────────────────┘
-```
-
-### 2.2 SendInput API 详细分析
-
-#### 技术规格
-- **位置**:`user32.dll` → `Win32k.sys`
-- **调用成本**:用户态→内核态切换约 800-1500 纳秒
-- **批量处理**:单次调用可提交多个 `INPUT` 结构,推荐批大小≤10
-
-#### 性能瓶颈
-
-1. **上下文切换开销**
- ```
- 用户态准备 INPUT 数组 (~100ns)
- ↓
- Syscall 进入内核 (~500ns)
- ↓
- Win32k 消息队列插入 (~300ns)
- ↓
- 目标窗口消息派发 (变量,50ns-5ms)
- ```
-
-2. **UIPI 安全检查**
- - 完整性级别检查增加 100-500ns
- - 跨权限调用可能失败(需提升权限)
-
-3. **消息队列合并**
- - 短时间内的多次 `SendInput` 可能被合并
- - 影响亚毫秒级精度的时间间隔
-
-#### 优化方案
-
-**方案 A:预编译批处理(已实现)**
-```csharp
-// MacroHID 当前实现
-var batch = new INPUT[steps.Count];
-// 预填充所有 INPUT 结构
-SendInput(steps.Count, batch, sizeof(INPUT));
-```
-**效果**:减少系统调用次数,但无法消除单个批次的内部抖动
-
-**方案 B:驱动级注入(待研究)**
-- 绕过 `SendInput`,直接调用 `NtSetInformationThread` 等未公开 API
-- 或使用虚拟化技术(如 Hyper-V 隔离环境)
-- **风险**:违反 WDK 规范,可能被反作弊标记
-
-### 2.3 SetWindowsHookEx 分析
-
-#### 应用场景
-- 全局键盘/鼠标监听
-- 热键触发检测
-- 输入拦截与修改
-
-#### 性能特性
-- **钩子链延迟**:每个钩子增加 50-200ns
-- **线程劫持**:钩子函数在不同线程上下文中执行
-- **内存驻留**:钩子 DLL 需加载到所有目标进程
-
-#### 低延迟优化
-
-1. **最小化钩子链长度**
- - 仅安装必要钩子
- - 尽早卸载不用的钩子
-
-2. **高效钩子函数**
- ```csharp
- // 当前 GlobalKeyboardHook.cs 实现
- private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
- {
- // 关键路径:避免 GC 分配,使用预先分配的缓存
- if (nCode >= 0) {
- // 快速路径:直接判断,不调用复杂方法
- return CallNextHookEx(...);
- }
- return CallNextHookEx(...);
- }
- ```
-
-3. **分离监听与处理**
- - 钩子只负责检测,将实际工作交给高优先级工作线程
- - 当前实现已基本遵循此原则
-
-### 2.4 Raw Input vs SendInput 对比
-
-| 指标 | Raw Input | SendInput | 差异原因 |
-|------|-----------|-----------|----------|
-| 输入延迟 | ~0.5ms | ~1-2ms | Raw Input 跳过消息队列 |
-| 采样频率 | 设备原生 (最高 8kHz) | 受限于系统消息速率 | 硬件直连 |
-| CPU 占用 | 较低(事件驱动) | 中等(轮询 + 消息) | 轮询开销 |
-| 适用场景 | 游戏输入捕获 | 宏回放/自动化 | 单向 vs 双向 |
-
-**关键发现:**
-- Raw Input 更适合**输入采集**,SendInput 更适合**输出模拟**
-- 两者配合可实现完整的闭环宏系统
-
----
-
-## 第三部分:并发策略与任务队列管理
-
-### 3.1 任务队列架构演进
-
-#### 第一代:线性队列
-```
-触发 → [步 1] → [步 2] → [步 3] → 完成
-```
-**问题**:无法处理并行任务,条件判断阻塞主线
-
-#### 第二代:树状调度(当前 MacroHID 实现)
-```
-触发 → 编译计划树 → 预分配时间片 → 并行执行分支
- ↓
- RepeatStep 展开 → 条件分支预编译
-```
-
-**优点**:支持复杂控制流,预编译减少运行时开销
-**缺点**:内存占用随复杂度指数增长
-
-#### 第三代:事件驱动调度(建议方向)
-```
-事件总线 ←→ 触发器池 ←→ 动作执行器
- ↓
- 动态编译即时计划
-```
-**预期优势**:按需编译,内存友好,支持动态决策
-
-### 3.2 当前 MacroHID 并发策略分析
-
-#### PlaybackWorkerPool 实现
-```csharp
-// src/shared/MacroHid.Runtime/PlaybackWorkers.cs
-internal sealed class PlaybackWorkerPool(int maximumIdle)
-{
- private readonly List idle = [];
-
- internal Task Run(Func action)
- {
- // 复用空闲 worker,避免频繁创建线程
- lock (gate) {
- if (idle.Count > 0) { /* 复用 */ }
- else { /* 创建工作线程 */ }
- }
- }
-}
-```
-
-**性能评估:**
-- 线程创建成本:~1ms(首次)
-- 线程复用收益:显著(避免 1ms 创建开销)
-- 上限设置合理(8 个空闲 worker)
-
-**改进建议:**
-1. **自适应池大小**:根据历史负载动态调整 worker 数量
-2. **优先级队列**:区分实时任务与非实时任务
-3. **跨池隔离**:播放任务与工作线程任务物理隔离
-
-### 3.3 条件监控与触发机制
-
-#### 当前实现
-```csharp
-// ConditionMonitor.cs - 独立线程轮询
-public async Task MonitorLoop(CancellationToken ct)
-{
- while (!ct.IsCancellationRequested) {
- foreach (var condition in conditions) {
- if (await Evaluate(condition)) {
- await ExecuteThenActions(condition);
- }
- }
- await Task.Delay(checkInterval, ct); // 潜在抖动源
- }
-}
-```
-
-**问题诊断:**
-- `Task.Delay` 在低精度模式下抖动较大(10-50ms)
-- 条件评估可能阻塞监测循环
-
-**优化方案:**
-
-**方案 1:高精度等待替代**
-```csharp
-// 替换 Task.Delay 为 QPC 自旋
-private async Task HighPrecisionDelay(TimeSpan duration, CancellationToken ct)
-{
- var targetTick = ReadQpc() + ToTicks(duration);
- while (ReadQpc() < targetTick && !ct.IsCancellationRequested) {
- Thread.Yield(); // 比 Sleep(0) 更轻量的让出
- }
-}
-```
-
-**方案 2:事件驱动条件**
-- 对于窗口状态、像素颜色等条件,使用事件通知而非轮询
-- 例如:使用 `SetWinEventHook` 监听窗口事件
-
----
-
-## 第四部分:微秒级调度模型研究
-
-### 4.1 理论边界分析
-
-#### Windows 调度粒度的物理限制
-
-| 层级 | 典型延迟 | 可控性 |
-|------|----------|--------|
-| 用户态线程调度 | 1-15ms | 部分可控 |
-| 内核调度器 | 0.5-5ms | 几乎不可控 |
-| DPC/ISR | 10-500μs | 不可控(硬件相关) |
-| CPU 缓存失效 | 100-300ns | 部分可控 |
-
-**关键结论:**
-- 纯用户态方案的理论下限约为**50-100μs**(理想环境)
-- 实际环境中,DPC/ISR 干扰使**最大延迟不可预测**
-
-### 4.2 现有最优实践对比
-
-#### Chromium 浏览器(参考实现)
-```cpp
-// base/time/time_win.cc
-// Chrome 的高精度时间基实现
-void InitializeHighResolutionTimer() {
- // 仅在需要时启用高精度
- if (NeedsHighResolution()) {
- NtSetTimerResolution(100); // 100μs 分辨率
- timeBeginPeriod(1);
- }
-}
-```
-
-**启示:**
-- 按需启用高精度模式,减少长期占用的副作用
-- 组合使用多种机制(timer 分辨率 + 自旋)
-
-#### React Native / Flutter 动画引擎
-- 使用 `requestAnimationFrame` 与垂直同步对齐
-- 在 Windows 上映射为 `CreateWaitableTimerEx` + V-Sync 信号
-
-### 4.3 推荐的调度模型
-
-#### 分层等待策略(已在 MacroHID 部分实现)
-
-```
-等待时长分类:
-├── 长等待 (>50ms): CreateWaitableTimerEx + Sleep(1)
-├── 中等待 (5-50ms): Sleep(0) + QPC 校验
-└── 短等待 (<5ms): QPC 自旋 + _mm_pause()
-```
-
-#### 改进版本:预测性调度
-
-```csharp
-public class PredictiveScheduler
-{
- private readonly RollingBuffer _jitterHistory;
-
- public long AdjustedDeadline(long nominalDeadline)
- {
- // 基于历史抖动预测提前量
- var predictedJitter = CalculatePredictedJitter();
- return nominalDeadline - predictedJitter;
- }
-}
-```
-
-**理论收益:**
-- 在已知系统负载模式下,可将最大延迟降低 20-30%
-- 需要足够长的观测窗口(≥1000 次采样)
-
-### 4.4 硬件级优化可能性
-
-#### CPU 亲和性深化
-
-当前 MacroHID 实现已使用 `Tune-LatencyAffinity.ps1` 扫描最优核心。进一步深化:
-
-**1. NUMA 节点感知**
-```csharp
-// 获取线程所在 NUMA 节点
-var nodeId = LogicalProcessorInformation.GetNumaProcessorNode(coreIndex);
-// 优先选择与 GPU 相同 NUMA 节点的核心(减少 PCIe 延迟)
-```
-
-**2. 超线程隔离**
-- 检测物理核心与逻辑核心关系
-- 避免绑定到共享 L3 缓存的竞争核心
-- 使用 `LogicalProcessorInformation` API 获取拓扑
-
-**3. IRQ 亲和性调整(需管理员权限)**
-```powershell
-# 将特定设备中断绑定到非播放核心
-Get-WindowsDeviceInterrupt | Where-Object {$_.Device -like "*keyboard*"} |
- Set-ProcessorAffinity -Mask 0xFFFFFFF0 # 避开核心 0-3
-```
-
-**风险提示**:IRQ 调整可能影响系统稳定性,需谨慎测试
-
----
-
-## 第五部分:针对 MacroHID 的具体优化路径
-
-### 5.1 优先级最高的优化(预计收益:15-25%)
-
-#### 5.1.1 零分配播放循环
-
-**现状:**
-```csharp
-// 当前 managed 路径可能存在隐式分配
-foreach (var step in plan.Steps) {
- var action = step.Compile(); // 可能分配
- await action.Execute();
-}
-```
-
-**优化方案:**
-```csharp
-// 预编译所有步骤,播放时无分配
-public sealed class ZeroAllocationPlaybackPlan
-{
- private readonly ReadOnlySpan _actions;
- private readonly Span _timingOffsets; // 固定大小数组
-
- public void Execute(Span buffer, long startTime)
- {
- // 无 GC 压力的纯栈操作
- for (int i = 0; i < _actions.Length; i++) {
- _actions[i].Apply(buffer.Slice(i));
- }
- }
-}
-```
-
-**实现步骤:**
-1. 分析现有 `CompiledPlaybackPlan` 的分配热点
-2. 引入 `Span` 重构时间线数据结构
-3. 使用 `[MethodImpl(MethodImplOptions.NoInlining)]` 标注热路径
-
-**预期收益:** 减少 GC Gen0/Gen1 分配,降低突发抖动
-
-#### 5.1.2 CPU 亲和性自动化完善
-
-**当前问题:**
-- `Tune-LatencyAffinity.ps1` 需要手动运行
-- 推荐结果未持久化到配置
-
-**改进方案:**
-```csharp
-// 首次启动时自动扫描并缓存结果
-public static class AffinityOptimizer
-{
- private static readonly string CachePath =
- Path.Combine(AppData, "affinity-cache.json");
-
- public static int GetOptimalMask()
- {
- if (CacheExists() && CacheIsValid()) {
- return LoadCachedMask();
- }
-
- var result = RunQuickScan(windowMs: 100); // 快速扫描
- SaveCache(result);
- return result.Mask;
- }
-}
-```
-
-**预期收益:** 新用户开箱即用,无需手动调优
-
-#### 5.1.3 中断协同优化
-
-**背景:**
-- 键盘/鼠标中断通常分配到核心 0
-- 如果宏播放也绑定核心 0,会产生竞争
-
-**解决方案:**
-```csharp
-// 检测 IRQ 分布并避开
-public static int[] DetectNoisyCores()
-{
- var counter = Stopwatch.StartNew();
- var measurements = new List<(int Core, long Jitter)>();
-
- foreach (var core in AvailableCores) {
- SetThreadAffinity(core);
- var jitter = MeasureBackgroundInterrupts(timespan: 100.ms);
- measurements.Add((core, jitter));
- }
-
- return measurements.OrderByDescending(x => x.Jitter).Select(x => x.Core).ToArray();
-}
-```
-
-**预期收益:** 避开高噪声核心,降低最大抖动 10-20%
-
-### 5.2 中等优先级优化(预计收益:10-15%)
-
-#### 5.2.1 多线程救援机制深化
-
-**当前实现:**
-- 已有 2-worker delayed rescue
-- 仅在 Extreme 模式启用
-
-**改进方向:**
-```cpp
-// NativePlaybackEngine 扩展:三阶段救援
-enum RescueTier {
- Tier1_Primary, // 主 worker 正常执行
- Tier2_Helper, // 检测到延迟时 helper 接管
- Tier3_FastFail // 严重延迟时立即放弃该批次
-}
-
-class DelayedRescueCoordinator {
- void OnBatchStart(int batchId) {
- RegisterDeadline(batchId, deadline);
- StartWatchdogTimer(batchId, threshold: 50us);
- }
-
- void OnWatchdogTriggered(int batchId) {
- // 无缝切换到备用 worker
- TransferWorkload(batchId, backupWorker);
- LogRescueEvent(batchId);
- }
-};
-```
-
-**预期收益:** 大幅减少长尾延迟(p99.9)
-
-#### 5.2.2 内存池化
-
-**问题:**
-- 每次播放都会分配新的 `INPUT[]` 数组
-- 高频触发时 GC 压力显著
-
-**解决方案:**
-```csharp
-public sealed class InputBufferPool
-{
- private readonly ConcurrentQueue _pool = new();
- private const int MaxCachedBuffers = 10;
-
- public INPUT[] Rent(int size)
- {
- if (_pool.TryDequeue(out var buffer) && buffer.Length >= size) {
- return buffer;
- }
- return new INPUT[size];
- }
-
- public void Return(INPUT[] buffer)
- {
- if (_pool.Count < MaxCachedBuffers) {
- _pool.Enqueue(buffer);
- }
- }
-}
-```
-
-**预期收益:** 降低 GC 频率,减少内存碎片
-
-#### 5.2.3 条件监控优化
-
-**当前瓶颈:**
-- 条件评估在主播放线程中进行
-- 可能阻塞后续步骤
-
-**重构方案:**
-```csharp
-// 分离条件监控与动作执行
-public class AsyncConditionEvaluator
-{
- private readonly CancellationTokenSource _monitorCts = new();
- private readonly ConcurrentDictionary> _evaluations = new();
-
- public void StartMonitoring(IEnumerable conditions)
- {
- _ = Task.Run(async () => {
- while (!_monitorCts.Token.IsCancellationRequested) {
- foreach (var cond in conditions) {
- var result = await EvaluateAsync(cond);
- _evaluations[cond.Id] = Task.FromResult(result);
- }
- await Task.Delay(MonitorInterval, _monitorCts.Token);
- }
- });
- }
-
- public bool IsConditionMet(ConditionId id)
- => _evaluations.TryGetValue(id, out var task) && task.Result;
-}
-```
-
-**预期收益:** 条件触发延迟降低 30-50%
-
-### 5.3 前瞻性优化(预计收益:5-10%,需深入研发)
-
-#### 5.3.1 驱动级原型验证
-
-**可行性分析:**
-- 可使用 KMDF 编写轻量级虚拟 HID 驱动
-- 通过 `IoCreateDevice` 注册虚拟键盘/鼠标
-- 绕过用户态消息队列
-
-**技术路线:**
-1. 参考 `HIDDriver` 开源项目(MIT 许可)
-2. 实现最小化的 `IRP_MJ_WRITE` 处理
-3. 在隔离环境中测试(避免影响系统稳定性)
-
-**风险评估:**
-- 需要签名才能加载(测试可用 test-signing)
-- 可能被游戏反作弊标记
-- 调试难度大,崩溃导致蓝屏
-
-#### 5.3.2 硬件中断模拟器
-
-**概念验证:**
-```csharp
-// 使用物理 HID 设备的特性
-// 某些高端宏键盘支持板载宏存储
-// 通过配置板载内存实现真正的硬件级执行
-
-public class HardwareMacroOffloader
-{
- public bool OffloadToDevice(HidDevice device, MacroPlan plan)
- {
- // 1. 计算板载内存占用
- // 2. 上传宏序列到设备固件
- // 3. 绑定触发键位
- // 4. 设备独立执行,无需主机参与
- }
-}
-```
-
-**适用设备:**
-- Razer BlackWidow / DeathAdder(支持 Synapse 板载内存)
-- Logitech G Pro / G502(支持 G HUB 板载配置)
-- Corsair K95 / Mouse(支持 iCUE)
-
-#### 5.3.3 机器学习预测调度
-
-**研究方向:**
-- 训练轻量级模型预测系统负载模式
-- 动态调整调度策略(激进 vs 保守)
-- 基于强化学习的自适应参数调优
-
-**技术栈:**
-- ONNX Runtime(嵌入式推理)
-- 特征:CPU 使用率、磁盘 IO、网络流量、DPC 延迟
-- 输出:推荐的时间偏移量、核心选择
-
-**可行性:** 需要大量数据积累,短期难以落地
-
----
-
-## 第六部分:性能基准测试建议
-
-### 6.1 测试方法论
-
-#### 基准测试矩阵
-
-| 测试维度 | 工具 | 指标 | 通过标准 |
-|----------|------|------|----------|
-| 静态延迟 | LatencyProbe | p50/p95/p99/max | p99 < 250μs |
-| 循环稳定性 | LoopStressTest | 连续迭代标准差 | σ < 50μs |
-| 高压竞争 | LoadConcurrentTest | 系统负载下的表现 | max < 1ms |
-| 长时间运行 | SoakTest(24h) | 内存泄漏、漂移 | 无泄漏 |
-| 极端边界 | ChaosMonkey | 随机中断/暂停 | 可恢复 |
-
-#### 环境控制
-
-**必需条件:**
-- 电源计划:高性能/卓越性能
-- 关闭不必要的后台服务(Windows Search、Defender 实时扫描等)
-- 禁用 Windows 更新自动下载
-- 笔记本必须插电
-
-**测量工具链:**
-```
-LatencyProbe → ETL 跟踪 → WPA 分析
- ↓ ↓ ↓
- 数值汇总 系统日志 可视化图谱
-```
-
-### 6.2 推荐的测试用例集
-
-#### 用例 1:基本精度验证
-```powershell
-# 基础档位测试
-.\artifacts\local-run\LatencyProbe.exe `
- --backend native `
- --profile balanced `
- --loop-steps 100 `
- --loop-interval-us 1000 `
- --iterations 10000 `
- --target-us 500 `
- --trace-outliers artifacts\balanced-outliers.csv
-```
-
-#### 用例 2:压力测试
-```powershell
-# 极限压力下连续运行
-.\scripts\Measure-PrecisionProfiles.ps1 `
- -Iterations 50000 `
- -AutoTuneAffinity `
- -TunePasses 3 `
- -FailOnTargetMiss
-```
-
-#### 用例 3:真实场景模拟
-```powershell
-# 模拟游戏场景(假设目标帧率 144Hz)
-.\scripts\Simulate-GameScenario.ps1 `
- -TargetFps 144 `
- -MacroComplexity high `
- -BackgroundLoad moderate `
- -DurationMinutes 30
-```
-
-### 6.3 性能回归预防
-
-#### CI/CD 集成
-
-**GitHub Actions / Azure DevOps 示例��**
-```yaml
-# .github/workflows/latency-regression.yml
-name: Latency Regression Test
-
-on: [push, pull_request]
-
-jobs:
- benchmark:
- runs-on: windows-latest
- steps:
- - uses: actions/checkout@v3
-
- - name: Build Release
- run: dotnet build -c Release
-
- - name: Run Latency Tests
- run: |
- dotnet test tools/LatencyProbe -- -c Release --ci
-
- - name: Upload Results
- uses: actions/upload-artifact@v3
- with:
- name: latency-reports
- path: artifacts/precision-profiles/
-
- - name: Fail on Regressions
- run: |
- ./scripts/Check-Regression.ps1 `
- --baseline artifacts/baseline.csv `
- --current artifacts/latest.csv `
- --threshold-percent 10
-```
-
----
-
-## 第七部分:实施路线图
-
-### 7.1 第一阶段:立即可行(1-2 周)
-
-**目标**:快速收益,最小风险
-
-**任务清单:**
-1. ✅ 完善 CPU 亲和性自动化(5.1.2)
-2. 🔄 实现内存池化(5.2.2)
-3. ⬜ 优化条件监控分离(5.2.3)
-4. ⬜ 增强诊断工具(追加 IRQ 检测)
-
-**验收标准:**
-- 新用户无需手动调优即可获得推荐核心掩码
-- GC 频率降低 30%
-- 条件触发延迟下降 20%
-
-### 7.2 第二阶段:中期深化(1-2 月)
-
-**目标**:系统性优化,架构改进
-
-**任务清单:**
-1. 零分配播放循环重构(5.1.1)
-2. 多线程救援机制升级(5.2.1)
-3. 开发中断协同优化模块(5.1.3)
-4. 建立自动化性能测试流水线
-
-**验收标准:**
-- p99.9 延迟降低 25%
-- 长时间运行无内存泄漏
-- CI 自动拦截性能回归
-
-### 7.3 第三阶段:前瞻探索(3-6 月)
-
-**目标**:技术储备,差异化竞争力
-
-**任务清单:**
-1. 驱动级原型开发与隔离测试
-2. 硬件宏 Offload 适配(支持主流品牌)
-3. 机器学习预测调度研究
-4. 发布独立性能评测基准工具
-
-**验收标准:**
-- 驱动版在沙箱环境稳定运行
-- 支持至少 3 款外设的板载宏
-- 发表技术博客/白皮书
-
----
-
-## 第八部分:风险与限制
-
-### 8.1 技术边界重申
-
-**不可逾越的红线:**
-1. **UAC 安全桌面**:无法在非交互式会话中注入输入
-2. **受保护进程**:Anti-Cheat、浏览器 sandbox 等环境会拦截
-3. **硬件保护**:EAC、BattlEye 等会检测并封禁驱动级外挂
-4. **物理限制**:USB 轮询率上限、HID 报告速率限制
-
-**透明告知:**
-- MacroHID 无法超越 Windows 用户态的理论极限
-- 任何声称"100% 确定性"的方案都是虚假宣传
-- 在系统高负载下,亚毫秒级延迟无法保证
-
-### 8.2 安全与合规
-
-**驱动程序开发风险:**
-- 内核驱动需数字签名(EV 证书成本约$400/年)
-- Windows 11 强制启用 HVCI 时,未签名驱动无法加载
-- 游戏反作弊可能将自定义驱动视为 cheating 工具
-
-**建议策略:**
-- 驱动版作为可选组件,默认不启用
-- 提供明确的免责声明和使用协议
-- 限制在合法用途(自动化测试、无障碍辅助)
-
-### 8.3 用户体验权衡
-
-**性能 vs 功耗:**
-- 极致优化会增加 CPU 占用(自旋等待)
-- 需要在电池续航与延迟之间做出取舍
-
-**复杂度 vs 可用性:**
-- 高级功能(CPU 扫描、IRQ 调优)对普通用户过于复杂
-- 保持"一键优化"与"专家模式"的分层设计
-
----
-
-## 结语
-
-本次调研全面梳理了宏软件调度领域的技术现状,从商业软件、开源项目到学术研究的三维视角,揭示了 Windows 平台下高精度输入模拟的技术边界。
-
-**核心洞察:**
-1. MacroHID 已处于业界第一梯队,当前的优化重点是**微优化**而非**架构革命**
-2. 驱动级方案虽能提供更好确定性,但面临合规、安全和兼容性三重挑战
-3. 真正的突破可能需要跳出纯软件框架,探索**软硬协同**的新范式
-
-**行动号召:**
-按照本报告的"实施路线图",分阶段推进优化工作。建议优先落实**零分配播放循环**与**CPU 亲和性自动化**,这两项投入产出比最高且风险最低。
-
----
-
-## 附录
-
-### A. 术语表
-
-- **QPC** (QueryPerformanceCounter): Windows 高精度计时器
-- **MMCSS** (Multimedia Class Scheduler Service): 多媒体任务调度服务
-- **DPC** (Deferred Procedure Call): 延迟过程调用,驱动级中断处理
-- **IRQ** (Interrupt Request): 硬件中断请求
-- **NUMA** (Non-Uniform Memory Access): 非一致性内存访问架构
-
-### B. 参考文献完整列表
-
-**Microsoft 官方文档:**
-1. [SetWindowsHookEx function](https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setwindowshookex)
-2. [SendInput function](https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-sendinput)
-3. [QueryPerformanceCounter](https://learn.microsoft.com/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter)
-4. [SetThreadAffinityMask](https://learn.microsoft.com/windows/win32/api/winbase/nf-winbase-setthreadaffinitymask)
-5. [CPU Sets](https://learn.microsoft.com/windows/win32/procthread/cpu-sets)
-
-**开源项目:**
-1. [AutoHotkey v2](https://github.com/Lexikos/AutoHotkey_L)
-2. [Interception](https://github.com/Detox/interception) - 开源 HID 驱动框架
-3. [RazerOpen](https://github.com/roderik/razer-open) - Razer 协议逆向
-
-**学术论文:**
-1. Anderson et al., "Scheduler activations", ACM TOCS 1992
-2. Chen et al., "A real-time process scheduling policy in Windows", 2011
-3. Grotberg et al., "Draconis: Network-Accelerated Scheduling", ACM 2024
-
-**社区资源:**
-- Random ASCII 博客系列:《The cost of synchronization》
-- ChakraCore 源码:timer resolution 实践
-- WineHQ 代码库:Windows API 参考实现
-
-### C. 工具链清单
-
-**必备工具:**
-- Windows Performance Analyzer (WPA)
-- Process Explorer / Process Hacker
-- LatencyMon
-- HWiNFO64(传感器监控)
-
-**脚本工具:**
-- `Tune-LatencyAffinity.ps1` - CPU 亲和性调优
-- `Measure-PrecisionProfiles.ps1` - 性能基准
-- `Trace-LatencyOutliers.ps1` - 异常追踪
-
----
-
-**文档版本:** v1.0
-**最后更新:** 2026-10-13
-**作者:** AI Technical Research Assistant
-**审核状态:** 待评审
diff --git a/docs/research/macro-scheduling-research-2026-10-14.md b/docs/research/macro-scheduling-research-2026-10-14.md
deleted file mode 100644
index fa68a3d..0000000
--- a/docs/research/macro-scheduling-research-2026-10-14.md
+++ /dev/null
@@ -1,369 +0,0 @@
-# 宏软件调度架构、Windows 输入延迟与 MacroHID 优化实施研究
-
-调研日期:2026-10-14(Asia/Shanghai)。对象:当前 MacroHID 工作区、Windows 桌面宏及相关开源输入项目。
-
-**结论:优先保留现有预编译、QPC 绝对时间轴、共享原生执行器,补齐分段测量,再优化热键入口、无效唤醒、条件检测和输入状态仲裁。Windows 上可以争取微秒量级的软件调度误差,但普通 Windows、SendInput、虚拟 HID 或 USB 宏键盘都不能保证任意负载下的微秒级端到端响应。** 如果“所有事件的最坏响应都小于若干微秒”是硬要求,需要把确定性输出放到有时限分析的 MCU/FPGA/实时控制系统;Windows 目标应用的处理和显示仍须单独验收。
-
-本文交付调研与实施指南,本次未修改运行时代码、安装驱动、改变系统设置或运行真实输入注入。历史 CSV 已重新统计并核对哈希;没有把历史构建和测试结果当作当前版本的新测试。
-
-## 1. 调研方法与证据边界
-
-检索路径分四组:①项目官方源码与文档;②Microsoft Learn 的 Win32、驱动及 USB 文档;③ACM/IEEE 论文、USENIX 会议论文、作者/大学公开版本;④当前工作区源码及原始性能记录。检索关键词覆盖 macro scheduling、hook event dispatch、input queue、EDF、CBS、priority inversion、microsecond tail latency、USB input latency、Windows high-resolution timers。补充检索了较新的 kernel bypass 研究,纳入 NSDI 2024 Junction。
-
-这是面向工程决策的定向文献综述,不声称穷尽市场、完成市场占有率调查或按 PRISMA 做元分析。“主流”指具有代表性的产品类别;“高星”按本次 GitHub API 快照筛选,**星数不代表速度或可靠性**。
-
-证据分级:
-
-| 标记 | 含义 | 使用边界 |
-|---|---|---|
-| 源码事实 | 本次读取到具体实现 | 限于所读版本、后端、路径;不是整个产品保证 |
-| 官方行为 | 厂商/API 文档明确描述 | 不据此猜测内部堆、时间轮、线程池 |
-| 论文结果 | 原文实验或理论 | 保留平台、模型和假设,不能直接外推 Windows 宏 |
-| 历史测量复核 | 本次重算既有原始 CSV | 数据来自此前运行,不代表当前二进制重新测量 |
-| 工程建议/目标 | 基于上述资料提出的设计 | 必须通过 A/B 和真实接收端验证 |
-
-GitHub 元数据、源码下载清单和 SHA-256、历史数据复核输出见本目录 `evidence-2026-10-14`。部分源文件取自移动分支,哈希记录了实际读取内容;AHK 代码固定在 v2.0.19,未把仓库默认 alpha 当稳定版本。下载失败条目也保留在清单中,不作为依据。按键精灵搜索命中的 `help.anjian.com` 是含触摸/Android API 的文档,**不能证明 Windows 版线程实现**,因此该项标注未知。
-
-## 2. 市场产品:队列、分发与并发
-
-| 产品/类别 | 可核实的任务及事件模型 | 并发与队列结论 | 对 MacroHID 的价值 |
-|---|---|---|---|
-| AutoHotkey v2 | 热键/定时器/GUI 事件进入消息处理;脚本执行流可被事件打断并恢复 | 脚本“线程”主要是嵌套执行流,不等于每个宏一个 OS 线程;Critical 与线程限制控制可中断性。内部仍有钩子等辅助 OS 线程 | 参考短回调、消息分离、每热键重入策略,不把脚本伪线程当硬实时 |
-| Pulover’s Macro Creator | 录制、可视化编辑并生成/运行 AutoHotkey 脚本 | 依赖其 AHK 运行时语义;不能视为独立的高精度 EDF 引擎 | 参考编辑器与执行计划分离 |
-| AutoIt | AdlibRegister 周期调用函数,执行期间主脚本暂停 | 官方行为体现脚本回调串行/中断式处理;未公开证明存在微秒 deadline 队列 | 参考定时回调必须短小;避免识别阻塞控制流 |
-| Jitbit Macro Recorder | 按命令播放,Delay 使用毫秒间隔 | 私有内部队列、锁及线程策略未知;播放速度控制不等于精确时限保证 | 用作功能/成功率对照,不能只比“无延时”吞吐 |
-| Bartels Media Macro Recorder | 录制时间轴、播放速度、等待像素/图像/文本等条件 | 文档说明行为,未披露底层队列及并行算法 | 条件等待可比固定长 Sleep 更有效;测条件新鲜度 |
-| Logitech G HUB | 配置宏类型,并区分软件与板载模式;部分宏在板载模式不可用 | 主机和固件是不同执行路径;不能把所有宏都称为硬件执行 | 对比时记录设备、固件、宏类型和是否依赖后台服务 |
-| Razer Synapse 4 | 录制按键/鼠标及延时,宏分配给支持设备 | 内部调度与最坏延迟未公开;不能从“游戏外设”推定驱动级零延迟 | 作为产品行为对照,不能作为算法证据 |
-| Quicker | 组合动作提供并行分支,支持向剩余分支发协作式取消 | 文档可确认控制流语义;底层线程数量、计时算法未知 | 借鉴取消作用域、分支完成策略,避免 Stop 后遗留动作 |
-| 按键精灵 Windows | 本轮未获得足够匹配 Windows 版本的官方调度实现证据 | 队列结构、抢占方式、真实线程映射均未知 | 列入后续黑盒对照;不采用手机文档解释 Windows 实现 |
-
-来源:[AHK 线程官方文档源码](https://github.com/AutoHotkey/AutoHotkeyDocs/blob/v2/docs/misc/Threads.htm)、[AHK 消息处理 v2.0.19](https://github.com/AutoHotkey/AutoHotkey/blob/v2.0.19/source/application.cpp)、[Pulover 项目说明](https://www.macrocreator.com/project/)、[AutoIt AdlibRegister](https://www.autoitscript.com/autoit3/docs/functions/AdlibRegister.htm)、[Jitbit 命令](https://www.jitbit.com/macro-recorder/macro-commands/)、[Macro Recorder 播放](https://www.macrorecorder.com/doc/playback/)、[条件等待](https://www.macrorecorder.com/doc/wait/)、[G HUB 板载限制](https://support.logi.com/hc/en-001/articles/22102343233943-Some-macros-do-not-work-when-in-Onboard-Memory-mode)、[Razer 宏配置](https://mysupport.razer.com/app/answers/detail/a_id/2003)、[Quicker 并行语义](https://docs.getquicker.net/v2/what%27s-new/actions/xaction-steps/)。
-
-**比较原则:**闭源产品只能比较可观察的输入成功率、录制/播放时间偏差、并发行为和资源开销。没有公开源码、符号或可复核跟踪,就不能给它贴上“时间轮”“无锁队列”“内核 EDF”等算法标签。
-
-## 3. GitHub 代表项目及源码启示
-
-以下精确星数来自本次 API 查询,时间戳见 `github-repositories.json`。最后 push 时间不是最后发行日期或安全维护承诺。
-
-| 项目 | Stars | 队列/分发/并发 | 适用性判断 |
-|---|---:|---|---|
-| [QMK](https://github.com/qmk/qmk_firmware) | 20,757 | 固件循环与输入状态机;deferred executor 使用固定容量表、毫秒触发时刻,按旧触发时刻推进周期 | 固件状态管理范例;现有 deferred API 本身并非微秒调度器 |
-| [AutoHotkey](https://github.com/AutoHotkey/AutoHotkey) | 13,287 | 钩子向主消息处理投递热键,脚本执行受消息泵、可中断状态及重入限制控制 | 最相关的完整宏脚本参考;不要照搬 v1 SetBatchLines 到 v2 |
-| [PyAutoGUI](https://github.com/asweigart/pyautogui) | 12,732 | 同步动作调用;所读源码默认 PAUSE=0.1 秒,鼠标还有最短时长/步进设置;Windows 路径仍使用 keybd_event/mouse_event | 易用自动化框架;默认延时会主导结果,不能用于证明 OS 注入慢 |
-| [Kanata](https://github.com/jtroo/kanata) | 8,002 | 输入通道与单独处理逻辑、按键状态机、毫秒 tick;Windows 有低级 hook 和 Interception 路径 | 优先参考事件通道、tap/hold 与状态语义;不把 tick_ms 推断成微秒保证 |
-| [KMonad](https://github.com/kmonad/kmonad) | 5,071 | 事件源→Dispatch→Hooks→Sluice→Keymap;STM 状态/重放缓冲;独立 emitter 从 TMVar 取输出 | 适合参考可组合事件处理和回放;TMVar 交接与 Haskell runtime 不提供硬时限 |
-| [keyboard](https://github.com/boppreh/keyboard) | 3,972 | GenericListener 中监听和处理两条线程;Queue FIFO,消费者依次执行 handler | 清晰的采集/处理解耦样例;无界积压与慢 handler 会影响尾延迟,阻断输入路径须另看 |
-| [AutoKey](https://github.com/autokey/autokey) | 3,896 | Linux 桌面自动化/脚本项目,本轮未深入其队列源码 | 跨平台功能参考,不作 Windows 输入架构证据 |
-| [nut.js](https://github.com/nut-tree/nut.js) | 2,857 | async/await 编排与输入 provider;所读分支键盘 autoDelayMs=300 | provider 可插拔值得借鉴;Promise 并发不代表硬件同时输入 |
-| [pynput](https://github.com/moses-palmer/pynput) | 2,173 | Listener 线程;Windows 上回调可能直接处于 OS 回调线程;官方建议把工作投到队列 | 采集层参考,不能在回调里跑识别、IO 或长计算 |
-| [Pulover Macro Creator](https://github.com/Pulover/PuloversMacroCreator) | 2,026 | AHK 生态的可视化录制/编排 | 适合参考宏表达和导出;本次快照最后 push 为 2022 年 |
-| [Interception](https://github.com/oblitum/Interception) | 2,019 | 键鼠过滤驱动及用户态等待、接收、发送接口 | 适合研究设备路径;不是调度算法,也没有证明比 SendInput 更快;快照最后 push 为 2021 年 |
-
-具体源码入口:[AHK hook.cpp](https://github.com/AutoHotkey/AutoHotkey/blob/v2.0.19/source/hook.cpp)、[keyboard GenericListener](https://github.com/boppreh/keyboard/blob/master/keyboard/_generic.py)、[PyAutoGUI 配置](https://github.com/asweigart/pyautogui/blob/master/pyautogui/__init__.py)、[Windows 后端](https://github.com/asweigart/pyautogui/blob/master/pyautogui/_pyautogui_win.py)、[Kanata 低级 hook](https://github.com/jtroo/kanata/blob/c391826f6bbc8d5dc1645bbedc5596e22ae5982a/src/kanata/windows/llhook.rs)、[Kanata 调度状态](https://github.com/jtroo/kanata/blob/main/src/kanata/mod.rs)、[KMonad 入口](https://github.com/kmonad/kmonad/blob/master/src/KMonad/App/Main.hs)、[KMonad Dispatch](https://github.com/kmonad/kmonad/blob/master/src/KMonad/Model/Dispatch.hs)、[pynput 回调约束](https://github.com/moses-palmer/pynput/blob/master/docs/keyboard-usage.rst)、[nut.js 键盘编排](https://github.com/nut-tree/nut.js/blob/e413fa1f19a19c4631812e4e1eaf47aa732b5cbe/core/nut.js/lib/keyboard.class.ts)、[QMK deferred executor](https://github.com/qmk/qmk_firmware/blob/master/quantum/deferred_exec.c)。
-
-这些项目的共同价值是**事件所有权、输入状态机、采集和执行解耦**。本轮没有发现可以直接移植后就承诺 Windows 微秒级端到端响应的通用宏引擎。Python/JavaScript 并非不能做宏,而是其默认动作节流、运行时停顿和事件循环应与精密时序执行层分开。源码借鉴与复制实现不同,若直接引入代码,另核对实际文件许可证;API 的 null/NOASSERTION 不表示无许可约束。
-
-## 4. Windows 调度与输入链路
-
-### 4.1 先定义测量对象
-
-设 t0 为物理按键闭合,t1 为本程序 hook 回调入口,t2 为任务入队,t3 为执行器开始,d 为该批次目标 QPC 时刻,t4/t5 为 SendInput 进入/返回,t6 为受控接收程序处理事件,t7 为关联画面实际发光。
-
-| 指标 | 定义 | 能证明什么 |
-|---|---|---|
-| 调度有符号误差 | t4 − d | 是否早发/迟发;必须保留符号 |
-| 调度迟到 | max(0, t4 − d) | deadline 服务水平,不代表端到端 |
-| 热键软件启动 | t4 − t1 | 本程序从收到 hook 到首次提交 |
-| API 调用时长 | t5 − t4 | 调用开销及其间可能发生的阻塞/抢占 |
-| 软件输入处理 | t6 − t4 | 注入开始到接收处理,需关联同一事件 |
-| 物理到接收 | t6 − t0 | 包含键盘固件、USB 和主机输入路径 |
-| 动作到发光 | t7 − t0 | 用户体验链路,需要光学/硬件证据 |
-
-**不要默认 t6 ≥ t5。** 接收方可能在 SendInput 返回前已经运行。用 t6−t5 作为唯一“系统排队时间”可能出现负数;应关联 t4、t5、t6 并保留并发关系。各段 P99 也不能相加后称为端到端 P99。
-
-```mermaid
-flowchart LR
- H[物理设备与 USB] --> I[Windows 输入栈]
- I --> K[专用 hook 消息线程]
- K --> Q[触发队列与取消标志]
- Q --> S[预编译计划与 QPC 调度]
- S --> A[输入状态仲裁]
- A --> N[SendInput 或 HID 后端]
- N --> R[目标程序处理]
- R --> F[渲染与显示]
- C[截图与条件识别] --> Q
-```
-
-这是测量/职责示意,不表示所有输入 API 都走完全相同的内核路径。
-
-### 4.2 优先级、时钟和定时器
-
-Windows 使用基于线程优先级的抢占调度;计时器到期只使等待可被满足,线程仍须获得 CPU。提升线程优先级不能压过硬件中断、某些 DPC 工作或固件停顿。CPU 迁移、缺页、锁竞争、频率/电源状态和同核竞争都是要测的变量。`REALTIME_PRIORITY_CLASS` 甚至会妨碍输入相关系统线程,不宜作为宏软件默认设置。[Windows 调度优先级](https://learn.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities)、[硬件 IRQL](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-hardware-priorities)。
-
-QPC/Stopwatch 是测量和绝对时间轴的基准,不是唤醒机制。QPC 频率是接口刻度,不等于 CPU 主频;不要用未经验证的裸 RDTSC 替换它。相对定时器参数的 100ns 单位也不代表 100ns 实际精度。[QPC 官方指南](https://learn.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps)。
-
-高分辨率 waitable timer 自 Windows 10 1803 起可用。建议远期阻塞等待、临近 deadline 短自旋,每次恢复后重新读取 QPC。原点使用 QPC,转换为相对 timer 间隔;不要把 QPC tick 当作绝对 FILETIME。`timeBeginPeriod` 在 Windows 10 2004 后具有进程相关行为,Windows 11 对完全遮挡/最小化等场景还有约束;它不提高 QPC 精度,更不能视为硬实时开关。[CreateWaitableTimerExW](https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createwaitabletimerexw)、[timeBeginPeriod](https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod)。
-
-MMCSS 可以改善时间敏感线程获得 CPU 的机会,但不是任意应用时限保证。本项目的 Pro Audio 配置应作为可回退实验档,比较目标应用帧时间、CPU 和功耗,不能以提高优先级次数作为成功指标。[MMCSS](https://learn.microsoft.com/en-us/windows/win32/procthread/multimedia-class-scheduler-service)。
-
-### 4.3 SetWindowsHookEx 的瓶颈与优化
-
-`WH_KEYBOARD_LL/WH_MOUSE_LL` 属于监听/拦截入口。低级键盘 hook 在安装线程上下文执行,必须持续泵消息;它不是“回调在目标进程运行”的普通 DLL hook。超时可能导致静默移除。Windows 11 已不支持传统 journaling hooks,不能用旧宏录制教程的 WH_JOURNALPLAYBACK 作为新架构。[LowLevelKeyboardProc](https://learn.microsoft.com/en-us/windows/win32/winmsg/lowlevelkeyboardproc)、[SetWindowsHookExW](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexw)。
-
-工程建议:
-
-1. 回调只做时间戳、注入标记过滤、必要键状态更新、查预建路由和有界投递;过滤/吞键决定必须在返回前完成,不能全部延后。
-2. 不在回调做进程名解析、图像识别、同步 UI 调用、等待工作线程或文件 IO。保持 delegate 有效并正确卸载。
-3. 若仅需标准组合热键,可独立比较 RegisterHotKey;录制/设备区分可比较 Raw Input;需要同步吞键时仍保留 LL hook。
-4. Raw Input 是另一种异步观测入口,不是 SendInput 的替代注入方法;不能假定它一定给出与 LL hook 完全相同的合成输入事件。
-5. 自身注入的过滤策略应显式定义:忽略所有 injected 事件简单,但会排除其他软件/辅助设备的合成输入。仅识别自身标记时也不能把标记当安全身份。
-
-Raw Input 能提供设备相关信息并支持后台接收配置,但不意味着目标应用就会更快消费输入。[Raw Input 概览](https://learn.microsoft.com/en-us/windows/win32/inputdev/about-raw-input)。
-
-### 4.4 SendInput 的瓶颈与优化
-
-SendInput 成功返回的是写入输入流的事件数,受完整性级别限制;UIPI 导致的失败不会由返回值/错误码明确指出。单次输入数组保持内部序列不被其他键鼠输入插入,但不提供执行时刻参数,也不保证应用已处理。[SendInput](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput)。
-
-优化应围绕自身可控成本:
-
-- 提前编码、复用 INPUT 缓冲;把同一时刻且语义可合并的事件做一次调用,减少调用次数。
-- 不把不同 deadline 的事件提前塞进一个数组;`INPUT.time` 也不是未来调度功能。批处理收益必须同时检查按住时长和目标程序识别率。
-- 键码/扫描码/Unicode 依语义选择;扫描码不是通用加速开关。鼠标相对/绝对移动、DPI、加速和移动合并需要独立验证。
-- 不持调度锁调用 API;否则输入回调、取消和其他宏可能被串行拖住。也不要让自己 hook 等待正在调用 SendInput 的线程。
-- 单次批次内部有序不代表多线程调用的跨宏顺序确定。需要显式的全局序号或冲突键仲裁。
-- 检查部分成功。不能盲目重放整批,否则可能重复按下;按已成功部分更新状态,停止/清理要有独立路径。
-- 不使用未公开 syscall 号绕过接口来赌性能;系统版本兼容性代价并没有消除目标程序排队。
-
-### 4.5 驱动级输入与“硬件中断模拟”
-
-| 路径 | 可解决什么 | 新增代价与无法解决的部分 | 推荐 |
-|---|---|---|---|
-| SendInput | 标准桌面键鼠合成 | 权限、输入状态、hook、目标消费节奏 | 默认基线 |
-| VHF 虚拟 HID | 按 HID 报告建模的虚拟设备、设备语义 | 内核驱动开发/签名/生命周期、报告缓冲;目标线程仍受 Windows 调度 | 有设备语义需求再做独立原型 |
-| Interception 类过滤驱动 | 观察/变换设备输入,选择设备路径 | 驱动及 IOCTL 跳转、设备兼容、部署;用户态策略仍会被抢占 | 作为兼容性研究分支,先测再选 |
-| MCU/FPGA USB HID | 主机外生成时间序列,减少主机触发依赖 | 主机轮询、USB 总线与输入栈仍在;PC 实时下发还新增传输延迟 | 预装计划/本地触发适用 |
-| 实时设备的定时 GPIO | 控制输出引脚的确定性边沿 | 不等价于 Windows 应用按键,也不是显示响应 | 真正硬时限的独立控制场景 |
-
-VHF 官方支持由 HID source driver 提交报告;它并没有承诺键鼠响应的微秒上界。[VHF](https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/virtual-hid-framework--vhf-)。
-
-USB 的 interrupt transfer 名字不意味着外设随时中断应用执行;主机按端点服务间隔轮询。1000Hz 的周期为 1ms,8000Hz 为 125µs;在随机相位、无其他排队的理想模型下仅轮询等待平均约半个周期。这是算术模型,不是某设备实测,也不能保证端到端小于该数值。[USB interrupt transfer](https://learn.microsoft.com/en-us/windows-hardware/drivers/usbcon/how-to-send-a-usb-interrupt-transfer--uwp-app-)。
-
-因此,“驱动级注入更低层,所以必然更快”“模拟中断即可绕过调度”均不是成立的性能论证。推荐用同一接收端对不同后端做配对实验。硬件端若要微秒级边沿,应采用硬件计时器/比较输出并测时钟误差;QMK 默认毫秒 deferred API 不足以直接承担该指标。
-
-## 5. 论文解读:可借鉴模型与不可迁移前提
-
-| 论文与出处 | 核心模型/发现 | MacroHID 落地方法 | 适用限制 |
-|---|---|---|---|
-| Liu & Layland, 1973, JACM, *Scheduling Algorithms for Multiprogramming in a Hard-Real-Time Environment*, DOI 10.1145/321738.321743 | 固定优先级 RM 与动态 EDF 的可调度性 | 按绝对到期时刻挑下一个小批次,避免长宏占着执行权 | 独立、可抢占、执行成本已知等假设,Windows 宏并不满足 |
-| Sha, Rajkumar & Lehoczky, 1990, IEEE TC, *Priority Inheritance Protocols: An Approach to Real-Time Synchronization*, DOI 10.1109/12.57058 | 优先级反转及继承/天花板协议 | 缩短共享锁,数据所有权化,避免高优先级依赖低优先级长工作 | 本轮查阅作者机构摘要;不把 Windows 普通 mutex 当作已实现论文协议 |
-| Abeni & Buttazzo, 1998, RTSS, *Integrating Multimedia Applications in Hard Real-Time Systems* | CBS 用预算/周期隔离执行超支 | 对 OCR/模板识别设置并发数、补充周期、最大挂起任务 | 应用层限额是借鉴,不具备内核 CBS 的带宽保证 |
-| Ousterhout et al., 2019, NSDI, *Shenango* | 高频资源调整、专门数据路径,结合忙轮询与资源效率 | 让控制、识别与精密提交分离;仅在临近 deadline 占核心 | 专用 IOKernel/网络数据中心平台,不能移植为普通 Windows 用户态抢占 |
-| Kaffes et al., 2019, NSDI, *Shinjuku* | 短任务被长任务阻塞导致尾延迟;借助特殊系统/硬件实现约 5µs 级抢占周期 | 把输入批次保持很短,识别分池;支持超时和协作取消 | 5µs 是其系统抢占周期,不是 Windows SendInput 延迟 |
-| Humphries et al., 2021, SOSP, *ghOSt*, DOI 10.1145/3477132.3483542 | 内核机制与用户态调度策略分离,消息/事务式调度接口 | 拆出计划、等待、选择、输入后端策略;使用带代数/序号的状态变更 | 依赖 Linux 内核机制,不是 Windows 现成 API |
-| Wimmer, Schmid & Bockes, 2019, CHI, *On the Latency of USB-Connected Input Devices*, DOI 10.1145/3290605.3300650 | 36 个设备的延迟分布;提高轮询率并非对所有设备都有效 | 单独测设备、主机、目标;报告分布而不只均值 | 论文具体设备数字不能外推本机/当前外设 |
-| Fried et al., 2024, NSDI, *Making Kernel Bypass Practical for the Cloud with Junction* | 在兼容性和资源密度约束下设计 kernel bypass | 审视“为了最低延迟永久占核”的成本;保持后端可替换和退回路径 | 网络/Linux 架构启示,不是 Windows HID 的旁路实现 |
-
-原文/机构链接:[Liu–Layland 原文](https://bears.ece.ucsb.edu/class/ece253/papers/laylandliu77.pdf)、[优先级继承作者机构记录](https://experts.illinois.edu/en/publications/priority-inheritance-protocols-an-approach-to-real-time-synchroni/)、[CBS 原文](https://retis.santannapisa.it/~giorgio/paps/1998/rtss98-cbs.pdf)、[Shenango 原文](https://www.usenix.org/system/files/nsdi19-ousterhout.pdf)、[Shinjuku 原文](https://www.usenix.org/system/files/nsdi19-kaffes.pdf)、[ghOSt 作者公开原文](https://www.ofirweisse.com/sosp21-ghost.pdf)、[USB 延迟作者版本](https://epub.uni-regensburg.de/40182/1/On_the_Latency_of_USB-Connected_Input_Devices_author_version.pdf)、[Junction 会议页](https://www.usenix.org/conference/nsdi24/presentation/fried)。
-
-在理想单核、独立周期任务、截止期等于周期、可抢占且无额外开销的模型中:
-
-\[
-U=\sum_i C_i/T_i,\quad U_{RM}\le n(2^{1/n}-1),\quad U_{EDF}\le1.
-\]
-
-RM 的上式是充分界而非所有任务集的必要界。现实应加入非抢占区、锁阻塞、唤醒抖动和外部干扰。当前宏调度更接近“下一到期事件选择”,批次的到期时刻通常同时是释放时刻;不能把它严格等同于论文里的可抢占 EDF 作业调度,也不能拿 U≤1 宣告达标。
-
-工程上可用 `服务时间/到达间隔` 估计负荷并留余量,但 P99 成本不是 WCET,经验占用阈值不是硬实时证明。其实际价值是提前拒绝过载、隔离重工作、减少排队。
-
-CBS 的可迁移部分是用预算 Q 和周期 P 表达服务份额 `u=Q/P`,耗尽预算时延期后续工作,而不是让识别无限抢占输入资源。应用层可以限制每轮准入的识别数、累计 CPU 时间和并发槽位,但普通 OCR 库的一次调用往往不可抢占:设置“2ms 预算”不会让一个 20ms 调用在 2ms 自动停止。须采用可分片识别、独立工作进程或超时后丢弃过期结果,并报告已启动工作的实际完成成本。
-
-## 6. 当前项目:已存在的能力与剩余风险
-
-本次检查工作区含大量既有未提交更改;以下描述对应读取时内容,不对应某个纯净发布标签。文件快照哈希见证据目录。
-
-| 模块 | 本次确认现状 | 应做增量优化 |
-|---|---|---|
-| `GlobalKeyboardHook.cs` | 独立 Dispatcher 消息线程、回调 QPC、注入事件过滤;事件同步调用订阅者 | 缩减订阅者在回调栈上的工作,测回调驻留分布 |
-| `MainWindow.xaml.cs` 热键处理 | UI 更新已 BeginInvoke;控制器启动直接调用;前台过滤会查询进程名,存在 triggerGate | 不应再提“先把全部热键搬离 UI”作为未实现项;优先测前台查询、锁与启动链 |
-| `MacroPlaybackController.cs` | 锁保护启动/停止,runId/取消/任务生命周期管理 | 投递与完成路径的分配和排队开销;保持重复触发、Hold/Toggle、停止 epoch 语义 |
-| `CompiledPlaybackPlan.cs` / executor | 批次预编译、prepared/native plan 缓存,条件计划准备 | 测热缓存命中和失效,不重复实现预编译 |
-| `QpcPlaybackDelayStrategy` | 已把自旋校准放入 Prepare 和缓存,构造函数不再每次校准;高精度等待 | 不再把“每次启动校准”列作当前事实;测不同核心类别缓存和等待回退 |
-| `SharedPlaybackScheduler.inl` | 两条复用工作线程;每宏只挑下一批;vector 扫描;等待及提交不持队列锁;inFlight 防同批重复认领 | 扫描成本、全局 generation/Notify、同键跨宏顺序、helper 获益 |
-| 原生等待模式 | 均衡末段 300µs,极限 1000µs;helper 比 owner 延后约 2µs 参与认领 | 把固定自旋/helper 改为经测量选择的策略实验,而非增加更多线程 |
-| `PlaybackWorkers.cs` / lease | 工作线程复用;空闲保留限额,ParallelWorkerLease 对特定并行工作设 32 上限 | 空闲上限不等于全系统活动线程上限;条件线程和同时宏需单独准入 |
-| `ConditionMonitor.cs` | 条件独立线程、绝对轮询目标、跳过已错过轮询;then 计划缓存 | 线程总量、帧新鲜度、条件预算;不做积压轮询补跑 |
-| `MacroModel.cs` | 两种条件步骤默认轮询间隔 25ms | 这是条件反应的主要量级因素;不能只优化输入末端 10µs |
-| `PixelFrame.cs` / evaluation cache | 已有 BGRA 缓冲和短期复用,识别结果合并窗口默认约 1ms | 区分抓帧时间和计算完成时间,测缓存命中对应的帧年龄 |
-| `AppLog.cs` | 已有有界后台日志队列 | 不能声称启动路径同步写磁盘;仍可测字符串/节点分配、队列锁 |
-| `PlaybackTrace.cs` | 有 Hotkey、Dispatch、Submit、Receiver 等 stage | 缺少足够的逐批次身份/字段时补齐;声明 Receiver 枚举不等于已有接收端测量 |
-| `LatencyProbe` | 明确不发送真实按键;空批次/假输入用于探针 | 它的结果不能表示真实 SendInput 或目标应用接收延迟 |
-
-定位入口(仓库相对路径,便于复查):
-
-- `src/ui/MacroStudio/GlobalKeyboardHook.cs`:KeyboardHookCallback、TriggerPressed。
-- `src/ui/MacroStudio/MainWindow.xaml.cs`:KeyboardHook_TriggerPressed。
-- `src/ui/MacroStudio/Services/ForegroundProcessService.cs`:GetForegroundProcessName。
-- `src/shared/MacroHid.Runtime/MacroPlaybackController.cs`:StartRunCore。
-- `src/shared/MacroHid.Runtime/MacroPlaybackExecutor.cs`:QpcPlaybackDelayStrategy、Prepare、TryGetPreparedPlan。
-- `src/native/MacroHid.NativePlayback/SharedPlaybackScheduler.inl`:Due、Loop、Notify、Submit。
-- `src/shared/MacroHid.Runtime/ConditionMonitor.cs`:PollCore。
-- `src/shared/MacroHid.Runtime/RecentEvaluationCache.cs`:Get。
-
-### 历史原始数据的本次复核
-
-两份 CSV 的 SHA-256 与既有 provenance 一致。原试验是两个并发计划、每计划 50,000 个 1ms 批次;假输入回调、预热、申请与播放会话一致的计时器设置。原环境记录为 Ryzen 9 7945HX、Windows 11 build 26200;本次没有重新验证机器状态或重跑。
-
-| 档位/计划 | N | CSV 整数 µs P50 | P95 | P99 | 按原始 QPC 重算 P99.9 | 原始 QPC 最大迟到 |
-|---|---:|---:|---:|---:|---:|---:|
-| 均衡 0 | 50,000 | 0 | 0 | 1 | 185.6µs | 1093.9µs |
-| 均衡 1 | 50,000 | 0 | 0 | 1 | 3.7µs | 876.2µs |
-| 极限 0 | 50,000 | 0 | 0 | 0 | 2.1µs | 644.0µs |
-| 极限 1 | 50,000 | 0 | 0 | 0 | 2.1µs | 1071.4µs |
-
-采用 nearest-rank 分位数。表中 0 来自整数微秒列,**不是零延迟**。全部样本合并后均衡 P99.9 为 110.1µs,但合并会掩盖计划差异,因此不能只报告合并结果。以上每档仅一轮历史数据;没有真实注入、真实物理热键或跨设备验证。
-
-原数据还需要注意统计口径:调度器记录相对 nominal due 的迟到,但 `Due()` 会加暂停、按键释放依赖及组合键保持约束。今后同时输出 nominal_due、effective_due,区分“语义要求推迟”和“调度器迟到”,避免误诊。
-
-## 7. 可落地算法设计
-
-### 7.1 任务队列分层
-
-推荐在现有框架上形成三层职责,不预设必须重写为无锁:
-
-1. **触发/控制入口**:固定结构 `(macroId, epoch, sourceQpc, sequence, action)`;hook 单生产者路径可用 SPSC,有 UI/条件多来源时用各自 SPSC 汇聚或真正 MPSC,不能把多生产者硬套 SPSC。容量以最大预期突发定义;满队列时明确拒绝并计数,停止信号走独立原子标志/唤醒。
-2. **时间队列**:每个 active run 保存下一批,不展开全部循环事件;候选键为 `(effectiveDue, stableSequence)`。小 N 先保留连续数组扫描,大 N 经测量再切小根堆。暂停、取消、依赖变化必须更新堆或带 generation 做失效过滤,并限制陈旧节点数量。
-3. **输入提交/仲裁**:决定同键和修饰键冲突后才提交;不可丢 key-up。输出日志/统计用有界缓冲,后台批量处理。
-
-| 结构 | 成本 | 实际取舍 |
-|---|---|---|
-| 连续数组线性扫描 | 选取 O(N) | N 很小时局部性好、更新简单,当前实现可能已经更合适 |
-| 二叉最小堆 | 入/出 O(log N) | 活跃宏较多时适用;处理暂停/取消/同刻顺序复杂 |
-| 时间轮 | 平均摊销可很低,精度受桶宽/设计影响 | 大量粗粒度定时器适用;少数精密宏未必有收益 |
-| FIFO 触发队列 | 常数级入出 | 只表示到达顺序,不自动满足 deadline 优先 |
-| 每宏一条精密线程 | 易写局部逻辑 | 多线程自旋、切换、同键交错;不作为扩容方向 |
-
-先比较 1/2/8/32 活跃宏;如产品确实支持更大规模,再比较 128/256。不要只用渐进复杂度推断 2 个宏时堆更快。
-
-### 7.2 绝对时间轴与迟到策略
-
-周期保持 `d(k)=t_origin+k*T`,不要在每次执行后重置为 `now+T`,否则开销累积成漂移。将目标时刻和动作允许语义分开:
-
-\[
-d_{effective}(k)=\max(d_{nominal}(k)+pause,
-t_{dependency}+hold_{min}).
-\]
-
-赶不上时间时明确选择:短暂迟到的离散按键按序执行并维持必要 hold;过期鼠标轨迹可在声明允许时合并;识别任务只处理最新帧;过载严重时终止宏并释放持有键。**不能统一用“追赶补发”造成按下/抬起挤在一起,也不能统一丢弃过期按键。**
-
-### 7.3 自适应阻塞+自旋
-
-令 W 为预留自旋窗口,e 为计时器实际恢复减去计划唤醒时刻的正向超时量,h 为唤醒后选取/认领的开销。工程实验模型:
-
-\[
-W=clip(Q_p(e+h)+margin, W_{min}, W_{max}).
-\]
-
-在后台统计最近窗口,缓慢调整,空闲/恢复/电源状态改变后重建样本;按档位、核心类别分别学习。p、margin 和上下限均为试验参数,不是定理。离群事件需要记录,不能用无限加大 W 来掩盖 DPC 或系统停顿。
-
-```text
-接收控制命令,检查取消/暂停/generation
-取当前最早有效批次,计算 effective_due
-若 remaining > W:设置相对高精度 timer,等待 timer 或更早任务/取消事件
-否则:短自旋并检查变化;不早于 effective_due
-再次校验计划与输入冲突,认领一次
-锁外提交输入,记录 submit_begin/submit_end/返回数量
-更新状态,只唤醒确实需要重新选任务的 worker
-```
-
-W=300µs、周期 1ms 时,单 worker 的末段自旋就可能接近一个逻辑核的 30%;W=1000µs 时可能接近持续占核。此处仅是忽略其他开销的上限量级估算,双 worker 是否同时自旋由实际负载决定,必须测 CPU time。更小调度误差若换来更差帧时间,产品价值可能下降。
-
-### 7.4 控制双 worker 的收益与副作用
-
-当前极限模式 helper 可在 owner 之后约 2µs 参与,`inFlight` 在锁内保护同批认领。保留这层正确性,不允许两条线程试发同一按键再择优。
-
-实验比较:单提交线程;当前双 worker;延迟更大的 helper;只在有历史长尾风险时启用 helper。采集 helper win 比例、同批认领竞争、锁等待、唤醒次数、跨核心缓存流量及目标帧时间。每次提交后的全局 Notify 会使其他 worker 重新检查 generation;改成按 owner/更早 deadline 唤醒时,必须验证无丢失唤醒、暂停/取消及时生效以及新任务可以打断旧等待。
-
-跨宏同键的状态不能由每宏独立字典模拟成独立物理设备。可选策略:冲突键串行租约、拒绝冲突宏、明确的共享按住引用计数。引用计数会改变脉冲/重复语义,不能静默替换。建议默认保守串行冲突,保留用户可见的冲突策略。紧急停止只释放本程序实际拥有的状态,并验证与物理按键同时按下的行为。
-
-### 7.5 条件检测:优化更大的延迟项
-
-默认 25ms 轮询,在事件相位均匀、计算足够快且未错过轮询的理想条件下,单轮询等待平均 12.5ms、P99 约 24.75ms。**这是模型推导,不是当前系统测量;识别和抓帧只会加入更多成本或改变分布。**
-
-推荐:
-
-- 像素/小 ROI、模板、OCR 分不同预算池;输入 deadline worker 不做识别。
-- 暂停语义允许等待识别时,应把这种等待单独计入控制流耗时,不能从用户观察延迟中删除。取消不支持中断的识别调用时,先阻止其结果启动新输入,再异步回收资源。
-- 每个检测源最多一个正在执行和一个最新待处理任务,防止旧帧排长队;不能对键盘动作应用这种覆盖规则。
-- 记录 `frameCapturedQpc`、`evaluationBegin/End`、`cacheAge`,共享抓帧和识别结果时检验来源帧的新鲜度。
-- 逐步实验像素轮询 25/10/5/2ms,模板/OCR 按实际服务时间设限,避免全量一律改 1ms。
-- 提供受控事件源时优先事件驱动;屏幕识别仍受应用帧更新、捕获和识别速度限制。
-- 图像已有 BGRA 优化和缓存,先测全链路,不重做已完成的每像素对象转换优化。
-
-## 8. 分阶段实施指南与验收门槛
-
-下列数字是拟议工程目标/决策门槛,不是已实现结果;具体机器不满足时报告失败并保留原始记录。
-
-| 优先级 | 改动与位置 | 验证与目标 | 预期收益依据 |
-|---|---|---|---|
-| P0 | 扩展 PlaybackTrace、Native run trace;新增受控接收端与统一关联 ID | 每批具有 runId/batchId、nominal/effective due、submit_begin/end、接收时间/缺失原因;测量模式开关开销可量化 | 消除把 API 返回当接收、把 hold 约束当迟到等误判 |
-| P0 | 热键路径分段计时;前台信息维护快照/必要复核;准备数据移出回调 | 建议 hook 自身工作 P99≤50µs;热启动按现有档位目标验收;禁止同步重 IO | 查询进程名和锁是源码确认的候选成本,收益待测 |
-| P1 | SharedPlaybackScheduler 定向唤醒、减少无效 generation 变化 | 双宏 1ms、8/32 宏突发;无丢唤醒/重复提交;P99.9 不退化;以唤醒数和 CPU 下降确认 | 当前每批 Notify 两个 event,存在减少冗余机会 |
-| P1 | 单/双 worker 与自适应 W 的 A/B | 均衡迟到 P99.9≤250µs,极限≤100µs;同时报告 >1ms 事件率和最大值 | 沿用现有目标;重点稳定长尾和减少资源占用 |
-| P1 | 同键/修饰键仲裁、过载与迟到政策 | 交错 down/up、部分成功、取消竞态、物理按键介入;零可观察重复认领/粘键 | 正确性决定微秒优化是否有效 |
-| P1 | ConditionMonitor 统一预算/帧年龄/最新任务策略 | 像素、模板、OCR 分别报告检测→提交;同场景减少轮询间隔须不破坏帧时间预算 | 25ms 默认轮询是明确的大量级成本 |
-| P2 | 数组扫描 vs heap;预分配 run/计时缓冲;托管工作准入 | 用真实 N 和到达模式找交叉点;记录 hot path bytes/op、GC 停顿、队列容量 | 限制规模增长的成本,不能预设小根堆获胜 |
-| P2 | CPU Sets/亲和性拓扑扩展、MMCSS 适配 | Ryzen/混合核 Intel、SMT、多处理器组、游戏负载下 A/B | 当前 32 逻辑处理器证据不覆盖所有拓扑 |
-| P3 | 可选 VHF 或外部 HID 后端 | 同接收端、同逻辑动作下比较成功率、P99.9、维护/功耗成本 | 只有测出目标路径获益才进入产品默认选项 |
-
-实施顺序:先 P0 建基线和入口拆分,再分别做 P1 小改;每个改动独立提交和 A/B,避免同时改 timer、亲和性、队列后无法归因。P2 根据剖析触发,P3 单独验证。预计拆为 6–10 个可评审变更包;这是范围拆分,不是交付工期承诺。
-
-现有策略中有历史枚举命名兼容:`PrecisionMode.Balanced` 在 policy 中映射低占用策略,`ExtremeDuringPlayback` 为界面均衡,`UltraLowJitter` 为极限。实施时以 `PlaybackPerformancePolicy.For()`、UI 标签和实际配置共同核对,不按枚举名字猜配置,也不要破坏已保存宏。
-
-建议游戏体验门槛:固定场景下与不运行宏比较,P99 帧时间相对退化≤3%,并报告绝对毫秒变化、平均 FPS、CPU time 和功耗;3% 是本报告提出的初始预算,不是通用标准。系统/采样噪声接近预算时增加重复,不把不显著差异解释为提升。
-
-## 9. 性能基准设计:能够复现,也能定位
-
-### 9.1 四层基准
-
-| 层 | 实验 | 需要的证据 | 不能声称 |
-|---|---|---|---|
-| A 算法 | 合成时钟验证顺序/取消/过载;实际 QPC+假 sink 测调度 | 原始 deadline、认领/提交时间、CPU/锁等待 | 真实键鼠延迟 |
-| B 注入 | 前台受控窗口接收带唯一序号的 SendInput | 实际 INPUT 数量、返回数、目标消息处理 QPC、缺失和重复 | 物理设备延迟或任何游戏延迟 |
-| C 应用 | 固定目标程序回放、接收或动作响应记录,加 PresentMon/ETW | 同场景重复、应用消费证据、帧时间 | 仅凭帧率 CSV 推断 input-to-photon |
-| D 物理 | 设备触发+逻辑分析仪/USB 分析仪+光电传感器 | 时钟校准、触发边沿、显示光学响应 | 不经时钟同步就相减 MCU 和 QPC 时间戳 |
-
-受控接收端可由同机器 QPC 统一时基。`dwExtraInfo` 可作为实验关联手段,但必须在目标接收路径实测保留情况;Raw Input、窗口消息和 hook 测得的端点不同。丢失、合并、乱序事件独立计数,不能从延迟表中无声删除。PresentMon 适合帧呈现指标与联合分析,单独使用不证明每个宏事件的因果显示延迟。[PresentMon 项目](https://github.com/GameTechDev/PresentMon)。
-
-### 9.2 工作负载与对照
-
-- 节奏:100µs、250µs、1ms、4ms、16.667ms、100ms,加突发和随机到达;极密节奏只用于调度能力,不意味着应用可辨别同样短的按键。
-- 并发:1、2、8、32 宏;同键/不同键、长等待旁短宏、暂停/恢复、取消中提交。
-- 条件:纯输入;小 ROI 像素;模板;OCR;识别与播放混合。
-- 环境:空闲、CPU 受限、GPU 受限、磁盘/网络活动;前台/遮挡;冷启动/预热;插电/电池;固定分辨率、刷新率、应用版本、驱动、Windows build。
-- 线程策略:单/双 worker;固定/自适应 spin;默认调度/指定核心;MMCSS 开/关。一次只改变一个变量。
-- 外部工具对照:AHK、Kanata、至少一个商业录制器;相同输出逻辑、按住时长、目标窗口和成功率。公布原脚本与默认延时,既测试开箱配置也测试合理调优配置,不取消安全停止功能来美化数字。
-
-### 9.3 统计规则
-
-每个正式场景至少五轮交替或随机化顺序,每轮十分钟;每方案有效事件不少于十万。低频负载十分钟达不到事件量时延长采样,不能复制样本凑数。P99.9 在十万样本只有约百个尾部观察值,仍需要多轮;不同计划分别报告,不只报混合分布。
-
-报告 N、P50/P95/P99/P99.9/max、有符号最小值、超 100/250/1000µs 比例、遗漏/重复/取消耗时、CPU 核秒、分配量、GC、上下文切换、温度与功耗。分位数方法固定为 nearest-rank 或明确指定算法。对重复运行给出逐轮值和区间;序列相关时使用分块方法,不能假装所有事件独立。
-
-避免 coordinated omission:测试发生停顿后仍应保留所有预定 deadline;不能只在上次完成后才生成下一次请求,从而丢掉拥堵期间本应出现的延迟。条件任务的 latest-only 策略应记录被覆盖帧数,它属于产品策略,不等于测试丢样。
-
-计时器/ETW/记录本身有开销:预分配二进制记录缓冲,离线计算;分别测记录关闭、仅 QPC、完整事件跟踪。事件丢失、缓冲满和追踪开销写入结果。针对最大离群事件,以 runId/batchId 对齐 CSwitch、ReadyThread、DPC/ISR、缺页、GC 和 CPU 迁移;ETW 无法解释的停顿保留“未知”,不能一概归咎于 SMI。
-
-### 9.4 项目现有工具和补齐项
-
-现有入口包括 `src/tools/LatencyProbe`、`tests/MacroHid.Scheduling.Benchmarks`、`tests/MacroHid.NativePlayback.Tests`、`scripts/Trace-LatencyOutliers.ps1`、`scripts/compare_frame_times.py`。调度基准目前输出项目和样本数有限,需要补齐 P99.9/max、逐事件关联和四层边界。`Scheduling.Benchmarks` 中 probe 是假输入,不能改个标题称真实接收测试。
-
-每次运行建立证据包:源码工作区指纹、EXE/DLL SHA-256、命令参数、系统和电源配置、启动时间、原始事件文件、ETL、PresentMon CSV、汇总脚本版本及统计 JSON。当前工作区有未提交修改时,只记 git commit 不足以定位被测版本。
-
-正式验收必须同时过三道门:①输入语义/取消/资源正确;②分位数、超限率和最大值透明;③目标应用成功率及帧时间不退化到预算外。**不能用空输入路径的 2µs 分位数,替代真实 SendInput、真实热键或真实游戏验收。**
-
-## 10. 决策建议
-
-本项目值得继续采用“托管编排+少量原生精密执行线程”的结构。当前最有价值的工作是补齐真实接收证据,缩短 hook 同步调用链,减少调度唤醒竞争,给条件识别设预算,并明确跨宏输入状态语义。堆、时间轮、无锁、驱动和外部硬件都属于按证据选择的手段,不是精度保证。
-
-将产品承诺写成可检验的形式:**在列明机器、系统、负载和宏语义的条件下,指定档位调度迟到 P99.9 达到目标,同时公布最大迟到、超限率、真实接收延迟和资源代价。** 若需要硬时限,则另立受控实时硬件目标,并明确它与 Windows 目标应用响应的边界。
diff --git a/docs/single-macro-optimization-2026-10-14.md b/docs/single-macro-optimization-2026-10-14.md
deleted file mode 100644
index 3be6bdf..0000000
--- a/docs/single-macro-optimization-2026-10-14.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# 单宏执行路径优化与验证
-
-本页保留第一轮测量。后续已完成最终版本的物理热键与 Win32/WPF 真实输入接收,并进行了固定核心五轮尾部对照,见 [单宏尾部与端到端实测](single-macro-tail-and-end-to-end-2026-10-14.md)。下文的真实接收未完成状态属于第一轮历史记录。
-
-日期:2026-10-14。当前阶段聚焦单个预编译键鼠宏,保留暂停、取消、按住时长与输入安全语义。
-
-## 实际改动
-
-- 原生执行器在单宏适用场景下认领整个连续段,一条线程连续推进,不为每个批次扫描队列、获取调度锁或通知两条线程。
-- 名义截止、实际按下后的最短按住时间、修饰键保持约束仍沿用原语义。控制事件或新任务到达时,通过 generation 变化退出连续段,交回已有调度器。
-- 修饰键边沿在创建预编译计划时计算一次,执行阶段直接读取。
-- 复用统计工作区时取消对整个提交时间数组的清零。释放依赖只引用本次已执行的前序批次,其时间戳先写后读。补充了同一计划连续复用且迟到启动的按住时长回归。
-- 单宏原生均衡按实际 timer 唤醒超时量,在每次运行内将末段自旋窗口有界提高至最多 800µs;初始为 300µs。控制事件不参与学习。
-- 极限定时事件保留辅助线程;只有相邻名义批次间隔≤20µs、当前有效截止已临近的密集段使用连续路径。
-
-单宏并不要求只使用一条内部线程。首轮对照显示,极限 1ms 节奏若完全关闭辅助线程,虽然 CPU 时间减半,但 P99.9 从约 3–5µs 上升至 22–87µs,因此最终版本保留定时事件的辅助抢救。不能把这一被否决的原型结果当作最终性能。
-
-## 基准口径
-
-`--single-benchmark` 使用测试专用假输入回调,不向桌面发送键鼠。预热完成并创建计划后,计量一次运行;CPU 时间为测试进程的 kernel+user CPU time,wall time 包含等待完成事件。运行结束后再计算统计和写 CSV,不将导出开销计入执行时间。
-
-1ms 节奏为五轮交替顺序、每轮 20,000 批次,每方案每档累计 100,000 样本。每轮约 20 秒,不是五轮各十分钟的完整场景验收。零等待密集段每次 100,000 批次,衡量调度吞吐;所有批次同一名义截止,因此其 P99.9 随累计执行时间增长,不作为周期抖动指标。
-
-原始数据、基线/最终测试 EXE、日志位于工作区 `tmp/single-macro-20261014`。基线 EXE 在修改执行器前编译并保存。已有工作区修改不回退,原生产 ABI 和宏配置枚举不变。
-
-## 本次结果
-
-本机:AMD Ryzen 9 7945HX、16 个物理核/32 个逻辑处理器,Windows 11 build 26200,电源方案 Turbo。没有主动施加 CPU/GPU 压力,也没有证明系统完全空闲;温度和频率未锁定。极限每个新进程按原准备流程测核,不固定同一对核心,因此不能将所有最大值变化归因于某一个代码改动。
-
-### 1ms 单宏:五轮,每方案每档累计十万事件
-
-下表分位数为五轮各自结果的最小–最大范围,最大值及超限数量为全部十万样本。使用原始 QPC、nearest-rank 分位数,不采用原生汇总中的整数微秒截取。单位均为微秒。
-
-| 档位/版本 | P50 | P95 | P99 | P99.9 | 全部样本最大迟到 | >1ms 数量 |
-|---|---:|---:|---:|---:|---:|---:|
-| 均衡/基线 | 72.1–78.4 | 265.9–315.5 | 373.4–420.1 | 477.3–537.5 | 1669.4 | 4 |
-| 均衡/最终 | 0.0 | 0.2–0.3 | 0.6–0.7 | 12.3–46.9 | 1428.3 | 2 |
-| 极限/基线 | 0.1–0.2 | 0.5–0.6 | 1.0–1.8 | 2.8–6.4 | 1989.6 | 9 |
-| 极限/最终 | 0.1–0.2 | 0.2–0.6 | 0.6–1.2 | 2.7–3.6 | 287.9 | 0 |
-
-QPC 本次频率为 10MHz,0.0µs 表示采样端点落在同一 tick,不表示键鼠响应或执行耗时为零。所有最终定时样本均未观察到早于名义截止的提交、缺失或批次乱序。输入数量是测试回调的逻辑计数,未验证目标应用收到输入。
-
-均衡的尾延迟改善有资源代价:每轮约 20 秒内,进程平均 CPU 时间从 1621.9ms 增至 9446.9ms,约从 0.08 到 0.47 个逻辑核心。极限约为 1.99 个逻辑核心,基本不变。它们是测试进程的消耗,不是全机 CPU 百分比。尚无游戏 FPS/帧时间或功耗收益结论。均衡最大迟到仍超过 1ms;极限本次零个 >1ms 事件也不是最坏时限保证。
-
-### 零等待密集段:每次十万批次,三轮
-
-| 档位 | 基线平均完成耗时 | 最终平均完成耗时 | 完成耗时降低 |
-|---|---:|---:|---:|
-| 均衡 | 148.64ms | 13.84ms | 90.7% |
-| 极限 | 95.60ms | 16.69ms | 82.5% |
-
-这表示假输入调度和记录的吞吐提升,不表示 SendInput 能在同样时间发送并被应用处理十万次输入,也不代表 14–17ms 的周期抖动。短试验的进程 CPU 时间粒度较粗,不以其计数推导微秒级 CPU 成本。
-
-详细逐轮统计及原始 CSV 的 SHA-256 见 `tmp/single-macro-20261014/summary.json`;生产 DLL、交付文件和源码指纹见 `provenance.json`。汇总可复现:
-
-```powershell
-python scripts/summarize_single_macro.py tmp/single-macro-20261014
-```
-
-### 功能与交付
-
-- Release 原生构建:0 警告、0 错误;解决方案构建:0 错误、12 个现有警告(依赖兼容及测试可空类型)。
-- 托管测试:388 项通过。原生回归通过,包括单宏运行中暂停、迟到启动的同计划复用、取消、部分失败、资源保留与退出重启;原有并发回归只用于确认兼容性。
-- 本地测试版:`build/single-macro-20261014/MacroStudio/MacroStudio.exe`,配套原生 DLL 与生产构建 SHA-256 相同,DLL 加载、预热和关闭检查通过。当前进程不会自动切换为新 DLL,使用测试版需启动这个新输出。
-- 真实接收探针因未取得前台焦点在发送前中止。物理热键、真实 SendInput 接收、长时间游戏负载与条件识别混合场景仍需独立验证。
-
-## 可复现入口
-
-正常回归及单宏假输入:
-
-```powershell
-.\tests\MacroHid.NativePlayback.Tests\x64\Release\MacroHid.NativePlayback.Tests.exe
-.\tests\MacroHid.NativePlayback.Tests\x64\Release\MacroHid.NativePlayback.Tests.exe --single-benchmark 1 20000 1000 balanced.csv
-.\tests\MacroHid.NativePlayback.Tests\x64\Release\MacroHid.NativePlayback.Tests.exe --single-benchmark 2 20000 1000 extreme.csv
-```
-
-参数依次为模式(1=原生均衡,2=极限)、批次数、间隔微秒、CSV 路径。快速路径回退开关须在进程启动前设为 `MACROHID_DISABLE_SINGLE_RUN_FASTPATH=1`;它用于隔离连续路径,不等于完整恢复优化前全部源码。
-
-真实输入探针需要在本机交互桌面运行:
-
-```powershell
-.\tests\MacroHid.NativePlayback.Tests\x64\Release\MacroHid.NativePlayback.Tests.exe --receiver-benchmark 1 10000 1000 receiver.csv
-```
-
-探针创建自己的测试窗口,只在该窗口为前台时发送带序号的 F24 按下/抬起。等待前台最多五秒,必要时点击测试窗口;切换窗口会使后续注入停止。CSV 包含计划时间、调度时间、SendInput 进入/返回、接收回调、实际发送数量和收到次数。目标处理可能早于 API 返回,分析时用 receiver−submit_begin,不能要求 receiver−submit_end 必须非负。
-
-本次非交互启动没有取得测试窗口前台焦点,探针在发送前中止。因此没有真实接收延迟结果,也未完成游戏/物理热键/光学响应验收。
diff --git a/docs/single-macro-tail-and-end-to-end-2026-10-14.md b/docs/single-macro-tail-and-end-to-end-2026-10-14.md
deleted file mode 100644
index 94da58a..0000000
--- a/docs/single-macro-tail-and-end-to-end-2026-10-14.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# 单宏极限档尾部优化与真实输入接收实测
-
-本轮保留原生工作线程的预配置,避免每次孤立热键触发时重新注册 MMCSS 和设置线程亲和性;热键专用线程使用 AboveNormal。两项更激进的调度改写经对比后撤回。完成最终版本的 **30 次物理 F8 → 独立 Win32 目标接收**,以及 Win32/WPF 各 2,000 次自动输入接收测试,最终版本共 4,030 个有效样本,无丢失、重复、失败或焦点拒绝。
-
-固定核心的五轮调度对照中,最大迟到由 1,230.4µs 降至 893.4µs,约下降 27.4%;但这只是本轮观察值,第四轮更差,不能声称获得稳定的最大迟到上界。真实接收仍是毫秒级,未实现微秒级端到端响应。
-
-## 保留与撤回的改动
-
-| 改动 | 处理 | 原因与范围 |
-|---|---|---|
-| 原生线程初次预热时先配置 MMCSS、优先级、亲和性,再报告就绪 | 保留 | 把首次提交前的配置工作移到准备阶段 |
-| 工作线程空闲时保留已配置状态,阻塞等待事件 | 保留 | 后续孤立触发不重复注册/撤销 MMCSS;模式变化和关闭时恢复。空闲时不自旋 |
-| 热键专用线程 AboveNormal | 保留 | 减少钩子线程受普通任务排队的影响;未调整整个进程优先级 |
-| 双工作线程以原子令牌连续推进单个计划,取消逐批队列锁与通知 | 撤回 | 固定核对照最大值 1,933.4µs,对照 1,202.3µs;不能因启动更快就接受尾部退化 |
-| 单宏备用线程取消 2µs 救援余量,立即竞争 | 撤回 | 同轮合并 P99.9 为 3.4µs,预热方案为 2.7µs;最大值也更大 |
-
-继续保留上一轮的单宏均衡连续路径、极密集极限批次路径、预计算组合键边界及诊断工作区复用。间隔较大的极限批次仍使用原有两线程救援方式,只有一个宏,不增加并行宏功能。已就绪引擎若后来改变准备核心集合,运行线程仍在选取计划时切换配置;不能把初次预配置效果推广到所有重新配置情形。
-
-## 调度器的固定核心对照
-
-环境:Windows 11 10.0.26200,AMD Ryzen 9 7945HX,16 物理核、32 逻辑处理器。采集时电源方案为 Turbo。没有建立受控游戏/GPU负载,也没有改变驱动、系统注册表或进程优先级。
-
-每个方案五轮,每轮 20,000 个批次,间隔 1ms,共 100,000 个事件;方案顺序轮换。限定处理器集合为 `0x50000000`,即逻辑处理器 28、30,原生准备检查通过其属于不同物理核心。测核可能交换两线程的顺序。采用相同的 0.5ms 计时器分辨率请求、假输入回调与 QPC 记录方式。
-
-这里测量的是计划到期时间至输入提交标记的误差,**不是实际 SendInput 或目标接收耗时**。分位数使用原始 QPC 差值及 nearest-rank 方法,保留首批与全部异常值。
-
-| 方案 | P50 | P95 | P99 | P99.9 | 最大 | >100µs | >1ms |
-|---|---:|---:|---:|---:|---:|---:|---:|
-| 本轮改动前 | 0.1µs | 0.4µs | 1.0µs | 2.8µs | 1,230.4µs | 17 | 1 |
-| 最终保留:预配置与空闲保留 | 0.1µs | 0.5µs | 1.0µs | 2.7µs | 893.4µs | 11 | 0 |
-| 撤回:立即救援竞争 | 0.2µs | 0.4µs | 0.8µs | 3.4µs | 1,189.5µs | 16 | 1 |
-
-各方案均无提前提交、漏批或重排。逐轮最大迟到(µs):
-
-| 轮次 | 改动前 | 最终保留 | 立即竞争 |
-|---|---:|---:|---:|
-| 1 | 859.0 | 278.8 | 1,189.5 |
-| 2 | 1,230.4 | 34.6 | 66.3 |
-| 3 | 224.1 | 200.4 | 888.8 |
-| 4 | 316.7 | 893.4 | 122.2 |
-| 5 | 977.4 | 776.3 | 259.2 |
-
-最终方案四轮的最大值更低,第四轮退化。启动到首次提交的原生耗时为 13–31µs,对照 49–54µs;CPU 消耗分别约 1.991 和 1.987 个逻辑核心,极限档持续 1ms 节奏仍接近占用两个核心。这不是低占用方案。
-
-上一轮报告的 287.9µs 最大值来自不同时间和自动选择的核心,不能与这里的 893.4µs 拼成同条件前后对比。本轮也有自动选核探索结果及更差的异常值,全部保存在证据目录。没有通过长时间、负载矩阵和跨设备验收。
-
-## 真实热键与接收程序
-
-测试工具复用生产 `GlobalKeyboardHook` 源码和真实 `MacroPlaybackController → MacroPlaybackExecutor → 原生引擎 → SendInput`。目标是工具启动的另一进程,分别用普通 Win32 `GetMessage` 循环或 WPF `HwndSourceHook` 记录实际 `WM_KEYDOWN(F24)`。宏为 F24 按下、等待 5ms、抬起,固定执行一次,不执行识别或游戏效果。仅当自己的接收窗口为前台时才允许发送。
-
-每次触发都等待宏结束和目标实际接收,并检查收到次数、运行成功标志及原生后端;不把 SendInput 返回当作接收。预热十次无热键播放,不计入样本。物理触发通过用户实际按下/松开 F8;自动组仅在独立探针中允许专属标记的注入 F8,生产程序继续拒绝注入热键。
-
-测量起点为 **F8 低层钩子回调入口**,终点为目标窗口的 F24 消息回调。未测机械开关闭合、USB轮询、驱动上报至钩子入口的物理链路,也未测游戏逻辑、画面或显示器响应。独立探针直接调用控制器,未覆盖完整主窗口的前台进程筛选和状态刷新。同步 EventSource 与共享内存记录存在测量开销。
-
-### 最终版本结果
-
-| 触发与目标 | N | P50 | P95 | P99 | P99.9 | 最大 |
-|---|---:|---:|---:|---:|---:|---:|
-| 物理 F8 → Win32 接收 | 30 | 3.461ms | 5.366ms | — | — | 8.708ms |
-| 标记注入 F8 → Win32 接收 | 2,000 | 4.336ms | 5.752ms | 6.578ms | 9.519ms | 10.455ms |
-| 标记注入 F8 → WPF 接收 | 2,000 | 4.589ms | 6.682ms | 8.650ms | 12.039ms | 16.680ms |
-
-30 次物理样本不足以估计 P99/P99.9,不用其最大值冒充分位数结论。首个物理样本是全组最大值,钩子至分发耗时 3.663ms;其余 29 次最大端到端为 5.366ms。这提示首次热键路径仍需诊断,但未通过运行时追踪证明具体由 JIT、GC 或其他因素引起,正式统计保留首样本。
-
-### 分段诊断(最终版本,µs)
-
-下表为 Win32 自动组 N=2,000。每段分别排序;分位数和最大值不能直接相加。
-
-| 段落 | P50 | P95 | P99 | P99.9 | 最大 |
-|---|---:|---:|---:|---:|---:|
-| 源 F8 SendInput 开始 → F8 钩子入口 | 457.4 | 717.8 | 1,413.9 | 1,849.2 | 2,073.9 |
-| F8 钩子 → 托管工作线程 | 79.4 | 110.1 | 141.7 | 306.2 | 330.3 |
-| F8 钩子 → 原生首次提交标记 | 296.1 | 401.5 | 615.5 | 2,386.7 | 4,981.8 |
-| 原生首次提交标记 → F24 钩子入口 | 244.9 | 439.1 | 603.7 | 988.8 | 1,649.9 |
-| F24 钩子入口 → CallNextHookEx 返回 | 870.6 | 1,357.6 | 1,814.3 | 2,717.5 | 3,968.0 |
-| CallNextHookEx 返回 → 目标 WM_KEYDOWN | 2,935.3 | 3,883.8 | 4,702.8 | 7,238.7 | 8,722.5 |
-
-WPF 自动组最后一段 P50=3,090.4µs、最大=14,840.1µs。两个目标均有明显的钩子返回后等待,因此不能只归因于 WPF。需要继续区分 Windows 输入路由、目标线程就绪/调度、消息队列和负载影响。钩子入口至转发返回还包括探针本身的少量记录工作,不能把整段直接归为其他应用钩子的耗时。
-
-`native_api_total_us` 是按下与抬起两个提交调用耗时之和,不是首个 API 的独立耗时。`submit_qpc` 在原生调用前取样,包含紧随其后的处理器查询等工作。`max_scheduler_late_us` 为名义时间轴迟到;键抬起为保证实际按住时间,会顺延至按下提交结束后的最小保持时间,因此不把该字段当作有效截止时间误差。
-
-### 探索阶段结果也保留
-
-| 文件 | 结果与用途 |
-|---|---|
-| `end-to-end-final.csv` | 旧候选版 30 次物理输入全部有效,最大 9.731ms;随后自动组因最后热键仍按住而超时,整条命令没有通过。已修复等待最终物理松开再进入自动组 |
-| `end-to-end-synthetic.csv` | 旧候选版 2,000 次自动输入通过,最大 133.639ms |
-| `end-to-end-observed.csv` | 增加输出钩子记录及热键线程优先级后的旧候选版 2,000 次通过,最大 24.065ms |
-
-不同阶段的版本、记录开销、时间与负载不同。不能把 133.639ms→16.680ms 全部归因于最终算法,也不能将探索版物理样本当作最终版本验收。本轮最终 Win32 与 WPF 组也是顺序执行,并非交替多轮目标比较。
-
-## 回归、证据与运行方法
-
-- 最终 Release 原生 DLL 构建:0 警告、0 错误;解决方案构建:0 错误、11 个现有依赖兼容性警告;探针构建:0 错误、4 个依赖兼容性警告。
-- 托管测试 388 项通过。原生回归通过,增加了均衡与极限单宏运行中暂停验证;保留取消、输入部分失败不重放、计划/控制器引用保留、批次不重复和退出重启检查。
-- 新运行输出:`build/single-tail-20261014/MacroStudio/MacroStudio.exe`。生产、探针、交付目录中的原生 DLL 校验值一致;旧输出保持原样。现有已启动进程需重新启动该输出才能使用新版本。
-- [调度器原始统计及逐轮哈希](../tmp/single-tail-20261014/scheduler-summary.json)、[撤回双线程连续路径统计](../tmp/single-tail-20261014/rejected-dual-summary.json)、[全部真实输入统计及 CSV 哈希](../tmp/single-tail-20261014/end-to-end-summary.json)、[最终文件与环境指纹](../tmp/single-tail-20261014/provenance.json)。原始 CSV、各方案 EXE、调度器源码快照和日志在同目录,本地证据不作为随发行版附带的性能承诺。
-
-重跑最终版本单宏调度(参数末尾为可选核心集合):
-
-```powershell
-.\tests\MacroHid.NativePlayback.Tests\x64\Release\MacroHid.NativePlayback.Tests.exe --single-benchmark 2 20000 1000 single.csv 0x50000000
-```
-
-重跑完整物理热键与真实接收(先构建独立探针):
-
-```powershell
-dotnet build src/tools/EndToEndProbe/EndToEndProbe.csproj -c Release
-.\src\tools\EndToEndProbe\bin\Release\net8.0-windows\EndToEndProbe.exe --physical 30 --automatic 2000 --receiver-kind win32 --timeout 300 --output physical-win32.csv
-```
-
-接收窗口就绪后按下/松开 F8,每次间隔约半秒;自动组期间保持窗口前台。纯自动组使用 `--physical 0`,目标可用 `--receiver-kind wpf`。失焦或缺少接收会停止并保留失败记录。不要在测量期间同时构建、跑其他基准或切换窗口。
-
-重新汇总:
-
-```powershell
-python scripts/summarize_tail_comparison.py tmp/single-tail-20261014 --log paired-eager.log --pattern '{variant}-eager-round{round}.csv' --output scheduler-summary.json
-python scripts/summarize_end_to_end.py tmp/single-tail-20261014/end-to-end-release-win32.csv tmp/single-tail-20261014/end-to-end-release-wpf.csv --output tmp/single-tail-20261014/final-input-summary.json
-```
-
-## 下一步的具体优化顺序
-
-1. 对最慢样本同步采集线程 Ready/CSwitch、DPC/ISR、GC/JIT 和输入事件,将钩子线程、原生两工作线程及目标线程放在同一条时间轴。先定位毫秒级尾部,避免只优化通常不到 3µs 的到期竞争。
-2. 分离首次热键路径与持续播放路径,检查回调内日志、事件记录及分发初始化;尝试可验证的预编译/初始化方案。需包含冷首样本的交替对照,不能靠丢弃首样本宣布完成优化。
-3. 用最终版本在固定目标负载下增加物理热键样本和至少五轮长测,同时记录最大值及超阈值次数。单次 2,000 自动样本的 P99.9 只对应很少几个尾部事件,仍需扩大样本。
-4. 最后验证真实目标程序的输入消费与游戏帧时间。输入消息到达不等于目标动作完成;驱动或硬件注入不会自动消除目标线程调度及显示延迟,不能据本轮结果承诺其收益。
diff --git a/docs/superpowers/plans/2026-10-14-scheduling-latency-phases.md b/docs/superpowers/plans/2026-10-14-scheduling-latency-phases.md
deleted file mode 100644
index 8682313..0000000
--- a/docs/superpowers/plans/2026-10-14-scheduling-latency-phases.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# 调度延迟优化分阶段计划
-
-基线与依据:`docs/research/macro-scheduling-research-2026-10-14.md`(第 8 节优先级表)、`docs/single-macro-tail-and-end-to-end-2026-10-14.md`(实测)。本计划只覆盖缺口,不重复已实现的预编译、QPC 时间轴和共享原生调度器。
-
-范围:阶段 1、2 立即实施;阶段 3 依据阶段 1 的实测决定;阶段 4 只记录,需单独设计。
-
-## 阶段 1:测量先行(纯增量,默认关闭或仅影响基准输出)
-
-| 任务 | 位置 | 验收 |
-|---|---|---|
-| 1.1 基准补 P99.9/max/超阈比例 | `tests/MacroHid.Scheduling.Benchmarks/Program.cs` | 输出含 n、P99.9、max、>0.1/0.25/1ms 比例(已完成) |
-| 1.2 钩子自耗时:回调入口/出口 QPC 写入预分配环形缓冲,可导出 P99 | `src/ui/MacroStudio/GlobalKeyboardHook.cs` | 回调内无分配;关闭时无可测开销;有单元测试覆盖环形缓冲 |
-| 1.3 原生逐批追踪:batchId、nominalDue、effectiveDue、submitBegin、submitEnd、sent;新导出读取 | `SharedPlaybackScheduler.inl`、`NativePlayback.h/.cpp` | 默认关闭;开启后可导出逐批 CSV;`MacroHid.NativePlayback.Tests` 通过;关闭时固定核心基准不退化 |
-
-## 阶段 2:热键入口
-
-| 任务 | 位置 | 验收 |
-|---|---|---|
-| 2.1 前台进程名快照:`SetWinEventHook(EVENT_SYSTEM_FOREGROUND)` 维护,热键路径只读 | `Services/ForegroundProcessService.cs`、`MainWindow.xaml.cs` | 热键路径无 `Process.GetProcessById`;前台切换后快照在下次热键前已更新;过滤语义不变 |
-| 2.2 评估是否把 TriggerPressed 处理移出钩子线程(吞键判断仍在回调内) | `GlobalKeyboardHook.cs` | 依据 1.2 数据决定;钩子自耗时 P99 ≤ 50µs;EndToEndProbe「钩子→托管工作线程」不变差 |
-
-## 阶段 3:调度器唤醒(有风险,逐项 A/B,任一项不达标即撤回)
-
-| 任务 | 验收 |
-|---|---|
-| 3.1 `Notify()` 定向唤醒,单宏独占时跳过 generation 递增 | 固定核心五轮对照:P99.9 不退化、最大迟到不增、唤醒数与 CPU 下降;无丢唤醒/重复提交 |
-| 3.2 自旋窗口增加衰减 | 同上 |
-
-## 阶段 4:仅记录
-
-跨宏同键仲裁(P1c)、条件检测预算与帧年龄(P1d)、堆队列与 VHF 后端(P2/P3)。会改变语义或引入驱动,需单独设计。
-
-## 边界
-
-端到端毫秒级的主要部分(钩子返回后到目标收到消息,约 2.9ms)在调度器之外,本计划不能消除。驱动级注入与硬件中断模拟不在范围内。
-
-## 实施结果(2026-10-14)
-
-| 任务 | 结果 |
-|---|---|
-| 1.1 | 完成。`Scheduling.Benchmarks` 输出 n、P99.9、max、>0.1/0.25/1ms 比例;n<1000 时 P99.9 等于 max |
-| 1.2 | 完成。`HookSelfTimeRecorder`,`MACROHID_HOOK_SELFTIME=1` 开启,钩子释放时写日志 |
-| 1.3 | 原生部分完成。`MACROHID_BATCH_TRACE=1` 开启,`MhpGetBatchTrace` 导出。托管互操作与 CSV 导出未接入 |
-| 2.1 | 完成。按 (hwnd, pid) 缓存,每次调用仍重读前台。原路径 P50 26µs / P99 48µs,命中约 0.1µs |
-| 2.2 | **不实施。** 同步调用 `TriggerPressedAsync` 在调用线程上 P50 27µs / P99 131µs(假输入接收端;首次冷调用 44ms 为 JIT)。移出需要一次线程交接,实测 P50 25µs / P99 74µs,等于把钩子停留时间换成宏启动延迟,P50 无净收益 |
-| 3.1 | **撤回。** 定向唤醒(按对方等待目标判断是否唤醒)通过全部原生回归(连续 8 次),但双宏 1ms 对照中 balanced 模式 P99.9 三轮均更差(485/491/486µs 对基线 409/382/383µs)。extreme 模式噪声过大无结论 |
-| 3.2 | **未实施。** 自旋窗口只在单次运行内增长且有 800/1000µs 上限;当前机器底噪(P99.9 在 6–739µs 间波动)无法分辨其收益,不对尾部关键循环做无法验证的改动 |
-
-### 若要继续
-
-- 2.2 的数据只覆盖托管同步段。若要优化,应缩短同步段本身(日志字符串、CTS/Task 分配、`RunAsync` 同步前缀),而不是移走它。
-- 冷启动首次热键的 44ms JIT 值得单独查:确认应用启动预热是否覆盖了 `MacroPlaybackController.StartRunCore` 路径。
-- 3.1/3.2 需要在安静机器(固定核心、关闭后台负载)上做至少五轮 A/B 才能下结论。
diff --git a/scripts/summarize_end_to_end.py b/scripts/summarize_end_to_end.py
deleted file mode 100644
index c135c34..0000000
--- a/scripts/summarize_end_to_end.py
+++ /dev/null
@@ -1,73 +0,0 @@
-"""Validate complete samples and summarize QPC-based input probes (stdlib only)."""
-import argparse
-import csv
-import hashlib
-import json
-import math
-from pathlib import Path
-
-
-def percentiles(values):
- values = sorted(values)
- if not values:
- raise ValueError("No samples")
- return {**{f"p{p * 100:g}_us": round(values[math.ceil(p * len(values)) - 1], 3)
- for p in (.5, .95, .99, .999)}, "max_us": round(values[-1], 3)}
-
-
-def summarize(path):
- with path.open(encoding="utf-8-sig", newline="") as stream:
- rows = list(csv.DictReader(stream))
- if not rows:
- raise ValueError(f"Empty CSV: {path}")
- if [int(row["sample"]) for row in rows] != list(range(len(rows))):
- raise ValueError(f"Missing/reordered rows: {path}")
- for row in rows:
- if any(int(row[field]) != 1 for field in ("deliveries", "run_ok", "native")):
- raise ValueError(f"Invalid delivery/run/backend: {path}, row={row['sample']}")
- ticks = [int(row[f"{field}_qpc"]) for field in
- ("hook", "dispatch", "worker", "queued", "submit")]
- if ticks[0] <= 0 or ticks != sorted(ticks) or int(row["receiver_qpc"]) < ticks[-1]:
- raise ValueError(f"Invalid timestamps: {path}, row={row['sample']}")
- if int(row["complete_qpc"]) < ticks[-1] or int(row["frequency"]) <= 0:
- raise ValueError(f"Incomplete sample: {path}, row={row['sample']}")
- result = {"file": path.name, "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), "groups": []}
- for source in sorted({row["source"] for row in rows}):
- group = [row for row in rows if row["source"] == source]
- stages = [("hook", "dispatch"), ("hook", "worker"), ("hook", "submit"),
- ("submit", "receiver"), ("hook", "receiver")]
- if source == "synthetic_tagged":
- stages.append(("source", "hook"))
- if "output_hook_qpc" in group[0]:
- if any(int(row["output_hook_qpc"]) <= 0 for row in group):
- raise ValueError(f"Missing output hook: {path}")
- stages += [("submit", "output_hook"), ("output_hook", "receiver")]
- if "output_forwarded_qpc" in group[0]:
- if any(int(row["output_forwarded_qpc"]) <= 0 for row in group):
- raise ValueError(f"Missing hook forwarding result: {path}")
- stages += [("output_hook", "output_forwarded"), ("output_forwarded", "receiver")]
- metrics = {f"{a}_to_{b}": percentiles([
- (int(row[f"{b}_qpc"]) - int(row[f"{a}_qpc"])) * 1e6 / int(row["frequency"])
- for row in group]) for a, b in stages}
- if "native_api_total_us" in group[0]:
- metrics["native_api_total"] = percentiles([int(row["native_api_total_us"]) for row in group])
- result["groups"].append({"source": source, "n": len(group), "metrics": metrics,
- "note": "N < 1000: p99.9 is an order statistic, not a tail estimate"
- if len(group) < 1000 else "nearest-rank percentiles"})
- return result
-
-
-def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("files", type=Path, nargs="+")
- parser.add_argument("--output", type=Path, required=True)
- args = parser.parse_args()
- report = [summarize(path) for path in args.files]
- args.output.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
- for file in report:
- for group in file["groups"]:
- print(file["file"], group["source"], "n=" + str(group["n"]), group["metrics"]["hook_to_receiver"])
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/summarize_single_macro.py b/scripts/summarize_single_macro.py
deleted file mode 100644
index b480522..0000000
--- a/scripts/summarize_single_macro.py
+++ /dev/null
@@ -1,91 +0,0 @@
-"""Summarize paired single-macro probes without third-party dependencies."""
-import argparse
-import csv
-import hashlib
-import json
-import math
-import pathlib
-import re
-
-
-def quantiles(values):
- ordered = sorted(values)
- if not ordered:
- raise ValueError("Empty sample series")
- return {
- **{f"p{p * 100:g}_us": ordered[math.ceil(p * len(ordered)) - 1]
- for p in (0.5, 0.95, 0.99, 0.999)},
- "min_us": ordered[0], "max_us": ordered[-1],
- }
-
-
-def main():
- parser = argparse.ArgumentParser()
- parser.add_argument("directory", type=pathlib.Path)
- args = parser.parse_args()
- directory = args.directory.resolve()
- log_metrics = {}
- for name, dense in (("paired.log", False), ("paired-dense.log", True)):
- key = None
- for line in (directory / name).read_text(encoding="utf-8-sig").splitlines():
- label = re.search(r"variant=(\w+) mode=(\d+) round=(\d+)", line)
- if label:
- variant, mode, repeat = label.groups()
- key = f"{'dense' if dense else 'paired'}-{variant}-mode{mode}-round{repeat}.csv"
- elif line.startswith("fake input;") and key:
- log_metrics[key] = {k: float(v) for k, v in re.findall(r"(\w+)=(-?[\d.]+)", line)}
- records = []
- for pattern, kind in (("paired-*-mode*-round*.csv", "periodic"),
- ("dense-*-mode*-round*.csv", "dense")):
- for path in sorted(directory.glob(pattern)):
- match = re.fullmatch(r"(?:paired|dense)-(\w+)-mode(\d+)-round(\d+)\.csv", path.name)
- if not match:
- continue
- variant, mode, repeat = match.groups()
- with path.open(newline="", encoding="utf-8-sig") as stream:
- samples = list(csv.DictReader(stream))
- metrics = log_metrics[path.name]
- if len(samples) != int(metrics["events"]) or [int(s["batch"]) for s in samples] != list(range(len(samples))):
- raise ValueError(f"Missing or reordered samples: {path.name}")
- signed = [(int(s["submit_qpc"]) - int(s["due_qpc"])) * 1e6 / int(s["frequency"])
- for s in samples]
- records.append({
- "file": path.name, "kind": kind, "variant": variant,
- "mode": int(mode), "round": int(repeat), "n": len(samples),
- "sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
- "signed_error": quantiles(signed),
- "early": sum(x < 0 for x in signed),
- "over_100_us": sum(x > 100 for x in signed),
- "over_250_us": sum(x > 250 for x in signed),
- "over_1000_us": sum(x > 1000 for x in signed),
- "process_cpu_ms": metrics["cpu_ms"], "wall_ms": metrics["wall_ms"],
- "startup_us": metrics["startup_us"],
- })
- groups = []
- for kind in ("periodic", "dense"):
- for mode in (1, 2):
- for variant in ("baseline", "final"):
- rows = [r for r in records if (r["kind"], r["mode"], r["variant"]) == (kind, mode, variant)]
- if not rows:
- continue
- groups.append({
- "kind": kind, "mode": mode, "variant": variant,
- "runs": len(rows), "n": sum(r["n"] for r in rows),
- "p999_range_us": [min(r["signed_error"]["p99.9_us"] for r in rows), max(r["signed_error"]["p99.9_us"] for r in rows)],
- "maximum_us": max(r["signed_error"]["max_us"] for r in rows),
- "early": sum(r["early"] for r in rows),
- "over_1000_us": sum(r["over_1000_us"] for r in rows),
- "mean_process_cpu_ms": sum(r["process_cpu_ms"] for r in rows) / len(rows),
- "mean_wall_ms": sum(r["wall_ms"] for r in rows) / len(rows),
- })
- binaries = {name: hashlib.sha256((directory / name).read_bytes()).hexdigest()
- for name in ("baseline.exe", "final.exe")}
- result = {"percentile": "nearest-rank from raw QPC; signed error retained",
- "note": "Fake input. Dense elapsed time measures throughput, not periodic jitter.",
- "binary_sha256": binaries, "groups": groups, "runs": records}
- (directory / "summary.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
- print(json.dumps(groups, indent=2))
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/summarize_tail_comparison.py b/scripts/summarize_tail_comparison.py
deleted file mode 100644
index 83679e7..0000000
--- a/scripts/summarize_tail_comparison.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""Summarize repeated native deadline probes, retaining maximums and early events."""
-import argparse
-import csv
-import hashlib
-import json
-import re
-from pathlib import Path
-from summarize_end_to_end import percentiles
-
-
-def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("directory", type=Path)
- parser.add_argument("--log", required=True)
- parser.add_argument("--pattern", required=True, help="CSV filename with {variant} and {round}")
- parser.add_argument("--output", required=True)
- args = parser.parse_args()
- reports, groups = [], {}
- key = None
- for line in (args.directory / args.log).read_text(encoding="utf-8-sig").splitlines():
- label = re.fullmatch(r"variant=(\w+) round=(\d+) mask=(\S+)", line)
- if label:
- key = label.groups()
- elif line.startswith("fake input;") and key:
- variant, repeat, mask = key
- metrics = {k: float(v) for k, v in re.findall(r"(\w+)=(-?[\d.]+)", line)}
- path = args.directory / args.pattern.format(variant=variant, round=repeat)
- with path.open(encoding="utf-8-sig", newline="") as stream:
- samples = list(csv.DictReader(stream))
- if len(samples) != int(metrics["events"]) or [int(s["batch"]) for s in samples] != list(range(len(samples))):
- raise ValueError(f"Missing/reordered events: {path}")
- errors = [(int(s["submit_qpc"]) - int(s["due_qpc"])) * 1e6 / int(s["frequency"])
- for s in samples]
- report = {"file": path.name, "variant": variant, "round": int(repeat), "mask": mask,
- "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), "n": len(errors),
- "metrics": percentiles(errors), "early": sum(e < 0 for e in errors),
- "over_100_us": sum(e > 100 for e in errors),
- "over_1000_us": sum(e > 1000 for e in errors), "process": metrics}
- reports.append(report)
- groups.setdefault(variant, []).extend(errors)
- key = None
- if not reports or key:
- raise ValueError("No complete benchmark results or incomplete final round")
- pooled = []
- for variant, errors in groups.items():
- runs = [r for r in reports if r["variant"] == variant]
- pooled.append({"variant": variant, "n": len(errors), "rounds": len(runs),
- "metrics": percentiles(errors), "early": sum(e < 0 for e in errors),
- "over_100_us": sum(e > 100 for e in errors),
- "over_1000_us": sum(e > 1000 for e in errors),
- "round_max_us": [r["metrics"]["max_us"] for r in runs],
- "startup_us": [r["process"]["startup_us"] for r in runs],
- "logical_cores_cpu": sum(r["process"]["cpu_ms"] for r in runs) /
- sum(r["process"]["wall_ms"] for r in runs)})
- output = {"measurement": "QPC immediately before fake input; not target receive latency",
- "percentiles": "nearest-rank; maxima are observations, not bounds", "rounds": reports, "pooled": pooled}
- (args.directory / args.output).write_text(json.dumps(output, indent=2) + "\n", encoding="utf-8")
- for group in pooled:
- print(group["variant"], "n=" + str(group["n"]), group["metrics"], "round_max", group["round_max_us"])
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/tools/EndToEndProbe/EndToEndProbe.csproj b/src/tools/EndToEndProbe/EndToEndProbe.csproj
deleted file mode 100644
index 8760836..0000000
--- a/src/tools/EndToEndProbe/EndToEndProbe.csproj
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
- Exe
- net8.0-windows
- true
- enable
- enable
- $(DefineConstants);MACROHID_HOTKEY_PROBE
-
-
-
-
-
-
-
diff --git a/src/tools/EndToEndProbe/NativeReceiver.cs b/src/tools/EndToEndProbe/NativeReceiver.cs
deleted file mode 100644
index 4f219ae..0000000
--- a/src/tools/EndToEndProbe/NativeReceiver.cs
+++ /dev/null
@@ -1,115 +0,0 @@
-using System.Diagnostics;
-using System.IO.MemoryMappedFiles;
-using System.Runtime.InteropServices;
-
-// A separate process with a plain GetMessage loop. This removes the WPF
-// dispatcher's workload from the receiver comparison, not from the hotkey hook.
-internal static class NativeReceiver
-{
- internal static void Run(string name)
- {
- using var memory = MemoryMappedFile.OpenExisting(name);
- using var view = memory.CreateViewAccessor();
- IntPtr label = IntPtr.Zero;
- int doneTicks = 0;
- string previous = "";
- WindowProc callback = (hwnd, message, w, l) =>
- {
- if (message == 0x100 && w.ToInt64() == 0x87)
- {
- long tick = Stopwatch.GetTimestamp();
- int row = view.ReadInt32(0);
- if (row >= 0 && row < view.ReadInt32(44))
- {
- long at = 128 + (long)row * 128;
- int count = view.ReadInt32(at + 72);
- view.Write(at + 72, count + 1);
- if (count == 0) { view.Write(at + 48, tick); view.Write(4, view.ReadInt32(4) + 1); }
- }
- }
- if (message == 0x113) // WM_TIMER; no per-input UI work
- {
- int phase = view.ReadInt32(48), received = view.ReadInt32(4);
- string text = phase switch
- {
- 0 => "请点击此窗口,将它置于前台。正在预热,请先不要按 F8。",
- 1 => $"请按下并松开 F8 共 {view.ReadInt32(52)} 次,每次间隔约半秒。\r\n已接收 {received} 次。请保持此窗口为前台。",
- 2 => $"正在自动测试,请不要切换窗口。\r\n已接收 {received} 次。",
- 3 => "测试完成,结果已保存。窗口即将关闭。",
- _ => "测试未完成,请查看错误记录。窗口即将关闭。"
- };
- if (text != previous) { SetWindowText(label, text); previous = text; }
- if (view.ReadInt32(8) != 0 && ++doneTicks >= 30) DestroyWindow(hwnd);
- return IntPtr.Zero;
- }
- if (message == 0x2) { view.Write(24, 1); PostQuitMessage(0); return IntPtr.Zero; }
- return DefWindowProc(hwnd, message, w, l);
- };
- string className = "MacroHID.Receiver." + Guid.NewGuid().ToString("N");
- var cls = new WindowClass { Size = (uint)Marshal.SizeOf(),
- Proc = Marshal.GetFunctionPointerForDelegate(callback), Instance = GetModuleHandle(null),
- Cursor = LoadCursor(IntPtr.Zero, new IntPtr(32512)), Background = new IntPtr(6), Name = className };
- if (RegisterClassEx(ref cls) == 0) throw new InvalidOperationException("Cannot register receiver window");
- IntPtr window = IntPtr.Zero;
- try
- {
- window = CreateWindowEx(8, className, "MacroHID 真实热键接收测试(Win32)", 0x10CF0000,
- 240, 240, 800, 300, IntPtr.Zero, IntPtr.Zero, cls.Instance, IntPtr.Zero);
- if (window == IntPtr.Zero) throw new InvalidOperationException("Cannot create receiver window");
- label = CreateWindowEx(0, "STATIC", "正在准备测试……", 0x50000000,
- 24, 40, 720, 180, window, IntPtr.Zero, cls.Instance, IntPtr.Zero);
- if (label == IntPtr.Zero || SetTimer(window, new UIntPtr(1), 100, IntPtr.Zero) == UIntPtr.Zero)
- throw new InvalidOperationException("Cannot initialize receiver UI");
- view.Write(16, window.ToInt64()); view.Write(12, 1);
- SetForegroundWindow(window);
- int result;
- while ((result = GetMessage(out var message, IntPtr.Zero, 0, 0)) > 0)
- { TranslateMessage(ref message); DispatchMessage(ref message); }
- if (result < 0) throw new InvalidOperationException("Receiver message loop failed");
- }
- finally
- {
- if (window != IntPtr.Zero) DestroyWindow(window);
- UnregisterClass(className, cls.Instance);
- GC.KeepAlive(callback);
- }
- }
-
- [UnmanagedFunctionPointer(CallingConvention.Winapi)]
- private delegate IntPtr WindowProc(IntPtr hwnd, uint message, IntPtr w, IntPtr l);
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
- private struct WindowClass
- {
- public uint Size, Style;
- public IntPtr Proc;
- public int ClassExtra, WindowExtra;
- public IntPtr Instance, Icon, Cursor, Background;
- public string? Menu, Name;
- public IntPtr SmallIcon;
- }
- [StructLayout(LayoutKind.Sequential)]
- private struct Message
- {
- public IntPtr Hwnd;
- public uint Id;
- public UIntPtr W;
- public IntPtr L;
- public uint Time;
- public int X, Y;
- public uint Private;
- }
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string? name);
- [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern ushort RegisterClassEx(ref WindowClass cls);
- [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool UnregisterClass(string name, IntPtr instance);
- [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr CreateWindowEx(uint ex, string cls, string title, uint style, int x, int y, int width, int height, IntPtr parent, IntPtr menu, IntPtr instance, IntPtr parameter);
- [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr DefWindowProc(IntPtr hwnd, uint message, IntPtr w, IntPtr l);
- [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr LoadCursor(IntPtr instance, IntPtr name);
- [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool SetWindowText(IntPtr hwnd, string text);
- [DllImport("user32.dll")] private static extern UIntPtr SetTimer(IntPtr hwnd, UIntPtr id, uint milliseconds, IntPtr callback);
- [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hwnd);
- [DllImport("user32.dll")] private static extern bool DestroyWindow(IntPtr hwnd);
- [DllImport("user32.dll")] private static extern void PostQuitMessage(int code);
- [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetMessage(out Message message, IntPtr hwnd, uint minimum, uint maximum);
- [DllImport("user32.dll")] private static extern bool TranslateMessage(ref Message message);
- [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr DispatchMessage(ref Message message);
-}
diff --git a/src/tools/EndToEndProbe/Program.cs b/src/tools/EndToEndProbe/Program.cs
deleted file mode 100644
index 1cda990..0000000
--- a/src/tools/EndToEndProbe/Program.cs
+++ /dev/null
@@ -1,294 +0,0 @@
-using System.Collections.Concurrent;
-using System.Diagnostics;
-using System.Diagnostics.Tracing;
-using System.IO;
-using System.IO.MemoryMappedFiles;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Interop;
-using System.Windows.Threading;
-using MacroHid.Core;
-using MacroHid.Runtime;
-using MacroStudio;
-
-internal static class Program
-{
- private const int Header = 128, Stride = 128;
- private const ulong Tag = 0x4D485052;
- private static long Pos(int row, int field) => Header + (long)row * Stride + field;
-
- [STAThread]
- private static int Main(string[] args)
- {
- try
- {
- if (args.Length > 0 && args[0] == "--receiver")
- {
- if (args.Length > 2 && args[2] == "win32") NativeReceiver.Run(args[1]);
- else RunReceiver(args[1]);
- return 0;
- }
- return RunProbe(args).GetAwaiter().GetResult();
- }
- catch (Exception ex) { Console.Error.WriteLine(ex); return 1; }
- finally { NativePlaybackWarmup.Shutdown(); }
- }
-
- private static void RunReceiver(string name)
- {
- using var memory = MemoryMappedFile.OpenExisting(name);
- using var view = memory.CreateViewAccessor();
- var text = new TextBlock { FontSize = 22, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(24) };
- var window = new Window { Title = "MacroHID 真实热键接收测试", Width = 800, Height = 300,
- Topmost = true, WindowStartupLocation = WindowStartupLocation.CenterScreen, Content = text };
- window.SourceInitialized += (_, _) =>
- {
- var handle = new WindowInteropHelper(window).Handle;
- HwndSource.FromHwnd(handle).AddHook((IntPtr hwnd, int message, IntPtr w, IntPtr l, ref bool handled) =>
- {
- if (message == 0x100 && w.ToInt64() == 0x87) // WM_KEYDOWN / F24
- {
- long received = Stopwatch.GetTimestamp();
- int row = view.ReadInt32(0);
- if (row >= 0 && row < view.ReadInt32(44))
- {
- long at = Pos(row, 72);
- int previous = view.ReadInt32(at);
- view.Write(at, previous + 1);
- if (previous == 0)
- {
- view.Write(Pos(row, 48), received);
- view.Write(4, view.ReadInt32(4) + 1);
- }
- }
- }
- return IntPtr.Zero;
- });
- view.Write(16, handle.ToInt64());
- view.Write(12, 1);
- Native.SetForegroundWindow(handle);
- };
- window.Closing += (_, _) => view.Write(24, 1);
- var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
- int doneTicks = 0;
- timer.Tick += (_, _) =>
- {
- int phase = view.ReadInt32(48), manual = view.ReadInt32(52), auto = view.ReadInt32(56);
- text.Text = phase switch
- {
- 0 => "请点击此窗口,把它置于前台。正在预热测试,请先不要按 F8。",
- 1 => $"请按下并松开 F8 共 {manual} 次,每次间隔约半秒。\n已接收 {view.ReadInt32(4)} 次。请保持此窗口为前台。",
- 2 => $"物理热键测试已完成,正在自动测试 {auto} 次。\n已接收 {view.ReadInt32(4)} 次。请不要按 F8 或切换窗口。",
- 3 => "测试已完成,结果已保存。此窗口即将自动关闭。",
- _ => "测试未完成,请查看记录的错误。此窗口即将关闭。"
- };
- if (view.ReadInt32(8) != 0 && ++doneTicks >= 30) window.Close();
- };
- timer.Start();
- new Application().Run(window);
- }
-
- private static async Task RunProbe(string[] args)
- {
- string Option(string key, string fallback)
- { int at = Array.IndexOf(args, key); return at >= 0 && at + 1 < args.Length ? args[at + 1] : fallback; }
- int manual = int.Parse(Option("--physical", "30")), automatic = int.Parse(Option("--automatic", "2000"));
- int timeout = int.Parse(Option("--timeout", "180"));
- string output = Path.GetFullPath(Option("--output", "end-to-end.csv"));
- string receiverKind = Option("--receiver-kind", "wpf");
- if (receiverKind is not ("wpf" or "win32")) throw new ArgumentException("Unknown receiver kind");
- if (manual < 0 || automatic < 0 || manual + automatic < 1 || manual + automatic > 100000 || timeout < 1)
- throw new ArgumentException("Invalid sample counts or timeout.");
- Directory.CreateDirectory(Path.GetDirectoryName(output)!);
- string name = "Local\\MacroHID-E2E-" + Guid.NewGuid().ToString("N");
- using var memory = MemoryMappedFile.CreateNew(name, Header + (long)(manual + automatic) * Stride);
- using var view = memory.CreateViewAccessor();
- view.Write(0, -1); view.Write(44, manual + automatic); view.Write(52, manual); view.Write(56, automatic);
- var info = new ProcessStartInfo(Environment.ProcessPath!) { UseShellExecute = false, CreateNoWindow = true };
- info.ArgumentList.Add("--receiver"); info.ArgumentList.Add(name); info.ArgumentList.Add(receiverKind);
- using var receiver = Process.Start(info) ?? throw new InvalidOperationException("Cannot start receiver");
- var document = McrxParser.Parse("{\"steps\":[{\"type\":\"key.down\",\"key\":\"F24\"},{\"type\":\"wait\",\"ms\":5},{\"type\":\"key.up\",\"key\":\"F24\"}]}");
- var options = new PlaybackExecutionOptions(PlaybackMode.FixedCount, 1, PixelEvaluationMode.Skip, false, PrecisionMode.UltraLowJitter);
- var sink = new SendInputMacroSink();
- var executor = new MacroPlaybackExecutor(sink);
- await using var controller = new MacroPlaybackController(document, executor, options);
- using var trace = new TraceRecorder(view);
- using var hook = new GlobalKeyboardHook();
- var completions = new TaskCompletionSource?[manual + automatic];
- var routeGate = new object();
- int accepted = 0, automaticPhase = 0, physicalReleases = 0;
- long sourceTick = 0;
- TaskCompletionSource? pending = null;
- var deadline = Stopwatch.StartNew();
- IntPtr target = IntPtr.Zero;
- void RequireTarget()
- {
- if (view.ReadInt32(24) != 0 || receiver.HasExited || Native.GetForegroundWindow() != target)
- throw new InvalidOperationException("Receiver closed or lost foreground focus; stopped before further injection.");
- }
- async Task Complete(int row, Task task)
- {
- try
- {
- var result = await task.ConfigureAwait(false);
- view.Write(Pos(row, 80), result.Status == PlaybackRunStatus.Completed && !result.Cancelled ? 1 : 0);
- view.Write(Pos(row, 84), result.InputStats?.BackendName.StartsWith("native", StringComparison.Ordinal) == true ? 1 : 0);
- view.Write(Pos(row, 96), result.InputStats?.NativeSubmitDurationMicroseconds ?? 0);
- view.Write(Pos(row, 104), result.InputStats?.EngineWakeCostMicroseconds ?? 0);
- view.Write(Pos(row, 112), result.InputStats?.MaxLateMicroseconds ?? 0);
- completions[row]!.TrySetResult(result);
- }
- catch (Exception ex) { completions[row]!.TrySetException(ex); }
- }
- hook.TriggerPressed += (_, e) =>
- {
- lock (routeGate)
- {
- bool isAutomatic = Volatile.Read(ref automaticPhase) != 0;
- if (e.ProbeInjected != isAutomatic) return;
- if (target == IntPtr.Zero || Native.GetForegroundWindow() != target)
- { view.Write(40, view.ReadInt32(40) + 1); return; }
- if (accepted >= completions.Length || (!isAutomatic && accepted >= manual)) return;
- if (controller.Status is PlaybackStatus.Running or PlaybackStatus.Stopping
- || (accepted > 0 && view.ReadInt64(Pos(accepted - 1, 48)) == 0))
- { view.Write(36, view.ReadInt32(36) + 1); return; }
- int row = accepted++;
- completions[row] = new(TaskCreationOptions.RunContinuationsAsynchronously);
- view.Write(0, row); view.Write(32, accepted);
- view.Write(Pos(row, 0), isAutomatic ? Volatile.Read(ref sourceTick) : 0);
- view.Write(Pos(row, 8), e.CallbackQpc); view.Write(Pos(row, 76), e.ProbeInjected ? 1 : 0);
- _ = controller.TriggerPressedAsync(e.CallbackQpc);
- _ = Complete(row, controller.WhenIdleAsync());
- pending?.TrySetResult(row);
- }
- };
- hook.TriggerReleased += (_, _) =>
- {
- if (Volatile.Read(ref automaticPhase) == 0) Interlocked.Increment(ref physicalReleases);
- };
- hook.ProbeInputObserved += (key, down, tick) =>
- {
- int row = view.ReadInt32(0);
- if (key == 0x87 && down && row >= 0) view.Write(Pos(row, 88), tick);
- };
- hook.ProbeOutputForwarded += tick =>
- {
- int row = view.ReadInt32(0);
- if (row >= 0) view.Write(Pos(row, 120), tick);
- };
- async Task WaitForRow(int row)
- {
- var task = completions[row]?.Task ?? throw new InvalidOperationException("Missing triggered run");
- await task.WaitAsync(TimeSpan.FromSeconds(3));
- var wait = Stopwatch.StartNew();
- while (view.ReadInt64(Pos(row, 48)) == 0 && wait.Elapsed.TotalSeconds < 3) await Task.Delay(1);
- if (view.ReadInt64(Pos(row, 48)) == 0 || view.ReadInt32(Pos(row, 72)) != 1
- || view.ReadInt32(Pos(row, 80)) != 1 || view.ReadInt32(Pos(row, 84)) != 1)
- throw new InvalidOperationException($"Missing, duplicate, failed or non-native sample {row}");
- }
- try
- {
- while (view.ReadInt32(12) == 0 && deadline.Elapsed.TotalSeconds < timeout) await Task.Delay(20);
- target = new IntPtr(view.ReadInt64(16));
- if (target == IntPtr.Zero) throw new InvalidOperationException("Receiver did not initialize");
- executor.Prepare(document, options);
- Console.WriteLine("WINDOW_READY: click the MacroHID receiver window; waiting for foreground.");
- while (Native.GetForegroundWindow() != target && deadline.Elapsed.TotalSeconds < timeout)
- { if (receiver.HasExited) throw new InvalidOperationException("Receiver closed"); await Task.Delay(50); }
- RequireTarget();
- // Warm the actual playback path against our foreground receiver.
- // These untriggered runs are excluded from both measured groups.
- for (int i = 0; i < 10; i++)
- { RequireTarget(); _ = controller.TriggerPressedAsync(); await controller.WhenIdleAsync(); }
- hook.Start(new HotkeyGesture(HidModifier.None, HidKey.F8));
- view.Write(48, 1);
- Console.WriteLine($"PHYSICAL_READY: press/release F8 {manual} times, about 0.5s apart; target pid={receiver.Id}, kind={receiverKind}.");
- while (Volatile.Read(ref accepted) < manual && deadline.Elapsed.TotalSeconds < timeout)
- { if (receiver.HasExited) throw new InvalidOperationException("Receiver closed"); await Task.Delay(20); }
- if (Volatile.Read(ref accepted) < manual) throw new TimeoutException("Physical sample collection timed out");
- for (int i = 0; i < manual; i++) await WaitForRow(i);
- while (Volatile.Read(ref physicalReleases) < manual && deadline.Elapsed.TotalSeconds < timeout) await Task.Delay(10);
- if (Volatile.Read(ref physicalReleases) < manual) throw new TimeoutException("Final physical hotkey is still held");
- Volatile.Write(ref automaticPhase, 1);
- hook.ProbeInjectedInputTag = new IntPtr((long)Tag);
- view.Write(48, 2);
- for (int i = 0; i < automatic; i++)
- {
- RequireTarget();
- pending = new(TaskCreationOptions.RunContinuationsAsynchronously);
- Volatile.Write(ref sourceTick, Stopwatch.GetTimestamp());
- Native.SendProbeHotkey(Tag);
- int row = await pending.Task.WaitAsync(TimeSpan.FromSeconds(3));
- await WaitForRow(row);
- await Task.Delay(10);
- }
- view.Write(48, 3);
- Console.WriteLine($"PASS: physical={manual}, synthetic={automatic}, receiver_pid={receiver.Id}, CSV={output}");
- return 0;
- }
- catch { view.Write(48, 4); throw; }
- finally
- {
- hook.Stop();
- await controller.StopAsync();
- using var file = new StreamWriter(output, false, new UTF8Encoding(false));
- file.WriteLine("sample,source,source_qpc,hook_qpc,dispatch_qpc,worker_qpc,queued_qpc,submit_qpc,receiver_qpc,complete_qpc,frequency,run_id,deliveries,run_ok,native,output_hook_qpc,native_api_total_us,engine_wake_us,max_scheduler_late_us,output_forwarded_qpc");
- for (int i = 0; i < Volatile.Read(ref accepted); i++)
- {
- var ticks = Enumerable.Range(0, 8).Select(n => view.ReadInt64(Pos(i, n * 8))).ToArray();
- file.WriteLine($"{i},{(view.ReadInt32(Pos(i, 76)) == 0 ? "physical_manual" : "synthetic_tagged")},{string.Join(',', ticks)},{Stopwatch.Frequency},{view.ReadInt64(Pos(i, 64))},{view.ReadInt32(Pos(i, 72))},{view.ReadInt32(Pos(i, 80))},{view.ReadInt32(Pos(i, 84))},{view.ReadInt64(Pos(i, 88))},{view.ReadInt64(Pos(i, 96))},{view.ReadInt64(Pos(i, 104))},{view.ReadInt64(Pos(i, 112))},{view.ReadInt64(Pos(i, 120))}");
- }
- view.Write(8, 1);
- if (!await Task.Run(() => receiver.WaitForExit(5000))) receiver.Kill();
- Console.WriteLine($"accepted={accepted}, rejected_busy={view.ReadInt32(36)}, rejected_focus={view.ReadInt32(40)}");
- }
- }
-
- private sealed class TraceRecorder(MemoryMappedViewAccessor view) : EventListener
- {
- private readonly ConcurrentDictionary rows = new();
- protected override void OnEventSourceCreated(EventSource source)
- { if (source.Name == "MacroHid-Playback") EnableEvents(source, EventLevel.Informational); }
- protected override void OnEventWritten(EventWrittenEventArgs args)
- {
- if (args.EventId != 1 || args.Payload is null) return;
- long run = (long)args.Payload[0]!, tick = (long)args.Payload[2]!;
- var stage = (PlaybackTraceStage)(int)args.Payload[1]!;
- if (stage == PlaybackTraceStage.Hotkey)
- {
- int row = view.ReadInt32(0);
- if (row < 0) return;
- rows[run] = row; view.Write(Pos(row, 64), run);
- }
- if (!rows.TryGetValue(run, out int index)) return;
- int field = stage switch
- {
- PlaybackTraceStage.Dispatch => 16, PlaybackTraceStage.WorkerStart => 24,
- PlaybackTraceStage.NativeQueued => 32, PlaybackTraceStage.NativeFirstSubmit => 40,
- PlaybackTraceStage.CleanupComplete => 56, _ => -1
- };
- if (field >= 0) view.Write(Pos(index, field), tick);
- }
- }
-
- private static class Native
- {
- [DllImport("user32.dll")] internal static extern IntPtr GetForegroundWindow();
- [DllImport("user32.dll")] internal static extern bool SetForegroundWindow(IntPtr window);
- [StructLayout(LayoutKind.Sequential)] private struct Keyboard
- { public ushort Key, Scan; public uint Flags, Time; public UIntPtr Extra; }
- [StructLayout(LayoutKind.Explicit, Size = 32)] private struct Union
- { [FieldOffset(0)] public Keyboard Keyboard; }
- [StructLayout(LayoutKind.Sequential)] private struct Input
- { public uint Type; public Union Union; }
- [DllImport("user32.dll", SetLastError = true)] private static extern uint SendInput(uint count, Input[] inputs, int size);
- internal static void SendProbeHotkey(ulong tag)
- {
- Input[] inputs = [new() { Type = 1, Union = new() { Keyboard = new() { Key = 0x77, Extra = new UIntPtr(tag) } } },
- new() { Type = 1, Union = new() { Keyboard = new() { Key = 0x77, Flags = 2, Extra = new UIntPtr(tag) } } }];
- if (SendInput(2, inputs, Marshal.SizeOf()) != 2) throw new InvalidOperationException("Hotkey SendInput failed");
- }
- }
-}
diff --git a/src/tools/FufuTargetProbe/FufuTargetProbe.csproj b/src/tools/FufuTargetProbe/FufuTargetProbe.csproj
deleted file mode 100644
index af834d2..0000000
--- a/src/tools/FufuTargetProbe/FufuTargetProbe.csproj
+++ /dev/null
@@ -1,3 +0,0 @@
-
-Exenet8.0-windowsenableenableapp.manifest
-
diff --git a/src/tools/FufuTargetProbe/Program.cs b/src/tools/FufuTargetProbe/Program.cs
deleted file mode 100644
index f570f99..0000000
--- a/src/tools/FufuTargetProbe/Program.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using System.Text.Json;
-
-Console.OutputEncoding = System.Text.Encoding.UTF8;
-Console.WriteLine("Fufu 帧率地址对照工具:只读内存,不修改游戏或 DLL。\n请保持同一游戏进程运行,关闭 Macro 的锁帧操作。");
-string output = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MacroStudio", "fufu-target-probe");
-Directory.CreateDirectory(output);
-nint handle = 0;
-try
-{
- using var game = Process.GetProcessesByName("YuanShen").Concat(Process.GetProcessesByName("GenshinImpact")).Single();
- var modules = game.Modules.Cast().Select(m => new Module(m.ModuleName, m.FileName, m.BaseAddress.ToInt64(), m.ModuleMemorySize)).ToArray();
- handle = Native.OpenProcess(0x410, false, game.Id); // Query + read only; no write rights.
- if (handle == 0) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
- Console.Write("确认游戏当前稳定为 178 FPS 后,按回车记录:"); Console.ReadLine();
- var found = new List(); long scanned = 0, failed = 0;
- var buffer = new byte[1024 * 1024];
- for (long cursor = 0x10000; ;)
- {
- if (Native.VirtualQueryEx(handle, (nint)cursor, out var info, (nuint)Marshal.SizeOf()) == 0) break;
- long start = info.BaseAddress.ToInt64();
- if (info.RegionSize > long.MaxValue || start > long.MaxValue - (long)info.RegionSize) break;
- long end = start + (long)info.RegionSize;
- if (end <= cursor) break;
- bool writable = info.State == 0x1000 && (info.Protect & 0x101) == 0 && (info.Protect & 0xCC) != 0;
- if (writable)
- {
- for (long address = start; address < end; address += buffer.Length)
- {
- int size = (int)Math.Min(buffer.Length, end - address);
- if (!Native.ReadProcessMemory(handle, (nint)address, buffer, (nuint)size, out var count)) { failed += size; continue; }
- int available = checked((int)count); scanned += available;
- for (int offset = (int)((4 - address % 4) % 4); offset + 4 <= available; offset += 4)
- if (BitConverter.ToInt32(buffer, offset) == 178) found.Add(address + offset);
- }
- }
- cursor = end;
- }
- string identity = $"{DateTime.Now:yyyyMMdd-HHmmss}-pid{game.Id}";
- string baselinePath = Path.Combine(output, identity + "-178.json");
- File.WriteAllText(baselinePath, JsonSerializer.Serialize(new { pid = game.Id, time = DateTime.Now, scannedBytes = scanned, unreadableBytes = failed, modules, addresses = found.Select(Describe).ToArray() }, new JsonSerializerOptions { WriteIndented = true }));
- Console.WriteLine($"已保存 178 快照:{found.Count} 个候选;已读 {scanned / 1024 / 1024} MB。\n{baselinePath}\n不要退出游戏。现在去芙芙工具箱改为 143,并确认实际帧率随之变化。");
- Console.Write("实际已稳定为 143 后,回到这里按回车:"); Console.ReadLine();
- if (game.HasExited) throw new InvalidOperationException("游戏进程已退出,快照不能继续比较。");
- var matches = Filter(found, 143);
- string firstPath = Path.Combine(output, identity + "-143.json");
- File.WriteAllText(firstPath, JsonSerializer.Serialize(matches.Select(Describe).ToArray(), new JsonSerializerOptions { WriteIndented = true }));
- Console.WriteLine($"178 → 143 同步变化的地址:{matches.Count} 个。\n现在再用芙芙工具箱改为 167,并确认实际帧率变化。");
- Console.Write("实际已稳定为 167 后,按回车:"); Console.ReadLine();
- if (game.HasExited) throw new InvalidOperationException("游戏进程已退出。");
- matches = Filter(matches, 167);
- string result = Path.Combine(output, identity + "-confirmed.json");
- File.WriteAllText(result, JsonSerializer.Serialize(matches.Select(Describe).ToArray(), new JsonSerializerOptions { WriteIndented = true }));
- Console.WriteLine($"两次同步变化的候选:{matches.Count} 个。结果:{result}\n结果仍需核对代码用途,不能直接认定为控制字段。可以在芙芙工具箱恢复原目标 178。");
- List Filter(List candidates, int target)
- {
- var bytes = new byte[4]; var result = new List();
- foreach (var candidate in candidates)
- if (Native.ReadProcessMemory(handle, (nint)candidate, bytes, 4, out var read) && read == 4 && BitConverter.ToInt32(bytes) == target) result.Add(candidate);
- return result;
- }
- object Describe(long address)
- {
- var module = modules.FirstOrDefault(m => address >= m.Base && address - m.Base < m.Size);
- Native.VirtualQueryEx(handle, (nint)address, out var region, (nuint)Marshal.SizeOf());
- return new { address = $"0x{address:X}", module = module?.Name, modulePath = module?.Path, rva = module == null ? null : $"0x{address - module.Base:X}", allocation = $"0x{region.AllocationBase:X}", regionType = $"0x{region.Type:X}" };
- }
-}
-catch (Exception ex) { Console.WriteLine("未完成:" + ex.Message); }
-finally { if (handle != 0) Native.CloseHandle(handle); }
-Console.WriteLine("按回车退出。"); Console.ReadLine();
-record Module(string Name, string Path, long Base, int Size);
-[StructLayout(LayoutKind.Sequential)]
-struct MemoryInfo { public nint BaseAddress, AllocationBase; public uint AllocationProtect; public nuint RegionSize; public uint State, Protect, Type; }
-static class Native
-{
- [DllImport("kernel32.dll", SetLastError = true)] public static extern nint OpenProcess(uint access, bool inherit, int pid);
- [DllImport("kernel32.dll", SetLastError = true)] public static extern bool ReadProcessMemory(nint process, nint address, byte[] bytes, nuint size, out nuint read);
- [DllImport("kernel32.dll")] public static extern nuint VirtualQueryEx(nint process, nint address, out MemoryInfo info, nuint size);
- [DllImport("kernel32.dll")] public static extern bool CloseHandle(nint handle);
-}
diff --git a/src/tools/FufuTargetProbe/app.manifest b/src/tools/FufuTargetProbe/app.manifest
deleted file mode 100644
index 6fc600e..0000000
--- a/src/tools/FufuTargetProbe/app.manifest
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/tests/MacroHid.Scheduling.Benchmarks/MacroHid.Scheduling.Benchmarks.csproj b/tests/MacroHid.Scheduling.Benchmarks/MacroHid.Scheduling.Benchmarks.csproj
deleted file mode 100644
index 678e43b..0000000
--- a/tests/MacroHid.Scheduling.Benchmarks/MacroHid.Scheduling.Benchmarks.csproj
+++ /dev/null
@@ -1 +0,0 @@
-Exenet8.0-windowsenableenable$(RuntimeRoot)/MacroHid.Core.dll$(RuntimeRoot)/MacroHid.Runtime.dll
diff --git a/tests/MacroHid.Scheduling.Benchmarks/Program.cs b/tests/MacroHid.Scheduling.Benchmarks/Program.cs
deleted file mode 100644
index c1c2b65..0000000
--- a/tests/MacroHid.Scheduling.Benchmarks/Program.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using System.Diagnostics;
-using MacroHid.Core;
-using MacroHid.Runtime;
-var options = new PlaybackExecutionOptions(PlaybackMode.FixedCount,1,PixelEvaluationMode.Skip,false,PrecisionMode.Balanced);
-var simple = McrxParser.Parse("{\"steps\":[{\"type\":\"key.down\",\"key\":\"A\"},{\"type\":\"wait\",\"ms\":3},{\"type\":\"key.up\",\"key\":\"A\"}]}");
-var parallel = new MacroDocument(1,"parallel",[new ParallelStep(new ParallelBranchStep("left",simple.Steps),new ParallelBranchStep("right",simple.Steps))]);
-var longWait = McrxParser.Parse("{\"steps\":[{\"type\":\"key.down\",\"key\":\"A\"},{\"type\":\"wait\",\"ms\":1000},{\"type\":\"key.up\",\"key\":\"A\"}]}");
-var startup = new List(); var skew = new List(); var stops = new List(); var jitter = new List();
-var process=Process.GetCurrentProcess(); int handlesBefore=process.HandleCount;
-int rounds = args.Length > 0 ? int.Parse(args[0]) : 300;
-for(int i=0;i=10) {startup.Add(Ms(times[0]-start));jitter.Add(Math.Abs(Ms(times[1]-times[0])-3));}
- }
- sink=new Probe();
- using(var executor=new MacroPlaybackExecutor(sink)) {
- await executor.RunAsync(parallel,options,CancellationToken.None);
- var times=sink.Times.ToArray();
- if(i>=10) skew.Add(Ms(times[1]-times[0]));
- }
- sink=new Probe();
- using(var executor=new MacroPlaybackExecutor(sink)) {
- using var cts=new CancellationTokenSource();
- var task=executor.RunAsync(longWait,options,cts.Token);
- if(!sink.Started.Wait(2000)) throw new Exception("start timeout");
- long start=Stopwatch.GetTimestamp(); cts.Cancel();
- try {await task;}catch(OperationCanceledException){}
- if(i>=10) stops.Add(Ms(Stopwatch.GetTimestamp()-start));
- }
- if ((i + 1) % (rounds > 300 ? 500 : 100) == 0) {
- GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); process.Refresh();
- Console.WriteLine($"checkpoint runs={i+1}: handles={process.HandleCount}; threads={process.Threads.Count}; privateMiB={process.PrivateMemorySize64/1048576.0:F2}");
- }
-}
-process.Refresh();
-foreach(var pair in new[]{("startup",startup),("parallelSkew",skew),("stop",stops),("3msIntervalAbsoluteError",jitter)}) {
- var values=pair.Item2.Order().ToArray();
- Console.WriteLine($"{pair.Item1}: n={values.Length} p50={Rank(values,.5):F4}ms p95={Rank(values,.95):F4}ms p99={Rank(values,.99):F4}ms p99.9={Rank(values,.999):F4}ms max={values[^1]:F4}ms >0.1ms={Over(values,.1):P2} >0.25ms={Over(values,.25):P2} >1ms={Over(values,1):P2}");
-}
-GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); process.Refresh();
-Console.WriteLine($"handles before={handlesBefore} after={process.HandleCount}; threads={process.Threads.Count}; privateMiB={process.PrivateMemorySize64/1048576.0:F2}");
-static double Ms(long ticks)=>ticks*1000.0/Stopwatch.Frequency;
-// nearest-rank on an ascending array; p99.9 equals max until n >= 1000.
-static double Rank(double[] sorted,double p)=>sorted[Math.Max(1,(int)Math.Ceiling(sorted.Length*p))-1];
-static double Over(double[] sorted,double thresholdMs)=>sorted.Count(v=>v>thresholdMs)/(double)sorted.Length;
-sealed class Probe : IMacroInputSink {
- public readonly System.Collections.Concurrent.ConcurrentQueue Times=new();
- public readonly ManualResetEventSlim Started=new();
- public bool IsAvailable=>true;
- public void Submit(uint sequence,InputAction action) {Times.Enqueue(Stopwatch.GetTimestamp());Started.Set();}
- public InputSubmissionStats? GetStats()=>null;
-}