Files
Just-DataInspector/Components/PlotView/Src/PlotViewCacheKey.cpp
T
DeepSeek V4.1 Flash 5cb84e4f97 UI 重构 P2:PlotViewPaint.cpp 按关注点分片(机械搬移,逐像素 0 差异)
PlotViewPaint.cpp 9,080 行里混住四类内容(真绘制 / 光栅化缓存 / 内容指纹 / 布局与命中几何),
按"函数名前缀"分片是上一轮膨胀的根因。本提交按关注点机械搬移函数定义,不改一行逻辑:

- 新增 PlotViewRaster.cpp(1,876 行):光晕离屏层、静态场景缓存、装饰层(AlphaBlend)、
  每网格宽基底图层/并行重建/滚动补条/预热预算。
- 新增 PlotViewCacheKey.cpp(474 行):SceneCacheKey、SceneContentHash/Geometry/Decor、
  样式与派生快照、只改外观的网格定位、样式补丁帧。
- PlotViewPaint.cpp 只留绘制:9,080 -> 6,252 行(-31%)。
- 顶层注释从「四个分片」改为「六个分片(按关注点)」,并保留
  「PlotView::Paint/PaintLane 只允许定义在 PlotViewPaint.cpp」。

pvdetail 全部 inline,故跨分片安全;无需改任何 include,Makefile 按 basename 自动收集。

验证:
- make release TARGET_FILE_NAME=TargetCheck 零警告;Target.exe 873,984 -> 875,008 B(+1KB 对齐差异)。
- 逐像素 A/B(仓库外一次性渲染哈希探针,三场景:全览 / 深放大+自由标记+均值滤波+弱峰调整+时间光标 /
  再平移一帧):分片前后哈希完全相同
  A 2166933039626F66、B C444ECFEB18FD73D、C 7984446D8E7A4418。
- Tools/uicheck 62 passed / 0 failed。

坑:用旧文件回退做 A/B 时必须先删除 Build/<Config>/PlotViewRaster.o 与 PlotViewCacheKey.o,
否则链接会因重复符号失败。
2026-10-28 12:18:42 +08:00

