From c1bf7e15c3fb491fa156dcbfc8368ef83036bdd1 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 20 Sep 2026 11:11:50 -0400 Subject: [PATCH] [MG_Backend] (Disaggregated): preserve server-owned texture contents during GLES image promotion --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 4 + MobileGL/MG_Backend/DirectGLES/DirectGLES.h | 9 ++ MobileGL/MG_Backend/DirectGLES/Managers.cpp | 113 +++++++++++--- MobileGL/MG_Backend/DirectGLES/Managers.h | 19 +-- .../DirectGLES/WireTextureReadback.inc | 147 ++++++++++++++++++ 5 files changed, 256 insertions(+), 36 deletions(-) create mode 100644 MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index f9feef4e..8f234d76 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -14485,6 +14485,10 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } +#if MOBILEGL_BUILD_DISAGGREGATED +#include "WireTextureReadback.inc" +#endif + static Bool IsLegacyNativeReadPixelsFormat(GLenum format) { return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER || format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 63a48bc4..b4e00fbb 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -23,6 +23,15 @@ namespace MobileGL::MG_Backend::DirectGLES { // Content uploads use scratch bindings, so draws and dispatches call this after // texture synchronization. void BindCurrentTextures(); +#if MOBILEGL_BUILD_DISAGGREGATED + // Native/server-only color level readback. Returns tightly packed, owned + // bytes in the requested pair; never consults a frontend texture or PACK/PBO. + // The caller resolves identity/extent from its resource record and supplies + // whether the SOURCE allocation already uses an image carrier. + Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget, + TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent, + Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector& bytes); +#endif void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil); void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value); void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index a80e8d86..61d336dc 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -5988,42 +5988,111 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } #if MOBILEGL_BUILD_DISAGGREGATED - // THE REFUSAL, AT THE ENTRY. The predicate is the SAME one the frontend overload - // hoisted to ahead of its first guarded contact - "would any level actually be - // replayed", answered from the server's own staged-texture store - so this changes - // WHERE the abort happens and not WHETHER it happens. What it buys is that the - // reason can still be stated: three frames deeper it was a MarkStorageDirty guard - // naming "texture-legacy-arm", which says nothing about image bindability. - // - // A texture with nothing defined yet is NOT refused: it is allocated image-bindable - // up front and pulls nothing, which is exactly what ImageBindableHint exists to make - // the common case. if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto format = static_cast(record.Desc.InternalFormat); + // ES image binding needs immutable storage, but an already + // immutable core-format allocation needs no replacement. Keep + // its GPU contents and native name; normal serial/shape checks + // below still consume pending uploads and real redefinitions. + if (m_isInitialized && m_backendStorageImmutable && !GetImageBindableStorageWidening(format)) { + m_imageBindableStorageRequired = true; + m_forceTextureParamsResync = true; + return; + } auto& store = MG_Remote::Server::ServerStagedTexture(); const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res); - Bool anyLevelWouldReplay = false; + const auto target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target); + GLenum canonicalFormat = GL_NONE, canonicalType = GL_NONE; + MG_Util::TextureFormatProcessor::NormalizePixelFormat( + MG_Util::ConvertTextureInternalFormatToGLEnum(format), {}, nullptr, + &canonicalFormat, &canonicalType); for (const auto& uploadTarget : BufferImpl::StagedUploadTargetsForPipeTarget(record.Desc.Target)) { for (Uint32 level = 0; level < record.Desc.Levels; ++level) { - if (store.IsLevelDefined(key, static_cast(uploadTarget), - static_cast(level))) { - anyLevelWouldReplay = true; - break; + const auto targetCode = static_cast(uploadTarget); + const auto levelCode = static_cast(level); + const IntVec3 extent = store.LevelExtentOrUndefined(key, targetCode, levelCode); + if (extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) continue; + const auto* pending = FindPipeTextureUpload(record, targetCode, levelCode); + const Bool covered = store.IsCovered(key, targetCode, levelCode); + const MG_Pipe::MGPBox whole{0, 0, 0, static_cast(extent.x()), + static_cast(extent.y()), static_cast(extent.z())}; + // A genuinely full pending write owns every byte. A + // union box covering the level is not sufficient when + // its explicit rectangles leave GPU-written gaps. + const auto coversWhole = [&](const auto& box) { + return box.X == 0 && box.Y == 0 && box.Z == 0 && + box.W == whole.W && box.H == whole.H && box.D == whole.D; + }; + const Bool pendingWhole = pending && (pending->Regions.empty() + ? coversWhole(pending->UnionBox) + : std::any_of(pending->Regions.begin(), pending->Regions.end(), coversWhole)); + if (m_isInitialized && !(covered && pendingWhole)) { + Vector snapshot; + if (!ReadTextureLevelTight(m_backendTextureId, target, uploadTarget, format, + static_cast(level), extent, /*sourceUsesImageCarrier=*/false, + canonicalFormat, canonicalType, snapshot)) { + MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-readback\"} {%u,%u} level=%u", + res.Slot, res.Gen, level); + std::abort(); + } + if (pending) { + // Native contents include prior draws/image + // writes; the pending stage owns ONLY its boxes. + // Merge those boxes before adopting a whole-level + // snapshot, never overwrite untouched GPU texels + // with the rest of a stale client staging run. + const SizeT pixels = static_cast(extent.x()) * extent.y() * extent.z(); + const SizeT stagedSize = store.LevelByteSize(key, targetCode, levelCode); + if (!covered || !pixels || snapshot.size() != stagedSize || stagedSize % pixels) { + MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"image-promotion-pending\"}"); + std::abort(); + } + const SizeT bpp = stagedSize / pixels; + const Uint8* staged = store.RequireLevelBytes(key, targetCode, levelCode, "image-promotion"); + const auto merge = [&](Int32 x, Int32 y, Int32 z, Uint32 w, Uint32 h, Uint32 d) { + if (x < 0 || y < 0 || z < 0 || Uint64(x) + w > whole.W || + Uint64(y) + h > whole.H || Uint64(z) + d > whole.D) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"image-promotion-region\"}"); + std::abort(); + } + for (Uint32 slice = 0; slice < d; ++slice) { + for (Uint32 row = 0; row < h; ++row) { + const SizeT offset = ((static_cast(z + slice) * whole.H + y + row) * whole.W + x) * bpp; + Memcpy(snapshot.data() + offset, staged + offset, static_cast(w) * bpp); + } + } + }; + if (pending->Regions.empty()) { + const auto& box = pending->UnionBox; + merge(box.X, box.Y, box.Z, box.W, box.H, box.D); + } else { + for (const auto& box : pending->Regions) + merge(box.X, box.Y, box.Z, box.W, box.H, box.D); + } + } + store.Adopt(key, targetCode, levelCode, extent, snapshot.data(), snapshot.size()); + } + // Undefined contents have no byte run to preserve. A + // first allocation can still be image-bindable without + // inventing an upload or consulting the frontend. + if (!store.IsCovered(key, targetCode, levelCode)) continue; + if (!RearmPipeTextureLevelUpload(res, + MG_Pipe::MGPipePackSubDataTarget(record.Desc.Target, static_cast(uploadTarget)), + levelCode, whole)) { + MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-pending-capacity\"}"); + std::abort(); } } - if (anyLevelWouldReplay) break; - } - if (anyLevelWouldReplay) { - MG_Pipe::MGPipeUnmigratedEmulation("image-bindable-redirty"); } } #else (void)res; (void)record; #endif - // Nothing to replay, so the transition is just the two sticky flags plus the - // parameter resync the widening's swizzle needs - the whole of the frontend - // overload's first half, with its second half refused above. + // Every defined byte now belongs to the server and every required + // replay is in its pending set. The existing upload path builds and + // fills the immutable/widened carrier from those records. m_imageBindableStorageRequired = true; m_isInitialized = false; // The widened carrier's swizzle override, exactly as the frontend overload's tail diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 92ba2884..0214442e 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -1852,20 +1852,11 @@ namespace MobileGL::MG_Backend::DirectGLES { void RequireImageBindableStorage( const SharedPtr& stateTextureObject); #if MOBILEGL_PIPE_PUSH - // P5e (tx2), CONTRACT-P5E ยง5.2 / G-S2-5. THE SAME TRANSITION WITH NO FRONTEND - // ARGUMENT, and with the re-dirty NAMED AT THE ENTRY instead of three frames deep. - // - // The frontend overload's whole second half is the re-dirty: it walks the CLIENT's - // level shadows to re-arm every level the widened carrier owes. A server has no - // client address space to walk, so under a transport that half is not "not migrated - // yet", it is not expressible - ImageBindableHint is the prevention (a texture that - // has ever been image-bound is allocated in the carrier from the start) and P9 owns - // the pull. The refusal is raised here, at the entry, where the reason can still be - // stated, rather than at the MarkStorageDirty guard the loop would reach. - // - // A texture arriving here with NO backend storage yet pulls nothing - it is simply - // allocated image-bindable up front - so that case is not refused, it is the whole - // point of the hint. + // Server-owned promotion: preserve an already image-bindable native + // allocation, or capture the old native contents, merge pending + // upload regions and re-arm the server's whole-level replay set. + // First allocations use staged bytes directly. No frontend dirty + // flag, allocator or pixel-store object participates. void RequireImageBindableStorageByHandle(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record); #endif diff --git a/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc b/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc new file mode 100644 index 00000000..c12be206 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/WireTextureReadback.inc @@ -0,0 +1,147 @@ +// Included in DirectGLES.cpp after the native packed-word readback helper. +// The server needs owned tight rows for a storage promotion, not client PACK +// layout or a fallback into a frontend texture's potentially stale shadow. + static Bool ReadWireColorRowsTight(GLsizei width, GLsizei height, GLenum format, GLenum type, + Bool forceOpaqueAlpha, const NormalizedImageCarrierRead& carrier, + Vector& bytes) { + ReadbackChannelMapping mapping{}; + if (!GetReadbackChannelMapping(format, mapping)) return false; + const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type); + if (!pixelBytes || width <= 0 || height <= 0) return false; + const GLenum componentType = QueryReadAttachmentComponentType(); + const Bool integerAttachment = componentType == GL_INT || componentType == GL_UNSIGNED_INT; + if (carrier.Active() ? (mapping.isInteger || !integerAttachment) + : mapping.isInteger != integerAttachment) return false; + + GLint implFormat = 0, implType = 0; + g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat); + g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType); + struct Candidate { GLenum format, type; }; + Vector candidates; + if (carrier.Active()) { + candidates.push_back({GL_RGBA_INTEGER, GL_UNSIGNED_INT}); + } else if (mapping.isInteger) { + if (GetWideReadChannelCount(implFormat) > 0 && IsIntegerReadFormat(implFormat) && + (implType == GL_INT || implType == GL_UNSIGNED_INT)) + candidates.push_back({static_cast(implFormat), static_cast(implType)}); + candidates.push_back({GL_RGBA_INTEGER, componentType == GL_INT ? GLenum(GL_INT) : GLenum(GL_UNSIGNED_INT)}); + } else { + if (GetWideReadChannelCount(implFormat) > 0 && !IsIntegerReadFormat(implFormat) && + (CanDecodeWideSourceType(implType) || + (implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV))) + candidates.push_back({static_cast(implFormat), static_cast(implType)}); + if (componentType == GL_FLOAT) candidates.push_back({GL_RGBA, GL_FLOAT}); + if (componentType == GL_SIGNED_NORMALIZED) { + candidates.push_back({GL_RGBA, GL_SHORT}); + candidates.push_back({GL_RGBA, GL_BYTE}); + } else candidates.push_back({GL_RGBA, GL_UNSIGNED_BYTE}); + } + ScopedPixelPackBuffer packBuffer(0); + ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0}); + const SizeT pixels = static_cast(width) * static_cast(height); + Vector wide; + GLenum wideType = GL_NONE; + Int channels = 0; + DrainESErrors(); + for (const auto candidate : candidates) { + channels = GetWideReadChannelCount(candidate.format); + wide.resize(pixels * static_cast(channels) * GetReadbackComponentSize(candidate.type)); + g_GLESFuncs.glReadPixels(0, 0, width, height, candidate.format, candidate.type, wide.data()); + if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { wideType = candidate.type; break; } + } + if (wideType == GL_NONE) return false; + + if (carrier.Active()) { + Vector decoded(pixels * 4 * sizeof(Float)); + auto* dst = reinterpret_cast(decoded.data()); + const auto* src = reinterpret_cast(wide.data()); + for (SizeT i = 0; i < pixels; ++i) { + for (SizeT c = 0; c < 4; ++c) { + const Float divisor = static_cast(carrier.ChannelMax[c]); + dst[i * 4 + c] = carrier.SignedNormalized + ? std::max(static_cast(static_cast(static_cast(src[i * 4 + c]))) / divisor, -1.0f) + : static_cast(src[i * 4 + c]) / divisor; + } + } + wide = Move(decoded); wideType = GL_FLOAT; channels = 4; + } else if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) { + Vector decoded(pixels * 4 * sizeof(Float)); + auto* dst = reinterpret_cast(decoded.data()); + for (SizeT i = 0; i < pixels; ++i) { + Uint32 word = 0; + Memcpy(&word, wide.data() + i * sizeof(word), sizeof(word)); + dst[i * 4] = static_cast(word & 1023u) / 1023.0f; + dst[i * 4 + 1] = static_cast((word >> 10) & 1023u) / 1023.0f; + dst[i * 4 + 2] = static_cast((word >> 20) & 1023u) / 1023.0f; + dst[i * 4 + 3] = static_cast((word >> 30) & 3u) / 3.0f; + } + wide = Move(decoded); wideType = GL_FLOAT; channels = 4; + } + if (channels < 4) ExpandNarrowWideRead(wide, pixels, channels, wideType); + if (forceOpaqueAlpha) ForceWideReadAlphaToOne(wide, pixels, wideType); + Vector converted(pixels * pixelBytes); + const SizeT sourceRowBytes = static_cast(width) * 4 * GetReadbackComponentSize(wideType); + for (GLsizei row = 0; row < height; ++row) + ReadbackImpl::ConvertWideReadbackRow(wide.data() + static_cast(row) * sourceRowBytes, + converted.data() + static_cast(row) * width * pixelBytes, width, wideType, mapping, type); + bytes = Move(converted); + return true; + } + + Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget, + TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent, + Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector& bytes) { + if (!texture || level < 0 || TextureImpl::IsMultisampleTextureTarget(target)) return false; + ReadbackChannelMapping mapping{}; + if (!GetReadbackChannelMapping(format, mapping)) return false; + const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type); + const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent); + if (!pixelBytes || extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) return false; + const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); + const GLenum faceTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); + const auto widening = sourceUsesImageCarrier + ? TextureImpl::GetImageBindableStorageWidening(logicalFormat) : TextureImpl::ImageBindableStorageWidening{}; + const SizeT sliceBytes = static_cast(extent.x()) * extent.y() * pixelBytes; + Vector result(sliceBytes * static_cast(extent.z())); + if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS); + g_GLESFuncs.glFinish(); + + // A raw packed word may have redundant encodings or NaN payloads. Read + // it through R32UI when the source really still uses that native layout. + const Bool rawPacked = !widening && MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer( + logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format), + MG_Util::ConvertGLEnumToTexturePixelDataType(type)); + if (rawPacked && pixelBytes == sizeof(Uint32)) { + const GLint face = uploadTarget >= TextureUploadTarget::CubeMapPositiveX && + uploadTarget <= TextureUploadTarget::CubeMapNegativeZ + ? static_cast(uploadTarget) - static_cast(TextureUploadTarget::CubeMapPositiveX) : 0; + for (GLint layer = 0; layer < extent.z(); ++layer) + if (!ReadPackedLevelWordsViaScratch(texture, nativeTarget, level, face + layer, extent.x(), extent.y(), + reinterpret_cast(result.data() + static_cast(layer) * sliceBytes))) return false; + bytes = Move(result); + return true; + } + + NormalizedImageCarrierRead carrier{}; + if (widening && widening.CarriesNormalizedCodes()) { + for (SizeT c = 0; c < 4; ++c) carrier.ChannelMax[c] = widening.ChannelMax[c]; + carrier.SignedNormalized = widening.SignedNormalized; + } + TempFBOBinder binder(true); + auto& framebuffer = binder.Framebuffer(); + for (GLint layer = 0; layer < extent.z(); ++layer) { + if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY || nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY) + ScratchFBOImpl::EnsureColorAttachmentLayer(framebuffer, GL_READ_FRAMEBUFFER, texture, level, layer); + else + ScratchFBOImpl::EnsureColorAttachment2D(framebuffer, GL_READ_FRAMEBUFFER, texture, faceTarget, level); + ScratchFBOImpl::EnsureReadBuffer(framebuffer, GL_COLOR_ATTACHMENT0); + if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false; + Vector slice; + if (!ReadWireColorRowsTight(extent.x(), extent.y(), format, type, + TextureImpl::BackendTextureFormatAddsAlpha(logicalFormat, target), carrier, slice) || + slice.size() != sliceBytes) return false; + Memcpy(result.data() + static_cast(layer) * sliceBytes, slice.data(), sliceBytes); + } + bytes = Move(result); + return true; + }