From 22761a692dbab7997c8256d49d729682d47e6f44 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 22 Sep 2026 18:27:43 -0400 Subject: [PATCH] [MG_Pipe, MG_Backend, MG_Remote, docs] (P7): ID-P7-30 - D3's two review fixes: the bit-8 and bit-13 rows are pinned, and deletion ordering is guaranteed by the Live/Gen test The vertex-input and buffer-bindings rows of SubsystemDeps.def became live behaviour when every reader started reading the table; two static_asserts now pin them the way the texture and programs rows already were. The "safe by construction" story about destroy ordering was inverted for the case where a view holds its owner's last reference - the owner's destroy reaches the wire first - so the three places that told it now say that the walk's Live/Gen test is the guarantee. --- MobileGL/MG_Backend/DirectGLES/Managers.h | 20 +++++++++------ MobileGL/MG_Pipe/MGPipe.h | 8 ++++++ MobileGL/MG_Remote/CONTRACT-P5E.md | 13 ++++++---- docs/Disaggregated/notes/p34b/espryt-d3.md | 25 +++++++++++-------- .../notes/p7/INTEGRATOR-DECISIONS-P7.md | 1 + 5 files changed, 44 insertions(+), 23 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index e0023f79..74c9c3bd 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -809,14 +809,18 @@ namespace MobileGL::MG_Backend::DirectGLES { // may not depend on a client invariant to terminate: a chain the wire could make cyclic ends // as a null answer rather than as a hang. // - // DELETION ORDERING IS SAFE BY CONSTRUCTION, AND CHECKED ANYWAY. A view holds a strong - // SharedPtr to its storage owner (TextureObjectView::m_storageOwner) and the client emits - // resource_destroy from the DESTRUCTOR - "the last SharedPtr to this object dropping, not the - // glDelete* that only marks the name and leaves a still-bound object very much alive" - // (MG_State/GLState/TextureState/TextureObject.cpp:63) - so an owner's record cannot die - // while a record that views it is live, however the application orders its glDeleteTextures. - // If one ever did, PipeTextureRecordForHandle tests Live and Gen, so a recycled slot answers - // null rather than a stranger's record. + // DELETION ORDERING: THE LIVE/GEN TEST IS THE GUARANTEE, NOT THE EMIT ORDER. A view holds a + // strong SharedPtr to its storage owner (TextureObjectView::m_storageOwner) and the client + // emits resource_destroy from the DESTRUCTOR - "the last SharedPtr to this object dropping, + // not the glDelete* that only marks the name and leaves a still-bound object very much alive" + // (MG_State/GLState/TextureState/TextureObject.cpp:63). So the owner's storage cannot go away + // under a live view, whatever order the application deletes the two names in. But the two + // destroys are emitted back to back from one destructor chain, and when the view holds the + // owner's LAST reference the member is destroyed before the view's base destructor runs, so + // the wire carries resource_destroy(owner) and then resource_destroy(view): for one apply the + // view record is live with Desc.ViewOf naming a freed slot. No draw can land in between, and + // PipeTextureRecordForHandle tests Live and Gen, so that window answers null (= not clean) + // rather than a recycled stranger's record. const MG_Pipe::MGPipeResourceRecord* PipeTextureStorageRecordForRecord( const MG_Pipe::MGPipeResourceRecord& record); diff --git a/MobileGL/MG_Pipe/MGPipe.h b/MobileGL/MG_Pipe/MGPipe.h index 953eb7d6..f936d174 100644 --- a/MobileGL/MG_Pipe/MGPipe.h +++ b/MobileGL/MG_Pipe/MGPipe.h @@ -234,6 +234,14 @@ namespace MobileGL::MG_Pipe { return ""; } + // Rows 1 and 6 became live behaviour when D3 made every reader read the table (before that + // the bit-7 tests were hard-coded in Managers.cpp and DirectGLES.cpp and these rows were + // dead data). An edit that drops either row now makes BOTH readers agree, so nothing + // downstream can catch it; these two lines can. + static_assert(MGPipeSubsystemRequires(kMGPipeSubsystemVertexInput) == kMGPipeSubsystemResources, + "D-K2's vertex-input row (bit 8 requires bit 7) has gone missing"); + static_assert(MGPipeSubsystemRequires(kMGPipeSubsystemBufferBindings) == kMGPipeSubsystemResources, + "D-K2's buffer-bindings row (bit 13 requires bit 7) has gone missing"); static_assert(MGPipeSubsystemRequires(kMGPipeSubsystemTextureResources) == (kMGPipeSubsystemResources | kMGPipeSubsystemSamplers), "D-K2's fourth row (bit 10 requires bit 11) has gone missing again"); diff --git a/MobileGL/MG_Remote/CONTRACT-P5E.md b/MobileGL/MG_Remote/CONTRACT-P5E.md index 7b99f510..c3a6fbd0 100644 --- a/MobileGL/MG_Remote/CONTRACT-P5E.md +++ b/MobileGL/MG_Remote/CONTRACT-P5E.md @@ -413,11 +413,14 @@ may not depend on a client invariant to terminate), and a storage record that ca **not clean**: the direction that re-syncs, not a refusal, and it raises nothing. `SyncTextureViewToBackendByRecord` stamps that same storage serial — or 0, which reads as never clean — at both its arms, so the stamp and the gate name one quantity. Deletion ordering cannot -strand the walk: a view holds a strong reference to its storage owner and the client emits -`resource_destroy` from the DESTRUCTOR rather than from `glDeleteTextures` -(`TextureObject.cpp:63`), so an owner's record outlives every record that views it whatever order -the application deletes the two names in; `Live`/`Gen` are checked anyway, so a recycled slot -answers null rather than a stranger's record. +strand the walk, and the guarantee is the check, not the emit order: a view holds a strong +reference to its storage owner and the client emits `resource_destroy` from the DESTRUCTOR rather +than from `glDeleteTextures` (`TextureObject.cpp:63`), so the storage cannot go away under a live +view whatever order the application deletes the two names in — but the two destroys leave one +destructor chain back to back, owner FIRST when the view held the owner's last reference, so for +one apply the view record is live with `ViewOf` naming a freed slot. No draw can land in that +window, and the walk tests `Live`/`Gen`, so it answers null (not clean) rather than a recycled +stranger's record. The four in-body live reads (`GetTarget` at `Managers.cpp:8876-8877, 8620-8621`; `IsTextureView` `:7060`; the TexBuffer backing `:8223-8231`) diff --git a/docs/Disaggregated/notes/p34b/espryt-d3.md b/docs/Disaggregated/notes/p34b/espryt-d3.md index 448fa761..1315f50f 100644 --- a/docs/Disaggregated/notes/p34b/espryt-d3.md +++ b/docs/Disaggregated/notes/p34b/espryt-d3.md @@ -112,17 +112,22 @@ cost to discover later: `PipeTextureRecordForHandle` tests `Live` and `Gen`, so a recycled slot answers null rather than a stranger's record. -**Deletion ordering for the chosen shape, checked rather than assumed.** A view may outlive its -owner's GL NAME — `glDeleteTextures(owner)` while the view is alive is legal and the storage -must survive. It cannot outlive the owner's RECORD, and that is by construction on the client: -`TextureObjectView::m_storageOwner` is a strong `SharedPtr`, and +**Deletion ordering for the chosen shape: the walk's `Live`/`Gen` test is the guarantee, not +the emit order** (the review corrected an earlier draft of this paragraph that claimed the +ordering alone was enough). A view may outlive its owner's GL NAME — `glDeleteTextures(owner)` +while the view is alive is legal and the storage must survive — and it cannot outlive the owner's +STORAGE: `TextureObjectView::m_storageOwner` is a strong `SharedPtr`, and `MGPipeEmitTextureDestroyAndFree` is called from `TextureObjectBase`'s DESTRUCTOR, whose own comment says why — *"the last SharedPtr to this object dropping, not the glDelete* that only -marks the name and leaves a still-bound object very much alive"* (`TextureObject.cpp:63`). So -`Desc.ViewOf` cannot dangle while the record that carries it is live, whatever order the -application deletes the two names in. The walk validates anyway, and **a null answer is NOT -clean** — the direction that re-syncs, never the direction that shows stale texels. It is not a -refusal and raises nothing (rule I: no new abort site, no new `Fatal` family word). +marks the name and leaves a still-bound object very much alive"* (`TextureObject.cpp:63`). But +`m_storageOwner` is a member of the derived class and there is no user destructor on the view, +so when the view holds the owner's last reference C++ destroys the member first: the wire +carries `resource_destroy(owner)` and then `resource_destroy(view)`, and for exactly one apply +the view record is live with `Desc.ViewOf` naming a freed slot. No draw can land between two +destroys from one destructor chain, and the walk tests `Live`/`Gen`, so that window answers +null — and **a null answer is NOT clean**, the direction that re-syncs, never the direction that +shows stale texels. It is not a refusal and raises nothing (rule I: no new abort site, no new +`Fatal` family word). **Views of views resolve transitively.** One hop always reaches storage in practice — `glTextureView` composes a view-of-a-view onto the ROOT at creation, which is what the spec's @@ -215,7 +220,7 @@ prose statements and the estimate counted two: | `MG_Impl/Pipe/PipeFill.cpp` `P4aFamilyDependencyBits` | its own `kMGPipeP4aFamilyDependencies[]`, four rows | `MGPipeSubsystemRequires()`; the local table is deleted | | `MG_Impl/Pipe/PipeFill.cpp` `P5eFamilyIsLive` | hand-coded `(pushMask & kMGPipeSubsystemResources) == 0` | `MGPipeSubsystemDependenciesAreSet(bit 13, mask)` | | `MG_Backend/DirectGLES/Managers.cpp` `PipeSubsystemDependencyMissing` | took a literal dependency BIT per call site | takes the FAMILY and asks the table | -| …its four `ResolveSubsystemArm` call sites | five calls with five hand-written sentences | four calls, one per family, naming only the family | +| …its `ResolveSubsystemArm` call sites | four calls with four hand-written sentences (`ResolveProgramSubsystemArm` never called it) | three calls, one per family that has a row, naming only the family | | `MG_Backend/DirectGLES/Managers.cpp` `ResolveVertexInputSubsystemArm` | hand-rolled `bitSet && !resourcesBitSet` | the table | | `MG_Backend/DirectGLES/DirectGLES.cpp` `ResolveBufferBindingSubsystemArm` | hand-rolled `bitSet && !resourcesBitSet` | the table | diff --git a/docs/Disaggregated/notes/p7/INTEGRATOR-DECISIONS-P7.md b/docs/Disaggregated/notes/p7/INTEGRATOR-DECISIONS-P7.md index f6a6a915..b8e7a509 100644 --- a/docs/Disaggregated/notes/p7/INTEGRATOR-DECISIONS-P7.md +++ b/docs/Disaggregated/notes/p7/INTEGRATOR-DECISIONS-P7.md @@ -32,4 +32,5 @@ | ID-P7-27 | 2026-09-22 | bsl-esc-menu-854 **不排除**:monolith 807 MiB 通过、spawn 的 server 进程 ~1.2 GiB 死于 scudo `internal map failure`(client 543 MiB、系统余 8 GB)、inproc 1.6 GiB 死——wire 臂的内存缺陷(≈2.1× 字节或映射数上限),派 **M1** 归因(host 复测、逐 store 归因、泄漏/平台、`max_map_count`),修复包随归因定分区。 | `W4-verify/README.md` §3c 的采样表;ID-P7-12 的规则是「monolith 自己红才排除」。 | `CONTRACT-P7.md` §12 | | ID-P7-28 | 2026-09-22 | wave 2-D3 落地(`6eaaf492 de6fb063 25b586d0`;其 `8274dcab` 与集成者的 `6da19f71` 重复,不取):view 陈旧缺陷取「谓词解析到 storage record」而非反向索引(删除顺序:反向索引要在 view 死亡时剪枝,否则 owner 上传会推到被回收给无关纹理的槽——静默跨纹理串号;且把可变状态放进两后端共读的 `MG_Pipe`);删除顺序由构造保证(`TextureObjectView::m_storageOwner` 强 `SharedPtr`,`resource_destroy` 在析构发出,`TextureObject.cpp:63`)。D-K2 「两处一行」实为六处读者,全部改读 `SubsystemDeps.def`,client 侧参数按 `kMGPipeP4aFamilySubsystems` 掩码(bit 8 的 vertex-input 发射归 P3a 规则,`TextureEmit.EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayLive` 抓到);**批准**「`SubsystemDepsTest` 从比两份副本变成比两个读者」的减弱,编译期一半(`MGPipe.h:225/:230`)承担。门:pull rc 0、`.text` 0xa52203、nm 0/0;unit 2426 / split 303 / spawn 219 / tcp 222 / magma 88/67/69 / full 530;census 79、ratchet 186、parity 0。门脚本模板自此对 `linux build rc≠0` 打 `GATE-RED`。 | `notes/p34b/espryt-d3.md`;两处基线引文更正(KHR-GL46 非 GL43;`texture_view` 1/7 红是 `view_sampling` 不是 `coherency`)。 | `Managers.{h,cpp}`、`PipeFill.cpp`、`DirectGLES.cpp`、`SubsystemDeps.def`、`CONTRACT-P5E.md` §5.2、`CONTRACT-P7.md` §12 | | ID-P7-29 | 2026-09-22 | wave 2-D2 返工落地(`3cdaa5ee ec56644f 623d6641 c13ba36b`,叠在 D3 之上;`SubsystemDepsTest` 取 D2 版守卫 + 两条注记,`split-scenario-census.md` 与 D3 的附记手工合并):TUS 散点内缩后 gold 不变(`emit=9 box=9 rect=0 jobs=9` 四臂同)但对象对了——**零补丁 red-once** `MOBILEGL_ESPRYT_DISABLE_UNPACK_RING=1` 给 `box=3 rect=6 jobs=183`(两张散点纹理都动,26 像素例仍绿);顺带抓到 `ASSERT_TRUE(clientWindow.found)` 被「counters ON」横幅满足的假读,改为带 `tex[]` 的窗口;`espryt_memo_purity.py` 锚点/allow 活性断言后 6 条 allow 剩 1;`+76`/72 数字改正、每片 Gates 表。门:pull rc 0、G1 恒等、unit 2426 / split 303 / spawn 219 / tcp 222(`F1WireScenario.TextureReadbackRejectsPackedDestinationOverflowBeforeWriting` 首跑 1 红、rerun 绿——tcp 车道已知 flake 形状,非本包文件)/ magma 88/67/69 / full 530。 | D2 报告;tcp flake 记 §12 的既有条。 | `TextureUploadShapeScenario.cpp`、`PipeStatsWindow.h`、`espryt_memo_purity.py`、`test.yml`、`split-scenario-census.md`、`MEASUREMENTS.md` §13.1 | +| ID-P7-30 | 2026-09-22 | D3 的 fable 审查判「带修复落地」,两处修复由集成者直接落在 pipe:(1) `SubsystemDeps.def` 第 1/6 行(bit 8 / bit 13 各 requires bit 7)自 D3 起是活行为、却无任何钉子(`MGPipe.h` 的 static_assert 只钉纹理行/程序行/0x7ff/0x1fff,`SubsystemDepsTest` 只扫四个 P4a resolver)——加两条 `static_assert`;(2) 「删除顺序由构造保证」的说法在常见情形下反了:`m_storageOwner` 是派生类成员、destroy 从基类析构发出,view 持有 owner 最后一个引用时 wire 上先 `resource_destroy(owner)` 后 `(view)`,一次 apply 内 view 记录活着而 `ViewOf` 指向已释放槽——保证是 walk 的 `Live`/`Gen` 检查,不是顺序;`Managers.h`、`CONTRACT-P5E.md` §5.2、`espryt-d3.md` 三处改写,call-site 计数「五→四」改「四→三」。审查确认:门代数对非 view 逐字节同旧;G1 每处改动都在 `PIPE_PUSH`/`BUILD_DISAGGREGATED` 守卫内;red-once 在 tcp 臂真跑真红;引文(KHR-GL46、`view_sampling`)属实。**批准** D3 改 `CONTRACT-P5E.md` §5.2(既非 R-5 也非 R-3,但是 D2 普查行点名的修正)。 | 审查报告;`~/w7/pipe` 上 `range-diff` 三个提交 `=`。 | `MGPipe.h`、`Managers.h`、`CONTRACT-P5E.md`、`espryt-d3.md` | | ID-P7-19 | 2026-09-22 | OpenRA 真机分歧改判为 **wire 臂的 GPU 时序竞态**(冷驱动缓存 = 慢 pipeline 创建 → 最终帧回读在最后几批 draw 完成前采样),归 wave 2-B 第 0 片;主机 red-once 用 pipeline 创建延迟旋钮复现;真机验收 = 三次 `pm clear` 冷跑 1.0。F1 保留(机制正确)但不再被当作分歧原因。 | E7:`pm clear` 三遍 0.976493989 / 14658 逐字相同、热跑 1.0、冷 monolith 1.0、`FRAMESINFLIGHT=1` 一绿一红;两臂 client/server 日志逐行相同;diff 只缺最后画的对象。 | `WireFramebuffer.inc`、`VulkanRenderer.cpp`、`F1-verify/README.md` |