[MG_Remote, MG_Backend] (Disaggregated): P5c tx - the server's texture staged shadow

StagedTextureStore (MG_Remote/Server/StagedTextureStore.h), the texture twin of R-11's
StagedShadowStore: keyed by the wire handle (a twin-address key would force glGenTextures
at adopt time), coverage = the whole staged run per (uploadTarget, level), defined-ness
tracked from the respecify hook so sparse chains keep their holes. ApplyTextureUpload
adopts the staged bytes at the last instant they are alive (rule C) through three new
disaggregated-only MGPipeResourceOps members; SyncMipmapsToBackend's four arms read the
store and the descriptor instead of the client's MipmapStorage (texels, extent, target,
defined-ness, dirty); Magma's GenerateMipmap marks the server shadow Defined+GpuDirty
instead of writing the client's level storage (T5). Monolith arms reproduce the pre-tx
expressions character for character behind the same macro discipline as P4a.

Evidence: unit 2147/2147 incl. two named-Fatal death tests and the copies/no-copies
difference suite; DirectGLES.Split 104/104 on WSL GPU; red-once - reverting the adoption
to pointer-dropping turns exactly StagedTextureProductionTest red; the two monolith lane
failures reproduce on the base commit (DirectVulkan.IterationRPProgram203Scenario,
DirectVulkan.F1WireScenario.GenerateMipmapPackedFloatPixels). CONTRACT-P5C section 2.
This commit is contained in:
2026-09-17 03:55:13 -04:00
parent 851bc70c1d
commit 4b54a6f902
8 changed files with 1219 additions and 39 deletions
@@ -21,6 +21,10 @@
// P3a: the applier's vertex-input records the re-keyed draw-buffer memo is validated against.
#include <MG_Pipe/PipeApply.h>
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): §1's server-side per-level extent derivation, for GenerateMipmap's shape reads.
#include <MG_Remote/Server/StagedTextureStore.h>
#endif
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
@@ -8494,6 +8498,35 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
MOBILEGL_ASSERT(mipmapTexture != nullptr, "Depth mipmap generation requires mipmap storage.");
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): the per-level extents are §1's derivation from the descriptor under an
// active transport - GetMipmapTexelSize is the client's per-level shape and the apply
// thread may not name it (rule E). Texture2D only, so x and y shrink and z stays 1.
// The record resolution is the same registry lookup texture sync and
// EnsureGenerateMipmapStorageAllocated's disaggregated arm already make.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto pushedHandle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get());
const auto* pushedRecord = PipeTextureRecordForHandle(pushedHandle);
if (pushedRecord == nullptr || pushedRecord->Desc.Width == 0 || pushedRecord->Desc.Levels == 0) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-shape");
}
const auto& desc = pushedRecord->Desc;
const GLuint textureId = backendTexture->GetBackendTextureId();
for (Uint32 level = 1; level < desc.Levels; ++level) {
const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(
desc.Target, desc.Width, desc.Height, desc.Depth, level - 1);
const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(
desc.Target, desc.Width, desc.Height, desc.Depth, level);
BlitDepthTexture2D(textureId, static_cast<GLint>(level - 1), 0, 0,
static_cast<GLsizei>(srcSize.x()), static_cast<GLsizei>(srcSize.y()),
textureId, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(dstSize.x()), static_cast<GLsizei>(dstSize.y()));
}
return;
}
#endif
const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount();
MOBILEGL_ASSERT(mipLevelCount > 0, "Depth mipmap generation requires allocated storage.");
@@ -8519,6 +8552,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
MOBILEGL_ASSERT(mipmapTexture != nullptr, "Color mipmap generation requires mipmap storage.");
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): GenerateDepthTexture2DMipmap's descriptor arm, for the color filter path -
// same §1 extent derivation, same registry resolution, same refusal when the handle
// arm has no record to read.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto pushedHandle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get());
const auto* pushedRecord = PipeTextureRecordForHandle(pushedHandle);
if (pushedRecord == nullptr || pushedRecord->Desc.Width == 0 || pushedRecord->Desc.Levels == 0) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-shape");
}
const auto& desc = pushedRecord->Desc;
const GLenum filter =
IsIntegerColorFormat(static_cast<TextureInternalFormat>(desc.InternalFormat)) ? GL_NEAREST
: GL_LINEAR;
const GLuint textureId = backendTexture->GetBackendTextureId();
for (Uint32 level = 1; level < desc.Levels; ++level) {
const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(
desc.Target, desc.Width, desc.Height, desc.Depth, level - 1);
const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(
desc.Target, desc.Width, desc.Height, desc.Depth, level);
BlitColorTexture2D(textureId, static_cast<GLint>(level - 1), 0, 0,
static_cast<GLsizei>(srcSize.x()), static_cast<GLsizei>(srcSize.y()),
textureId, static_cast<GLint>(level), 0, 0,
static_cast<GLsizei>(dstSize.x()), static_cast<GLsizei>(dstSize.y()), filter);
}
return;
}
#endif
const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount();
MOBILEGL_ASSERT(mipLevelCount > 0, "Color mipmap generation requires allocated storage.");
+288 -26
View File
@@ -20,6 +20,7 @@
// R-11's server-owned staging copy. Header-only and package v1's; see its own header block for
// why GLESBufferResource does not simply gain a member.
#include <MG_Remote/Server/StagedShadow.h>
#include <MG_Remote/Server/StagedTextureStore.h>
#include <MG_Remote/Server/ServerLoop.h>
#endif
@@ -2508,6 +2509,139 @@ namespace MobileGL::MG_Backend::DirectGLES {
return result;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- P5c (tx): the TEXTURE half of the resource family, staged server-side --------
//
// The buffer half's R-11 pattern (ServerStaged() above), for texture levels: the
// staged bytes of resource_subdata's texture half are adopted into the server's
// StagedTextureStore AT APPLY TIME, because `bytes` names SEG_STAGE and is dead the
// moment the record retires (rule C). The store, its ownership and its coverage
// rules are documented in MG_Remote/Server/StagedTextureStore.h; these are only the
// three hook bodies. All three are no-ops in monolith (CopiesIntoServerStorage()
// false), which is what keeps the monolith expression character for character.
//
// NOTHING here mints or reads a texture twin: the store is keyed by the wire handle
// the record carried (StagedTextureStore.h explains why not the twin address), so
// adoption needs no GL call and no frontend object.
// GetUploadTargets() answered from the descriptor's Target: one static list per
// target, matching the frontend classes' own lists member for member
// (TextureObject*.h / TextureObjectStubs.h: a cube is six faces, a cube array is
// the single CubeMapArray target, everything else its own one).
const Vector<TextureUploadTarget>& StagedUploadTargetsForPipeTarget(Uint8 pipeResourceTarget) {
static const Vector<TextureUploadTarget> kUnknown{};
static const Vector<TextureUploadTarget> kTex1D{TextureUploadTarget::Texture1D};
static const Vector<TextureUploadTarget> kTex2D{TextureUploadTarget::Texture2D};
static const Vector<TextureUploadTarget> kTex3D{TextureUploadTarget::Texture3D};
static const Vector<TextureUploadTarget> kTex1DArray{TextureUploadTarget::Texture1DArray};
static const Vector<TextureUploadTarget> kTex2DArray{TextureUploadTarget::Texture2DArray};
static const Vector<TextureUploadTarget> kTexCube{
TextureUploadTarget::CubeMapPositiveX, TextureUploadTarget::CubeMapNegativeX,
TextureUploadTarget::CubeMapPositiveY, TextureUploadTarget::CubeMapNegativeY,
TextureUploadTarget::CubeMapPositiveZ, TextureUploadTarget::CubeMapNegativeZ};
static const Vector<TextureUploadTarget> kTexCubeArray{TextureUploadTarget::CubeMapArray};
static const Vector<TextureUploadTarget> kTex2DMS{TextureUploadTarget::Texture2DMultisample};
static const Vector<TextureUploadTarget> kTex2DMSArray{TextureUploadTarget::Texture2DMultisampleArray};
static const Vector<TextureUploadTarget> kTexRect{TextureUploadTarget::TextureRectangle};
static const Vector<TextureUploadTarget> kTexBuffer{TextureUploadTarget::TextureBuffer};
switch (static_cast<MG_Pipe::MGPipeResourceTarget>(pipeResourceTarget)) {
case MG_Pipe::MGPipeResourceTarget::Tex1D: return kTex1D;
case MG_Pipe::MGPipeResourceTarget::Tex2D: return kTex2D;
case MG_Pipe::MGPipeResourceTarget::Tex3D: return kTex3D;
case MG_Pipe::MGPipeResourceTarget::Tex1DArray: return kTex1DArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DArray: return kTex2DArray;
case MG_Pipe::MGPipeResourceTarget::TexCube: return kTexCube;
case MG_Pipe::MGPipeResourceTarget::TexCubeArray: return kTexCubeArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DMS: return kTex2DMS;
case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return kTex2DMSArray;
case MG_Pipe::MGPipeResourceTarget::TexRect: return kTexRect;
case MG_Pipe::MGPipeResourceTarget::TexBuffer: return kTexBuffer;
default: return kUnknown;
}
}
// The inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the sync's
// target reads (ConvertTextureTargetToBackendGLEnum and MapToBackendTextureTarget
// both want the frontend enum).
TextureTarget StagedTextureTargetForPipeTarget(Uint8 pipeResourceTarget) {
switch (static_cast<MG_Pipe::MGPipeResourceTarget>(pipeResourceTarget)) {
case MG_Pipe::MGPipeResourceTarget::Tex1D: return TextureTarget::Texture1D;
case MG_Pipe::MGPipeResourceTarget::Tex2D: return TextureTarget::Texture2D;
case MG_Pipe::MGPipeResourceTarget::Tex3D: return TextureTarget::Texture3D;
case MG_Pipe::MGPipeResourceTarget::Tex1DArray: return TextureTarget::Texture1DArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DArray: return TextureTarget::Texture2DArray;
case MG_Pipe::MGPipeResourceTarget::TexCube: return TextureTarget::TextureCubeMap;
case MG_Pipe::MGPipeResourceTarget::TexCubeArray: return TextureTarget::TextureCubeMapArray;
case MG_Pipe::MGPipeResourceTarget::Tex2DMS: return TextureTarget::Texture2DMultisample;
case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return TextureTarget::Texture2DMultisampleArray;
case MG_Pipe::MGPipeResourceTarget::TexRect: return TextureTarget::TextureRectangle;
case MG_Pipe::MGPipeResourceTarget::TexBuffer: return TextureTarget::TextureBuffer;
default: return TextureTarget::Unknown;
}
}
void Ops_H_TextureSubData(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPSubData& record,
const void* bytes, const MG_Pipe::MGPSubRegion* regions) {
// The region set is the upload planner's shape and stays in the applier's
// pending set; the store's coverage is the staged run itself
// (StagedTextureStore.h's coverage ruling).
(void)regions;
auto& store = MG_Remote::Server::ServerStagedTexture();
if (!store.CopiesIntoServerStorage()) return;
// The applier's gate has already faulted every shape that reaches here without
// bytes, and under split the codec declared the run's length (Blob.Size) -
// TextureEmit.h:1285's "the bytes this record declares ARE the level shadow".
if (bytes == nullptr || record.Blob.Size == 0) return;
const auto* stored = PipeTextureRecordForHandle(res);
if (stored == nullptr) return;
const IntVec3 extent = MG_Remote::Server::StagedTextureMipExtent(
stored->Desc.Target, stored->Desc.Width, stored->Desc.Height, stored->Desc.Depth,
static_cast<Uint32>(record.Level));
store.Adopt(MG_Remote::Server::StagedTextureStore::KeyForHandle(res),
MG_Pipe::MGPipeSubDataUploadTargetOf(record.Target), record.Level, extent,
bytes, static_cast<SizeT>(record.Blob.Size));
}
void Ops_H_TextureRespecify(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
const MG_Pipe::MGPRespecifiedLevel* level) {
auto& store = MG_Remote::Server::ServerStagedTexture();
if (!store.CopiesIntoServerStorage()) return;
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
if (level != nullptr) {
// ONE glTexImage*D redefined one level: it exists from here on, at the
// derived extent (§1). NoteLevelDefined keeps a same-extent level's bytes.
store.NoteLevelDefined(
key, MG_Pipe::MGPipeSubDataUploadTargetOf(level->UploadTarget), level->Level,
MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, level->Level));
return;
}
// A whole-resource redefinition: every level's old coordinate system is gone.
// glTexStorage* then defines the WHOLE chain at once (GL 4.6 core 8.19 - all six
// cube faces included), so an immutable descriptor re-marks every level of every
// upload target; a mutable whole-resource respecify (a texture view) defines
// nothing here and the store simply forgets the old levels.
store.ResetLevels(key);
if (desc.Immutable == 0 || desc.Levels == 0) return;
for (const auto& uploadTarget : StagedUploadTargetsForPipeTarget(desc.Target)) {
for (Uint32 levelIndex = 0; levelIndex < desc.Levels; ++levelIndex) {
store.NoteLevelDefined(
key, static_cast<Uint16>(uploadTarget), static_cast<Uint16>(levelIndex),
MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, levelIndex));
}
}
}
void Ops_H_TextureDestroy(MG_Pipe::MGPipeHandle res) {
// Deliberately NOT gated on CopiesIntoServerStorage(): Drop's own m_any gate
// makes the monolith call one acquire load, and an unconditional drop cannot
// strand a key the latch state was misread for.
MG_Remote::Server::ServerStagedTexture().Drop(
MG_Remote::Server::StagedTextureStore::KeyForHandle(res));
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
const MG_Pipe::MGPipeResourceOps g_glesResourceOps = {
.Create = Ops_H_Create,
.Respecify = Ops_H_RespecifyTracked,
@@ -2518,6 +2652,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
.Destroy = Ops_H_DestroyTracked,
.MapPersistent = Ops_H_MapPersistentTracked,
.UnmapPersistent = Ops_H_UnmapPersistent,
#if MOBILEGL_BUILD_DISAGGREGATED
.TextureSubData = Ops_H_TextureSubData,
.TextureRespecify = Ops_H_TextureRespecify,
.TextureDestroy = Ops_H_TextureDestroy,
#endif
};
#endif // MOBILEGL_PIPE_PUSH
@@ -2812,6 +2951,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// generation, and a shadow that outlived its twin would be looked up by a RECYCLED
// address on the next allocation - which is the quietest possible wrong answer.
MGL_SERVER_STAGED_DROP_ALL();
#if MOBILEGL_BUILD_DISAGGREGATED
// tx's texture shadows die for the same reason, keyed by handle rather than address
// but with the same recycled-identity failure mode: a new context's allocator may
// hand out a {slot, gen} the old one's store still answers for.
MG_Remote::Server::ServerStagedTexture().DropAll();
#endif
#endif
}
@@ -6586,7 +6731,91 @@ namespace MobileGL::MG_Backend::DirectGLES {
// pre-P4a expression exactly when MOBILEGL_PIPE_PUSH is off, so the pull build's
// preprocessed text, and therefore its object code, is unchanged. Both are #undef'd
// immediately after the function.
#if MOBILEGL_PIPE_PUSH
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): THE READS THE FOUR UPLOAD ARMS MAKE, re-sourced. With an active transport the
// apply thread may not name the client's TextureObjectMipmap at all (rule E), so on the
// handle arm:
//
// * the level TEXELS come from the server's staged-texture store, adopted at apply time
// (MGB_LEVEL_TEXELS - Fatal{StageSnapshotTooNarrow} when no record covered the level,
// which is the data-correctness refusal of the texture half);
// * the per-level EXTENT and DEFINED-NESS come from the same store (fed by the sub-data
// adoption and the respecify hook; {0,0,0} for a level nothing defined, which is exactly
// GetMipmapTexelSize's answer for one);
// * the level BYTE SIZE is the adopted run's length;
// * the texture TARGET and the UPLOAD-TARGET list come from the descriptor.
//
// Every macro keeps the P4a discipline: the non-disaggregated expansion is the original
// frontend read, character for character modulo one pair of parentheses, so the pull and
// push builds compile exactly what they compiled before tx, and every disaggregated arm
// falls back to the frontend read when there is no active transport (the legacy arm, and
// monolith). MGB_STAGED_TEXTURE_LIVE is the runtime discriminator; pushedStorage/pushedRes
// are the function's own locals. All are #undef'd with the rest after the function.
#define MGB_STAGED_TEXTURE_LIVE \
(pushedStorage != nullptr && MG_Remote::Server::ServerStagedTexture().CopiesIntoServerStorage())
#define MGB_TEXTURE_TARGET(obj) \
(MGB_STAGED_TEXTURE_LIVE ? BufferImpl::StagedTextureTargetForPipeTarget(pushedStorage->Desc.Target) \
: (obj)->GetTarget())
#define MGB_UPLOAD_TARGETS(obj) \
(MGB_STAGED_TEXTURE_LIVE ? BufferImpl::StagedUploadTargetsForPipeTarget(pushedStorage->Desc.Target) \
: (obj)->GetUploadTargets())
#define MGB_LEVEL_TEXEL_SIZE(obj, tgt, lvl) \
(MGB_STAGED_TEXTURE_LIVE \
? MG_Remote::Server::ServerStagedTexture().LevelExtentOrUndefined( \
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl)) \
: (obj)->GetMipmapTexelSize(tgt, lvl))
#define MGB_LEVEL_BYTE_SIZE(obj, tgt, lvl) \
(MGB_STAGED_TEXTURE_LIVE \
? MG_Remote::Server::ServerStagedTexture().LevelByteSize( \
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl)) \
: (obj)->GetMipmapByteSize(tgt, lvl))
#define MGB_LEVEL_TEXELS(obj, tgt, lvl, site) \
(MGB_STAGED_TEXTURE_LIVE \
? MG_Remote::Server::ServerStagedTexture().RequireLevelBytes( \
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl), site) \
: (obj)->MapMipmapData(tgt, lvl))
#else
#define MGB_TEXTURE_TARGET(obj) ((obj)->GetTarget())
#define MGB_UPLOAD_TARGETS(obj) ((obj)->GetUploadTargets())
#define MGB_LEVEL_TEXEL_SIZE(obj, tgt, lvl) ((obj)->GetMipmapTexelSize(tgt, lvl))
#define MGB_LEVEL_BYTE_SIZE(obj, tgt, lvl) ((obj)->GetMipmapByteSize(tgt, lvl))
#define MGB_LEVEL_TEXELS(obj, tgt, lvl, site) ((obj)->MapMipmapData(tgt, lvl))
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): the disaggregated pair adds ONE term and ONE clear to the P4a shapes - the
// staged-texture store's GPU-dirty mark (T5: a level the GPU generated dirties the SERVER's
// shadow, and the pending set cannot see it). The mark is never set on Espryt today - its
// GenerateMipmap fills the levels on the driver - so the term is inert here and is the
// contract-shaped answer (§2.2's last row) rather than a hot-path cost: one m_any acquire
// load when the store is empty.
#define MGB_LEVEL_NEEDS_UPLOAD(obj, tgt, lvl) \
(pushedStorage != nullptr \
? (FindPipeTextureUpload(*pushedStorage, static_cast<Uint16>(tgt), static_cast<Uint16>(lvl)) != nullptr || \
MG_Remote::Server::ServerStagedTexture().IsLevelGpuDirty( \
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl))) \
: (obj)->IsStorageDirty(tgt, lvl))
// Re-resolves the record itself, so it is safe after any amount of driver work - and it
// invalidates any PendingUpload* taken earlier for THIS texture, which is why every such pointer
// is used and dropped inside one level's iteration.
#define MGB_LEVEL_UPLOAD_DONE(obj, tgt, lvl) \
do { \
if (pushedStorage != nullptr) { \
ConsumePipeTextureUpload(pushedRes, static_cast<Uint16>(tgt), static_cast<Uint16>(lvl)); \
if (MGB_STAGED_TEXTURE_LIVE) { \
MG_Remote::Server::ServerStagedTexture().MarkLevelGpuDirty( \
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes), static_cast<Uint16>(tgt), \
static_cast<Uint16>(lvl), false); \
} \
} else { \
(obj)->MarkStorageDirty(tgt, lvl, false); \
} \
} while (0)
#elif MOBILEGL_PIPE_PUSH
#define MGB_LEVEL_NEEDS_UPLOAD(obj, tgt, lvl) \
(pushedStorage != nullptr \
? FindPipeTextureUpload(*pushedStorage, static_cast<Uint16>(tgt), static_cast<Uint16>(lvl)) != nullptr \
@@ -6783,8 +7012,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId,
stateTextureObject->GetExternalIndex());
GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget());
auto targetInternal = stateTextureObject->GetTarget();
GLenum target = ConvertTextureTargetToBackendGLEnum(MGB_TEXTURE_TARGET(stateTextureObject));
auto targetInternal = MGB_TEXTURE_TARGET(stateTextureObject);
MGLOG_D(" Texture target for syncing is %s",
MG_Util::ConvertTextureTargetToString(targetInternal).c_str());
if (!IsSupportedTextureTarget(targetInternal)) {
@@ -6921,11 +7150,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureImpl::GenerateTextureFormatInfo(MGB_STORAGE_FORMAT(textureMipmapObject), &glInternalFormat,
&glFormat, &glType, targetInternal);
const auto& uploadTargets = textureMipmapObject->GetUploadTargets();
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
ScopedDefaultUnpackState unpackState;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
auto levelTexelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
// A level the application never defined reads back as {0, 0, 0}; now that a
// sparse chain is synced rather than skipped whole, leave those undefined on
// the driver instead of giving the name a 0x0 image at that index.
@@ -6933,11 +7162,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
continue;
}
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = (levelDirty && levelByteSize != 0)
? textureMipmapObject->MapMipmapData(uploadTarget, level)
? MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level,
"append-mips")
: nullptr;
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
@@ -6951,8 +7181,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
const IntVec3 uploadSize =
GetBackendUploadSize(stateTextureObject->GetTarget(), levelTexelSize);
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
GetBackendUploadSize(MGB_TEXTURE_TARGET(stateTextureObject), levelTexelSize);
switch (MapToBackendTextureTarget(MGB_TEXTURE_TARGET(stateTextureObject))) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
g_GLESFuncs.glTexImage2D(
@@ -7022,7 +7252,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
&glFormat, &glType, targetInternal);
ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType);
const auto& uploadTargets = textureMipmapObject->GetUploadTargets();
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
@@ -7120,13 +7350,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
ScopedDefaultUnpackState unpackState;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
const bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level);
if (levelDirty && levelByteSize != 0) {
auto levelTexelSize =
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = textureMipmapObject->MapMipmapData(uploadTarget, level);
auto* pData = MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level,
"immutable-regen");
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
const void* uploadData = PrepareFallbackUpload(
@@ -7183,7 +7414,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ScopedDefaultUnpackState unpackState;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
auto levelTexelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
// See the append-mips loop: an undefined level stays undefined on the
// driver rather than becoming a 0x0 image.
if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 ||
@@ -7191,11 +7422,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGB_LEVEL_UPLOAD_DONE(textureMipmapObject, uploadTarget, level);
continue;
}
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
auto levelByteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
bool levelDirty = MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
auto* pData = (levelDirty && levelByteSize != 0)
? textureMipmapObject->MapMipmapData(uploadTarget, level)
? MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level,
"mutable-regen")
: nullptr;
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
@@ -7214,7 +7446,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
DebugImpl::ErrorLopper::Clear();
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
auto textureTarget = stateTextureObject->GetTarget();
auto textureTarget = MGB_TEXTURE_TARGET(stateTextureObject);
const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize);
switch (MapToBackendTextureTarget(textureTarget)) {
case TextureTarget::Texture2D:
@@ -7263,7 +7495,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
{ // Update all dirty mipmap levels
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
const auto& uploadTargets = textureMipmapObject->GetUploadTargets();
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
for (const auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
if (MGB_LEVEL_NEEDS_UPLOAD(textureMipmapObject, uploadTarget, level)) {
@@ -7283,7 +7515,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// requires glTexSubImage's `format` to match the storage's base internal
// format, so a GL_RG upload into a GL_RGBA32F image is GL_INVALID_OPERATION.
ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType);
const auto& uploadTargets = textureMipmapObject->GetUploadTargets();
const auto& uploadTargets = MGB_UPLOAD_TARGETS(textureMipmapObject);
ScopedDefaultUnpackState unpackState;
for (auto& uploadTarget : uploadTargets) {
for (SizeT level = 0; level < mipmapCount; ++level) {
@@ -7291,7 +7523,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
continue;
}
auto byteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
auto byteSize = MGB_LEVEL_BYTE_SIZE(textureMipmapObject, uploadTarget, level);
if (byteSize == 0) {
MGLOG_D("Mipmap level %d has no data, skipping update.", level);
continue;
@@ -7301,8 +7533,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("%s: Updating dirty mip %d for texture ID %u, size: %dx%d, "
"byteSize: %d",
__func__, level, m_backendTextureId,
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).x(),
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).y(), byteSize);
MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level).x(),
MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level).y(), byteSize);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
@@ -7311,8 +7543,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line,
MG_Util::ConvertGLEnumToString(err).c_str());
});
auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level);
const void* mipData = textureMipmapObject->MapMipmapData(uploadTarget, level);
auto texelSize = MGB_LEVEL_TEXEL_SIZE(textureMipmapObject, uploadTarget, level);
const void* mipData = MGB_LEVEL_TEXELS(textureMipmapObject, uploadTarget, level,
"dirty-level");
Vector<Float> convertedUploadData;
Vector<Uint8> widenedUploadData;
const void* uploadData = PrepareFallbackUpload(
@@ -7329,7 +7562,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
uploadData = PrepareImageWidenedUpload(imageWidening, texelSize, uploadData, byteSize,
imageWidenedUploadData);
const IntVec3 uploadSize =
GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize);
GetBackendUploadSize(MGB_TEXTURE_TARGET(stateTextureObject), texelSize);
// Sub-rect upload: when only a region of the level changed (a
// 16x16 sprite in a 1024x512 atlas, the per-frame lightmap) and
// the shadow bytes go to the driver unconverted, upload just that
@@ -7357,6 +7590,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
: nullptr;
const auto dirtyRegion = [&]() -> MG_State::GLState::MipmapDirtyRegion {
if (pendingUpload == nullptr) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): a level this arm owes with NO pending upload behind it
// was dirtied by the GPU (T5), and the dirty answer is the
// server's own mark on the staged shadow - the whole level,
// because a generation touches all of it. The client is never
// asked (§2.2's last row).
if (MGB_STAGED_TEXTURE_LIVE) {
const IntVec3 gpuExtent =
MG_Remote::Server::ServerStagedTexture().LevelExtentOrUndefined(
MG_Remote::Server::StagedTextureStore::KeyForHandle(pushedRes),
static_cast<Uint16>(uploadTarget), static_cast<Uint16>(level));
return MG_State::GLState::MipmapDirtyRegion{IntVec3{0, 0, 0}, gpuExtent};
}
#endif
return textureMipmapObject->GetStorageDirtyRegion(uploadTarget, level);
}
// MGPBox is {origin, extent}; MipmapDirtyRegion is {lo, hi}. The
@@ -7507,6 +7754,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
region.Z + static_cast<Int32>(region.D)}};
}
} else
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// tx: with an active transport the rect list is the record's (the
// pendingUpload arm above) or nothing - the server's GPU-dirty mark
// is whole-level and has no scatter refinement to hand out, and the
// frontend's rect model is not this side's to read.
if (!MGB_STAGED_TEXTURE_LIVE)
#endif
dirtyRectCount = textureMipmapObject->GetStorageDirtyRects(
uploadTarget, level, dirtyRects,
@@ -7661,7 +7915,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs,
rectShape ? static_cast<Uint64>(dirtyRectCount) : 1u);
}
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
switch (MapToBackendTextureTarget(MGB_TEXTURE_TARGET(stateTextureObject))) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap:
if (subRectEligible && dirtyRectCount >= 2) {
@@ -8009,6 +8263,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
#undef MGB_STORAGE_FIXED_SAMPLE_LOCATIONS
#undef MGB_STORAGE_IMMUTABLE
#undef MGB_STORAGE_KIND
#undef MGB_TEXTURE_TARGET
#undef MGB_UPLOAD_TARGETS
#undef MGB_LEVEL_TEXEL_SIZE
#undef MGB_LEVEL_BYTE_SIZE
#undef MGB_LEVEL_TEXELS
#if MOBILEGL_BUILD_DISAGGREGATED
#undef MGB_STAGED_TEXTURE_LIVE
#endif
#if MOBILEGL_PIPE_PUSH
const SamplerParameters* BackendTextureObject::ResolvePushedBuiltinSampler(
@@ -33,6 +33,10 @@
#include "MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h"
#include "MG_Util/Texture/PixelStoreProcessor.h"
#include <Config.h>
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (T5 / tx): the server's staged-texture shadow GenerateMipmap defines its chain on.
#include <MG_Remote/Server/StagedTextureStore.h>
#endif
#include <algorithm>
#include <bit>
#include <cstdlib>
@@ -1608,6 +1612,52 @@ void main() {
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (T5 / tx): the split arm of EnsureGenerateMipmapStorageAllocated. Under an active
// transport the apply thread may not WRITE the client's level storage - AllocateStorage
// and MarkStorageDirty on a frontend TextureObjectMipmap are §6 layer-1 surfaces
// (CONTRACT-P5C §2.3) - so the generated chain is defined on the SERVER's staged-texture
// shadow instead, keyed by this renderer's own texture twin (the TextureResource,
// node-stable in VkTextureManager's map). Same levels, same extents - derived from the
// Vulkan-space base extent, whose depth is already 1 for every array target (layers
// live in arrayLayers there, so the fixed-component split the GL-space derivation
// needs is unnecessary here; these shadow extents answer in Vulkan space, which every
// consumer of a Magma-keyed entry shares) - and every generated level is marked
// dirty-in-shadow, because its texels are generated on the GPU and no byte answer
// exists on this side. The client's chain is left stale, which §2.3 rules CORRECT: the
// two readers that could observe the staleness are both named refusals under split.
// The upload-target list still comes from the frontend object - a shape READ, the P7
// registry's residual, not one of the writes this arm exists to remove.
static Bool EnsureGenerateMipmapShadowAllocated(const VkTextureManager::TextureResource& resource,
Uint32 baseMipLevel,
const Vector<TextureUploadTarget>& uploadTargets) {
if (resource.mipLevels <= baseMipLevel || uploadTargets.empty()) {
return false;
}
const IntVec3 storageBaseTexelSize = {static_cast<Int>(resource.extent.width),
static_cast<Int>(resource.extent.height),
static_cast<Int>(resource.depth)};
const IntVec3 baseTexelSize = ComputeMipTexelSize(storageBaseTexelSize, baseMipLevel);
if (baseTexelSize.x() <= 0 || baseTexelSize.y() <= 0 || baseTexelSize.z() <= 0) {
return false;
}
const Uint32 requiredMipLevelCount = baseMipLevel + ComputeFullMipLevelCount(baseTexelSize);
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForTwinAddress(&resource);
for (const auto uploadTarget : uploadTargets) {
for (Uint32 level = baseMipLevel + 1; level < requiredMipLevelCount; ++level) {
// A level the shadow already tracks (an adopted base chain) keeps its bytes;
// the generation made the GPU newer than either, which the mark says.
store.NoteLevelDefined(key, static_cast<Uint16>(uploadTarget), static_cast<Uint16>(level),
ComputeMipTexelSize(storageBaseTexelSize, level));
store.MarkLevelGpuDirty(key, static_cast<Uint16>(uploadTarget), static_cast<Uint16>(level),
true);
}
}
return true;
}
#endif
static VkImageLayout ResolveGenerateMipmapFinalLayout(VkImageAspectFlags aspectMask) {
return (aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
@@ -11314,7 +11364,17 @@ void main() {
"GenerateMipmap: depth-stencil mipmap generation is not supported yet.");
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (T5 / tx): under an active transport the generated chain is defined on the
// server's staged shadow (keyed by the synced TextureResource above) and the client's
// level storage is never written; in monolith the client-object path runs unchanged.
const Bool allocatedMipmapStorage =
MG_Config::Transport != MG_Config::TransportMode::Monolith
? EnsureGenerateMipmapShadowAllocated(*resource, baseMipLevel, texture->GetUploadTargets())
: EnsureGenerateMipmapStorageAllocated(*mipmapTexture, baseMipLevel);
#else
const Bool allocatedMipmapStorage = EnsureGenerateMipmapStorageAllocated(*mipmapTexture, baseMipLevel);
#endif
MOBILEGL_ASSERT(allocatedMipmapStorage, "GenerateMipmap could not allocate a full mip chain for this texture.");
resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
+42 -5
View File
@@ -986,11 +986,13 @@ namespace MobileGL::MG_Pipe {
return true;
}
// The texture half of resource_subdata, and it DISPATCHES TO NOBODY. Nothing in this
// family reaches the backend at GL-call time today: a texture write marks a level dirty
// and Espryt uploads it at its own sync point, out of the accumulated set below. So the
// whole of this function is the gate, the accumulation and the serial - which is also
// why MGPipeResourceOps did not have to grow a member for it.
// The texture half of resource_subdata, and it DISPATCHES TO NOBODY at GL-call time:
// a texture write marks a level dirty and Espryt uploads it at its own sync point, out
// of the accumulated set below. So the whole of this function is the gate, the
// accumulation, the serial and - P5c (tx), disaggregated builds only - the adoption
// hook that moves the staged bytes into the server's staged-texture store while they
// are still alive. MGPipeResourceOps grew its three texture members for exactly that
// hook; the monolith shape of everything above them is unchanged.
Bool ApplyTextureUpload(const MGPSubData& record, const void* bytes, const MGPSubRegion* regions) {
MGPipeResourceRecord* stored =
ResolveResourceIn(g_applier.TextureResources, "resource_subdata", record.Res);
@@ -1032,6 +1034,16 @@ namespace MobileGL::MG_Pipe {
// record was accumulated, so the texels are the server's now, and that is the true
// this returns.
++stored->Serial;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): THE STAGED BYTES ARE ADOPTED HERE, at the last instant `bytes` is known
// alive (rule C: SEG_STAGE retires when this record does, and PendingUpload
// deliberately holds no byte pointer). The hook copies the run into the server's
// staged-texture store keyed by this record's own handle; it is a no-op in
// monolith, so the monolith shape keeps P5's pointer-dropping expression exactly.
if (g_resourceOps != nullptr && g_resourceOps->TextureSubData != nullptr) {
g_resourceOps->TextureSubData(record.Res, record, bytes, regions);
}
#endif
return true;
}
@@ -1782,6 +1794,22 @@ namespace MobileGL::MG_Pipe {
record->PendingUploads.clear();
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): the staged-texture store's defined-ness and drop bookkeeping rides THE SAME
// scope rules as the pending-set drops above - a named level redefines that one
// (uploadTarget, level), a whole-resource respecify drops every level, and a metadata
// update replaces no storage and is not delivered. Without this the store could not say
// whether a level exists at all (a null-data glTexImage*D carries no sub-data), and a
// whole-resource respecify would leave levels keyed against a replaced coordinate
// system. Textures only: a buffer's storage is the ops table's own Respecify hook, and
// a renderbuffer has no levels.
if (desc.Target != kMGPipeResourceTargetBuffer &&
desc.Target != static_cast<Uint8>(MGPipeResourceTarget::Renderbuffer) && !metadataOnly &&
g_resourceOps != nullptr && g_resourceOps->TextureRespecify != nullptr) {
g_resourceOps->TextureRespecify(desc.Resource, desc, level);
}
#endif
// resource_respecify is the catalogue's only kNeedsAck call, and the per-record half
// of that flag is MGPipeResourceRespecifyNeedsAck(desc): glBufferStorage is a real
// synchronous allocation and the only entry point allowed a synchronous ack, while
@@ -1978,6 +2006,15 @@ namespace MobileGL::MG_Pipe {
// AND ONLY A BUFFER IS HANDED ON, for resource_create's reason: the op table is the
// buffer family's, its Destroy takes a handle whose kind that backend registered for,
// and a texture's death is read out of the record at the sync that would have used it.
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): with ONE exception - the staged-texture store is keyed by the handle, so
// the death must reach it or a recycled slot's stale levels would answer for the
// successor. This does not hand the texture to the buffer family's Destroy.
if (static_cast<MGPipeKind>(handle.Kind) == MGPipeKind::Texture &&
g_resourceOps != nullptr && g_resourceOps->TextureDestroy != nullptr) {
g_resourceOps->TextureDestroy(handle.Handle);
}
#endif
if (static_cast<MGPipeKind>(handle.Kind) != MGPipeKind::Buffer) return;
if (g_resourceOps != nullptr && g_resourceOps->Destroy != nullptr) {
g_resourceOps->Destroy(handle.Handle);
+23
View File
@@ -41,6 +41,9 @@ namespace MobileGL::MG_State::GLState {
namespace MobileGL::MG_Pipe {
struct PipeInputs;
// P5c (tx): MGPipeResourceOps::TextureRespecify names it; the definition is beside the
// applier entry point that produces one (below, with MGPipeApplyResourceRespecify).
struct MGPRespecifiedLevel;
// ---------------------------------------------------------------------------------
// The CSO store
@@ -94,6 +97,26 @@ namespace MobileGL::MG_Pipe {
void (*Destroy)(MGPipeHandle res);
void* (*MapPersistent)(MGPipeHandle res, Uint64 size, const void* seedBytes);
void (*UnmapPersistent)(MGPipeHandle res);
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): the TEXTURE half of the resource family, appended so every positional
// initialiser of the nine P3a members keeps its meaning. TextureSubData is called
// from ApplyTextureUpload AFTER the gate, the accumulation and the serial, while
// `bytes` still names the staged run (SEG_STAGE retires when the record does, so an
// adoption anywhere later would read dead bytes - rule C); the backend copies the
// run into the server's staged-texture store (MG_Remote/Server/StagedTextureStore.h)
// and does nothing in monolith. TextureRespecify is the defined-ness/drop channel:
// it rides MGPipeApplyResourceRespecify's own scope rules (a named level redefines
// that level, a whole-resource respecify drops them all, a metadata update is not
// delivered). TextureDestroy is resource_destroy's texture arm - the applier hands
// only a buffer to Destroy, and a store keyed by the handle needs the death to drop
// its key. All three may be null together: a backend that has not adopted the staged
// shadow leaves them null and keeps the pre-tx shape.
void (*TextureSubData)(MGPipeHandle res, const MGPSubData& record, const void* bytes,
const MGPSubRegion* regions);
void (*TextureRespecify)(MGPipeHandle res, const MGPResourceDesc& desc,
const MGPRespecifiedLevel* level);
void (*TextureDestroy)(MGPipeHandle res);
#endif
};
// Install / read the table. A null argument uninstalls, which is what a backend does at
@@ -0,0 +1,356 @@
// MobileGL - MobileGL/MG_Remote/Server/StagedTextureStore.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P5c tx - THE SERVER'S OWN COPY OF THE STAGED TEXTURE LEVELS. The texture twin of
// StagedShadow.h (R-11), held to its four rulings (CONTRACT-P5C.md §2.1).
//
// THE DEFECT THIS ENDS (T1/T5). resource_subdata's texture half stages the level bytes into
// SEG_STAGE and ApplyTextureUpload (PipeApply.cpp:989-1036) drops the pointer after the gate,
// the accumulation and the serial. Espryt then re-reads the client's MipmapStorage at sync
// time (Managers.cpp:6940, :7129, :7198, :7315), reads per-level shape off the client object
// (:6928, :6936, :7314, :7332, DirectGLES.cpp:8502-8507, :8528-8529), and Magma WRITES the
// client's level storage outright (VulkanRenderer.cpp:1562-1609: AllocateStorage +
// MarkStorageDirty). In monolith every one of those is correct - the bytes belong to a
// frontend object that outlives the call. Under split the pointer names SEG_STAGE, valid
// only until retiredSeq passes the record, and w1's MOBILEGL_IPC_AUDIT=1 fills retired
// staging with 0xDD precisely so an implementation that kept the pointer is DISTINGUISHABLE
// from one that copied. So this copies, at apply time, into server-owned storage, and the
// sync reads nothing but this store and the descriptor.
//
// WHAT A KEY IS, AND WHY IT IS NOT THE TWIN ADDRESS THE CONTRACT'S FIRST DRAFT SAID.
// CONTRACT-P5C §2.1 rules "keyed by the texture resource twin's address". The buffer half
// keys by twin address because the twin is minted at sub-data time under split
// (Managers.cpp:2139-2142) and GLESBufferResource's constructor is GL-free. The texture
// twin's constructor is NOT: BackendTextureObject() calls glGenTextures (Managers.cpp:5496),
// so minting it at sub-data time would allocate a driver name for every texture that is
// written and never drawn, and would put a GL call into a unit test that has no context
// (R-16's unit case, StagedTextureStoreTest, exercises the REAL ops table headless). The
// wire HANDLE the record carried serves the same purpose the address served - stable for
// the object's life, liveness-exact through the generation, already in every caller's hand
// (rule E: the apply thread resolves every object from a handle the record carried) - so
// KeyForHandle is the primary key. KeyForTwinAddress exists for the one caller that has a
// server-side twin and no handle: Magma's T5 shadow writes key by the TextureResource. The
// two namespaces cannot collide (handle keys carry the top bit; user-space heap addresses
// never do). Every event that ends a key's life has a call site: a named-level respecify
// re-defines the level (NoteLevelDefined), a whole-resource respecify drops every level
// (ResetLevels), resource_destroy drops the key (Ops_H_TextureDestroy), context death drops
// all (OnBackendContextDestroyed, beside MGL_SERVER_STAGED_DROP_ALL).
//
// COVERAGE IS EXACTLY THE STAGED RUN, NEVER WIDENED AND NEVER NARROWER. The texture half of
// resource_subdata ALWAYS stages the whole level shadow (TextureEmit.h:1285: "the bytes this
// record declares ARE the level shadow", Blob.Size = the level's byte count), so one Adopt
// covers its level whole. The record's region set is deliberately NOT the coverage unit: it
// declares which texels CHANGED (the upload planner's shape, which the applier's pending set
// already carries), while the full-level upload paths - every conversion fallback, and the
// immutable/mutable regeneration arms - read the whole level including texels no region
// named. Those texels crossed in the staged run; treating them as uncovered would Fatal a
// legal glTexStorage-then-small-glTexSubImage sequence whose first (and only) record names a
// small box, and moving them into the store is not the buffer half's silent-zero case
// because they are the client's own shadow bytes, delivered and declared. A sync that asks
// for a level this store has no covered run for is Fatal{StageSnapshotTooNarrow} - the same
// words as the buffer half, for the same reason: the bytes have never existed on this side,
// and inventing them is silent data loss, not a missing optimisation.
//
// DEFINED-NESS IS TRACKED, NOT DERIVED ALONE. §1's per-level extent derivation (max(1,
// base >> level), layer axes fixed) computes the extent of a level that EXISTS; it cannot
// say whether the level was ever defined, and the mutable regen arms skip undefined levels
// (a sparse chain stays sparse on the driver). The client does not emit per-level extents,
// but every storage-defining call DOES cross as a respecify, so defined-ness reaches this
// store through the ops table's TextureRespecify hook: a named level marks that (uploadTarget,
// level), an immutable whole-resource respecify (glTexStorage*) marks every level of every
// upload target. Levels the store has never heard of answer extent {0,0,0}, which is the
// exact answer the frontend's GetMipmapTexelSize gives for them - FBO completeness over a
// null-data level and the sparse-chain skip both reproduce the monolith arm.
//
// GPU-GENERATED LEVELS (T5) ARE DEFINED HERE WITH NO BYTES AND A DIRTY MARK. A GPU-side
// generation (Magma's GenerateMipmap) dirties the SERVER's shadow, not the client's: the
// level is NoteLevelDefined + MarkLevelGpuDirty(true), it holds no bytes (they were made on
// the GPU and never crossed), and the mark is the "dirty region, level has NO pending
// upload" answer of §2.2's table. A texel read of such a level is the
// Fatal{StageSnapshotTooNarrow} case above, which is correct: the two readers that could
// ask are both named refusals under split (§2.3).
//
// WHY IT IS A HEADER AND NOT A BLOCK INSIDE Managers.cpp - StagedShadow.h's reason, and it
// is the one that matters here too: a block inside Managers.cpp could only ever be exercised
// by a test that also has a GL context, a resource twin and a live session, which is exactly
// how a rule ends up with no check that can fail for its own reason (R-16). Here `copies` is
// a constructor parameter rather than a read of MG_Config::Transport, so a unit case builds
// one store of each kind and asserts the DIFFERENCE between them; the production wiring is
// asserted separately, through the real ops table, in StagedTextureStoreTest.
#pragma once
#include <Includes.h>
#include <MG_Pipe/MGPipeTypes.h>
#include <MG_Util/Debug/Log.h>
#include <MG_Util/Math/VectorTypes.h>
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <mutex>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#endif
namespace MobileGL::MG_Remote::Server {
// §1's server-side per-level extent (CONTRACT-P5C table 0): max(1, base_extent >> level)
// per SHRINKING axis, with an array texture's layer count fixed - it is not a dimension of
// the image (GL 4.6 core 8.14.3), and GetMipmapTexelSize parks it in the slot after the
// image's own dimensions. Texture1DArray shrinks x only; Texture2DArray and
// TextureCubeMapArray shrink x and y; every other target shrinks all three. This is the
// mip chain's definition (MG_State's IsMipmapCompleteForFilter applies the same split,
// TextureObject.cpp:705-734), so two honest ends compute the same number.
inline Int StagedTextureShrinkingAxisCount(Uint8 pipeResourceTarget) {
switch (static_cast<MG_Pipe::MGPipeResourceTarget>(pipeResourceTarget)) {
case MG_Pipe::MGPipeResourceTarget::Tex1DArray:
return 1;
case MG_Pipe::MGPipeResourceTarget::Tex2DArray:
case MG_Pipe::MGPipeResourceTarget::TexCubeArray:
return 2;
default:
return 3;
}
}
inline IntVec3 StagedTextureMipExtent(Uint8 pipeResourceTarget, Uint32 baseWidth, Uint32 baseHeight,
Uint32 baseDepth, Uint32 level) {
const Int shrinking = StagedTextureShrinkingAxisCount(pipeResourceTarget);
const Int shift = static_cast<Int>(level);
IntVec3 extent{static_cast<Int>(baseWidth), static_cast<Int>(baseHeight), static_cast<Int>(baseDepth)};
for (Int axis = 0; axis < shrinking && axis < 3; ++axis) {
extent[axis] = std::max<Int>(extent[axis] >> shift, 1);
}
return extent;
}
class StagedTextureStore {
public:
// `copies` is "this process is really split". False reproduces the monolith expression
// character for character: the client shadow answers every question (the sync's macros
// never consult this store when CopiesIntoServerStorage() is false), nothing is
// allocated, and every push and verify lane stays byte-identical to what it was
// before tx.
explicit StagedTextureStore(Bool copies) : m_copies(copies) {}
Bool CopiesIntoServerStorage() const { return m_copies; }
// The wire handle the record carried, tagged so it can never alias a twin address.
// Slot and Gen are the client allocator's identity for one live object, so a recycled
// slot's new owner keys a different entry than its predecessor's stale one.
static Uint64 KeyForHandle(MG_Pipe::MGPipeHandle handle) {
return (Uint64{1} << 63) | (static_cast<Uint64>(handle.Slot) << 32) |
static_cast<Uint64>(handle.Gen);
}
// For the caller whose server-side twin has no wire handle (Magma's TextureResource,
// T5). Node-stable by that table's own ruling (std::unordered_map nodes).
static Uint64 KeyForTwinAddress(const void* twin) {
return static_cast<Uint64>(reinterpret_cast<std::uintptr_t>(twin));
}
// THE ADOPTION. Copies the record's staged run - the whole level shadow, byteSize =
// MGPSubData::Blob.Size under split - into server-owned storage and marks the level
// covered whole (see the header comment for why the run, not the region set, is the
// coverage). REPLACES the level's entry: the run is the level's complete current
// content, so nothing of a previous run survives, and a fresh adoption is by
// definition not GPU-dirty. Returns the server-owned base; under monolith it does
// nothing and returns nullptr, and the caller (Ops_H_TextureSubData) has already
// returned before reaching here.
const Uint8* Adopt(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
const void* bytes, SizeT byteSize) {
if (!m_copies) return nullptr;
const std::lock_guard<std::mutex> lock(m_mutex);
LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)];
shadow.Extent = extent;
shadow.Bytes.clear();
if (bytes != nullptr && byteSize != 0) {
const auto* raw = static_cast<const Uint8*>(bytes);
shadow.Bytes.assign(raw, raw + byteSize);
}
shadow.Defined = true;
shadow.GpuDirty = false;
m_any.store(true, std::memory_order_release);
return shadow.Bytes.empty() ? nullptr : shadow.Bytes.data();
}
// A storage-defining respecify named this level: it EXISTS from here on, at this
// extent (null-data glTexImage*D, a generated level). An extent move redefines the
// level's coordinate system, so the bytes and the dirty mark of the old one go with
// it; an extent-restating one keeps them, which is the same answer the driver gives
// (a same-shape redefinition that carries no new upload leaves the old texels in
// place - undefined content is allowed to be the old content).
void NoteLevelDefined(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent) {
if (!m_copies) return;
const std::lock_guard<std::mutex> lock(m_mutex);
LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)];
if (!shadow.Defined || shadow.Extent != extent) {
shadow.Bytes.clear();
shadow.GpuDirty = false;
}
shadow.Extent = extent;
shadow.Defined = true;
m_any.store(true, std::memory_order_release);
}
// A whole-resource redefinition (glTexStorage*, a texture view): every level's old
// coordinate system is gone, so every level entry goes. The caller marks the new
// chain defined level by level afterwards where the call defines one.
void ResetLevels(Uint64 key) {
if (!m_any.load(std::memory_order_acquire)) return;
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows.erase(key);
}
// T5's dirty mark: a GPU-side generation made this level's texels newer than any
// shadow. The level typically holds NO bytes - they were generated on the GPU and
// never crossed - and the mark is what answers "dirty region, level has no pending
// upload" without asking the client (§2.2's last row).
void MarkLevelGpuDirty(Uint64 key, Uint16 uploadTarget, Uint16 level, Bool dirty) {
if (!m_copies) return;
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows[key].Levels[PackLevel(uploadTarget, level)].GpuDirty = dirty;
m_any.store(true, std::memory_order_release);
}
void Drop(Uint64 key) {
if (!m_any.load(std::memory_order_acquire)) return;
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows.erase(key);
}
void DropAll() {
if (!m_any.load(std::memory_order_acquire)) return;
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows.clear();
}
// Fatal when a sync wants texels this store has no covered run for. This is THE data
// -correctness refusal of the texture half: a pending upload with no adoption behind
// it, a GPU-generated level (no bytes by construction), a level the client never
// defined, and a monolith-arm misuse all land here, because in every one of them the
// bytes have never existed on this side and re-reading the client's shadow for them
// is the cross-role access tx exists to end.
const Uint8* RequireLevelBytes(Uint64 key, Uint16 uploadTarget, Uint16 level, const char* site) const {
if (!m_copies) return nullptr;
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
if (shadow != nullptr && shadow->Defined && !shadow->Bytes.empty()) {
return shadow->Bytes.data();
}
MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the texture sync wants the "
"bytes of (uploadTarget=%u, level=%u) and the server's staged shadow has no "
"covered run for it. Under split the authoritative shadow is SERVER-OWNED "
"(rule C) and resource_subdata is the only way bytes reach it, so these "
"texels have never existed on this side: the record that should have "
"carried them is missing, or the level's texels were generated on the GPU "
"and no byte answer exists at all. Re-reading the client's shadow would be "
"the cross-role access this store exists to end",
site, static_cast<Uint32>(uploadTarget), static_cast<Uint32>(level));
std::abort();
}
// Diagnostics the sync path and the unit cases read, so that a check can assert WHAT
// HAPPENED rather than that nothing blew up.
Bool IsCovered(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
return shadow != nullptr && shadow->Defined && !shadow->Bytes.empty();
}
Bool IsLevelDefined(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
return shadow != nullptr && shadow->Defined;
}
Bool IsLevelGpuDirty(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
if (!m_any.load(std::memory_order_acquire)) return false;
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
return shadow != nullptr && shadow->GpuDirty;
}
// {0,0,0} for a level this store has never heard of - the exact answer the frontend's
// GetMipmapTexelSize gives for an undefined level, which is what keeps the regen
// arms' sparse-chain skip intact.
IntVec3 LevelExtentOrUndefined(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
if (!m_any.load(std::memory_order_acquire)) return IntVec3{0, 0, 0};
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
if (shadow == nullptr || !shadow->Defined) return IntVec3{0, 0, 0};
return shadow->Extent;
}
SizeT LevelByteSize(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
if (!m_any.load(std::memory_order_acquire)) return 0;
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
if (shadow == nullptr || !shadow->Defined) return 0;
return shadow->Bytes.size();
}
Bool HasShadow(Uint64 key) const {
if (!m_any.load(std::memory_order_acquire)) return false;
const std::lock_guard<std::mutex> lock(m_mutex);
return m_shadows.find(key) != m_shadows.end();
}
SizeT TrackedResources() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return m_shadows.size();
}
SizeT TrackedLevelCount(Uint64 key) const {
const std::lock_guard<std::mutex> lock(m_mutex);
const auto it = m_shadows.find(key);
return it == m_shadows.end() ? 0 : it->second.Levels.size();
}
private:
static Uint32 PackLevel(Uint16 uploadTarget, Uint16 level) {
return (static_cast<Uint32>(uploadTarget) << 16) | static_cast<Uint32>(level);
}
struct LevelShadow {
IntVec3 Extent{0, 0, 0};
// The level's whole staged run. EMPTY for a defined-but-byteless level (null-data
// definition, GPU-generated) - emptiness is the coverage answer, not an error.
Vector<Uint8> Bytes;
Bool Defined = false;
Bool GpuDirty = false;
};
struct TextureShadow {
ska::flat_hash_map<Uint32, LevelShadow> Levels;
};
const LevelShadow* FindLevel(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
const auto textureIt = m_shadows.find(key);
if (textureIt == m_shadows.end()) return nullptr;
const auto levelIt = textureIt->second.Levels.find(PackLevel(uploadTarget, level));
return levelIt == textureIt->second.Levels.end() ? nullptr : &levelIt->second;
}
const Bool m_copies;
mutable std::mutex m_mutex;
ska::flat_hash_map<Uint64, TextureShadow> m_shadows;
// Read on every IsLevelGpuDirty / LevelExtentOrUndefined, so the monolith cost is one
// acquire load of a never-written flag rather than a mutex and two hash lookups.
std::atomic<Bool> m_any{false};
};
#if MOBILEGL_BUILD_DISAGGREGATED
// ONE PER PROCESS, and its copying arm is decided ONCE at first use - StagedShadow's
// ServerStaged() ruling verbatim: the two arms hold the authoritative bytes in DIFFERENT
// places, so an answer that changed mid-run would strand every level already staged.
// Leaked at exit like every other MG_Remote singleton (ID-8). In a header rather than in
// Managers.cpp because TWO backends consume it: Espryt's adoption and sync
// (Managers.cpp) and Magma's T5 shadow writes (VulkanRenderer.cpp) must name the same
// store, and an inline function's one static gives them exactly that.
inline StagedTextureStore& ServerStagedTexture() {
static StagedTextureStore& store =
*new StagedTextureStore(MG_Config::Transport != MG_Config::TransportMode::Monolith);
return store;
}
#endif
} // namespace MobileGL::MG_Remote::Server
+11 -8
View File
@@ -73,18 +73,21 @@ gtest_discover_tests(PipeWireCodecTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS un
# main() of its own. All three also reach MG_Pipe, MG_Backend and MG_State, so all three carry
# those include paths.
#
# RemoteClientTest the 71-slot emit table, the caps mirror, R-8's liveness gates, R-17's
# routing and ID-47/ID-49's readback rules (c1)
# ServerLoopTest the apply thread, the blocking control mailbox, the verb stamp and
# R-11's server-owned staging copy (v1)
# SessionHandshakeTest the two null-union guards driven THROUGH ServerSession::Accept and
# ClientSession::StartOverTransportPair, and the ABI fingerprint's
# sensitivity case driven from CapsAbiFingerprint() (s1, ID-46 6 and 7)
# RemoteClientTest the 71-slot emit table, the caps mirror, R-8's liveness gates, R-17's
# routing and ID-47/ID-49's readback rules (c1)
# ServerLoopTest the apply thread, the blocking control mailbox, the verb stamp and
# R-11's server-owned staging copy (v1)
# SessionHandshakeTest the two null-union guards driven THROUGH ServerSession::Accept and
# ClientSession::StartOverTransportPair, and the ABI fingerprint's
# sensitivity case driven from CapsAbiFingerprint() (s1, ID-46 6 and 7)
# StagedTextureStoreTest the texture half of R-11: tx's staged-texture store, its
# Fatal{StageSnapshotTooNarrow}, and the E-P5c #2 production wiring
# through the real resource op table (P5c tx)
#
# THIS FILE IS THE PHASE'S ONE RECURRING MERGE CONFLICT, and it is the same-point-append shape
# BRIEF §5's ownership table exists to prevent: three packages, three targets, one end-of-file.
# The loop is what stops there being a fourth: a package adding a suite adds a NAME.
foreach (wiretest IN ITEMS RemoteClientTest ServerLoopTest SessionHandshakeTest)
foreach (wiretest IN ITEMS RemoteClientTest ServerLoopTest SessionHandshakeTest StagedTextureStoreTest)
add_executable(${wiretest} ${wiretest}.cpp)
target_include_directories(${wiretest} PRIVATE
@@ -0,0 +1,376 @@
// MobileGL - MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// Package tx's suite (P5c): the server's staged-texture shadow.
//
// THE SHAPE IS StagedShadowTest's (ServerLoopTest.cpp:606-760), for the R-16 reason stated
// there: every case must be able to go red for the reason it exists, and no other. The store
// unit cases build one store of each kind and assert the DIFFERENCE (copies is a constructor
// parameter, not a read of MG_Config::Transport); the production case drives the REAL
// resource op table - the same g_glesResourceOps RegisterBufferBackendOps installs -
// because a suite that only exercised StagedTextureStore in isolation would stay green with
// the ops-table registration deleted.
//
// E-P5c GATE #2 LIVES IN THE PRODUCTION CASE: reverting the adoption to P5's pointer-dropping
// (delete the copy inside Ops_H_TextureSubData, or the ops-table registration, or the
// ApplyTextureUpload hook call) turns it red - that is what makes "the server consumes the
// staged bytes" a checked fact rather than a design intention.
#include <Config.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_Remote/Server/StagedTextureStore.h>
#include <MG_State/GLState/TextureState/TextureEnum.h>
#include <csignal>
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#if defined(_WIN32)
#include <process.h>
#else
#include <unistd.h>
#endif
using namespace MobileGL;
namespace Server = MobileGL::MG_Remote::Server;
namespace {
std::string g_logPath;
std::string ReadLog() {
std::ifstream in(g_logPath, std::ios::binary);
if (!in) return {};
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}
unsigned ProcessId() {
#if defined(_WIN32)
return static_cast<unsigned>(_getpid());
#else
return static_cast<unsigned>(::getpid());
#endif
}
constexpr Uint16 kTex2DTarget = static_cast<Uint16>(TextureUploadTarget::Texture2D);
MG_Pipe::MGPipeHandle TestHandle(Uint32 slot, Uint32 gen) {
MG_Pipe::MGPipeHandle handle{};
handle.Slot = slot;
handle.Gen = gen;
return handle;
}
} // namespace
// =====================================================================================
// The store, in isolation
// =====================================================================================
// THE PROPERTY MOBILEGL_IPC_AUDIT=1's 0xDD FILL EXISTS TO TEST, at unit scope: after the
// staged source bytes are overwritten - which is what the decoder does to a retired SEG_STAGE
// run - the server's copy still reads the original. The monolith store is the control: the
// SAME calls are no-ops on it, because the monolith sync answers from the client shadow.
TEST(StagedTextureStoreTest, TheSplitArmCopiesAndSurvivesTheSourceBeingPoisoned) {
Server::StagedTextureStore splitStore(/*copies=*/true);
Server::StagedTextureStore monolithStore(/*copies=*/false);
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(3, 1));
Vector<Uint8> staged(64, 0xAB);
const IntVec3 extent{4, 4, 1};
const Uint8* splitBase = splitStore.Adopt(key, kTex2DTarget, 0, extent, staged.data(), staged.size());
const Uint8* monolithBase =
monolithStore.Adopt(key, kTex2DTarget, 0, extent, staged.data(), staged.size());
ASSERT_NE(splitBase, nullptr);
EXPECT_EQ(monolithBase, nullptr)
<< "the monolith arm allocates nothing and answers nothing - the client shadow answers";
EXPECT_EQ(monolithStore.TrackedResources(), 0u);
EXPECT_FALSE(monolithStore.IsCovered(key, kTex2DTarget, 0));
EXPECT_NE(splitBase, staged.data()) << "the adoption returned the CLIENT's pointer - the "
"exact rule-C violation this store exists to fix";
// w1's retired-stage poison, by hand and at the right moment: the record has retired, so
// the staging run is dead.
std::fill(staged.begin(), staged.end(), Uint8{0xDD});
for (SizeT i = 0; i < 64; ++i) {
EXPECT_EQ(splitBase[i], 0xAB) << "byte " << i << " of the server's copy is the poison, "
<< "so the copy never happened";
}
}
// Defined-ness is tracked separately from bytes: a null-data definition (NoteLevelDefined
// with no Adopt) makes the level EXIST at the derived extent without covering any bytes, and
// an extent move redefines the coordinate system, so the old run goes with it while the
// level stays defined. A same-extent re-note keeps the run - the driver keeps the old texels
// for a same-shape redefinition too.
TEST(StagedTextureStoreTest, DefinednessIsTrackedAndAnExtentMoveDropsTheBytes) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(4, 1));
EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 0), IntVec3(0, 0, 0))
<< "a level nothing defined must answer {0,0,0} - the sparse-chain skip reads exactly "
"this, and the frontend's GetMipmapTexelSize answers the same";
EXPECT_FALSE(store.IsLevelDefined(key, kTex2DTarget, 0));
store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{4, 4, 1});
EXPECT_TRUE(store.IsLevelDefined(key, kTex2DTarget, 0));
EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 0)) << "defined-without-bytes covers nothing";
EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 0), IntVec3(4, 4, 1));
EXPECT_EQ(store.LevelByteSize(key, kTex2DTarget, 0), 0u);
Vector<Uint8> bytes(64, 0x11);
store.Adopt(key, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes.data(), bytes.size());
EXPECT_TRUE(store.IsCovered(key, kTex2DTarget, 0));
EXPECT_EQ(store.LevelByteSize(key, kTex2DTarget, 0), 64u);
store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{4, 4, 1});
EXPECT_TRUE(store.IsCovered(key, kTex2DTarget, 0)) << "a same-extent re-definition keeps the run";
store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{8, 8, 1});
EXPECT_TRUE(store.IsLevelDefined(key, kTex2DTarget, 0));
EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 0))
<< "an extent move replaced the coordinate system; the old run must not answer for it";
EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 0), IntVec3(8, 8, 1));
}
// Keys are independent, ResetLevels drops one resource's whole chain, Drop one key and
// DropAll every one - the three events the contract names (respecify, destroy, context death)
// each have their call site, and these are the answers those call sites rely on.
TEST(StagedTextureStoreTest, ResetDropAndDropAllForgetExactlyWhatTheyName) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 a = Server::StagedTextureStore::KeyForHandle(TestHandle(5, 1));
const Uint64 b = Server::StagedTextureStore::KeyForHandle(TestHandle(6, 1));
Vector<Uint8> bytes(16, 0x22);
store.Adopt(a, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes.data(), bytes.size());
store.Adopt(a, kTex2DTarget, 1, IntVec3{2, 2, 1}, bytes.data(), bytes.size());
store.Adopt(b, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes.data(), bytes.size());
ASSERT_EQ(store.TrackedResources(), 2u);
ASSERT_EQ(store.TrackedLevelCount(a), 2u);
store.ResetLevels(a);
EXPECT_FALSE(store.HasShadow(a)) << "a whole-resource respecify forgets every level";
EXPECT_TRUE(store.IsCovered(b, kTex2DTarget, 0));
store.Adopt(a, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes.data(), bytes.size());
store.Drop(a);
EXPECT_EQ(store.TrackedResources(), 1u);
EXPECT_TRUE(store.IsCovered(b, kTex2DTarget, 0));
store.DropAll();
EXPECT_EQ(store.TrackedResources(), 0u);
EXPECT_FALSE(store.HasShadow(b));
}
// T5's dirty mark: a GPU-side generation dirties the SERVER's shadow, and the mark - not the
// client - answers "dirty region, level has no pending upload". It is settable and clearable
// per (uploadTarget, level), independent of bytes, and inert on a monolith store.
TEST(StagedTextureStoreTest, TheGpuDirtyMarkIsTheServersOwnDirtyAnswer) {
Server::StagedTextureStore store(/*copies=*/true);
Server::StagedTextureStore monolithStore(/*copies=*/false);
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(7, 1));
EXPECT_FALSE(store.IsLevelGpuDirty(key, kTex2DTarget, 2));
store.NoteLevelDefined(key, kTex2DTarget, 2, IntVec3{2, 2, 1});
store.MarkLevelGpuDirty(key, kTex2DTarget, 2, true);
EXPECT_TRUE(store.IsLevelGpuDirty(key, kTex2DTarget, 2));
EXPECT_FALSE(store.IsLevelGpuDirty(key, kTex2DTarget, 3)) << "the mark is per level";
EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 2))
<< "a generated level holds no bytes; a texel read of it is the Fatal case";
store.MarkLevelGpuDirty(key, kTex2DTarget, 2, false);
EXPECT_FALSE(store.IsLevelGpuDirty(key, kTex2DTarget, 2));
monolithStore.MarkLevelGpuDirty(key, kTex2DTarget, 2, true);
EXPECT_FALSE(monolithStore.IsLevelGpuDirty(key, kTex2DTarget, 2));
EXPECT_EQ(monolithStore.TrackedResources(), 0u);
}
// The two key namespaces share one map, so their disjointness is a property to pin, not to
// assume: handle keys carry the top bit, twin addresses (user-space, aligned) never do.
TEST(StagedTextureStoreTest, HandleKeysAndTwinAddressKeysCannotCollide) {
const MG_Pipe::MGPipeHandle handle = TestHandle(7, 1);
const Uint64 handleKey = Server::StagedTextureStore::KeyForHandle(handle);
int twin = 0;
const Uint64 twinKey = Server::StagedTextureStore::KeyForTwinAddress(&twin);
EXPECT_NE(handleKey, twinKey);
EXPECT_NE(handleKey, Server::StagedTextureStore::KeyForHandle(TestHandle(7, 2)))
<< "a recycled slot's new generation must key a different entry";
}
// §1's derivation: max(1, base >> level) per SHRINKING axis, with an array texture's layer
// count fixed. This is the mip chain's definition, so the server and the client compute the
// same number - and the layer axes are exactly where a naive shift would diverge.
TEST(StagedTextureStoreTest, TheMipExtentDerivationKeepsArrayLayersFixed) {
EXPECT_EQ(Server::StagedTextureMipExtent(static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D), 8, 4, 1, 2),
IntVec3(2, 1, 1));
EXPECT_EQ(Server::StagedTextureMipExtent(static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex3D), 8, 8, 8, 3),
IntVec3(1, 1, 1));
EXPECT_EQ(
Server::StagedTextureMipExtent(static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2DArray), 8, 8, 6, 2),
IntVec3(2, 2, 6)) << "the layer count is not a dimension of the image";
EXPECT_EQ(
Server::StagedTextureMipExtent(static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex1DArray), 16, 4, 1, 3),
IntVec3(2, 4, 1)) << "a 1D array's HEIGHT is the layer count";
EXPECT_EQ(Server::StagedTextureMipExtent(static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::TexCube), 7, 7, 1, 3),
IntVec3(1, 1, 1)) << "shrinking clamps at 1, never 0";
}
// The Fatal, and it asserts ITS OWN failure string rather than "the process died" -
// ServerLoopTest.cpp:702-704's reason: a death test that only checks for a crash goes green
// on any other abort in the same body. ONE death per case: the forked children of two
// EXPECT_EXITs would share this process's log file, and the second child's truncated open
// would erase the first's line.
#if !defined(_WIN32)
TEST(StagedTextureStoreTest, ATexelReadOutsideTheStagedCoverageIsFatalByName) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(8, 1));
Vector<Uint8> bytes(16, 0x33);
store.Adopt(key, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes.data(), bytes.size());
// In coverage: no Fatal, asserted first so the death below cannot be a function that
// aborts on everything.
ASSERT_NE(store.RequireLevelBytes(key, kTex2DTarget, 0, "unit"), nullptr);
// The death MODE and the diagnostic, both pinned: KilledBySignal(SIGABRT) refuses a
// SIGSEGV, and the log grep names the exact wording. The log flush is pinned the way
// ServerLoopTest's is: Log.cpp's WriteToFile fflushes after every write and MGLOG_F logs
// before abort(), so the line is on disk in the forked child before it dies.
EXPECT_EXIT(store.RequireLevelBytes(key, kTex2DTarget, 5, "unit_undefined_level"),
::testing::KilledBySignal(SIGABRT), ".*");
const std::string log = ReadLog();
EXPECT_NE(log.find("Fatal{StageSnapshotTooNarrow, \"unit_undefined_level\"}"), std::string::npos)
<< "the abort happened but not for this rule's reason; the log says: " << log;
}
TEST(StagedTextureStoreTest, AGpuGeneratedLevelHasNoBytesAndItsTexelReadIsFatalByName) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(9, 1));
// Defined-without-bytes (T5's GPU-generated level): the level EXISTS, and a texel read of
// it is the same named refusal, because no byte answer exists on this side.
store.NoteLevelDefined(key, kTex2DTarget, 1, IntVec3{2, 2, 1});
ASSERT_TRUE(store.IsLevelDefined(key, kTex2DTarget, 1));
ASSERT_FALSE(store.IsCovered(key, kTex2DTarget, 1));
EXPECT_EXIT(store.RequireLevelBytes(key, kTex2DTarget, 1, "unit_gpu_level"),
::testing::KilledBySignal(SIGABRT), ".*");
const std::string log = ReadLog();
EXPECT_NE(log.find("Fatal{StageSnapshotTooNarrow, \"unit_gpu_level\"}"), std::string::npos)
<< "the abort happened but not for this rule's reason; the log says: " << log;
}
#endif
// =====================================================================================
// The production wiring - E-P5c gate #2 at unit scope
// =====================================================================================
// THE GATE. The REAL resource op table (RegisterBufferBackendOps installs g_glesResourceOps,
// the exact table the apply path dispatches through), a REAL applier record, and the REAL
// TextureSubData hook ApplyTextureUpload calls - then w1's poison. Reverting ANY link of the
// adoption - the ops-table registration, the hook call in PipeApply.cpp, or the copy inside
// Ops_H_TextureSubData (i.e. going back to P5's pointer-dropping) - turns this red, which is
// the R-16 red-once for "the server consumes the staged bytes" (E-P5c #2).
TEST(StagedTextureProductionTest, TextureSubDataThroughTheRealOpsTableCopiesAndSurvivesTheSourcePoison) {
// MG_Config::Transport is InProcess (main), so ServerStagedTexture() latches its copying
// arm on - the same latch ServerLoopTest's R-11 production case relies on.
MG_Backend::DirectGLES::BufferImpl::RegisterBufferBackendOps();
const MG_Pipe::MGPipeResourceOps* ops = MG_Pipe::MGPipeGetResourceOps();
ASSERT_NE(ops, nullptr) << "RegisterBufferBackendOps did not install the resource op table";
ASSERT_NE(ops->TextureSubData, nullptr)
<< "the texture half of resource_subdata has no adoption hook - the staged bytes are "
"dropped at apply time and the sync re-reads the client";
// A real applier record: the hook derives the level's extent from the record's
// descriptor, so the record must exist the way resource_create makes it.
const MG_Pipe::MGPipeHandle res = TestHandle(41, 1);
MG_Pipe::MGPResourceDesc desc{};
desc.Resource = res;
desc.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
desc.Width = 4;
desc.Height = 4;
desc.Depth = 1;
desc.Levels = 1;
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc));
Vector<Uint8> src(64, 0xAB); // 4x4 texels, 4 bytes each
MG_Pipe::MGPSubData rec{};
rec.Res = res;
rec.Target = MG_Pipe::MGPipePackSubDataTarget(
static_cast<Uint32>(MG_Pipe::MGPipeResourceTarget::Tex2D),
static_cast<Uint32>(TextureUploadTarget::Texture2D));
rec.Level = 0;
rec.UnionBox = {0, 0, 0, 4, 4, 1};
rec.RegionCount = 0;
// Under split the codec declares the run's length: "the bytes this record declares ARE
// the level shadow" (TextureEmit.h:1285). The adoption reads exactly this field.
rec.Blob.Size = src.size();
// THE PRODUCTION CALL. Not StagedTextureStore::Adopt directly - the whole point of the
// gate is that the OPS TABLE carries the bytes into the store.
ops->TextureSubData(res, rec, src.data(), nullptr);
auto& store = Server::ServerStagedTexture();
const Uint64 key = Server::StagedTextureStore::KeyForHandle(res);
ASSERT_TRUE(store.IsCovered(key, kTex2DTarget, 0))
<< "the hook ran but nothing was adopted - the sync will Fatal or re-read the client";
EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 0), IntVec3(4, 4, 1));
EXPECT_EQ(store.LevelByteSize(key, kTex2DTarget, 0), src.size());
const Uint8* base = store.RequireLevelBytes(key, kTex2DTarget, 0, "unit_production");
ASSERT_NE(base, nullptr);
EXPECT_NE(base, static_cast<const Uint8*>(src.data()))
<< "the sync's texel base points into the CLIENT's staging run - the pointer-dropping "
"shape P5 had; restoring it turns this red, and that is the gate";
// w1's retired-stage poison, by hand and at the right moment: the record has retired, so
// the staging run is dead. A server that copied still reads the original bytes.
std::fill(src.begin(), src.end(), Uint8{0xDD});
for (SizeT i = 0; i < 64; ++i) {
ASSERT_EQ(base[i], 0xAB) << "byte " << i << " of the sync's texel source is the poison, "
<< "so the adoption never happened";
}
// And the death drops the key, through the same ops table the applier dispatches.
MG_Pipe::MGPHandleOnly death{};
death.Handle = res;
death.Kind = static_cast<Uint32>(MG_Pipe::MGPipeKind::Texture);
MG_Pipe::MGPipeApplyResourceDestroy(death);
EXPECT_FALSE(store.HasShadow(key))
<< "a destroyed texture's staged levels must not answer for the slot's next owner";
}
int main(int argc, char** argv) {
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
// write, and caches the FILE*. The name carries this process's pid, because
// gtest_discover_tests runs every case as its own process, in parallel under ctest -j.
namespace fs = std::filesystem;
const fs::path path =
fs::temp_directory_path() / ("mobilegl-stagedtexture-test-" + std::to_string(ProcessId()) + ".log");
std::error_code ec;
fs::remove(path, ec);
g_logPath = path.string();
#if defined(_WIN32)
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
#else
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
#endif
// THIS PROCESS IS A SPLIT ONE - ServerLoopTest's main() ruling, and it matters twice here:
// ServerStagedTexture() latches its copying arm off MG_Config::Transport at first use,
// and a suite that left it at Monolith would be testing the monolith answers.
MG_Config::Transport = MG_Config::TransportMode::InProcess;
::testing::InitGoogleTest(&argc, argv);
const int rc = RUN_ALL_TESTS();
fs::remove(path, ec);
return rc;
}