fix: secure Symbol Retained drawable lifetime and record investigations
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
161
docs/40_任务/STEP-110_R0_Placement跨段归因只读调查_Codex任务_v1.md
Normal file
161
docs/40_任务/STEP-110_R0_Placement跨段归因只读调查_Codex任务_v1.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# STEP-110 R0 Placement 跨段归因与缓存安全边界只读调查 — Codex 任务 v1
|
||||||
|
|
||||||
|
## 一、任务性质与核心问题
|
||||||
|
|
||||||
|
**性质:Particle Native / Shared Core 专项,只读调查;不实现、不修复、不编译、不上机。**
|
||||||
|
|
||||||
|
用户要求粒子项目独立回答:
|
||||||
|
|
||||||
|
> Native 能否区分“R0 自己的 Placement 变化”和“其他 RenderSegment 引起的 Placement 变化”?如果能区分,是否足以在**不破坏 Symbol 碰撞、文字/图标位置及图层顺序**的条件下让未变化的 R0 复用缓存?
|
||||||
|
|
||||||
|
必须把以下不同概念分开审查:
|
||||||
|
1. **事件来源**:哪个 source/layer/segment 引发了 placement/repaint/revision;
|
||||||
|
2. **实际影响范围**:全局碰撞计算是否使 R0 的 Symbol 位置、可见性、fade 或绘制结果改变;
|
||||||
|
3. **缓存安全判据**:能否证明 R0 内容/Placement 仍与已缓存画面等价;
|
||||||
|
4. **当前实现**:上述信息目前是否已有、是否按 Segment 使用、还是仅采用全局保守 Gate。
|
||||||
|
|
||||||
|
禁止预设“全局 Placement 变化一定是 Bug”,也禁止以“R0 自己没有提交数据”直接推导“R0 视觉必然没有变化”。
|
||||||
|
|
||||||
|
## 二、仓库、版本与事实优先级
|
||||||
|
|
||||||
|
- 项目:`/Users/tei/dev/MapLibreParticleLab`,正式分支 `particle/main`、远端 `origin/particle/main`。
|
||||||
|
- 创建任务时只读确认:`particle/main` HEAD `2df504cdc0403bdad85d3f31f16fd54a8744abac`、干净工作树。执行时必须**重新读取 Git 实际状态**,不得把此 HEAD 当作永恒基线。
|
||||||
|
- NavSea 产品实机所用 APK 内 `assets/component-manifest.json` 记录:RN / Android Native AAR `13.6.1-particle.23`。V23 正式来源对应 `refs/tags/maplibre-particle/rn/13.6.1-particle.23`、GSRL Git commit `87f3d1c59a52581fb8339f0ea7d106b9f4b94fef`。
|
||||||
|
- **必须分别标明 V23 发布源码与当前 `particle/main` 的事实**。如当前 main 有未发行后续改动,不能以 main 代替 V23,不能以 V23 APK 推断未发行修复已经生效。
|
||||||
|
- 跨项目事实遵循 `D:\React\navsea-particle-integration\instructions\NAVSEA_PROJECT_INSTRUCTIONS.md` 及最新 COORD / RFC / ADR / Contract / Status;当前既有外部契约 C-001。此任务不新增/更改公共 API 或契约。
|
||||||
|
- 粒子项目根 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目规范/`、`docs/00_项目总则/README.md` 与四份有效总则必须先读,按当前项目事实处理过时的历史环境/发布坐标。
|
||||||
|
- 必读既有证据:STEP-088、089、090、092、094、099、100、102、106、108、109;重点对比 STEP-090 早期缓存/快路成功条件、STEP-102 placement-free 分段隔离范围、STEP-106 T1/T2 Symbol 与 Annotation 边界的真实验收。
|
||||||
|
|
||||||
|
## 三、来自 NavSea 的问题证据(仅作为输入,不由本任务修改 NavSea)
|
||||||
|
|
||||||
|
FZG1-2 / Android 13 / V23 当前 APK,NavSea 采用**两段 R-C**:
|
||||||
|
|
||||||
|
- R0:RETAINED,31 层,`background` 至 `place-labels-land-ja`,包含有 Placement 能力的 Symbol;
|
||||||
|
- C1:CONTINUOUS,82 层,`rn-weather-color` 至 `chartDangerScanCenterLineLayer`,包含天气/粒子和其他业务层。
|
||||||
|
|
||||||
|
固定同一风场、同一 map 生命周期,连续约 15.24 秒,`mapReactTag=864`、`generation=4`、`configRevision=1`、`topologyRevision=8`、113 层均不变:
|
||||||
|
- 风场期间进程 CPU 样本均值约 98.6% / 99.1%,关闭天气后约 4%~8%;
|
||||||
|
- `fastPathHit Δ0`、`fastPathMiss Δ452`、`fullCpuFrame Δ453`;
|
||||||
|
- R0 `build Δ0`、`reuse Δ0`、`invalidation Δ453`、`fallbackCount Δ453`、texture/composite create `Δ0`;
|
||||||
|
- 两端 `valid=true`、`fallback=false`、`sourceReady=true`、`sourceStable=true`、`placementChanged=true`、`placementGatePassed=false`、`cache.supported=false`、`cacheState=fallback`、`lastInvalidationReason=static-placement`;
|
||||||
|
- `mainBufferExecutionMode=mixed`、`globalLiveFallbackReason=none`、`fastPathFallbackReason=cache-invalid`。
|
||||||
|
|
||||||
|
**这些是症状,不代表已证明 C1 导致 R0 Placement 改变。** 此外,历史较早的 V23 短时快照曾观察到 `placementChanged=false` 但 R0 记录仍为 `static-placement`,需要区分“当前门槛值”和“上次失效原因/诊断时序”,避免误读。
|
||||||
|
|
||||||
|
Mac 粒子团队如无法访问 Windows 原始快照,可依据上述冻结事实做 Native 只读审计;缺失证据须列为“需要后续验证”,不得自行编造 NavSea 图层或运行结果。
|
||||||
|
|
||||||
|
## 四、必须回答的调查问题
|
||||||
|
|
||||||
|
### Q1:Placement 的产生与传播
|
||||||
|
|
||||||
|
从真实 V23 Native 代码绘制完整的因果链:
|
||||||
|
- 触发 RenderTree / Placement 计算的入口与时机是什么?
|
||||||
|
- `placementChanged`、`symbolFadeChange`、`placementTransitionActive`、`crossSourceCollisions` 分别表示**当前帧/上帧/全局/局部**的什么事实?何时清零或稳定?
|
||||||
|
- Source/Layer 的 layout、tile、visibility、camera、animation 等变化,分别可能影响哪些 Placement 状态?
|
||||||
|
- R0 以外的 C1 Symbol/Annotation/业务层变化,**在哪些条件下**可能影响 R0 的符号碰撞与最终视觉?
|
||||||
|
- 如果 RenderTree 每次都重新算 Placement 但画面没有变化,如何区分“执行过计算”和“R0 最终结果发生变化”?
|
||||||
|
|
||||||
|
### Q2:现有 R0 Gate 的作用域
|
||||||
|
|
||||||
|
调查 V23 实际调用链及数据结构,至少解释:
|
||||||
|
- R0 结构性 `placementCapable` 如何判定,与动态 `needsPlacement()` 有何区别;
|
||||||
|
- R0 `placementGatePassed` 如何使用全局 `placementChanged`、fade、transition;
|
||||||
|
- R0 `cache.supported`、`static-placement`、`actualExecution=live` 的完整因果关系;
|
||||||
|
- R0 的 source-local mutation、segment fingerprint、retained revision 与 placement gate 是否真正各自独立;
|
||||||
|
- STEP-102 对 **placement-free** R 的隔离已解决什么、**placement-bearing** R 仍未解决什么;
|
||||||
|
- `lastInvalidationReason` 与当前失败条件是否可能属于不同采样/缓存生命周期。
|
||||||
|
|
||||||
|
### Q3:能否证明“R0 不受 C1 变化影响”
|
||||||
|
|
||||||
|
针对以下反事实场景逐项作出有证据的判断:
|
||||||
|
1. R0 含静态地名,C1 只有普通非 Symbol 动画/颜色/粒子更新;
|
||||||
|
2. R0 含静态地名,C1 的 Symbol 内容变化,但不同 Source 且跨源碰撞关闭;
|
||||||
|
3. R0 含静态地名,C1 的 Symbol 内容变化且跨源碰撞开启;
|
||||||
|
4. R0 Symbol 自身 layout/tile/visibility/zoom 发生变化;
|
||||||
|
5. Camera/pan/zoom/rotation/pitch 变化;
|
||||||
|
6. Symbol Fade/Placement Transition 活跃或刚结束;
|
||||||
|
7. C1 有内部自动插入的 Annotation Symbol(参照 STEP-106)。
|
||||||
|
|
||||||
|
每行必须分别给出:
|
||||||
|
- “谁触发 Placement”能否识别;
|
||||||
|
- “R0 最终 Placement/可见性是否改变”能否独立证明;
|
||||||
|
- **允许 R0 Reuse 的必要安全条件**;
|
||||||
|
- 当前 V23 是否具备这种判定能力及证据来源;
|
||||||
|
- 无法证明时为什么必须继续 fail-closed。
|
||||||
|
|
||||||
|
严禁把“不同 Segment”简单等同于“碰撞和 Placement 相互独立”。
|
||||||
|
|
||||||
|
### Q4:对 Fast Path 的影响(仅限前置依赖)
|
||||||
|
|
||||||
|
解释“R0 缓存有效”是否为 Fast Path 的必要条件但**非充分条件**。只读标出 R0 Gate 与单 C / unsupported continuous layer / revision / pending changes 等后续 Gate 的先后关系。不要扩大成 C1 全图层性能调查,不实施多 C 优化。
|
||||||
|
|
||||||
|
## 五、只读检查范围与方法
|
||||||
|
|
||||||
|
允许读取:
|
||||||
|
- 当前 Particle Core 的 Renderer、RenderOrchestrator、PlacementController、RenderTree、Symbol/Collision、Retained Cache、RenderSegment Plan、Diagnostics 与 Fast Path 相关源码;
|
||||||
|
- V23 tag 与当前 `particle/main` 的只读 `git show`、`git diff`、`git log`;
|
||||||
|
- STEP-090/102/106 等历史设计、报告、设备验证结果;
|
||||||
|
- 现有 C++ 定向测试源码与 Validation 场景源码(只读,不执行、不修改)。
|
||||||
|
|
||||||
|
建议从当前实际路径 `src/mln/renderer/` 与相关 Shared Core 源码定位,**不得沿用旧 STEP 中过时的路径假设**。
|
||||||
|
|
||||||
|
每一条重要结论必须给出:**文件路径 + 类/函数或结构名称 + 适当行范围或 commit + 相关调用顺序**。核心论断不得只引用历史文档或本任务描述。
|
||||||
|
|
||||||
|
允许纯只读 Git/文本搜索,不要求也不允许额外建立实验分支、修改 Native/JS/Kotlin/测试代码、插桩、添加日志、运行 Jest、运行测试、Gradle、CMake/Ninja/Bazel、ADB 或重新安装 App。不得改 NavSea、Integration 或正式 release/tag。
|
||||||
|
|
||||||
|
## 六、最终必须提供的结论矩阵
|
||||||
|
|
||||||
|
**每个问题必须选择明确判定**,而不是只罗列建议:
|
||||||
|
|
||||||
|
| 维度 | 可选结论 |
|
||||||
|
|---|---|
|
||||||
|
| 能否定位 Placement 事件来源 | 现有数据可区分 / 现有数据不足 / 条件性可区分 |
|
||||||
|
| 能否证明 R0 的实际视觉结果未变化 | 已能证明 / 需扩展内部安全证据 / 不能按段安全证明 |
|
||||||
|
| 现有 Gate 是什么性质 | 正确的设计保守限制 / 明确可证的过度阻塞 Bug / 混合 / 证据不足 |
|
||||||
|
| 缓存失效的主要责任边界 | Native Placement 数据模型 / Native Gate 判定 / 当前跨段碰撞语义 / Host Style 组合 / 多因素 / 未确定 |
|
||||||
|
| 可选后续方向 | 无需代码调整 / Native 内部诊断补强 / Native 内部安全判据设计 / 宿主层序方案需协调 / 需真机对照实验 |
|
||||||
|
|
||||||
|
如果判断是 Bug,必须指出**违背了哪项既有语义/可复现的状态不变量**;如果判断是安全限制,必须指出**缺少何种证明才不能放宽**。如有多个阻塞项,按必要性和因果顺序排序,避免“修第一个之后才继续猜下一个”。
|
||||||
|
|
||||||
|
## 七、报告与交接产物
|
||||||
|
|
||||||
|
只允许新建/更新以下本 Step 文档(不改现有源码):
|
||||||
|
|
||||||
|
1. `docs/20_调查/R0_Placement跨段归因与缓存安全边界调查_v1.md`(主报告);
|
||||||
|
2. `docs/50_Step记录/STEP-110_R0_Placement跨段归因只读调查_v1.md`(状态及完成项);
|
||||||
|
3. `docs/报告/STEP-110_R0_Placement跨段归因只读调查_Codex完成报告_v1.md`(执行报告);
|
||||||
|
4. `粒子开发记录.md`(简短追加式记录)。
|
||||||
|
|
||||||
|
调查主报告必备章节:
|
||||||
|
- V23 tag / 当前 main 基线对照,说明是否有相关行为变化;
|
||||||
|
- Q1~Q4 的逐条答案及代码证据;
|
||||||
|
- **数据流/状态流**:Placement 事件 → 全局/局部状态 → R0 安全判定 → Cache Build/Reuse/Fallback → Fast Path;
|
||||||
|
- 七类反事实场景矩阵,明确跨源碰撞、Fade、Annotation 的影响;
|
||||||
|
- 已确定事实、假设、证据不足之处三栏;
|
||||||
|
- 结论矩阵、根因优先级、最小下一步验证建议;
|
||||||
|
- 若需调整公共 Contract / API,则仅提出 Integration 阻塞,不自行实施。
|
||||||
|
|
||||||
|
完成报告必须逐项按“**已完成 / 未完成 / 无法验证**”回答任务项并说明原因;记录实际检查范围、当前 branch/HEAD、Git 提交及推送信息。不得用“源码已读”代替代码证据。
|
||||||
|
|
||||||
|
**状态判定**:只读因果审计证据完整才可标为 `调查 PASS(不等于修复 PASS)`;否则 `PARTIAL/BLOCKED` 并列出缺失项。严禁声称“Native 已修复”、“R0 已缓存”或“CPU 已下降”。
|
||||||
|
|
||||||
|
## 八、Git 收尾与执行界限
|
||||||
|
|
||||||
|
- 本任务**只负责调查**。Codex/粒子项目未经用户后续明确授权,不得实施任何缓存、Placement、Fast Path 逻辑修改。
|
||||||
|
- 完成相关文档后,仅提交本 Step 新增报告、Step 记录及追加的开发记录;保留所有无关 dirty/untracked 文件,不得强制清理。
|
||||||
|
- 将本任务相关文档 scoped commit 到正式主线 `particle/main` 并 push 到公开远端 `origin/particle/main`。禁止 force push、改写历史、推送 upstream 或增加无关文件。
|
||||||
|
- 完成报告必须给出**实际 commit hash、commit message、push 结果、远端分支**;push 失败不得宣称完成,应明确报告阻塞。
|
||||||
|
- 本 MD 是交给粒子项目执行的**待办合同**。写入任务书本身不代表调查已执行、已验证或已得出结论。
|
||||||
|
- 本任务不包含 Jest,不涉及原生编译、测试执行或 Android 设备操作。
|
||||||
|
|
||||||
|
## 九、停止条件
|
||||||
|
|
||||||
|
遇到以下情形须停留在调查层,提交 PARTIAL/BLOCKED 事实,不得扩大实现:
|
||||||
|
1. V23 tag 与当前代码差异导致无法确认实际执行版本;
|
||||||
|
2. Placement 影响关系必须依赖运行时数据,静态源码无法证明;
|
||||||
|
3. 证明缓存安全需新增 per-segment collision/placement fingerprint 或改写 Core 算法;
|
||||||
|
4. 需要改变对外 Native/RN 公共接口或跨项目责任;
|
||||||
|
5. 需要 NavSea 配合修改 Style、增加调试或重新编译/测试;
|
||||||
|
6. 所需证据缺失或与 STEP-090/102/106 的历史结论相冲突。
|
||||||
|
|
||||||
|
出现以上情况,在报告中给出**最小下一步任务建议**,等待用户/Integration 决定;不得在本任务中直接实施。
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# STEP-111 R 段 Placement 结果等价与独立刷新可行性只读调查 — Codex 任务 v1
|
||||||
|
|
||||||
|
## 一、用户目标
|
||||||
|
用户希望 Native 对 R-C-R-C-R-C(简称 RCRCRC)中的每一个 RETAINED R 段单独决定是否更新。若该段本身最终视觉输出未变化,不应仅因另一段 Symbol/Placement/Fade 全局状态更新而被动重建;确实受跨段碰撞影响的 R 段必须正确更新,不能破坏文字、图标或层序。此任务是下一步架构/可实现性调查,不是授权实施优化。
|
||||||
|
|
||||||
|
**核心区别**:A. 哪个 Layer/Source/Segment 触发重新计算;B. 全图 Placement 后每一个 R 段的最终 Symbol 视觉结果是否改变;C. Retained R 段缓存能否安全复用;D. 是否可进一步省略 R 段 prepare/placement 计算。A≠B、B≠C、C≠D。不要把各 R 分段直接当作 Collision Domain,也不要承诺每段无需参与全图 Placement 计算。
|
||||||
|
|
||||||
|
## 二、正式边界与必读
|
||||||
|
- Mac /Users/tei/dev/MapLibreParticleLab,particle/main,origin/particle/main;执行时重新记录 HEAD、branch、dirty、远端基线;保留未跟踪 STEP-110 任务书。
|
||||||
|
- 先读仓库 AGENTS.md、PARTICLE_BASELINE.md、docs/00_项目规范/、docs/00_项目总则/README.md 和全部现行总则;读 STEP-110 主报告和 Codex 完成报告,以及 STEP-102、106、109、Fast Path 相关历史证据;以当前源码事实为准。
|
||||||
|
- 先检查 /Users/tei/dev/navsea-particle-integration 的 main 与 origin/main 同步状态,读取其 README、WORKFLOW、TEAM_MODEL、contracts、rfc、adr、status、compatibility;已有跨项目结论优先。本 Step 只讨论 Shared Core Native,不直接修改 Integration/NavSea/Android/iOS。
|
||||||
|
- STEP-110 已证明:PlacementController 仍是整图语义;R 的 placementCapable=true 时 Gate 消费 global placementChanged、symbolFadeChange、placementTransitionActive;跨 Source Collision 可能影响 R0,source≠segment。STEP-102/106 只验收过 placement-free R 的安全复用。本 Step 不能简单重复这些事实,重点给出**新增的判等/判无变化能力**具体可行性。
|
||||||
|
|
||||||
|
## 三、只读调查工作项
|
||||||
|
1. **从源码定位可实现性**:追踪 CrossTileSymbolIndex、Placement::placeLayers/commit、CollisionIndex/CollisionGroups、symbol buckets、placed icon/text、opacity/fade、RenderTree、RenderLayer、RenderSegment/Retained Cache 的真实控制与数据所有权。指出计算结束何处可以读取每一个 R 的最终 Placement 输出,是否已有稳定 ID、可见性/位置/opacity;写明路径、函数、行号和生命周期。辨别层间/Source 间/段间影响。
|
||||||
|
2. **定义每段结果等价契约候选**:提出具体 per-R PlacementResultKey/Signature/Revision 候选字段(Symbol 实例稳定 ID、tile/source/bucket generation、placement result、collision visible/hidden、text/icon anchor/offset、paint opacity/fade/transition、zoom/camera、glyph/image atlas、style/layer order、render target boundary 等)。逐项解释哪些是 Placement 专属,哪些已有其他 cache gate 覆盖,哪些必须动态比较,哪些不需要加入,避免对每个 Symbol 每帧做过度 O(N) 或漏比较;比较发生于何时、如何处理首次帧、transition、reset、missing data。
|
||||||
|
3. **RCRCRC 示例与判定表**:至少 R0/C0/R1/C1/R2/C2,其中 R0/R1/R2 都有 Symbol。分别分析:
|
||||||
|
- C1 仅粒子/颜色动画,R 均无可见变化;
|
||||||
|
- C1 Symbol 变动且只影响 R2;
|
||||||
|
- C1 Symbol 变动,碰撞也影响 R0/R1;
|
||||||
|
- R1 自己改变但 R0/R2 不变;
|
||||||
|
- 同一 Source 跨段、不同 Source 且 crossSourceCollisions=true/false;
|
||||||
|
- Annotation Symbol 自动插入,layer 顺序、camera/zoom/pitch、tile/glyph fade/opacity transition;
|
||||||
|
- 不能证明者 fail-closed。
|
||||||
|
结论表需区分“依然执行全局 Placement”、“该段仅避免 cache rebuild/composite 是否可复用”、“能否避免 render/prepare”、“Fast Path 仍被哪些其它 Gate 阻断”。
|
||||||
|
4. **方案对比与难度**:对 A 只加 tracing/per-R 指标(不改变判定)、B 新增 per-R result fingerprint 但 shadow compare(仅记录真假)、C 利用已证明等价替换 global-only Symbol Gate 允许单段 Reuse、D 深改 Placement/Collision 成分段增量计算,逐一评估前提、复杂度(1-5)、改动范围、CPU/GPU/内存开销、风险、能否只改 Shared Core、对 Android OpenGL/iOS Metal 的影响。不能因为理论上可 hash 就声称 hash 一定安全。
|
||||||
|
5. **执行与验收计划**:给出最小分阶段实现建议和真实可验证指标,包括 per-R signature stable/change、false invalidation、false reuse(严格 0)、build/reuse/fallback、视觉 A/B、Fade 长尾、CPU/frame、same frame draw ordering;指出先做诊断或原型是否需要新 Native 内部接口,哪些行为必须同时覆盖 Android/iOS。分别评估改善 retained cache 命中与改善整帧 CPU/Fast Path 的边界。
|
||||||
|
6. **明确推荐结论**:回答每个 R 段可否“自身未变就不刷新”、可做到的最小粒度、不能保证的情形、最佳第一阶段(原型)与中长期方案,给出具体源码位置和工程量区间;区分源码已证实 vs 推断 vs 必须运行时验证。
|
||||||
|
|
||||||
|
## 四、禁止项
|
||||||
|
调查只允许读取 Native/历史 Git/本机历史实验文件以及新增本 Step 文档。禁止修改任何 C++/JS/Kotlin/Swift/编译脚本/测试代码;禁止执行编译、链接、测试、Jest、Gradle、ADB、Bazel、CMake、设备操作;禁止更改 NavSea、Integration、公用 Contract/API、GSRL、release/Source Lock、发行 Tag;禁止放宽当前 fail-closed Gate。禁止重置/清除现有 dirty、禁止 force push。除非明确用户授权,不执行后续实施。
|
||||||
|
|
||||||
|
## 五、必做输出(实际生成)
|
||||||
|
1. docs/20_调查/STEP-111_R段Placement结果等价与独立刷新可行性调查_v1.md — 主报告:代码路径/行号、RCRCRC 场景矩阵、Signature 草案字段逐项依据与生命周期、可行方案 A-D 对比、复杂度与工程量估计、风险、可执行的后续 Step 拆分、源码已证/待验证标识。
|
||||||
|
2. docs/50_Step记录/STEP-111_R段Placement结果等价调查_v1.md — 简短中文 Step 记录,状态 PASS(调查完整)/PARTIAL/BLOCKED,记录未验证项与下一步。
|
||||||
|
3. docs/报告/STEP-111_R段Placement结果等价调查_Codex完成报告_v1.md — 完成报告:实际模型与 reasoning effort(仅凭运行会话证据)、读取资料、逐任务结果、调查起/终 HEAD、branch、dirty、文件修改、未运行构建和测试、检查命令、限制、Git/Push 状态如实记录。
|
||||||
|
4. 粒子开发记录.md — 本阶段简短追加(不要重写历史)。
|
||||||
|
Codex 仅负责内容调查和上述报告落盘,Git scoped commit/push/远端验证由 GPT 指挥 RDC 独立负责,完成报告不得预报未执行 Git 结果。原有未跟踪 STEP-110 任务文件不纳入提交。本任务书自身保持未跟踪,避免误提交。
|
||||||
|
|
||||||
|
## 六、验收与停止
|
||||||
|
只在调查结果有对应源码位置/函数/调用顺序,明确安全条件、未证实风险、工程阶段与复杂度对比时,记“调查 PASS”;如生产实际视觉等价无法仅通过源码证明,可记“调查 PASS / 运行验证未执行”,不得声称优化已通过。必要的实机状态/插桩超出只读范围时明确给出下一阶段最小需求,不得扩大当前范围。不得把历史 STEP-110 的 PARTIAL 当成本次已经实现结果。任务退出前向 GPT 汇报生成文件、最后状态和未完事项。
|
||||||
41
docs/50_Step记录/STEP-119_SymbolRetained生命周期修复_v1.md
Normal file
41
docs/50_Step记录/STEP-119_SymbolRetained生命周期修复_v1.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# STEP-119 SymbolRetained 生命周期修复 v1
|
||||||
|
|
||||||
|
日期:2026-10-12(JST)。Step 状态:进行中。代码交付:**IMPLEMENTED / WAITING_RDC_VALIDATION**。这是此前中断任务的续作;不关闭整体 STEP-119,不构成编译、GTest、GPU 或设备验收 PASS。
|
||||||
|
|
||||||
|
## 目标与基线
|
||||||
|
|
||||||
|
按照 `/tmp/fzg1_astra_guidance_20261012.md` 修复内部 Symbol Drawable 的 RenderTile 同源绑定、旧 Drawable 更新及 Bucket/Atlas 更换生命周期;保留原缓存、证书和 Full → Build → 成功 Full Reuse → Fast 顺序。
|
||||||
|
|
||||||
|
- 机器:Mac.lan / Darwin 24.6.0 / x86_64;仓库:`/Users/tei/dev/MapLibreParticleLab`。
|
||||||
|
- 开工及交付分支:`particle/main`;HEAD:`badb515d590fbdf221751b6677de3da0a5650d7e`。未切换分支/worktree,未提交或操作远端。
|
||||||
|
- 接续已有四个源码修改;起点另有受保护 STEP-110/111 两份未跟踪任务书。
|
||||||
|
- 用户本轮明确指定模型 `GPT-6.1 Sol High`(`gpt-6.1-sol / high`)。当前工具未提供执行器模型元数据,且本机当日 sessions 目录不存在,无法独立核验实际运行参数;不宣称已从日志核实,没有主动切换模型或启动子代理。
|
||||||
|
- 当前发行锁为 Android/RN `13.6.1-particle.25`、iOS `6.29.0-particle.25`,构建冻结源 `f32f6f45fd359007e8b3c77911b66d9e7fc93848`;本修复不改 Source Lock/发行物,不声称已进入 v25 制品。
|
||||||
|
|
||||||
|
## 必做项
|
||||||
|
|
||||||
|
- [x] 读取 Astra 指导、AGENTS、PARTICLE_BASELINE、规范、四份当前总则、当前 STEP-119 任务/实现/测试资料,核对现有四文件 diff。
|
||||||
|
- [x] 同次 source prepare 获取 raw owner 和 Y 排序引用列表;仅比较地址完成同源校验,再复制当前 ID/atlas。
|
||||||
|
- [x] 同 Bucket/Atlas 的完整 tile/pass 组先检查,再全部重新绑定当前 owner/RenderTile;不逐帧重建 Drawable/tweaker。
|
||||||
|
- [x] Bucket/Atlas/组内成员失配或缺 atlas 时移除整组,走原创建路径;Text/IconRGBA/IconSDF/Halo 使用同一 atlas 与实际 tweaker 配对。
|
||||||
|
- [x] Retained 使用内部强引用 atlas 和当前地址映射,不沿旧 Drawable 裸指针读取 atlas/Tile;缺证立即 complete=false。
|
||||||
|
- [x] 保活 RenderTile → renderData → LayoutResult 的证据资源,不把每次 prepare 的 owner 地址写入比较字节。
|
||||||
|
- [x] 编写并静态核对 13 项 SymbolRetainedLifetime GTest 源码,修正 atlas-only TileRenderData 查询默认 assert 的测试替身缺口。
|
||||||
|
- [x] 中文 Step、完成报告及开发记录末尾追加;四源码路径 scoped diff 空白检查 exit0。
|
||||||
|
- [ ] RDC 实际编译/链接并发现、运行生命周期与 STEP-119 定向 GTest。
|
||||||
|
- [ ] Android OpenGL、Metal 实际路径验证与 Vulkan 编译兼容性证据。
|
||||||
|
- [ ] 新源码锁/APK/SO 身份闭环、FZ-G1 动态生命周期、原24Gate、Full/Fast 视觉及释放验收。
|
||||||
|
|
||||||
|
## 修改范围与当前结果
|
||||||
|
|
||||||
|
生产修改限 `src/mln/gfx/symbol_drawable_data.hpp`、`src/mln/renderer/layers/render_symbol_layer.cpp`、`src/mln/renderer/layers/render_symbol_layer.hpp`;测试限 `test/renderer/retained_underlay_cache.test.cpp`。沿用此前修改,续作只补未知 Symbol 类型拒绝、测试替身空查询覆盖、Text/Icon/SDF/Halo 连续 prepare 稳定性与证书资源释放检查。
|
||||||
|
|
||||||
|
未改 RN/JNI 公共 API、原24Gate/证书判据、Placement、缓存开关、发行物、Integration、旧报告或两份保护任务书。未运行 CMake、编译/链接/语法编译、GTest、Gradle/APK、ADB/安装、Jest;没有新制品或设备数据。
|
||||||
|
|
||||||
|
## 阻塞与下一步
|
||||||
|
|
||||||
|
实现已落盘,可编译性与运行正确性仍待 RDC。现 verifier 会拒绝 tracked dirty,后续 Git 来源锁须在独立审计和另行授权后处理;本轮禁止 Git 收尾,不能绕过 verifier 或借旧 APK。历史 Metal/libuv 缺口未重新验证,不安装升级依赖。
|
||||||
|
|
||||||
|
RenderTile 容器不拥有底层 `Tile&`;强引用 atlas/texture 也不能证明原地像素或槽位不变,继续依赖既有 content epoch 和真实绑定复核。原始寄存器 tombstone 缺失,不能断言 `0x8cdc7c` 唯一根因为本缺陷;保留原 SO Build ID `485459ba44f806ef0a0c08633c717bfd5d82ad8f` 与匹配符号交 RDC。
|
||||||
|
|
||||||
|
准确命令、测试覆盖限制、停止门槛和交接证据见 [本轮完成报告](../报告/STEP-119_SymbolRetained生命周期修复_Codex完成报告_v1.md)。Codex 到此交接,RDC 独立审计后逐条验证;失败保留首错与现场,不放宽条件。
|
||||||
128
docs/报告/STEP-119_SymbolRetained生命周期修复_Codex完成报告_v1.md
Normal file
128
docs/报告/STEP-119_SymbolRetained生命周期修复_Codex完成报告_v1.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# STEP-119 SymbolRetained 生命周期修复 Codex 完成报告 v1
|
||||||
|
|
||||||
|
日期:2026-10-12(JST)。状态:**IMPLEMENTED / WAITING_RDC_VALIDATION**。接续此前因用量限制中断的同一任务,四个既有修改经静态复审及明确缺口修正后交付。没有运行编译或测试,整体 STEP-119 仍未验收,不报告整体 PASS。
|
||||||
|
|
||||||
|
## 1. 模型、环境与 Git 基线
|
||||||
|
|
||||||
|
- 用户本轮明确指定 `GPT-6.1 Sol High`,即 `gpt-6.1-sol / high`。当前工具没有执行器模型/推理参数元数据;轻量查找 `/Users/tei/.codex/sessions/2026/10/12` 返回目录不存在,实际运行参数无法独立核验。记录用户指定值,不把它写成已从执行器日志核实。未主动升级模型、启动第二实例或子代理。
|
||||||
|
- `Mac.lan`,Darwin 24.6.0,x86_64;目录 `/Users/tei/dev/MapLibreParticleLab`。
|
||||||
|
- 开工/交付 branch=`particle/main`,HEAD=`badb515d590fbdf221751b6677de3da0a5650d7e`。基线为四个已有 tracked 修改及 STEP-110/111 两份 untracked 任务书;不从头复写,不改分支/worktree/HEAD。
|
||||||
|
- `git submodule status` 已只读检查,没有变更前缀;无子模块修改。未 commit/push/tag/fetch/pull/rebase/reset/clean,未回读远端 HEAD(本轮禁止远端操作)。
|
||||||
|
- 只读当前 v25 Source Lock:Android/RN `13.6.1-particle.25`,iOS `6.29.0-particle.25`,冻结构建源 `f32f6f45fd359007e8b3c77911b66d9e7fc93848`。发行记录与制品未修改,本轮未生成制品,不能以现有 v25 包验证本工作区修复。
|
||||||
|
- 首先读取 Astra 原文 `/tmp/fzg1_astra_guidance_20261012.md`,再读取仓库规则、当前资料和现有 diff。当前用户续作授权优先于旧任务中的历史基线/跨项目调查和 Git 收尾安排。
|
||||||
|
|
||||||
|
## 2. 实际修改文件及生命周期不变量
|
||||||
|
|
||||||
|
| 文件 | 实际变更与不变量 |
|
||||||
|
|---|---|
|
||||||
|
| `src/mln/gfx/symbol_drawable_data.hpp` | 内部数据保存强类型 atlas 与实际原生 atlas tweaker;创建时两者来自同一份 atlas,增加内部 Drawable 类型标记。它们不拥有 RenderTile 或底层 Tile。 |
|
||||||
|
| `src/mln/renderer/layers/render_symbol_layer.hpp` | 私有同源/绑定/整组复用/采集辅助方法及当前地址→ID/atlas 映射;仅保存当前代次,不建立历史容器列表。friend 测试入口不修改公共 API。 |
|
||||||
|
| `src/mln/renderer/layers/render_symbol_layer.cpp` | prepare 从同次 source 获取 raw owner 与原 Y 排序列表,先校验成员地址再读 ID/atlas,保留 fading tile。成功复用先检查 tile/pass 整组,再全部刷新当前 RenderTile/owner;Bucket、atlas、layer tweaker、数据/类型或成员失配整组移除重建,atlas 缺失移除旧绘制。Retained 从强引用 atlas 取证,旧指针只作当前地址映射查找,使用 Drawable 自己的 ID;类型未知/绑定不符/未上传立即拒绝。owner 只放入 resources,不序列化 owner 地址。 |
|
||||||
|
| `test/renderer/retained_underlay_cache.test.cpp` | 13 项 `SymbolRetainedLifetime` 源码调用生产 prepare/bind/rebind/retained 辅助路径与原生 atlas tweaker;真实类接口/签名和友元访问逐项静态核对。atlas-only TileRenderData 显式覆盖空 Bucket/LayerRenderData 查询,避免基类 assert;补组内类型失配、Text/IconRGBA/IconSDF/Halo 稳定身份、未知类型及证书释放检查。 |
|
||||||
|
| `docs/50_Step记录/STEP-119_SymbolRetained生命周期修复_v1.md`(新增) | 本阶段目标、基线、完成/待验清单及交接。 |
|
||||||
|
| `docs/报告/STEP-119_SymbolRetained生命周期修复_Codex完成报告_v1.md`(新增) | 本报告,记录事实、准确命令、风险和停止条件。 |
|
||||||
|
| `粒子开发记录.md` | 仅末尾追加本次实现与未验证事实,保留历史内容。 |
|
||||||
|
|
||||||
|
两个新文档路径创建前已确认不存在,同主题无旧版本,选择 v1;使用独占创建,未覆盖旧报告。前三个生产文件及测试均沿用开工时修改;本续作生产补丁仅增加 Retained 对未知 SymbolType 的拒绝,测试补丁只处理上述明确缺口及覆盖不足。
|
||||||
|
|
||||||
|
### 所有权与稳定性
|
||||||
|
|
||||||
|
1. Drawable 只由通过同源校验的 owner/元素绑定。复用检查不从旧 RenderTile 读取 ID/Bucket/atlas;任何一个 Text/Icon/Halo 成员不兼容时整组移除,避免 `updateTile()` 的“任一成功”留下半新半旧组。
|
||||||
|
2. 同 Bucket/atlas 保持 Drawable 和 tweaker 身份,仅刷新 owner/RenderTile 与既有动态属性;source 每次产生新容器不应自行造成每帧重建或证书字节变化。
|
||||||
|
3. 当前容器拥有 RenderTile;RenderTile 独占 renderData;真实 GeometryTileRenderData 强持有 LayoutResult/atlas,LayoutResult 负责槽位释放。证据 resources 保活容器,但不调用 `result.retain(renderTilesOwner)`,因此 owner 地址不进入比较字节。证书 candidate/completed 替换或 invalidate 后旧证据可退出,未新增无界容器。
|
||||||
|
4. Retained 先验证当前地址/保存的 ID、内部数据类型、atlas 来源、实际 tweaker 和 shader,再检查真实 glyph/icon 纹理绑定及上传状态。任何缺证立即返回 incomplete,不沿失效裸指针追读 renderData/atlas。
|
||||||
|
5. RenderTile 内 `Tile&` 的寿命独立;保活容器不保活 Tile,不允许把旧 Tile 上的 getOverscaledTileID/holdForFade 当成安全操作。测试分别计数 Tile 与 TileRenderData,旧 Tile 销毁后仅比较旧地址及读取 atlas 强引用。
|
||||||
|
6. 同 atlas 的 texture 替换经原生 tweaker 更新实际绑定,前后证据不同;原地像素更新即使上传后指针不变,仍依赖真实 content epoch。既有 Build 前/后及提交后复核、证书版本/R cache/prepared Fast 保护保留,未改变准入顺序或放宽任何判据。
|
||||||
|
|
||||||
|
## 3. 静态审查与测试源码范围
|
||||||
|
|
||||||
|
13 项生命周期源码覆盖:连续 prepare 后刷新/释放旧 renderData、owner/元素错配、atlas 替换整组清理与新纹理绑定、组内单成员失配、atlas 缺失、同 atlas texture 更换及重新 Build/Reuse 状态、未上传原地更新拒绝、owner 身份不进入完整空组结果字节、Tile 与 renderData 寿命分离、Layer 移除及证书失效释放 atlas、同名 Source 替换、tile 退出清理、Text/Icon/SDF/Halo 连续三代保持身份、未知 Symbol 类型拒绝(其中相关情形合并在同一用例)。
|
||||||
|
|
||||||
|
测试使用真实 RenderSource/Tile/TileRenderData/RenderTile、SymbolBucket、Drawable/TileLayerGroup 接口和生产辅助方法;GPU Texture/Drawable/Shader 使用 CPU 替身。真实 API 的纯虚函数、构造参数、Mutable→Immutable 转换、SymbolLayer.baseImpl 可见性及派生类 friend 访问继承 protected 成员已静态核对,没有用悬空指针解引用构造普通反例。
|
||||||
|
|
||||||
|
**覆盖限制**:atlas-only fixture 不生成真实 Placement/顶点/GPU 命令;atlasResult 是生产采集的局部证据,不能代表完整渲染证书。整组失配测试走生产移除入口,但重新创建由 fixture 添加 Drawable,仍需 RDC 实际 update/builder/GPU 链验证。tile 退出测试直接调用正常 update 使用的清理入口,没有伪造 Context 或声称执行完整 update。owner 稳定证书测试走完整生产空组采集;非空 Text/Icon/Halo 稳定性只检查绑定输入和身份。原地 image patch 上传后的 epoch、Style/context 清理引用既有 `Step119SymbolResults.RealTileGlyphImageStyleContextCallbacksRejectBuiltCertificate` 等回调测试源码,不等同真实槽位释放、Source/Style/context 完整运行验收。
|
||||||
|
|
||||||
|
## 4. 已做及未做检查
|
||||||
|
|
||||||
|
已做:必要文件阅读、开工 Git/机器/版本及 diff 审查;真实 C++ 接口/生命周期调用静态核对;确认测试文件在 `test/CMakeLists.txt` 已登记;源码静态计数 `SymbolRetainedLifetime` 为13项;新增文档版本/路径检查;STEP-110/111 SHA-256 保护检查;开发记录追加与文件范围检查。
|
||||||
|
|
||||||
|
已执行四源码路径 scoped `git diff --check -- src/mln/gfx/symbol_drawable_data.hpp src/mln/renderer/layers/render_symbol_layer.cpp src/mln/renderer/layers/render_symbol_layer.hpp test/renderer/retained_underlay_cache.test.cpp`:**exit=0,stdout/stderr 空**。最终全工作区及七任务路径空白检查由本轮落盘后再次执行,结果见下方交付记录。空白检查不证明 C++ 编译或测试通过。
|
||||||
|
|
||||||
|
未执行:CMake 配置、Native/NDK 编译、`-fsyntax-only`、链接、GTest、Metal/iOS/GPU运行、Gradle/APK、ADB/安装、Jest、Bazel/codegen、发行、Git写操作及远端读取。没有新编译失败日志、设备日志、截图、APK/SO SHA 或 Build/Reuse/Fast 数据;既有 libuv 环境失败只作为历史限制,未重试也未修改环境。
|
||||||
|
|
||||||
|
保护的未跟踪任务书 SHA-256:
|
||||||
|
|
||||||
|
- STEP-110:`33037df71b4b30aa7956d59f4b99f77af3085405b24111bc0fe11ec857a93db0`。
|
||||||
|
- STEP-111:`f3a5806f25384bf46055db2edfea0d21bfbc4124184bc813b46e38b8079c1ec4`。
|
||||||
|
|
||||||
|
## 5. RDC 准确编译与定向测试命令(Codex 未执行)
|
||||||
|
|
||||||
|
独立审查本报告和实际 diff 后,在同一工作区逐条执行;每条保存 command/exit/耗时/完整日志路径,失败立即停止下一步,回传首个真实 error、target 与必要上下文。PATH 找不到 cmake 时可使用既有 `/usr/local/opt/cmake/bin/cmake`;不得安装升级依赖或关闭宏绕过 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='SymbolRetainedLifetime.*:Step119SymbolResults.*' --gtest_list_tests
|
||||||
|
build-macos-metal/mbgl-test-runner --gtest_filter='SymbolRetainedLifetime.*:Step119SymbolResults.*:RetainedUnderlayCache.*:RenderOrchestratorError.*:RenderOrchestratorTeardown.*:CrossTileSymbolLayerIndex.*' --gtest_color=no
|
||||||
|
```
|
||||||
|
|
||||||
|
必须发现本轮13项 `SymbolRetainedLifetime` 与既有13项 `Step119SymbolResults`(后者分布两个已登记文件),真实执行数量非零。零匹配、旧 runner、仅语法检查不能写 GTest PASS。Metal 与 Android OpenGL 分别保留编译及运行事实;Android 实际 compile_commands 的类型核对须保持原 `-Werror`,不加 `-Wno-error`。本轮未生成额外编译脚本。
|
||||||
|
|
||||||
|
Vulkan 至少取得生产修改的兼容性编译证据;已有 macOS Vulkan 环境可用以下真实 preset/target,环境缺失则记未验证/阻塞,不升级环境:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake --preset macos-vulkan
|
||||||
|
cmake --build build-macos-vulkan --target mbgl-core --parallel 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Vulkan Symbol provenance 既有保守边界保持,编译成功不等于其 Symbol retained 运行通过。可在既有 sanitizer 配置下补查 UAF,不改造构建体系;Host Metal/GTest 也不替代 iOS 真机。
|
||||||
|
|
||||||
|
## 6. 后续 FZ-G1 验收命令与证据(Codex 未执行)
|
||||||
|
|
||||||
|
**前置门槛**:独立审计/Native验证完成、后续 Git 来源锁和 Android/设备操作获得授权。当前 tracked dirty,verifier 的 `SOURCE_NOT_LOCKED` 会拒绝本工作区;本轮禁止提交,故这是后续验收前置条件,不能改 verifier、伪造 build-info 或用旧 APK。本报告仅交命令,不代执行 Android/ADB,也不代授权 Git。
|
||||||
|
|
||||||
|
用户/RDC 按其阶段授权完成一次标准新 APK 构建与安装,固定 FZG1-2、serial=`192.168.200.139:5555`、OpenGL Debug/x86_64:
|
||||||
|
|
||||||
|
```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
|
||||||
|
python3 -B tools/particle_android_validation/verify_step113_fast_path.py --serial 192.168.200.139:5555 --scenario RCRR_C_NARROW_NO_TIDE --seconds 90
|
||||||
|
python3 -B tools/particle_android_validation/verify_step113_fast_path.py --serial 192.168.200.139:5555 --scenario STATIC_RC --seconds 90
|
||||||
|
```
|
||||||
|
|
||||||
|
正向缓存保持开启,每次稳定后须真实 Build>0、Reuse>0、FastDelta>0,完整120帧24Gate mismatch=0;原63层 STATIC_RC 缺 tide 继续作为安全拒绝负对照,验证器可能返回 NO_FAST_OBSERVED/exit2,不能当正向 PASS,也不能添加假 tide。正向静止90秒不是生命周期验收。
|
||||||
|
|
||||||
|
RDC/用户还需反复 pan/zoom/rotate、跨 tile/world wrap、Source同名替换、Style清理/重载、glyph/icon/atlas更新、后台恢复/context reset;每次变化保存失效 → Full → 新 Build → 成功 Full Reuse → Fast 的顺序证据,Text/Icon/SDF/Halo 不消失、残影或错位。相同相机的 Full/Fast 图像对照、资源随循环最终释放均须实测。保持正式 Style/Source 和公共契约,不以禁用 Retained、每帧重建或放宽24Gate替代验证。
|
||||||
|
|
||||||
|
必须保存:新锁定源码 HEAD/diff、build-info/完整构建日志、本地及安装 APK SHA-256、实际 libmaplibre.so Build ID与匹配符号、测试发现/执行数、Build/Reuse/Full/Fast/Invalidate 区间增量、完整24Gate快照、操作前后截图及视觉判断、资源数量/释放证据和全部未验证项。证据写新目录,不覆盖 ignored 历史证据。
|
||||||
|
|
||||||
|
异常先保存 crash 日志/bugreport,不先清日志;由获授权执行者执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
STEP119_EVIDENCE_DIR=$(mktemp -d /tmp/step119-symbol-lifetime-XXXXXX)
|
||||||
|
adb -s 192.168.200.139:5555 logcat -b crash -d > "$STEP119_EVIDENCE_DIR/crash-logcat.txt"
|
||||||
|
adb -s 192.168.200.139:5555 bugreport "$STEP119_EVIDENCE_DIR/bugreport.zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 风险、证据缺口与停止门槛
|
||||||
|
|
||||||
|
- 缺原始寄存器 tombstone;原 Build ID `485459ba44f806ef0a0c08633c717bfd5d82ad8f` 和匹配符号应保留,不能断言 `0x8cdc7c` 唯一原因是本 UAF 链。
|
||||||
|
- 当前模型运行参数没有可独立回读的执行器元数据,RDC报告须补该身份事实;不伪造“已核实”。
|
||||||
|
- 真实 C++ 编译、GTest、Metal/OpenGL/Vulkan、底层 Tile 寿命及真实 atlas 槽位/像素变化、异步线程边界、释放/内存开销均未验收。静态检查只能排除部分明显问题。
|
||||||
|
- 若必须改变公共契约、重构全部 Tile 所有权、永久禁用缓存、放宽测试/证书或修改发行物才能继续,停止扩大修改并标 BLOCKED。
|
||||||
|
- 编译/链接/GTest失败、SIGSEGV、24Gate mismatch、Fast永久归零、资源循环单调增长、Symbol视觉异常均保留现场,不能通过降门槛、旧APK或隐藏失败制造 PASS。
|
||||||
|
|
||||||
|
本轮实现交接到此,后续由 RDC 独立验证;失败按 Codex 修复 → RDC 验证闭环返回,不继续后续开发阶段。
|
||||||
|
|
||||||
|
## 8. 最终交付静态检查记录
|
||||||
|
|
||||||
|
- 完整命令 `git diff --check`:exit=0,stdout/stderr 空。
|
||||||
|
- 七个任务文件显式路径的 `git diff --check -- ...`:exit=0,stdout/stderr 空。新建 Step/报告尚未跟踪,Git diff 不包含其内容;另以轻量 Python 文件检查确认两份 UTF-8 文档状态、末尾换行及无行尾空白,不借此声称 Git 已检查 untracked 内容。
|
||||||
|
- `git status --short`:仅四个接续源码、开发记录、两份新增本任务文档及两份原有保护任务书;没有其它修改。branch/HEAD 与开工一致。
|
||||||
|
- 最终 STEP-110/111 SHA-256 与开工值一致;开发记录内容以 HEAD 原文的完整字节为前缀,只有末尾追加。
|
||||||
|
- 静态计数:生命周期13项,既有 Step119SymbolResults 为9+4=13项。没有发现/执行 GTest,不能声称实际26项通过。
|
||||||
|
- 没有新的编译/测试/设备验收结论;状态维持 IMPLEMENTED / WAITING_RDC_VALIDATION。
|
||||||
@@ -5,14 +5,19 @@
|
|||||||
#include <mln/util/tiny_unordered_map.hpp>
|
#include <mln/util/tiny_unordered_map.hpp>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace mln {
|
namespace mln {
|
||||||
|
class TileAtlasTextures;
|
||||||
namespace style {
|
namespace style {
|
||||||
enum class SymbolType : uint8_t;
|
enum class SymbolType : uint8_t;
|
||||||
} // namespace style
|
} // namespace style
|
||||||
namespace gfx {
|
namespace gfx {
|
||||||
|
class DrawableAtlasesTweaker;
|
||||||
|
|
||||||
struct SymbolDrawableData : public DrawableData {
|
struct SymbolDrawableData : public DrawableData {
|
||||||
|
static constexpr std::size_t drawableType = 1;
|
||||||
SymbolDrawableData(const bool isHalo_,
|
SymbolDrawableData(const bool isHalo_,
|
||||||
const bool bucketVariablePlacement_,
|
const bool bucketVariablePlacement_,
|
||||||
const style::SymbolType symbolType_,
|
const style::SymbolType symbolType_,
|
||||||
@@ -20,7 +25,9 @@ struct SymbolDrawableData : public DrawableData {
|
|||||||
const style::AlignmentType rotationAlignment_,
|
const style::AlignmentType rotationAlignment_,
|
||||||
const style::SymbolPlacementType placement_,
|
const style::SymbolPlacementType placement_,
|
||||||
const style::IconTextFitType textFit_,
|
const style::IconTextFitType textFit_,
|
||||||
const bool isOffset_)
|
const bool isOffset_,
|
||||||
|
std::shared_ptr<TileAtlasTextures> atlases_,
|
||||||
|
std::shared_ptr<DrawableAtlasesTweaker> atlasTweaker_)
|
||||||
: isHalo(isHalo_),
|
: isHalo(isHalo_),
|
||||||
bucketVariablePlacement(bucketVariablePlacement_),
|
bucketVariablePlacement(bucketVariablePlacement_),
|
||||||
symbolType(symbolType_),
|
symbolType(symbolType_),
|
||||||
@@ -28,7 +35,9 @@ struct SymbolDrawableData : public DrawableData {
|
|||||||
rotationAlignment(rotationAlignment_),
|
rotationAlignment(rotationAlignment_),
|
||||||
placement(placement_),
|
placement(placement_),
|
||||||
textFit(textFit_),
|
textFit(textFit_),
|
||||||
isOffset(isOffset_) {}
|
isOffset(isOffset_),
|
||||||
|
atlases(std::move(atlases_)),
|
||||||
|
atlasTweaker(std::move(atlasTweaker_)) {}
|
||||||
~SymbolDrawableData() override = default;
|
~SymbolDrawableData() override = default;
|
||||||
|
|
||||||
const bool isHalo;
|
const bool isHalo;
|
||||||
@@ -39,6 +48,10 @@ struct SymbolDrawableData : public DrawableData {
|
|||||||
const style::SymbolPlacementType placement;
|
const style::SymbolPlacementType placement;
|
||||||
const style::IconTextFitType textFit;
|
const style::IconTextFitType textFit;
|
||||||
const bool isOffset;
|
const bool isOffset;
|
||||||
|
// Paired at creation with the actual native atlas tweaker. Neither
|
||||||
|
// resource owns RenderTile; its current container is bound separately.
|
||||||
|
const std::shared_ptr<TileAtlasTextures> atlases;
|
||||||
|
const std::shared_ptr<DrawableAtlasesTweaker> atlasTweaker;
|
||||||
};
|
};
|
||||||
|
|
||||||
using UniqueSymbolDrawableData = std::unique_ptr<SymbolDrawableData>;
|
using UniqueSymbolDrawableData = std::unique_ptr<SymbolDrawableData>;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
#include <mln/renderer/layers/collision_layer_tweaker.hpp>
|
#include <mln/renderer/layers/collision_layer_tweaker.hpp>
|
||||||
|
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace mln {
|
namespace mln {
|
||||||
|
|
||||||
@@ -195,14 +196,96 @@ bool RenderSymbolLayer::hasCrossfade() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RenderSymbolLayer::setRenderTiles(Immutable<std::vector<RenderTile>> owner, RenderTiles sorted) {
|
||||||
|
renderTilesOwner = std::move(owner);
|
||||||
|
renderTiles = std::move(sorted);
|
||||||
|
currentTileBindings.clear();
|
||||||
|
renderTilesSameSource = false;
|
||||||
|
if (!renderTilesOwner.get() || !renderTiles) return;
|
||||||
|
|
||||||
|
std::unordered_set<const RenderTile*> addresses;
|
||||||
|
addresses.reserve(renderTilesOwner->size());
|
||||||
|
for (const auto& tile : *renderTilesOwner) addresses.insert(&tile);
|
||||||
|
for (const auto& ref : *renderTiles) {
|
||||||
|
if (!addresses.contains(&ref.get())) {
|
||||||
|
// Do not dereference a reference from another prepare generation.
|
||||||
|
renderTiles.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& ref : *renderTiles) {
|
||||||
|
const auto& tile = ref.get();
|
||||||
|
currentTileBindings.emplace(&tile, TileBinding{tile.getOverscaledTileID(), tile.getAtlasTextures()});
|
||||||
|
}
|
||||||
|
renderTilesSameSource = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderSymbolLayer::bindRenderTile(gfx::Drawable& drawable, const RenderTile& tile) {
|
||||||
|
const auto it = currentTileBindings.find(&tile);
|
||||||
|
if (!renderTilesSameSource || it == currentTileBindings.end() ||
|
||||||
|
!drawable.getTileID() || *drawable.getTileID() != it->second.id) return false;
|
||||||
|
drawable.setRenderTile(renderTilesOwner, &tile);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderSymbolLayer::rebindTileDrawables(const RenderTile& tile,
|
||||||
|
const std::shared_ptr<Bucket>& bucket,
|
||||||
|
const TileAtlasTexturesPtr& atlases) {
|
||||||
|
const auto binding = currentTileBindings.find(&tile);
|
||||||
|
if (!renderTilesSameSource || binding == currentTileBindings.end()) return false;
|
||||||
|
const auto& tileID = binding->second.id;
|
||||||
|
auto* group = static_cast<TileLayerGroup*>(layerGroup.get());
|
||||||
|
if (!group) return false;
|
||||||
|
const auto count = group->getDrawableCount(passes, tileID);
|
||||||
|
bool compatible = bucket && layerTweaker && atlases && binding->second.atlases == atlases;
|
||||||
|
group->visitDrawables(passes, tileID, [&](gfx::Drawable& drawable) {
|
||||||
|
if (drawable.getLayerTweaker() != layerTweaker || drawable.getBucket() != bucket ||
|
||||||
|
drawable.getType() != gfx::SymbolDrawableData::drawableType || !drawable.getData() ||
|
||||||
|
!drawable.getTileID() || *drawable.getTileID() != tileID ||
|
||||||
|
drawable.getRenderPass() != passes) {
|
||||||
|
compatible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& data = static_cast<const gfx::SymbolDrawableData&>(*drawable.getData());
|
||||||
|
if (!data.atlasTweaker || data.atlases != atlases || drawable.getTweakers().size() != 1 ||
|
||||||
|
drawable.getTweakers().front() != data.atlasTweaker) compatible = false;
|
||||||
|
if (!atlases || (data.symbolType == SymbolType::Text ? !atlases->glyph : !atlases->icon) ||
|
||||||
|
(data.symbolType != SymbolType::Text && data.symbolType != SymbolType::IconRGBA &&
|
||||||
|
data.symbolType != SymbolType::IconSDF)) compatible = false;
|
||||||
|
});
|
||||||
|
if (!compatible || group->getDrawableCount(passes, tileID) != count) {
|
||||||
|
// updateTile() means ANY updated. Preflight the entire text/icon/halo
|
||||||
|
// group before changing any member, so partial reuse cannot survive.
|
||||||
|
removeTile(passes, tileID);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!count) return false;
|
||||||
|
group->visitDrawables(passes, tileID, [&](gfx::Drawable& drawable) {
|
||||||
|
if (!bindRenderTile(drawable, tile)) compatible = false;
|
||||||
|
});
|
||||||
|
if (!compatible || group->getDrawableCount(passes, tileID) != count) {
|
||||||
|
removeTile(passes, tileID);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void RenderSymbolLayer::prepare(const LayerPrepareParameters& params) {
|
void RenderSymbolLayer::prepare(const LayerPrepareParameters& params) {
|
||||||
renderTiles = params.source->getRenderTilesSortedByYPosition();
|
// Keep the Y order and fading tiles of the Symbol source path, while
|
||||||
|
// taking both views from the same completed source prepare.
|
||||||
|
auto owner = params.source->getRawRenderTiles();
|
||||||
|
auto sorted = params.source->getRenderTilesSortedByYPosition();
|
||||||
|
setRenderTiles(std::move(owner), std::move(sorted));
|
||||||
|
placementData.clear();
|
||||||
|
if (!renderTilesSameSource) {
|
||||||
|
renderTileIDs.clear();
|
||||||
|
removeAllDrawables();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
updateRenderTileIDs();
|
updateRenderTileIDs();
|
||||||
addRenderPassesFromTiles();
|
addRenderPassesFromTiles();
|
||||||
|
|
||||||
placementData.clear();
|
|
||||||
|
|
||||||
for (const RenderTile& renderTile : *renderTiles) {
|
for (const RenderTile& renderTile : *renderTiles) {
|
||||||
auto* bucket = static_cast<SymbolBucket*>(renderTile.getBucket(*baseImpl));
|
auto* bucket = static_cast<SymbolBucket*>(renderTile.getBucket(*baseImpl));
|
||||||
if (bucket && bucket->bucketLeaderID == getID() && static_cast<Bucket*>(bucket)->check(SYM_GUARD_LOC)) {
|
if (bucket && bucket->bucketLeaderID == getID() && static_cast<Bucket*>(bucket)->check(SYM_GUARD_LOC)) {
|
||||||
@@ -475,6 +558,53 @@ std::size_t RenderSymbolLayer::removeAllDrawables() {
|
|||||||
return stats.drawablesRemoved - oldValue;
|
return stats.drawablesRemoved - oldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RenderSymbolLayer::appendRetainedTileResult(SymbolRenderResult& result, const gfx::Drawable& drawable) const {
|
||||||
|
// Only compare a possibly old pointer as an address. IDs were copied while
|
||||||
|
// source prepare owned a live Tile; never call through the old RenderTile.
|
||||||
|
const auto it = currentTileBindings.find(drawable.getRenderTile());
|
||||||
|
if (!renderTilesSameSource || it == currentTileBindings.end() || !drawable.getTileID() ||
|
||||||
|
*drawable.getTileID() != it->second.id ||
|
||||||
|
drawable.getType() != gfx::SymbolDrawableData::drawableType || !drawable.getData()) {
|
||||||
|
result.complete = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& data = static_cast<const gfx::SymbolDrawableData&>(*drawable.getData());
|
||||||
|
const auto& atlases = data.atlases;
|
||||||
|
if ((data.symbolType != SymbolType::Text && data.symbolType != SymbolType::IconRGBA &&
|
||||||
|
data.symbolType != SymbolType::IconSDF) ||
|
||||||
|
!atlases || atlases != it->second.atlases || !data.atlasTweaker ||
|
||||||
|
drawable.getTweakers().size() != 1 || drawable.getTweakers().front() != data.atlasTweaker ||
|
||||||
|
!drawable.getShader()) {
|
||||||
|
result.complete = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& expected = data.symbolType == SymbolType::Text ? atlases->glyph : atlases->icon;
|
||||||
|
if (!expected || expected->needsUpload() ||
|
||||||
|
drawable.getTexture(shaders::idSymbolImageTexture) != expected ||
|
||||||
|
(drawable.getShader()->getSamplerLocation(shaders::idSymbolImageIconTexture) &&
|
||||||
|
(!atlases->icon || atlases->icon->needsUpload() ||
|
||||||
|
drawable.getTexture(shaders::idSymbolImageIconTexture) != atlases->icon))) {
|
||||||
|
result.complete = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
if (texture && texture->needsUpload()) {
|
||||||
|
result.complete = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
result.retain(texture);
|
||||||
|
if (texture) {
|
||||||
|
result.append(texture->getSize());
|
||||||
|
result.append(texture->getFormat());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.complete;
|
||||||
|
}
|
||||||
|
|
||||||
SymbolRenderResult RenderSymbolLayer::retainedRenderResult(float fadeChange, uint64_t frame) {
|
SymbolRenderResult RenderSymbolLayer::retainedRenderResult(float fadeChange, uint64_t frame) {
|
||||||
SymbolRenderResult result;
|
SymbolRenderResult result;
|
||||||
result.append(layerIndex);
|
result.append(layerIndex);
|
||||||
@@ -483,7 +613,13 @@ SymbolRenderResult RenderSymbolLayer::retainedRenderResult(float fadeChange, uin
|
|||||||
// A hidden layer is admitted only after processChanges has removed both
|
// A hidden layer is admitted only after processChanges has removed both
|
||||||
// groups (the orchestrator verifies registry membership separately).
|
// groups (the orchestrator verifies registry membership separately).
|
||||||
if (!isLayerRenderable()) return result;
|
if (!isLayerRenderable()) return result;
|
||||||
if (collisionTileLayerGroup && !collisionTileLayerGroup->empty()) result.complete = false;
|
if (!renderTilesSameSource || (collisionTileLayerGroup && !collisionTileLayerGroup->empty())) {
|
||||||
|
result.complete = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// Keep TileRenderData/LayoutResult (including atlas slots) alive across
|
||||||
|
// completed Full evidence. The per-prepare owner address is NOT an input.
|
||||||
|
result.resources.emplace_back(std::make_shared<Immutable<std::vector<RenderTile>>>(renderTilesOwner));
|
||||||
if (!layerGroup || layerGroup->empty()) {
|
if (!layerGroup || layerGroup->empty()) {
|
||||||
// Empty is provable only if there are no renderable Symbol buckets.
|
// Empty is provable only if there are no renderable Symbol buckets.
|
||||||
if (renderTiles) for (const auto& tileRef : *renderTiles) {
|
if (renderTiles) for (const auto& tileRef : *renderTiles) {
|
||||||
@@ -501,7 +637,10 @@ SymbolRenderResult RenderSymbolLayer::retainedRenderResult(float fadeChange, uin
|
|||||||
// RenderSymbolLayer is final and creates only this tweaker and the native
|
// RenderSymbolLayer is final and creates only this tweaker and the native
|
||||||
// atlas tweaker. Unknown placement-capable layers never call this method.
|
// atlas tweaker. Unknown placement-capable layers never call this method.
|
||||||
const auto& tweaker = static_cast<const SymbolLayerTweaker&>(*layerTweaker);
|
const auto& tweaker = static_cast<const SymbolLayerTweaker&>(*layerTweaker);
|
||||||
if (tweaker.getRenderResultFrame() != frame) result.complete = false;
|
if (tweaker.getRenderResultFrame() != frame) {
|
||||||
|
result.complete = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
result.appendResult(tweaker.getRenderResult());
|
result.appendResult(tweaker.getRenderResult());
|
||||||
result.retain(layerGroup);
|
result.retain(layerGroup);
|
||||||
result.append(layerGroup->getEnabled());
|
result.append(layerGroup->getEnabled());
|
||||||
@@ -509,12 +648,14 @@ SymbolRenderResult RenderSymbolLayer::retainedRenderResult(float fadeChange, uin
|
|||||||
std::size_t visited = 0;
|
std::size_t visited = 0;
|
||||||
visitLayerGroupDrawables(*layerGroup, [&](gfx::Drawable& drawable) {
|
visitLayerGroupDrawables(*layerGroup, [&](gfx::Drawable& drawable) {
|
||||||
++visited;
|
++visited;
|
||||||
|
if (!result.complete) return;
|
||||||
if (drawable.getIsCustom() || drawable.getLayerTweaker() != layerTweaker ||
|
if (drawable.getIsCustom() || drawable.getLayerTweaker() != layerTweaker ||
|
||||||
!drawable.getBucket() || !drawable.getTileID() || !drawable.getData() ||
|
!drawable.getBucket() || !drawable.getTileID() || !drawable.getData() ||
|
||||||
!drawable.getShader() || drawable.getTweakers().size() != 1) {
|
!drawable.getShader() || drawable.getTweakers().size() != 1) {
|
||||||
result.complete = false;
|
result.complete = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!appendRetainedTileResult(result, drawable)) return;
|
||||||
result.append(drawable.getID());
|
result.append(drawable.getID());
|
||||||
if (!SymbolShaderProvenance::get().supports(*drawable.getShader())) result.complete = false;
|
if (!SymbolShaderProvenance::get().supports(*drawable.getShader())) result.complete = false;
|
||||||
result.retain(drawable.getTweakers().front()); // immutable native atlas/sampler policy
|
result.retain(drawable.getTweakers().front()); // immutable native atlas/sampler policy
|
||||||
@@ -588,29 +729,6 @@ SymbolRenderResult RenderSymbolLayer::retainedRenderResult(float fadeChange, uin
|
|||||||
if (indices->empty()) result.complete = false;
|
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;
|
if (visited != layerGroup->getDrawableCount()) result.complete = false;
|
||||||
return result;
|
return result;
|
||||||
@@ -623,7 +741,7 @@ void RenderSymbolLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
const PaintParameters&,
|
const PaintParameters&,
|
||||||
const RenderTree&,
|
const RenderTree&,
|
||||||
UniqueChangeRequestVec& changes) {
|
UniqueChangeRequestVec& changes) {
|
||||||
if (!renderTiles || renderTiles->empty() || passes == RenderPass::None) {
|
if (!renderTilesSameSource || !renderTiles || renderTiles->empty() || passes == RenderPass::None) {
|
||||||
removeAllDrawables();
|
removeAllDrawables();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -732,6 +850,12 @@ void RenderSymbolLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
const auto& renderData = *optRenderData;
|
const auto& renderData = *optRenderData;
|
||||||
const auto& bucket = static_cast<const SymbolBucket&>(*renderData.bucket);
|
const auto& bucket = static_cast<const SymbolBucket&>(*renderData.bucket);
|
||||||
const auto& evaluated = getEvaluated<SymbolLayerProperties>(renderData.layerProperties);
|
const auto& evaluated = getEvaluated<SymbolLayerProperties>(renderData.layerProperties);
|
||||||
|
const auto& atlases = tile.getAtlasTextures();
|
||||||
|
if (!atlases || (bucket.hasTextData() && !atlases->glyph) ||
|
||||||
|
((bucket.hasIconData() || bucket.hasSdfIconData() || bucket.iconsInText) && !atlases->icon)) {
|
||||||
|
removeTile(passes, tileID);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const auto prevBucketID = getRenderTileBucketID(tileID);
|
const auto prevBucketID = getRenderTileBucketID(tileID);
|
||||||
if (prevBucketID != util::SimpleIdentity::Empty && prevBucketID != bucket.getID()) {
|
if (prevBucketID != util::SimpleIdentity::Empty && prevBucketID != bucket.getID()) {
|
||||||
@@ -807,18 +931,14 @@ void RenderSymbolLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
// If we already have drawables for this tile, update them.
|
// If we already have drawables for this tile, update them.
|
||||||
// Just update the drawables we already created
|
// Just update the drawables we already created
|
||||||
auto updateExisting = [&](gfx::Drawable& drawable) {
|
auto updateExisting = [&](gfx::Drawable& drawable) {
|
||||||
if (drawable.getLayerTweaker() != layerTweaker) {
|
|
||||||
// This drawable was produced on a previous style/bucket, and should not be updated.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
propertiesAsUniforms.first.clear();
|
propertiesAsUniforms.first.clear();
|
||||||
propertiesAsUniforms.second.clear();
|
propertiesAsUniforms.second.clear();
|
||||||
|
|
||||||
updateTileDrawable(drawable, bucket, bucketPaintProperties, evaluated);
|
updateTileDrawable(drawable, bucket, bucketPaintProperties, evaluated);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
if (updateTile(passes, tileID, std::move(updateExisting))) {
|
if (rebindTileDrawables(tile, renderData.bucket, atlases) &&
|
||||||
|
updateTile(passes, tileID, std::move(updateExisting))) {
|
||||||
// re-create collision drawables
|
// re-create collision drawables
|
||||||
if (collisionTileLayerGroup) {
|
if (collisionTileLayerGroup) {
|
||||||
collisionTileLayerGroup->removeDrawables(passes, tileID);
|
collisionTileLayerGroup->removeDrawables(passes, tileID);
|
||||||
@@ -855,11 +975,6 @@ void RenderSymbolLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto& atlases = tile.getAtlasTextures();
|
|
||||||
if (!atlases) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bucket.hasIconData() && atlases->icon) {
|
if (bucket.hasIconData() && atlases->icon) {
|
||||||
addRenderables(bucket.icon, SymbolType::IconRGBA);
|
addRenderables(bucket.icon, SymbolType::IconRGBA);
|
||||||
}
|
}
|
||||||
@@ -879,7 +994,7 @@ void RenderSymbolLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
using RawVertexVec = std::vector<std::uint8_t>; // raw buffer for vertexes of <int16_t, 4>
|
using RawVertexVec = std::vector<std::uint8_t>; // raw buffer for vertexes of <int16_t, 4>
|
||||||
struct TileInfo {
|
struct TileInfo {
|
||||||
RawVertexVec textVertices, iconVertices;
|
RawVertexVec textVertices, iconVertices;
|
||||||
gfx::DrawableTweakerPtr textTweaker, iconTweaker;
|
std::shared_ptr<gfx::DrawableAtlasesTweaker> textTweaker, iconTweaker;
|
||||||
};
|
};
|
||||||
|
|
||||||
mln::unordered_map<UnwrappedTileID, TileInfo> tileCache;
|
mln::unordered_map<UnwrappedTileID, TileInfo> tileCache;
|
||||||
@@ -1031,13 +1146,14 @@ void RenderSymbolLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
for (auto& drawable : builder->clearDrawables()) {
|
for (auto& drawable : builder->clearDrawables()) {
|
||||||
drawable->setTileID(tileID);
|
drawable->setTileID(tileID);
|
||||||
drawable->setLayerTweaker(layerTweaker);
|
drawable->setLayerTweaker(layerTweaker);
|
||||||
drawable->setRenderTile(renderTilesOwner, &tile);
|
if (!bindRenderTile(*drawable, tile)) continue;
|
||||||
|
|
||||||
PaintPropertyBindersBase& textBinders = bucketPaintProperties.textBinders;
|
PaintPropertyBindersBase& textBinders = bucketPaintProperties.textBinders;
|
||||||
PaintPropertyBindersBase& iconBinders = bucketPaintProperties.iconBinders;
|
PaintPropertyBindersBase& iconBinders = bucketPaintProperties.iconBinders;
|
||||||
drawable->setBinders(renderable.renderData.bucket, isText ? &textBinders : &iconBinders);
|
drawable->setBinders(renderable.renderData.bucket, isText ? &textBinders : &iconBinders);
|
||||||
|
|
||||||
const bool hasVariablePlacement = bucket.hasVariablePlacement;
|
const bool hasVariablePlacement = bucket.hasVariablePlacement;
|
||||||
|
drawable->setType(gfx::SymbolDrawableData::drawableType);
|
||||||
drawable->setData(std::make_unique<gfx::SymbolDrawableData>(
|
drawable->setData(std::make_unique<gfx::SymbolDrawableData>(
|
||||||
/*.isHalo=*/isHalo,
|
/*.isHalo=*/isHalo,
|
||||||
/*.bucketVaraiblePlacement=*/hasVariablePlacement,
|
/*.bucketVaraiblePlacement=*/hasVariablePlacement,
|
||||||
@@ -1046,7 +1162,9 @@ void RenderSymbolLayer::update(gfx::ShaderRegistry& shaders,
|
|||||||
/*.rotationAlignment=*/values.rotationAlignment,
|
/*.rotationAlignment=*/values.rotationAlignment,
|
||||||
/*.placement=*/bucketLayout.get<SymbolPlacement>(),
|
/*.placement=*/bucketLayout.get<SymbolPlacement>(),
|
||||||
/*.textFit=*/bucketLayout.get<IconTextFit>(),
|
/*.textFit=*/bucketLayout.get<IconTextFit>(),
|
||||||
/*.isOffset=*/isOffset));
|
/*.isOffset=*/isOffset,
|
||||||
|
atlases,
|
||||||
|
isText ? tileInfo.textTweaker : tileInfo.iconTweaker));
|
||||||
|
|
||||||
tileLayerGroup->addDrawable(passes, tileID, std::move(drawable));
|
tileLayerGroup->addDrawable(passes, tileID, std::move(drawable));
|
||||||
++stats.drawablesAdded;
|
++stats.drawablesAdded;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace mln {
|
namespace mln {
|
||||||
|
class TileAtlasTextures;
|
||||||
|
|
||||||
namespace style {
|
namespace style {
|
||||||
|
|
||||||
@@ -82,6 +83,20 @@ protected:
|
|||||||
std::size_t removeTile(RenderPass, const OverscaledTileID &) override;
|
std::size_t removeTile(RenderPass, const OverscaledTileID &) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend struct SymbolRetainedLifetimeTestAccess;
|
||||||
|
// Validate addresses before reading any reference-based tile input.
|
||||||
|
void setRenderTiles(Immutable<std::vector<RenderTile>>, RenderTiles);
|
||||||
|
bool bindRenderTile(gfx::Drawable&, const RenderTile&);
|
||||||
|
bool appendRetainedTileResult(SymbolRenderResult&, const gfx::Drawable&) const;
|
||||||
|
bool rebindTileDrawables(const RenderTile&, const std::shared_ptr<Bucket>&,
|
||||||
|
const std::shared_ptr<TileAtlasTextures>&);
|
||||||
|
struct TileBinding {
|
||||||
|
OverscaledTileID id;
|
||||||
|
std::shared_ptr<TileAtlasTextures> atlases;
|
||||||
|
};
|
||||||
|
std::unordered_map<const RenderTile*, TileBinding> currentTileBindings;
|
||||||
|
bool renderTilesSameSource = false;
|
||||||
|
|
||||||
void transition(const TransitionParameters &) override;
|
void transition(const TransitionParameters &) override;
|
||||||
void evaluate(const PropertyEvaluationParameters &) override;
|
void evaluate(const PropertyEvaluationParameters &) override;
|
||||||
bool hasTransition() const override;
|
bool hasTransition() const override;
|
||||||
|
|||||||
@@ -10,6 +10,22 @@
|
|||||||
#include <mln/renderer/buckets/symbol_bucket.hpp>
|
#include <mln/renderer/buckets/symbol_bucket.hpp>
|
||||||
#include <mln/renderer/symbol_shader_provenance.hpp>
|
#include <mln/renderer/symbol_shader_provenance.hpp>
|
||||||
#include <mln/shaders/symbol_layer_ubo.hpp>
|
#include <mln/shaders/symbol_layer_ubo.hpp>
|
||||||
|
#include <mln/gfx/drawable_atlases_tweaker.hpp>
|
||||||
|
#include <mln/gfx/drawable_impl.hpp>
|
||||||
|
#include <mln/gfx/symbol_drawable_data.hpp>
|
||||||
|
#include <mln/geometry/line_atlas.hpp>
|
||||||
|
#include <mln/renderer/image_manager.hpp>
|
||||||
|
#include <mln/renderer/layers/render_symbol_layer.hpp>
|
||||||
|
#include <mln/renderer/layers/symbol_layer_tweaker.hpp>
|
||||||
|
#include <mln/renderer/paint_parameters.hpp>
|
||||||
|
#include <mln/renderer/pattern_atlas.hpp>
|
||||||
|
#include <mln/renderer/render_source.hpp>
|
||||||
|
#include <mln/renderer/render_tile.hpp>
|
||||||
|
#include <mln/renderer/render_tree.hpp>
|
||||||
|
#include <mln/renderer/tile_render_data.hpp>
|
||||||
|
#include <mln/style/layers/symbol_layer.hpp>
|
||||||
|
#include <mln/style/sources/geojson_source_impl.hpp>
|
||||||
|
#include <mln/tile/tile.hpp>
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
@@ -22,6 +38,40 @@
|
|||||||
|
|
||||||
using namespace mln;
|
using namespace mln;
|
||||||
|
|
||||||
|
namespace mln {
|
||||||
|
// Exercise the same internal methods used by prepare/update/result collection,
|
||||||
|
// without a GPU Context or a second copy of their compatibility predicates.
|
||||||
|
struct SymbolRetainedLifetimeTestAccess {
|
||||||
|
static void prepare(RenderSymbolLayer& layer, const LayerPrepareParameters& params) { layer.prepare(params); }
|
||||||
|
static void setTiles(RenderSymbolLayer& layer, Immutable<std::vector<RenderTile>> owner, RenderTiles refs) {
|
||||||
|
layer.setRenderTiles(std::move(owner), std::move(refs));
|
||||||
|
}
|
||||||
|
static void setup(RenderSymbolLayer& layer, std::shared_ptr<TileLayerGroup> group) {
|
||||||
|
layer.layerGroup = std::move(group);
|
||||||
|
layer.passes = RenderPass::Translucent;
|
||||||
|
layer.layerTweaker = std::make_shared<SymbolLayerTweaker>(layer.getID(), layer.evaluatedProperties);
|
||||||
|
layer.isRenderable = true;
|
||||||
|
}
|
||||||
|
static const LayerTweakerPtr& tweaker(const RenderSymbolLayer& layer) { return layer.layerTweaker; }
|
||||||
|
static bool bind(RenderSymbolLayer& layer, gfx::Drawable& drawable, const RenderTile& tile) {
|
||||||
|
return layer.bindRenderTile(drawable, tile);
|
||||||
|
}
|
||||||
|
static bool reuse(RenderSymbolLayer& layer, const RenderTile& tile, const std::shared_ptr<Bucket>& bucket,
|
||||||
|
const std::shared_ptr<TileAtlasTextures>& atlases) {
|
||||||
|
return layer.rebindTileDrawables(tile, bucket, atlases);
|
||||||
|
}
|
||||||
|
static SymbolRenderResult atlasResult(const RenderSymbolLayer& layer, const gfx::Drawable& drawable) {
|
||||||
|
SymbolRenderResult result;
|
||||||
|
layer.appendRetainedTileResult(result, drawable);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
static void removeLayer(RenderSymbolLayer& layer) {
|
||||||
|
UniqueChangeRequestVec changes;
|
||||||
|
layer.layerRemoved(changes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace mln
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
RetainedUnderlayKey key(Size size = {640, 480}, int32_t boundary = 4) {
|
RetainedUnderlayKey key(Size size = {640, 480}, int32_t boundary = 4) {
|
||||||
RetainedUnderlayKey result{size, 0, boundary, {}};
|
RetainedUnderlayKey result{size, 0, boundary, {}};
|
||||||
@@ -2109,3 +2159,425 @@ TEST(FastGateTrace, RetainedSamplesHaveDistinctPhasesAndExplicitLimit) {
|
|||||||
EXPECT_EQ(frame.post.retained[0].cache.build, 1u);
|
EXPECT_EQ(frame.post.retained[0].cache.build, 1u);
|
||||||
EXPECT_EQ(frame.post.retained[0].cache.sourceRevision, 7u);
|
EXPECT_EQ(frame.post.retained[0].cache.sourceRevision, 7u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class LifetimeUniformBuffers final : public gfx::UniformBufferArray {
|
||||||
|
public:
|
||||||
|
void bind(gfx::RenderPass&) override {}
|
||||||
|
protected:
|
||||||
|
std::unique_ptr<gfx::UniformBuffer> copy(const gfx::UniformBuffer&) override { return nullptr; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class LifetimeDrawable final : public gfx::Drawable {
|
||||||
|
public:
|
||||||
|
LifetimeDrawable() : Drawable("symbol-lifetime") {}
|
||||||
|
void draw(PaintParameters&) const override {}
|
||||||
|
void updateVertexAttributes(gfx::VertexAttributeArrayPtr, std::size_t, gfx::DrawMode,
|
||||||
|
gfx::IndexVectorBasePtr, const SegmentBase*, std::size_t) override {}
|
||||||
|
void setVertices(std::vector<uint8_t>&&, std::size_t, gfx::AttributeDataType) override {}
|
||||||
|
void setIndexData(gfx::IndexVectorBasePtr, std::vector<UniqueDrawSegment>) override {}
|
||||||
|
const gfx::UniformBufferArray& getUniformBuffers() const override { return buffers; }
|
||||||
|
gfx::UniformBufferArray& mutableUniformBuffers() override { return buffers; }
|
||||||
|
private:
|
||||||
|
LifetimeUniformBuffers buffers;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LifetimeGroup final : public TileLayerGroup {
|
||||||
|
public:
|
||||||
|
LifetimeGroup() : TileLayerGroup(0, 4, "symbol-lifetime") {}
|
||||||
|
const gfx::UniformBufferArray& getUniformBuffers() const override { return buffers; }
|
||||||
|
gfx::UniformBufferArray& mutableUniformBuffers() override { return buffers; }
|
||||||
|
private:
|
||||||
|
LifetimeUniformBuffers buffers;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LifetimeTexture final : public gfx::Texture2D {
|
||||||
|
public:
|
||||||
|
Texture2D& setSamplerConfiguration(const SamplerState&) noexcept override { return *this; }
|
||||||
|
Texture2D& setFormat(gfx::TexturePixelType, gfx::TextureChannelDataType) noexcept override { return *this; }
|
||||||
|
Texture2D& setSize(Size value) noexcept override { size = value; return *this; }
|
||||||
|
Texture2D& setImage(std::shared_ptr<PremultipliedImage>) noexcept override { dirty = true; return *this; }
|
||||||
|
gfx::TexturePixelType getFormat() const noexcept override { return gfx::TexturePixelType::RGBA; }
|
||||||
|
Size getSize() const noexcept override { return size; }
|
||||||
|
size_t getDataSize() const noexcept override { return size.width * size.height * 4; }
|
||||||
|
size_t getPixelStride() const noexcept override { return 4; }
|
||||||
|
size_t numChannels() const noexcept override { return 4; }
|
||||||
|
void create() override {}
|
||||||
|
void upload(const void*, const Size&) override { dirty = false; }
|
||||||
|
void uploadSubRegion(const void*, const Size&, uint16_t, uint16_t) override { dirty = true; }
|
||||||
|
void upload() override { dirty = false; }
|
||||||
|
bool needsUpload() const noexcept override { return dirty; }
|
||||||
|
bool dirty = false;
|
||||||
|
private:
|
||||||
|
Size size{8, 8};
|
||||||
|
};
|
||||||
|
|
||||||
|
class LifetimeTileData final : public TileRenderData {
|
||||||
|
public:
|
||||||
|
LifetimeTileData(std::shared_ptr<TileAtlasTextures> atlases, std::shared_ptr<int> live_)
|
||||||
|
: TileRenderData(std::move(atlases)), live(std::move(live_)) { ++*live; }
|
||||||
|
~LifetimeTileData() override { --*live; }
|
||||||
|
private:
|
||||||
|
// This fixture supplies atlas-only render data. The base implementations
|
||||||
|
// assert: prepare() and the empty-group collector must dispatch here.
|
||||||
|
const LayerRenderData* getLayerRenderData(const style::Layer::Impl&) const override { return nullptr; }
|
||||||
|
Bucket* getBucket(const style::Layer::Impl&) const override { return nullptr; }
|
||||||
|
std::shared_ptr<int> live;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LifetimeTile final : public Tile {
|
||||||
|
public:
|
||||||
|
LifetimeTile(std::shared_ptr<int> liveData_, std::shared_ptr<int> liveTiles_)
|
||||||
|
: Tile(Kind::Geometry, OverscaledTileID{2, 0, {2, 1, 1}}, "symbol-lifetime-source"),
|
||||||
|
liveData(std::move(liveData_)), liveTiles(std::move(liveTiles_)) { ++*liveTiles; }
|
||||||
|
~LifetimeTile() override { --*liveTiles; }
|
||||||
|
std::unique_ptr<TileRenderData> createRenderData() override {
|
||||||
|
return std::make_unique<LifetimeTileData>(atlases, liveData);
|
||||||
|
}
|
||||||
|
void cancel() override {}
|
||||||
|
bool layerPropertiesUpdated(const Immutable<style::LayerProperties>&) override { return true; }
|
||||||
|
bool holdForFade() const override { return true; }
|
||||||
|
std::shared_ptr<TileAtlasTextures> atlases = std::make_shared<TileAtlasTextures>();
|
||||||
|
private:
|
||||||
|
std::shared_ptr<int> liveData, liveTiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LifetimeSource final : public RenderSource {
|
||||||
|
public:
|
||||||
|
LifetimeSource(std::shared_ptr<int> liveData, std::shared_ptr<int> liveTiles)
|
||||||
|
: RenderSource(makeMutable<style::GeoJSONSource::Impl>(
|
||||||
|
"symbol-lifetime-source", style::GeoJSONOptions::defaultOptions())),
|
||||||
|
tile(std::make_unique<LifetimeTile>(std::move(liveData), std::move(liveTiles))) {}
|
||||||
|
bool isLoaded() const override { return true; }
|
||||||
|
void update(Immutable<style::Source::Impl>, const std::vector<Immutable<style::LayerProperties>>&,
|
||||||
|
bool, bool, const TileParameters&) override {}
|
||||||
|
std::unique_ptr<RenderItem> createRenderItem() override { return nullptr; }
|
||||||
|
void prepare(const SourcePrepareParameters& params) override {
|
||||||
|
auto tiles = makeMutable<std::vector<RenderTile>>();
|
||||||
|
tiles->emplace_back(tile->id.toUnwrapped(), *tile);
|
||||||
|
tiles->back().prepare(params);
|
||||||
|
owner = std::move(tiles);
|
||||||
|
sorted = std::make_shared<std::vector<std::reference_wrapper<const RenderTile>>>(
|
||||||
|
std::initializer_list<std::reference_wrapper<const RenderTile>>{owner->front()});
|
||||||
|
}
|
||||||
|
Immutable<std::vector<RenderTile>> getRawRenderTiles() const override { return owner; }
|
||||||
|
RenderTiles getRenderTilesSortedByYPosition() const override { return sorted; }
|
||||||
|
// Deliberately excludes the fading tile, unlike the Symbol path.
|
||||||
|
RenderTiles getRenderTiles() const override {
|
||||||
|
return std::make_shared<std::vector<std::reference_wrapper<const RenderTile>>>();
|
||||||
|
}
|
||||||
|
void updateFadingTiles() override {}
|
||||||
|
bool hasFadingTiles() const override { return true; }
|
||||||
|
std::unordered_map<std::string, std::vector<Feature>> queryRenderedFeatures(
|
||||||
|
const ScreenLineString&, const TransformState&, const std::unordered_map<std::string, const RenderLayer*>&,
|
||||||
|
const RenderedQueryOptions&, const mat4&) const override { return {}; }
|
||||||
|
std::vector<Feature> querySourceFeatures(const SourceQueryOptions&) const override { return {}; }
|
||||||
|
void reduceMemoryUse() override {}
|
||||||
|
void dumpDebugLogs() const override {}
|
||||||
|
std::unique_ptr<LifetimeTile> tile;
|
||||||
|
Immutable<std::vector<RenderTile>> owner = makeMutable<std::vector<RenderTile>>();
|
||||||
|
RenderTiles sorted;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SymbolRetainedLifetime : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
SymbolRetainedLifetime()
|
||||||
|
: styleLayer("symbol-lifetime", "symbol-lifetime-source"),
|
||||||
|
layer(staticImmutableCast<style::SymbolLayer::Impl>(styleLayer.baseImpl)) {
|
||||||
|
state.setSize({640, 480});
|
||||||
|
source = std::make_unique<LifetimeSource>(liveData, liveTiles);
|
||||||
|
source->tile->atlases->glyph = std::make_shared<LifetimeTexture>();
|
||||||
|
source->tile->atlases->icon = std::make_shared<LifetimeTexture>();
|
||||||
|
SymbolRetainedLifetimeTestAccess::setup(layer, group);
|
||||||
|
prepare();
|
||||||
|
}
|
||||||
|
void prepare() {
|
||||||
|
TransformParameters transform(state);
|
||||||
|
const auto debug = MapDebugOptions::NoDebug;
|
||||||
|
source->prepare({transform, debug, *images, "symbol-lifetime-source"});
|
||||||
|
SymbolRetainedLifetimeTestAccess::prepare(layer, {source.get(), *images, patterns, lines, state});
|
||||||
|
}
|
||||||
|
std::shared_ptr<SymbolBucket> makeBucket() {
|
||||||
|
return std::make_shared<SymbolBucket>(makeMutable<style::SymbolLayoutProperties::PossiblyEvaluated>(),
|
||||||
|
std::map<std::string, Immutable<style::LayerProperties>>{}, 16.0f, 1.0f, 0.0f, false, false,
|
||||||
|
"symbol-lifetime", std::vector<SymbolInstance>{}, std::vector<SortKeyRange>{}, 1.0f, false,
|
||||||
|
std::vector<style::TextWritingModeType>{}, false);
|
||||||
|
}
|
||||||
|
LifetimeDrawable& add(const std::shared_ptr<Bucket>& bucket, style::SymbolType type = style::SymbolType::Text,
|
||||||
|
bool halo = false) {
|
||||||
|
auto drawable = std::make_unique<LifetimeDrawable>();
|
||||||
|
auto& ref = *drawable;
|
||||||
|
ref.setTileID(source->tile->id);
|
||||||
|
ref.setRenderPass(RenderPass::Translucent);
|
||||||
|
ref.setLayerTweaker(SymbolRetainedLifetimeTestAccess::tweaker(layer));
|
||||||
|
ref.setBinders(bucket, nullptr); // The production rebind path never reads binders.
|
||||||
|
ref.setShader(std::make_shared<Step119UnknownSymbolProgram>());
|
||||||
|
const auto atlases = source->tile->atlases;
|
||||||
|
auto tweaker = std::make_shared<gfx::DrawableAtlasesTweaker>(atlases,
|
||||||
|
shaders::idSymbolImageIconTexture, shaders::idSymbolImageTexture, type == style::SymbolType::Text,
|
||||||
|
type == style::SymbolType::IconSDF, style::AlignmentType::Viewport, false, true);
|
||||||
|
ref.addTweaker(tweaker);
|
||||||
|
ref.setType(gfx::SymbolDrawableData::drawableType);
|
||||||
|
ref.setData(std::make_unique<gfx::SymbolDrawableData>(halo, false, type, style::AlignmentType::Viewport,
|
||||||
|
style::AlignmentType::Viewport, style::SymbolPlacementType::Point, style::IconTextFitType::None,
|
||||||
|
false, atlases, tweaker));
|
||||||
|
EXPECT_TRUE(SymbolRetainedLifetimeTestAccess::bind(layer, ref, source->owner->front()));
|
||||||
|
tweaker->init(ref); // Actual production texture binding, CPU texture backend only.
|
||||||
|
group->addDrawable(RenderPass::Translucent, source->tile->id, std::move(drawable));
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
bool reuse(const std::shared_ptr<Bucket>& bucket) {
|
||||||
|
return SymbolRetainedLifetimeTestAccess::reuse(layer, source->owner->front(), bucket, source->tile->atlases);
|
||||||
|
}
|
||||||
|
void releaseTiles() {
|
||||||
|
SymbolRetainedLifetimeTestAccess::setTiles(layer, makeMutable<std::vector<RenderTile>>(),
|
||||||
|
std::make_shared<std::vector<std::reference_wrapper<const RenderTile>>>());
|
||||||
|
source.reset();
|
||||||
|
}
|
||||||
|
std::shared_ptr<int> liveData = std::make_shared<int>(0), liveTiles = std::make_shared<int>(0);
|
||||||
|
style::SymbolLayer styleLayer;
|
||||||
|
RenderSymbolLayer layer;
|
||||||
|
std::shared_ptr<LifetimeGroup> group = std::make_shared<LifetimeGroup>();
|
||||||
|
TransformState state;
|
||||||
|
std::shared_ptr<ImageManager> images = ImageManager::create();
|
||||||
|
PatternAtlas patterns;
|
||||||
|
LineAtlas lines;
|
||||||
|
std::unique_ptr<LifetimeSource> source;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, ConsecutiveSourcePrepareRebindsAndReleasesOldRenderData) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
auto& drawable = add(bucket);
|
||||||
|
const auto* oldTile = drawable.getRenderTile();
|
||||||
|
const auto drawableID = drawable.getID();
|
||||||
|
const auto atlasTweaker = drawable.getTweakers().front();
|
||||||
|
prepare();
|
||||||
|
EXPECT_NE(oldTile, &source->owner->front()); // Old owner is still held by drawable.
|
||||||
|
EXPECT_EQ(*liveData, 2);
|
||||||
|
EXPECT_TRUE(reuse(bucket));
|
||||||
|
EXPECT_EQ(drawable.getRenderTile(), &source->owner->front());
|
||||||
|
EXPECT_EQ(*liveData, 1); // No old tree/owner remains after rebind.
|
||||||
|
EXPECT_EQ(drawable.getID(), drawableID);
|
||||||
|
EXPECT_EQ(drawable.getTweakers().front(), atlasTweaker);
|
||||||
|
EXPECT_TRUE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable).complete);
|
||||||
|
EXPECT_TRUE(drawable.getRenderTile()->getAtlasTextures()); // Valid new renderData.
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, MismatchedOwnerAndElementAreRejectedBeforeReading) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
auto& drawable = add(bucket);
|
||||||
|
const auto validOldOwner = source->owner;
|
||||||
|
const auto oldRefs = source->sorted;
|
||||||
|
prepare();
|
||||||
|
EXPECT_FALSE(SymbolRetainedLifetimeTestAccess::bind(layer, drawable, validOldOwner->front()));
|
||||||
|
EXPECT_FALSE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable).complete);
|
||||||
|
SymbolRetainedLifetimeTestAccess::setTiles(layer, source->owner, oldRefs);
|
||||||
|
EXPECT_FALSE(SymbolRetainedLifetimeTestAccess::bind(layer, drawable, source->owner->front()));
|
||||||
|
EXPECT_FALSE(layer.retainedRenderResult(1, 0).complete);
|
||||||
|
// Keep the counterexample owner live; no dangling pointer is dereferenced.
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, AtlasReplacementDropsWholeTextIconHaloGroupAndBindsNewTextures) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
add(bucket);
|
||||||
|
add(bucket, style::SymbolType::Text, true);
|
||||||
|
add(bucket, style::SymbolType::IconRGBA);
|
||||||
|
source->tile->atlases = std::make_shared<TileAtlasTextures>();
|
||||||
|
source->tile->atlases->glyph = std::make_shared<LifetimeTexture>();
|
||||||
|
source->tile->atlases->icon = std::make_shared<LifetimeTexture>();
|
||||||
|
prepare();
|
||||||
|
EXPECT_FALSE(reuse(bucket));
|
||||||
|
EXPECT_TRUE(group->empty());
|
||||||
|
auto& rebuilt = add(bucket);
|
||||||
|
EXPECT_EQ(rebuilt.getTexture(shaders::idSymbolImageTexture), source->tile->atlases->glyph);
|
||||||
|
EXPECT_TRUE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, rebuilt).complete);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, OneIncompatibleMemberDropsWholeGroup) {
|
||||||
|
for (unsigned mismatch = 0; mismatch < 5; ++mismatch) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
add(bucket);
|
||||||
|
auto& halo = add(bucket, style::SymbolType::Text, true);
|
||||||
|
add(bucket, style::SymbolType::IconRGBA);
|
||||||
|
if (mismatch == 0) halo.setBinders(makeBucket(), nullptr);
|
||||||
|
if (mismatch == 1) halo.setLayerTweaker(nullptr);
|
||||||
|
if (mismatch == 2) halo.clearTweakers();
|
||||||
|
if (mismatch == 3) halo.setData(nullptr);
|
||||||
|
if (mismatch == 4) halo.setType(0);
|
||||||
|
EXPECT_FALSE(reuse(bucket));
|
||||||
|
EXPECT_TRUE(group->empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, MissingAtlasDropsOldDrawables) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
add(bucket);
|
||||||
|
source->tile->atlases.reset();
|
||||||
|
prepare();
|
||||||
|
EXPECT_FALSE(reuse(bucket));
|
||||||
|
EXPECT_TRUE(group->empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, SameAtlasTextureReplacementRejectsOldBindingAndRequiresNewBuild) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
auto& drawable = add(bucket);
|
||||||
|
auto original = SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable);
|
||||||
|
ASSERT_TRUE(original.complete);
|
||||||
|
SymbolResultCertificate certificate;
|
||||||
|
const auto binding = step119Binding();
|
||||||
|
step119Settle(certificate, original, binding);
|
||||||
|
const auto atlasTweaker = drawable.getTweakers().front();
|
||||||
|
source->tile->atlases->glyph = std::make_shared<LifetimeTexture>();
|
||||||
|
EXPECT_TRUE(reuse(bucket)); // Same atlas policy is retained and will run normally.
|
||||||
|
auto unbound = SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable);
|
||||||
|
EXPECT_FALSE(unbound.complete);
|
||||||
|
EXPECT_FALSE(certificate.matchesCandidate(unbound));
|
||||||
|
atlasTweaker->init(drawable);
|
||||||
|
auto rebound = SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable);
|
||||||
|
EXPECT_TRUE(rebound.complete);
|
||||||
|
EXPECT_NE(rebound.bytes, original.bytes);
|
||||||
|
EXPECT_FALSE(certificate.observeFull(rebound, binding, true));
|
||||||
|
certificate.completeFull(binding, true, false);
|
||||||
|
EXPECT_TRUE(certificate.observeFull(rebound, binding, true));
|
||||||
|
EXPECT_FALSE(certificate.candidateHasBuild(binding));
|
||||||
|
certificate.completeFull(binding, true, true);
|
||||||
|
EXPECT_TRUE(certificate.observeFull(rebound, binding, true));
|
||||||
|
certificate.completeFull(binding, true, false, true);
|
||||||
|
EXPECT_TRUE(certificate.canFast(binding));
|
||||||
|
EXPECT_EQ(drawable.getTweakers().front(), atlasTweaker);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, InPlaceTextureUploadRejectsEvidenceUntilUploaded) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
auto& drawable = add(bucket);
|
||||||
|
const auto texture = std::static_pointer_cast<LifetimeTexture>(source->tile->atlases->glyph);
|
||||||
|
texture->uploadSubRegion(nullptr, {1, 1}, 0, 0);
|
||||||
|
EXPECT_FALSE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable).complete);
|
||||||
|
texture->upload();
|
||||||
|
EXPECT_TRUE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable).complete);
|
||||||
|
// Pixel updates AFTER upload additionally require the real content epoch;
|
||||||
|
// Step119SymbolResults.RealTileGlyphImageStyleContextCallbacksRejectBuiltCertificate covers it.
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, OwnerIdentityDoesNotEnterCertificateAndEvidenceKeepsRenderData) {
|
||||||
|
// Empty native Symbol group still exercises the complete production collector.
|
||||||
|
auto first = layer.retainedRenderResult(1, 0);
|
||||||
|
ASSERT_TRUE(first.complete);
|
||||||
|
prepare();
|
||||||
|
auto second = layer.retainedRenderResult(1, 0);
|
||||||
|
ASSERT_TRUE(second.complete);
|
||||||
|
EXPECT_EQ(first.bytes, second.bytes);
|
||||||
|
EXPECT_EQ(*liveData, 2);
|
||||||
|
SymbolResultCertificate certificate;
|
||||||
|
const auto binding = step119Binding();
|
||||||
|
EXPECT_FALSE(certificate.observeFull(first, binding, true));
|
||||||
|
certificate.completeFull(binding, true, false);
|
||||||
|
EXPECT_TRUE(certificate.observeFull(second, binding, true));
|
||||||
|
certificate.completeFull(binding, true, true);
|
||||||
|
EXPECT_TRUE(certificate.observeFull(second, binding, true));
|
||||||
|
certificate.completeFull(binding, true, false, true);
|
||||||
|
EXPECT_TRUE(certificate.canFast(binding));
|
||||||
|
first = {}; // Old renderData can now leave, despite the old ref-list having existed.
|
||||||
|
EXPECT_EQ(*liveData, 1);
|
||||||
|
second = {};
|
||||||
|
releaseTiles();
|
||||||
|
EXPECT_EQ(*liveTiles, 0); // Container evidence does not own Tile&.
|
||||||
|
EXPECT_EQ(*liveData, 1); // But it owns RenderTile -> TileRenderData.
|
||||||
|
certificate.invalidate();
|
||||||
|
EXPECT_EQ(*liveData, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, LayerRemovalAndCertificateInvalidationReleaseResources) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
auto& drawable = add(bucket);
|
||||||
|
std::weak_ptr<TileAtlasTextures> atlas = source->tile->atlases;
|
||||||
|
SymbolResultCertificate certificate;
|
||||||
|
step119Settle(certificate, SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable), step119Binding());
|
||||||
|
SymbolRetainedLifetimeTestAccess::removeLayer(layer);
|
||||||
|
EXPECT_TRUE(group->empty());
|
||||||
|
releaseTiles();
|
||||||
|
EXPECT_EQ(*liveTiles, 0);
|
||||||
|
EXPECT_EQ(*liveData, 0);
|
||||||
|
EXPECT_FALSE(atlas.expired()); // Retained atlas evidence outlives the removed group.
|
||||||
|
certificate.invalidate();
|
||||||
|
EXPECT_TRUE(atlas.expired());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, SameSourceIDReplacementRejectsOldAtlasAndReleasesOldContainer) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
add(bucket);
|
||||||
|
std::weak_ptr<TileAtlasTextures> oldAtlas = source->tile->atlases;
|
||||||
|
source = std::make_unique<LifetimeSource>(liveData, liveTiles); // Same source/tile ID, new Tile and atlas.
|
||||||
|
source->tile->atlases->glyph = std::make_shared<LifetimeTexture>();
|
||||||
|
source->tile->atlases->icon = std::make_shared<LifetimeTexture>();
|
||||||
|
prepare(); // Never invoke a method on the old Tile&, which has already gone.
|
||||||
|
EXPECT_FALSE(reuse(bucket));
|
||||||
|
EXPECT_TRUE(group->empty());
|
||||||
|
EXPECT_TRUE(oldAtlas.expired());
|
||||||
|
EXPECT_EQ(*liveData, 1);
|
||||||
|
auto& rebuilt = add(bucket);
|
||||||
|
EXPECT_TRUE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, rebuilt).complete);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, TileLeavesCoverCleanupRemovesDrawable) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
add(bucket);
|
||||||
|
releaseTiles();
|
||||||
|
// The early update branch has no Context dependencies. Use removeAllDrawables,
|
||||||
|
// the production branch's cleanup entry, instead of fabricating a Context reference.
|
||||||
|
EXPECT_EQ(layer.removeAllDrawables(), 1u);
|
||||||
|
EXPECT_TRUE(group->empty());
|
||||||
|
EXPECT_EQ(*liveData, 0);
|
||||||
|
EXPECT_EQ(*liveTiles, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, TextIconSdfAndHalosKeepIdentityAcrossRepeatedPrepare) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
add(bucket);
|
||||||
|
add(bucket, style::SymbolType::Text, true);
|
||||||
|
add(bucket, style::SymbolType::IconRGBA);
|
||||||
|
add(bucket, style::SymbolType::IconSDF);
|
||||||
|
add(bucket, style::SymbolType::IconSDF, true);
|
||||||
|
std::vector<util::SimpleIdentity> identities;
|
||||||
|
std::vector<gfx::DrawableTweakerPtr> tweakers;
|
||||||
|
std::vector<std::vector<uint8_t>> inputs;
|
||||||
|
group->visitDrawables([&](gfx::Drawable& drawable) {
|
||||||
|
identities.push_back(drawable.getID());
|
||||||
|
tweakers.push_back(drawable.getTweakers().front());
|
||||||
|
const auto result = SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable);
|
||||||
|
EXPECT_TRUE(result.complete);
|
||||||
|
inputs.push_back(result.bytes);
|
||||||
|
});
|
||||||
|
for (unsigned generation = 0; generation < 3; ++generation) {
|
||||||
|
prepare();
|
||||||
|
ASSERT_TRUE(reuse(bucket));
|
||||||
|
ASSERT_EQ(group->getDrawableCount(), identities.size());
|
||||||
|
std::size_t index = 0;
|
||||||
|
group->visitDrawables([&](gfx::Drawable& drawable) {
|
||||||
|
EXPECT_EQ(drawable.getID(), identities[index]);
|
||||||
|
EXPECT_EQ(drawable.getTweakers().front(), tweakers[index]);
|
||||||
|
EXPECT_EQ(drawable.getRenderTile(), &source->owner->front());
|
||||||
|
const auto result = SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable);
|
||||||
|
EXPECT_TRUE(result.complete);
|
||||||
|
EXPECT_EQ(result.bytes, inputs[index]);
|
||||||
|
++index;
|
||||||
|
});
|
||||||
|
EXPECT_EQ(*liveData, 1);
|
||||||
|
}
|
||||||
|
// This checks production binding evidence, not complete GPU scene equivalence.
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SymbolRetainedLifetime, UnknownSymbolTypeRejectsEvidenceAndDropsWholeGroup) {
|
||||||
|
const auto bucket = makeBucket();
|
||||||
|
add(bucket);
|
||||||
|
auto& drawable = add(bucket, style::SymbolType::Text, true);
|
||||||
|
const auto atlases = source->tile->atlases;
|
||||||
|
const auto tweaker = std::static_pointer_cast<gfx::DrawableAtlasesTweaker>(drawable.getTweakers().front());
|
||||||
|
drawable.setData(std::make_unique<gfx::SymbolDrawableData>(true, false, static_cast<style::SymbolType>(255),
|
||||||
|
style::AlignmentType::Viewport, style::AlignmentType::Viewport, style::SymbolPlacementType::Point,
|
||||||
|
style::IconTextFitType::None, false, atlases, tweaker));
|
||||||
|
EXPECT_FALSE(SymbolRetainedLifetimeTestAccess::atlasResult(layer, drawable).complete);
|
||||||
|
EXPECT_FALSE(reuse(bucket));
|
||||||
|
EXPECT_TRUE(group->empty());
|
||||||
|
}
|
||||||
|
|||||||
@@ -1208,3 +1208,12 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
|
|||||||
- 2026-10-12 v25 GSRL 发行记录准备:跳过 v24,Android/RN 13.6.1-particle.25、iOS 6.29.0-particle.25,同源构建提交 f32f6f45fd359007e8b3c77911b66d9e7fc93848。统一构建 exit0、六项制品 NAS 公网回读逐字节 PASS、SwiftPM 发布 PASS;新增 Source Lock 与使用说明,最终 Tag/Push/remote verify 须以后续 Pipeline 结果为准。iOS arm64 预编译未变化资源 Assets.car 的来源/Hash 已另文记录;v25 真机运行 NOT_TESTED,STEP-119 CPU 结果不冒充 v25 运行时验证。
|
- 2026-10-12 v25 GSRL 发行记录准备:跳过 v24,Android/RN 13.6.1-particle.25、iOS 6.29.0-particle.25,同源构建提交 f32f6f45fd359007e8b3c77911b66d9e7fc93848。统一构建 exit0、六项制品 NAS 公网回读逐字节 PASS、SwiftPM 发布 PASS;新增 Source Lock 与使用说明,最终 Tag/Push/remote verify 须以后续 Pipeline 结果为准。iOS arm64 预编译未变化资源 Assets.car 的来源/Hash 已另文记录;v25 真机运行 NOT_TESTED,STEP-119 CPU 结果不冒充 v25 运行时验证。
|
||||||
|
|
||||||
- 2026-10-12 v25 正式发行 CLOSED / GSRL PASS:Build Source=f32f6f45fd359007e8b3c77911b66d9e7fc93848;统一 Build/Publish/Finalize 均 exit0,六项公网制品重复回读 SHA/size PASS,SwiftPM 6.29.0-particle.25 已发布;正式 Tag maplibre-particle/rn/13.6.1-particle.25 指向 3774d0a8418daa36d7a988b2ce9650a6a1178fc4,远端 particle/main 与 Tag 在 Finalize 后相同。Source Lock 路径 release/maplibre-particle/13.6.1-particle.25/SOURCE_LOCK.json。v25 正式包真机运行、RN Host 联调 NOT_TESTED;不借 STEP119 旧 APK 的 CPU 结果替代。
|
- 2026-10-12 v25 正式发行 CLOSED / GSRL PASS:Build Source=f32f6f45fd359007e8b3c77911b66d9e7fc93848;统一 Build/Publish/Finalize 均 exit0,六项公网制品重复回读 SHA/size PASS,SwiftPM 6.29.0-particle.25 已发布;正式 Tag maplibre-particle/rn/13.6.1-particle.25 指向 3774d0a8418daa36d7a988b2ce9650a6a1178fc4,远端 particle/main 与 Tag 在 Finalize 后相同。Source Lock 路径 release/maplibre-particle/13.6.1-particle.25/SOURCE_LOCK.json。v25 正式包真机运行、RN Host 联调 NOT_TESTED;不借 STEP119 旧 APK 的 CPU 结果替代。
|
||||||
|
|
||||||
|
|
||||||
|
### 2026-10-12 STEP-119 SymbolRetained 生命周期修复续作
|
||||||
|
|
||||||
|
- 状态 IMPLEMENTED / WAITING_RDC_VALIDATION,接续此前中断四文件修改;Mac.lan particle/main@badb515d590fbdf221751b6677de3da0a5650d7e 保持不变。用户指定 GPT-6.1 Sol High(gpt-6.1-sol/high);本轮工具无执行器元数据、当日 sessions 路径不存在,实际参数未能独立核验,不宣称日志核实,无模型升级/子代理。
|
||||||
|
- 读取 Astra 指导及当前规则/STEP119资料、现有 diff;沿用同源 owner/RenderTile 校验、整组 Bucket/Atlas 失配重建、稳定组刷新绑定与 Retained 强引用取证。证据保活 RenderTile/renderData/LayoutResult,不把 owner 地址写入稳定字节;底层 Tile& 寿命仍独立,原24Gate/证书/缓存与原地内容 epoch 保持。
|
||||||
|
- 续作补 Retained 未知 Symbol 类型 fail-closed;修测试替身未覆盖 TileRenderData 查询导致基类 assert 的缺口,强化 Text/IconRGBA/IconSDF/Halo 连续 prepare 身份、组内类型失配及证书失效释放。SymbolRetainedLifetime 共13项测试源码,真实 GPU/Placement/完整 update 与资源释放仍待 RDC,不能声称实际测试 PASS。
|
||||||
|
- 四源码路径 git diff --check exit0;只做轻量静态检查,未 CMake/编译/语法编译/链接/GTest/Gradle/APK/ADB/安装/Jest,无 commit/push/远端操作。保护 STEP110/111 两份未跟踪任务书及 v25 Source Lock/制品/API/Integration。
|
||||||
|
- 新增 docs/50_Step记录/STEP-119_SymbolRetained生命周期修复_v1.md 与 docs/报告/STEP-119_SymbolRetained生命周期修复_Codex完成报告_v1.md(未覆盖旧版),报告含准确 RDC 命令、源码锁前置条件、FZ-G1 正负及动态生命周期/视觉/释放证据、风险与停止门槛;本记录仅末尾追加。立即交 RDC 独立验证,整体 STEP119 未关闭。
|
||||||
|
|||||||
Reference in New Issue
Block a user