Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
249818e
Add input API comparison experiment design
NotBadSlime Oct 14, 2026
4472f4b
Add input API comparison implementation plan
NotBadSlime Oct 14, 2026
dd63402
Scaffold InputApiProbe and refine experiment design
NotBadSlime Oct 14, 2026
9036613
Add input API summary script with verdict gate and tests
NotBadSlime Oct 14, 2026
d5372ec
Harden InputApiProbe window and injection primitives
NotBadSlime Oct 14, 2026
cbffd6c
Add four capture strategies to InputApiProbe
NotBadSlime Oct 14, 2026
115c823
Add capture experiment runner with provenance
NotBadSlime Oct 14, 2026
4924910
Add paired physical-key capture comparison
NotBadSlime Oct 14, 2026
9fc67c0
Add Win32 receiver process and empty hook for injection experiment
NotBadSlime Oct 14, 2026
250cc4c
Add injection experiment sender
NotBadSlime Oct 14, 2026
2694d6f
Stop on missing receipts and harden probe cleanup
NotBadSlime Oct 14, 2026
c9df954
Report block CPU/errors, match paired captures by time, judge variant…
NotBadSlime Oct 14, 2026
ac26e8c
Record input API comparison results
NotBadSlime Oct 14, 2026
5a8badd
Add latency follow-up work list
NotBadSlime Oct 14, 2026
bb4fb44
Add raw-diag command to tell undelivered WM_INPUT from filtered keys
NotBadSlime Oct 15, 2026
d7d00cb
Let raw-diag arm other strategies alongside to isolate interference
NotBadSlime Oct 15, 2026
44ca065
Split paired capture into main and hotkey groups and drop auto-repeat…
NotBadSlime Oct 15, 2026
0fd7ddd
Record corrected paired-capture results and the RegisterHotKey/Raw In…
NotBadSlime Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/current-precision-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

