[Fix] (Espryt, P9 W2): a texture re-mint never needs the client - read back only levels the driver holds, replay the staged level when the readback is refused, count trp on the server path, and retire the texture-remint-pull markers and OnTexturePullRequest (callbacks 9 -> 8)

This commit is contained in:
2026-09-29 03:23:54 -04:00
parent 738b29f3e4
commit 6c2f8a560c
16 changed files with 203 additions and 139 deletions
@@ -13232,9 +13232,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
//
// WHAT THE SKIP LOSES IS BOUNDED BY TWO FATALS, which is the whole reason it is allowed
// to be a skip rather than a port: a later glGetTexImage of the destination served from
// the shadow is class C wave 3 (Fatal{UnmigratedVerb, "GetTexImage"}, P9) and a texture
// re-mint that re-uploads the level is Fatal{UnmigratedEmulation, "texture-remint-pull"}
// (Managers.cpp:5634). Neither can silently read the un-mirrored shadow.
// the shadow is class C wave 3 (Fatal{UnmigratedVerb, "GetTexImage"}, P9), and a texture
// re-mint under a transport reads the GPU level itself (RequireImageBindableStorageByHandle,
// P9 W2) rather than any shadow. Neither can silently read the un-mirrored shadow.
//
// BEHIND #if MOBILEGL_BUILD_DISAGGREGATED so the pull build's code does not move (G1),
// and the arm is the TRANSPORT and not the build - build-split runs its unit and
@@ -14952,10 +14952,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Session::Fail entirely, which is the funnel the census gate watches.
//
// THE FUNNEL KEEPS ITS TEETH FOR THE OTHER SITES: MGPipeUnmigratedEmulation still
// aborts for generate-mipmap-storage, generate-mipmap-cpu-fallback,
// generate-mipmap-cpu-filter and texture-remint-pull, which really do reach into a
// client address space. Only this call site is retired, and only because its own
// premise did not hold.
// aborts for generate-mipmap-storage, generate-mipmap-cpu-fallback and
// generate-mipmap-cpu-filter, which really do reach into a client address space. Only
// this call site is retired here, and only because its own premise did not hold
// (texture-remint-pull went the same way in P9 W2, for the same reason).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (shadow == nullptr) {
MGLOG_E_ONCE("GetTexImage: no level shadow on this side for target=0x%x level=%d, and under an "
@@ -32,6 +32,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes);
// Whether the NATIVE texture currently holds `level` of `uploadTarget` at `logicalExtent`
// (glGetTexLevelParameteriv). False for a level the driver never allocated - one defined after
// the texture's last sync - so a re-mint does not read what is not there. A query the driver
// refuses answers true, keeping the caller on its readback path.
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
GLint level, const IntVec3& logicalExtent);
#endif
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
+80 -73
View File
@@ -6303,6 +6303,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_forceTextureParamsResync = true;
return;
}
// P9 W2 (notes/p9/W2-REMINT.md): EVERY BYTE THIS RE-MINT REPLAYS IS ALREADY ON THE
// SERVER. The GPU image holds what the GPU wrote (render targets, clears, copies,
// generated mips, image stores) and the staged store holds what the client uploaded;
// the client is never asked, so no pull protocol exists. Reading only the store would
// be wrong: a GPU-written level's store run is stale or absent - both host corpus
// re-mints (iris-photon, iris-derivative) are render targets with no staged bytes.
const Bool hadBackendStorage = m_isInitialized;
// The R-16 control for W2-b's fallback below: a readback the driver refuses cannot be
// produced on the host (llvmpipe renders every image format), so the test lane forces
// one. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs.
static const Bool s_forceReadbackFailure =
std::getenv("MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE") != nullptr;
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
const auto target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target);
@@ -6331,16 +6343,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool pendingWhole = pending && (pending->Regions.empty()
? coversWhole(pending->UnionBox)
: std::any_of(pending->Regions.begin(), pending->Regions.end(), coversWhole));
if (m_isInitialized && !(covered && pendingWhole)) {
// W2-a: ONLY A LEVEL THE DRIVER ACTUALLY HOLDS IS READ BACK. The store's
// Defined-ness follows the client's respecifies, the driver's follows this
// twin's last sync, and the two differ for a level defined (null data, or null
// data plus a partial upload) after that sync: the driver has no such level, so
// the GPU cannot have written it. A covered level then replays its staged run
// (the client's whole-level shadow, pending boxes included) and an uncovered one
// is undefined content with nothing to preserve. Reading it anyway was
// Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL.
const Bool nativeLevel =
m_isInitialized &&
NativeTextureLevelHasExtent(m_backendTextureId, target, uploadTarget,
static_cast<GLint>(level), extent);
if (nativeLevel && !(covered && pendingWhole)) {
Vector<Uint8> snapshot;
if (!ReadTextureLevelTight(m_backendTextureId, target, uploadTarget, format,
static_cast<GLint>(level), extent, /*sourceUsesImageCarrier=*/false,
canonicalFormat, canonicalType, snapshot)) {
const Bool readBack =
!s_forceReadbackFailure &&
ReadTextureLevelTight(m_backendTextureId, target, uploadTarget, format,
static_cast<GLint>(level), extent,
/*sourceUsesImageCarrier=*/false, canonicalFormat,
canonicalType, snapshot);
if (!readBack && !covered) {
// The GPU holds this level, the driver will not hand it back, and no
// CPU byte of it exists anywhere: the one case with no honest answer.
MGLOG_F("MGPipe: Fatal{ResourceUnavailable, \"image-promotion-readback\"} {%u,%u} level=%u",
res.Slot, res.Gen, level);
std::abort();
}
if (pending) {
if (!readBack) {
// W2-b: the driver refused the readback (a format it cannot attach
// to a framebuffer - SNORM without EXT_render_snorm, 16-bit norm
// without EXT_texture_norm16, float without EXT_color_buffer_float)
// but the store covers the level. Its staged run is exactly what the
// monolith re-mint replays; texels the GPU wrote since that upload
// are not preserved, which is monolith's answer too.
MGLOG_W_ONCE("MGPipe: image promotion of texture {%u,%u} level %u: the driver "
"refused the readback (internal format %u); replaying the level's "
"staged bytes instead (remint-readback-fallback)",
res.Slot, res.Gen, level, record.Desc.InternalFormat);
}
if (readBack && pending) {
// Native contents include prior draws/image
// writes; the pending stage owns ONLY its boxes.
// Merge those boxes before adopting a whole-level
@@ -6375,7 +6417,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
merge(box.X, box.Y, box.Z, box.W, box.H, box.D);
}
}
store.Adopt(key, targetCode, levelCode, extent, snapshot.data(), snapshot.size());
if (readBack) {
store.Adopt(key, targetCode, levelCode, extent, snapshot.data(), snapshot.size());
}
}
// Undefined contents have no byte run to preserve. A
// first allocation can still be image-bindable without
@@ -6389,6 +6433,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
}
// W2-d: `trp=` counts a re-mint of storage the backend ALREADY held, on this arm as
// on the frontend one (which counts in RequireImageBindableStorage). The keep path
// above is not a re-mint and returns before this; a first allocation holds nothing.
if (hadBackendStorage && MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1);
}
}
#else
(void)res;
@@ -6412,10 +6462,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
#if MOBILEGL_PIPE_PUSH
// Whether this transition re-mints storage that ALREADY EXISTED on the backend: that
// is the remint PULL (the levels below are replayed from the client's shadow to fill
// the new carrier), and it is what ROADMAP open question 2 counts. A texture reaching
// here uninitialised is allocated image-bindable up front and pulls nothing.
// Whether this transition re-mints storage that ALREADY EXISTED on the backend (the
// levels below are replayed from the client's shadow to fill the new carrier): what
// `trp=` counts. A texture reaching here uninitialised is allocated image-bindable up
// front and replays nothing.
const Bool hadBackendStorage = m_isInitialized;
#endif
m_imageBindableStorageRequired = true;
@@ -6431,28 +6481,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// glGetTexImage, a draw that samples it, an FBO attach - which is why it survived so
// long: the scenario that binds the image immediately after uploading never sees it.
//
// P4a (D-M): THIS RE-DIRTY IS AN EMULATION THAT CANNOT SURVIVE A SPLIT, and it is
// the head of ARCHITECTURE.md:299-308's one new stall class. The server is reaching
// BACK into the client's own dirty model to ask for texels it does not hold; under
// split there is no client address space to reach into and the levels have to be
// PULLED across the reverse channel. P4a supplies mitigation 1 - the prevention
// half, MGPResourceDesc::ImageBindableHint on every create and respecify, so a
// texture that has ever been image-bound is allocated in the carrier from the start
// and never reaches this path - and NAMES the site. Mitigations 2-4 (the async pull,
// the bounded retention and the ResourceSubDataComplete terminator) and
// TextureRemintPullScenario are P9's, and P4a must not build half a terminator.
// THIS IS THE MONOLITH ARM ONLY. Under a transport the re-mint is the server's own
// (RequireImageBindableStorageByHandle: the GPU level read back, the staged store for
// what the client uploaded), and nothing selects this function there - the image
// units sync from the record, the metadata respecify calls the by-handle overload, and
// a frontend object reaching the loop below would meet the layer-1 guard
// (Fatal{RoleViolation, "texture-legacy-arm"}) first. P9 W2 retired the
// `texture-remint-pull` marker this comment used to describe: no pull protocol exists
// because no byte of a re-mint has to come from the client (notes/p9/W2-REMINT.md).
//
// In monolith the code below keeps running exactly as it does today: the Fatal is a
// split-only arm and the monolith body of MGPipeUnmigratedEmulation is a no-op.
// KNOWN MONOLITH DEFECT, NOT FIXED HERE (W2-REMINT.md §6): replaying the client's shadow
// loses whatever the GPU wrote into the level since the upload - a render target, a
// clear, a generated mip - because the shadow never saw it. The split arm reads the GPU.
//
// THE MARKER IS RAISED WHERE A LEVEL IS ACTUALLY REPLAYED, not on entry (review
// N-4). ImageBindableHint IS the prevention half of mitigation 1, and D's own
// metadata-respecify arm makes the hint's ARRIVAL the trigger for entering this
// function - so a glBindImageTexture on a texture whose storage is not yet defined
// reaches here, finds no defined level, replays nothing, and would nevertheless have
// counted (and, under split, aborted at) the very emulation the hint exists to
// prevent. What the marker is about is stated in its own comment above: reaching
// BACK into the client's dirty model for texels. No texels owed, nothing to mark.
// THE COUNT IS TAKEN WHERE A LEVEL IS ACTUALLY REPLAYED, not on entry (review N-4): a
// glBindImageTexture on a texture whose storage is not yet defined reaches here, finds
// no defined level and replays nothing.
#if MOBILEGL_PIPE_PUSH
// P4a (review M-1): ON THE HANDLE ARM THE RE-DIRTY HAS TO REACH THE APPLIER'S SET,
// not only the frontend's model. The client cleared its own flags when it emitted
@@ -6465,7 +6509,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Both models are written, not one: the frontend's because the legacy arm reads it
// and because the client's own NoteLevelDirty hook rides on it, the applier's
// because that is what this arm consumes.
Bool markedRemintPull = false;
Bool countedRemint = false;
const MG_Pipe::MGPipeHandle rearmRes =
TextureResourceSubsystemEnabled()
? [&]() {
@@ -6485,41 +6529,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
"pending set - they would be allocated empty",
stateTextureObject->GetExternalIndex());
}
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (gt): under an active transport the replay loop below reaches the layer-1
// texture guard (MarkStorageDirty on a frontend object is Fatal{RoleViolation,
// "texture-legacy-arm"}) BEFORE the N-4 marker it owns - so the marker is hoisted
// here, ahead of the first guarded contact, with its semantics unchanged: raised
// only when a level would actually be replayed. The predicate is answered by the
// server's own staged-texture store rather than the frontend's extent walk: a
// Defined level has a non-zero extent and bytes on the frontend (the two conditions
// the loop tests), and an undefined one reads {0,0,0} and is skipped. A texture with
// no applier record (rearmRes null, the MGLOG_E_ONCE arm above) falls through to the
// guard, which names the same violation one level down.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Pipe::MGPipeHandleIsNull(rearmRes)) {
const auto* rearmRecord = PipeTextureRecordForHandle(rearmRes);
if (rearmRecord != nullptr) {
auto& rearmStore = MG_Remote::Server::ServerStagedTexture();
const Uint64 rearmKey = MG_Remote::Server::StagedTextureStore::KeyForHandle(rearmRes);
Bool anyLevelWouldReplay = false;
for (const auto& uploadTarget :
BufferImpl::StagedUploadTargetsForPipeTarget(rearmRecord->Desc.Target)) {
for (Uint32 level = 0; level < rearmRecord->Desc.Levels; ++level) {
if (rearmStore.IsLevelDefined(rearmKey, static_cast<Uint16>(uploadTarget),
static_cast<Uint16>(level))) {
anyLevelWouldReplay = true;
break;
}
}
if (anyLevelWouldReplay) break;
}
if (anyLevelWouldReplay) {
MG_Pipe::MGPipeUnmigratedEmulation("texture-remint-pull");
}
}
}
#endif
if (auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject.get())) {
const auto levelCount = mipmapObject->GetMipmapLevelCount();
@@ -6530,13 +6539,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (mipmapObject->GetMipmapByteSize(uploadTarget, level) == 0) continue;
mipmapObject->MarkStorageDirty(uploadTarget, level, true);
#if MOBILEGL_PIPE_PUSH
// N-4: one level owed is what makes this an unmigrated emulation. Once
// per transition, not once per level: the marker names the SITE.
if (!markedRemintPull) {
MG_Pipe::MGPipeUnmigratedEmulation("texture-remint-pull");
markedRemintPull = true;
// THE COUNTER BEHIND ROADMAP OPEN QUESTION 2 (final review M-A): one per
// transition that replays a level of storage the backend already held.
// THE COUNTER BEHIND ROADMAP OPEN QUESTION 2 (final review M-A): once per
// transition that replays a level of storage the backend already held,
// not once per level.
if (!countedRemint) {
countedRemint = true;
if (hadBackendStorage && MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1);
}
+4 -1
View File
@@ -1963,7 +1963,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// allocation, or capture the old native contents, merge pending
// upload regions and re-arm the server's whole-level replay set.
// First allocations use staged bytes directly. No frontend dirty
// flag, allocator or pixel-store object participates.
// flag, allocator or pixel-store object participates. P9 W2: a level
// the driver does not hold (defined after the last sync) is never read
// back, and a readback the driver refuses falls back to the staged
// level when the store covers it (notes/p9/W2-REMINT.md).
void RequireImageBindableStorageByHandle(MG_Pipe::MGPipeHandle res,
const MG_Pipe::MGPipeResourceRecord& record);
#endif
@@ -280,6 +280,44 @@
return true;
}
// P9 W2-a. The store's Defined-ness follows the client's respecifies; the driver's follows the
// twin's last sync. A re-mint asks this before reading a level back, because a level defined
// after that sync (null data, or null data plus a partial upload) is not on the driver yet and
// its FBO attachment is incomplete - which used to end the session at
// Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL.
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
GLint level, const IntVec3& logicalExtent) {
if (!texture || level < 0 || !g_GLESFuncs.glGetTexLevelParameteriv) return true;
const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent);
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
GLenum binding = GL_NONE;
switch (nativeTarget) {
case GL_TEXTURE_2D: binding = GL_TEXTURE_BINDING_2D; break;
case GL_TEXTURE_3D: binding = GL_TEXTURE_BINDING_3D; break;
case GL_TEXTURE_2D_ARRAY: binding = GL_TEXTURE_BINDING_2D_ARRAY; break;
case GL_TEXTURE_CUBE_MAP: binding = GL_TEXTURE_BINDING_CUBE_MAP; break;
case GL_TEXTURE_CUBE_MAP_ARRAY: binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break;
default: return true;
}
// A cube's levels are per face; every other target answers for the whole level.
const GLenum queryTarget = nativeTarget == GL_TEXTURE_CUBE_MAP
? TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget) : nativeTarget;
DrainESErrors();
GLint previous = 0;
g_GLESFuncs.glGetIntegerv(binding, &previous);
g_GLESFuncs.glBindTexture(nativeTarget, texture);
GLint width = 0, height = 0, depth = 0;
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_WIDTH, &width);
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_HEIGHT, &height);
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_DEPTH, &depth);
const Bool answered = g_GLESFuncs.glGetError() == GL_NO_ERROR;
g_GLESFuncs.glBindTexture(nativeTarget, static_cast<GLuint>(previous));
if (!answered) return true;
const Bool layered = nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY ||
nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
return width == extent.x() && height == extent.y() && (!layered || depth == extent.z());
}
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes) {
const auto* record = PipeTextureRecordForHandle(info.Res);
if (!record) return false;
+4 -5
View File
@@ -233,9 +233,9 @@ namespace MobileGL::MG_Pipe {
desc.StorageKind = MGPipeTextureStorageKindForTarget(texture.GetTarget());
desc.BindMask = bindMask;
// STICKY AND FOREVER: everImageBound, the client-side answer MGPipeTypes.h asks for.
// It is the PREVENTION half of the texture-remint stall class - a texture the server
// knows may be image-bound is allocated image-bindable up front, so the re-mint that
// would have to pull its texels back never happens.
// It PREVENTS the texture re-mint - a texture the server knows may be image-bound is
// allocated image-bindable up front, so the server never has to read its levels back
// and re-upload them into a new carrier (P9 W2).
desc.ImageBindableHint = (bindMask & kMGPipeBindShaderImage) != 0 ? 1 : 0;
if (storageDefined) {
const MGPipeTextureExtent extent = MGPipeTextureExtentOf(texture);
@@ -702,8 +702,7 @@ namespace MobileGL::MG_Pipe {
// mask rides resource_create and every resource_respecify - and an IMMUTABLE texture has
// no further respecify, that being what immutable means - so for the canonical order
// `glTexStorage2D(...); glBindImageTexture(...)` the applier's record kept
// ImageBindableHint = 0 for ever and the PREVENTION half of the texture-remint stall
// class never fired. So a mask that actually MOVES re-emits the stored descriptor with
// ImageBindableHint = 0 for ever and the re-mint PREVENTION never fired. So a mask that actually MOVES re-emits the stored descriptor with
// the new mask: every storage-defining field is byte-identical to what the applier
// holds, which is exactly the shape wire applies as a METADATA UPDATE - the descriptor
// is replaced, no reallocation is acked, and NO pending upload is dropped, so a mask
+17 -14
View File
@@ -15,12 +15,20 @@
// The backend -> frontend reverse channel, named (plan B section 7.1).
//
// Today this traffic is 95 call sites across 17 methods poked directly into frontend
// objects. gallium has no vocabulary for shadow writeback, GPU-write notification, texture
// re-send requests or default-framebuffer geometry, because in Mesa the state tracker and
// the driver share an address space. Naming them as eight callbacks plus one forward
// terminator (MGPipeContext::ResourceSubDataComplete) is the deliberate deviation (D8).
// objects. gallium has no vocabulary for shadow writeback, GPU-write notification or
// default-framebuffer geometry, because in Mesa the state tracker and the driver share an
// address space. Naming them as callbacks is the deliberate deviation (D8).
//
// EIGHT, NOT THE TEN PLAN B WROTE. The tenth was OnXfbScatterReady, and it went with the
// SEVEN, NOT THE TEN PLAN B WROTE. Three went, each because nothing produces it.
//
// OnTexturePullRequest, the server asking the client to re-send a texture's levels after
// re-minting it (with op 50 ResourceSubDataComplete as the forward terminator). P9 W2 measured
// that no re-mint needs the client: Espryt's server reads the GPU level back and replays its own
// staged store, Magma copies GPU to GPU, on every arm (notes/p9/W2-REMINT.md). So the callback
// went; op 50 stays in the opcode table as a retired row (opcodes only ever grow) and its decoder
// still declines it.
//
// OnXfbScatterReady went with the
// design it belonged to: plan B put the XFB scatter on the CLIENT (the server would hand
// back the packed scratch and the client would run the patch loop over its own shadow), so
// there had to be a callback that told the client the layout. P5c/P5f went the other way -
@@ -31,8 +39,8 @@
// reader of this file has to rule out by hand, and the size assertion below made it look
// load-bearing.
//
// The ninth was OnLog, deleted in P9 W3 for the same reason: zero producers, zero consumers,
// no EventKind. Plan B pictured the server's log lines as reverse-channel records; they never
// And OnLog, deleted in P9 W3 for the same reason: zero producers, zero consumers, no
// EventKind. Plan B pictured the server's log lines as reverse-channel records; they never
// were. They travel the CONTROL plane as LogLine frames (protocol.fbs), written by the server's
// logger through its log forward (MG_Remote/Transport/LogForward.h) - which is also where the
// severity policy this entry's comment promised ("<= WARN lossy, >= ERROR lossless and rate
@@ -50,11 +58,6 @@ namespace MobileGL::MG_Pipe {
void (*OnGpuWritten)(MGPipeHandle res, Uint rangeCount, const MGPRange* ranges);
void (*OnBufferWriteback)(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes);
void (*OnTextureWriteback)(MGPipeHandle res, const MGPBox* box, MGPBlobRef bytes);
// The one new stall class in this design (D-B6): the server recast a texture and
// needs its texels back. The client answers with zero or more ResourceSubData
// records terminated by ResourceSubDataComplete carrying the same pullSerial.
void (*OnTexturePullRequest)(MGPipeHandle res, Uint16 target, Uint16 firstLevel, Uint16 levelCount,
Uint64 pullSerial);
// SHAPE ONLY, never bytes: the client owns the CPU shadow and allocates the levels
// itself.
void (*OnMipLevelsGenerated)(MGPipeHandle res, Uint16 base, Uint16 count);
@@ -64,9 +67,9 @@ namespace MobileGL::MG_Pipe {
void (*OnCapsInvalidated)();
};
// Eight, and the count is asserted so a ninth cannot be added without touching the
// Seven, and the count is asserted so an eighth cannot be added without touching the
// transport's reverse-channel record table.
inline constexpr SizeT kMGPipeCallbackCount = 8;
inline constexpr SizeT kMGPipeCallbackCount = 7;
static_assert(sizeof(MGPipeCallbacks) == kMGPipeCallbackCount * sizeof(void (*)()),
"MGPipeCallbacks gained or lost a callback");
+2 -2
View File
@@ -1217,8 +1217,8 @@ namespace MobileGL::MG_Pipe {
// BindMask / ImageBindableHint bit reaches the applier only on a respecify, and an
// IMMUTABLE texture has no further one - that is what immutable means - so the canonical
// order (glTexStorage2D, then glBindImageTexture or an FBO attachment) would leave the
// record's hint at 0 for ever, and the hint is the PREVENTION half of the texture-remint
// stall class. So B re-emits the descriptor when the mask moves, and a record whose
// record's hint at 0 for ever, and the hint is what PREVENTS the texture re-mint (a
// server-side readback and re-upload, P9 W2). So B re-emits the descriptor when the mask moves, and a record whose
// STORAGE-DEFINING fields all equal the stored descriptor's is applied as a metadata
// update:
//
+4 -3
View File
@@ -430,15 +430,16 @@ namespace MobileGL::MG_Pipe {
// all, glBindImageTexture's state setter (TextureState.h, MG_State), which may include no
// emit header at all. So the note goes through this door, exactly as the birth hooks do.
// Nothing produced either bit before the fix round: ImageBindableHint was always 0, the
// metadata respecify (ID-18 M4) had no live trigger, and the remint pull the hint exists to
// metadata respecify (ID-18 M4) had no live trigger, and the re-mint the hint exists to
// prevent was neither prevented nor counted.
//
// UNCONDITIONAL IN A PUSH BUILD, like the mints: the mask is CLIENT state the framebuffer
// emitter ORs into whether or not the texture family is on, and the emission a mask move
// causes (the metadata respecify) is gated inside the emitter on the family's own pair.
void MGPipeNoteTextureBoundAs(MGPipeHandle texture, Uint32 bindBit);
// glBindImageTexture. The hint is the PREVENTION half of the texture-remint stall class -
// a texture the server knows may be image-bound is allocated image-bindable up front - so it
// glBindImageTexture. The hint PREVENTS the texture re-mint (a server-side readback and
// re-upload of every defined level, P9 W2) - a texture the server knows may be image-bound is
// allocated image-bindable up front - so it
// has to reach the applier before the texture's first sync, i.e. at the bind itself, not at
// the validate point's image walk (which notes it as well, D-A4's letter).
void MGPipeNoteTextureImageBound(MG_State::GLState::ITextureObject& texture);
+4 -2
View File
@@ -2323,8 +2323,10 @@ namespace MobileGL::MG_Remote::Wire {
}
case MGPWireOp::ResourceSubDataComplete:
// The forward terminator of a SERVER-initiated texture pull (section 7.1). There
// is no client producer and no applier; the reverse channel is P7/P9.
// RETIRED (P9 W2). It was to be the forward terminator of a SERVER-initiated texture
// pull (section 7.1), and no such pull exists: a re-mint reads the GPU level and the
// server's own staged store (notes/p9/W2-REMINT.md). The row stays because opcodes
// only ever grow; no client emits it and a record that claims it is declined.
return false;
case MGPWireOp::ResourceFlushRange:
@@ -35,9 +35,9 @@ namespace MobileGL::MG_State::GLState {
++Version;
#if MOBILEGL_PIPE_PUSH
// P4a D-A4 / final review M-A: the EARLIEST producer of kMGPipeBindShaderImage. The
// ImageBindableHint the bit feeds is the prevention half of the texture-remint stall
// class (a texture the server knows may be image-bound is allocated image-bindable
// up front), so it has to reach the applier before the texture's first sync - at
// ImageBindableHint the bit feeds prevents the texture re-mint (a texture the server
// knows may be image-bound is allocated image-bindable up front, P9 W2), so it has
// to reach the applier before the texture's first sync - at
// the bind, not at the next validate point's image walk. Push-only through the
// contract's door, like every other hook in this directory (G1).
if (Texture) MG_Pipe::MGPipeNoteTextureImageBound(*Texture);
+2 -2
View File
@@ -614,8 +614,8 @@ void main() { imageStore(img, ivec2(0), vec4(1.0)); }
// FINAL REVIEW M-A: glBindImageTexture IS THE EARLIEST PRODUCER OF kMGPipeBindShaderImage -
// the bit the ImageBindableHint is derived from - and the emitted image set's walk is D-A4's
// (any texture named in an emitted MGPImageView). The hint is the PREVENTION half of the
// texture-remint stall class: a texture the server knows may be image-bound is allocated
// (any texture named in an emitted MGPImageView). The hint PREVENTS the texture re-mint
// (P9 W2): a texture the server knows may be image-bound is allocated
// image-bindable up front, so it has to arrive before the first sync, i.e. at the bind.
// Nothing produced the bit before the fix round.
TEST(ImageEmit, AnImageBoundTextureIsMarkedShaderImageBoundAtTheBind) {
+16 -14
View File
@@ -1109,19 +1109,21 @@ TEST(PipeCatalogue, PipelineSubsetMembersArePinned) {
"ScissorTestEnabledMask");
}
// The reverse channel is exactly EIGHT callbacks: plan B's section 7.1 wrote ten, and the tenth
// The reverse channel is exactly SEVEN callbacks: plan B's section 7.1 wrote ten, and the tenth
// (OnXfbScatterReady) went with the design it belonged to when P5c/P5f moved the XFB scatter to
// the server's own staged shadow and its OnBufferWriteback return path. P3b/P4b espryt D1 slice 3
// deleted the declaration; this case is what makes the struct's shrink a measured fact rather
// than a claim, since the static_assert beside kMGPipeCallbackCount only proves the two agree.
// P9 W3 deleted the ninth, OnLog, which never had a producer either: the server's log lines are
// LogLine frames on the control plane (MG_Remote/Transport/LogForward.h), not reverse records.
// P9 took two more, neither of which ever had a producer: W2 deleted OnTexturePullRequest (a
// texture re-mint never needs the client's texels - the server reads the GPU level and its own
// staged store, notes/p9/W2-REMINT.md), and W3 deleted OnLog (the server's log lines are LogLine
// frames on the control plane, MG_Remote/Transport/LogForward.h, not reverse records).
//
// THE CASE NAME STAYS "…HasTenCallbacks" DELIBERATELY. G2/G14 say the ctest name set only ever
// grows, so renaming this would delete a name the gate is watching; the count it asserts is what
// has to be right, and the comment is where the number lives.
TEST(PipeCatalogue, ReverseChannelHasTenCallbacks) {
EXPECT_EQ(kMGPipeCallbackCount, 8u);
EXPECT_EQ(kMGPipeCallbackCount, 7u);
EXPECT_EQ(sizeof(MGPipeCallbacks), kMGPipeCallbackCount * sizeof(void (*)()));
}
@@ -1424,16 +1426,22 @@ TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) {
// say WHICH one was lost. The purity gate greps the count; this says what the count is of.
TEST(PipeCatalogue, EveryUnmigratedEmulationIsNamedOnce) {
// Every one of these is an emulation that reads or writes CLIENT memory a split server
// would not have: a CPU shadow mirror, a CPU mipmap fallback, a shadow-conversion readback,
// and the re-dirty of already-uploaded levels that a texture re-mint performs.
// would not have: a CPU shadow mirror, a CPU mipmap fallback and a shadow-conversion
// readback.
//
// FOUR, NOT FIVE (P9 W2). The fifth was "texture-remint-pull", the re-dirty of
// already-uploaded levels that RequireImageBindableStorage performs, and the head of the one
// new stall class the design admitted. It went because the stall class did: under a transport
// the re-mint is the server's own (RequireImageBindableStorageByHandle reads the GPU level and
// replays the staged store), the frontend arm that carried the marker is monolith-only, and no
// pull protocol or ResourceSubDataComplete terminator exists (notes/p9/W2-REMINT.md).
const char* const kNames[] = {
"copy-image-shadow-mirror", // the glCopyImageSubData CPU-shadow mirror
"generate-mipmap-storage", // EnsureGenerateMipmapStorageAllocated
"generate-mipmap-cpu-fallback", // GenerateThreeChannelFloatMipmapOnCpu
"get-tex-image-shadow", // GetTexImageViaShadowConversion
"texture-remint-pull", // RequireImageBindableStorage's re-dirty
};
EXPECT_EQ(std::size(kNames), 5u);
EXPECT_EQ(std::size(kNames), 4u);
// No duplicates: two sites sharing a name would make the grepped count and this list
// disagree in the one direction nobody would notice.
for (SizeT i = 0; i < std::size(kNames); ++i) {
@@ -1441,12 +1449,6 @@ TEST(PipeCatalogue, EveryUnmigratedEmulationIsNamedOnce) {
EXPECT_STRNE(kNames[i], kNames[j]);
}
}
// The last one is the head of the only NEW stall class the design admits, and P4a supplies
// exactly one of its four mitigations - prevention, through ImageBindableHint on every
// create and respecify. The async pull, the bounded retention and the
// ResourceSubDataComplete terminator are a later phase's, and P4a must not build half a
// terminator.
EXPECT_STREQ(kNames[4], "texture-remint-pull");
#if MOBILEGL_PIPE_PUSH
// In monolith it really is a no-op: calling it changes nothing and returns nothing. The
// teeth are a split server's, and the call site is what P8 gives them to.
+3 -3
View File
@@ -433,8 +433,8 @@ TEST(TextureEmit, AnImageBoundTextureCarriesTheImageBindableHintForever) {
EXPECT_EQ(Textures().LastDesc().ImageBindableHint, 0);
Textures().NoteTextureBoundAs(handle, kMGPipeBindShaderImage);
// The next respecify carries the hint - and it is the PREVENTION half of the texture-remint
// stall class, so it must never go back to 0 afterwards.
// The next respecify carries the hint - and it is what PREVENTS the texture re-mint, so it
// must never go back to 0 afterwards.
texture->AllocateStorage(TextureUploadTarget::Texture2D, 1, MipmapInput{IntVec3{4, 4, 1}, 4 * 4 * 4});
EXPECT_EQ(Textures().LastDesc().ImageBindableHint, 1);
texture->AllocateStorage(TextureUploadTarget::Texture2D, 2, MipmapInput{IntVec3{2, 2, 1}, 2 * 2 * 4});
@@ -1348,7 +1348,7 @@ TEST(TextureEmit, EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayL
// THE CANONICAL ORDER FOR THE TEXTURES THE HINT WAS WRITTEN FOR: glTexStorage2D, then
// glBindImageTexture. An IMMUTABLE texture has no further respecify - that is what immutable
// means - so before this the applier's record kept ImageBindableHint = 0 for ever and the
// PREVENTION half of the texture-remint stall class was a no-op for exactly its own target.
// texture re-mint PREVENTION was a no-op for exactly its own target.
TEST(TextureEmit, AnImmutableTexturesImageBindableHintReachesTheApplierAfterItsAllocation) {
TextureScope scope;
const auto texture = MakeTexture2D(18, 32);
+4 -3
View File
@@ -609,9 +609,10 @@ namespace MobileGL::MG_Util::PipeStats {
line += " sie=" + std::to_string(calls[static_cast<Uint32>(CallClass::ShaderImageEmissions)]);
line += " ctu=" +
std::to_string(calls[static_cast<Uint32>(CallClass::ClientTextureUploadEmissions)]);
// trp is the texture-remint pull count (ROADMAP open question 2): every one is a texture
// Espryt had already allocated and then had to re-mint image-bindable, replaying its
// levels from the client's shadow, because ImageBindableHint reached it too late.
// trp is the texture re-mint count (ROADMAP open question 2): every one is a texture
// Espryt had already allocated and then had to re-mint image-bindable because
// ImageBindableHint reached it too late - replaying the client's shadow in monolith, the
// GPU level and the staged store on the server under a transport (P9 W2; no pull).
line += " trp=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureRemintPulls)]);
// rsd is P7's resident sub-data emission count (OQ-10): one per buffer_subdata_resident
// record (opcode 49) the client put on the wire. It is the ONLY observable that
+9 -7
View File
@@ -182,13 +182,15 @@ namespace MobileGL::MG_Util::PipeStats {
// hides is ~+6 ms/frame, so an emission-shape divergence has to be a difference of two
// numbers rather than something only a GPU can see.
ClientTextureUploadEmissions,
// THE TEXTURE-REMINT PULL RATE (ROADMAP open question 2; P4a final review M-A). Counted
// by Espryt once per transition in which a texture that ALREADY HAD backend storage is
// re-minted image-bindable and its defined levels are replayed from the client's shadow
// (RequireImageBindableStorage) - the reach-back a split cannot make (D-M) and the one
// ImageBindableHint exists to prevent. A texture whose hint arrived before its first
// sync is allocated image-bindable up front and never counts. `trp=` on the summary
// line; the number that decides MOBILEGL_PIPE_TEXEL_RETAIN_MB's default.
// THE TEXTURE RE-MINT RATE (ROADMAP open question 2; P4a final review M-A). Counted by
// Espryt once per transition in which a texture that ALREADY HAD backend storage is
// re-minted image-bindable - on the monolith arm by RequireImageBindableStorage (levels
// replayed from the client's shadow), under a transport by
// RequireImageBindableStorageByHandle (the GPU level read back, the staged store for
// pending boxes; an immutable core-format texture it keeps as-is does not count). The
// name still says "pull": P9 W2 found no re-mint needs the client, so none is pulled,
// and the name stays because it is printed and peeked (`trp=`). A texture whose hint
// arrived before its first sync is allocated image-bindable up front and never counts.
TextureRemintPulls,
// P7 wave 2 package C, OQ-10 (CONTRACT-P7 §5.4). `rsd=` - THE RESIDENT SUB-DATA
// EMISSION COUNT: one per `buffer_subdata_resident` record (opcode 49) the CLIENT