[Fix] (DirectGLES/Managers): under an active transport the server staged copy is the only base - liveHostBase no longer falls back to the client object (M-2); the pool-reuse and whole-store respecify readers go through RequireCoverage (M-3); a twin surviving DropAll has its freed hostBytes nulled (m-5)

This commit is contained in:
2026-09-16 07:45:29 -04:00
parent 0244fc4a50
commit 0fde95357c
@@ -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<const Uint8*>(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