From 3d989a00a822989339a6d7cb4d23acc04396670e Mon Sep 17 00:00:00 2001 From: tei Date: Sun, 11 Oct 2026 16:47:21 +0900 Subject: [PATCH] feat: admit settled Symbol render results to retained cache --- ...119_Symbol结果证书与R缓存Build实现依据_v1.md | 77 +++++ ...š结果证书与R首次Build_Fast准入_Codex任务_v1.md | 33 +++ .../STEP-119_Symbol缓存实际实现_v1.md | 51 ++++ .../STEP-119_原PBF_FAST实机联合验收计划_v1.md | 89 ++++++ .../STEP-119_Symbol缓存_Codex完成报告_v1.md | 88 ++++++ src/mln/mtl/context.cpp | 23 ++ .../renderer/layers/render_symbol_layer.cpp | 143 ++++++++++ .../renderer/layers/render_symbol_layer.hpp | 4 + .../renderer/layers/symbol_layer_tweaker.cpp | 30 ++ .../renderer/layers/symbol_layer_tweaker.hpp | 6 + src/mln/renderer/render_orchestrator.cpp | 86 +++++- src/mln/renderer/render_orchestrator.hpp | 22 +- src/mln/renderer/render_tree.hpp | 3 +- src/mln/renderer/renderer.cpp | 35 ++- src/mln/renderer/renderer_impl.cpp | 254 ++++++++++++----- src/mln/renderer/renderer_impl.hpp | 4 +- src/mln/renderer/symbol_render_inputs.hpp | 51 ++++ src/mln/renderer/symbol_render_result.hpp | 141 ++++++++++ src/mln/renderer/symbol_shader_provenance.hpp | 51 ++++ src/mln/shaders/gl/shader_program_gl.cpp | 27 +- src/mln/shaders/mtl/shader_program.cpp | 5 +- src/mln/text/placement.cpp | 4 + src/mln/text/placement.hpp | 2 + .../render_orchestrator_error.test.cpp | 136 +++++++++ .../renderer/retained_underlay_cache.test.cpp | 266 ++++++++++++++++++ 粒子开发记录.md | 7 + 26 files changed, 1534 insertions(+), 104 deletions(-) create mode 100644 docs/20_调查/STEP-119_Symbol结果证书与R缓存Build实现依据_v1.md create mode 100644 docs/40_任务/STEP-119_Symbol稳定结果证书与R首次Build_Fast准入_Codex任务_v1.md create mode 100644 docs/50_Step记录/STEP-119_Symbol缓存实际实现_v1.md create mode 100644 docs/60_测试/STEP-119_原PBF_FAST实机联合验收计划_v1.md create mode 100644 docs/报告/STEP-119_Symbol缓存_Codex完成报告_v1.md create mode 100644 src/mln/renderer/symbol_render_inputs.hpp create mode 100644 src/mln/renderer/symbol_render_result.hpp create mode 100644 src/mln/renderer/symbol_shader_provenance.hpp diff --git a/docs/20_调查/STEP-119_Symbol结果证书与R缓存Build实现依据_v1.md b/docs/20_调查/STEP-119_Symbol结果证书与R缓存Build实现依据_v1.md new file mode 100644 index 000000000..d2a78f429 --- /dev/null +++ b/docs/20_调查/STEP-119_Symbol结果证书与R缓存Build实现依据_v1.md @@ -0,0 +1,77 @@ +# STEP-119 Symbol结果证书与R缓存Build实现依据 v1 + +日期:2026-10-11(JST)。状态:IMPLEMENTED_WAITING_ANDROID;Shared Core已有实质实现,尚无本轮编译、GPU、Android或视觉验收结论。 + +## 1. 基线与原证据 + +仓库 `/Users/tei/dev/MapLibreParticleLab`,Mac.lan / Darwin24.6.0 / x86_64,particle/main@869930046b11dbeb3b147e3be952a2e847316292。Native版本13.6.1-particle.23。Integration本地只读HEAD=80667d6c343411d03a760425438e3d292f452118;已读取AGENTS、instructions、README、WORKFLOW、TEAM_MODEL和contracts/rfc/adr/status/compatibility,未写跨仓文件。纯Renderer内部优化,不新增公共接口、所有权或错误协议;C-001/C-002和Core-owned timeline/playback保持。 + +STEP117揭示:Continuous重新计算令placementChanged=true;recent复用设置stale,hasTransitions=true。旧Symbol gate要求changed=false、fade=0、transition=false,两个分支都不能自举。global retainedPlacementStable、retainedStaticStable、retainedSupported、preparedSceneSupported还有独立阻断。 + +STEP118保留原63基础层manifest,仅测试副本排除tide-spots:62基础层+2天气层,Native64,R[0,62)、C[62,64),missingSourceIds=[],static-placement、Build0/Reuse0。旧设备90.00806697399821秒FastDelta0/FullDelta942、decisionMismatch0,不是本轮结果。 + +旧APK源码3051196054c7b69815c298040dae61d26499d9f2,SHA=44afcfabe0cf4f1ec93f35899241bd73093aa6db4b5cb82ca2120dd531b8f3e9。旧result.json SHA=d3d20fc35bbd17e9f0ea0dd014ca4af8e0c5c5284d9f0c009bc20981d876ff10;screen.png SHA=c8b57a8432626c9209ca9a614a747728d4256bd09273b28c26a793a75a6d44d4。本轮只读解析/核对,文件未修改。 + +## 2. 实际渲染生命周期 + +1. 正常Full先createRenderTree:Source/style差分、跨瓦片ID匹配、全场Placement碰撞与commit继续使用原实现,没有按R段拆碰撞或冻结新结果。 +2. RenderTreeImpl::prepare调用Placement::updateLayerBuckets,继续处理text/icon、重复符号、variable anchors、方向、opacity、动态顶点、沿线投影和排序。 +3. Renderer依次上传source/layer/atlas,updateLayers创建或更新Drawable,processChanges实际应用注册/移除队列。 +4. 所有正常Full LayerGroup先runTweakers并upload。SymbolLayerTweaker记录**同一份传给GPU的**evaluated props、drawable/tile UBO字段;记录frameCount,并确认evaluated/drawable/tile UBO存在和consolidated容量,未执行、出错、缺buffer或错误帧不能补证。 +5. 此后采集每R的原生Symbol绘制输入,与上一成功提交Full的证据精确比较。符合就准入首次Build,不依赖已有缓存。 +6. Build保持原双pass和原始全局ordinal;finishBuild后再检查输入未改变。资源/绘制错误走Fallback;实际主缓冲按原R/C层序composite或live绘制。 +7. present和endFrame成功返回之后再次检查输入、epoch及错误代次,才记录完成Full/Build证书。它证明成功编码和提交,不宣称GPU fence完成或像素等价已经测量。 +8. 后续一次正常Full Reuse成功提交后,才允许Fast。Fast使用相同证书版本、content/topology/config、相机、backend viewport、pixelRatio和crossSourceCollisions,并继续原24项Gate。 + +普通Full替换旧prepared tree时只销毁引用式RenderItem;证据拥有复制的字节和必要资源引用,跨Full保留。style/context/显式prepared-tree reset仍清证书。symbolCompletedSignature独立保存上一Full的非相机输入,防止glyphURL/fontFaces/placement设置等在旧树销毁后丢失保护。 + +## 3. 结果证据包含什么 + +| 实际输入 | 采集与保护 | +|---|---| +| Symbol uniforms | 从SymbolLayerTweaker实际提交的CPU参数复制;包含paint颜色/透明度/halo、tile/label/coord矩阵、尺寸/插值、gamma、text/icon/halo标志;忽略SymbolDrawableUBO末尾12字节非shader padding | +| 顶点与实例属性 | 对实际Drawable绑定的VertexAttributeArray/InstanceAttributeArray记录属性索引、类型、stride、offset、vertexOffset和共享原始字节;包含静态布局、动态位置/角度、opacity、data-driven paint、排序实例;共享属性必须已有GPU背板,索引必须已上传且非dirty | +| 已释放的不可变buffer | 只在producer已release且存在GPU backing时用保持存活的对象/背板身份;不能读取的可变输入拒绝。不是把任意空数据当相等 | +| text/icon/索引与排序 | 原生SymbolBucket对应text/icon/sdfIcon buffer、segment范围、instance/vertex/index长度、sortKey;两条路径均记录实际三角索引字节,release后必须保持不可变索引对象与GPU背板身份;instancing固定quad和segment仍核对 | +| fade | 检查每个实际opacity顶点的packed值及当帧fade;必须在放置目标方向达到shader clamp终点,并且真实Placement fade已结束;非有限值、负值、非整数或越界packed值拒绝 | +| atlas/texture/sampler | 保持atlas、glyph/icon及实际绑定纹理/原生atlas tweaker身份,核对尺寸/format、上传状态和实际绑定一致。原生tweaker的sampler策略由固定bucket/layout和相同camera/无手势状态决定;glyph/icon像素原地更新由既有全局epoch保护 | +| shader | 独立provenance记录只在实际GL/Metal编译源码与原生Symbol源码完全相符、额外宏属于原生uniform白名单时登记;保留shader对象身份。名称相同的未知/替换shader不借用fade契约 | +| LayerGroup/层序 | processChanges之后验证Symbol组确实注册,隐藏层无旧注册组,collision额外组或陌生组拒绝;记录全局group总数、原始ordinal、组身份/层索引/数量/enable,以及Drawable实际混合方程/因子/颜色mask、剔除/深度/3D状态。全局depth range与跨段排序因此参与证据 | +| 版本与生命周期 | 精确结果revision、成功Build revision、成功Reuse状态;绑定content/topology/config、camera矩阵、viewport、pixelRatio、crossSourceCollisions;原update signature/SourceRevisionBook/Annotation Gate继续使用 | + +每个结果字节缓冲上限16MiB,超限拒绝证书;没有提高公共容量或假装完整证明。资源引用避免地址ABA;比较对象是字节序列而不是散列。既有segment cache revision组合仍保留,但Fast还直接核对证书状态,不能靠组合散列碰撞取得证书;Full Reuse也要求candidateHasBuild精确版本,缺匹配Build时显式invalidate旧Cache,再走Build。 + +## 4. Placement与fade语义 + +没有修改placementIsRecent、setPlacementStale、原hasTransitions、commit/fadeStartTime、updatePeriod或placement计算频率。原hasTransitions仍用于正常renderer请求刷新。新增内部hasActiveSymbolFade仅表示实际fade;retainedRangeIsStable、retainedSegmentIsStable、retainedPlacementTransitionActive和Fast动态输入改用它。 + +GL三个原生Symbol vertex shader和Metal symbol.hpp三类shader均使用unpack后的base opacity加/减fadeChange并clamp到[0,1]。>=1在合法packed值上足以达到fade终点,但不能证明顶点、uniform、atlas或层序相同。因此没有单独用>=1放行,而是逐opacity检查+完整结果比较+提交代次。 + +重算仍令placementChanged=true,诊断继续如实记录;它不再被当成最终GPU输入改变。recent/stale仍请求刷新,不能单独代表正在fade。Fast rebase保留被证书确认的Full fade值,不把它清为0来取得准入。 + +## 5. 完整准入与失效 + +首次Build路径:已完成Full证据 → 下一Full同绑定同输入且已settled → Build编码成功 → present/endFrame及复核 → Build证书。Reuse/Fast路径:同版本缓存 → 正常Full Reuse成功提交 → 原24Gate全部通过 → Fast。preparedSceneSupported与global/perSegment稳定性均使用同一结果证明,不存在只改perSegment条件的旁路。 + +以下继续fail-closed:missing Source、CustomMVTVector未知revision、tile fading、未loaded style/sprite/image manager、paint/light transition、非粒子repaint、3D/debug/非continuous、不支持C、Annotation pending、相机/viewport/pixelRatio/非相机参数、异步content或pending changes、资源/编译/上传/绘制错误、未知placement-capable类型或shader(Symbol原生typeInfo指针必须匹配,名称相同不足以强制转换)、证据超限/不可读取。 + +Glyph requested/error/loaded、Tile error/changed、Symbol error新增或保持epoch推进;Image missing/remove/update、Source差分、Context reset、Style clear沿用原保护。捕获到的renderer/registry错误推进错误代次与全局content epoch;逃出上传/编码/present的异常由Renderer外围捕获,清prepared/证书、失效所有旧Cache后原样抛出;证书不能使用失败更新的旧Drawables。Build前和成功提交后都有epoch复核,证据观察期间的变化拒绝完成证书。 + +现有回调/registry操作运行在renderer调度线程;content revision是原有atomic。此优化不建立后台并发访问Drawable的通道,不改变callback线程合同。真正跨线程竞态仍需RDC平台实测。 + +## 6. 反例与保守限制 + +- 同placed布尔但variable anchor/角度/opacity不同:实际属性字节变化,revision递增,重新Full取证。 +- controller不fade但尚未上传/陌生shader/atlas未绑定:complete=false,拒绝证书。 +- glyph/icon纹理指针不变但内容更新:content epoch变化,旧Build/ReUse证书失配。 +- Source同名替换、原63层缺tide:SourceRevisionBook/loaded/缺源保护仍拒绝,不能借空RenderItem作零贡献证明。 +- 下方段删除group导致上方R ordinal/depth改变:全局group数量/ordinal证据变化,上方R不得复用旧结果。 +- Build存在但没有成功Full Reuse:canFast=false;失败/未完成的Full不能刷新成功提交证据。 +- Vulkan/WebGPU目前没有本轮原生shader provenance登记,Symbol结果证书保守拒绝;它们的普通Full渲染不变。本轮不宣称跨后端运行通过,也不以GL代替Metal验证。 +- 16MiB上限、非常大的PBF、持续Glyph/Tile更新、不同native实例buffer生命周期均可能继续阻止Build;须保留真实结果修复,不能提高测试阈值制造PASS。 + +## 7. 测试与待证事实 + +新增13项Step119SymbolResults,位于已登记的两个test/renderer文件。覆盖真实Controller lifecycle+SymbolBucket opacity/dynamic buffer、packed clamp反例、variable位置/角度、camera/viewport/pixelRatio、CollisionGroups、真实uniform/wrapped tile编码、未知shader、失败提交、Annotation与24Gate、真实Tile/Glyph/Image/Context/Style回调、Source同名替换/缺失和跨段registry移除。 + +所有新增测试NOT COMPILED/NOT RUN。部分Build标记和字节payload是状态机fixture,绝不称真实GPU等价;现有CrossTileSymbolLayerIndex和Annotations图像测试必须由RDC一并验证。原PBF跨段collision、实际Glyph/Atlas延迟、Full/Fast图像、真实Build/Reuse/Fast和性能仍全部待独立验收。源码去除逻辑自举死锁,不承诺本机实机目标已实现。 diff --git a/docs/40_任务/STEP-119_Symbol稳定结果证书与R首次Build_Fast准入_Codex任务_v1.md b/docs/40_任务/STEP-119_Symbol稳定结果证书与R首次Build_Fast准入_Codex任务_v1.md new file mode 100644 index 000000000..3ba664560 --- /dev/null +++ b/docs/40_任务/STEP-119_Symbol稳定结果证书与R首次Build_Fast准入_Codex任务_v1.md @@ -0,0 +1,33 @@ +# STEP-119 Symbol稳定结果证书与R首次Build/Fast安全准入 + +日期2026-10-11。用户批准立即继续实施上一轮讨论的Shared Core方案,不是只读调查。 +项目 /Users/tei/dev/MapLibreParticleLab,particle/main基线869930046b11dbeb3b147e3be952a2e847316292;Integration /Users/tei/dev/navsea-particle-integration已同步origin/main=80667d6c343411d03a760425438e3d292f452118。实际Codex须GPT-6.1-Sol High(gpt-6.1-sol,model_reasoning_effort=high)。 +Codex负责开发、定向非Jest测试源码、中文文档,不执行Gradle/ADB/Native大编译/Bazel/Jest/commit/push。RDC独立核验、限定Git收尾并按用户授权集中执行一次APK build、install和真实设备验证。 + +## 优先读取 +根AGENTS.md、PARTICLE_BASELINE.md、docs/00_项目规范、docs/00_项目总则README和4份当前总则;Integration的instructions/PARTICLE_PROJECT_INSTRUCTIONS.md、README.md、WORKFLOW.md、coordination/TEAM_MODEL.md、contracts/、rfc/、adr/、status/、compatibility/。STEP117根因调查 docs/20_调查/STEP-117_PBF_R首次缓存Build安全证据_v1.md 与STEP118真机报告 docs/60_测试/STEP-118_潮汐清理新APK设备实测结果_20261011.md及STEP112-116关键报告。必须逐段核对src/mln/text/placement.cpp、render_orchestrator.cpp、renderer_impl.cpp、render_tree.cpp、retained_underlay_cache.cpp、include/mln/renderer/render_segment.hpp、RenderSymbolLayer/SymbolBucket/atlas/shader、24FastGate/RevisionBook及现有测试。 + +## 已证实 +STEP118原生产63基础PBF图层测试副本只排除一条失效tide-spots(用户允许),保留其它62基础层、天气2层=Native64;R[0,62)、C[62,64),missingSourceIds=[]、R fallback=static-placement、R Cache Build0/Reuse0;90.008s FastDelta0 FullDelta942;PlacementGatePassed=false,24Gate mismatch0。当前源码perSegment Gate对Symbol要求 !placementChanged && symbolFadeChange==0 && !placementTransitionActive;Full新排布置changed=true,recent复用设stale->hasTransitions=true,而Fade完全稳定时通常>=1而非0。独立global retainedPlacementStable/retainedStaticStable/retainedSupported/particlePreparedSceneSupported又阻断。首次Build与Fast复用需要分开。 +旧设备真实证据在Git忽略artifacts/maplibre-particle/android-validation/step113-retest-20261011_140448/static_rc_no_tide/result.json和screen.png。新测试APK源commit=3051196054c7b69815c298040dae61d26499d9f2、SHA=44afcfabe0cf4f1ec93f35899241bd73093aa6db4b5cb82ca2120dd531b8f3e9。现HEAD较新只包含报告。 + +## 本次必须优先真正实施的内容 +1. 在平台无关的Shared Core内部区分Placement计算调用与最终Renderable Symbol结果变化,建立可检验稳定状态、revisions/epoch/证书。不能用单一布尔值代替真正GPU输入一致性,不能把placed结果布尔不变当成vertices/opacity/atlas全部一致。保留跨瓦片collision、text/icon、variable anchors/方向、fade、glyph/icon/atlas、shader uniform/tweaker及绘制层序语义。必须检查GL和Metal两侧shader的symbolFadeChange clamp行为。普通Full应完成placement、updateBuckets/vertices、processChanges、tweakers/uploads,才决定可安全建立R缓存(同次或下一帧)。Build/reuse需要匹配版本,任何可影响像素的异步状态立即invalid并走Full。 +2. 解决Placement::placementIsRecent / setPlacementStale / hasTransitions语义和symbolFadeChange的>=1状态对首次Build的逻辑死锁。重新计算 != 真正GPU结果变化;stale不是强制正在fade。fade未完成仍必须阻断,所有真实动态Symbol/tiles/glyphs/Source/style/camera/viewport/pixelRatio/context/Annotation更新都fail-closed。不得简单常置false或单改renderSegmentPlacementGatePassed、删除24Gate、修改计数或强行把其它Symbol放进C。 +3. 必须同时审查并修复global/perSegment稳定性、revision/state跟踪和preparedSceneSupported,让首次R在安全的Full后建立Build,Reuse后才能Fast。缺失证明的Symbol子类严格拒绝,不能用概括性稳定状态假设可缓存。尽可能真正做到原62基础层静止时Build>0、Reuse>0、FastDelta>0;若架构上无法证明,明示BLOCKED,安全停止危险路径,但应尽量完成有证明的实现。 +4. 非Jest C++定向测试(已登记test/renderer文件),覆盖真实PlacementController生命周期、fresh->settled->build->reuse、fade过程拒绝、相机/缩放/旋转、纹理字形异步、新Source/style、Annotation、Context、tile/wrap/crossTile collisions、跨段LayerGroup安全重建;不能把无GPU predicate当真正GPU等价。 +5. 不改RN/JNI/ObjC/Java/Kotlin公共API与ABI、正式PBF/Source/MapLibre外部URL或NavSea/iOS业务、不变更Core-owned timeline/playback、不能改天气层语义。跨项目公共接口新语义须Integration RFC/ADR授权,必要则只报告并保留旧合同。 +6. 保持原STEP113 63 PBF和新STEP118 STATIC_RC_NO_TIDE派生测试、原24Gate、现有真实version/SHA的严格核对。不能新增虚构tide数据。 +7. 不触碰用户两个未跟踪 docs/40_任务/STEP-110_...与STEP-111_...,不git add . / add -A / reset --hard / clean;不修改Git忽略旧真机证据。Codex不运行Gradle/ADB/Jest/Native大编译、也不commit/push,最后交RDC。 + +## 必做中文输出文件(缺一不得完成) +- 实际Core实现+必要非Jest C++测试源码及直接相关诊断/README改动。 +- docs/20_调查/STEP-119_Symbol结果证书与R缓存Build实现依据_v1.md:真实渲染生命周期、核心不变量、结果证书如何绑定真实GPU绘制、反例、风险。 +- docs/50_Step记录/STEP-119_Symbol缓存实际实现_v1.md:状态、变更、验证、下一步。 +- docs/60_测试/STEP-119_原PBF_FAST实机联合验收计划_v1.md:一次build/install,新STATIC_RC_NO_TIDE >=90秒真实Build/Reuse/Fast+24Gate mismatch0,原63负对照及source/style/camera/glyph/annotation/context负例,视觉核验与fail closed。 +- docs/报告/STEP-119_Symbol缓存_Codex完成报告_v1.md:真实模型档位、文件、已实施/未实施、清楚BLOCKED或IMPLEMENTED_WAITING_ANDROID、静态检查、RDC命令、git commit由RDC负责。 +- 粒子开发记录.md在末尾中文追加。 +Codex只跑轻量静态范围和git diff --check;不可宣称Native/Android/iOS/用户视觉PASS。 + +## RDC严格实机目标 +有实质Shared Core行为修改且独立审计通过后,Mac标准 ./tools/particle_android_validation/build_validation_apk.sh 一次Android OpenGL x86_64 APK;设备FZG1-2 ADB serial 192.168.200.139:5555且全部命令显式-s;运行 python3 -B tools/particle_android_validation/verify_step113_fast_path.py --scenario STATIC_RC_NO_TIDE --seconds 90。必须真实R Build>0、Reuse>0、FastDelta>0、Symbol画面/24Gate一致、更新时缓存失效与Full正确。失败最多5轮当前步骤修复,不能跳过并声称通过。无行为改动则不重编毫无意义APK。 diff --git a/docs/50_Step记录/STEP-119_Symbol缓存实际实现_v1.md b/docs/50_Step记录/STEP-119_Symbol缓存实际实现_v1.md new file mode 100644 index 000000000..8a4674a5f --- /dev/null +++ b/docs/50_Step记录/STEP-119_Symbol缓存实际实现_v1.md @@ -0,0 +1,51 @@ +# STEP-119 Symbol缓存实际实现 v1 + +日期:2026-10-11(JST)。状态:进行中;代码阶段IMPLEMENTED_WAITING_ANDROID,独立验收未完成。 + +## 目标与基线 + +实现Shared Core内部最终Symbol结果证书,解决静止真实PBF派生R首次Build逻辑自举死锁,支持后续Reuse/Fast,同时保护所有真实渲染失效与原24Gate。 + +particle/main@869930046b11dbeb3b147e3be952a2e847316292;Mac.lan/Darwin24.6.0/x86_64。Native13.6.1-particle.23;Integration本地HEAD80667d6c343411d03a760425438e3d292f452118,只读。实际会话rollout 01a1299f-0c2b-78b0-8599-e50812c108dd的turn_context核实gpt-6.1-sol/high,未升级、未启动子代理。 + +## 必做项 + +- [x] 读取根规则、规范/总则、当前任务、Integration规则与STEP112–118关键证据,核对实际Renderer/Placement/Bucket/Atlas/GL/Metal。 +- [x] 实现真实上传输入证据、revision/epoch、完成Full/Build/Reuse绑定,global/perSegment/preparedScene同时接线。 +- [x] 分离stale刷新需求与真实fade;保留原Placement算法/原Style/Source/公开API/ABI。 +- [x] 完成13项非Jest C++针对性测试源码,两个文件均已登记。 +- [x] 中文调查、阶段记录、实机联合验收计划、完成报告与开发记录追加。 +- [x] 轻量静态范围/原证据哈希/git diff --check检查。 +- [ ] Native编译与13项GTest实际运行,GL/Metal真实代码路径独立验证。 +- [ ] 同源新APK/设备SHA与>=90秒真实Build>0、Reuse>0、FastDelta>0、24Gate mismatch0。 +- [ ] 原63缺tide负对照、全部失效矩阵、Full/Fast视觉与iOS/Metal独立验收。 + +## 允许与禁止范围 + +仅内部Shared Core/原生shader来源薄接入、已登记C++测试与本Step文档。未修改include公开头、RN/JNI/ObjC/Java/Kotlin、正式PBF/URL/Style、天气语义、Core-owned timeline/playback、发行版本或Integration协议。 + +没有Gradle/ADB/Native构建/Bazel/Jest/codegen/commit/push;没有更改STEP110/111未跟踪任务书或Git忽略原设备证据。没有修改长期项目总则、RDC Runner或后台执行器。 + +## 当前结果 + +两次成功Full取证的同一settled GPU输入版本可建立R Build,成功Full Reuse后才允许Fast。真实opacity clamp、动态/paint属性、UBO、索引/排序/tiles、atlas/shader身份、registry ordinal/depth与异步epoch共同参与;缺失证明仍Full。 + +普通Full销毁旧RenderTree时保留复制证据,真正reset时清理;否则两帧比较仍会自举失败。原24Gate evaluator/掩码/顺序/计数和公开诊断结构没有修改。STEP117历史保守helper与断言保留,生产接线改为最终结果证书。 + +源码没有运行验证,不宣称当前设备已从STEP118的Build0/Reuse0/Fast0改善。证书明确绑定编码/提交成功,GPU完成与像素仍需独立验收。Vulkan/WebGPU无本轮原生shader来源登记,Symbol证书继续fail-closed,普通Full不变。 + +## 阻塞与下一步 + +本工作区未锁定源码;既有验证器会拒绝tracked dirty,不能绕过。历史Mac缺libuv,本轮没有重跑配置。新的Native可编译性、真实PBF输入是否低于16MiB、实际GPU等价、异步竞态与性能均待证。 + +立即交RDC按本Step联合验收计划独立审计、在其授权范围完成源码锁/定向Native验证,并集中一次APK build/install/真实设备验收。失败保存证据,按固定Codex修复→RDC验证闭环;未通过不得标完成/冻结,不自行执行下一阶段。 + +## RDC独立编译检查记录(2026-10-11 JST) + +用户明确要求编译然后测试,并要求不要因本次功能无新第三方依赖而破坏已存在原生环境。RDC先同步核实Integration origin/main=80667d6c343411d03a760425438e3d292f452118与Particle origin/particle/main=869930046b11dbeb3b147e3be952a2e847316292,且保护的两份STEP110/111未跟踪MD的SHA未变。 + +Mac Metal原生CMake配置首次因PATH找不到cmake而失败,改用既有/usr/local/opt/cmake/bin/cmake后,配置因早已缺少的libuv失败(不是本轮代码引入的依赖)。Homebrew建议连带升级CMake、Python和OpenSSL,RDC及时中断该安装并复核原CMake仍存在、无libuv新依赖安装。**Mac Metal CMake/GTest仍未运行,不能记PASS。** + +改走原项目已存在 Android NDK OpenGL/x86_64 compile_commands.json,保持实际C++20警告/Werror、不加Wno-error、不切宏、不改源数据。编译实际Core生产7文件(placement.cpp、render_symbol_layer.cpp、symbol_layer_tweaker.cpp、render_orchestrator.cpp、renderer_impl.cpp、renderer.cpp、shader_program_gl.cpp)与两个非Jest已注册GTest源码,合计9个编译单元,经过本地定点修复后全部严格 -fsyntax-only exit0(每文件独立日志保存在Git忽略artifacts/maplibre-particle/step119-native-syntax/)。本轮修复3处小编译缺口:SymbolLayer及Retained test各一处toString补上util命名空间,Orchestrator error test将构造结果从const Mutable转换为Immutable,保留测试语义。该结果是**Android NDK语法/类型检查**,非真实链接、GTest运行或Metal构建。 + +下一关:RDC scoped Git锁定源码后,基于新源码一次集中标准Android Gradle/OpenGL x86_64 APK构建;FZG1-2安装和STATIC_RC_NO_TIDE 90秒验证。任何编译/设备错误继续按原步修复,不能绕过24Gate或声称Fast已通过。 diff --git a/docs/60_测试/STEP-119_原PBF_FAST实机联合验收计划_v1.md b/docs/60_测试/STEP-119_原PBF_FAST实机联合验收计划_v1.md new file mode 100644 index 000000000..112150f95 --- /dev/null +++ b/docs/60_测试/STEP-119_原PBF_FAST实机联合验收计划_v1.md @@ -0,0 +1,89 @@ +# STEP-119 原PBF FAST实机联合验收计划 v1 + +日期:2026-10-11(JST)。执行者:RDC/用户,Codex本轮仅交源码和命令。状态:NOT RUN;没有新的Native、Android、Metal或视觉PASS。 + +## 1. 先独立审计,再锁定源码 + +读取AGENTS、STEP119任务、调查、Step、完成报告及实际diff,检查原Style/API/ABI/24Gate/SourceRevisionBook、原STEP117/118证据与两个保护任务书。重点核对:普通Full不清结果证据、证书在upload之后采集、成功present之后完成、Build不能借Reuse自举、Reuse成功后才Fast、所有缺证/变更/错误仍拒绝。 + +此工作区尚有本轮未提交源码。现有verify_step113_fast_path.py严格要求tracked clean与APK来源HEAD一致,会报SOURCE_NOT_LOCKED;本轮保留该保护,不能添加dirty bypass、借旧APK或只提交报告来通过预检。源码锁定由RDC按其阶段明确授权scoped处理;Codex没有commit/push。任何Git收尾不得包含STEP110/111、无关dirty或旧ignored artifacts;若push已获授权,必须回读远端HEAD。 + +如果仍无法审计实际GPU输入覆盖、shader provenance、提交/恢复或异步线程边界,保持BLOCKED,不进入APK周期。 + +## 2. Native定向验证(仅交RDC执行) + +在本轮最终锁定源码上,逐条执行;任一步失败立即停止后续命令并保存首个真实error、target、exit、耗时、完整日志路径。旧Mac缺libuv是历史环境证据,本轮未重试/未安装依赖,不能推断当前结果。 + +```bash +cd /Users/tei/dev/MapLibreParticleLab +cmake --preset macos-metal +cmake --build build-macos-metal --target mbgl-test-runner --parallel 3 +build-macos-metal/mbgl-test-runner --gtest_filter='Step119SymbolResults.*' --gtest_list_tests +build-macos-metal/mbgl-test-runner --gtest_filter='Step119SymbolResults.*:Step117Safety.*:Step116Safety.*:Step114FastPathSafety.*:Annotations.*:CrossTileSymbolLayerIndex.*:RenderOrchestratorError.*:RenderOrchestratorTeardown.*:RetainedUnderlayCache.*:RetainedUnderlayRevision.*:FastGateTrace.*:ParticleFastPath.*:RenderSegmentPlacementFree.*:RenderSegmentLocality.*:RenderSegmentPlan.*:RenderSegmentDiagnostics.*' --gtest_color=no +``` + +必须发现并运行13个新增Step119SymbolResults;不得用零匹配/旧runner/仅fsyntax-only称GTest PASS。renderer=Metal、target=mbgl-test-runner、并行3;Host Native测试不能替代iOS真机或Android OpenGL。本轮没有Jest,没有Bazel/codegen授权。 + +RDC还须按已有Android OpenGL/x86_64真实compile_commands.json独立核查修改的Core/GL及两个测试文件,保持-Werror,不附加-Wno-error;语法结果单独记录。Metal context/provenance/UBO consolidation和原生source组合须编译实际路径,不能靠宏关闭绕过。 + +## 3. 一次集中APK build/install与严格来源闭环 + +独立审计及源码锁通过后,由RDC/用户按本阶段授权执行一次标准流水线;任何失败不得安装旧文件,不重复启动并发构建。本轮不是正式发行,不改VERSION/Source Lock/公开坐标。 + +```bash +cd /Users/tei/dev/MapLibreParticleLab +./tools/particle_android_validation/build_validation_apk.sh +adb -s 192.168.200.139:5555 install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --serial 192.168.200.139:5555 --preflight-only +python3 -B tools/particle_android_validation/verify_step113_fast_path.py --serial 192.168.200.139:5555 --scenario STATIC_RC_NO_TIDE --seconds 90 +``` + +固定FZG1-2、x86_64、OpenGL Debug、Java17。ADB每条显式-s,包括人工补采样命令。流水线记录build-info、branch/HEAD/dirty、完整build.log、APK大小/ZIP/ABI/strip及SHA;设备base.apk SHA必须与本次本地APK相同。版本号仍.23不足以判定新源码,旧3051196054/44afcfabe… APK不得冒充本轮。 + +原STEP118 result.json与screen.png只读保留,不覆盖、移动或加入Git。现有verifier固定生成新的step113-retest-/目录(Git忽略);保留此真实输出路径,在RDC交付报告标记为STEP119本轮证据,不改脚本或覆盖旧目录。 + +## 4. 静止真实PBF正向验收 + +| 项目 | 必须取得的真实结果 | +|---|---| +| 原始资源 | 相同正式PBF/Sprite/Glyph/Tile URI,不修改服务器或造tide;manifest保留原63基础层 | +| 派生场景 | STATIC_RC_NO_TIDE,唯一排除tide-spots/tide;有效基础62+天气2,Native64,R[0,62)、C[62,64),missingSourceIds=[] | +| 启动/settle | 正常Full完成原生Placement/vertices/opacity/atlas/UBO;fade未完成拒绝;不得预置Cache有效 | +| 首次Build | R真实Build>0,texture/composite创建与Build后的static/build revision匹配,placementGatePassed=true,截图仍包含其余Symbol | +| Reuse | R真实Reuse>0;成功Full Reuse先于Fast完成,不能只在状态机测试手工mark | +| Fast | PID确认之后持续观察>=90秒,FastDelta>0;FullDelta、真实窗口和生命周期分别记录,不把生命周期累计当区间增量 | +| 24Gate | 完整120帧knownMask=0xffffff,decisionMismatch=0;candidate帧24项全部通过、completed fast-rendered;失败帧保留全部失败位 | +| 像素/层序 | 同相机和资源对照强制Full与开启retained后的截图;用户核对航标/文字/图标/重叠/方向/沿线/variable anchors及WeatherColor/Particle层序,无缺失或ghost | + +自动验证器已有真实Build/Reuse/Fast检查,不改判据;完整120帧通过现有EXPORT LAST SNAPSHOT取得。保存场景开始、Build后、Reuse后及Fast稳定的快照/日志,pre是上一完成树、post是当前完成树,不能跨帧拼接条件。没有图像证据时即使计数增长仍为PARTIAL。 + +## 5. 原63层负对照与变更矩阵 + +在同一个新APK中运行原STATIC_RC至少90秒,原63基础层+天气2保持;missingSourceIds=[tide]、missing-source、R不Build、不Reuse、不Fast。这是缺源负对照,不是需要被此次证书放行的正向目标。PBF63/PARTICLE_OFF/PARTIAL_RCRR/SOURCE_CHANGE原入口与定义保留,不能用STEP114烟测替代。 + +每项动作保存操作前、动作提交时和稳定后证据;至少保留一次真实失效后的Full与新Build/Reuse。500ms之后的摘要不足以证明瞬态,必要时现有完整环形快照与平台trace补证。 + +| 负例 | 安全预期与证据 | +|---|---| +| Source数据/同名Source替换/恢复 | Native source/global revision与lastMutation变化;旧证书失配、Full先执行;不能复用同名旧GPU结果 | +| Style布局/paint/显隐/层序、glyphURL/fontFaces | signature或结果/segment revision变化;真实颜色/透明度/位置正确;恢复仍重新取证 | +| pan/zoom/rotate/pitch、手势/动画 | #7/#8或动态保护失效,旧camera结果不composite;停稳重新Build,Symbol没有跳位/碰撞残影 | +| viewport/pixelRatio | #6/#8及新binding,缓存按新尺寸重建,没有拉伸或旧FBO | +| Glyph/Icon/Atlas延迟加载与替换 | requested/error/loaded/image epoch实际更新;opacity/vertex/atlas已上传才取证,旧纹理不可凭指针复用 | +| text/icon fade、variable anchor/方向/沿线 | fade过程中拒绝,settled不靠fade==0;新顶点/UBO或排序改变时旧证书无效 | +| tile边界/world wrap/crossTile碰撞/跨Source | 正常全场Placement不拆段,异步tile epoch和最终多段结果对应;碰撞受影响的另一R也正确重建 | +| 跨段LayerGroup添加/移除/隐藏Symbol | pending先拒绝;processChanges后全局数量/ordinal/深度和注册关系更新,不能使用旧上方R | +| 真实Annotation添加/更新/删除 | #24或Source/update输入失效,先Full;原SDK helper恢复正确、没有遗漏Annotation。执行既有Annotations.Step116图像测试 | +| context丢失/后台恢复、缓存关闭再开、Style clear/reload | GPU资源与prepared结果重置,旧证书不能跨代;退出没有旧Drawables/资源悬挂 | +| shader替换/未知placement-capable类型/不可读或超限输入 | strict拒绝证书,普通Full继续;不能用相同shader名或空数据取得证明 | +| 上传/绘制/提交错误 | error/content revision变化,失败Full不记已完成证明,保存失败现场 | + +设备缺少某个可控入口时写NOT VERIFIED并给出具体限制,不新增业务数据、不修改正式Style或公开接口。完整测试源码的predicate/模拟buffer只验证局部不变量,不能替代这张设备矩阵。 + +## 6. 结果判定、修复闭环与交付证据 + +实现审计、Native构建、GTest、APK自验、来源/设备SHA、90秒正向、原63负对照、各负例、视觉、Metal/iOS分别记录。全部必需项实际通过之前不写全任务PASS。 + +FastDelta=0、Build=0、Reuse=0、24Gate mismatch非零或可见Symbol异常:保存新的result.json、完整120帧、screenshots、crash log、source HEAD/APK与设备SHA,报告FAIL/PARTIAL/BLOCKED,不降低阈值。最多5轮当前阶段明确修复;编译/测试首次真error保留,架构或公共协议问题交Integration,不能跳下一阶段或改Contract。 + +RDC回传必要信息:命令、exit、耗时、首错/target及日志路径、实际用例数量、正负场景Layer/Source/R/C清单、Build/Reuse/Full/Fast增量、24Gate window、frame输入来源、视觉判定与未验项目。本计划命令全部尚未由Codex运行。 diff --git a/docs/报告/STEP-119_Symbol缓存_Codex完成报告_v1.md b/docs/报告/STEP-119_Symbol缓存_Codex完成报告_v1.md new file mode 100644 index 000000000..4cf845465 --- /dev/null +++ b/docs/报告/STEP-119_Symbol缓存_Codex完成报告_v1.md @@ -0,0 +1,88 @@ +# STEP-119 Symbol缓存 Codex完成报告 v1 + +日期:2026-10-11(JST)。结论:**IMPLEMENTED_WAITING_ANDROID**。Shared Core实现与13项非Jest测试源码已完成;Native可编译性、测试运行、真实R Build/Reuse/Fast和视觉结果均未验收,不能写全任务PASS。现阶段立即交RDC独立审计与验证。 + +## 1. 真实执行环境与授权 + +- 仓库:/Users/tei/dev/MapLibreParticleLab;Mac.lan,Darwin24.6.0,x86_64。 +- 分支:particle/main;开工/交付HEAD均为869930046b11dbeb3b147e3be952a2e847316292,本轮没有Git提交或push。 +- 实际模型/推理档位:gpt-6.1-sol / high,来自本会话rollout的turn_context核实,未升级、未启动子代理或另一个Codex实例。核实文件:/Users/tei/.codex/sessions/2026/10/11/rollout-2026-10-11T15-20-51-01a1299f-0c2b-78b0-8599-e50812c108dd.jsonl。 +- Native版本仍13.6.1-particle.23,未更改发行坐标。Integration本地HEAD80667d6c343411d03a760425438e3d292f452118;已读取当前全部要求的协作规则/协议,只读,无跨仓写入。 +- 当前任务仅内部Renderer实现、两个已登记测试文件与四份中文文档/开发记录。禁止的构建、设备、Jest及Git写操作全部未执行。 + +## 2. 已实施的行为 + +原死锁是多个条件共同导致:新Placement令changed=true、recent令stale/hasTransitions=true、已结束fade通常>=1,并且global/perSegment/preparedScene存在独立阻断。本轮实现最终绘制输入证书,没有只改单个布尔条件。 + +正常Full保持原全场Placement/crossTile碰撞、updateLayerBuckets、动态/opacity/paint顶点、processChanges和原Style语义。先运行原生Symbol tweaker及上传,核查UBO容量、共享属性GPU背板与非dirty索引,再复制实际UBO、Drawable绑定属性、text/icon索引与segments/排序、wrap tile、atlas/texture/shader身份、实际混合/剔除/深度状态、全局组数/ordinal和原注册关系。精确字节比较不以placed布尔或hash代替GPU输入。 + +同绑定、同settled输入与上一成功提交Full相等时准入首次Build;成功present/endFrame及再次复核后,建立匹配Build revision。下一成功Full Reuse之后才Fast。Full Reuse也检查精确candidateHasBuild,防止组合revision碰撞借旧结果。普通Full替换旧prepared tree保留复制证据;真实reset仍清空。global稳定性、perSegment Gate和preparedSceneSupported全部接入同一证书。 + +新增内部hasActiveSymbolFade区分刷新stale与真实fade,旧hasTransitions继续用于正常刷新。GL/Metal原生opacity clamp逐顶点检查,未结束fade/异常packed值拒绝;Fast保留被认证的Full fade值。只有实际编译的原生GL/Metal Symbol源码及合法uniform宏才能登记shader来源,名字相同的未知shader拒绝。 + +Source/style/tile/glyph/image/atlas/camera/viewport/pixelRatio/context/Annotation/update signature、ready与异步epoch保护继续保留;补上Glyph requested/error、Tile/Symbol error。注册/绘制错误推进error/content代次;上传/编码/提交异常清旧缓存和证书后原样抛出。未知Symbol类型须原生typeInfo身份一致;pending/旧隐藏组/collision额外组/不可读或超16MiB输入均保守拒绝。原24Gate evaluator、顺序、mask、诊断公开结构和计数判据未修改。 + +## 3. 实际修改文件(共25个) + +| 文件 | 变更用途 | +|---|---| +| src/mln/renderer/symbol_render_result.hpp(新增) | 精确输入、合法fade终点、绑定、完成Full/Build/Reuse证书 | +| src/mln/renderer/symbol_render_inputs.hpp(新增) | 实际vertex/instance属性字节与不可变buffer身份 | +| src/mln/renderer/symbol_shader_provenance.hpp(新增) | 原生shader来源及uniform宏白名单 | +| src/mln/renderer/layers/render_symbol_layer.hpp | 内部结果采集入口与原group身份 | +| src/mln/renderer/layers/render_symbol_layer.cpp | 原生Symbol实际Drawable/Bucket/Atlas/索引/绘制状态取证 | +| src/mln/renderer/layers/symbol_layer_tweaker.hpp | 实际UBO证据与执行帧记录 | +| src/mln/renderer/layers/symbol_layer_tweaker.cpp | 复制实际上传字段,覆盖consolidation两路径 | +| src/mln/text/placement.hpp | 内部真实fade查询声明 | +| src/mln/text/placement.cpp | 真实fade查询,保留原stale/placement频率 | +| src/mln/renderer/render_orchestrator.hpp | 内部采集/error代次与visitor错误失效 | +| src/mln/renderer/render_orchestrator.cpp | registry/source/global取证、真fade与异步epoch | +| src/mln/renderer/renderer_impl.hpp | 证书容器及独立完成signature | +| src/mln/renderer/renderer_impl.cpp | 上传后准入、global/perSegment/prepared/Fast、Build/Reuse/完成复核 | +| src/mln/renderer/renderer.cpp | 普通Full保留证据,逃出编码/提交异常清缓存 | +| src/mln/renderer/render_tree.hpp | Fast保留认证fade参数 | +| src/mln/shaders/gl/shader_program_gl.cpp | 原生GL编译来源薄接入 | +| src/mln/mtl/context.cpp | 原生Metal源码/entry/宏薄接入 | +| src/mln/shaders/mtl/shader_program.cpp | Metal来源生命周期撤销 | +| test/renderer/retained_underlay_cache.test.cpp | 新增9项Step119SymbolResults | +| test/renderer/render_orchestrator_error.test.cpp | 新增4项Step119SymbolResults | +| docs/20_调查/STEP-119_Symbol结果证书与R缓存Build实现依据_v1.md(新增) | 生命周期、不变量、反例、限制 | +| docs/50_Step记录/STEP-119_Symbol缓存实际实现_v1.md(新增) | 本阶段状态、已做/未验与下一步 | +| docs/60_测试/STEP-119_原PBF_FAST实机联合验收计划_v1.md(新增) | RDC精确命令、来源锁、正负场景/失效/视觉矩阵 | +| docs/报告/STEP-119_Symbol缓存_Codex完成报告_v1.md(新增) | 本报告 | +| 粒子开发记录.md | 仅末尾追加STEP119中文记录 | + +没有修改include公开API头、RN/JNI/ObjC/Java/Kotlin、正式PBF/Style/URL、STEP118派生场景、Integration协议、天气层或Core-owned timeline/playback。未修改测试登记/build规范、验证器、STEP110/111任务书、历史报告与ignored原设备证据。 + +## 4. 已执行检查与结果 + +- 仓库/Integration规则、当前源码/任务与STEP112–118关键证据阅读;实际branch/HEAD/status/version、模型turn_context核实。 +- 轻量静态接线复核:Full/Build/Reuse/Fast、原24Gate、SourceRevisionBook、GL/Metal source与clamp、reset/error路径、测试登记与验收命令。 +- git diff --check:exit0;这是空白检查,不是C++编译或测试。 +- 13项新增TEST源码静态计数,两个文件已有CMake登记;实际共享CPU vector无GPU背板/已release无背板反例纳入失败测试;未修改STEP117/118既有断言。 +- 静态文件范围、开发记录仅追加、公开头/Style/设备证据及两个保护任务书哈希核查。具体保护SHA见下节。 + +本轮未执行CMake/Ninja/Native编译、fsyntax-only、GTest、Metal/iOS运行、Gradle/APK构建、ADB安装/设备命令、Bazel/codegen、Jest、commit/push或网络远端HEAD回读。没有新的APK/设备SHA、GPU fence、截图或计数结果;没有凭进程退出或静态检查声称PASS。未产生新的编译/测试失败日志;旧libuv环境问题仅作为历史限制。 + +## 5. 原证据与保护边界 + +STEP118旧真机事实:原63→派生62基础Layer+天气2=Native64,R[0,62)、C[62,64),missingSourceIds=[];fallback=static-placement,Build0/Reuse0;90.00806697399821秒FastDelta0/FullDelta942、24Gate mismatch0。旧APK源3051196054c7b69815c298040dae61d26499d9f2,SHA44afcfabe0cf4f1ec93f35899241bd73093aa6db4b5cb82ca2120dd531b8f3e9。这些是旧失败证据,不能改写成本轮成功。 + +| 只读保护文件 | SHA-256 | +|---|---| +| 未跟踪STEP110任务MD | 33037df71b4b30aa7956d59f4b99f77af3085405b24111bc0fe11ec857a93db0 | +| 未跟踪STEP111任务MD | f3a5806f25384bf46055db2edfea0d21bfbc4124184bc813b46e38b8079c1ec4 | +| artifacts/maplibre-particle/android-validation/step113-retest-20261011_140448/static_rc_no_tide/result.json | d3d20fc35bbd17e9f0ea0dd014ca4af8e0c5c5284d9f0c009bc20981d876ff10 | +| 同目录screen.png | c8b57a8432626c9209ca9a614a747728d4256bd09273b28c26a793a75a6d44d4 | + +## 6. 未验收、阻塞和RDC交接 + +- 当前源码尚未由RDC独立审计/锁定。现有verifier要求tracked clean以及APK来源HEAD/本地与设备SHA一致,会对当前dirty报SOURCE_NOT_LOCKED;这不是本轮已运行报错,而是读取代码确认的预检。Codex无commit/push授权,不绕过保护,Git收尾由RDC在其明确阶段授权内scoped负责。 +- 所有新增C++代码/13项测试仍NOT COMPILED/NOT RUN。状态机fixture的手工Build标记、模拟payload/CollisionGroups不等于真实Symbol GPU、碰撞或像素等价,必须另跑真实路径。 +- Android原STATIC_RC_NO_TIDE Build>0/Reuse>0/FastDelta>0尚未证实;原63缺tide负对照仍应拒绝。Source/Style/相机/异步/Annotation/Context矩阵与Full/Fast视觉全部待证。 +- Metal/iOS实际source/UBO/instancing路径尚未验证;Vulkan/WebGPU无本轮provenance登记,Symbol缓存继续保守拒绝,普通Full不变。 +- 16MiB输入上限、数据量、取证CPU/内存成本、真实异步竞态与平台GPU完成仍是待测限制。超限或缺证明继续Full,不能调低验收判据制造通过。 + +RDC先读本报告/实际diff,按[联合验收计划](../60_测试/STEP-119_原PBF_FAST实机联合验收计划_v1.md)锁源码并执行准确命令:macos-metal配置、mbgl-test-runner编译、13项发现与定向GTest;Android GL真实编译参数核查。通过后集中一次标准APK流水线/install,以显式serial 192.168.200.139:5555验证STATIC_RC_NO_TIDE至少90秒、真实R Build/Reuse/Fast与120帧24Gate mismatch0,再做原63负对照及变更/视觉矩阵。 + +RDC须保留新锁定HEAD、本地APK/设备SHA、命令/exit/耗时/日志、测试数量、Layer/Source/R/C清单、Build/Reuse/Full/Fast区间增量、完整24Gate快照、截图与用户视觉结果。失败保存首个真实错误/现场,最多5轮当前Step修复,不跳后续阶段。源码交付到此停止;独立验收尚未完成。 diff --git a/src/mln/mtl/context.cpp b/src/mln/mtl/context.cpp index 71f084d6f..83a81abc8 100644 --- a/src/mln/mtl/context.cpp +++ b/src/mln/mtl/context.cpp @@ -19,6 +19,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -182,6 +185,26 @@ UniqueShaderProgram Context::createProgram(shaders::BuiltIn shaderID, std::move(name), backend, std::move(vertexFunction), std::move(fragmentFunction)); observer->onPostCompileShader(shaderID, gfx::Backend::Type::Metal, defineStr); + const auto matchesSymbol = [&]() { + using Source = shaders::ShaderSource; + return source == std::string(shaders::prelude) + Source::prelude + Source::source && + vertexName == Source::vertexMainFunction && fragmentName == Source::fragmentMainFunction; + }; + bool originalSymbol = false; + switch (shaderID) { + case shaders::BuiltIn::SymbolIconShader: + originalSymbol = matchesSymbol.template operator()(); break; + case shaders::BuiltIn::SymbolSDFShader: + originalSymbol = matchesSymbol.template operator()(); break; + case shaders::BuiltIn::SymbolTextAndIconShader: + originalSymbol = matchesSymbol.template operator()(); break; + default: break; + } + for (const auto& [name_, value] : additionalDefines) { + originalSymbol = originalSymbol && symbolUniformDefine(name_) && value.empty(); + } + if (originalSymbol) SymbolShaderProvenance::get().compiledOwned(*shader); + return shader; } diff --git a/src/mln/renderer/layers/render_symbol_layer.cpp b/src/mln/renderer/layers/render_symbol_layer.cpp index cbfea31be..2b8ed5e86 100644 --- a/src/mln/renderer/layers/render_symbol_layer.cpp +++ b/src/mln/renderer/layers/render_symbol_layer.cpp @@ -26,6 +26,8 @@ #include #include #include +#include +#include #include #include #include @@ -473,6 +475,147 @@ std::size_t RenderSymbolLayer::removeAllDrawables() { return stats.drawablesRemoved - oldValue; } +SymbolRenderResult RenderSymbolLayer::retainedRenderResult(float fadeChange, uint64_t frame) { + SymbolRenderResult result; + result.append(layerIndex); + result.append(passes); + result.append(isLayerRenderable()); + // A hidden layer is admitted only after processChanges has removed both + // groups (the orchestrator verifies registry membership separately). + if (!isLayerRenderable()) return result; + if (collisionTileLayerGroup && !collisionTileLayerGroup->empty()) result.complete = false; + if (!layerGroup || layerGroup->empty()) { + // Empty is provable only if there are no renderable Symbol buckets. + if (renderTiles) for (const auto& tileRef : *renderTiles) { + const auto* bucket = static_cast(tileRef.get().getBucket(*baseImpl)); + if (bucket && (bucket->hasTextData() || bucket->hasIconData() || bucket->hasSdfIconData())) { + result.complete = false; + } + } + return result; + } + if (!layerTweaker) { + result.complete = false; + return result; + } + // RenderSymbolLayer is final and creates only this tweaker and the native + // atlas tweaker. Unknown placement-capable layers never call this method. + const auto& tweaker = static_cast(*layerTweaker); + if (tweaker.getRenderResultFrame() != frame) result.complete = false; + result.appendResult(tweaker.getRenderResult()); + result.retain(layerGroup); + result.append(layerGroup->getEnabled()); + result.append(layerGroup->getDrawableCount()); + std::size_t visited = 0; + visitLayerGroupDrawables(*layerGroup, [&](gfx::Drawable& drawable) { + ++visited; + if (drawable.getIsCustom() || drawable.getLayerTweaker() != layerTweaker || + !drawable.getBucket() || !drawable.getTileID() || !drawable.getData() || + !drawable.getShader() || drawable.getTweakers().size() != 1) { + result.complete = false; + return; + } + result.append(drawable.getID()); + if (!SymbolShaderProvenance::get().supports(*drawable.getShader())) result.complete = false; + result.retain(drawable.getTweakers().front()); // immutable native atlas/sampler policy + result.retain(drawable.getShader()); + result.retain(drawable.getBucket()); + const auto tileID = util::toString(*drawable.getTileID()); + result.appendBlock(tileID.data(), tileID.size()); + result.append(drawable.getDrawPriority()); + result.append(drawable.getRenderPass()); + result.append(drawable.getEnabled()); + result.append(drawable.getEnableColor()); + result.append(drawable.getEnableDepth()); + result.append(drawable.getEnableStencil()); + result.append(drawable.getSubLayerIndex()); + result.append(drawable.getDepthType()); + result.append(drawable.getIs3D()); + const auto& cull = drawable.getCullFaceMode(); + result.append(cull.enabled); + result.append(cull.side); + result.append(cull.winding); + const auto& color = drawable.getColorMode(); + color.blendFunction.match([&](const auto& blend) { + result.append(blend.equation); + result.append(blend.srcFactor); + result.append(blend.dstFactor); + }); + result.append(color.blendColor); + result.append(color.mask.r); + result.append(color.mask.g); + result.append(color.mask.b); + result.append(color.mask.a); + result.append(drawable.getUBOIndex()); + appendSymbolAttributes(result, drawable.getVertexAttributes()); + appendSymbolAttributes(result, drawable.getInstanceAttributes()); + + const auto& bucket = static_cast(*drawable.getBucket()); + const auto& data = static_cast(*drawable.getData()); + const auto& buffer = data.symbolType == SymbolType::Text ? bucket.text : + data.symbolType == SymbolType::IconSDF ? bucket.sdfIcon : bucket.icon; + const auto& opacity = buffer.opacityAttributeData(); + if (opacity.empty()) result.complete = false; + for (const auto& vertex : opacity.vector()) { + if (!symbolOpacityIsSettled(vertex.a1[0], fadeChange)) result.complete = false; + } + result.append(buffer.segments.size()); + for (const auto& segment : buffer.segments) { + result.append(segment.vertexOffset); + result.append(segment.indexOffset); + result.append(segment.vertexLength); + result.append(segment.indexLength); + result.append(segment.baseInstance); + result.append(segment.instanceCount); + result.append(segment.sortKey); + } +#if MLN_USE_SYMBOL_INSTANCING + const auto& indices = staticDataIndices; +#else + const auto& indices = buffer.sharedTriangles; +#endif + if (!indices) { + result.complete = false; + } else { + if (!indices->getBuffer() || indices->getDirty()) result.complete = false; + result.append(indices->isReleased()); + if (indices->isReleased()) { + result.retain(indices); + result.append(reinterpret_cast(indices->getBuffer())); + if (!indices->getBuffer()) result.complete = false; + } else { + result.appendBlock(indices->data(), indices->bytes()); + if (indices->empty()) result.complete = false; + } + } + const auto* tile = drawable.getRenderTile(); + const auto& atlases = tile ? tile->getAtlasTextures() : TileAtlasTexturesPtr{}; + if (!atlases) { + result.complete = false; + return; + } + result.retain(atlases); + result.retain(atlases->glyph); + result.retain(atlases->icon); + for (std::size_t id = 0; id < shaders::maxTextureCountPerShader; ++id) { + const auto& texture = drawable.getTexture(id); + result.retain(texture); + if (texture) { + result.append(texture->getSize()); + result.append(texture->getFormat()); + if (texture->needsUpload()) result.complete = false; + } + } + const auto& expected = data.symbolType == SymbolType::Text ? atlases->glyph : atlases->icon; + if (!expected || expected->needsUpload() || + drawable.getTexture(shaders::idSymbolImageTexture) != expected) result.complete = false; + if (drawable.getShader()->getSamplerLocation(shaders::idSymbolImageIconTexture) && + drawable.getTexture(shaders::idSymbolImageIconTexture) != atlases->icon) result.complete = false; + }); + if (visited != layerGroup->getDrawableCount()) result.complete = false; + return result; +} + void RenderSymbolLayer::update(gfx::ShaderRegistry& shaders, gfx::Context& context, const TransformState& state, diff --git a/src/mln/renderer/layers/render_symbol_layer.hpp b/src/mln/renderer/layers/render_symbol_layer.hpp index cb75470d8..6dc9c8cfd 100644 --- a/src/mln/renderer/layers/render_symbol_layer.hpp +++ b/src/mln/renderer/layers/render_symbol_layer.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -57,6 +58,9 @@ public: /// Remove all the drawables for tiles std::size_t removeAllDrawables() override; + // Called after Full update/processChanges/tweakers/uploads, before Build. + SymbolRenderResult retainedRenderResult(float fadeChange, uint64_t frame); + const LayerGroupBase* retainedResultGroup() const { return layerGroup.get(); } protected: /// @brief Called by the RenderOrchestrator during RenderTree construction. diff --git a/src/mln/renderer/layers/symbol_layer_tweaker.cpp b/src/mln/renderer/layers/symbol_layer_tweaker.cpp index 152cd7d3b..2bdc86b8f 100644 --- a/src/mln/renderer/layers/symbol_layer_tweaker.cpp +++ b/src/mln/renderer/layers/symbol_layer_tweaker.cpp @@ -53,6 +53,8 @@ auto getInterpFactor(const SymbolBucket::PaintProperties& paintProps, bool isTex } // namespace void SymbolLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParameters& parameters) { + renderResult = {}; + renderResultFrame = parameters.frameCount; if (layerGroup.empty()) { return; } @@ -84,10 +86,16 @@ void SymbolLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParamete .icon_halo_blur = constOrDefault(evaluated), .pad2 = 0}; context.emplaceOrUpdateUniformBuffer(evaluatedPropsUniformBuffer, &propsUBO); + evaluatedResult = {}; + evaluatedResult.append(propsUBO); propertiesUpdated = false; } auto& layerUniforms = layerGroup.mutableUniformBuffers(); layerUniforms.set(idSymbolEvaluatedPropsUBO, evaluatedPropsUniformBuffer); + if (!evaluatedPropsUniformBuffer || evaluatedPropsUniformBuffer->getSize() < sizeof(SymbolEvaluatedPropsUBO)) { + renderResult.complete = false; + } + renderResult.appendResult(evaluatedResult); #if MLN_UBO_CONSOLIDATION int i = 0; @@ -102,6 +110,7 @@ void SymbolLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParamete visitLayerGroupDrawables(layerGroup, [&](gfx::Drawable& drawable) { if (!drawable.getTileID() || !drawable.getData()) { + renderResult.complete = false; return; } @@ -115,6 +124,7 @@ void SymbolLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParamete const auto bucket = std::static_pointer_cast(drawable.getBucket()); const auto* tile = drawable.getRenderTile(); if (!bucket || !tile || (!textBinders && !iconBinders)) { + renderResult.complete = false; assert(false); return; } @@ -205,12 +215,29 @@ void SymbolLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParamete .pad1 = 0, }; +#if MLN_UBO_CONSOLIDATION + // Capture the same CPU inputs passed to the consolidated GPU buffers. + const auto& resultDrawableUBO = drawableUBOVector[i]; + const auto& resultTileUBO = tilePropsUBOVector[i]; +#else + const auto& resultDrawableUBO = drawableUBO; + const auto& resultTileUBO = tilePropsUBO; +#endif + renderResult.append(drawable.getID()); + // SymbolDrawableUBO has twelve bytes of trailing alignment padding. + // Compare shader fields only, never indeterminate padding. + renderResult.appendBytes(&resultDrawableUBO, offsetof(SymbolDrawableUBO, halo_blur_t) + sizeof(float)); + renderResult.append(resultTileUBO); + #if MLN_UBO_CONSOLIDATION drawable.setUBOIndex(i++); #else auto& drawableUniforms = drawable.mutableUniformBuffers(); drawableUniforms.createOrUpdate(idSymbolDrawableUBO, &drawableUBO, context); drawableUniforms.createOrUpdate(idSymbolTilePropsUBO, &tilePropsUBO, context); + if (!drawableUniforms.get(idSymbolDrawableUBO) || !drawableUniforms.get(idSymbolTilePropsUBO)) { + renderResult.complete = false; + } #endif }); @@ -233,6 +260,9 @@ void SymbolLayerTweaker::execute(LayerGroupBase& layerGroup, const PaintParamete layerUniforms.set(idSymbolDrawableUBO, drawableUniformBuffer); layerUniforms.set(idSymbolTilePropsUBO, tilePropsUniformBuffer); + if (!drawableUniformBuffer || !tilePropsUniformBuffer || + drawableUniformBuffer->getSize() < drawableUBOVectorSize || + tilePropsUniformBuffer->getSize() < tilePropsUBOVectorSize) renderResult.complete = false; #endif } diff --git a/src/mln/renderer/layers/symbol_layer_tweaker.hpp b/src/mln/renderer/layers/symbol_layer_tweaker.hpp index 7a7ada2af..7ee31e626 100644 --- a/src/mln/renderer/layers/symbol_layer_tweaker.hpp +++ b/src/mln/renderer/layers/symbol_layer_tweaker.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -19,8 +20,13 @@ public: ~SymbolLayerTweaker() override = default; void execute(LayerGroupBase&, const PaintParameters&) override; + const SymbolRenderResult& getRenderResult() const { return renderResult; } + uint64_t getRenderResultFrame() const { return renderResultFrame; } private: + SymbolRenderResult renderResult; + SymbolRenderResult evaluatedResult; + uint64_t renderResultFrame = 0; gfx::UniformBufferPtr evaluatedPropsUniformBuffer; #if MLN_UBO_CONSOLIDATION diff --git a/src/mln/renderer/render_orchestrator.cpp b/src/mln/renderer/render_orchestrator.cpp index f0110e831..807990391 100644 --- a/src/mln/renderer/render_orchestrator.cpp +++ b/src/mln/renderer/render_orchestrator.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -1143,11 +1144,76 @@ bool RenderOrchestrator::retainedRangeUsesPlacement(int32_t begin, int32_t endEx } bool RenderOrchestrator::retainedRangeHasPlacementCapableLayer(int32_t begin, int32_t endExclusive) const { - return renderSegmentHasPlacementCapableLayer(retainedStyleLayers, begin, endExclusive); + if (begin < 0 || endExclusive < begin || endExclusive > static_cast(orderedLayers.size())) return true; + if (renderSegmentHasPlacementCapableLayer(retainedStyleLayers, begin, endExclusive)) return true; + for (int32_t index = begin; index < endExclusive; ++index) { + if (orderedLayers[index].get().baseImpl->getTypeInfo()->crossTileIndex == + style::LayerTypeInfo::CrossTileIndex::Required) return true; + } + return false; +} + +void RenderOrchestrator::recordRenderError(std::exception_ptr error) { + invalidateRetainedRenderResults(); + observer->onRenderError(error); +} + +void RenderOrchestrator::invalidateRetainedRenderResults() { + ++renderErrorRevision; + advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::LayerGroupMutation); +} + +SymbolRenderResult RenderOrchestrator::retainedSymbolRenderResult( + int32_t begin, int32_t endExclusive, float fadeChange, uint64_t frame) { + SymbolRenderResult result; + result.append(numLayerGroups()); // global depth range depends on all groups + if (begin < 0 || endExclusive > static_cast(orderedLayers.size()) || begin > endExclusive) { + result.complete = false; + return result; + } + // Registry membership is checked after processChanges, including old groups + // of hidden layers. Preserve global ordinals for depth and cross-segment order. + for (int32_t index = begin; index < endExclusive; ++index) { + auto& layer = orderedLayers[index].get(); + const auto* info = layer.baseImpl->getTypeInfo(); + const bool placementCapable = info->type == std::string_view("symbol") || + info->crossTileIndex == style::LayerTypeInfo::CrossTileIndex::Required; + if (!placementCapable) continue; + if (info != style::SymbolLayer::Impl::staticTypeInfo()) { + result.complete = false; + continue; + } + auto& symbol = static_cast(layer); + const auto registered = layerGroupsByLayerIndex.equal_range(index); + std::size_t groups = 0; + for (auto it = registered.first; it != registered.second; ++it) { + ++groups; + if (!symbol.isLayerRenderable() || it->second.get() != symbol.retainedResultGroup()) { + result.complete = false; + } + } + if (groups > 1 || (symbol.isLayerRenderable() && symbol.retainedResultGroup() && groups != 1)) { + result.complete = false; + } + result.appendResult(symbol.retainedRenderResult(fadeChange, frame)); + } + auto it = layerGroupsByLayerIndex.lower_bound(begin); + std::size_t ordinal = static_cast(std::distance(layerGroupsByLayerIndex.begin(), it)); + for (; it != layerGroupsByLayerIndex.end() && it->first < endExclusive; ++it, ++ordinal) { + if (!it->second) { result.complete = false; continue; } + const auto& group = *it->second; + result.append(ordinal); + result.retain(it->second); + result.append(group.getLayerIndex()); + result.append(group.getDrawableCount()); + result.append(group.getEnabled()); + } + result.complete = result.complete && pendingChanges.empty(); + return result; } bool RenderOrchestrator::retainedPlacementTransitionActive(TimePoint timePoint) const { - return placementController.hasTransitions(timePoint); + return placementController.hasActiveSymbolFade(timePoint); } bool RenderOrchestrator::retainedUnderlayIsStable(TimePoint timePoint, int32_t boundary) const { @@ -1156,7 +1222,7 @@ bool RenderOrchestrator::retainedUnderlayIsStable(TimePoint timePoint, int32_t b bool RenderOrchestrator::retainedRangeIsStable(TimePoint timePoint, int32_t begin, int32_t endExclusive) const { if (renderLight.hasTransition()) return false; - if (retainedRangeUsesPlacement(begin, endExclusive) && placementController.hasTransitions(timePoint)) return false; + if (retainedRangeUsesPlacement(begin, endExclusive) && placementController.hasActiveSymbolFade(timePoint)) return false; const auto first = std::clamp(begin, 0, static_cast(orderedLayers.size())); const auto last = std::clamp(endExclusive, first, static_cast(orderedLayers.size())); @@ -1213,7 +1279,7 @@ bool RenderOrchestrator::retainedSegmentIsStable(TimePoint timePoint, int32_t begin, int32_t endExclusive) const { if (renderLight.hasTransition()) return false; - if (retainedRangeUsesPlacement(begin, endExclusive) && placementController.hasTransitions(timePoint)) return false; + if (retainedRangeUsesPlacement(begin, endExclusive) && placementController.hasActiveSymbolFade(timePoint)) return false; const auto first = std::clamp(begin, 0, static_cast(orderedLayers.size())); const auto last = std::clamp(endExclusive, first, static_cast(orderedLayers.size())); for (int32_t index = first; index < last; ++index) { @@ -1285,7 +1351,7 @@ ParticleFastPathDynamicState RenderOrchestrator::particleFastPathDynamicState(Ti } } } - const bool controllerTransition = placementController.hasTransitions(timePoint); + const bool controllerTransition = placementController.hasActiveSymbolFade(timePoint); result.placementTransition = controllerTransition && (retainedStyleLayers.size() != renderLayers.size() || particleFastPathHasPlacementTransition(retainedStyleLayers, controllerTransition)); @@ -1459,7 +1525,7 @@ void RenderOrchestrator::updateLayers(gfx::ShaderRegistry& shaders, try { renderLayer.update(shaders, context, state, updateParameters, paintParameters, renderTree, changes); } catch (...) { - observer->onRenderError(std::current_exception()); + recordRenderError(std::current_exception()); } } addChanges(changes); @@ -1487,7 +1553,7 @@ void RenderOrchestrator::updateParticleLayers(gfx::ShaderRegistry& shaders, try { renderLayer.update(shaders, context, state, updateParameters, paintParameters, renderTree, changes); } catch (...) { - observer->onRenderError(std::current_exception()); + recordRenderError(std::current_exception()); } } addChanges(changes); @@ -1535,6 +1601,7 @@ void RenderOrchestrator::onGlyphsError(const FontStack& fontStack, const GlyphRange& glyphRange, std::exception_ptr error) { MLN_TRACE_FUNC(); + advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::GlyphMutation); std::stringstream ss; ss << "Failed to load glyph range "; @@ -1550,11 +1617,13 @@ void RenderOrchestrator::onGlyphsError(const FontStack& fontStack, } void RenderOrchestrator::onGlyphsRequested(const FontStack& fontStack, const GlyphRange& range) { + advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::GlyphMutation); observer->onGlyphsRequested(fontStack, range); } void RenderOrchestrator::onTileError(RenderSource& source, const OverscaledTileID& tileID, std::exception_ptr error) { MLN_TRACE_FUNC(); + advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::TileMutation, source.baseImpl->id); Log::Error(Event::Style, "Failed to load tile " + util::toString(tileID) + " for source " + source.baseImpl->id + ": " + @@ -1585,7 +1654,8 @@ void RenderOrchestrator::onTileAction(RenderSource&, observer->onTileAction(op, id, sourceID); } -void RenderOrchestrator::onSymbolError(RenderSource&, const std::string& message) { +void RenderOrchestrator::onSymbolError(RenderSource& source, const std::string& message) { + advanceRetainedUnderlayContentRevision(RetainedUnderlayReason::TileMutation, source.baseImpl->id); observer->onSymbolError(message); } diff --git a/src/mln/renderer/render_orchestrator.hpp b/src/mln/renderer/render_orchestrator.hpp index d479bcb22..e9f388729 100644 --- a/src/mln/renderer/render_orchestrator.hpp +++ b/src/mln/renderer/render_orchestrator.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -128,6 +129,10 @@ public: bool retainedUnderlayHasUnsupportedLayer(int32_t boundary) const; bool retainedRangeUsesPlacement(int32_t begin, int32_t endExclusive) const; bool retainedRangeHasPlacementCapableLayer(int32_t begin, int32_t endExclusive) const; + SymbolRenderResult retainedSymbolRenderResult(int32_t begin, int32_t endExclusive, float fadeChange, uint64_t frame); + void recordRenderError(std::exception_ptr); + void invalidateRetainedRenderResults(); + uint64_t getRenderErrorRevision() const { return renderErrorRevision; } bool retainedPlacementTransitionActive(TimePoint timePoint) const; bool retainedRangeIsStable(TimePoint, int32_t begin, int32_t endExclusive) const; struct RetainedSegmentSourceState { @@ -159,7 +164,7 @@ public: try { f(*pair.second); } catch (...) { - observer->onRenderError(std::current_exception()); + recordRenderError(std::current_exception()); } } } @@ -174,7 +179,7 @@ public: if (it->second) { try { f(*it->second, ordinal); } catch (...) { if (failed) *failed = true; - observer->onRenderError(std::current_exception()); + recordRenderError(std::current_exception()); } } } @@ -191,7 +196,7 @@ public: if (last->second) { try { f(*last->second, ordinal); } catch (...) { if (failed) *failed = true; - observer->onRenderError(std::current_exception()); + recordRenderError(std::current_exception()); } } } @@ -206,7 +211,7 @@ public: try { f(*pair.second, ordinal); } catch (...) { - observer->onRenderError(std::current_exception()); + recordRenderError(std::current_exception()); } } ++ordinal; @@ -223,7 +228,7 @@ public: try { f(*it->second, ordinal); } catch (...) { - observer->onRenderError(std::current_exception()); + recordRenderError(std::current_exception()); } } ++ordinal; @@ -241,7 +246,7 @@ public: try { f(*end->second, ordinal); } catch (...) { - observer->onRenderError(std::current_exception()); + recordRenderError(std::current_exception()); } } } @@ -257,7 +262,7 @@ public: try { f(*it->second, ordinal); } catch (...) { - observer->onRenderError(std::current_exception()); + recordRenderError(std::current_exception()); } } } @@ -270,7 +275,7 @@ public: try { f(*rit->second); } catch (...) { - observer->onRenderError(std::current_exception()); + recordRenderError(std::current_exception()); } } } @@ -413,6 +418,7 @@ private: // Vectors with reserved capacity of layerImpls->size() to avoid // reallocation on each frame. std::vector> filteredLayersForSource; + uint64_t renderErrorRevision = 0; RenderLayerReferences orderedLayers; std::vector retainedStyleLayers; ResolvedRenderSegmentPlan renderSegmentPlan; diff --git a/src/mln/renderer/render_tree.hpp b/src/mln/renderer/render_tree.hpp index 87a750871..1b32721ae 100644 --- a/src/mln/renderer/render_tree.hpp +++ b/src/mln/renderer/render_tree.hpp @@ -106,7 +106,8 @@ public: parameters->timePoint = timePoint; parameters->needsRepaint = true; parameters->placementChanged = false; - parameters->symbolFadeChange = 0.0f; + // Preserve the certified Full shader value. Zero would undo a settled + // fade if any certified Symbol input is inspected on the fast frame. startTime = util::MonotonicTimer::now().count(); } diff --git a/src/mln/renderer/renderer.cpp b/src/mln/renderer/renderer.cpp index 4c817db94..a3aec3bbb 100644 --- a/src/mln/renderer/renderer.cpp +++ b/src/mln/renderer/renderer.cpp @@ -53,18 +53,31 @@ void Renderer::render(const std::shared_ptr& updateParameters) auto& context = impl->backend.getContext(); impl->dynamicTextureAtlas = std::make_unique(context); } - if (impl->tryRenderParticleFastFrame(updateParameters)) return; - // A full createRenderTree may replace RenderLayer/RenderSource instances. - // Drop the old prepared tree first so its reference-based render items can - // never outlive the orchestrator objects they describe. - impl->clearPreparedRenderTree(); - if (auto renderTree = impl->orchestrator.createRenderTree(updateParameters, impl->dynamicTextureAtlas)) { - renderTree->prepare(); - impl->render(*renderTree, updateParameters, false); - impl->commitPreparedRenderTree(std::move(renderTree), updateParameters); - } else { + try { + if (impl->tryRenderParticleFastFrame(updateParameters)) return; + // A full createRenderTree may replace RenderLayer/RenderSource instances. + // Drop the old prepared tree first so its reference-based render items can + // never outlive the orchestrator objects they describe. + // Result evidence owns copied inputs/resources, not RenderLayer references. + // Preserve it across ordinary Full replacement; resets still invalidate it. + impl->clearPreparedRenderTree(false); + if (auto renderTree = impl->orchestrator.createRenderTree(updateParameters, impl->dynamicTextureAtlas)) { + renderTree->prepare(); + impl->render(*renderTree, updateParameters, false); + impl->commitPreparedRenderTree(std::move(renderTree), updateParameters); + } else { + impl->clearPreparedRenderTree(); + if (impl->retainedRuntimeState) impl->retainedRuntimeState->completeFastGateAttempt("no-render-tree"); + } + } catch (...) { + // Upload/encode/present failures that escape the visitors must not leave + // a previously valid cache or completed evidence available for retry. impl->clearPreparedRenderTree(); - if (impl->retainedRuntimeState) impl->retainedRuntimeState->completeFastGateAttempt("no-render-tree"); + impl->orchestrator.invalidateRetainedRenderResults(); + for (auto& cache : impl->retainedSegmentCaches) { + if (cache) cache->invalidate("render-error"); + } + throw; } } diff --git a/src/mln/renderer/renderer_impl.cpp b/src/mln/renderer/renderer_impl.cpp index 235d672a1..2b017c6de 100644 --- a/src/mln/renderer/renderer_impl.cpp +++ b/src/mln/renderer/renderer_impl.cpp @@ -131,11 +131,14 @@ void Renderer::Impl::onPostCompileShader(shaders::BuiltIn shaderID, void Renderer::Impl::onShaderCompileFailed(shaders::BuiltIn shaderID, gfx::Backend::Type type, const std::string& additionalDefines) { + for (auto& certificate : retainedSymbolCertificates) certificate.invalidate(); + orchestrator.invalidateRetainedRenderResults(); observer->onShaderCompileFailed(shaderID, type, additionalDefines); } void Renderer::Impl::onRenderError(std::exception_ptr error) { - observer->onRenderError(error); + for (auto& certificate : retainedSymbolCertificates) certificate.invalidate(); + orchestrator.recordRenderError(error); } void Renderer::Impl::setObserver(RendererObserver* observer_) { @@ -249,10 +252,23 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptrcrossSourceCollisions, + }; for (const auto& segment : plan.segments) { if (segment.strategy == RenderSegmentStrategy::Retained) { staticStable = staticStable && orchestrator.retainedRangeIsStable( updateParameters->timePoint, segment.beginIndex, segment.endExclusive); + if (orchestrator.retainedRangeHasPlacementCapableLayer(segment.beginIndex, segment.endExclusive)) { + staticStable = staticStable && segment.segmentIndex < retainedSymbolCertificates.size() && + retainedSymbolCertificates[segment.segmentIndex].canFast(fastSymbolBinding); + } } } @@ -269,10 +285,9 @@ bool Renderer::Impl::tryRenderParticleFastFrame(const std::shared_ptr render const auto& plan = orchestrator.getRenderSegmentPlan(); const auto* continuous = singleContinuousSegment(plan); particlePreparedSignature = makeParticleFastPathUpdateSignature(*updateParameters); + symbolCompletedSignature = particlePreparedSignature; particlePreparedContentRevision = orchestrator.getRetainedUnderlayContentRevision(); particlePreparedTopologyRevision = retainedUnderlayTopologyTracker.current(); particlePreparedStaticRevision = retainedUnderlayRevisionTracker.current(); @@ -322,7 +338,11 @@ void Renderer::Impl::commitPreparedRenderTree(std::unique_ptr render particlePreparedTree.replace(std::move(renderTree)); } -void Renderer::Impl::clearPreparedRenderTree() { +void Renderer::Impl::clearPreparedRenderTree(bool invalidateSymbolResults) { + if (invalidateSymbolResults) { + retainedSymbolCertificates.clear(); + symbolCompletedSignature.reset(); + } particlePreparedTree.reset(); particlePreparedSignature.reset(); particlePreparedContentRevision = 0; @@ -434,6 +454,7 @@ void Renderer::Impl::render(const RenderTree& renderTree, } const auto& renderTreeParameters = renderTree.getParameters(); + const auto renderErrorRevisionAtStart = orchestrator.getRenderErrorRevision(); staticData->has3D = renderTreeParameters.has3D; const Size renderableSize = backend.getDefaultRenderable().getSize(); @@ -538,6 +559,8 @@ void Renderer::Impl::render(const RenderTree& renderTree, retainedSegmentCaches.clear(); retainedSegmentCaches.resize(segmentPlan.segments.size()); retainedSegmentFingerprints.assign(segmentPlan.segments.size(), 0); + retainedSymbolCertificates.clear(); + retainedSymbolCertificates.resize(segmentPlan.segments.size()); bool continuousSeen = false; for (const auto& segment : segmentPlan.segments) { if (segment.strategy == RenderSegmentStrategy::Continuous) { @@ -595,21 +618,107 @@ void Renderer::Impl::render(const RenderTree& renderTree, } } } - bool retainedStaticUsesPlacement = false; + // Upload layer groups + { + const auto uploadPass = parameters.encoder->createUploadPass("layerGroup-upload", + parameters.backend.getDefaultRenderable()); +#if !defined(NDEBUG) + const auto debugGroup = uploadPass->createDebugGroup("layerGroup-upload"); +#endif + + // Update the debug layer groups only on the full path. Debug rendering + // is excluded by the fast-path gate. + if (!particleFastFrame) orchestrator.updateDebugLayerGroups(renderTree, parameters); + + // Tweakers are run in the upload pass so they can set up uniforms. + parameters.currentLayer = 0; + if (particleFastFrame && singleContinuous) { + orchestrator.visitLayerGroupsInRange( + singleContinuous->beginIndex, singleContinuous->endExclusive, + [&](LayerGroupBase& layerGroup, size_t ordinal) { + parameters.currentLayer = static_cast(ordinal); + layerGroup.runTweakers(renderTree, parameters); + }); + } else { + orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { + layerGroup.runTweakers(renderTree, parameters); + parameters.currentLayer++; + }); + parameters.currentLayer = 0; + orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) { + layerGroup.runTweakers(renderTree, parameters); + parameters.currentLayer++; + }); + } + + // Give the layers a chance to upload + if (particleFastFrame && singleContinuous) { + orchestrator.visitLayerGroupsInRange( + singleContinuous->beginIndex, singleContinuous->endExclusive, + [&](LayerGroupBase& layerGroup, size_t) { layerGroup.upload(*uploadPass); }); + } else { + orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); }); + } + + // Give the render targets a chance to upload + orchestrator.visitRenderTargets([&](RenderTarget& renderTarget) { renderTarget.upload(*uploadPass); }); + + // Upload the Debug layer group + if (!particleFastFrame) { + orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); }); + } + } + + if (retainedSymbolCertificates.size() != segmentPlan.segments.size()) { + retainedSymbolCertificates.resize(segmentPlan.segments.size()); + } + const SymbolResultBinding symbolBinding{ + .viewport = parameters.renderableSize, + .camera = retainedCameraMatrix, + .pixelRatio = updateParameters->pixelRatio, + .content = orchestrator.getRetainedUnderlayContentRevision(), + .topology = retainedTopologyRevision, + .config = segmentPlan.configRevision, + .crossSourceCollisions = updateParameters->crossSourceCollisions, + }; + if (!particleFastFrame && symbolCompletedSignature && + !particleFastPathNonCameraInputsMatch(*symbolCompletedSignature, + makeParticleFastPathUpdateSignature(*updateParameters))) { + for (auto& certificate : retainedSymbolCertificates) certificate.invalidate(); + } + std::vector symbolResultGates(segmentPlan.segments.size(), true); + bool retainedPlacementStable = true; + for (const auto& segment : segmentPlan.segments) { + if (segment.strategy != RenderSegmentStrategy::Retained || + !orchestrator.retainedRangeHasPlacementCapableLayer(segment.beginIndex, segment.endExclusive)) continue; + auto& certificate = retainedSymbolCertificates[segment.segmentIndex]; + if (particleFastFrame) { + symbolResultGates[segment.segmentIndex] = certificate.canReuse(symbolBinding); + } else { + const auto source = orchestrator.retainedSegmentSourceState(segment.beginIndex, segment.endExclusive); + const bool ready = renderTreeParameters.loaded && updateParameters->styleLoaded && + updateParameters->spriteLoaded && orchestrator.retainedGlobalResourcesReady() && + source.ready && source.stable && !state.isChanging() && !state.isGestureInProgress() && + !orchestrator.retainedPlacementTransitionActive(renderTreeParameters.timePoint) && + orchestrator.retainedSegmentIsStable(renderTreeParameters.timePoint, + segment.beginIndex, segment.endExclusive) && + orchestrator.getRenderErrorRevision() == renderErrorRevisionAtStart; + symbolResultGates[segment.segmentIndex] = certificate.observeFull( + orchestrator.retainedSymbolRenderResult(segment.beginIndex, segment.endExclusive, + parameters.symbolFadeChange, frameCount), + symbolBinding, ready); + } + retainedPlacementStable = retainedPlacementStable && symbolResultGates[segment.segmentIndex]; + } bool retainedStaticStable = retainedMode; bool retainedUnsupportedStaticLayer = false; for (const auto& segment : segmentPlan.segments) { if (segment.strategy != RenderSegmentStrategy::Retained) continue; - retainedStaticUsesPlacement = retainedStaticUsesPlacement || - orchestrator.retainedRangeUsesPlacement(segment.beginIndex, segment.endExclusive); retainedStaticStable = retainedStaticStable && orchestrator.retainedRangeIsStable( renderTreeParameters.timePoint, segment.beginIndex, segment.endExclusive); retainedUnsupportedStaticLayer = retainedUnsupportedStaticLayer || orchestrator.retainedRangeHasUnsupportedLayer(segment.beginIndex, segment.endExclusive); } - const bool retainedPlacementStable = - !retainedStaticUsesPlacement || - (!renderTreeParameters.placementChanged && renderTreeParameters.symbolFadeChange == 0.0f); const bool placementTransitionActive = orchestrator.retainedPlacementTransitionActive( renderTreeParameters.timePoint); const auto retainedContentRevision = orchestrator.getRetainedUnderlayContentRevision(); @@ -666,7 +775,7 @@ void Renderer::Impl::render(const RenderTree& renderTree, const auto sourceState = orchestrator.retainedSegmentSourceState( segment.beginIndex, segment.endExclusive); segmentSourceStates[segment.segmentIndex] = sourceState; - const auto segmentContentRevision = sourceState.revision ^ + auto segmentContentRevision = sourceState.revision ^ (fingerprint + 0x9e3779b97f4a7c15ULL + (sourceState.revision << 6U) + (sourceState.revision >> 2U)) ^ static_cast(updateParameters->pixelRatio * 1000000.0f); @@ -674,9 +783,12 @@ void Renderer::Impl::render(const RenderTree& renderTree, renderTreeParameters.timePoint, segment.beginIndex, segment.endExclusive); const bool placementCapable = orchestrator.retainedRangeHasPlacementCapableLayer( segment.beginIndex, segment.endExclusive); - const bool segmentPlacementGatePassed = renderSegmentPlacementGatePassed( - placementCapable, renderTreeParameters.placementChanged, - renderTreeParameters.symbolFadeChange, placementTransitionActive); + const bool segmentPlacementGatePassed = !placementCapable || symbolResultGates[segment.segmentIndex]; + if (placementCapable) { + const auto revision = retainedSymbolCertificates[segment.segmentIndex].current(); + segmentContentRevision ^= revision + 0x9e3779b97f4a7c15ULL + + (segmentContentRevision << 6U) + (segmentContentRevision >> 2U); + } segmentPlacementGates[segment.segmentIndex] = segmentPlacementGatePassed; const bool segmentSupported = retainedMode && updateParameters->styleLoaded && updateParameters->spriteLoaded && orchestrator.retainedGlobalResourcesReady() && @@ -698,6 +810,12 @@ void Renderer::Impl::render(const RenderTree& renderTree, : renderTreeParameters.debugOptions != MapDebugOptions::NoDebug ? "debug-options" : updateParameters->mode != MapMode::Continuous ? "map-mode" : nullptr; + // Exact Symbol proof also controls Full Reuse. A combined revision hash + // cannot authorize reuse of a cache built for different result bytes. + if (placementCapable && segmentPlacementGatePassed && cache->isValid() && + !retainedSymbolCertificates[segment.segmentIndex].candidateHasBuild(symbolBinding)) { + cache->invalidate("symbol-result-mutation"); + } decision = cache->evaluate(segmentKeys[segment.segmentIndex], segmentContentRevision, segmentSupported, @@ -712,61 +830,16 @@ void Renderer::Impl::render(const RenderTree& renderTree, } } - // Upload layer groups - { - const auto uploadPass = parameters.encoder->createUploadPass("layerGroup-upload", - parameters.backend.getDefaultRenderable()); -#if !defined(NDEBUG) - const auto debugGroup = uploadPass->createDebugGroup("layerGroup-upload"); -#endif - // Update the debug layer groups only on the full path. Debug rendering - // is excluded by the fast-path gate. - if (!particleFastFrame) orchestrator.updateDebugLayerGroups(renderTree, parameters); - - // Tweakers are run in the upload pass so they can set up uniforms. - parameters.currentLayer = 0; - if (particleFastFrame && singleContinuous) { - orchestrator.visitLayerGroupsInRange( - singleContinuous->beginIndex, singleContinuous->endExclusive, - [&](LayerGroupBase& layerGroup, size_t ordinal) { - parameters.currentLayer = static_cast(ordinal); - layerGroup.runTweakers(renderTree, parameters); - }); - } else { - orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { - layerGroup.runTweakers(renderTree, parameters); - parameters.currentLayer++; - }); - parameters.currentLayer = 0; - orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) { - layerGroup.runTweakers(renderTree, parameters); - parameters.currentLayer++; - }); - } - - // Give the layers a chance to upload - if (particleFastFrame && singleContinuous) { - orchestrator.visitLayerGroupsInRange( - singleContinuous->beginIndex, singleContinuous->endExclusive, - [&](LayerGroupBase& layerGroup, size_t) { layerGroup.upload(*uploadPass); }); - } else { - orchestrator.visitLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); }); - } - - // Give the render targets a chance to upload - orchestrator.visitRenderTargets([&](RenderTarget& renderTarget) { renderTarget.upload(*uploadPass); }); - - // Upload the Debug layer group - if (!particleFastFrame) { - orchestrator.visitDebugLayerGroups([&](LayerGroupBase& layerGroup) { layerGroup.upload(*uploadPass); }); - } - if (retainedMode) { - for (const auto& segment : segmentPlan.segments) { - if (segment.strategy == RenderSegmentStrategy::Retained && - wantsResources(segmentDecisions[segment.segmentIndex])) { - retainedSegmentCaches[segment.segmentIndex]->upload(*uploadPass); - } + // Admit and upload compositors only after the Full Symbol inputs have been + // updated, tweaked and uploaded. Build has no dependency on an old cache. + if (retainedMode) { + const auto uploadPass = parameters.encoder->createUploadPass( + "retained-composite-upload", parameters.backend.getDefaultRenderable()); + for (const auto& segment : segmentPlan.segments) { + if (segment.strategy == RenderSegmentStrategy::Retained && + wantsResources(segmentDecisions[segment.segmentIndex])) { + retainedSegmentCaches[segment.segmentIndex]->upload(*uploadPass); } } } @@ -833,7 +906,8 @@ void Renderer::Impl::render(const RenderTree& renderTree, const auto buildRetainedRange = [&](RetainedUnderlayCache& cache, RetainedUnderlayDecision& decision, int32_t begin, - int32_t endExclusive) { + int32_t endExclusive, + uint32_t segmentIndex) { if (decision != RetainedUnderlayDecision::Build || begin >= endExclusive) return; bool drawFailed = false; bool globalUniformsBound = false; @@ -870,9 +944,14 @@ void Renderer::Impl::render(const RenderTree& renderTree, if (globalUniformsBound && parameters.renderPass) { context.unbindGlobalUniformBuffers(*parameters.renderPass); } - observer->onRenderError(error); + onRenderError(error); parameters.renderPass.reset(); } + if (orchestrator.retainedRangeHasPlacementCapableLayer(begin, endExclusive)) { + drawFailed = drawFailed || orchestrator.getRetainedUnderlayContentRevision() != symbolBinding.content || + !retainedSymbolCertificates[segmentIndex].matchesCandidate( + orchestrator.retainedSymbolRenderResult(begin, endExclusive, parameters.symbolFadeChange, frameCount)); + } if (drawFailed) { cache.markBuildFailed("resource-or-compositor"); decision = RetainedUnderlayDecision::Fallback; @@ -990,12 +1069,23 @@ void Renderer::Impl::render(const RenderTree& renderTree, drawable3DPass(); } drawableTargetsPass(); + if (orchestrator.getRetainedUnderlayContentRevision() != symbolBinding.content || + orchestrator.getRenderErrorRevision() != renderErrorRevisionAtStart) { + particlePreparedSceneSupported = false; + for (const auto& segment : segmentPlan.segments) { + if (segment.strategy != RenderSegmentStrategy::Retained) continue; + retainedSegmentCaches[segment.segmentIndex]->invalidate("content-mutation"); + retainedSymbolCertificates[segment.segmentIndex].invalidate(); + segmentDecisions[segment.segmentIndex] = RetainedUnderlayDecision::Fallback; + } + } for (const auto& segment : segmentPlan.segments) { if (segment.strategy == RenderSegmentStrategy::Retained) { buildRetainedRange(*retainedSegmentCaches[segment.segmentIndex], segmentDecisions[segment.segmentIndex], segment.beginIndex, - segment.endExclusive); + segment.endExclusive, + segment.segmentIndex); } } if (retainedRuntimeState) { @@ -1072,6 +1162,24 @@ void Renderer::Impl::render(const RenderTree& renderTree, parameters.encoder.reset(); context.endFrame(); + if (!particleFastFrame) { + auto completionBinding = symbolBinding; + completionBinding.content = orchestrator.getRetainedUnderlayContentRevision(); + const bool submitted = orchestrator.getRenderErrorRevision() == renderErrorRevisionAtStart; + for (const auto& segment : segmentPlan.segments) { + if (segment.strategy != RenderSegmentStrategy::Retained || + !orchestrator.retainedRangeHasPlacementCapableLayer(segment.beginIndex, segment.endExclusive)) continue; + const auto index = segment.segmentIndex; + const bool resultUnchanged = retainedSymbolCertificates[index].matchesCandidate( + orchestrator.retainedSymbolRenderResult(segment.beginIndex, segment.endExclusive, + parameters.symbolFadeChange, frameCount)); + retainedSymbolCertificates[index].completeFull( + completionBinding, submitted && resultUnchanged, + segmentDecisions[index] == RetainedUnderlayDecision::Build && retainedSegmentCaches[index]->isValid(), + segmentDecisions[index] == RetainedUnderlayDecision::Reuse && retainedSegmentCaches[index]->isValid()); + } + } + #if MLN_RENDER_BACKEND_METAL if constexpr (EnableMetalCapture) { if (commandCaptureScope) { diff --git a/src/mln/renderer/renderer_impl.hpp b/src/mln/renderer/renderer_impl.hpp index 09068813c..da4f44915 100644 --- a/src/mln/renderer/renderer_impl.hpp +++ b/src/mln/renderer/renderer_impl.hpp @@ -49,7 +49,7 @@ private: void synchronizeRetainedUnderlayState(const std::shared_ptr&); bool tryRenderParticleFastFrame(const std::shared_ptr&); void commitPreparedRenderTree(std::unique_ptr, const std::shared_ptr&); - void clearPreparedRenderTree(); + void clearPreparedRenderTree(bool invalidateSymbolResults = true); void reduceMemoryUse(); @@ -64,6 +64,7 @@ private: std::unique_ptr staticData; std::vector> retainedSegmentCaches; std::vector retainedSegmentFingerprints; + std::vector retainedSymbolCertificates; std::shared_ptr retainedRuntimeState; uint64_t retainedCacheTopologyRevision = 0; RetainedUnderlayRevisionTracker retainedUnderlayRevisionTracker; @@ -107,6 +108,7 @@ private: const ParticleFastPathUpdateSignature&); std::optional particlePreparedSignature; + std::optional symbolCompletedSignature; uint64_t particlePreparedContentRevision = 0; uint64_t particlePreparedTopologyRevision = 0; uint64_t particlePreparedStaticRevision = 0; diff --git a/src/mln/renderer/symbol_render_inputs.hpp b/src/mln/renderer/symbol_render_inputs.hpp new file mode 100644 index 000000000..99337123a --- /dev/null +++ b/src/mln/renderer/symbol_render_inputs.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +namespace mln { + +inline void appendSymbolAttributes(SymbolRenderResult& result, const std::shared_ptr& attributes) { + result.append(static_cast(attributes)); + if (!attributes) return; + result.append(attributes->allocatedSize()); + for (std::size_t id = 0; id < attributes->allocatedSize(); ++id) { + const auto& attr = attributes->get(id); + result.append(static_cast(attr)); + if (!attr) continue; + result.append(attr->getIndex()); + result.append(attr->getDataType()); + result.append(attr->getCount()); + result.append(attr->getStride()); + result.append(attr->getSharedType()); + result.append(attr->getSharedOffset()); + result.append(attr->getSharedVertexOffset()); + result.append(attr->getSharedStride()); + const auto& shared = attr->getSharedRawData(); + if (shared) { + // Collection happens after upload; readable CPU bytes alone cannot + // certify an attribute that never obtained a GPU backing buffer. + if (!shared->getBuffer()) result.complete = false; + // Released vectors are immutable by the producer contract; retain + // their identity and GPU backing to prevent pointer ABA. Mutable + // vectors (placement, variable anchors, sort and paint) use bytes. + result.append(shared->isReleased()); + if (shared->isReleased()) { + result.retain(shared); + result.append(reinterpret_cast(shared->getBuffer())); + if (!shared->getBuffer()) result.complete = false; + } else { + result.append(shared->getRawSize()); + result.appendBlock(shared->getRawData(), shared->getRawSize() * shared->getRawCount()); + } + } else { + const auto& raw = attr->getRawData(); + result.appendBlock(raw.data(), raw.size()); + // A non-shared attribute without readable bytes is not a proof. + if (attr->getCount() && raw.empty()) result.complete = false; + } + } +} + +} // namespace mln diff --git a/src/mln/renderer/symbol_render_result.hpp b/src/mln/renderer/symbol_render_result.hpp new file mode 100644 index 000000000..1426bc494 --- /dev/null +++ b/src/mln/renderer/symbol_render_result.hpp @@ -0,0 +1,141 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mln { + +// Internal CPU evidence for the inputs actually consumed by the Symbol shaders. +// This is exact byte comparison, not a hash of placed/unplaced booleans. +struct SymbolRenderResult { + std::vector bytes; + std::vector> resources; + bool complete = true; + static constexpr std::size_t maxBytes = 16 * 1024 * 1024; + + void appendBytes(const void* data, std::size_t size) { + if (!complete || size > maxBytes - bytes.size() || (size && !data)) { + complete = false; + return; + } + if (size) { + const auto* begin = static_cast(data); + bytes.insert(bytes.end(), begin, begin + size); + } + } + template void append(const T& value) { + static_assert(std::is_trivially_copyable_v); + appendBytes(&value, sizeof(value)); + } + void appendBlock(const void* data, std::size_t size) { + append(size); + appendBytes(data, size); + } + template void retain(const std::shared_ptr& resource) { + append(reinterpret_cast(resource.get())); + if (resource) resources.emplace_back(resource); + } + void appendResult(const SymbolRenderResult& result) { + complete = complete && result.complete; + appendBlock(result.bytes.data(), result.bytes.size()); + resources.insert(resources.end(), result.resources.begin(), result.resources.end()); + } +}; + +// GL and Metal unpack the low bit as the fade direction and clamp the +// remaining seven bits / 127 + signed fadeChange to [0,1]. An endpoint in +// the target direction remains constant as time advances. A stale controller +// says "refresh placement", not "the shader is changing opacity". +inline bool symbolOpacityIsSettled(float packed, float fadeChange) { + if (!std::isfinite(packed) || !std::isfinite(fadeChange) || fadeChange < 0 || + packed < 0 || packed > 255 || std::floor(packed) != packed) return false; + const auto value = static_cast(packed); + const float opacity = static_cast(value / 2) / 127.0f; + return (value & 1U) ? opacity + fadeChange >= 1.0f : opacity - fadeChange <= 0.0f; +} + +struct SymbolResultBinding { + Size viewport; + mat4 camera{}; + float pixelRatio = 1; + uint64_t content = 0; + uint64_t topology = 0; + uint64_t config = 0; + bool crossSourceCollisions = true; + bool operator==(const SymbolResultBinding&) const = default; +}; + +// Build admission is independent of a previous cache. It requires two +// completed Full observations of the same settled, uploaded result. Reuse +// additionally requires a Build submitted with this exact result revision. +class SymbolResultCertificate { +public: + bool observeFull(SymbolRenderResult result, const SymbolResultBinding& binding, bool ready) { + inFlight = true; + const bool changed = !candidateBinding || *candidateBinding != binding || + candidate.bytes != result.bytes || !ready || !result.complete; + if (changed) { + ++revision; + builtRevision = 0; + } + candidate = std::move(result); + candidateBinding = binding; + candidateReady = ready && candidate.complete; + stable = candidateReady && completedBinding && *completedBinding == binding && + completed.bytes == candidate.bytes; + return stable; + } + void completeFull(const SymbolResultBinding& binding, bool submitted, bool built, bool reused = false) { + if (!submitted || !candidateReady || !candidateBinding || *candidateBinding != binding) { + invalidate(); + return; + } + completed = candidate; + completedBinding = binding; + inFlight = false; + if (stable && built) { + builtRevision = revision; + reuseSubmitted = false; + } + if (stable && reused && builtRevision == revision) reuseSubmitted = true; + } + bool canReuse(const SymbolResultBinding& binding) const { + return !inFlight && stable && builtRevision == revision && builtRevision != 0 && + completedBinding && *completedBinding == binding; + } + bool canFast(const SymbolResultBinding& binding) const { return canReuse(binding) && reuseSubmitted; } + bool candidateHasBuild(const SymbolResultBinding& binding) const { + return candidateReady && stable && builtRevision != 0 && builtRevision == revision && + candidateBinding && *candidateBinding == binding; + } + bool matchesCandidate(const SymbolRenderResult& result) const { + return candidateReady && result.complete && candidate.bytes == result.bytes; + } + uint64_t current() const { return revision; } + void invalidate() { + ++revision; + stable = candidateReady = false; + inFlight = reuseSubmitted = false; + builtRevision = 0; + completedBinding.reset(); + candidateBinding.reset(); + candidate = {}; + completed = {}; + } + +private: + SymbolRenderResult candidate, completed; + std::optional candidateBinding, completedBinding; + uint64_t revision = 1, builtRevision = 0; + bool candidateReady = false, stable = false; + bool inFlight = false, reuseSubmitted = false; +}; + +} // namespace mln diff --git a/src/mln/renderer/symbol_shader_provenance.hpp b/src/mln/renderer/symbol_shader_provenance.hpp new file mode 100644 index 000000000..0140b63a0 --- /dev/null +++ b/src/mln/renderer/symbol_shader_provenance.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace mln { + +// No public shader API/ABI changes. Only compilation sites that verify the +// original Symbol source can register provenance. Names/types alone cannot +// certify a replacement/custom shader with a different fade contract. +class SymbolShaderProvenance { +public: + static SymbolShaderProvenance& get() { static SymbolShaderProvenance value; return value; } + void compiled(const std::shared_ptr& shader) { + std::lock_guard lock(mutex); + std::erase_if(shared, [](const auto& item) { return item.expired(); }); + shared.emplace_back(shader); + } + void compiledOwned(const gfx::ShaderProgramBase& shader) { + std::lock_guard lock(mutex); + owned.push_back(shader.getID().id()); + } + void destroyedOwned(const gfx::ShaderProgramBase& shader) { + std::lock_guard lock(mutex); + std::erase(owned, shader.getID().id()); + } + bool supports(const gfx::ShaderProgramBase& shader) const { + std::lock_guard lock(mutex); + if (std::find(owned.begin(), owned.end(), shader.getID().id()) != owned.end()) return true; + return std::any_of(shared.begin(), shared.end(), [&](const auto& item) { + const auto program = item.lock(); + return program && program.get() == &shader; + }); + } + +private: + mutable std::mutex mutex; + std::vector> shared; + std::vector owned; +}; + +inline bool symbolUniformDefine(std::string_view name) { + return name == "HAS_UNIFORM_u_opacity" || name == "HAS_UNIFORM_u_fill_color" || + name == "HAS_UNIFORM_u_halo_color" || name == "HAS_UNIFORM_u_halo_width" || + name == "HAS_UNIFORM_u_halo_blur" || name == "HAS_UNIFORM_u_sorted_instance"; +} + +} // namespace mln diff --git a/src/mln/shaders/gl/shader_program_gl.cpp b/src/mln/shaders/gl/shader_program_gl.cpp index d46d0d29b..9f19343c1 100644 --- a/src/mln/shaders/gl/shader_program_gl.cpp +++ b/src/mln/shaders/gl/shader_program_gl.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -177,7 +178,31 @@ std::shared_ptr ShaderProgramGL::create( addAttr(attrs, attributesInfo[location].id, location, length, size, glType); } - return std::make_shared(std::move(program), std::move(attrs), std::move(samplerLocations)); + auto result = std::make_shared(std::move(program), std::move(attrs), std::move(samplerLocations)); + bool originalSymbol = false; + const auto matches = [&]() { + using Source = shaders::ShaderSource; + return vertexSource == Source::vertex && fragmentSource == Source::fragment; + }; + switch (programParameters.getProgramType()) { + case shaders::BuiltIn::SymbolIconShader: + originalSymbol = matches.template operator()(); break; + case shaders::BuiltIn::SymbolSDFShader: + originalSymbol = matches.template operator()(); break; + case shaders::BuiltIn::SymbolTextAndIconShader: + originalSymbol = matches.template operator()(); break; + default: break; + } + std::size_t offset = 0; + while (originalSymbol && offset < additionalDefines.size()) { + const auto end = additionalDefines.find('\n', offset); + const std::string_view line(additionalDefines.data() + offset, + (end == std::string::npos ? additionalDefines.size() : end) - offset); + originalSymbol = line.starts_with("#define ") && symbolUniformDefine(line.substr(8)); + offset = end == std::string::npos ? additionalDefines.size() : end + 1; + } + if (originalSymbol) SymbolShaderProvenance::get().compiled(result); + return result; } catch (const std::exception& e) { context.getObserver().onShaderCompileFailed( programParameters.getProgramType(), gfx::Backend::Type::OpenGL, additionalDefines); diff --git a/src/mln/shaders/mtl/shader_program.cpp b/src/mln/shaders/mtl/shader_program.cpp index 3cc471e3f..eb503a7c3 100644 --- a/src/mln/shaders/mtl/shader_program.cpp +++ b/src/mln/shaders/mtl/shader_program.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -80,7 +81,9 @@ ShaderProgram::ShaderProgram(std::string name, RendererBackend& backend_, MTLFun vertexFunction(std::move(vert)), fragmentFunction(std::move(frag)) {} -ShaderProgram::~ShaderProgram() noexcept = default; +ShaderProgram::~ShaderProgram() noexcept { + SymbolShaderProvenance::get().destroyedOwned(*this); +} MTLRenderPipelineStatePtr ShaderProgram::getRenderPipelineState(const gfx::Renderable& renderable, const MTLVertexDescriptorPtr& vertexDescriptor, diff --git a/src/mln/text/placement.cpp b/src/mln/text/placement.cpp index 2834336fe..dc5a83505 100644 --- a/src/mln/text/placement.cpp +++ b/src/mln/text/placement.cpp @@ -175,6 +175,10 @@ bool PlacementController::hasTransitions(TimePoint now) const { return placement->hasTransitions(now); } +bool PlacementController::hasActiveSymbolFade(TimePoint now) const { + return placement->transitionsEnabled() && placement->hasTransitions(now); +} + // Placement implementation Placement::Placement(std::shared_ptr updateParameters_, diff --git a/src/mln/text/placement.hpp b/src/mln/text/placement.hpp index 48e295d87..4e65d131d 100644 --- a/src/mln/text/placement.hpp +++ b/src/mln/text/placement.hpp @@ -102,6 +102,8 @@ public: void setPlacementStale() { stale = true; } bool placementIsRecent(TimePoint now, float zoom, std::optional periodOverride = std::nullopt) const; bool hasTransitions(TimePoint now) const; + // Cache result stability excludes stale's scheduling-only refresh request. + bool hasActiveSymbolFade(TimePoint now) const; private: Immutable placement; diff --git a/test/renderer/render_orchestrator_error.test.cpp b/test/renderer/render_orchestrator_error.test.cpp index 216cd9f73..ece9414cc 100644 --- a/test/renderer/render_orchestrator_error.test.cpp +++ b/test/renderer/render_orchestrator_error.test.cpp @@ -10,8 +10,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -435,3 +437,137 @@ TEST(Step117Safety, RealAsyncCallbacksAndContextStyleCleanupAdvanceContentEpoch) scheduler.runRenderJobs(); // Actual callback dispatch, but no network delivery, GPU, or Android assertion. } + +namespace { +void step119Certify(SymbolResultCertificate& certificate, SymbolResultBinding binding) { + SymbolRenderResult result; + result.append(255.0f); + EXPECT_FALSE(certificate.observeFull(result, binding, true)); + certificate.completeFull(binding, true, false); + EXPECT_TRUE(certificate.observeFull(result, binding, true)); + certificate.completeFull(binding, true, true); + EXPECT_TRUE(certificate.observeFull(result, binding, true)); + certificate.completeFull(binding, true, false, true); + EXPECT_TRUE(certificate.canFast(binding)); +} +} // namespace + +TEST(Step119SymbolResults, RealTileGlyphImageStyleContextCallbacksRejectBuiltCertificate) { + TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; + RenderOrchestrator orchestrator(false, scheduler, std::nullopt); + std::atomic deferred{0}, jobs{0}, detached{0}; + auto source = std::make_unique(scheduler, deferred, jobs, detached); + const auto rejects = [&](auto event) { + SymbolResultBinding binding; + binding.content = orchestrator.getRetainedUnderlayContentRevision(); + SymbolResultCertificate certificate; + step119Certify(certificate, binding); + event(); + binding.content = orchestrator.getRetainedUnderlayContentRevision(); + EXPECT_FALSE(certificate.canFast(binding)); + }; + rejects([&] { + static_cast(orchestrator).onTileChanged(*source, OverscaledTileID{2, 1, {2, 1, 1}}); + }); + rejects([&] { static_cast(orchestrator).onGlyphsLoaded({}, {0, 255}); }); + rejects([&] { static_cast(orchestrator).onStyleImageMissing("step119-icon", [] {}); }); + rejects([&] { orchestrator.markContextLost(); }); + rejects([&] { orchestrator.clearData(); }); + source->setObserver(nullptr); + source.reset(); + scheduler.waitForEmpty(); + scheduler.runRenderJobs(); +} + +TEST(Step119SymbolResults, AppliedCrossSegmentRegistryRebuildChangesExactEvidence) { + TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; + RenderOrchestrator orchestrator(false, scheduler, std::nullopt); + auto runtime = std::make_shared(); + style::BackgroundLayer lower("step119-lower"), continuous("step119-middle"), upper("step119-upper"); + const Immutable>> layers = + makeMutable>>( + std::initializer_list>{lower.baseImpl, continuous.baseImpl, upper.baseImpl}); + const Immutable>> sources = + makeMutable>>(); + auto tree = orchestrator.createRenderTree(step117TreeParameters(layers, sources, runtime, TimePoint{}), nullptr); + ASSERT_TRUE(tree); + orchestrator.processChanges(); + auto group = std::make_shared(0); + ASSERT_TRUE(orchestrator.addLayerGroup(group)); + ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared(2))); + const auto before = orchestrator.retainedSymbolRenderResult(2, 3, 1, 0); + ASSERT_TRUE(before.complete); + SymbolResultCertificate certificate; + SymbolResultBinding binding; + step119Certify(certificate, binding); + UniqueChangeRequestVec changes; + changes.emplace_back(std::make_unique(group)); + RenderOrchestratorTeardownTestAccess::enqueue(orchestrator, changes); + const auto pending = orchestrator.retainedSymbolRenderResult(2, 3, 1, 0); + EXPECT_FALSE(pending.complete); + EXPECT_FALSE(certificate.observeFull(pending, binding, true)); + orchestrator.processChanges(); + EXPECT_EQ(orchestrator.numLayerGroups(), 1u); + const auto after = orchestrator.retainedSymbolRenderResult(2, 3, 1, 0); + EXPECT_TRUE(after.complete); + EXPECT_NE(before.bytes, after.bytes); // actual global count + original ordinal changed + EXPECT_FALSE(certificate.canFast(binding)); +} + +TEST(Step119SymbolResults, RangeDrawFailureInvalidatesContentAndCompletedEvidence) { + TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; + RenderOrchestrator orchestrator(false, scheduler, std::nullopt); + ErrorObserver observer; + orchestrator.setObserver(&observer); + ASSERT_TRUE(orchestrator.addLayerGroup(std::make_shared(0))); + SymbolResultBinding binding; + binding.content = orchestrator.getRetainedUnderlayContentRevision(); + SymbolResultCertificate certificate; + step119Certify(certificate, binding); + const auto errors = orchestrator.getRenderErrorRevision(); + bool failed = false; + orchestrator.visitLayerGroupsInRange(0, 1, [](LayerGroupBase&, size_t) { + throw std::runtime_error("step119-upload-or-draw"); + }, &failed); + EXPECT_TRUE(failed); + EXPECT_EQ(observer.errors, 1); + EXPECT_GT(orchestrator.getRenderErrorRevision(), errors); + binding.content = orchestrator.getRetainedUnderlayContentRevision(); + EXPECT_FALSE(certificate.canReuse(binding)); + EXPECT_FALSE(certificate.canFast(binding)); +} + +TEST(Step119SymbolResults, RealSourceReplacementAndMissingSourceRetainFailClosed) { + TaggedScheduler scheduler{Scheduler::GetBackground(), {}}; + RenderOrchestrator orchestrator(false, scheduler, std::nullopt); + auto runtime = std::make_shared(); + ASSERT_TRUE(runtime->setRenderSegmentPlan({1, {{RenderSegmentStrategy::Retained, std::nullopt}}})); + style::SymbolLayer symbol("step119-symbol", "step119-source"); + const Immutable>> layers = + makeMutable>>( + std::initializer_list>{symbol.baseImpl}); + const auto sourceList = []() -> Immutable>> { + return makeMutable>>( + std::initializer_list>{ + makeMutable("step119-source", style::GeoJSONOptions::defaultOptions())}); + }; + auto tree = orchestrator.createRenderTree(step117TreeParameters(layers, sourceList(), runtime, TimePoint{}), nullptr); + ASSERT_TRUE(tree); + SymbolResultBinding binding; + binding.content = orchestrator.getRetainedUnderlayContentRevision(); + SymbolResultCertificate certificate; + step119Certify(certificate, binding); + tree.reset(); + tree = orchestrator.createRenderTree(step117TreeParameters(layers, sourceList(), runtime, TimePoint{} + Seconds(1)), nullptr); + ASSERT_TRUE(tree); + binding.content = orchestrator.getRetainedUnderlayContentRevision(); + EXPECT_FALSE(certificate.canFast(binding)); + tree.reset(); + const Immutable>> absent = + makeMutable>>(); + tree = orchestrator.createRenderTree(step117TreeParameters(layers, absent, runtime, TimePoint{} + Seconds(2)), nullptr); + ASSERT_TRUE(tree); + EXPECT_STREQ(orchestrator.retainedSegmentSourceState(0, 1).reason, "missing-source"); + binding.content = orchestrator.getRetainedUnderlayContentRevision(); + EXPECT_FALSE(certificate.canFast(binding)); +} diff --git a/test/renderer/retained_underlay_cache.test.cpp b/test/renderer/retained_underlay_cache.test.cpp index 6640efcac..44e2b66fd 100644 --- a/test/renderer/retained_underlay_cache.test.cpp +++ b/test/renderer/retained_underlay_cache.test.cpp @@ -5,11 +5,18 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include +#include +#include #include #include @@ -98,6 +105,265 @@ std::shared_ptr step117PlacementParameters(TimePoint now, } } // namespace +namespace { +SymbolResultBinding step119Binding() { + SymbolResultBinding binding; + binding.viewport = {640, 480}; + binding.camera = key().cameraMatrix; + binding.content = binding.topology = binding.config = 1; + return binding; +} + +SymbolRenderResult step119BufferResult(const SymbolBucket::Buffer& buffer, float fade) { + SymbolRenderResult result; + const auto& dynamic = buffer.dynamicAttributeData(); + const auto& opacity = buffer.opacityAttributeData(); + result.appendBlock(dynamic.getRawData(), dynamic.bytes()); + result.appendBlock(opacity.getRawData(), opacity.bytes()); + for (const auto& vertex : opacity.vector()) { + result.complete = result.complete && symbolOpacityIsSettled(vertex.a1[0], fade); + } + return result; +} + +void step119Settle(SymbolResultCertificate& certificate, const SymbolRenderResult& result, + const SymbolResultBinding& binding) { + EXPECT_FALSE(certificate.observeFull(result, binding, true)); + certificate.completeFull(binding, true, false); + EXPECT_TRUE(certificate.observeFull(result, binding, true)); + EXPECT_FALSE(certificate.candidateHasBuild(binding)); + certificate.completeFull(binding, true, true); + EXPECT_TRUE(certificate.canReuse(binding)); + EXPECT_FALSE(certificate.canFast(binding)); + EXPECT_TRUE(certificate.observeFull(result, binding, true)); + EXPECT_TRUE(certificate.candidateHasBuild(binding)); + certificate.completeFull(binding, true, false, true); + EXPECT_TRUE(certificate.canFast(binding)); +} +} // namespace + +TEST(Step119SymbolResults, FreshSettledBuildReuseWithRealControllerAndBuffer) { + const auto start = TimePoint{} + Seconds(10); + PlacementController controller; + SymbolBucket::Buffer buffer; + buffer.dynamicAttributeData().emplace_back(SymbolBucket::dynamicLayoutAttributes({20, 30}, 0)); + buffer.opacityAttributeData().emplace_back(SymbolBucket::opacityAttributes(true, 1)); + SymbolResultCertificate certificate; + RetainedUnderlayCache cache; + const auto binding = step119Binding(); + unsigned builds = 0, reuses = 0, calculations = 0; + for (unsigned frame = 0; frame < 50; ++frame) { + const auto now = start + Milliseconds(frame * 30); + if (!controller.placementIsRecent(now, 0)) { + auto placement = Placement::create(step117PlacementParameters(now), controller.getPlacement()); + placement->placeLayers({}); + controller.setPlacement(std::move(placement)); + ++calculations; + } else { + controller.setPlacementStale(); + EXPECT_TRUE(controller.hasTransitions(now)); // scheduling preserved + EXPECT_FALSE(controller.hasActiveSymbolFade(now)); + } + const auto result = step119BufferResult(buffer, controller.getPlacement()->symbolFadeChange(now)); + const bool stable = certificate.observeFull(result, binding, !controller.hasActiveSymbolFade(now)); + const auto decision = cache.evaluate(key(), certificate.current(), stable, "static-placement"); + EXPECT_FALSE(certificate.canFast(binding)); // Full still in flight + if (decision == RetainedUnderlayDecision::Build) { cache.markBuildSucceeded(); ++builds; } + if (decision == RetainedUnderlayDecision::Reuse) ++reuses; + certificate.completeFull(binding, true, decision == RetainedUnderlayDecision::Build, + decision == RetainedUnderlayDecision::Reuse); + } + EXPECT_GT(calculations, 1u); + EXPECT_EQ(builds, 1u); + EXPECT_GT(reuses, 0u); + EXPECT_TRUE(certificate.canFast(binding)); + // Cache submission here is a state-machine fixture, not a GPU draw proof. +} + +TEST(Step119SymbolResults, ActualPackedOpacityRejectsFadeAndAcceptsShaderEndpoints) { + for (unsigned opacity = 0; opacity <= 127; ++opacity) { + const float packedIn = static_cast(opacity * 2 + 1); + const float packedOut = static_cast(opacity * 2); + EXPECT_EQ(symbolOpacityIsSettled(packedIn, 0), opacity == 127); + EXPECT_EQ(symbolOpacityIsSettled(packedOut, 0), opacity == 0); + EXPECT_TRUE(symbolOpacityIsSettled(packedIn, 1)); + EXPECT_TRUE(symbolOpacityIsSettled(packedOut, 1)); + EXPECT_TRUE(symbolOpacityIsSettled(packedIn, 5)); + } + EXPECT_FALSE(symbolOpacityIsSettled(1, 0.5f)); + EXPECT_FALSE(symbolOpacityIsSettled(254, 0.5f)); + EXPECT_FALSE(symbolOpacityIsSettled(1.5f, 1)); + EXPECT_FALSE(symbolOpacityIsSettled(256, 1)); + EXPECT_FALSE(symbolOpacityIsSettled(1, std::numeric_limits::infinity())); + EXPECT_FALSE(symbolOpacityIsSettled(1, std::numeric_limits::quiet_NaN())); + EXPECT_FALSE(symbolOpacityIsSettled(255, -1)); + SymbolBucket::Buffer buffer; + buffer.opacityAttributeData().emplace_back(SymbolBucket::opacityAttributes(true, 0)); + SymbolResultCertificate certificate; + const auto binding = step119Binding(); + const auto fading = step119BufferResult(buffer, 0.5f); + ASSERT_FALSE(fading.complete); + EXPECT_FALSE(certificate.observeFull(fading, binding, true)); + certificate.completeFull(binding, true, true); + EXPECT_FALSE(certificate.canReuse(binding)); +} + +TEST(Step119SymbolResults, SamePlacedBooleanDoesNotProveVariableAnchorOrOrientation) { + SymbolBucket::Buffer buffer; + buffer.dynamicAttributeData().emplace_back(SymbolBucket::dynamicLayoutAttributes({20, 30}, 0)); + buffer.opacityAttributeData().emplace_back(SymbolBucket::opacityAttributes(true, 1)); + const auto binding = step119Binding(); + for (const auto changed : {SymbolBucket::dynamicLayoutAttributes({21, 30}, 0), + SymbolBucket::dynamicLayoutAttributes({20, 30}, 1)}) { + buffer.dynamicAttributeData().at(0) = SymbolBucket::dynamicLayoutAttributes({20, 30}, 0); + SymbolResultCertificate certificate; + step119Settle(certificate, step119BufferResult(buffer, 1), binding); + buffer.dynamicAttributeData().at(0) = changed; + EXPECT_FALSE(certificate.observeFull(step119BufferResult(buffer, 1), binding, true)); + EXPECT_FALSE(certificate.canFast(binding)); + } +} + +TEST(Step119SymbolResults, CameraViewportPixelRatioAndCollisionConfigurationInvalidate) { + SymbolRenderResult result; + result.append(255.0f); + const auto original = step119Binding(); + std::vector updates(7, original); + updates[0].camera[12] += 1; // pan + updates[1].camera[0] *= 2; // zoom + updates[2].camera[1] = 0.5; // rotation/pitch projection + updates[3].viewport.width += 1; + updates[4].pixelRatio = 2; + updates[5].crossSourceCollisions = false; + updates[6].config += 1; + for (const auto& update : updates) { + SymbolResultCertificate certificate; + step119Settle(certificate, result, original); + EXPECT_FALSE(certificate.canFast(update)); + EXPECT_FALSE(certificate.observeFull(result, update, true)); + } + CollisionGroups shared(true), isolated(false); + EXPECT_EQ(shared.get("A").first, shared.get("B").first); + EXPECT_NE(isolated.get("A").first, isolated.get("B").first); +} + +TEST(Step119SymbolResults, FailedUploadDrawSubmissionAndUnreadableResultsCannotCertify) { + SymbolRenderResult result; + result.append(255.0f); + const auto binding = step119Binding(); + SymbolResultCertificate certificate; + EXPECT_FALSE(certificate.observeFull(result, binding, true)); + certificate.completeFull(binding, false, false); + EXPECT_FALSE(certificate.observeFull(result, binding, true)); + certificate.completeFull(binding, true, false); + EXPECT_TRUE(certificate.observeFull(result, binding, true)); + certificate.completeFull(binding, true, false); // resource/compositor failed + EXPECT_FALSE(certificate.canReuse(binding)); + result.complete = false; + EXPECT_FALSE(certificate.observeFull(result, binding, true)); + certificate.completeFull(binding, true, true); + EXPECT_FALSE(certificate.canFast(binding)); + SymbolRenderResult oversize; + oversize.appendBytes(nullptr, SymbolRenderResult::maxBytes + 1); + EXPECT_FALSE(oversize.complete); + auto attributes = std::make_shared(); + const auto& attribute = attributes->set(shaders::idSymbolFadeOpacityAttribute); + ASSERT_TRUE(attribute); + auto opacity = std::make_shared(); + opacity->emplace_back(SymbolBucket::opacityAttributes(true, 1)); + attribute->setSharedRawData(opacity, 0, 0, sizeof(SymbolOpacityAttributes), gfx::AttributeDataType::Float); + SymbolRenderResult notUploaded; + appendSymbolAttributes(notUploaded, attributes); + EXPECT_FALSE(notUploaded.complete); // real CPU vector without a GPU buffer + opacity->release(); + SymbolRenderResult releasedWithoutBacking; + appendSymbolAttributes(releasedWithoutBacking, attributes); + EXPECT_FALSE(releasedWithoutBacking.complete); +} + +TEST(Step119SymbolResults, ExactPayloadAndAtlasIdentityRequireNewResultRevision) { + const auto binding = step119Binding(); + SymbolRenderResult original; + original.append(255.0f); + original.append(0.0f); // uniform / draw ordering test payload + for (unsigned component = 0; component < original.bytes.size(); ++component) { + SymbolResultCertificate certificate; + step119Settle(certificate, original, binding); + auto changed = original; + changed.bytes[component] ^= 1; + const auto revision = certificate.current(); + EXPECT_FALSE(certificate.observeFull(changed, binding, true)); + EXPECT_FALSE(certificate.candidateHasBuild(binding)); + EXPECT_GT(certificate.current(), revision); + EXPECT_FALSE(certificate.matchesCandidate(original)); + } + // Shader/texture identities are kept alive as part of the exact evidence. + auto oldAtlas = std::make_shared(1), newAtlas = std::make_shared(1); + SymbolRenderResult oldResult, newResult; + oldResult.retain(oldAtlas); + newResult.retain(newAtlas); + SymbolResultCertificate certificate; + step119Settle(certificate, oldResult, binding); + EXPECT_FALSE(certificate.observeFull(newResult, binding, true)); + // Payload coverage is not a texture readback or real wrap/collision draw. +} + +TEST(Step119SymbolResults, ActualUniformAndWrappedTileInputsDifferEvenWithSamePlacement) { + shaders::SymbolDrawableUBO uniform{}; + uniform.matrix[0] = 1; + uniform.opacity_t = 0.5; + const auto payload = [](const shaders::SymbolDrawableUBO& ubo, const OverscaledTileID& tile) { + SymbolRenderResult result; + result.appendBytes(&ubo, offsetof(shaders::SymbolDrawableUBO, halo_blur_t) + sizeof(float)); + const auto name = util::toString(tile); + result.appendBlock(name.data(), name.size()); + return result; + }; + const auto binding = step119Binding(); + const OverscaledTileID tile{2, 0, {2, 1, 1}}, wrapped{2, 1, {2, 1, 1}}; + const auto original = payload(uniform, tile); + SymbolResultCertificate certificate; + step119Settle(certificate, original, binding); + EXPECT_FALSE(certificate.matchesCandidate(payload(uniform, wrapped))); + uniform.opacity_t = 0.75; + EXPECT_FALSE(certificate.observeFull(payload(uniform, tile), binding, true)); + EXPECT_FALSE(certificate.canFast(binding)); +} + +TEST(Step119SymbolResults, AnnotationGateBlocksOtherwiseCertifiedResultAndKeeps24GateAgreement) { + SymbolRenderResult result; + result.append(255.0f); + const auto binding = step119Binding(); + SymbolResultCertificate certificate; + step119Settle(certificate, result, binding); + auto input = fastPathInput(); + input.staticStable = certificate.canFast(binding); + ASSERT_TRUE(evaluateParticleFastPath(input).candidate); + input.annotationMutation = true; + EXPECT_FALSE(evaluateParticleFastPath(input).candidate); + EXPECT_EQ(evaluateParticleFastPath(input).reason, ParticleFastPathMissReason::AnnotationMutation); + EXPECT_EQ(particleFastGateFailureMask(input), 1u << 23); +} + +namespace { +class Step119UnknownSymbolProgram final : public gfx::ShaderProgramBase { +public: + const std::string_view typeName() const noexcept override { return "GenericGLShader"; } + std::optional getSamplerLocation(size_t) const override { return std::nullopt; } + const gfx::VertexAttributeArray& getVertexAttributes() const override { throw std::logic_error("unknown shader"); } + const gfx::VertexAttributeArray& getInstanceAttributes() const override { throw std::logic_error("unknown shader"); } +}; +} // namespace + +TEST(Step119SymbolResults, UnknownShaderCannotBorrowNativeFadeContractByName) { + Step119UnknownSymbolProgram replacement; + EXPECT_FALSE(SymbolShaderProvenance::get().supports(replacement)); + EXPECT_TRUE(symbolUniformDefine("HAS_UNIFORM_u_opacity")); + EXPECT_TRUE(symbolUniformDefine("HAS_UNIFORM_u_sorted_instance")); + EXPECT_FALSE(symbolUniformDefine("HAS_UNIFORM_u_opacity 1\n#define main custom")); + EXPECT_FALSE(symbolUniformDefine("HAS_UNIFORM_u_custom_animation")); +} + TEST(RetainedDynamicBoundary, SelectsFirstCandidate) { RetainedDynamicBoundaryTracker none; none.consider(0, false); diff --git a/粒子开发记录.md b/粒子开发记录.md index 9e2a45aaa..7504f7196 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -1187,3 +1187,10 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - 2026-10-11 STEP-118 用户允许删除tide:旧PBF正式sources实际无tide,唯一坏引用为63基础Layer的tide-spots Symbol。RDC新增TestApp独立STATIC_RC_NO_TIDE副本,只排除唯一孤立Layer,原五场景/63正式Style/Native24Gate不变;严格Python verifier校验原63→有效62加天气2(预期Native64)及真实SHA、来源和R边界。正式远端Style未改。待本轮Mac APK与设备实测判断R missing-source是否解除、Symbol/Cache/Fast是否改善。 - 2026-10-11 STEP-118 实机:用户授权后RDC Mac构建Test App源码3051196054,APK SHA 44afcfabe0cf4f1ec93f35899241bd73093aa6db4b5cb82ca2120dd531b8f3e9,FZG1-2设备adb install成功且base.apk SHA相同。新STATIC_RC_NO_TIDE(仅Test App修改)90.008秒:原63→有效62基础Layer、Native64,R[0,62)、missingSourceIds=[]、fallback由missing-source变static-placement,缺tide已真机解除;Cache Build/Reuse0/0,FastDelta0、FullDelta942,PlacementGate=false,整体PARTIAL/原Fast FAIL。报告docs/60_测试/STEP-118_潮汐清理新APK设备实测结果_20261011.md。 + + +- 2026-10-11 STEP-119 Codex(实际gpt-6.1-sol/high已核实):在particle/main@869930046b11dbeb3b147e3be952a2e847316292实现Shared Core最终Symbol结果证书。普通Full先完成全场Placement、vertices、processChanges、tweakers/uploads,再精确比较真实UBO、动态/paint/opacity/索引/排序、wrap、atlas/shader、混合/剔除/全局ordinal与版本;成功提交Full/Build后绑定revision,成功Full Reuse后才Fast。区分stale刷新与实际fade,global/perSegment/preparedScene全部接入;普通Full保留证据、真正reset/异步/错误失效,原24Gate/API/ABI/Style/Source/天气/timeline不改。 +- 新增13项非Jest C++测试源码(两个已登记文件),包含真实Controller/Buffer/回调/Source/registry与失败/Annotation/未知shader反例;全部NOT COMPILED/NOT RUN,fixture不是GPU等价证据。四份中文文档已写,完成报告docs/报告/STEP-119_Symbol缓存_Codex完成报告_v1.md;状态IMPLEMENTED_WAITING_ANDROID,不宣称真机Build/Reuse/Fast改善。16MiB上限、取证性能、Metal/iOS与实际像素/竞态待独立验证,Vulkan/WebGPU Symbol缓存保持保守拒绝。 +- 本轮仅轻量阅读、静态范围/保护哈希与git diff --check;未运行Native/CMake/Ninja/fsyntax-only/GTest、Gradle/APK、ADB/设备、Bazel/codegen/Jest,未commit/push。STEP110/111未跟踪MD、STEP117/118原证据与63/62场景保留。现有verifier源码锁/新APK设备SHA保护不绕过;立即交RDC按STEP119联合验收计划独立审计、源码锁、定向验证及一次集中build/install和>=90秒正负场景/失效/视觉闭环。 + +- 2026-10-11 STEP119 RDC编译核验:Integration与Particle已fetch对应远端主线,保护STEP110/111 SHA不变。Mac CMake配置因既有libuv缺失失败;Homebrew意外尝试连带升级工具链,已停止且原CMake可用,不继续扩大环境修改。复用现有Android NDK OpenGL x86_64精确compile_commands,7个生产Core文件+2个非Jest GTest源码合计9个严格-Werror语法编译最终PASS;修正SymbolLayer/test各一处util::toString命名空间、test Mutable→Immutable。GTest/完整链接/Android真机仍待独立执行,未宣称PASS。