From 1646049ac9693d5ba272d3229b3bfe306e0c7d26 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 16 Sep 2026 10:31:29 -0400 Subject: [PATCH] [Fix, Test] (DirectGLES/Managers, MG_Test/Wire): M-3's two whole-store refusals key on the DESCRIPTOR's HasDefinedContent - content the application supplied (glBufferData(size, data)) must be fully staged by the draw, a gap is a missing record and Fatal{StageSnapshotTooNarrow} by name; a store the application orphaned (glBufferData(size, NULL)) has undefined bytes by its own declaration, so the streaming idiom (orphan, partial glBufferSubData, draw) uploads the staged bytes plus zero-fill exactly as the monolith arm uploads MappedData(); round 2 refused both shapes and aborted six LargeArenaAdoption / ResourceSubsystemControl joint entries by name (hidden under c1 round 2's BlobMissing abort until c1 round 3); two positive gates run the idiom end to end in a forked child through each reader and red on their own string when the refusal is made unconditional again --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 52 +++++++-- MobileGL/MG_Test/Wire/ServerLoopTest.cpp | 114 ++++++++++++++++++-- 2 files changed, 148 insertions(+), 18 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index a9eda1a4..b1883c2c 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2003,6 +2003,23 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto* record = ResourceRecordOf(res); return record != nullptr ? static_cast(record->Desc.Width) : 0; } +#if MOBILEGL_BUILD_DISAGGREGATED + // M-3's rule for the two WHOLE-STORE readers (v1 round 3). The descriptor is the + // application's own statement about the store: HasDefinedContent set means it SUPPLIED + // the content (glBufferData(size, data)), which under split arrives as resource_subdata + // records behind the respecify (table 1 row 19) - so a coverage gap at the draw is a + // MISSING RECORD and the zero-fill past the coverage is not the application's bytes. + // Clear means it ORPHANED the store (glBufferData(size, NULL), glBufferStorage(NULL)): + // every byte it has not staged since is UNDEFINED by its own declaration, the streaming + // idiom (orphan, partial glBufferSubData, draw) is the ordinary case, and uploading the + // shadow's zero-fill for the rest is exactly what the monolith arm uploads from + // MappedData(). Round 2 refused both shapes and aborted six LargeArenaAdoption / + // ResourceSubsystemControl entries on the joint by name (v1-v3.md 6). + Bool ResourceContentIsDeclared(MG_Pipe::MGPipeHandle res) { + const auto* record = ResourceRecordOf(res); + return record != nullptr && record->Desc.HasDefinedContent != 0; + } +#endif void Ops_H_Create(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc) { (void)res; @@ -3028,13 +3045,19 @@ namespace MobileGL::MG_Backend::DirectGLES { resource->pendingRespecify = false; BindBufferId(TempBufferTarget, reused); // M-3 / codex 4: this is a WHOLE-STORE upload from the base, and under split - // the base is the server shadow (M-2), whose zero-filled bytes past the staged - // coverage are not the application's - uploading them is the silent data loss - // the M-6 ruling forbids. RequireCoverage is a no-op for the legacy arm's - // MappedData() and for a non-copying store; under split it Fatals by name on a - // sparse shadow rather than seeding the driver with zeroes. - MGL_SERVER_STAGED_REQUIRE(*resource, liveHostBase(), 0, poolSize, - "pool_reuse_whole_store"); + // the base is the server shadow (M-2). For a store whose content the + // application SUPPLIED, zero-filled bytes past the staged coverage are not the + // application's - uploading them is the silent data loss the M-6 ruling + // forbids, and a gap is a missing record: Fatal by name. For a store the + // application ORPHANED the gap is its own undefined content and the upload is + // legal (ResourceContentIsDeclared, above). RequireCoverage is a no-op for the + // legacy arm's MappedData() and for a non-copying store. +#if MOBILEGL_BUILD_DISAGGREGATED + if (ResourceContentIsDeclared(res)) { + MGL_SERVER_STAGED_REQUIRE(*resource, liveHostBase(), 0, poolSize, + "pool_reuse_whole_store"); + } +#endif g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize, liveHostBase()); if (MG_Util::PipeStats::Enabled()) { MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, @@ -3112,13 +3135,22 @@ namespace MobileGL::MG_Backend::DirectGLES { // [0, size) upload from the base, so it owes the same coverage the pending-range drain // below owes - the two respecify arms were the readers M-6's "never widened" rule did // not reach. No-op for the legacy arm's MappedData() and for a non-copying store; a - // Fatal{StageSnapshotTooNarrow, "respecify_whole_store"} under split when the shadow's - // coverage does not span the store, instead of uploading its zero-fill as content. + // Fatal{StageSnapshotTooNarrow, "respecify_whole_store"} under split when the + // DESCRIPTOR says the application supplied the content and the shadow's coverage does + // not span the store (a missing record), instead of uploading its zero-fill as content. + // NOT for a store the application orphaned: there `shadowHasContent` is the frontend + // object's flag, which the streaming idiom's partial glBufferSubData flips to true, and + // the bytes it did not write are undefined by its own glBufferData(NULL) - the rule at + // ResourceContentIsDeclared. Round 2 refused that idiom and aborted six joint entries. const auto requireWholeStoreCoverage = [&]() { - if (initialData != nullptr) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (initialData != nullptr && record->Desc.HasDefinedContent != 0) { MGL_SERVER_STAGED_REQUIRE(*resource, static_cast(initialData), 0, size, "respecify_whole_store"); } +#else + (void)initialData; +#endif }; if (resource->pendingRespecify || !resource->storageInitialized || resource->storageSize != size) { diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index b7272931..77693034 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -1397,13 +1397,14 @@ TEST(ServerLoopEglTest, AReadPixelsReplyIsTheTightExtentWhateverDstSizeTheClient // production sequence, on the real op table and the real backend: A - glBufferData(64, data), drawn // (ensured), then deleted on the apply thread, so its id retires into the buffer pool; two presents // with a finish between them move the frame-completion watermark past the id's retire serial, which -// is the pool's hand-out rule. B - glBufferData(64, NULL) then glBufferSubData(0, 16), the ordinary -// streaming idiom, so its server shadow covers [0, 16) of 64. B's first ensure finds A's id in the -// pool and would seed the driver's whole 64-byte store from that shadow, 48 bytes of which nothing -// staged. The refusal fires BEFORE the glBufferSubData, so the forked child - whose only thread is a -// copy of this one and holds no context - reaches it with no GL call that matters (a no-context call -// dispatches to a no-op). Red once by deleting the pool-reuse MGL_SERVER_STAGED_REQUIRE: the child -// seeds the store and does not die. +// is the pool's hand-out rule. B - glBufferData(64, data2), whose 64 bytes cross as resource_subdata +// behind the respecify (table 1 row 19) - and only 16 of them arrive, the MISSING-RECORD shape. B's +// first ensure finds A's id in the pool and would seed the driver's whole 64-byte store from a shadow +// 48 bytes of which nothing staged. The refusal fires BEFORE the glBufferSubData, so the forked +// child - whose only thread is a copy of this one and holds no context - reaches it with no GL call +// that matters (a no-context call dispatches to a no-op). Red once by deleting the pool-reuse +// MGL_SERVER_STAGED_REQUIRE: the child seeds the store and does not die. (The ORPHANED shape of the +// same sequence, glBufferData(64, NULL) + 16 bytes, must NOT die: TheStreamingIdiom... below.) TEST(StagedShadowProductionTest, ASparseShadowForcedThroughAPoolReuseUploadIsFatalByName) { EglServerFixture fixture; MGL_EGL_BRING_UP_OR_BAIL(fixture); @@ -1423,7 +1424,7 @@ TEST(StagedShadowProductionTest, ASparseShadowForcedThroughAPoolReuseUploadIsFat MOBILEGL_OK); ASSERT_TRUE(aEnsured) << "A's ensure minted no storage, so nothing retired into the pool"; - const MG_Pipe::MGPipeHandle b = DeclareBuffer(BufferDesc(22, 64, false)); + const MG_Pipe::MGPipeHandle b = DeclareBuffer(BufferDesc(22, 64, true)); StageBytes(b, 0, 16, 0x22); ASSERT_NE(Twin(b), nullptr) << "the subdata did not mint B's twin (ID-52 item 3)"; ASSERT_EQ(Twin(b)->id, 0u) << "B already has a store; the pool-reuse arm cannot be reached"; @@ -1606,6 +1607,103 @@ TEST(StagedShadowProductionTest, TheEnsurePathUploadsTheServerShadowNotTheClient fixture.TearDown(); } + +namespace { + std::string ReadLogFrom(SizeT offset) { + const std::string whole = ReadLog(); + return offset < whole.size() ? whole.substr(offset) : std::string(); + } + + // THE STREAMING IDIOM, end to end, in a FORKED CHILD that brings the server up itself (the + // integration harness's pre-flight shape): a Fatal on the apply thread ends the child, not the + // case, so the case can name it. glBufferData(64, NULL) then glBufferSubData(0, 16) then a draw: + // the frontend object ORPHANS its store and writes 16 bytes - which flips ITS HasDefinedContent + // to true - while on the wire that is a resource_respecify with HasDefinedContent CLEAR and one + // 16-byte resource_subdata, so the server shadow covers [0, 16) of 64 and the descriptor says + // the rest is undefined by the application's own declaration. The draw's whole-store upload + // (the respecify reader, or the pool-reuse reader when a 64-byte id is waiting in the pool) + // must go through and leave 16 bytes of pattern and 48 of zero in the driver's store. Exit 0 + // when it did, 7 when the wrong bytes landed, 8 when the child's bring-up failed. + enum class IdiomArm { Respecify, PoolReuse }; + + [[noreturn]] void RunTheStreamingIdiomAndExit(IdiomArm arm) { + EglServerFixture fixture; + if (!fixture.BringUp().empty() || !fixture.MakeCurrent()) ::_exit(8); + Vector sixteen(16, Uint8{0x77}); + auto buffer = MakeShared(arm == IdiomArm::PoolReuse ? 33u : 32u); + buffer->Respecify(64, nullptr); + buffer->UploadSubData(DataPtr{sixteen.data(), sixteen.size()}, 0); + Bool ok = false; + (void)OnApply([&] { + if (arm == IdiomArm::PoolReuse) { + const MG_Pipe::MGPipeHandle a = DeclareBuffer(BufferDesc(29, 64, true)); + StageBytes(a, 0, 64, 0x11); + if (Ensure(a) == nullptr) return; + MG_Pipe::MGPipeGetResourceOps()->Destroy(a); + for (int frame = 0; frame < 2; ++frame) { + if (MG_Backend::DirectGLES::g_GLESFuncs.glFinish) MG_Backend::DirectGLES::g_GLESFuncs.glFinish(); + MG_Backend::DirectGLES::Present(); + } + } + const Uint32 slot = arm == IdiomArm::PoolReuse ? 30u : 28u; + const MG_Pipe::MGPipeHandle b = DeclareBuffer(BufferDesc(slot, 64, false)); + StageBytes(b, 0, 16, 0x77); + auto* twin = MG_Backend::DirectGLES::BufferImpl::EnsureBufferResourceForHandle(buffer, b); // the draw + if (twin == nullptr || twin->id == 0) return; + const auto& gl = MG_Backend::DirectGLES::g_GLESFuncs; + gl.glBindBuffer(GL_ARRAY_BUFFER, twin->id); + const auto* view = static_cast(gl.glMapBufferRange(GL_ARRAY_BUFFER, 0, 64, GL_MAP_READ_BIT)); + if (view == nullptr) return; + ok = view[0] == 0x77 && view[15] == 0x77 && view[16] == 0 && view[63] == 0; + gl.glUnmapBuffer(GL_ARRAY_BUFFER); + }); + ::_exit(ok ? 0 : 7); + } +} // namespace + +// M-3, ROUND 3's RULE, positive direction (the round-2 refusal aborted six LargeArenaAdoption / +// ResourceSubsystemControl entries on the joint by this exact name): the ordinary streaming idiom +// through the RESPECIFY reader is uploaded, not refused. Red once by making the refusal +// unconditional again (the descriptor's HasDefinedContent ignored): the child dies of +// Fatal{StageSnapshotTooNarrow, "respecify_whole_store"} and the appended log names it. +TEST(StagedShadowProductionTest, TheStreamingIdiomOrphanThenPartialSubDataIsUploadedNotRefused) { + { + EglServerFixture probe; + MGL_EGL_BRING_UP_OR_BAIL(probe); + probe.TearDown(); + } + const SizeT mark = ReadLog().size(); + EXPECT_EXIT(RunTheStreamingIdiomAndExit(IdiomArm::Respecify), ::testing::ExitedWithCode(0), ".*") + << "the streaming idiom - glBufferData(64, NULL), glBufferSubData(0, 16), draw - did not put " + "its 16 bytes and 48 undefined (zero) bytes into the driver's store through the respecify " + "reader: exit 7 is the wrong bytes, a signal is the whole-store refusal firing on a store " + "the application itself orphaned"; + const std::string appended = ReadLogFrom(mark); + EXPECT_EQ(appended.find("Fatal{StageSnapshotTooNarrow"), std::string::npos) + << "the orphan-then-partial-subdata idiom was refused by name (M-3's whole-store refusal must " + "key on the descriptor's HasDefinedContent, not on the frontend flag a partial subdata " + "flips); the log says: " << appended; +} + +// The same idiom through the POOL-REUSE reader: a 64-byte id retired to the pool by a previous +// buffer's delete, handed to the orphaned store's first draw, seeded from the sparse shadow. +TEST(StagedShadowProductionTest, TheStreamingIdiomThroughAPoolReuseIsUploadedNotRefused) { + { + EglServerFixture probe; + MGL_EGL_BRING_UP_OR_BAIL(probe); + probe.TearDown(); + } + const SizeT mark = ReadLog().size(); + EXPECT_EXIT(RunTheStreamingIdiomAndExit(IdiomArm::PoolReuse), ::testing::ExitedWithCode(0), ".*") + << "the streaming idiom through a recycled pool id did not put its 16 bytes and 48 undefined " + "(zero) bytes into the driver's store: exit 7 is the wrong bytes, a signal is the " + "whole-store refusal firing on a store the application itself orphaned"; + const std::string appended = ReadLogFrom(mark); + EXPECT_EQ(appended.find("Fatal{StageSnapshotTooNarrow"), std::string::npos) + << "the orphan-then-partial-subdata idiom through a pool reuse was refused by name (M-3's " + "whole-store refusal must key on the descriptor's HasDefinedContent); the log says: " + << appended; +} #endif // !_WIN32 int main(int argc, char** argv) {