From 48a306025d1d37ffa3263ecb8f7156607c31bf45 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 29 Sep 2026 06:30:54 -0400 Subject: [PATCH] [Fix] (Espryt, P11 M): the push builds' monolith re-mint merges the applier's pending boxes over the GPU readback - the client clears its dirty flags at emission there, so a pending glTexSubImage was read back over and lost --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 91 ++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 3f3d37d7..1643cba1 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -6463,10 +6463,23 @@ namespace MobileGL::MG_Backend::DirectGLES { // every byte of the level, the level is stored compressed, the driver has no image at this // level and extent (defined after the last sync), or the readback cannot be served (a // multisample image, a format the driver will not attach to a framebuffer). + // + // P11 M, `pipePending` (push builds): THE CLIENT WRITES THE DRIVER HAS NOT BEEN SENT MAY + // LIVE IN THE APPLIER'S PENDING SET INSTEAD. On the texture handle arm the client clears its + // own dirty flags when it emits a level (D-D5), so the frontend model below answers "clean" + // for a write that is still only in the shadow; the entry the applier accumulated for this + // (uploadTarget, level) names the same shadow-grid boxes. Both are merged over the + // snapshot, and an entry whose union box covers the level owns every byte of it, exactly as + // a covering dirty region does. Null on the pull build's arm and the legacy arm. static Bool AdoptDriverLevelIntoShadow(GLuint backendTextureId, BackendTextureObject& backendTexture, const MG_State::GLState::ITextureObject& stateTextureObject, MG_State::GLState::TextureObjectMipmap& mipmapObject, - TextureUploadTarget uploadTarget, Uint level) { + TextureUploadTarget uploadTarget, Uint level +#if MOBILEGL_PIPE_PUSH + , + const MG_Pipe::MGPipeResourceRecord::PendingUpload* pipePending +#endif + ) { const IntVec3 texelSize = mipmapObject.GetMipmapTexelSize(uploadTarget, level); const IntVec3 extent{texelSize.x(), texelSize.y(), std::max(texelSize.z(), 1)}; const Bool dirty = mipmapObject.IsStorageDirty(uploadTarget, level); @@ -6474,6 +6487,36 @@ namespace MobileGL::MG_Backend::DirectGLES { dirty ? mipmapObject.GetStorageDirtyRegion(uploadTarget, level) : MG_State::GLState::MipmapDirtyRegion{}; if (dirty && dirtyRegion.CoversWholeLevel(extent)) return false; +#if MOBILEGL_PIPE_PUSH + // Every box the entry names, checked against the level before anything is read: a box + // outside it is a corrupt record, and the answer is the pre-readback one (keep the + // shadow) rather than a copy out of bounds. + const auto pipeBoxInLevel = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) { + return x >= 0 && y >= 0 && z >= 0 && Uint64(x) + w <= Uint64(extent.x()) && + Uint64(y) + h <= Uint64(extent.y()) && Uint64(z) + d <= Uint64(extent.z()); + }; + if (pipePending != nullptr) { + const auto& box = pipePending->UnionBox; + if (!pipeBoxInLevel(box.X, box.Y, box.Z, box.W, box.H, box.D)) { + MGLOG_E_ONCE("MGPipe: texture %u level %u: a pending upload's box lies outside the level; the " + "image-bindable re-mint replays the shadow as it stands", + stateTextureObject.GetExternalIndex(), level); + return false; + } + for (const auto& region : pipePending->Regions) { + if (!pipeBoxInLevel(region.X, region.Y, region.Z, region.W, region.H, region.D)) { + MGLOG_E_ONCE("MGPipe: texture %u level %u: a pending upload's region lies outside the " + "level; the image-bindable re-mint replays the shadow as it stands", + stateTextureObject.GetExternalIndex(), level); + return false; + } + } + if (box.X == 0 && box.Y == 0 && box.Z == 0 && box.W == static_cast(extent.x()) && + box.H == static_cast(extent.y()) && box.D == static_cast(extent.z())) { + return false; + } + } +#endif if (mipmapObject.GetMipmapCompressedFormat(uploadTarget, level) != GL_NONE) return false; if (!g_GLESFuncs.glGetTexLevelParameteriv) return false; @@ -6536,6 +6579,33 @@ namespace MobileGL::MG_Backend::DirectGLES { } } } +#if MOBILEGL_PIPE_PUSH + if (pipePending != nullptr) { + // The same merge for the applier's entry: its regions when it carries a list, its + // union box when it does not (RegionCount 0 = "the box is the whole story", D-D3). + const SizeT bpp = shadowBytes / texelCount; + const SizeT rowBytes = static_cast(extent.x()) * bpp; + const SizeT sliceBytes = static_cast(extent.y()) * rowBytes; + const auto mergePipeBox = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) { + for (Uint32 slice = 0; slice < d; ++slice) { + for (Uint32 row = 0; row < h; ++row) { + const SizeT offset = static_cast(z + static_cast(slice)) * sliceBytes + + static_cast(y + static_cast(row)) * rowBytes + + static_cast(x) * bpp; + Memcpy(snapshot.data() + offset, shadow + offset, static_cast(w) * bpp); + } + } + }; + if (pipePending->Regions.empty()) { + const auto& box = pipePending->UnionBox; + mergePipeBox(box.X, box.Y, box.Z, box.W, box.H, box.D); + } else { + for (const auto& region : pipePending->Regions) { + mergePipeBox(region.X, region.Y, region.Z, region.W, region.H, region.D); + } + } + } +#endif Memcpy(shadow, snapshot.data(), shadowBytes); return true; } @@ -6646,9 +6716,26 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level); if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0) continue; if (mipmapObject->GetMipmapByteSize(uploadTarget, level) == 0) continue; +#if MOBILEGL_PIPE_PUSH + // This level's entry in the applier's pending set, read BEFORE the re-arm + // below turns it into a whole-level box: the client writes it still owes + // (see AdoptDriverLevelIntoShadow). Resolved per level - the re-arm may grow + // the record's set and move what an earlier pointer named. + const MG_Pipe::MGPipeResourceRecord* rearmRecord = + MG_Pipe::MGPipeHandleIsNull(rearmRes) ? nullptr : PipeTextureRecordForHandle(rearmRes); + const MG_Pipe::MGPipeResourceRecord::PendingUpload* rearmPending = + rearmRecord != nullptr ? FindPipeTextureUpload(*rearmRecord, static_cast(uploadTarget), + static_cast(level)) + : nullptr; +#endif if (hadBackendStorage && !AdoptDriverLevelIntoShadow(m_backendTextureId, *this, *stateTextureObject, - *mipmapObject, uploadTarget, level)) { + *mipmapObject, uploadTarget, level +#if MOBILEGL_PIPE_PUSH + , + rearmPending +#endif + )) { MGLOG_D("Texture %u level %u: re-minting from the shadow, the driver's copy is not " "readable here", stateTextureObject->GetExternalIndex(), level);