Code Review :代码审查 + 产物体积 −12.2%(890,880 → 782,336 B)+ 面板结构收敛 + 离屏验证工具入库 #3

Merged
LuChiChick merged 22 commits from Code_Review into dev 2026-11-04 09:54:20 +00:00
5 changed files with 173 additions and 181 deletions
Showing only changes of commit 7a3fefb9c2 - Show all commits

No files matched your search

@@ -1081,6 +1081,103 @@ namespace jdi
/** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */ /** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */
inline const int kEdgePanelKindCount = static_cast<int>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0])); inline const int kEdgePanelKindCount = static_cast<int>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0]));
// ---- 面板状态 / 滚动量的**唯一派发点** --------------------------------------
//
// ⚠️⚠️ 为什么必须有这一组函数:`Lane` 里每种实验性功能各有一份
// `LanePanelState` 与滚动偏移(7 种 × 2 = 14 个字段)。旧实现在**每个**需要
// 按种类取用这些字段的地方各写一遍多层三元式或 `switch`:
// * `EdgePanelLayoutFor` 的 `enabled / open / collapsed` 各一条 7 层嵌套三元式
// (历史多次因为“多/少一个右括号”编译失败);
// * 标题栏拖拽、✕ 关闭、滚轮滚动、折叠切换、缓存键……各自再写一遍。
// 新增一种面板要改 20+ 处,漏一处就是“面板拖不动 / 关不掉 / 滚轮滚不到底”
//(`MARK`、`RESAMPLE`、`CLIP` 都真实踩过)。
//
// 现在这些字段只在这一组函数里按种类分派;**调用方一律用它**。
// ⚠️ 这些 `switch` **不得写 `default:`** —— 让 `-Wswitch` 在新增
// `EdgePanelKind` 而漏登记时直接报警(编译期保护,0 运行时代价)。
/** @brief 取某种面板的 UI 状态(展开/折起/位置)——可写版本。 */
inline LanePanelState& LanePanelStateOf(Lane& lane, EdgePanelKind kind)
{
switch (kind)
{
case EdgePanelKind::COUNT:
return lane.edgePanel;
case EdgePanelKind::CENTER:
return lane.edgeCenterPanel;
case EdgePanelKind::MEAN:
return lane.meanFilterPanel;
case EdgePanelKind::PEAK:
return lane.weakPeakPanel;
case EdgePanelKind::MARK:
return lane.freeMarkerPanel;
case EdgePanelKind::RESAMPLE:
return lane.resamplePanel;
case EdgePanelKind::CLIP:
return lane.clipPanel;
}
// 枚举值越界(只可能来自非法强制转换):返回一个安全的占位对象。
return lane.edgePanel;
}
/** @brief 取某种面板的 UI 状态——只读版本。 */
inline const LanePanelState& LanePanelStateOf(const Lane& lane, EdgePanelKind kind)
{
return LanePanelStateOf(const_cast<Lane&>(lane), kind);
}
/** @brief 取某种面板的**内容滚动偏移**(客户区像素)——可写版本。 */
inline float& LanePanelScrollOf(Lane& lane, EdgePanelKind kind)
{
switch (kind)
{
case EdgePanelKind::COUNT:
return lane.edgeCountScroll;
case EdgePanelKind::CENTER:
return lane.edgeCenterScroll;
case EdgePanelKind::MEAN:
return lane.meanFilterScroll;
case EdgePanelKind::PEAK:
return lane.weakPeakScroll;
case EdgePanelKind::MARK:
return lane.freeMarkerScroll;
case EdgePanelKind::RESAMPLE:
return lane.resampleScroll;
case EdgePanelKind::CLIP:
return lane.clipScroll;
}
return lane.meanFilterScroll;
}
/** @brief 取某种面板的**内容滚动偏移**——只读版本。 */
inline const float& LanePanelScrollOf(const Lane& lane, EdgePanelKind kind)
{
return LanePanelScrollOf(const_cast<Lane&>(lane), kind);
}
/** @brief 该面板对应的功能是否已启用(右键菜单里的开关)。 */
inline bool LanePanelEnabled(const Lane& lane, EdgePanelKind kind)
{
switch (kind)
{
case EdgePanelKind::COUNT:
return lane.edgeCount;
case EdgePanelKind::CENTER:
return lane.edgeCenter;
case EdgePanelKind::MEAN:
return lane.meanFilter;
case EdgePanelKind::PEAK:
return lane.weakPeak;
case EdgePanelKind::MARK:
return lane.freeMarker;
case EdgePanelKind::RESAMPLE:
return lane.resample;
case EdgePanelKind::CLIP:
return lane.clip;
}
return false;
}
/** /**
* @brief 计算一条派生曲线在给定采样窗口内的取值(同步路径与后台线程共用)。 * @brief 计算一条派生曲线在给定采样窗口内的取值(同步路径与后台线程共用)。
* *
+5 -48
View File
@@ -2186,38 +2186,13 @@ namespace jdi
return false; return false;
} }
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)]; Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
LanePanelState* state = nullptr; // ⚠️ 面板状态字段的唯一派发点(见 `LanePanelStateOf` 的说明)。
switch (kind) LanePanelState& state = LanePanelStateOf(lane, kind);
{ if (state.collapsed == collapsed)
case EdgePanelKind::COUNT:
state = &lane.edgePanel;
break;
case EdgePanelKind::CENTER:
state = &lane.edgeCenterPanel;
break;
case EdgePanelKind::MEAN:
state = &lane.meanFilterPanel;
break;
case EdgePanelKind::PEAK:
state = &lane.weakPeakPanel;
break;
case EdgePanelKind::MARK:
state = &lane.freeMarkerPanel;
break;
case EdgePanelKind::RESAMPLE:
state = &lane.resamplePanel;
break;
case EdgePanelKind::CLIP:
state = &lane.clipPanel;
break;
default:
return false;
}
if (state->collapsed == collapsed)
{ {
return false; return false;
} }
state->collapsed = collapsed; state.collapsed = collapsed;
return true; return true;
} }
@@ -2228,25 +2203,7 @@ namespace jdi
return false; return false;
} }
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)]; const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
switch (kind) return LanePanelStateOf(lane, kind).collapsed;
{
case EdgePanelKind::COUNT:
return lane.edgePanel.collapsed;
case EdgePanelKind::CENTER:
return lane.edgeCenterPanel.collapsed;
case EdgePanelKind::MEAN:
return lane.meanFilterPanel.collapsed;
case EdgePanelKind::PEAK:
return lane.weakPeakPanel.collapsed;
case EdgePanelKind::MARK:
return lane.freeMarkerPanel.collapsed;
case EdgePanelKind::RESAMPLE:
return lane.resamplePanel.collapsed;
case EdgePanelKind::CLIP:
return lane.clipPanel.collapsed;
default:
return false;
}
} }
bool PlotView::ToggleLanePanelCollapsed(int laneIndex, EdgePanelKind kind) bool PlotView::ToggleLanePanelCollapsed(int laneIndex, EdgePanelKind kind)
+32 -62
View File
@@ -1736,42 +1736,12 @@ namespace jdi
top = std::max(m_plotRect.Y + 2.0f * m_scale, top = std::max(m_plotRect.Y + 2.0f * m_scale,
std::min(top, m_plotRect.Y + m_plotRect.Height - panel.Height - 2.0f * m_scale)); std::min(top, m_plotRect.Y + m_plotRect.Height - panel.Height - 2.0f * m_scale));
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(m_edgePanelDragLane)]; Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(m_edgePanelDragLane)];
switch (m_edgePanelDragKind) // ⚠️ 面板位置必须写进**它自己的** `LanePanelState`(见 `LanePanelStateOf` 的说明):
{ // 漏写会让面板“纹丝不动”(位置写进别的面板、布局又从自己那里读)。
case EdgePanelKind::CENTER: // 历史上 `MARK` 与 `RESAMPLE` 都踩过这个坑,因此这里只保留唯一派发点。
lane.edgeCenterPanel.x = left; LanePanelState& dragState = LanePanelStateOf(lane, m_edgePanelDragKind);
lane.edgeCenterPanel.y = top; dragState.x = left;
break; dragState.y = top;
case EdgePanelKind::MEAN:
lane.meanFilterPanel.x = left;
lane.meanFilterPanel.y = top;
break;
case EdgePanelKind::PEAK:
lane.weakPeakPanel.x = left;
lane.weakPeakPanel.y = top;
break;
case EdgePanelKind::MARK:
// ⚠️ 漏掉这一支会让「自由标记」面板**拖不动**(位置写进了 edgePanel,
// 下一帧又从 freeMarkerPanel 读到旧值 ⇒ 面板纹丝不动)。
lane.freeMarkerPanel.x = left;
lane.freeMarkerPanel.y = top;
break;
case EdgePanelKind::RESAMPLE:
// ⚠️ 同类缺陷:漏掉这一支会让「重采样」面板**拖不动**(用户 2026-11-03 报告)。
// 位置必须写进它自己的 `resamplePanel`,布局也只从那里读。
lane.resamplePanel.x = left;
lane.resamplePanel.y = top;
break;
case EdgePanelKind::CLIP:
lane.clipPanel.x = left;
lane.clipPanel.y = top;
break;
case EdgePanelKind::COUNT:
default:
lane.edgePanel.x = left;
lane.edgePanel.y = top;
break;
}
m_dragMoved = true; m_dragMoved = true;
return true; return true;
} }
@@ -1850,6 +1820,9 @@ namespace jdi
const float ratio = (thumbTop - layout.scrollTrack.Y) / travel; const float ratio = (thumbTop - layout.scrollTrack.Y) / travel;
const float target = ratio * maxScroll; const float target = ratio * maxScroll;
bool scrolled = false; bool scrolled = false;
// ⚠️ 穷举所有面板种类且**不写 `default:`**:将来新增面板而忘登记时,
// `-Wswitch`(属 `-Wall`)会直接报错,而不是静默落到其它面板上。
// (「自由标记」在上面的独立分支处理,不会到达这里。)
switch (m_meanDragKind) switch (m_meanDragKind)
{ {
case EdgePanelKind::COUNT: case EdgePanelKind::COUNT:
@@ -1858,18 +1831,20 @@ namespace jdi
case EdgePanelKind::CENTER: case EdgePanelKind::CENTER:
scrolled = SetLaneEdgeCenterScroll(m_markerScrollDragLane, target); scrolled = SetLaneEdgeCenterScroll(m_markerScrollDragLane, target);
break; break;
case EdgePanelKind::MEAN:
scrolled = SetLaneMeanFilterScroll(m_markerScrollDragLane, target);
break;
case EdgePanelKind::PEAK: case EdgePanelKind::PEAK:
scrolled = SetLaneWeakPeakScroll(m_markerScrollDragLane, target); scrolled = SetLaneWeakPeakScroll(m_markerScrollDragLane, target);
break; break;
case EdgePanelKind::MARK:
break;
case EdgePanelKind::RESAMPLE: case EdgePanelKind::RESAMPLE:
scrolled = SetLaneResampleScroll(m_markerScrollDragLane, target); scrolled = SetLaneResampleScroll(m_markerScrollDragLane, target);
break; break;
case EdgePanelKind::CLIP: case EdgePanelKind::CLIP:
scrolled = SetLaneClipScroll(m_markerScrollDragLane, target); scrolled = SetLaneClipScroll(m_markerScrollDragLane, target);
break; break;
default:
scrolled = SetLaneMeanFilterScroll(m_markerScrollDragLane, target);
break;
} }
if (scrolled) if (scrolled)
{ {
@@ -3419,8 +3394,12 @@ namespace jdi
if (layout.close.Width > 0.0f && if (layout.close.Width > 0.0f &&
UiPointInRect(layout.close, static_cast<float>(x), static_cast<float>(y))) UiPointInRect(layout.close, static_cast<float>(x), static_cast<float>(y)))
{ {
// ⚠️ 穷举所有面板种类且**不写 `default:`**(理由同上:漏登记时编译器报警)。
switch (kind) switch (kind)
{ {
case EdgePanelKind::COUNT:
SetLaneEdgeCount(lane, false);
break;
case EdgePanelKind::CENTER: case EdgePanelKind::CENTER:
SetLaneEdgeCenterCount(lane, false); SetLaneEdgeCenterCount(lane, false);
break; break;
@@ -3434,17 +3413,11 @@ namespace jdi
SetLaneFreeMarker(lane, false); SetLaneFreeMarker(lane, false);
break; break;
case EdgePanelKind::RESAMPLE: case EdgePanelKind::RESAMPLE:
// ⚠️ 漏掉这一支时 ✕ 点了没反应(落进 `default` 关掉的是「边沿计数」),
// 用户只能改走右键菜单取消功能(用户 2026-11-03 报告)。
SetLaneResample(lane, false); SetLaneResample(lane, false);
break; break;
case EdgePanelKind::CLIP: case EdgePanelKind::CLIP:
SetLaneClip(lane, false); SetLaneClip(lane, false);
break; break;
case EdgePanelKind::COUNT:
default:
SetLaneEdgeCount(lane, false);
break;
} }
changed = true; changed = true;
} }
@@ -3569,11 +3542,11 @@ namespace jdi
panelLane, panelKind)) panelLane, panelKind))
{ {
const EdgePanelKind hoveredKind = static_cast<EdgePanelKind>(panelKind); const EdgePanelKind hoveredKind = static_cast<EdgePanelKind>(panelKind);
// 「均值滤波 / 中心轴边沿计数 / 弱峰调整 / 边沿计数」面板:内容区超高时用滚轮翻页 // 「均值滤波 / 中心轴边沿计数 / 弱峰调整 / 边沿计数 / 重采样 / 选区截取」面板:
//(上百个信号合并时必备)。 // 内容区超高时用滚轮翻页(上百个信号合并时必备)。
if (hoveredKind == EdgePanelKind::MEAN || hoveredKind == EdgePanelKind::CENTER || // ⚠️ 除「自由标记」外的所有面板都走这一条(它有自己的独立分支,见下)——
hoveredKind == EdgePanelKind::PEAK || hoveredKind == EdgePanelKind::COUNT || // 写成“逐个列举 kind”会让新增面板默默失去滚轮(历史踩过)。
hoveredKind == EdgePanelKind::RESAMPLE || hoveredKind == EdgePanelKind::CLIP) if (hoveredKind != EdgePanelKind::MARK)
{ {
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoveredKind); const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoveredKind);
if (layout.content.Width > 0.0f && if (layout.content.Width > 0.0f &&
@@ -3586,17 +3559,11 @@ namespace jdi
{ {
const Lane& wheelLane = const Lane& wheelLane =
Tab().Lanes()[static_cast<std::size_t>(panelLane)]; Tab().Lanes()[static_cast<std::size_t>(panelLane)];
const float current = (hoveredKind == EdgePanelKind::CENTER) // ⚠️ 读取当前滚动量必须按**同一种分派规则**(`LanePanelScrollOf`):
? wheelLane.edgeCenterScroll // 旧实现这里是一条三元式链,漏掉新面板时会退化成
: (hoveredKind == EdgePanelKind::PEAK) // `meanFilterScroll`(恒 0)⇒ 每次滚轮都从 0 开始算、
? wheelLane.weakPeakScroll // 永远滚不到底(用户 2026-11-03 报告「选区截取」的正是这一处)。
: (hoveredKind == EdgePanelKind::COUNT) const float current = LanePanelScrollOf(wheelLane, hoveredKind);
? wheelLane.edgeCountScroll
: (hoveredKind == EdgePanelKind::RESAMPLE)
? wheelLane.resampleScroll
: (hoveredKind == EdgePanelKind::CLIP)
? wheelLane.clipScroll
: wheelLane.meanFilterScroll;
const float next = current - static_cast<float>(notches * 40.0 * m_scale); const float next = current - static_cast<float>(notches * 40.0 * m_scale);
const float target = std::max(0.0f, std::min(next, maxScroll)); const float target = std::max(0.0f, std::min(next, maxScroll));
switch (hoveredKind) switch (hoveredKind)
@@ -3616,9 +3583,12 @@ namespace jdi
case EdgePanelKind::CLIP: case EdgePanelKind::CLIP:
SetLaneClipScroll(panelLane, target); SetLaneClipScroll(panelLane, target);
break; break;
default: case EdgePanelKind::MEAN:
SetLaneMeanFilterScroll(panelLane, target); SetLaneMeanFilterScroll(panelLane, target);
break; break;
case EdgePanelKind::MARK:
// 「自由标记」由下面的独立分支处理(它的内容高度来自布局)。
break;
} }
return true; return true;
} }
+14 -71
View File
@@ -343,38 +343,18 @@ namespace jdi
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel || clipPanel); const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel || clipPanel);
const bool countTable = (kind == EdgePanelKind::COUNT); const bool countTable = (kind == EdgePanelKind::COUNT);
const bool hasButton = (meanFilter || weakPeak || resamplePanel || clipPanel); const bool hasButton = (meanFilter || weakPeak || resamplePanel || clipPanel);
const bool enabled = meanFilter ? lane.meanFilter // ⚠️ 面板的「启用 / 展开 / 折起」状态一律通过 `LanePanelEnabled` /
: (weakPeak ? lane.weakPeak // `LanePanelStateOf` 取值(见 `PlotViewInternal.hpp` 的说明):
: (freeMarker ? lane.freeMarker // 旧实现是三条 7 层嵌套三元式,新增面板时极容易漏改或写错括号。
: (resamplePanel ? lane.resample const bool enabled = LanePanelEnabled(lane, kind);
: (clipPanel ? lane.clip const LanePanelState& panelState = LanePanelStateOf(lane, kind);
: (centerAxis ? lane.edgeCenter const bool open = panelState.open;
: lane.edgeCount)))));
const bool open = meanFilter ? lane.meanFilterPanel.open
: (weakPeak ? lane.weakPeakPanel.open
: (freeMarker ? lane.freeMarkerPanel.open
: (resamplePanel
? lane.resamplePanel.open
: (clipPanel
? lane.clipPanel.open
: (centerAxis
? lane.edgeCenterPanel.open
: lane.edgePanel.open)))));
if (!enabled || !open) if (!enabled || !open)
{ {
return layout; return layout;
} }
// 面板的**折起**状态(只显示标题栏)。折起时仍然算宽度与位置,但内容区全部省略。 // 面板的**折起**状态(只显示标题栏)。折起时仍然算宽度与位置,但内容区全部省略。
const bool collapsed = meanFilter ? lane.meanFilterPanel.collapsed const bool collapsed = panelState.collapsed;
: (weakPeak ? lane.weakPeakPanel.collapsed
: (freeMarker ? lane.freeMarkerPanel.collapsed
: (resamplePanel
? lane.resamplePanel.collapsed
: (clipPanel
? lane.clipPanel.collapsed
: (centerAxis
? lane.edgeCenterPanel.collapsed
: lane.edgePanel.collapsed)))));
layout.collapsed = collapsed; layout.collapsed = collapsed;
int rowCount = 0; int rowCount = 0;
@@ -597,26 +577,8 @@ namespace jdi
//(用户 2026-10-17 报告:“在当前范围内的网格开启实验性功能,面板首次出现在右下角”)。 //(用户 2026-10-17 报告:“在当前范围内的网格开启实验性功能,面板首次出现在右下角”)。
// 改用**可见序数**:它的量级只有一屏的网格数(约 20),再夹到“面板上沿不超过绘图区 // 改用**可见序数**:它的量级只有一屏的网格数(约 20),再夹到“面板上沿不超过绘图区
// 中间偏下”以内,于是面板始终停**右上区域**,同时保留“依次错开”的观感。 // 中间偏下”以内,于是面板始终停**右上区域**,同时保留“依次错开”的观感。
const double storedX = meanFilter ? lane.meanFilterPanel.x const double storedX = panelState.x;
: (weakPeak ? lane.weakPeakPanel.x const double storedY = panelState.y;
: (freeMarker ? lane.freeMarkerPanel.x
: (resamplePanel
? lane.resamplePanel.x
: (clipPanel
? lane.clipPanel.x
: (centerAxis
? lane.edgeCenterPanel.x
: lane.edgePanel.x)))));
const double storedY = meanFilter ? lane.meanFilterPanel.y
: (weakPeak ? lane.weakPeakPanel.y
: (freeMarker ? lane.freeMarkerPanel.y
: (resamplePanel
? lane.resamplePanel.y
: (clipPanel
? lane.clipPanel.y
: (centerAxis
? lane.edgeCenterPanel.y
: lane.edgePanel.y)))));
int openPanelOrdinal = 0; int openPanelOrdinal = 0;
{ {
// ⚠️ 错开量用「**当前真正显示出来的面板**中排在本面板之前的个数」: // ⚠️ 错开量用「**当前真正显示出来的面板**中排在本面板之前的个数」:
@@ -626,37 +588,18 @@ namespace jdi
// ⚠️ 不能用 `laneIndex`(上百个网格时会把面板推到右下角); // ⚠️ 不能用 `laneIndex`(上百个网格时会把面板推到右下角);
// ⚠️ 也不能只看 `*Panel.open`:该标志**默认就是 true**,必须同时看功能是否启用 // ⚠️ 也不能只看 `*Panel.open`:该标志**默认就是 true**,必须同时看功能是否启用
// (与 `EdgePanelLayoutFor` 的 `enabled && open` 判定一致)。 // (与 `EdgePanelLayoutFor` 的 `enabled && open` 判定一致)。
const auto shown = [](const Lane& lane, EdgePanelKind panelKind) -> bool const auto shown = [](const Lane& panelLane, EdgePanelKind panelKind) -> bool
{ {
switch (panelKind) return LanePanelEnabled(panelLane, panelKind) &&
{ LanePanelStateOf(panelLane, panelKind).open;
case EdgePanelKind::COUNT:
return lane.edgeCount && lane.edgePanel.open;
case EdgePanelKind::CENTER:
return lane.edgeCenter && lane.edgeCenterPanel.open;
case EdgePanelKind::MEAN:
return lane.meanFilter && lane.meanFilterPanel.open;
case EdgePanelKind::PEAK:
return lane.weakPeak && lane.weakPeakPanel.open;
case EdgePanelKind::MARK:
return lane.freeMarker && lane.freeMarkerPanel.open;
case EdgePanelKind::RESAMPLE:
return lane.resample && lane.resamplePanel.open;
case EdgePanelKind::CLIP:
return lane.clip && lane.clipPanel.open;
}
return false;
}; };
static const EdgePanelKind kOrder[7] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER, // ⚠️ 面板错开序数按 `kEdgePanelKinds` 的固定顺序统计(与绘制 / 命中同源)。
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE,
EdgePanelKind::CLIP};
int openSeen = 0; int openSeen = 0;
bool found = false; bool found = false;
for (std::size_t lane = 0; lane < m_lanes.size() && !found; ++lane) for (std::size_t lane = 0; lane < m_lanes.size() && !found; ++lane)
{ {
const Lane& tabLane = Tab().Lanes()[lane]; const Lane& tabLane = Tab().Lanes()[lane];
for (const EdgePanelKind panelKind : kOrder) for (const EdgePanelKind panelKind : kEdgePanelKinds)
{ {
if (!shown(tabLane, panelKind)) if (!shown(tabLane, panelKind))
{ {
+25
View File
@@ -54,3 +54,28 @@
**验证**:release + debug **全量重建**(删 `*.o`/`*.d`)零 error 零 warning; **验证**:release + debug **全量重建**(删 `*.o`/`*.d`)零 error 零 warning;
map 中 `cp-demangle.o` 已无 `.text` 段分配。 map 中 `cp-demangle.o` 已无 `.text` 段分配。
### P2 Release 关闭异步展开表 + 修正 Debug 构建参数 —— ✅ 完成(提交 `94fc1c4`)
**改动**
1. 新增 `RELEASE_ONLY_OPT = -fno-asynchronous-unwind-tables`,**只**加在 Release 的两条
编译规则上。`-fno-exceptions` 下绝大多数函数不再需要 `.pdata`/`.xdata`。
* 代价(已评估接受):Release 崩溃时 WER 拿不到可靠调用栈;不打印栈回溯的程序
功能不受影响,Debug 构建保留完整展开表。
* `-fno-unwind-tables`(全关)会链接失败,不采用。
2. **修正真实缺陷**:`DEBUG_OPT = -O0` 定义了却**从未被使用** —— Debug 的两条编译规则
误用 `$(RELEASE_OPT)`(`-Oz`),且全程没有 `-g`。所谓 "Debug 构建" 其实只是
"不 strip 的 Release",根本无法调试。现改用 `$(DEBUG_OPT) -g`,链接也加 `-g`。
**实测**
| 指标 | 改前 | 改后 |
| --- | --- | --- |
| Release | 829,440 B | **793,088 B**(−36,352) |
| `.pdata` | 30,420 B | 12,684 B |
| `.xdata` | 28,904 B | 10,156 B |
| Debug | 1,734,578 B | 5,992,064 B(真实 `-O0 -g`,属预期) |
**验证**:release 与 debug 均全量重建,零 error 零 warning(`-O0` 下也没有新警告)。