mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 09:08:31 +09:00
[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:
@@ -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.");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -80,11 +80,14 @@ gtest_discover_tests(PipeWireCodecTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS un
|
||||
# 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;
|
||||
}
|
||||
Reference in New Issue
Block a user