「调整」数值符号规则 + 边沿统计按帧推进(含首帧公平分摊)+ 浮动面板排版与弱峰复位控件

一、「调整」类数值的符号(SignalPanel)
1. FormatSignedNumber:0 不再显示 `+`(旧实现恒带符号 → `+0.00`),Y 轴偏移默认值显示 0.00。
2. FormatGainText:非 0 正值带 `+`、负值带 `-`、0 不带符号;仅用于非编辑态显示,
   进入编辑的初始文本仍为无符号纯数值。
3. 新增 FormatOffsetTimeText:`Δt:` 后按正负带符号(Δt:+5s 60ms / Δt:-5s 60ms / Δt:0s),
   打开面板 / CMD_OFFSET_TIME 提交 / CMD_OFFSET_RESET 三处统一走它。

二、边沿统计:按帧预算 + 每状态公平配额
(用户报告:按住横向光标不动时统计停在“统计中…”不更新;改中心轴参数回车后不动鼠标时停在部分值)
根因:预算按 GetTickCount64() 分桶,而系统时钟粒度约 15.6ms;RequestEdgeScanFrame 的
PostMessage 自持重绘把帧率拉高 ⇒ 同一 tick 内连画多帧却完全不推进。
1. Paint 开头 ++m_edgeScanFrame; m_edgeScanBudget = kEdgeScanPairsPerFrame;(按帧重置),
   删除 m_edgeScanTick。
2. 每个状态各自持有每帧配额(pumpedFrame/pumpedPairs):perState = max(60000, 1.2M/share),
   share = max(kEdgeScanFairShare=6, m_edgeScans.size())。
   注意:不能直接用 size() 分摊 —— 状态在同一帧内逐个创建,第一个那一刻 size()==1 会独占整额,
   同帧后面新建的状态全部被截断为 0 配额(即“只有第一条信号推进”)。
实测(合成 5 信号 × 200 万采样、同一网格):首帧 5 个状态全部推进;9 帧内全部扫完(期间不注入
任何鼠标消息);数值与独立参考实现逐位一致;阈值变化后就地重置、随后“停住不动”仍精确扫完。

三、浮动面板排版
1. 信号名宽度上限 kEdgePanelNameMaxWidth=150 逻辑像素:宽度计算与绘制都用 min(实测, 上限) +
   UiEllipsize(300 字符名字只比单字符宽 44px,旧实现要多 5100px)。
2. 统计信息两列等宽(新增 EdgeRisingText/EdgeFallingText/EdgeCountsPending):面板宽度按
   2×max(两列文本宽)+kEdgePanelStatColumnGap 预留,绘制时两列各占 (可用宽−间距)/2 ⇒ 多条信号的
   “上升沿/下降沿”列起点固定、严格垂直对齐。
3. 未完成时 rising 与 falling 两个数字后面都带 `…`(旧实现只在整行末尾追加一次,
   视觉上只跟在“下降沿”的数字后面)。

四、弱峰调整面板
1. 每行滑块右侧新增复位控件(复用还原图标绘制 PaintPanelResetIcon):悬停高亮 + IDC_HAND +
   一次性提示;点击恢复幅度 0 / 迭代 1 且 maxIterations 回到初始上限 10。
   EdgePanelLayout 新增 rowLevelResets/rowIterResets(并进 PanelSlotKey),两行按钮共用同一 X。
2. 迭代次数上限输入框的空内容提示改为“输入上限(当前N)”。
3. 滑块标签去掉多余的 %/N(kWeakPeakLevelLabel=调整幅度;新增 kWeakPeakIterationsSliderLabel=
   迭代次数;MEAN / CENTER 输入框的“迭代次数 N”保持不变)。

五、验证(全部离屏;进程/线程 BELOW_NORMAL、窗口 SW_HIDE、不注入真实鼠标键盘)
- 新增 .preflight/perf/panelcheck_main.cpp:46/46 通过(符号规则走真实菜单路径;按帧推进 /
  不饿死 / 精确性 / 自持驱动;两列等宽预留(含未完成时两处 …);名字上限;
  弱峰复位控件位置 / 对齐 / 点击恢复默认值与上限)。
