mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_Backend] (Disaggregated): preserve server-owned texture contents during GLES image promotion
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user