最新单宏补充见 [尾部优化与真实接收实测](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 项通过。仍不能声称微秒级端到端或硬实时最大值保证。

2026-10-15 输入 API 对照(低层钩子 / Raw Input / RegisterHotKey / 轮询,以及 SendInput 各写法)的结果见 [输入 API 对照](input-api-comparison-2026-10-15.md):合成输入下没有任何候选优于现有路径,不接入;物理按键并列组(修正后重测,每组 30 次)同样没有候选早于低层钩子。这不是游戏内延迟结论。

三档策略、准备缓存、共享双线程原生调度、图像缓冲优化、界面状态与验收工具已实现。本轮功能回归通过;真实热键端到端、真实 SendInput 接收延迟及游戏帧时间联合验收尚未完成。以下数据不能视为硬实时保证,也不能推广到未测试设备或游戏。

## 2026-10-14 单宏增量
Expand Down
148 changes: 148 additions & 0 deletions docs/input-api-comparison-2026-10-15.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# 输入 API 对照实验结果

日期:2026-10-15。设计见 [输入 API 对照实验设计](superpowers/specs/2026-10-15-input-api-comparison-design.md),计划见 [实现计划](superpowers/plans/2026-10-15-input-api-comparison.md)。工具为 `src/tools/InputApiProbe`,统计为 `scripts/summarize_input_api.py`。

**结论先行:** 在这台机器上,把低层键盘钩子换成 Raw Input、RegisterHotKey 或轮询,没有任何一种在延迟上更好;把 `SendInput` 换成扫描码写法或 `keybd_event` 也没有可测量的差别。所有候选的判定都是「无净收益」,没有任何一个进入「值得接入」名单。用户态 API 替换不是降低端到端延迟的有效手段。

## 环境与口径

- Windows 11 10.0.26200,AMD Ryzen 9 7945HX(32 个逻辑处理器),.NET 8.0.22,电源方案 Turbo。测量时申请 1ms 计时器分辨率。机器上同时运行着 Armoury Crate 和 Razer Synapse 等后台软件,没有关闭。
- 探针二进制 `InputApiProbe.dll` SHA-256:`46B9EB11…E4621B3`,其余指纹见各 `*-provenance.json`。
- 测量的是**合成输入**的延迟。实验 A 为 `SendInput` 调用前到各捕获方式回调入口;实验 B 为 `SendInput` 调用前到另一进程 Win32 窗口(Normal 优先级,`GetMessage` 循环)收到 `WM_KEYDOWN`。合成输入与物理键盘的路径不完全相同,数值只适合候选之间互相比较,不代表物理键盘到回调的延迟,更不代表游戏内延迟。
- 分位数使用 nearest-rank;每个候选五轮,每轮 2,000 次,各轮候选顺序轮换。预热样本不计入。全部样本无丢失、无重复。
- 原始数据保存在本地 `tmp/input-api-20261015/`(不随仓库提交)。SHA-256:
- `capture-samples.csv`:`a5efcd4ecf0608dcb9a6499f4e7f702f1dbc0fac3c937972c92a04e55a7dec57`
- `inject-samples.csv`:`7304e02adf81e0cfe99b281420333544b6bcbe572f4fa2bde29e3da79e8984c0`
- `capture-blocks.csv`:`435bef065c7d655fd7b2cd0e77d3134e492f9f232a1b82b71dc653e6ab96426f`
- `capture-paired.csv`:`aabca4eaea771cfb43c381a282fc74f9cd3698599d2e79d0684715f42dd51cf2`;第一次(按住过久)`capture-paired-run1-held-keys.csv`:`e531461bf21bb2ae14d4c8054f6d18bd63202bb6e66f5defe37f57d9254d3b62`(**第一版并列组,已作废**,见并列组一节)
- 修正后的并列组与 Raw Input 诊断:
- `capture-paired-main.csv`:`9C330FC7EA7D20B4F1952DB194A014ECF9305DF6ED01952E6FD1727D2927014C`
- `capture-paired-hotkey.csv`:`49EE1B088E2C8411E1F62C7812CB4125DFED032C1C46AC68AC8FB0B578063C43`
- `raw-diag-f8-alone.csv`(单独运行、物理 F8):`26C3579E4B3610485EC00568EDD238064C8053958F848B42A6F5F076227F64E5`
- 合成 F24 的 `raw-diag --also` 对照(`raw-diag-synthetic/`,5 次按键四组、20 次按键三组;测试期间键盘上有人打字,所以总记录数多于 F24 的次数,只统计 F24):
- `also-alone.csv`:`81F1B7E2F35A3C5F911A2D60945F391A2CF31A1952A60D2FD7C0940BDC13E28E`
- `also-ll-hook.csv`:`8017CCD0A5AE140A9620AE6D127E073C5AF8FBA5942625070783A57D841E176E`
- `also-poll-spin.csv`:`A9B06B74B9C26C92CEDA53B022732D6F1E50F6D22C49FBCDDF1FD8341569989C`
- `also-register-hotkey.csv`:`E4427736B60383E87B78A499869C825DC308FE01AB58BFE7EB101EF3FBB12538`
- `also20-spin-only.csv`:`54CBEC63F0A695FD1A43D7301E06A6EC4EDF2574DE7463EBBF3E421A64418C04`
- `also20-hotkey+spin.csv`:`7D79F7992F016B6C77B349D0539370E12726864D0B18F3BE3BBB9D6F1DE19C49`
- `also20-all5.csv`:`B195F225AA328F359C98D541E67857D6EA2C913019BB81BAB54044745D3A752F`

## 实验 A:触发捕获(单独组)

每个候选 N=10,000(5 轮 × 2,000),单位 µs。

| 候选 | P50 | P95 | P99 | P99.9 | 最大 | 五轮 P50 范围 | 每 1000 次触发 CPU | 判定 |
|---|---:|---:|---:|---:|---:|---|---:|---|
| ll-hook(基线,生产 `GlobalKeyboardHook`) | 27.7 | 55.4 | 303.7 | 912.4 | 1,180.7 | 25.9–28.7 | 287ms | 基线 |
| raw-input | 182.6 | 313.5 | 862.0 | 1,204.5 | 3,440.7 | 176.0–187.9 | 291ms | 无净收益 |
| register-hotkey | 154.3 | 268.3 | 796.8 | 1,232.5 | 2,039.2 | 149.6–160.7 | 208ms | 无净收益 |
| poll-spin(持续自旋) | 198.2 | 353.8 | 952.6 | 1,282.6 | 2,107.0 | 192.0–215.7 | 10,474ms | 无净收益 |
| poll-1ms(睡眠 1ms) | 1,496.3 | 2,002.9 | 2,009.8 | 2,074.1 | 2,789.3 | 1495.4–1497.2 | 280ms | 无净收益 |

要点:
- 现用低层钩子的 P50 最低,比其他方式低约 125–170µs(poll-1ms 受 `Sleep(1)` 粒度限制,约 1.5ms,符合预期)。基线五轮 P50 极差仅 2.8µs,底噪远小于 100µs 门槛,所以结论不是「无结论」。
- poll-spin 每 1000 次触发占用约 10.5 秒 CPU,等于占满一个逻辑核,且延迟并不更好。
- 低层钩子 P99 与最大值较重,但仍好于其余各方式的同口径数值。
- 此项只比较了到回调入口的时间,没有比较各方式能做到的事情(见语义表)。

### 并列组(物理按键)

目的是用同一次物理 F8 按键同时触发各种方式,比较到达先后。**第一版并列组的数据作废**,原因见下;修正后重测得到有效数据。

#### 第一版为什么作废(原先的解读也有误)

最初两次尝试(低层钩子 32 / 30 次,其余方式约一半,Raw Input 为 0)当时被记作「原因不明」,并且写成「低层钩子正好 30 次,说明按键本身没有问题」。这个判断是错的,实际有两处缺陷:

1. **低层钩子把自动重复也算成按下。** 按住键时 Windows 每约 31ms 重复一次按下,基线因此虚高。核实旧文件:低层钩子 30 次里,相邻间隔小于 100ms 的有 11 个(即自动重复),去掉后为 19 次;另有 3 个间隔在 100–400ms,无法区分是重复还是快速连按。其余方式为 16、15、15 次。**这只能说明基线多算了一部分,不能说明剩下的差异已经完全消除**;旧数据本身不再使用,以重测为准。
2. **RegisterHotKey 会吞掉 Raw Input 的按下。** 用 `raw-diag` 做了单变量对照(合成 F24,每次只改 Raw Input 的伙伴):

| Raw Input 的伙伴 | 按键次数 | Raw Input 收到的按下 | 收到的抬起 |
|---|---:|---:|---:|
| 无(单独) | 5 | 5 | 5 |
| 低层钩子 | 5 | 5 | 5 |
| 自旋轮询 | 5 | 5 | 5 |
| **RegisterHotKey** | 5 | **0** | 5 |
| 自旋轮询(单开) | 20 | 20 | 20 |
| **RegisterHotKey + 自旋轮询** | 20 | **0** | 20 |
| **全部四种(低层钩子、RegisterHotKey、两种轮询)** | 20 | **0** | 20 |

也就是说,被 `RegisterHotKey` 注册的键,Raw Input 只收到抬起、收不到按下;而 Raw Input 的探针只统计按下,所以记成 0。这与「厂商软件吞掉 F8」无关。单独运行时,Raw Input 对物理 F8 完全正常(9 次按键:按下 15 次含自动重复、抬起 9 次,设备 1 个,读取失败 0 次)。

修正:低层钩子和 Raw Input 只计「抬起之后的第一次按下」;并列组拆成两组,`RegisterHotKey` 不再与 Raw Input 同组。

#### 修正后的结果

每组物理 F8 轻点 30 次,每个方式与低层钩子按最近时间戳配对(±50ms),30 对 30,无未匹配;低层钩子相邻捕获最短间隔 523ms,已无自动重复。数值为「该方式的捕获时刻减去低层钩子的捕获时刻」,单位 µs,正数表示更晚。

| 组 | 方式 | n | P50 | P95 | P99 | 最大 |
|---|---|---:|---:|---:|---:|---:|
| main(同组并发) | raw-input | 30 | +527.4 | +1008.2 | +1357.0 | +1357.0 |
| main | poll-spin | 30 | +496.1 | +922.1 | +1326.8 | +1326.8 |
| main | poll-1ms | 30 | +996.8 | +2203.0 | +2467.4 | +2467.4 |
| hotkey(两方式并发) | register-hotkey | 30 | +435.1 | +540.7 | +3169.4 | +3169.4 |

- **方向与单独组一致:每一种都晚于低层钩子,没有任何一种更早。** 物理按键下也没有候选值得接入。
- 数值比单独组大(单独组的 P50 差约为 raw-input +155、poll-spin +170、register-hotkey +127µs)。并发的几个方式互相拖慢(设计文档已说明并列组不与单独组的绝对值混用),自旋轮询还会占满一个核,所以这里**只看先后,不看绝对大小**,也不据此推断物理路径本身更慢。
- n=30、单次会话,不按正式判定门槛(五轮、每轮 2,000 次)判定,P99/最大值只是最大的几个样本,仅供参考。
- 两组并列组使用同一个探针二进制(SHA-256 `02E8E80D…02A02C`),与本文前面实验 A、B 的二进制(`46B9EB11…`)不同,因为之后又改了代码;`raw-diag` 的几次运行是不同时间构建的,各自的指纹见对应的 `*-provenance.json`(`raw-diag-f8-alone` 为 `AFB4AF02…601DAAC`)。实验 A、B 的分位数不受自动重复影响,合成输入是「按下、等待、抬起」,没有按住。

## 实验 B:注入到接收

每个条件 N=10,000,单位 µs。判定对象是同一钩子条件下的 `vk`(见设计文档)。

| 条件 | P50 | P95 | P99 | P99.9 | 最大 | 五轮 P50 范围 | 判定 |
|---|---:|---:|---:|---:|---:|---|---|
| vk \| none(基线) | 766.2 | 1149.9 | 1424.3 | 2026.5 | 6087.2 | 734.8–798.0 | 基线 |
| scan \| none | 756.3 | 1122.0 | 1415.5 | 1944.9 | 2553.8 | 731.9–770.2 | 无净收益 |
| vk-scan \| none | 760.1 | 1115.8 | 1395.8 | 1932.5 | 4234.1 | 720.8–784.3 | 无净收益 |
| keybd-event \| none | 755.6 | 1105.3 | 1395.8 | 1907.7 | 3202.6 | 739.0–761.5 | 无净收益 |
| merged-pair \| none | 865.9 | 1316.4 | 1609.1 | 1994.4 | 3773.9 | 848.8–892.7 | 无净收益 |
| vk \| ll-empty(基线) | 785.7 | 1145.2 | 1460.3 | 1918.0 | 2707.9 | 756.7–815.8 | 基线 |
| scan \| ll-empty | 792.8 | 1175.3 | 1470.5 | 2140.1 | 2634.4 | 752.2–811.4 | 无净收益 |
| vk-scan \| ll-empty | 815.4 | 1172.7 | 1469.0 | 2068.9 | 6118.2 | 781.9–845.9 | 无净收益 |
| keybd-event \| ll-empty | 801.0 | 1171.6 | 1468.8 | 2027.5 | 3685.6 | 789.6–813.1 | 无净收益 |
| merged-pair \| ll-empty | 960.7 | 1512.4 | 1822.3 | 3041.8 | 4200.3 | 938.5–1000.7 | 无净收益 |

- 虚拟键码、扫描码、键码加扫描码、`keybd_event` 四种写法的按下延迟 P50 相差不到 11µs,在底噪范围内,**没有可测量的差别**;五轮 P50 极差在 22–64µs,也小于 100µs 门槛。
- 按下和抬起合并为一次 `SendInput` 的写法,按下消息反而慢约 100µs(P50 865.9 对 766.2)。原因没有深究。
- 键抬起方向(不含 merged-pair):P50 650–670µs(hook=none)、705–718µs(hook=ll-empty),写法之间同样无差别。
- 注入侧存在约 0.75ms 的 P50 下限:从调用 `SendInput` 到 Normal 优先级的 Win32 接收窗口收到消息,与具体 API 写法无关。

### 钩子税(发送进程内是否有空的低层钩子)

池化后的 `ll-empty` 减 `none`,单位 µs;每项只是一个差值,不是分布。

| 变体 | P50 差 | P99 差 |
|---|---:|---:|
| vk | 19.5 | 36.0 |
| scan | 36.5 | 55.0 |
| vk-scan | 55.3 | 73.2 |
| keybd-event | 45.4 | 73.0 |
| merged-pair | 94.8 | 213.2 |

一个什么都不做的低层钩子只给这条路径增加约 20–95µs(P50)。此前端到端实测里 F24 钩子入口到转发返回约 0.87ms,远大于此值;两者条件不同(那一段含有探针自身的记录工作),所以这里**不推断**该 0.87ms 的成因,只说明钩子机制本身的代价较小。

## 语义兼容表

「文档」指依据 Win32 文档,「实测」指本次观察,「未验证」表示本次没有测。

| 方式 | 能否吞键 | 区分注入与物理 | 抬起事件 | 鼠标键 | 修饰键组合 | 需要消息泵 |
|---|---|---|---|---|---|---|
| 低层钩子 | 能(返回非零,文档) | 能(`LLKHF_INJECTED`,实测探针靠 `dwExtraInfo` 区分) | 有 | 有(`WH_MOUSE_LL`) | 自行跟踪状态 | 需要,在安装线程上 |
| Raw Input | 不能(只观察,文档) | 未验证 | 有(`RI_KEY_BREAK`) | 有(鼠标用途页,文档) | 自行跟踪状态 | 需要 |
| RegisterHotKey | 会消耗该键(文档) | 未验证 | 没有 | 只支持键盘 | 支持修饰键加主键 | 需要 |
| 轮询 | 不能 | 不能 | 靠边沿检测 | 可轮询鼠标键 | 自行跟踪状态 | 不需要,但要专用线程;按下时间短于周期会漏 |

实测补充:
- Raw Input 对合成输入和对物理 F8 都能收到按下与抬起(单独运行)。
- **被 `RegisterHotKey` 注册的键,Raw Input 收不到按下,只收到抬起**(合成 F24,20 次:按下 0、抬起 20;换成低层钩子或轮询同开则不受影响)。两者不能依赖同一个键。
- 按住键时,低层钩子和 Raw Input 都会重复收到按下(约每 31ms),需要自己用「按下后抬起前」的状态去重;`RegisterHotKey` 加 `MOD_NOREPEAT` 则不会重复。

## 结论与后续

1. **不接入任何候选。** 现用低层钩子在捕获侧的 P50 最低,且是唯一能吞键的方式;注入侧各写法没有差别。保持产品默认路径不变。
2. 本次实验排除了「换一种用户态 API 即可明显降低延迟」的可能,剩余的毫秒级延迟更可能在 Windows 输入路由、目标线程调度和消息队列一侧。要进一步定位,需要对最慢样本做 ETW 追踪(线程调度、DPC/ISR、GC/JIT),这是此前文档中的第一项后续工作,仍未做。
3. 注入侧约 0.75ms 的下限说明,仅替换注入 API 无法越过它。驱动级注入(虚拟 HID)仍会经过同样的 Windows 输入线程和目标消息队列,是否有收益目前**没有证据**,需要原型对照后才能判断。按之前的决定,这部分另开新仓库研究,不放进本仓库;新仓库的名称待定(本仓库产品已叫 MacroHID,需避免混淆)。
4. 并列组物理数据已补全(见上):物理按键下各候选同样晚于低层钩子,结论与单独组一致。最初「Raw Input 为 0」是 `RegisterHotKey` 同开造成的,不是厂商软件,也不是 Raw Input 收不到物理按键。
5. 局限:合成输入、单台机器、单一负载状态;没有游戏内延迟、帧时间或跨设备数据;「最大值不变差」这一门槛基于池化后的单个极值,噪声较大。
Loading
Loading