[MG_Backend] (Disaggregated): preserve server-owned texture contents during GLES image promotion

This commit is contained in:
2026-09-20 11:11:50 -04:00
parent bb3a51995c
commit c1bf7e15c3
5 changed files with 256 additions and 36 deletions
@@ -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;
@@ -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<Uint8>& 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);
+91 -22
View File
@@ -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<TextureInternalFormat>(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<Uint16>(uploadTarget),
static_cast<Uint16>(level))) {
anyLevelWouldReplay = true;
break;
const auto targetCode = static_cast<Uint16>(uploadTarget);
const auto levelCode = static_cast<Uint16>(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<Uint32>(extent.x()),
static_cast<Uint32>(extent.y()), static_cast<Uint32>(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<Uint8> snapshot;
if (!ReadTextureLevelTight(m_backendTextureId, target, uploadTarget, format,
static_cast<GLint>(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<SizeT>(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<SizeT>(z + slice) * whole.H + y + row) * whole.W + x) * bpp;
Memcpy(snapshot.data() + offset, staged + offset, static_cast<SizeT>(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<Uint32>(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
+5 -14
View File
@@ -1852,20 +1852,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
void RequireImageBindableStorage(
const SharedPtr<MG_State::GLState::ITextureObject>& 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
@@ -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<Uint8>& 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<Candidate> 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<GLenum>(implFormat), static_cast<GLenum>(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<GLenum>(implFormat), static_cast<GLenum>(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<SizeT>(width) * static_cast<SizeT>(height);
Vector<Uint8> wide;
GLenum wideType = GL_NONE;
Int channels = 0;
DrainESErrors();
for (const auto candidate : candidates) {
channels = GetWideReadChannelCount(candidate.format);
wide.resize(pixels * static_cast<SizeT>(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<Uint8> decoded(pixels * 4 * sizeof(Float));
auto* dst = reinterpret_cast<Float*>(decoded.data());
const auto* src = reinterpret_cast<const Uint32*>(wide.data());
for (SizeT i = 0; i < pixels; ++i) {
for (SizeT c = 0; c < 4; ++c) {
const Float divisor = static_cast<Float>(carrier.ChannelMax[c]);
dst[i * 4 + c] = carrier.SignedNormalized
? std::max(static_cast<Float>(static_cast<Int16>(static_cast<Uint16>(src[i * 4 + c]))) / divisor, -1.0f)
: static_cast<Float>(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<Uint8> decoded(pixels * 4 * sizeof(Float));
auto* dst = reinterpret_cast<Float*>(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<Float>(word & 1023u) / 1023.0f;
dst[i * 4 + 1] = static_cast<Float>((word >> 10) & 1023u) / 1023.0f;
dst[i * 4 + 2] = static_cast<Float>((word >> 20) & 1023u) / 1023.0f;
dst[i * 4 + 3] = static_cast<Float>((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<Uint8> converted(pixels * pixelBytes);
const SizeT sourceRowBytes = static_cast<SizeT>(width) * 4 * GetReadbackComponentSize(wideType);
for (GLsizei row = 0; row < height; ++row)
ReadbackImpl::ConvertWideReadbackRow(wide.data() + static_cast<SizeT>(row) * sourceRowBytes,
converted.data() + static_cast<SizeT>(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<Uint8>& 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<SizeT>(extent.x()) * extent.y() * pixelBytes;
Vector<Uint8> result(sliceBytes * static_cast<SizeT>(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<GLint>(uploadTarget) - static_cast<GLint>(TextureUploadTarget::CubeMapPositiveX) : 0;
for (GLint layer = 0; layer < extent.z(); ++layer)
if (!ReadPackedLevelWordsViaScratch(texture, nativeTarget, level, face + layer, extent.x(), extent.y(),
reinterpret_cast<Uint32*>(result.data() + static_cast<SizeT>(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<Uint8> 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<SizeT>(layer) * sliceBytes, slice.data(), sliceBytes);
}
bytes = Move(result);
return true;
}