Merge branch p5e/mv into feat/disaggregated (P5e mv)

This commit is contained in:
2026-09-18 12:07:22 -04:00
5 changed files with 295 additions and 62 deletions
+4 -25
View File
@@ -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",
+32
View File
@@ -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).
+175 -37
View File
@@ -11,6 +11,7 @@
#include <MG_State/GLState/Core.h>
#include <MG_Pipe/PipeInputsSwitch.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstdlib>
#include <cstring>
#include <limits>
@@ -91,11 +92,151 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
return resource ? resource->id : 0;
}
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
static const SharedPtr<MG_State::GLState::BufferObject> 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<MG_State::GLState::BufferObject>& 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<const Uint8*>(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<SizeT>(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<SizeT>(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
@@ -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=<x> -> <x> (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/<entry>.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:
@@ -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.