505 lines
21 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// PlotView 实现分片 —— 内容指纹与缓存键("为什么这一帧可以复用上一帧的像素")。
//
// 本分片只包含 `PlotView` 的**成员函数定义**(从 `PlotViewPaint.cpp` 机械搬移,行为与搬移前逐字节一致)。
// 职责:
// * 场景缓存键 `SceneCacheKey` 的构造与比较;
// * 场景内容指纹(`SceneContentHash` / `SceneGeometryHash` / `SceneDecorHash`);
// * 逐信号样式快照与"只改了外观"的网格定位(`CollectSceneStyles` / `FindStyleOnlyLanes`);
// * 派生曲线参数快照(`CollectSceneDerived`);
// * 样式补丁帧(贴图 → 只重画受影响网格 → 写回缓存)。
//
// ⚠️ 硬约束:任何"会影响像素"的状态都必须进指纹;漏一个字段的症状是"贴旧像素"
// (本项目历史上至少 4 次真实 BUG 都是这个原因)。
// ⚠️ 本文件**不做**像素分配与贴图(那些在 `PlotViewRaster.cpp`),也不做绘制(`PlotViewPaint.cpp`)。
#include "PlotViewInternal.hpp"
namespace jdi
{
PlotView::SceneCacheKey PlotView::MakeSceneCacheKey() const
{
SceneCacheKey key;
key.viewMin = Tab().ViewMin();
key.viewMax = Tab().ViewMax();
key.laneScroll = Tab().LaneScroll();
key.laneCount = static_cast<int>(m_lanes.size());
key.areaX = m_area.X;
key.areaY = m_area.Y;
key.areaW = m_area.Width;
key.areaH = m_area.Height;
key.scale = m_scale;
const SignalRef halo = SceneHaloRef();
key.haloGroup = halo.groupIndex;
key.haloSignal = halo.signalIndex;
key.contentHash = SceneContentHash();
return key;
}
bool PlotView::SceneCacheMatches() const
{
if (!m_sceneValid)
{
return false;
}
// 缓存里存的是「快速帧」(关掉了抗锯齿)→ 不能直接命中:
// 必须重画一次全质量版本,否则降级的画面会一直留在屏幕上。
if (m_sceneDegraded)
{
return false;
}
const SceneCacheKey& cached = m_sceneKey;
const SceneCacheKey key = MakeSceneCacheKey();
return (key.viewMin == cached.viewMin) && (key.viewMax == cached.viewMax) &&
(key.laneScroll == cached.laneScroll) && (key.laneCount == cached.laneCount) &&
(key.areaX == cached.areaX) && (key.areaY == cached.areaY) &&
(key.areaW == cached.areaW) && (key.areaH == cached.areaH) &&
(key.scale == cached.scale) && (key.haloGroup == cached.haloGroup) &&
(key.haloSignal == cached.haloSignal) && (key.contentHash == cached.contentHash);
}
SignalRef PlotView::SceneHaloRef() const
{
// 只有拖拽中的信号光晕是“整个拖拽过程都不变”的,可以安全地绘入静态场景缓存;
// 悬停光晕随鼠标移动,必须画在缓存之外(见 PaintHoverHighlight)。
if (m_dragMode == PlotDragMode::MOVE_SIGNAL && m_dragSignal.IsValid())
{
return m_dragSignal;
}
return SignalRef();
}
std::uint64_t PlotView::SceneContentHash(bool includeStyles, bool includeDerived,
bool includeLaneRange, bool includeLaneScroll) const
{
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
// 不需要在各处手工调用 InvalidateSceneCache(容易漏)。
// `includeStyles == false` 时跳过逐信号的外观样式(颜色/不透明度/线宽/模式),
// 得到的就是几何指纹:它一致说明“只有外观样式变了”。
std::uint64_t hash = 1469598103934665603ull;
const auto mix = [&hash](std::uint64_t value)
{
hash ^= value;
hash *= 1099511628211ull;
};
const auto mixFloat = [&mix](float value)
{
std::uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
mix(bits);
};
const auto mixText = [&mix](const std::wstring& text)
{
mix(text.size());
for (const wchar_t character : text)
{
mix(static_cast<std::uint64_t>(character));
}
};
mixFloat(m_plotRect.X);
mixFloat(m_plotRect.Y);
mixFloat(m_plotRect.Width);
mixFloat(m_plotRect.Height);
mixFloat(m_laneContentHeight);
mix(m_scrollBarVisible ? 1ull : 0ull);
// ⚠️ 网格纵向滚动量必须参与**静态场景缓存**的指纹:它只改变各网格的绘制位置。
// 若不参与,平移宽缓冲区 / 静态场景缓存会把旧滚动位置的像素当成当前内容贴回,
// 表现为“网格位置错位 / 拖影 / 残留”等错误元素。
// ⚠️ 但它是**纯屏幕落点**(不影响任何网格内容),因此“宽基底可用性”判据
//(`SceneGeometryHash`)把它排除:网格滚动时 `BlitWideBuffer` 本来就按 `lane.rect`
// 逐网格贴图,内容完全复用 —— 这样“网格滚动到可视区 / 回到最上方”这类
// **每帧都在改滚动量的动画**才不会退化成每帧整幅重绘(用户 2026-10-17 报告卡顿)。
if (includeLaneScroll)
{
mixFloat(Tab().LaneScroll());
}
// 选中信号会改变图例条目的底色与文字颜色(绘制在缓存的静态场景里),
// 因此必须参与指纹:否则在左侧信号面板点选后图例不会高亮联动。
{
const SignalRef selected = SelectedRef();
mix(selected.IsValid() ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(selected.groupIndex));
mix(static_cast<std::uint64_t>(selected.signalIndex));
}
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
{
const LaneVisual& visual = m_lanes[laneIndex];
// ⚠️ 各网格的屏幕纵向位置同样只是“贴图落点”(见 `includeLaneScroll` 的说明)。
if (includeLaneScroll)
{
mixFloat(visual.rect.Y);
}
mixFloat(visual.rect.Height);
// ⚠️ 幅值范围只在 `includeLaneRange` 时计入:拖拽平移时的纵向分量会每帧小幅改动它,
// 若参与“宽基底可用性”判据,每一帧都会退化成整幅重绘(质量下降 + 卡顿)。
if (includeLaneRange)
{
mixFloat(static_cast<float>(visual.yMin));
mixFloat(static_cast<float>(visual.yMax));
}
const std::vector<SignalRef>& signals = Tab().Lanes()[laneIndex].signals;
// 「边沿计数(中心轴)」「均值滤波」「弱峰调整」的开关与每条被勾选信号的参数
// 会改变网格内的曲线绘制。它们**只影响网格内容**(不影响网格结构/几何),
// 因此只在 includeStyles 时计入 —— 于是拖参数滑块可以走“局部重画”快路径,
// 而不必整幅重绘(`FindStyleOnlyLanes` 用 `m_sceneDerived` 定位受影响的网格)。
// `includeDerived == false` 时连这些参数也不计入(用于装饰层指纹:图例/Y 轴刻度
// 与派生曲线参数无关,拖滑块时装饰层无需失效)。
if (includeStyles && includeDerived)
{
mix(Tab().Lanes()[laneIndex].edgeCenter ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].edgeCenterItems.size()));
for (const Lane::EdgeCenterItem& item : Tab().Lanes()[laneIndex].edgeCenterItems)
{
mix(static_cast<std::uint64_t>(item.ref.groupIndex));
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
mix(static_cast<std::uint64_t>(item.points));
mix(static_cast<std::uint64_t>(item.iterations));
}
mix(Tab().Lanes()[laneIndex].meanFilter ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].meanFilterItems.size()));
for (const Lane::MeanFilterItem& item : Tab().Lanes()[laneIndex].meanFilterItems)
{
mix(static_cast<std::uint64_t>(item.ref.groupIndex));
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
mix(static_cast<std::uint64_t>(item.points));
mix(static_cast<std::uint64_t>(item.iterations));
}
mix(Tab().Lanes()[laneIndex].weakPeak ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].weakPeakItems.size()));
for (const Lane::WeakPeakItem& item : Tab().Lanes()[laneIndex].weakPeakItems)
{
// 逐信号参数(等级 + 迭代次数)都参与指纹:改任一条的参数都会重画。
mix(static_cast<std::uint64_t>(item.ref.groupIndex));
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
mix(static_cast<std::uint64_t>(item.amountPercent));
mix(static_cast<std::uint64_t>(item.iterations));
}
}
mix(signals.size());
for (const SignalRef& ref : signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
mix(static_cast<std::uint64_t>(ref.groupIndex));
mix(static_cast<std::uint64_t>(ref.signalIndex));
mix(resolved.visible ? 1ull : 0ull);
if (resolved.data == nullptr || resolved.style == nullptr)
{
continue;
}
// 以下四项是“只影响外观”的样式:仅在 includeStyles 时计入指纹。
// (不参与时得到的就是 SceneGeometryHash:它一致说明只有外观变了,
// 于是可以只重画颜色变了的网格,而不必整幅重绘。)
if (includeStyles)
{
mix(static_cast<std::uint64_t>(resolved.style->color));
mixFloat(resolved.style->opacity);
mixFloat(resolved.style->lineWidth);
mix(static_cast<std::uint64_t>(resolved.style->mode));
// 绘制前的数值变换(系数 / 绝对值 / 镜像)会改变波形形状。
mixFloat(resolved.style->gain);
mix(resolved.style->absolute ? 1ull : 0ull);
mix(resolved.style->mirrorVertical ? 1ull : 0ull);
mix(resolved.style->mirrorHorizontal ? 1ull : 0ull);
// 偏移(Y 平移 + 时间平移)同样会改变波形。
mixFloat(resolved.style->offsetY);
mixFloat(static_cast<float>(resolved.style->offsetTime));
}
// 图例文案参与绘制,重命名必须让缓存失效。
mixText(resolved.data->name);
if (resolved.group != nullptr)
{
mixText(resolved.group->name);
}
}
}
return hash;
}
std::uint64_t PlotView::SceneGeometryHash() const
{
// 外观样式不参与;各网格的幅值范围也不参与(那由逐网格图层指纹负责,
// 这样拖拽时的纵向分量不会把整幅重绘的快路径打掉);
// ⚠️ 网格纵向滚动量 / 各网格屏幕纵向位置也不参与:那只是图层的**贴图落点**,
// 不影响任何网格的像素内容(`BlitWideBuffer` 按 `lane.rect` 逐网格贴图),
// 因此滚动网格时可以继续复用宽基底(否则“滚动到可视区 / 回到最上方”的
// 每一帧都会退化成整幅重绘 —— 用户 2026-10-17 报告的滚动动画卡顿)。
return SceneContentHash(false, true, false, false);
}
std::uint64_t PlotView::SceneDecorHash() const
{
// 装饰层(图例 + Y 轴刻度)的指纹:含外观样式,但**不含派生曲线参数**
//(拖弱峰调整等级/迭代次数滑块时图例与刻度不变,装饰层缓存不应失效)。
return SceneContentHash(true, false);
}
void PlotView::CollectSceneStyles(std::vector<std::vector<SceneStyleEntry>>& out) const
{
out.clear();
out.resize(m_lanes.size());
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
{
const std::vector<SignalRef>& signals = Tab().Lanes()[laneIndex].signals;
std::vector<SceneStyleEntry>& lane = out[laneIndex];
lane.reserve(signals.size());
for (const SignalRef& ref : signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
SceneStyleEntry entry;
if (resolved.style != nullptr)
{
entry.color = resolved.style->color;
entry.opacity = resolved.style->opacity;
entry.lineWidth = resolved.style->lineWidth;
entry.mode = static_cast<int>(resolved.style->mode);
entry.gain = resolved.style->gain;
entry.absolute = resolved.style->absolute;
entry.mirrorVertical = resolved.style->mirrorVertical;
entry.mirrorHorizontal = resolved.style->mirrorHorizontal;
entry.offsetY = resolved.style->offsetY;
entry.offsetTime = resolved.style->offsetTime;
}
lane.push_back(entry);
}
}
}
bool PlotView::FindStyleOnlyLanes(std::vector<int>& lanes) const
{
lanes.clear();
if (!m_sceneValid || m_sceneGeometryHash != SceneGeometryHash())
{
// 网格结构 / 视图几何 / 信号集合 / 图例文案有变化:必须整幅重绘。
return false;
}
// ⚠️ **必须额外确认“视图几何与缓存完全一致”**:`SceneGeometryHash()` 刻意不含视界,
// 所以它相等**并不能**说明视图没变。若视界已经变了还走“只重画受影响网格”的局部补丁,
// 就会把缓存里**旧视界**的画面(时间轴、其它网格、底色)留在屏上 —— 表现为用户反馈的
// “拖影 / 残留 / 错误元素”。因此这里逐项比较除内容指纹之外的缓存键。
{
const SceneCacheKey key = MakeSceneCacheKey();
if (key.viewMin != m_sceneKey.viewMin || key.viewMax != m_sceneKey.viewMax ||
key.laneScroll != m_sceneKey.laneScroll || key.laneCount != m_sceneKey.laneCount ||
key.areaX != m_sceneKey.areaX || key.areaY != m_sceneKey.areaY ||
key.areaW != m_sceneKey.areaW || key.areaH != m_sceneKey.areaH ||
key.scale != m_sceneKey.scale || key.haloGroup != m_sceneKey.haloGroup ||
key.haloSignal != m_sceneKey.haloSignal)
{
return false;
}
}
std::vector<std::vector<SceneStyleEntry>> current;
CollectSceneStyles(current);
if (current.size() != m_sceneStyles.size())
{
return false;
}
// 叠加预览曲线(均值滤波 / 中心轴 / 弱峰调整)的参数变化同样只需局部重画。
std::vector<SceneDerivedEntry> currentDerived;
CollectSceneDerived(currentDerived);
const bool derivedComparable = (currentDerived.size() == m_sceneDerived.size());
for (std::size_t laneIndex = 0; laneIndex < current.size(); ++laneIndex)
{
const std::vector<SceneStyleEntry>& now = current[laneIndex];
const std::vector<SceneStyleEntry>& before = m_sceneStyles[laneIndex];
if (now.size() != before.size())
{
return false;
}
bool differs = false;
for (std::size_t index = 0; index < now.size() && !differs; ++index)
{
// ⚠️ 必须与 `SceneContentHash(includeStyles) / SceneGeometryHash` 的口径**完全一致**:
// 几何哈希刻意不含外观,而外观哈希含 color/opacity/lineWidth/mode **以及**
// 系数 / 绝对值 / 镜像 / Y 偏移 / 时间偏移。旧实现只比前 4 项 ⇒ 一个网格改颜色、
// 另一个网格同时改「调整」时,后者不会被登记为脏;随后 `RepaintSceneCachePatch`
// 又写入包含这些字段的新 `m_sceneKey`,那个网格就带着旧像素被当成“已是最新”,
// 一直残留到某次无关的失效。
differs = !(now[index] == before[index]);
}
if (!differs && derivedComparable)
{
const SceneDerivedEntry& nowDerived = currentDerived[laneIndex];
const SceneDerivedEntry& beforeDerived = m_sceneDerived[laneIndex];
differs = (nowDerived.edgeCenter != beforeDerived.edgeCenter) ||
(nowDerived.meanFilter != beforeDerived.meanFilter) ||
(nowDerived.weakPeak != beforeDerived.weakPeak) ||
(nowDerived.edgeCenterItems != beforeDerived.edgeCenterItems) ||
(nowDerived.meanFilterItems != beforeDerived.meanFilterItems) ||
(nowDerived.weakPeakItems != beforeDerived.weakPeakItems);
}
if (differs)
{
lanes.push_back(static_cast<int>(laneIndex));
}
}
return !lanes.empty();
}
void PlotView::CollectSceneDerived(std::vector<SceneDerivedEntry>& out) const
{
out.clear();
out.resize(m_lanes.size());
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
{
const Lane& lane = Tab().Lanes()[laneIndex];
SceneDerivedEntry entry;
entry.edgeCenter = lane.edgeCenter ? 1 : 0;
entry.meanFilter = lane.meanFilter ? 1 : 0;
entry.weakPeak = lane.weakPeak ? 1 : 0;
const auto flatten = [](const auto& items, std::vector<int>& target, auto valueOf)
{
target.clear();
target.reserve(items.size() * 4);
for (const auto& item : items)
{
target.push_back(item.ref.groupIndex);
target.push_back(item.ref.signalIndex);
// 均值滤波/中心轴取 points,弱峰调整取 amountPercent(由 `valueOf` 决定)。
target.push_back(static_cast<int>(valueOf(item)));
target.push_back(item.iterations);
}
};
flatten(lane.edgeCenterItems, entry.edgeCenterItems,
[](const Lane::EdgeCenterItem& item) { return item.points; });
flatten(lane.meanFilterItems, entry.meanFilterItems,
[](const Lane::MeanFilterItem& item) { return item.points; });
flatten(lane.weakPeakItems, entry.weakPeakItems,
[](const Lane::WeakPeakItem& item) { return item.amountPercent; });
out[laneIndex] = std::move(entry);
}
}
void PlotView::RepaintSceneCachePatch(gd::Graphics& g, const std::vector<int>& lanes)
{
if (lanes.empty() || m_sceneDc == nullptr)
{
return;
}
// 重画范围 = 受影响网格与绘图区的交集(并且不超出缓存区域)。
float top = 1.0e9f;
float bottom = -1.0e9f;
for (const int laneIndex : lanes)
{
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
{
continue;
}
const gd::RectF& rect = m_lanes[static_cast<std::size_t>(laneIndex)].rect;
top = std::min(top, rect.Y);
bottom = std::max(bottom, rect.Y + rect.Height);
}
if (bottom <= top)
{
return;
}
top = std::max(top, m_plotRect.Y);
bottom = std::min(bottom, m_plotRect.Y + m_plotRect.Height);
if (bottom <= top)
{
return;
}
// ⚠️ 补丁矩形必须**向内**对齐到整数像素(ceil 上边界、floor 下边界):
// 网格矩形是浮点数,而 GDI `FillRect`/`BitBlt` 会按 floor/ceil 向外取整,
// "向外"会多擦掉/多写回一整行,那一行属于相邻网格(例如它的分界线),
// 于是补丁边界上会留下一像素接缝。向内对齐后,边界外的行一律由缓存提供
// (缓存内容与整幅重绘逐像素一致,已由对照帧验证)。
const int x0 = std::max(static_cast<int>(std::ceil(m_area.X)), static_cast<int>(std::ceil(m_area.X)));
const int y0 = std::max(static_cast<int>(std::ceil(top)), static_cast<int>(std::ceil(m_area.Y)));
const int x1 = std::min(static_cast<int>(std::floor(m_area.X + m_area.Width)),
static_cast<int>(std::floor(m_area.X + m_area.Width)));
const int y1 = std::min(static_cast<int>(std::floor(bottom)),
static_cast<int>(std::floor(m_area.Y + m_area.Height)));
if (x1 <= x0 || y1 <= y0)
{
return;
}
const gd::RectF patch(static_cast<float>(x0), static_cast<float>(y0),
static_cast<float>(x1 - x0), static_cast<float>(y1 - y0));
// 补丁区域必须被**完整**重画:凡是与它相交的网格都要重画一遍
// (补丁已向内对齐,正常只会命中受影响的那几个网格)。
std::vector<int> repaint;
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
{
const gd::RectF& rect = m_lanes[laneIndex].rect;
if (rect.Y < static_cast<float>(y1) && rect.Y + rect.Height > static_cast<float>(y0))
{
repaint.push_back(static_cast<int>(laneIndex));
}
}
if (repaint.empty())
{
return;
}
// 与整幅重绘保持完全相同的绘制顺序:底色 → 逐网格内容(PaintLane 会与当前裁剪区求交)。
{
const int saveState = g.Save();
g.SetClip(patch, gd::CombineModeIntersect);
// ⚠️ 底色填充必须**按补丁矩形本身**填充,不能用 m_area / m_plotRect:
// `UiFillRect` 对大面积不透明矩形会走 GDI `FillRect` 快路径,而 GDI 绘制
// **不受 GDI+ 裁剪区约束**,会把已经贴好的缓存内容整片覆盖掉。
UiFillRect(g, patch, m_theme->plotBg);
const float cardTop = std::max(patch.Y, m_plotRect.Y);
const float cardBottom = std::min(patch.Y + patch.Height, m_plotRect.Y + m_plotRect.Height);
if (cardBottom > cardTop)
{
UiFillRect(g, gd::RectF(patch.X, cardTop, patch.Width, cardBottom - cardTop),
m_theme->cardBg);
}
const double viewMin = Tab().ViewMin();
const double viewMax = Tab().ViewMax();
const double span = viewMax - viewMin;
const double timeStep = NiceStep((span > 0.0) ? span : 1.0, kTimeTickTarget);
std::vector<double> timeTicks;
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
// ⚠️ **补丁必须就地绘制装饰**(而不是"跳过装饰"):补丁范围覆盖整个受影响网格
// (含网格左缘的 Y 轴胶囊与图例),若跳过绘制,这部分像素会被网格底色/波形覆盖,
// 而补丁结果还会写回场景缓存 —— 表现为"改样式时图例/Y 轴读数闪掉一下"。
const bool savedUseDecorCache = m_useDecorCache;
m_useDecorCache = false;
for (const int laneIndex : repaint)
{
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
{
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
}
}
m_useDecorCache = savedUseDecorCache;
g.Restore(saveState);
}
// 把重画结果写回缓存位图,使缓存与当前样式一致(下一帧即回到"完全命中"快路径)。
{
HDC target = g.GetHDC();
if (target != nullptr)
{
BitBlt(m_sceneDc, x0 - m_sceneX, y0 - m_sceneY, x1 - x0, y1 - y0, target, x0, y0, SRCCOPY);
g.ReleaseHDC(target);
}
}
m_sceneKey = MakeSceneCacheKey();
m_sceneGeometryHash = SceneGeometryHash();
CollectSceneStyles(m_sceneStyles);
CollectSceneDerived(m_sceneDerived);
m_sceneDegraded = m_sceneDegraded || m_fastFrame;
}
} // namespace jdi