diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index a9adb1d4..39af5460 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2926,6 +2926,19 @@ namespace MobileGL::MG_Backend::DirectGLES { // fallback for the drains that have NO object (the readback flush), and it is // nulled at both of those events. const auto liveHostBase = [&resource, &bufferObject]() -> const Uint8* { +#if MOBILEGL_BUILD_DISAGGREGATED + // M-2 / codex 3 / ID-52 item 3: under an ACTIVE TRANSPORT the server's staged + // copy (resource->hostBytes, filled by MGL_SERVER_STAGED_ADOPT in Ops_H_SubData / + // Ops_H_FlushRange) is the ONLY authoritative base. Preferring the frontend + // object's MappedData() here - which under inproc is always non-null, same process + // - meant every reduced-path drain read the CLIENT's shadow and NEVER the server's, + // so a corrupt staged upload rendered the frontend's correct bytes and the R-2.5 + // 0xDD audit could not reach a draw. R-2 / table 3: honest inproc is inproc that + // does not read the client object's memory. Under monolith nothing changes. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + return resource->hostBytes; + } +#endif if (bufferObject) return bufferObject->MappedData(); return resource->hostBytes; }; @@ -2942,6 +2955,17 @@ namespace MobileGL::MG_Backend::DirectGLES { resource->persistentMapped = false; resource->persistentPtr = nullptr; resource->immutableStorage = false; +#if MOBILEGL_BUILD_DISAGGREGATED + // m-5 / codex 5: OnBackendContextDestroyed ran MGL_SERVER_STAGED_DROP_ALL(), which + // frees every server shadow but does NOT null the hostBytes that name them - so a + // twin that SURVIVES a context loss (this is the block that repairs it) still + // carries a base into the freed allocation. The two other drop sites pair the drop + // with something that makes the base unreachable (Ops_H_Destroy retires the twin; + // the map-persistent site nulls hostBytes on the next line); DropAll did neither. + // Null it here, at the one place a surviving twin is re-armed, so no freed base + // reaches glBufferData/glBufferSubData before the next content record refills it. + resource->hostBytes = nullptr; +#endif } // An immutable store nothing maps any more, retired here on the thread that can. @@ -2977,6 +3001,14 @@ namespace MobileGL::MG_Backend::DirectGLES { resource->storageInitialized = true; 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"); g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize, liveHostBase()); if (MG_Util::PipeStats::Enabled()) { MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, @@ -3050,8 +3082,21 @@ namespace MobileGL::MG_Backend::DirectGLES { const Bool shadowHasContent = bufferObject ? bufferObject->HasDefinedContent() : (record->Desc.HasDefinedContent != 0); const void* initialData = shadowHasContent ? hostBase : nullptr; + // M-3 / codex 4: a RespecifyStorageWith(..., initialData != nullptr) is a WHOLE-STORE + // [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. + const auto requireWholeStoreCoverage = [&]() { + if (initialData != nullptr) { + MGL_SERVER_STAGED_REQUIRE(*resource, static_cast(initialData), 0, + size, "respecify_whole_store"); + } + }; if (resource->pendingRespecify || !resource->storageInitialized || resource->storageSize != size) { + requireWholeStoreCoverage(); RespecifyStorageWith(*resource, size, usage, initialData, serial); } else if (!resource->pendingRanges.empty()) { // Same rule as Ops_H_Readback's, at the draw-time drain: with no frontend object @@ -3062,6 +3107,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } else if (resource->syncedChangeSerial != serial) { // Mutations this backend could not track (the table was unregistered between // contexts); re-upload everything. + requireWholeStoreCoverage(); RespecifyStorageWith(*resource, size, usage, initialData, serial); } return resource; @@ -3113,6 +3159,11 @@ namespace MobileGL::MG_Backend::DirectGLES { resource->persistentMapped = false; resource->persistentPtr = nullptr; resource->immutableStorage = false; +#if MOBILEGL_PIPE_PUSH + // m-5 / codex 5: mirror the handle arm - DropAll freed the shadow this base named + // on context loss, so a surviving twin repaired here must not carry it forward. + resource->hostBytes = nullptr; +#endif } // An immutable store nothing maps any more: a respecification of a buffer that