From 90d05f596a42c157502f9a2a5f20425c999cebab Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Sun, 13 Sep 2026 14:10:32 +0800 Subject: [PATCH] fix: close STEP-075 projection identity audit --- ...5_QBIN2_V4_Binding与ProjectionIdentity_v2.md | 41 +++++++++++++++++++ ...ding与ProjectionIdentity_Codex完成报告_v2.md | 25 +++++++++++ include/mbgl/particle/field/field_binding.hpp | 19 ++++++--- src/mbgl/particle/field/field_binding.cpp | 26 ++++++------ test/particle/field_binding.test.cpp | 38 ++++++++++++++--- 粒子开发记录.md | 6 +++ 6 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v2.md create mode 100644 docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v2.md diff --git a/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v2.md b/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v2.md new file mode 100644 index 000000000..ba50bf90b --- /dev/null +++ b/docs/50_Step记录/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_v2.md @@ -0,0 +1,41 @@ +# STEP-075 — QBIN2 V4 Binding 与 Projection Identity v2 + +状态:完成 + +日期:2026-09-13 + +## 目标与基线 + +- 基于 STEP-075 v1(`3616daec30079395847cc6a538fbd963641b7e12`)完成独立审计收口。 +- 仅修改 provider-neutral Core Binding/Projection、其离线单元测试与本 Step 文档;没有进入 STEP-076。 + +## 必做项 + +- [x] 新增 `ProjectionFrameCoordinate`,只以 `fieldID + validTimeMs` 表达 T0/T1 window coordinate;完整 `FrameContentIdentity` 仍保留在 resolved frame 中作 revision/runtime 诊断。 +- [x] Color 和 Particle identity 均统一比较 binding、T0/T1 coordinate、各自 selected plane content identity 及 T0/T1 shared validity content identity。 +- [x] 移除 Resolver 公共 `InterpolationIncompatible` 错误码;`FieldSeries::submit` 已拒绝不兼容相邻 frame,`resolve` 只返回已验证 window。 +- [x] 通过 Store 的 same-valid-time `ReplacedRevision` 测试 Scalar-only、U/V-only、validity-only revision:分别为 Color-only change、Particle-only change、两者均 change。 +- [x] 保持各 binding 独立 fieldKey/cadence、MIX 不进入 identity、in-window storage stable,以及 DirectionAngle success/failure coverage。 + +## 允许修改 + +- `include/mbgl/particle/field/field_binding.hpp` +- `src/mbgl/particle/field/field_binding.cpp` +- `test/particle/field_binding.test.cpp` +- 本 Step v2、完成报告 v2 与 `粒子开发记录.md` + +## 禁止修改 + +- 未修改 Decoder、FieldSeries、Profile Validator、Renderer、Host/JNI/RN、Android/iOS 或网络数据路径。 +- 未执行 Android Gradle/APK/ADB/Jest、iOS/Metal、Renderer/GPU 或真实 QBIN Golden 数据。 + +## 验证与当前结果 + +- Linux OpenGL `cmake --build build-linux-opengl --target mbgl-test-runner -j3` 真实重建 PASS(输出节流)。 +- 规定 GTest XML `/tmp/step075-gtest.xml`:50 tests、0 failures、0 errors。Binding 4;Runtime/Profile 11;QBIN Decoder/Ingress 24;V3 Source 11。 +- `git diff --check` PASS。 + +## 阻塞与下一步 + +- 无实现阻塞。Android/Jest/iOS/Metal 等禁止范围没有验证,不能视为 PASS。 +- STEP-075 已关闭;按任务书停止,不进入 STEP-076。 diff --git a/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v2.md b/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v2.md new file mode 100644 index 000000000..1ab9a28d0 --- /dev/null +++ b/docs/报告/STEP-075_QBIN2_V4_Binding与ProjectionIdentity_Codex完成报告_v2.md @@ -0,0 +1,25 @@ +# STEP-075 QBIN2 V4 Binding 与 Projection Identity Codex 完成报告 v2 + +日期:2026-09-13 + +## 已完成 + +- Projection content identity 改为显式 `ProjectionFrameCoordinate(fieldID, validTimeMs)` 加上 binding 实际消费的 T0/T1 selected plane 和 shared validity content identity。完整 generation/dataVersion 不再污染任一 projection identity。 +- 对称 revision 隔离已由 Store 的 `ReplacedRevision` 公共路径验证:Scalar-only 仅改变 Color,U/V-only 仅改变 Particle,validity-only 同时改变 Color 与 Particle。 +- `InterpolationIncompatible` 已从 Resolver 公共错误语义移除;FieldSeries submit 已 fail-closed 拒绝不兼容相邻 frame,合法公开 resolve 路径不可形成该状态。 +- 独立 fieldKey/cadence、MIX identity/storage stability、DirectionAngle success/failure 与既有 V4/V3 回归均保持。 + +## 验证结果 + +- Linux OpenGL `mbgl-test-runner` 真实重建 PASS(输出节流)。 +- 规定 GTest XML:50 tests、0 failures、0 errors;Binding 4、Runtime/Profile 11、QBIN Decoder/Ingress 24、V3 Source 11。 +- `git diff --check` PASS。 + +## 未完成或无法验证 + +- Android Gradle/APK/ADB/Jest、iOS/Metal、Renderer/GPU、Host/JNI/RN、网络和真实 QBIN Golden 数据均按任务禁止或不在范围内,未执行且不能标记 PASS。 +- 无已知实现阻塞;本任务不进入 STEP-076。 + +## Git + +- 本报告随本 Step scoped commit 提交;提交与 `origin/particle/main` push 结果在最终 Git 回填提交中记录。 diff --git a/include/mbgl/particle/field/field_binding.hpp b/include/mbgl/particle/field/field_binding.hpp index b7abdb007..260a51c83 100644 --- a/include/mbgl/particle/field/field_binding.hpp +++ b/include/mbgl/particle/field/field_binding.hpp @@ -37,10 +37,20 @@ struct FieldSampleState { float mix = 0.0f; }; +// The stable coordinate of a FieldSeries interpolation endpoint. Revisions +// are deliberately excluded: projection identities add only the content that +// their binding actually consumes. +struct ProjectionFrameCoordinate { + std::string fieldID; + uint64_t validTimeMs = 0; + + bool operator==(const ProjectionFrameCoordinate&) const; +}; + struct ColorProjectionContentIdentity { ColorBinding binding; - FrameContentIdentity t0Frame; - FrameContentIdentity t1Frame; + ProjectionFrameCoordinate t0Coordinate; + ProjectionFrameCoordinate t1Coordinate; std::string t0Plane; std::string t1Plane; std::string t0Validity; @@ -51,8 +61,8 @@ struct ColorProjectionContentIdentity { struct ParticleProjectionContentIdentity { ParticleBinding binding; - FrameContentIdentity t0Frame; - FrameContentIdentity t1Frame; + ProjectionFrameCoordinate t0Coordinate; + ProjectionFrameCoordinate t1Coordinate; std::string t0FirstPlane; std::string t1FirstPlane; std::string t0SecondPlane; @@ -97,7 +107,6 @@ enum class BindingProjectionError : uint8_t { PlaneUnitMismatch, PlaneComponentMismatch, PlaneGroupMismatch, - InterpolationIncompatible, }; struct ColorProjectionResult { diff --git a/src/mbgl/particle/field/field_binding.cpp b/src/mbgl/particle/field/field_binding.cpp index ef7764e88..d7dcef2f0 100644 --- a/src/mbgl/particle/field/field_binding.cpp +++ b/src/mbgl/particle/field/field_binding.cpp @@ -11,6 +11,9 @@ bool validKey(const FieldPlaneKey& key) { return key.semanticID != 0 && validCom bool sameSelector(const FieldPlaneKey& a, const FieldPlaneKey& b) { return a.semanticID == b.semanticID && a.groupID == b.groupID; } +ProjectionFrameCoordinate coordinate(const DecodedFieldFrame& frame) { + return {frame.frameContentIdentity.fieldID, frame.frameContentIdentity.validTimeMs}; +} const DecodedFieldPlane* findPlane(const DecodedFieldFrame& frame, const FieldPlaneKey& selector) { for (const auto& plane : frame.planes) if (sameSelector(plane.key, selector)) return &plane; return nullptr; @@ -50,7 +53,9 @@ BindingProjectionError validateSelected(const DecodedFieldPlane* t0, if (t0->unitID != unit || t1->unitID != unit) return BindingProjectionError::PlaneUnitMismatch; if (t0->key.component != component || t1->key.component != component) return BindingProjectionError::PlaneComponentMismatch; - if (!t0->isInterpolationCompatibleWith(*t1)) return BindingProjectionError::InterpolationIncompatible; + // FieldSeries::submit rejects incompatible neighbours, and resolve only + // returns a validated window. Resolver has no public incompatible-window + // state, so do not expose an unreachable error code here. return BindingProjectionError::Accepted; } @@ -65,21 +70,16 @@ bool ParticleBinding::operator==(const ParticleBinding& other) const { sameSelector(vPlane, other.vPlane) && sameSelector(directionPlane, other.directionPlane) && expectedUnitID == other.expectedUnitID; } +bool ProjectionFrameCoordinate::operator==(const ProjectionFrameCoordinate& other) const { + return fieldID == other.fieldID && validTimeMs == other.validTimeMs; +} bool ColorProjectionContentIdentity::operator==(const ColorProjectionContentIdentity& other) const { - return binding == other.binding && t0Frame == other.t0Frame && t1Frame == other.t1Frame && + return binding == other.binding && t0Coordinate == other.t0Coordinate && t1Coordinate == other.t1Coordinate && t0Plane == other.t0Plane && t1Plane == other.t1Plane && t0Validity == other.t0Validity && t1Validity == other.t1Validity; } bool ParticleProjectionContentIdentity::operator==(const ParticleProjectionContentIdentity& other) const { - // A FieldFrame revision can carry an unrelated plane update. Projection - // identity therefore uses the selected frame time/field as its window - // coordinate; selected plane and validity content identities carry the - // revision-sensitive content part. This preserves a Particle projection - // across a scalar-only revision in the same frame window. - const auto sameWindowFrame = [](const FrameContentIdentity& a, const FrameContentIdentity& b) { - return a.fieldID == b.fieldID && a.validTimeMs == b.validTimeMs; - }; - return binding == other.binding && sameWindowFrame(t0Frame, other.t0Frame) && sameWindowFrame(t1Frame, other.t1Frame) && + return binding == other.binding && t0Coordinate == other.t0Coordinate && t1Coordinate == other.t1Coordinate && t0FirstPlane == other.t0FirstPlane && t1FirstPlane == other.t1FirstPlane && t0SecondPlane == other.t0SecondPlane && t1SecondPlane == other.t1SecondPlane && t0Validity == other.t0Validity && t1Validity == other.t1Validity; @@ -98,7 +98,7 @@ ColorProjectionResult BindingProjectionResolver::resolve(const EncodedFieldStore t0 ? t0->key.component : FieldComponent::None); error != BindingProjectionError::Accepted) return {error, {}}; - ColorProjection projection{{binding, window->t0->frameContentIdentity, window->t1->frameContentIdentity, + ColorProjection projection{{binding, coordinate(*window->t0), coordinate(*window->t1), t0->contentIdentity, t1->contentIdentity, window->t0->validityContentIdentity, window->t1->validityContentIdentity}, {requested, window->mix}, window->t0, window->t1, t0->storage, t1->storage, @@ -135,7 +135,7 @@ ParticleProjectionResult BindingProjectionResolver::resolve(const EncodedFieldSt if (t0First->key.groupID != t0Second->key.groupID || t1First->key.groupID != t1Second->key.groupID) return {BindingProjectionError::PlaneGroupMismatch, {}}; } - ParticleProjection projection{{binding, window->t0->frameContentIdentity, window->t1->frameContentIdentity, + ParticleProjection projection{{binding, coordinate(*window->t0), coordinate(*window->t1), t0First->contentIdentity, t1First->contentIdentity, t0Second ? t0Second->contentIdentity : std::string{}, t1Second ? t1Second->contentIdentity : std::string{}, diff --git a/test/particle/field_binding.test.cpp b/test/particle/field_binding.test.cpp index e45427170..bcc69c303 100644 --- a/test/particle/field_binding.test.cpp +++ b/test/particle/field_binding.test.cpp @@ -73,10 +73,10 @@ TEST(ParticleFieldBinding, ResolvesIndependentFieldKeyCadence) { ASSERT_TRUE(c.accepted()); ASSERT_TRUE(p.accepted()); EXPECT_FLOAT_EQ(c.projection->sampleState.mix, 0.5f); EXPECT_FLOAT_EQ(p.projection->sampleState.mix, 0.5f); - EXPECT_EQ(c.projection->contentIdentity.t0Frame.validTimeMs, Base); - EXPECT_EQ(c.projection->contentIdentity.t1Frame.validTimeMs, Base + 3 * Hour); - EXPECT_EQ(p.projection->contentIdentity.t0Frame.validTimeMs, Base + Hour); - EXPECT_EQ(p.projection->contentIdentity.t1Frame.validTimeMs, Base + 2 * Hour); + EXPECT_EQ(c.projection->contentIdentity.t0Coordinate.validTimeMs, Base); + EXPECT_EQ(c.projection->contentIdentity.t1Coordinate.validTimeMs, Base + 3 * Hour); + EXPECT_EQ(p.projection->contentIdentity.t0Coordinate.validTimeMs, Base + Hour); + EXPECT_EQ(p.projection->contentIdentity.t1Coordinate.validTimeMs, Base + 2 * Hour); EXPECT_NE(c.projection->t0Frame, p.projection->t0Frame); EXPECT_NE(c.projection->t0ValidityStorage, p.projection->t0ValidityStorage); } @@ -98,7 +98,7 @@ TEST(ParticleFieldBinding, MixChangesSampleStateButNotContentIdentityOrStorage) EXPECT_EQ(at0.projection->t0PlaneStorage, at3.projection->t0PlaneStorage); } -TEST(ParticleFieldBinding, ScalarRevisionChangesOnlyColorIdentity) { +TEST(ParticleFieldBinding, ProjectionIdentityIsolatedBySelectedRevision) { particle::EncodedFieldStore store; submit(store, "weather.wind", makeFrame("wind", Base, 1, "0")); submit(store, "weather.wind", makeFrame("wind", Base + Hour, 1, "1")); @@ -118,6 +118,34 @@ TEST(ParticleFieldBinding, ScalarRevisionChangesOnlyColorIdentity) { EXPECT_EQ(beforeParticle.projection->contentIdentity, afterParticle.projection->contentIdentity); EXPECT_EQ(beforeParticle.projection->t0FirstPlaneStorage, afterParticle.projection->t0FirstPlaneStorage); EXPECT_EQ(beforeParticle.projection->t0ValidityStorage, afterParticle.projection->t0ValidityStorage); + + auto vectorRevision = *afterParticle.projection->t0Frame; + vectorRevision.frameContentIdentity.generation = 3; + vectorRevision.frameContentIdentity.dataVersion = 3; + vectorRevision.planes[0].contentIdentity = "u-revised"; + vectorRevision.planes[1].contentIdentity = "v-revised"; + ASSERT_EQ(store.submit("weather.wind", std::make_shared(std::move(vectorRevision))), + particle::FieldSeriesSubmitResult::ReplacedRevision); + const auto afterVectorColor = particle::BindingProjectionResolver::resolve(store, color("weather.wind"), Base + Hour / 2); + const auto afterVectorParticle = particle::BindingProjectionResolver::resolve(store, wind(), Base + Hour / 2); + ASSERT_TRUE(afterVectorColor.accepted()); ASSERT_TRUE(afterVectorParticle.accepted()); + EXPECT_EQ(afterColor.projection->contentIdentity, afterVectorColor.projection->contentIdentity); + EXPECT_NE(afterParticle.projection->contentIdentity, afterVectorParticle.projection->contentIdentity); + + auto validityRevision = *afterVectorParticle.projection->t0Frame; + validityRevision.frameContentIdentity.generation = 4; + validityRevision.frameContentIdentity.dataVersion = 4; + validityRevision.validityContentIdentity = "valid-revised"; + validityRevision.validityBits = std::make_shared>(std::vector{14}); + validityRevision.validityPlane.contentIdentity = validityRevision.validityContentIdentity; + validityRevision.validityPlane.storage = validityRevision.validityBits; + ASSERT_EQ(store.submit("weather.wind", std::make_shared(std::move(validityRevision))), + particle::FieldSeriesSubmitResult::ReplacedRevision); + const auto afterValidityColor = particle::BindingProjectionResolver::resolve(store, color("weather.wind"), Base + Hour / 2); + const auto afterValidityParticle = particle::BindingProjectionResolver::resolve(store, wind(), Base + Hour / 2); + ASSERT_TRUE(afterValidityColor.accepted()); ASSERT_TRUE(afterValidityParticle.accepted()); + EXPECT_NE(afterVectorColor.projection->contentIdentity, afterValidityColor.projection->contentIdentity); + EXPECT_NE(afterVectorParticle.projection->contentIdentity, afterValidityParticle.projection->contentIdentity); } TEST(ParticleFieldBinding, FailsClosedAndSupportsDirectionAngle) { diff --git a/粒子开发记录.md b/粒子开发记录.md index e44bdde3e..e780e65b3 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -740,3 +740,9 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - 新增 provider-neutral Color/Particle Binding 与 Resolver;每个 binding 按自己的 fieldKey 解析 FieldSeries Window,SampleState 单独保存 requested time/mix,Content Identity 不包含 mix 或视觉/播放状态。 - Vector U/V 与 DirectionAngle 都严格按 selected plane assertion fail-closed;Scalar-only revision 保持 Particle U/V/validity storage 与 identity 不变。 - Linux runner 真实重建、规定 Core filter 50/50 PASS、`git diff --check` PASS;未执行 Android Gradle/APK/ADB/Jest/iOS/Metal,未进入 Renderer/GPU/Host/JNI/RN,STEP-075 关闭并停止。 + +### 2026-09-13 STEP-075 v2 Binding/Projection Identity 审计收口 + +- Color 与 Particle identity 统一以 `ProjectionFrameCoordinate(fieldID, validTimeMs)` 定位 T0/T1 window,只纳入各 binding 实际 selected plane 与 shared validity content identity;无关 plane 的 generation/dataVersion revision 不再污染另一 projection。 +- Store public-path `ReplacedRevision` 对称覆盖:Scalar-only 仅改变 Color、U/V-only 仅改变 Particle、validity-only 同时改变两者;MIX 仍不进入 identity,in-window storage 仍稳定。 +- Resolver 删除不可达 `InterpolationIncompatible`;相邻 frame compatibility 继续由既有 `FieldSeries::submit` fail-closed 保证。真实 Linux runner rebuild、规定过滤 50/50、`git diff --check` 全部 PASS;未执行 Android Gradle/APK/ADB/Jest,按范围关闭并停止,不进入 STEP-076。