- 既有 verify_main.cpp:47/47(另 2 条为本机 .preflight 未随远端同步的旧规则断言)。
- make release 零警告。

规则同步:Documents/agent_mem.md 新增 §9.26,§11.2 新增第 43–48 条硬约束,变更日志补一行。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-15 01:35:47 +08:00
1 parent 65f7625dce
commit fba40ee1a2
4 files changed
+564 -80

No files matched your search

+112
View File
@@ -1275,6 +1275,88 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
---
### 9.26 「调整」数值符号规则 + 边沿统计按帧推进(含首帧公平分摊)+ 浮动面板排版与弱峰复位控件(本轮)
> 背景(用户报告):① 时间轴偏移 / 系数 / Y 偏移的符号显示不符合要求;
> ② **按住**横向光标不动时边沿计数停在“统计中…”,松开鼠标才更新;
> ③ 改中心轴参数回车后**不移动鼠标**时统计停在部分值(数字后还有 `…`);
> ④ 浮动面板没有信号名长度上限,超长名字把面板顶得很长;
> ⑤ 多信号统计信息错位(没有排版),且只有“下降沿”的数字带 `…`;
> ⑥ 弱峰调整面板需要每行复位控件、上限输入框提示文案、并去掉滑块前多余的 `%` / `N` 标签。
#### 一、「调整」类数值的符号规则(`SignalPanel`)
- `FormatSignedNumber`:**0 不再显示 `+`**(旧实现恒带符号 → `+0.00`);
Y 轴偏移默认值现在显示 `0.00`。
- `FormatGainText`:非 0 正值带 `+`、负值带 `-`、0 不带符号。
⚠️ 仅用于**非编辑态**显示;进入编辑的初始文本仍用不带符号的纯数值(`FormatNumberText`),
避免用户一进编辑框就看到 `+`。
- 新增 `FormatOffsetTimeText(double)`:`Δt:` 后带正负号 ——
`Δt:+5s 60ms` / `Δt:-5s 60ms` / `Δt:0s`(`FormatDurationCompound` 内部取绝对值,
符号由这里补)。打开面板、`CMD_OFFSET_TIME` 提交、`CMD_OFFSET_RESET` 三处统一走它。
#### 二、边沿统计推进:**按帧**预算 + 每状态**公平配额**
- **根因(用户报告②③)**:预算按 `GetTickCount64()` 分桶,而系统时钟粒度约 15.6ms;
`RequestEdgeScanFrame` 的 `PostMessage` 自持重绘会把帧率拉到远高于它 ⇒
**同一 tick 内连画多帧但完全不推进**。“按住不动”“回车后不动鼠标”因此停住。
修法:`Paint` 开头 `++m_edgeScanFrame; m_edgeScanBudget = kEdgeScanPairsPerFrame;`
(**按帧**重置),删除 `m_edgeScanTick`。
- **每状态配额**(修“很多项停在统计中不更新”):`perState = max(kEdgeScanPairsPerStateMin,
kEdgeScanPairsPerFrame / share)`,`share = max(kEdgeScanFairShare = 6, m_edgeScans.size())`;
每个状态记录 `pumpedFrame` / `pumpedPairs`,新帧开始时清零。
⚠️ **不能直接用 `size()` 分摊**:状态是**同一帧内逐个创建**的,第一个状态那一刻 `size() == 1`
会拿走整额预算,同帧后面新建的状态全部被截断为 0 配额(表现为“首帧只有第一条信号推进”)。
- 实测(合成 5 信号 × 200 万采样、全部同一网格):**首帧 5 个状态全部推进**;
**9 帧内全部扫完**(期间不注入任何鼠标消息);统计值与参考实现**逐位一致**;
阈值变化(拖动横向光标)后就地重置,随后“停住不动”仍能扫完且精确。
- 自持驱动断言:未完成时确实投递 `kMessagePlotDerivedReady` —— 这是
“用户不移动鼠标也会持续推进”的**唯一**驱动源,任何“分帧推进”的新逻辑都要照做。
#### 三、浮动面板排版
- **信号名宽度上限** `kEdgePanelNameMaxWidth = 150`(逻辑像素):`EdgePanelWidth` 与绘制
都用 `min(实测宽度, 上限)` 并走 `UiEllipsize`(超长名字不再把面板顶长)。
实测:300 字符名字的面板宽度只比单字符名字宽 **44px**(上限 225px @scale1.5),
不设上限则要多 **5100px**。
- **统计信息两列等宽**(修“错位”):新增 `EdgeRisingText` / `EdgeFallingText` /
`EdgeCountsPending`;面板宽度按 **`2 × max(两列文本宽) + kEdgePanelStatColumnGap`**
预留,绘制时两列各占 `(可用宽 − 间距) / 2` ⇒ 多条信号的“上升沿 / 下降沿”
**列起点固定、严格垂直对齐**(用户明确要求)。
⚠️ 绘制函数 `paintCounts` 与宽度计算必须用**同一套等宽假设**,否则较窄的那一列会被截断。
- **两个数字后面都带 `…`**:旧实现只在整行末尾追加一次,视觉上只跟在“下降沿”的数字后;
现在 rising / falling 各自在未完成时追加 ` …`。
#### 四、弱峰调整面板
- 每行滑块右侧新增**复位控件**(复用「系数/偏移」二级面板的还原图标绘制:
PlotView 内新增 `PaintPanelResetIcon`,与 `PopupMenu::PaintResetIcon` 同一套笔画):
悬停高亮 + `IDC_HAND` + 一次性提示“点击恢复该参数的默认值”;
点击把该行参数恢复默认(幅度滑块 → `amountPercent = 0`;迭代滑块 → `iterations = 1`
**且 `maxIterations` 回到初始上限 10**)。
`EdgePanelLayout` 新增 `rowLevelResets` / `rowIterResets`(并进 `PanelSlotKey` 以保证
悬停高亮能触发重绘),两行按钮共用同一 X(上下对齐),面板宽度计入按钮列。
- 「迭代次数上限」输入框的空内容提示改为 **“输入上限(当前N)”**(旧实现只显示一个数字)。
- 滑块标签去掉多余的 `%` / `N`:`kWeakPeakLevelLabel = L"调整幅度"`、
新增 `kWeakPeakIterationsSliderLabel = L"迭代次数"`;
⚠️ MEAN / CENTER 参数输入框的标签常量 `kWeakPeakIterationsLabel`(`迭代次数 N`)**保持不变**。
#### 五、验证(全部离屏,进程/线程均 `BELOW_NORMAL`,窗口 `SW_HIDE`,不注入真实鼠标/键盘)
- 新增探针 `.preflight/perf/panelcheck_main.cpp`(合成 5 信号 × 200 万采样、同一网格,
只写本地 stdout 的 ASCII):**46/46 通过** —— 覆盖
符号规则 10 条(全部走**真实菜单路径**:`OpenGainMenu` + 读 `PopupMenu` 条目)、
按帧推进 / 不饿死 / 精确性(与独立参考实现逐位比对)/ 自持驱动、
两列等宽预留(含未完成时两处 `…`)、名字上限、弱峰复位控件(存在 / 位置 / 对齐 /
点击恢复默认值与上限)。
- 既有 `verify_main.cpp`(本机版本)**47/47(另有 2 条已知过期断言)**:
`derived offsetTime kept (cannot be baked)`(§9.21 已推翻该规则)与
`mouse in lane1 lower half -> target 2`(§9.21 已把让位判据改为“严格超过 ±0.5H”)。
本机 `.preflight` 是**本机自建**、未随远端同步 ⇒ 这两条是**旧规则断言**,与本轮改动无关。
- `make release` 零警告。
---
## 10. 变更日志(本文件维护记录)
| 日期 | 设备/模型 | 摘要 |
@@ -1313,6 +1395,7 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
| 2026-10-14 | DeepSeek-V4.1-Flash(**起源设备**) | **修复“偶发的改一个参数、其它信号也重新统计”(详见 §9.25)**:根因是中心轴统计靠 `AcquireCoveringCurve`/`ScheduleCurveJob` 拿**整条**曲线 — 千万级信号一条 ≈103MB,多试几个参数值就把曲线缓存推到 768MB 上限 → `PruneCurveEntriesLocked` 从队首淘汰(往往是**其它信号**的曲线)→ 那些信号显示“统计中…”并在后台重新排队(**偶发**,取决于缓存里积了多少条目)。修法:`EdgeScanState` 新增 `localCurve/localBegin/localEnd/localValid`,扫描时**就地计算「当前扫描块 ± 滤波半径」**(半径 = `points*iterations`,窗口 ≥ 8 个块,成本计入扫描预算,扫完即释放)⇒ 与曲线缓存、与其它信号**彻底解耦**(实测统计时 `curve entries 0`);删除“曲线未就绪就把整 tick 预算清零”的门控(它会把同帧后面排队的其它统计**饿死**);`kEdgeScanStateLimit` **16→64** 且超限时优先淘汰「已扫完」的状态。验证:新增间歇性探针 `[AU]`(5 信号同网格、真实编辑路径连改 25 次、逐帧分类 CASE A/B/C)——修复前 baseline **0/10 完成**,修复后 **10/10 完成且 25 次编辑 CASE A/B/C 全 0**;新增「本地窗口 vs 整条曲线」12 组参数(points 5/17/64/300 × iterations 1/2/4)**逐位一致**断言;合计 **789/789 通过**,`make release` 零警告。 |
| 2026-10-14 | DeepSeek-V4.1-Flash(**起源设备**) | **新标签页分组默认折起 + GX 徽标悬停不改光标 + 标题栏 RAM 改用「私有工作集」+ 边沿计数改为「逐块累积 + 每 tick 预算推进」(详见 §9.22)**。① `NewTab(bool collapseGroups)`(首个标签页 false、用户新建 true)+ `TabConfig::SetGroupsCollapsedByDefault/SetAllCollapsed`;`SyncToDataSet` 用该默认值初始化**新出现的**分组。② `QueryCursor` 的 GX 徽标分支由 `IDC_HAND` 改为 `nullptr`(悬停不改形状,仅拖拽时 `IDC_SIZENS`)。③ **RAM 口径修正**:原取 `WorkingSetSize`(总工作集,含映射 DLL/共享页与已释放未回收的驻留页)→ 改为 `PROCESS_MEMORY_COUNTERS_EX2::PrivateWorkingSetSize`(与任务管理器「内存」列一致;失败退回 `PrivateUsage` → `WorkingSetSize`)。实测加载后 117.55/109.96 MiB、关闭分组后 15.44/7.62 MiB —— **无泄漏,内存完整归还**。④ **边沿计数性能**:改前 25.8M 采样 ≈**115ms/条/帧**(5 条同网格 = 575ms/帧,用户报告"开启即卡顿、之后拖不动");改为按 `kEdgeBlockSamples=8192` 分块累积、每 tick 预算 `kEdgeScanPairsPerTick=1.2M`、区间用块和相加(区间不进键)、拖动横向光标期间不推进并显示上一个完整值 + `complete=false`(`EdgeCountsText` 显示"统计中…/…");实测首帧 1.7ms、单次最坏 1.9ms、拖动中 10 次查询 0.11ms、最终结果与参考实现逐位一致。⑤ **内存排查报告**(用户要求,第 4 项只排查未改动):弱峰调整工作缓冲实测 **≈34 B/采样**(极值表 `PeakExtremum` 24 B/极值为主导),**4 条工作线程并发 +412 MiB / 3M 采样** ⇒ 25.8M 采样峰值 ≈**3.5GB**(+数据 1.03GB +曲线缓存 ≤768MB ⇒ 4~5GB,与用户 4.4GB 观测吻合);**全仓无 `try`/`catch`** ⇒ 低内存时 `bad_alloc` 逸出线程入口 ⇒ `std::terminate()` 崩溃(无优雅降级机制)。验证:离屏 `uitest` **738/738 通过**(新增 `[AR]` 边沿计数预算/精确性/拖动期 O(1)、`[AS]` 标题栏 RAM 口径),`make release` 零警告。 |
| 2026-10-14 | DeepSeek-V4.1-Flash(**起源设备**) | **边沿计数改用「绘制值」+ 派生信号烘入时间偏移 + 图例托底统一为 GX 徽标款 + 调整二级面板向上展开(详见 §9.21)**。① **边沿计数恒为 0**:`EdgeCountsFor`/`EdgeCenterCountsFor` 只比原始采样值,而阈值是屏幕上的值 → 「系数 50」的信号统计恒为 0;修法:判定值改为 `ApplyDrawTransform(style, values[SourceIndexFor(...)])`(中心轴两侧同套变换;水平镜像时曲线窗口也要先映射下标);缓存新增 **`styleKey`**(否则改完「调整」仍命中旧计数)。② **区间边界点被误计**:`EdgeIndexRange` 左端“多取一个采样点”的外扩**已删除**,收紧为“**两个端点都在区间内**才计入”(光标恰落在越过阈值的点 B 上时 B 不再被算作上升沿)。③ **`offsetTime` 也烘进派生采样值**(**推翻 §9.19 的“唯一例外”**):按 `time[i]-offsetTime` 取最近采样点重采样(单调时间轴用只前进的游标,O(n);越界置 NaN),新信号样式**全默认**;非单调时间轴退回到把 `offsetTime` 带在样式上。④ **图例托底 = GX 徽标同款**(同高 / 圆角 = 高度一半 / 同配色 / 水平内边距 = 胶囊半径),文本改用新增字体 **`uiTiny`(10 逻辑 px)**;`kLegendPlateRadius` 已删。⑤ **`PopupMenu::Open(..., expandUp=true)`**(锤点为**左下角**、向上展开,`m_buildExpandUp` 供 `RebuildLayout` 复用)+ 「系数/偏移」「镜像」面板锤在按钮左上角再上移 `kAdjustMenuGap`。验证:`.preflight/uitest` 新增 `[AR]`(五组断言全部与**独立参考实现**逐位比对,含 A/B:“按原始采样值算会得到 7/6 个边沿”);**并同步修正 harness 里两处旧规则断言**(`[AP]` 派生信号继承 → 默认样式 + 烘值;`[AN]` 图例带无托底 → 装饰矩形逐块且暴露列可见底层内容);合计 **722/722 通过**,`make release` 零警告。 |
| 2026-10-15 | DeepSeek-V4.1-Flash(**另一台设备**) | **「调整」数值符号规则 + 边沿统计按帧推进(含首帧公平分摊)+ 浮动面板排版与弱峰复位控件(详见 §9.26)**。① **符号**:Y 轴偏移 0 不再显示 `+0.00`;系数非 0 正值带 `+`(编辑态初始文本仍无符号);`Δt:` 后按正负带符号(`Δt:+5s 60ms` / `Δt:-5s 60ms` / `Δt:0s`)。② **边沿统计卡住**根因:预算按 `GetTickCount64()` 分桶(系统粒度 15.6ms)而 `PostMessage` 自持重绘把帧率拉高 ⇒ 同一 tick 内多帧完全不推进 → 改为 **`Paint` 开头按帧重置**;并给**每个状态独立每帧配额**(`share = max(6, m_edgeScans.size())`,避免同帧新建状态被第一个独占整额)→ 实测首帧 5 个状态全部推进、**9 帧内全部扫完且不注入任何鼠标消息**、与参考实现逐位一致。③ **面板排版**:信号名宽度上限 `kEdgePanelNameMaxWidth=150`(300 字符名字只多 44px,旧实现多 5100px);统计信息**两列等宽**(`2×max(列宽)+间距`)⇒ 多信号“上升沿/下降沿”列起点固定、垂直对齐;未完成时**两个数字后都带 `…`**(旧实现只跟在“下降沿”后)。④ **弱峰调整**:每行滑块右侧新增**复位控件**(复用还原图标,悬停高亮 + `IDC_HAND` + 提示;点击恢复幅度 0 / 迭代 1 且上限回 10);上限输入框提示改为“输入上限(当前N)”;滑块标签去掉多余的 `%` / `N`。验证:新增离屏探针 `panelcheck` **46/46 通过**(全部走真实菜单/点击路径),既有 `verify` 47/47(另 2 条为本机未同步的旧规则断言),`make release` 零警告。 |
---
@@ -1449,6 +1532,35 @@ GDI+ 抗锯齿逐段扫描这些**近竖直、满幅**的边,代价爆炸(
且超限时**优先淘汰「已扫完」的最旧状态**,绝不淘汰正在推进的。
上限太小会淘汰其它信号的已扫完状态 → 它们从头重扫(与第 39 条同一类症状)。
实测 10 个信号 × (普通+中心轴)= 20 个状态,原来的 16 就会踩到。
43. ⚠️ **“分帧推进”的预算必须按帧重置,禁止按 `GetTickCount64()` 分桶**:
系统时钟粒度约 15.6ms,而 `RequestEdgeScanFrame` 的 `PostMessage` 自持重绘会把帧率
拉到远高于它 ⇒ **同一 tick 内连画多帧但完全不推进**(用户报告“按住光标不动时统计
卡在统计中”“回车确认参数后不移动鼠标就停在部分值”)。
✓ 正确做法:`Paint` 开头 `++m_edgeScanFrame; m_edgeScanBudget = kEdgeScanPairsPerFrame;`。
44. ⚠️ **每个累积状态必须各自持有每帧配额**:共享一个总额 + “从列表第一个扫起”会让
多条信号里只有第一条有进展、其余长期显示“统计中…”(用户报告“很多项都停在统计中不更新”)。
⚠️ 分摊时**不能用 `m_edgeScans.size()` 当分母**:状态是**同一帧内逐个创建**的,
第一个状态那一刻 `size() == 1` 会独占整额,同帧后面新建的状态全部被截断为 0 配额
(表现为“首帧只有第一条信号推进”)。用 `share = max(kEdgeScanFairShare, size())`。
45. ⚠️ **「调整」类数值的符号规则**(用户明确要求,勿回退):
* **Y 轴偏移**:>0 带 `+`、<0 带 `-`、**0 不带符号**(旧实现恒带符号 → `+0.00`);
* **系数**:非 0 正值带 `+`、负值带 `-`、0 不带符号 —— 仅**非编辑态**;
进入编辑的初始文本用**不带符号**的纯数值;
* **时间轴偏移**:`Δt:` 后按正负带符号(`Δt:+5s 60ms` / `Δt:-5s 60ms` / `Δt:0s`),
统一走 `SignalPanel::FormatOffsetTimeText`(`FormatDurationCompound` 内部取绝对值)。
46. ⚠️ **浮动面板的信号名必须限宽**(`kEdgePanelNameMaxWidth = 150` 逻辑像素):宽度计算与
绘制都用 `min(实测, 上限)` + `UiEllipsize`;否则超长信号名会把面板顶得极长
(300 字符名实测多出 5100px,限宽后只多 44px)。
47. ⚠️ **边沿统计的“上升沿 / 下降沿”必须两列等宽排版**(列起点固定 → 多条信号垂直对齐):
面板宽度按 `2 × max(两列文本宽) + kEdgePanelStatColumnGap` 预留,绘制时两列各占
`(可用宽 − 间距) / 2`;⚠️ **绘制与宽度计算必须用同一套等宽假设**,否则较窄的列会被截断。
未完成时 **rising 与 falling 两个数字后面都要带 `…`**(旧实现只在整行末尾追加一次)。
48. ⚠️ **弱峰调整面板每行滑块右侧有复位控件**(与「系数/偏移」二级面板的还原图标同一套绘制):
点击把该行恢复默认 —— 幅度滑块 → `amountPercent = 0`;迭代滑块 → `iterations = 1`
**且 `maxIterations` 回到初始上限 10**。`EdgePanelLayout` 的 `rowLevelResets` /
`rowIterResets` **必须进 `PanelSlotKey`**(否则悬停高亮不会重绘);两行按钮共用同一 X。
⚠️ 滑块标签不得带多余的 `%` / `N`(但 MEAN / CENTER 参数输入框的 `迭代次数 N` 保持不变);
迭代次数上限输入框的空内容提示为 `输入上限(当前N)`。
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)