mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 06:38:31 +09:00
[Fix, Test] (MG_Impl, MG_Backend/DirectGLES): DSA by-name texture calls corrupted borrowed-slot memo pairings - process-wide glyph death under Iris
Field report: on Espryt with a BSL Iris pipeline built, every glyph in the game died - HUD, menu labels, even the vanilla title screen after leaving the world - while sprites kept rendering. Captured on-device (FCL apitrace rig), reproduced headlessly on llvmpipe, and pinned with a three-way replay: the same trace renders full text on raw Mesa desktop GL and on Magma, so the stream was intact and the execution was Espryt's. MECHANISM. WithTemporarilyBoundNamedTexture implements the by-name (DSA) texture entry points by binding the named texture onto the active unit's real slot, running the bound-texture code, and restoring - without moving the texture bind generation on either edge. DirectGLES's per-draw texture sync memo keys on that generation and BORROWS the slot pointer, so a memo built for texture A kept passing every key while a by-name call had texture B sitting in the slot: A's backend twin was driven with B's frontend object, and SyncMipmapsToBackend re-specified A's storage with B's shape. In the trace, a by-name upload to a BSL 2048x2048 map while the 16x16 lightmap was bound re-specified the lightmap's GL texture 2048x2048-NULL and back 16x16-NULL. The lightmap exists only as render output - no glTexSubImage2D ever touches it - so it stayed zero forever, and rendertype_text (vertexColor = Color * texelFetch(lightmap, ...)), alpha-discards every glyph. Background quads never sample the lightmap, which is why only text died. FIX, class-level, two layers: - Frontend (shared, closes the same hole for DirectVulkan's generation- keyed memos): the temporary bind and the restore each bump the texture bind generation (only when the slot actually changed), and the restore is an RAII scope guard so a throwing body can no longer leak the temporary binding - a second latent bug of the same class. Deliberately a generation bump and not a touched-unit note: the high-water mark must not chase by-name calls, and a completed bind/restore pair leaves the content epoch unchanged, so the cost is an owner-compare re-walk, not a memo rebuild. - DirectGLES defense in depth: both borrowed-pair memos (g_unitTextureSyncList, g_fboTextureSyncList) record which frontend texture each backend twin was paired with and re-check it before any replay (last in the key conjunction, behind the context-id compare). A stale pairing now costs a list rebuild instead of silent cross-texture storage corruption. Tests, both red with their own layer reverted: TextureTest.NamedTextureCallKeepsUnitBindingAccountingCoherent (the accounting contract) and DirectGLESTextureSync.UnitMemoRefusesToDriveA- TwinFromAnotherTexture (the corrupting sequence shape against a mock GLES table, asserting the resident texture's storage is never re-specified). 595/595 unit at default and with the async kill switch. Replay evidence: the captured BSL ESC-menu trace renders all text through Espryt post-fix, byte-comparable to the Mesa-direct and Magma replays; the no-shaderpack control is unchanged. A trace fixture wiring this scene into CI follows in a separate commit.
This commit is contained in:
@@ -1127,10 +1127,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// slot the key covers still holds a reference to it. Holding either side by shared_ptr
|
||||
// instead would keep dead frontend textures alive and defeat the registry's
|
||||
// weak-reference GC.
|
||||
//
|
||||
// `texture` records WHICH frontend object `backend` was paired with when the entry was
|
||||
// built, and PairingsIntact re-checks it before any replay. The keys above are the
|
||||
// primary guard, but they are all derived state: a slot swap that never reaches the
|
||||
// bind generation (the DSA by-name emulation used to swap a slot silently) would leave
|
||||
// every key matching while the borrowed slot pointed at a different texture, and the
|
||||
// replay would then drive texture A's backend twin from texture B's frontend state -
|
||||
// re-specifying A's backend storage with B's shape and destroying A's contents. A raw
|
||||
// pointer compare per entry is far cheaper than the walk it guards, and a stale pairing
|
||||
// costs only a list rebuild, so this stays as the structural net under the keys.
|
||||
struct UnitTextureSyncEntry {
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>* slot = nullptr;
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
BackendTextureObject* backend = nullptr;
|
||||
};
|
||||
// True while every entry's borrowed slot still holds the texture the entry was paired
|
||||
// with. Callers put it LAST in the key conjunction so it only runs on a key hit.
|
||||
static Bool PairingsIntact(const Vector<UnitTextureSyncEntry>& list) {
|
||||
for (const auto& entry : list) {
|
||||
if (entry.slot->get() != entry.texture) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static Vector<UnitTextureSyncEntry> g_unitTextureSyncList;
|
||||
static Bool g_unitTextureSyncListValid = false;
|
||||
static Uint64 g_unitTextureSyncListContextId = 0;
|
||||
@@ -1197,7 +1216,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_unitTextureSyncListMaxUnit == maxTouchedUnit &&
|
||||
g_unitTextureSyncListContextGeneration == g_textureContextGeneration &&
|
||||
g_unitTextureSyncListEpoch == unitBindingsEpoch &&
|
||||
g_unitTextureSyncListSamplingGeneration == samplingGeneration) {
|
||||
g_unitTextureSyncListSamplingGeneration == samplingGeneration &&
|
||||
PairingsIntact(g_unitTextureSyncList)) {
|
||||
for (const auto& entry : g_unitTextureSyncList) {
|
||||
// Aggregate gate == the conjunction of the three callees' own
|
||||
// early-outs (see IsDrawSyncClean); skipping on true is
|
||||
@@ -1219,8 +1239,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// An image-less default texture (name 0) is the slot's initial / "unbound"
|
||||
// state; it has nothing to sync, so skip it as cheaply as the old null slot.
|
||||
if (textureObject && !MG_State::GLState::IsUndefinedDefaultTexture(textureObject.get())) {
|
||||
g_unitTextureSyncList.push_back(
|
||||
{&textureObject, SyncTextureObjectToBackend(textureObject).get()});
|
||||
g_unitTextureSyncList.push_back({&textureObject, textureObject.get(),
|
||||
SyncTextureObjectToBackend(textureObject).get()});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1254,7 +1274,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_fboTextureSyncListSlotVersion == fboSlotVersion &&
|
||||
g_fboTextureSyncListObjectVersion == fboObjectVersion &&
|
||||
g_fboTextureSyncListContextId == keys.contextId &&
|
||||
g_fboTextureSyncListContextGeneration == g_textureContextGeneration;
|
||||
g_fboTextureSyncListContextGeneration == g_textureContextGeneration &&
|
||||
PairingsIntact(g_fboTextureSyncList);
|
||||
if (fboListValid) {
|
||||
for (const auto& entry : g_fboTextureSyncList) {
|
||||
// Same aggregate gate as the unit list above.
|
||||
@@ -1273,8 +1294,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!attachment.IsTexture()) continue;
|
||||
auto& textureObject = attachment.GetTexture();
|
||||
if (textureObject) {
|
||||
g_fboTextureSyncList.push_back(
|
||||
{&textureObject, SyncTextureObjectToBackend(textureObject).get()});
|
||||
g_fboTextureSyncList.push_back({&textureObject, textureObject.get(),
|
||||
SyncTextureObjectToBackend(textureObject).get()});
|
||||
}
|
||||
}
|
||||
g_fboTextureSyncListFbo = currentFBO.get();
|
||||
|
||||
@@ -630,6 +630,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||
Bool imageBindableStorageRequired = false);
|
||||
// Brings every texture the next draw reads - the touched units' bindings and the draw
|
||||
// FBO's texture attachments - onto the backend, through the two borrowed-pair memos
|
||||
// documented at their definitions. Declared here so tests can drive those memos directly.
|
||||
void SyncNeccessaryTextures();
|
||||
extern Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||
g_boundTexturesCache;
|
||||
|
||||
Reference in New Issue
Block a user