From 98ea16042a52fea68317adc97a40cb457f6ebdd6 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 11:38:28 -0400 Subject: [PATCH 1/2] [MG_Backend] (Disaggregated): P5e mv - the multi-draw path stops reading the frontend VAO - MultiDraw.cpp asked the bound element array buffer three questions (is one bound, what GL name is bound and how big is its store, give me its bytes on the CPU) and asked all three of MGB_CTX->GetBoundVertexArray() on the apply thread. That was the last unguarded read of that field in the backend: package vi retired the ordinary draw path's copy and this one survived only because these 9 entries used to die earlier, on the framebuffer row fb has since retired. - ResolveBoundIndexBuffer(question, entry) answers all three from MGPipeApplier().IndexBuffer.Res on the record arm - EnsureBufferResourceForHandle for the driver name, ResourceWidthForHandle for the size, the server's staged shadow (hostBytes + RequireStagedCoverage) for the CPU bytes - and keeps the monolith body token for token below it. No wire row was added. - The arm is STATED, not inferred (ID-81 / ID-110): BufferImpl::VertexInputReadsRecords(), the family's own selector, which reduces to Transport != Monolith AND the vertex-input bit. Nothing here decides an arm from a null pointer or a noted handle. - A missing record on the handle arm is a named refusal: Fatal{RoleViolation, multidraw-index-buffer-arm}, the shape of RefuseNullFrontendTextureOffTheHandleArm. The frontend element slot is not a second opinion on this side - it answers for whatever the client has bound now. - VertexInputReadsRecords() moved from DirectGLES.cpp to Managers.h, unchanged, so the two translation units on this draw path share ONE selector rather than a copy each. - Strict census: GetBoundVertexArray@DrawArrays 9 -> 0. The same 9 entries now stop on GetProgramForDraw@DrawArrays, which is pa's row; no other row moved, no new row. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 29 +-- MobileGL/MG_Backend/DirectGLES/Managers.h | 32 +++ MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp | 212 +++++++++++++++--- 3 files changed, 211 insertions(+), 62 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 1cc58a8c..0c25b5e1 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -522,31 +522,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // TODO: deletion for deleted objects namespace BufferImpl { - // P5e (vi), CONTRACT-P5E §5.1 + §5.8 (ruling 1 / ID-81): THE ARM SELECTOR for this - // family, and it is a conjunction on purpose. - // - // Transport != Monolith the record arm exists because there is no frontend VAO on - // this side of a real split. Under Transport=monolith the - // push build keeps its frontend arms token for token - that - // is what the verify comparator compares against, and what - // makes MOBILEGL_IPC_RUN_AHEAD=0 a pure wait-rule A/B on - // identical server code rather than an arm swap. - // the family bit `0x0ff` (bit 7 on, bit 8 off) is a supported A/B and must - // keep running the legacy vertex-input walk; the bit is - // already the gate the rest of this family reads. - // - // It is NOT gated on run-ahead. The records carry the whole family either way, so a - // lockstep split session reads them too and the wait rule changes nothing here - which - // is the only reason ra can flip one constant at the end of the phase and change no - // backend code at all. - inline Bool VertexInputReadsRecords() { -#if MOBILEGL_BUILD_DISAGGREGATED - return MG_Config::Transport != MG_Config::TransportMode::Monolith && - VertexInputSubsystemEnabled(); -#else - return false; -#endif - } + // P5e (mv): THE ARM SELECTOR of this family - VertexInputReadsRecords() - moved to + // Managers.h, unchanged. MultiDraw.cpp is a second translation unit on the same draw + // path and has to select the same arm; a copy of the conjunction there would be two + // selectors for one family. Its rationale travelled with it. #if MOBILEGL_BUILD_DISAGGREGATED // P5e (vi): "does this draw fetch any attribute out of the application's own memory", diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 8105d415..60ce40db 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -1094,6 +1094,38 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint CurrentBufferContextGeneration(); #endif + // P5e (vi), CONTRACT-P5E §5.1 + §5.8 (ruling 1 / ID-81): THE ARM SELECTOR for this + // family, and it is a conjunction on purpose. + // + // Transport != Monolith the record arm exists because there is no frontend VAO on + // this side of a real split. Under Transport=monolith the + // push build keeps its frontend arms token for token - that + // is what the verify comparator compares against, and what + // makes MOBILEGL_IPC_RUN_AHEAD=0 a pure wait-rule A/B on + // identical server code rather than an arm swap. + // the family bit `0x0ff` (bit 7 on, bit 8 off) is a supported A/B and must + // keep running the legacy vertex-input walk; the bit is + // already the gate the rest of this family reads. + // + // It is NOT gated on run-ahead. The records carry the whole family either way, so a + // lockstep split session reads them too and the wait rule changes nothing here - which + // is the only reason ra can flip one constant at the end of the phase and change no + // backend code at all. + // + // P5e (mv): it lives in the HEADER rather than in DirectGLES.cpp because the multi-draw + // path is a second translation unit that has to select the SAME arm - and the addendum's + // rule is that a site states the transport test it relies on, which a copy of the + // conjunction in MultiDraw.cpp would satisfy in letter while giving the family two + // selectors that can drift apart. One definition, spelled at every site that reads it. + inline Bool VertexInputReadsRecords() { +#if MOBILEGL_BUILD_DISAGGREGATED + return MG_Config::Transport != MG_Config::TransportMode::Monolith && + VertexInputSubsystemEnabled(); +#else + return false; +#endif + } + // Registered as the frontend's BufferBackendOps at backend init and on // every MakeCurrent (the ES context can be destroyed and recreated, e.g. // by the trace replayer's probe context). diff --git a/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp b/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp index 7c993c75..7229ec79 100644 --- a/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp +++ b/MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -91,11 +92,151 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { return resource ? resource->id : 0; } - const SharedPtr& BoundIndexBuffer() { - static const SharedPtr none; + // --------------------------------------------------------------------------- + // The bound index buffer, and WHICH SIDE ANSWERS FOR IT + // + // P5e (mv), CONTRACT-P5E §5.1 + §5.8 (ruling ID-81): this file asks the bound element + // array buffer exactly three questions - "is one bound at all", "what GL name did + // PrepareForDraw leave on GL_ELEMENT_ARRAY_BUFFER (and how big is its store)", and + // "give me its bytes on the CPU" - and until now it asked all three of the FRONTEND + // VAO, on the apply thread, once per indexed multi-draw. That was the last unguarded + // `MGB_CTX->GetBoundVertexArray()` in the backend: package vi retired the ordinary + // draw path's copy (DirectGLES.cpp's SyncCurrentVertexAttributeValues / PrepareForDraw) + // and this one survived only because these batches used to die earlier, on the + // framebuffer row fb has since retired. + // + // THE ARM IS STATED, NEVER INFERRED. BufferImpl::VertexInputReadsRecords() is the + // family's own selector and reduces to `Transport != Monolith && the vertex-input bit`; + // it is the same conjunction RefuseElementArrayBufferFromTheFrontend guards the + // single-draw path with. Nothing below decides an arm by noticing that a handle or a + // pointer happens to be null - conflating "a handle was noted" with "the record arm is + // selected" is what ID-107 cost this phase 137 scenarios. + // --------------------------------------------------------------------------- + + // How much of the index buffer a caller needs. DELIBERATELY not nested levels: each + // value is exactly the work its original call site did, in its original order, so the + // monolith arm makes the same calls in the same sequence it made before this package. + enum class IndexBufferQuestion { + Presence, // is an element array buffer bound at all + DriverName, // + the GL name currently bound to GL_ELEMENT_ARRAY_BUFFER + DriverNameAndSize,// + the store's size in bytes (the compute tier's source) + HostBytes, // a CPU-readable copy of the whole store, and its size + }; + + struct BoundIndexBufferView { + Bool Present = false; + Uint Id = 0; // DriverName*: 0 when there is no backend store yet + SizeT Size = 0; // DriverNameAndSize / HostBytes + const Uint8* HostBytes = nullptr; // HostBytes: null when there is no CPU copy + }; + +#if MOBILEGL_BUILD_DISAGGREGATED + // A missing record on the handle arm is a NAMED refusal and never a quiet fall-back to + // the frontend (TASK-mv requirement 2; the shape is Managers.cpp's + // RefuseNullFrontendTextureOffTheHandleArm). The frontend element slot is not a second + // opinion on this side: it answers for whatever the CLIENT has bound NOW, which is a + // later draw than the one being applied. Drawing a batch from it would be a silently + // wrong picture, which is precisely what MultiDrawScenario's batch-matches-unrolled + // assertions exist to catch and what a fall-back would hide from them. + [[noreturn]] void RefuseMissingIndexBufferRecord(const char* entry, MG_Pipe::MGPipeHandle res, + const char* missing) { + MGLOG_F("MGPipe: Fatal{RoleViolation, \"multidraw-index-buffer-arm\"} - %s found no %s for " + "the index buffer {%u, %u} this batch's record named. The index buffer of this " + "family is MGPipeApplier().IndexBuffer.Res (CONTRACT-P5E §5.1) and the arm is " + "selected by Transport != Monolith AND the vertex-input subsystem bit (§5.8, " + "ID-81); falling back to the frontend VAO's element slot here would draw this " + "multi-draw from whatever the CLIENT has bound now", + entry, missing, res.Slot, res.Gen); + std::abort(); + } + + // Does the applier's record say the application SUPPLIED this store's content? The M-3 + // rule ScopedRestartIndexSubstitution spells: for a supplied store a coverage gap in the + // server shadow is a missing record and Fatal by name; for one the application ORPHANED + // the gap is its own undefined content, and reading the shadow's zero-fill is exactly + // what the monolith arm's MappedData() hands back. + Bool IndexBufferRecordHasDefinedContent(const MG_Pipe::MGPipeApplierState& st, + MG_Pipe::MGPipeHandle res) { + if (res.Slot >= st.Resources.size()) return false; + const auto& candidate = st.Resources[res.Slot]; + if (!candidate.Live || candidate.Gen != res.Gen) return false; + return candidate.Desc.HasDefinedContent != 0; + } +#endif + + BoundIndexBufferView ResolveBoundIndexBuffer(IndexBufferQuestion question, const char* entry) { + BoundIndexBufferView view; +#if MOBILEGL_BUILD_DISAGGREGATED + if (BufferImpl::VertexInputReadsRecords()) { + const auto& st = MG_Pipe::MGPipeApplier(); + const MG_Pipe::MGPipeHandle res = BufferImpl::ResolveDrawIndexBufferFromRecord(st).Res; + // A null Res is "no element array buffer bound", exactly as a null frontend slot + // is on the arm below - the batch's indices are a client array, and every caller + // already has an arm for that. It is NOT the arm test; the arm was decided above. + if (MG_Pipe::MGPipeHandleIsNull(res)) return view; + view.Present = true; + if (question == IndexBufferQuestion::Presence) return view; + + if (question == IndexBufferQuestion::HostBytes) { + // No Sync* pair here and none is missing: on this side the applier has + // already consumed the persistent-map blocks and the shader writebacks for + // this resource before the draw verb replayed, so the server's staged shadow + // IS the synced copy. That is also why this arm cannot be expressed as "find + // the object and run the monolith body". + auto* resource = BufferImpl::FindBufferResourceForHandle(res); + if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource"); + view.Size = BufferImpl::ResourceWidthForHandle(res); + view.HostBytes = resource->hostBytes; + if (view.HostBytes != nullptr && IndexBufferRecordHasDefinedContent(st, res)) { + BufferImpl::RequireStagedCoverage(*resource, view.HostBytes, 0, view.Size, + "multidraw_index_rebase"); + } + // A null HostBytes is not a refusal: MappedData() on the monolith arm is + // equally allowed to be null (an adopted coherent map keeps its bytes + // elsewhere), and the one caller that asks declines the tier for it. + return view; + } + + auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res); + if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource"); + view.Id = resource->id; + if (question == IndexBufferQuestion::DriverNameAndSize) { + view.Size = BufferImpl::ResourceWidthForHandle(res); + } + return view; + } +#endif + // MONOLITH GLUE from here down, token for token what each call site did before. const auto& vao = MGB_CTX->GetBoundVertexArray(); - if (!vao) return none; - return vao->GetIndexBufferBindingSlot().GetBoundObject(); + if (!vao) return view; + const auto& ibo = vao->GetIndexBufferBindingSlot().GetBoundObject(); + if (!ibo) return view; + view.Present = true; + switch (question) { + case IndexBufferQuestion::Presence: + break; + case IndexBufferQuestion::DriverName: { + const auto* resource = BufferImpl::EnsureBufferResource(ibo); + view.Id = resource ? resource->id : 0; + break; + } + case IndexBufferQuestion::DriverNameAndSize: { + const auto* resource = BufferImpl::EnsureBufferResource(ibo); + view.Id = resource ? resource->id : 0; + view.Size = ibo->GetSize(); + break; + } + case IndexBufferQuestion::HostBytes: + // The shadow is the source of truth for CPU reads, but a persistent map or + // a shader write may have moved past it since the last sync. + ibo->SyncPersistentMappedRange(); + ibo->SyncGpuWrites(); + view.HostBytes = ibo->MappedData(); + view.Size = ibo->GetSize(); + break; + } + (void)entry; + return view; } // The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what a tier @@ -103,10 +244,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { // matters beyond tidiness: the VAO twin memoises that it already synced this // index binding and will not re-issue it on the next draw. Uint BoundIndexBufferId() { - const auto& ibo = BoundIndexBuffer(); - if (!ibo) return 0; - const auto* resource = BufferImpl::EnsureBufferResource(ibo); - return resource ? resource->id : 0; + return ResolveBoundIndexBuffer(IndexBufferQuestion::DriverName, "BoundIndexBufferId").Id; } // --------------------------------------------------------------------------- @@ -361,13 +499,18 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { } } - // CPU-readable bytes of one sub-draw's indices, from the frontend shadow of the - // bound index buffer or straight from the client array. Null when the sub-draw - // would read outside the buffer. - const Uint8* ResolveSubDrawIndices(const SharedPtr& indexBuffer, - const Uint8* indexBufferBytes, SizeT indexBufferSize, const void* indices, - SizeT indexCount, SizeT indexSize) { - if (!indexBuffer) { + // CPU-readable bytes of one sub-draw's indices, from the CPU copy of the bound index + // buffer (the frontend shadow on the monolith arm, the server's staged shadow on the + // record arm) or straight from the client array. Null when the sub-draw would read + // outside the buffer. + // + // P5e (mv): `hasIndexBuffer` is a Bool and not the frontend object it used to be, + // because presence is the only thing this function ever asked of it - and on the record + // arm there is no such object on this side to hand it. + const Uint8* ResolveSubDrawIndices(Bool hasIndexBuffer, const Uint8* indexBufferBytes, + SizeT indexBufferSize, const void* indices, SizeT indexCount, + SizeT indexSize) { + if (!hasIndexBuffer) { return static_cast(indices); } if (!indexBufferBytes) return nullptr; @@ -410,8 +553,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { if (indexSize == 0) return false; // Indirect commands address indices as an element offset into the bound element // array buffer, and an indirect draw is not defined without one. - const auto& indexBuffer = BoundIndexBuffer(); - if (!indexBuffer) return false; + if (!ResolveBoundIndexBuffer(IndexBufferQuestion::Presence, "RunIndirect").Present) return false; g_commandStaging.resize(static_cast(drawcount)); for (GLsizei i = 0; i < drawcount; ++i) { @@ -502,17 +644,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { if (total == 0) return true; if (total > kMaxFlattenedIndices) return false; - const auto& indexBuffer = BoundIndexBuffer(); - const Uint8* indexBufferBytes = nullptr; - SizeT indexBufferSize = 0; - if (indexBuffer) { - // The shadow is the source of truth for CPU reads, but a persistent map or - // a shader write may have moved past it since the last sync. - indexBuffer->SyncPersistentMappedRange(); - indexBuffer->SyncGpuWrites(); - indexBufferBytes = indexBuffer->MappedData(); - indexBufferSize = indexBuffer->GetSize(); - } + const BoundIndexBufferView indexBuffer = + ResolveBoundIndexBuffer(IndexBufferQuestion::HostBytes, "RunRebasedDrawElements"); + const Uint8* const indexBufferBytes = indexBuffer.HostBytes; + const SizeT indexBufferSize = indexBuffer.Size; const Bool restartActive = RestartActive(); const Uint32 restartSentinel = RestartSentinelFor(type); @@ -531,8 +666,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl { for (GLsizei i = 0; i < drawcount; ++i) { if (count[i] <= 0) continue; const SizeT subDrawCount = static_cast(count[i]); - const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i], - subDrawCount, indexSize); + const Uint8* source = ResolveSubDrawIndices(indexBuffer.Present, indexBufferBytes, indexBufferSize, + indices[i], subDrawCount, indexSize); if (!source) { MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index " "buffer; skipping the batch", @@ -714,16 +849,18 @@ void main() { // The shader reads the source indices as a storage buffer, so there has to be // a real buffer to read - a client-memory index array has none. - const auto& indexBuffer = BoundIndexBuffer(); - if (!indexBuffer) return; + if (!ResolveBoundIndexBuffer(IndexBufferQuestion::Presence, "FlattenWithCompute").Present) return; // A dispatch inside an open capture span is not legal, and the span would also // observe one merged draw rather than the batch it asked for. if (XfbImpl::IsCaptureSpanOpen()) return; - auto* sourceResource = BufferImpl::EnsureBufferResource(indexBuffer); - if (!sourceResource || sourceResource->id == 0) return; - const SizeT sourceSize = indexBuffer->GetSize(); + // Asked a second time, and one question later: the ensure below may do GL work, so + // it stays BEHIND the capture-span check exactly as it was before P5e (mv). + const BoundIndexBufferView source = + ResolveBoundIndexBuffer(IndexBufferQuestion::DriverNameAndSize, "FlattenWithCompute"); + if (source.Id == 0) return; + const SizeT sourceSize = source.Size; // std430 addresses the source as uint[]; a tail shorter than a word is not // reachable, so a narrow index type needs a word-multiple buffer. if (indexSize < 4 && (sourceSize % 4) != 0) return; @@ -762,7 +899,7 @@ void main() { return; } - BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id); + BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, source.Id); BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id); BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 2, g_flattenedIndices.id); @@ -906,7 +1043,8 @@ void main() { const Bool arbitraryRestart = restartKind == RestartSubstitutionKind::RewriteIndices; const ScopedSuppressedPrimitiveRestart restartCapOverride(restartKind); - const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr; + const Bool hasIndexBuffer = + ResolveBoundIndexBuffer(IndexBufferQuestion::Presence, "DrawElementsBatch").Present; // The compute tier dispatches BEFORE the draw state is established: doing it // afterwards would mean unpicking the program, SSBO and index bindings From 5c777b49e8d11a15acf1b6dda03d0ed96bc0ab74 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 11:59:28 -0400 Subject: [PATCH 2/2] [MG_Test] (Disaggregated): P5e mv - the multi-draw tiers get gated entries of their own - The resolved multi-draw tier is ONE process-wide read of MOBILEGL_ESPRYT_MULTIDRAW_MODE (MultiDraw.cpp's ResolveTierOnce) and the d1 block leaves it at "auto", which on this driver lands on "ext" - the one tier that asks the bound index buffer nothing. Three of the five sites the previous commit migrated therefore had no entry in the set the gate runs: ID-107's shape one level down, with the env knob in place of MG_Config::Transport. - Five blocks, one per tier "auto" cannot reach (basevertex, multiindirect, indirect, drawelements, compute), prefix DirectGLES.Split.MultiDrawTier., both labels, and MOBILEGL_ESPRYT_MULTIDRAW_MODE pinned as a ctest ENVIRONMENT property rather than a job-level export - review finding N-6's reason, and the PushMonolithArm block's precedent. - "ext" is deliberately NOT registered: the d1 block already IS the ext lane. Every other tier was confirmed to resolve to ITSELF here rather than falling back to ext, so no entry in this set is a green that silently means "ext again". - The filter is the tier-sensitive workload: MultiDrawScenario (minus the client-indices case d1 already excludes by name) plus IndexedDrawFamilyScenario's three MultiDraw cases. 12 entries per tier, 60 in total. - SplitLogPaths.cmake.in gives each of them a private log path. That is the rule every DirectGLES.Split. entry follows (SplitLogPaths.PrivateAndDistinct), and for these it also repairs the census blind spot: a third name segment means strict_census2.sh would re-run them without the very pin under test, so the marker is read where the lane wrote it. - integration-split 116 -> 176, integration-gpu 1294 -> 1354, both fully green. Under the strict lane all 60 stop on GetProgramForDraw@DrawArrays, which is pa's row: PrepareForDraw runs before the tier dispatch, so no new marker class appears and mv's row stays at 0. --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 73 +++++++++++++++++++ .../Harness/SplitLogPaths.cmake.in | 11 +++ 2 files changed, 84 insertions(+) diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index f72d467a..23e13c74 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -2014,6 +2014,79 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ) endforeach() + # ---- P5e mv: THE MULTI-DRAW TIERS, INSIDE THE GATED SET (ID-109's rule, one level down) -- + # + # WHY FIVE MORE LANES FOR ONE FILE. MultiDrawImpl asks the bound index buffer three questions + # and P5e (mv) moved all three onto the applier's record. Five of this driver's six tiers ask + # them from a DIFFERENT place in the batch - `RunIndirect` before any GL work, the rebased + # tier from the CPU shadow, `FlattenWithCompute` before PrepareForDraw has even run - and the + # resolved tier is ONE process-wide read of MOBILEGL_ESPRYT_MULTIDRAW_MODE (MultiDraw.cpp's + # ResolveTierOnce). The d1 block above leaves it at `auto`, and on every driver this gate has + # run against, `auto` lands on `ext` - a real multi-draw entry point that asks the index + # buffer nothing at all. Three of the five migrated sites therefore had no gated entry. + # + # That is ID-107's shape exactly one level down: code on a real arm, with no entry in the set + # the gate runs. The phase already paid 137 SEGFAULTs across 38 scenarios for the version of + # this that lived at the transport level, and the lesson ID-109 drew - "the gate runs the SET, + # so put the second arm in the set" - does not stop being true because the selector is an env + # knob rather than MG_Config::Transport. A manual sweep run once by one package decays to + # nothing by the next one. + # + # MOBILEGL_ESPRYT_MULTIDRAW_MODE IS A ctest ENVIRONMENT PROPERTY, not a job-level export, for + # review finding N-6's reason and the PushMonolithArm block's: the property overrides the job + # environment, so a gate that exported a mode of its own could not silently collapse these + # five lanes into five more copies of the `auto` one - the single failure mode a lane whose + # whole purpose is to pin a tier cannot survive. + # + # `ext` IS NOT REGISTERED HERE, deliberately: it is what `auto` already resolves to, so the + # d1 block above IS the ext lane and a sixth block would only run it twice. Every other tier + # was confirmed to resolve to ITSELF on this driver rather than falling back + # (`ResolveTier`'s "MOBILEGL_ESPRYT_MULTIDRAW_MODE= -> (driver supports: ext, + # basevertex, multiindirect, indirect, drawelements, compute (opt-in))"); a tier the driver + # refuses would resolve to `ext` and register an entry that quietly proves nothing, so if this + # list ever runs somewhere poorer, drop the refused tier rather than keeping a green that + # means "ext again". + # + # THE FILTER IS THE TIER-SENSITIVE WORKLOAD and nothing else, which is this file's own rule + # for an armed Split entry: MultiDrawScenario (its whole point is that a batch matches the + # unrolled draws, which is precisely what a wrong index buffer breaks quietly) and + # IndexedDrawFamilyScenario's three MultiDraw cases. The client-side-indices case keeps the + # d1 block's exclusion - it is refused by name under split before any tier is chosen. + # + # NOTE FOR THE STRICT-MARKER CENSUS (PREAMBLE-ADDENDUM-WAVE3 §4): these entries carry a THIRD + # name segment, so `strict_census2.sh` re-derives their gtest filter from the last two + # components and re-runs them WITHOUT the mode this block pins - which for these lanes is the + # whole point of the entry. They join Ct. / F1. / SmallRing. / PersistentMapArm. / NamedBlit. + # in that blind spot, so SplitLogPaths.cmake.in gives each of them a private log path: read + # the marker out of `build-split/MobileGL/MG_IntegrationTest/split-logs/.log`, which + # the LANE wrote under its own pin, instead of out of a re-run that lost it. + set(MGL_SPLIT_MULTIDRAW_TIERS basevertex multiindirect indirect drawelements compute) + set(MGL_SPLIT_MULTIDRAW_TIER_PREFIX_basevertex "BaseVertex") + set(MGL_SPLIT_MULTIDRAW_TIER_PREFIX_multiindirect "MultiIndirect") + set(MGL_SPLIT_MULTIDRAW_TIER_PREFIX_indirect "Indirect") + set(MGL_SPLIT_MULTIDRAW_TIER_PREFIX_drawelements "DrawElements") + set(MGL_SPLIT_MULTIDRAW_TIER_PREFIX_compute "Compute") + set(MGL_SPLIT_MULTIDRAW_TIER_FILTER + "MultiDrawScenario.*:IndexedDrawFamilyScenario.MultiDraw*-MultiDrawScenario.ClientSideIndicesBatchMatchesUnrolledDraws") + foreach(mglItestMultiDrawTier IN LISTS MGL_SPLIT_MULTIDRAW_TIERS) + mgl_itest_join_environment(MGL_ITEST_GLES_SPLIT_MULTIDRAW_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=inproc" + "MOBILEGL_IPC_SERVER_PATH=${MGL_ITEST_SPLIT_SERVER_PATH}" "MGITEST_SPLIT_LANE=1" + "MOBILEGL_ESPRYT_MULTIDRAW_MODE=${mglItestMultiDrawTier}" + ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV}) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX + "DirectGLES.Split.MultiDrawTier${MGL_SPLIT_MULTIDRAW_TIER_PREFIX_${mglItestMultiDrawTier}}." + TEST_LIST "MGL_SPLIT_MULTIDRAW_${mglItestMultiDrawTier}_TESTS" + TEST_FILTER "${MGL_SPLIT_MULTIDRAW_TIER_FILTER}" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS "integration-gpu\;integration-split" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_MULTIDRAW_ENVIRONMENT}" + ) + endforeach() + # ---- P5b package i1 (MG_Remote/CONTRACT-P5B.md §2 i1) -------------------------------- # # THE LANE CASE THAT PROVES THE RECORDS CROSSED, and it is a RESULT and not a probe (R-16: diff --git a/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in index 3bc1c1f6..d4117c39 100644 --- a/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in +++ b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in @@ -26,6 +26,17 @@ foreach(scenario @MGL_SPLIT_D1_SCENARIOS@) endforeach() endforeach() +# P5e mv's five tier-pinned lanes, the same rule for the same reason - and here it also repairs +# the addendum §4 blind spot for them: these entries carry a third name segment, so the strict +# census cannot re-derive their MOBILEGL_ESPRYT_MULTIDRAW_MODE, but a private log per entry means +# its marker can be read where the LANE wrote it instead of from a re-run that lost the pin. +foreach(tier @MGL_SPLIT_MULTIDRAW_TIERS@) + foreach(entry IN LISTS MGL_SPLIT_MULTIDRAW_${tier}_TESTS) + set_tests_properties("${entry}" PROPERTIES ENVIRONMENT + "MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log") + endforeach() +endforeach() + # P5b package i1's four scenarios, the same shape as the small-ring loop above: every # DirectGLES.Split. entry owns exactly one absolute log path and shares it with nobody, which # is what SplitLogPaths.PrivateAndDistinct checks over the discovered set.