[Test] (P8-A): register the monolith-only integration cases on the split, spawn and tcp arms (P8Coverage.cmake: 246 ambient cases, five server-knob lanes each on its own tcp supervisor), retire the ClientSideIndices and GuiBatch exclusions, and gate the rest with split_coverage.py and its exemption table

This commit is contained in:
2026-09-29 22:14:31 -04:00
parent 8d3e2317a6
commit 976fbe4cd4
5 changed files with 668 additions and 16 deletions
+9
View File
@@ -1461,6 +1461,15 @@ jobs:
- name: Spawn/split lane parity (9.2)
run: python3 scripts/ci/spawn_lane_parity.py build-split
# P8-A (ID-P8-2): every monolith case, tails normalised, is on a gated split arm or is named
# with a reason class in scripts/data/split_coverage_exemptions.txt. The same `ctest -N`
# cost as the parity step above, and the question it cannot answer: parity compares the
# split arms with EACH OTHER, so a case missing from all three passes it.
- name: Split coverage against monolith (P8-A)
run: |
python3 scripts/ci/split_coverage.py --self-test
python3 scripts/ci/split_coverage.py build-split
- name: Unit tests on the split runtime
working-directory: build-split
env:
+28 -16
View File
@@ -2587,8 +2587,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# had no split coverage at all, so every claim about it was a reading of the code rather than
# a run of it. Arming the lanes is the first half of the work and the census is the second:
# what reds here IS the remaining worklist, and each red is either D1's to fix or excluded BY
# NAME with the Fatal quoted, the way the d1 draw block excludes
# MultiDrawScenario.ClientSideIndicesBatchMatchesUnrolledDraws.
# NAME with the Fatal quoted, the way the d1 draw block excluded
# MultiDrawScenario.ClientSideIndicesBatchMatchesUnrolledDraws until P8-A.
mgl_itest_register_split_arms("PixelStoreSweepScenario.*" "")
mgl_itest_register_split_arms("PackedWordReadbackScenario.*" "")
mgl_itest_register_split_arms("LayeredTextureReadbackScenario.*" "")
@@ -2627,7 +2627,12 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# left all of them passing. A split arm that inherited the ambient default would be measuring
# the same non-path three more times. So the pin is MIRRORED here, which is what the plan's
# row asks for.
foreach(mglItestDsArm IN LISTS MGL_ITEST_MAGMA_ARMS)
#
# P8-A: SPLIT AND SPAWN ONLY HERE. The knob is read by the server, and the Tcp arm's server
# is the lane-wide fixture, so a Tcp copy registered here ran the NATIVE readback under the
# ForcedDs. name. The same-named Tcp entries are registered in P8Coverage.cmake, against a
# supervisor started with the knob.
foreach(mglItestDsArm Split Spawn)
mgl_itest_magma_arm_shape(${mglItestDsArm})
string(TOLOWER "${mglItestDsArm}" mglItestDsArmLower)
mgl_itest_join_environment(mglItestDsEnvironment
@@ -2755,19 +2760,20 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# the two multi-draws, an indirect draw), so a record that did not cross is a wrong picture
# and not a green. MultiDrawScenario, DrawParametersScenario and PrimitiveRestartScenario are
# the census's own DrawElements / MultiDrawElementsBaseVertex / DrawArraysInstancedBaseInstance
# / MultiDraw*Indirect* first-blocker scenarios, now at their pictures. The ONE case excluded
# by filter is MultiDrawScenario's ClientSideIndicesBatchMatchesUnrolledDraws: a multi-draw
# with client-side indices is refused BY NAME under split (Fatal{UnmigratedVerb,
# "MultiDrawElementsBaseVertex+CLIENT_INDICES"}, CONTRACT-P5B.md d1 - P8's HostResolve.cpp
# flattens it), which is its next first blocker and not a lane failure. It is listed in
# d1-v1.md, not deleted (G14). GuiBatchScenario (the census's other DrawElements scenario) is
# NOT registered: past its draws it stops at Fatal{UnmigratedVerb, "MemoryBarrier"}, which is
# i1's to flip; i1 registers it when that lands.
# / MultiDraw*Indirect* first-blocker scenarios, now at their pictures. The whole of
# MultiDrawScenario is here since P8-A: its ClientSideIndicesBatchMatchesUnrolledDraws was
# excluded while a multi-draw with client-side indices was refused BY NAME under split
# (Fatal{UnmigratedVerb, "MultiDrawElementsBaseVertex+CLIENT_INDICES"}), and the client now
# flattens every sub-draw's client indices into one owned element buffer before the record is
# emitted (EmitTables.cpp's EmitMultiIndexedDraw, OwnedDrawInputs.h), so the
# server's CLIENT_INDICES latch is unreachable from our client and this case is the entry that
# would see it come back. GuiBatchScenario (the census's other DrawElements scenario) waited
# on Fatal{UnmigratedVerb, "MemoryBarrier"}; that verb has crossed since i1 and class C is
# empty, so P8Coverage.cmake registers it with the other monolith-only cases.
set(MGL_SPLIT_D1_SCENARIOS IndexedDrawFamilyScenario MultiDrawScenario DrawParametersScenario
PrimitiveRestartScenario)
set(MGL_SPLIT_D1_FILTER_IndexedDrawFamilyScenario "IndexedDrawFamilyScenario.*")
set(MGL_SPLIT_D1_FILTER_MultiDrawScenario
"MultiDrawScenario.*-MultiDrawScenario.ClientSideIndicesBatchMatchesUnrolledDraws")
set(MGL_SPLIT_D1_FILTER_MultiDrawScenario "MultiDrawScenario.*")
set(MGL_SPLIT_D1_FILTER_DrawParametersScenario "DrawParametersScenario.*")
set(MGL_SPLIT_D1_FILTER_PrimitiveRestartScenario "PrimitiveRestartScenario.*")
foreach(mglItestD1Scenario IN LISTS MGL_SPLIT_D1_SCENARIOS)
@@ -2810,8 +2816,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# 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.
# IndexedDrawFamilyScenario's three MultiDraw cases. The client-side-indices case joined with
# the d1 block's (P8-A): its flattened indices arrive as one owned element buffer, which is
# exactly the bound index buffer each tier asks its questions of.
#
# 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
@@ -2827,7 +2834,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
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")
"MultiDrawScenario.*:IndexedDrawFamilyScenario.MultiDraw*")
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_ITEST_REQUIRE_GPU=1"
@@ -4648,6 +4655,11 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
endforeach()
endif()
# P8-A
if (MOBILEGL_BUILD_DISAGGREGATED)
include(${CMAKE_CURRENT_SOURCE_DIR}/P8Coverage.cmake)
endif()
# =====================================================================================
# P7 PACKAGE L: THE INFORMATIONAL MAGMA CENSUS - TIER 2 OF TWO
# =====================================================================================
@@ -0,0 +1,239 @@
# P8-A (ID-P8-2): the monolith cases' split, spawn and tcp registrations.
#
# Included ONCE from CMakeLists.txt, inside `if (MOBILEGL_BUILD_DISAGGREGATED)`, and the place is
# load-bearing twice over: it must come BEFORE the Magma tier-2 replay (every
# mgl_itest_register_split_arms call here is replayed onto DirectVulkan's informational
# integration-magma-all-{split,spawn,tcp} arms from MGL_SPLIT_ARM_CALLS) and BEFORE the
# SplitLogPaths configure_file at the end of that file (every entry here gets its private
# MOBILEGL_LOG_FILE_PATH from MGL_SPLIT_ARM_TEST_LISTS, or SplitLogPaths.PrivateAndDistinct reds).
#
# THE GATE IS scripts/ci/split_coverage.py: after the arm and tail segments come off, every
# monolith DirectGLES. / DirectVulkan. case is on a gated split arm or is named, with a reason
# class, in scripts/data/split_coverage_exemptions.txt. Deleting a line below reds it BY NAME.
# The per-case census this list was cut from (inproc / spawn / tcp, junit per arm) is
# docs/Disaggregated/notes/p8/A.md.
# ---- (a) THE AMBIENT CASES -----------------------------------------------------------------
#
# Every case here was registered on monolith only, and on all three arms under the armed split
# environment it either PASSED (records crossed: ScenarioFixture's EmitSeq assertion is armed on
# these arms) or SKIPPED for the same reason its monolith entry skips on this host (ClipDistance,
# DualSourceBlend, IterationRP*, SsboDeclarationForm's two known gaps, and the knob-pinned cases
# the (a') block below runs with their knob). Cases that belong to monolith by nature are in the
# exemption table instead, never here: no-records (limits / shader-binary queries answered from
# the caps mirror), single-backend (Magma-only lanes and declines), mechanism, pending-fix.
#
# ONE FILTER, not one call per scenario: each call is three discovery runs plus three more for
# the Magma replay. Explicit names where a scenario is split between registered and exempt
# cases; `Scenario.*` only where every case of the scenario is here.
set(MGL_P8A_AMBIENT_CASES
AdvertisedLimitsScenario.ImageUnitBindingsAreReportedFromTheFrontendState
AdvertisedLimitsScenario.IndexedBufferBindingsAreReportedVerbatimOnBothWidths
BufferTextureScenario.*
ClearTexImageUndefinedLevelZeroScenario.*
ClipDistanceScenario.*
CopyImageLevelRangeScenario.*
CopyImagePacked16Scenario.*
CrossFrameBufferScenario.*
DefaultFramebufferAcrossSwapScenario.*
DepthStencilReadbackAttachmentShapeScenario.DepthOfALayeredCubeAttachmentReadsBack
DepthStencilReadbackAttachmentShapeScenario.DepthOfAPlainTexture2DAttachmentReadsBack
DepthStencilReadbackAttachmentShapeScenario.DepthOfAnArrayLayerAttachmentReadsBack
DepthStencilReadbackAttachmentShapeScenario.PackedArrayLayerAttachmentReadsBackBothAspects
DepthStencilReadbackScenario.*
DoublePrecisionScenario.*
DualSourceBlendScenario.*
EmptyScissorScenario.*
F1WireScenario.ColorBlitToDefaultFromANonSampleableSourceDegrades
F1WireScenario.ColorBlitToDefaultHonorsScissorAndReversedRect
F1WireScenario.ColorBlitToDefaultIgnoresViewportAndPreservesOrientation
F1WireScenario.ComputeImageStoreFramebufferPixels
F1WireScenario.ComputeImageStoreThroughViewPreservesOtherRootLayer
F1WireScenario.ComputeWrittenVertexAndIndexBuffersDrawAndReadBack
F1WireScenario.CopyImageInPlaceAcrossLevelsAndLayers
F1WireScenario.EnabledVertexBufferDrawKeepsNamedP7Fatal
F1WireScenario.GenerateMipmapHonorsMutableBaseAndMaxWithoutChangingOtherLevels
F1WireScenario.GenerateMipmapThroughViewKeepsOwnerLevelsAndLayersOutsideItsWindow
F1WireScenario.MultisampleColorBlitScalesAndFlipsThroughTheResolveScratch
F1WireScenario.PartialTextureUploadPreservesGpuClearPixels
F1WireScenario.SrgbDrawTracksFramebufferConversion
F1WireScenario.StreamedBufferSubDataBeforeEachDrawIsOrdered
F1WireScenario.Texture1DFramebufferClearPixels
F1WireScenario.TextureReadbackLargerThanAReplySlotContainsGpuWrites
F1WireScenario.TextureViewClearTargetsItsRootMipAndLayer
F1WireScenario.UniformBufferRangeAndRebindPixels
F1WireScenario.VertexIdSamplerAndScalarUniformPixels
FormatlessImageBakeScenario.*
FragCoordOriginScenario.*
FragmentOutputArrayIndexScenario.*
GeometryDrawModeScenario.*
GuiBatchScenario.*
ImageFormatQualifierScenario.*
ImageLoadStoreSsoScenario.PerElementImageUnitsReachAPipelineDraw
ImageLoadStoreSsoScenario.ReassigningAnImageUnitBetweenDrawsReachesTheNextDraw
IterationRPFirstReductionScenario.*
IterationRPProgram203Scenario.*
IterationRPScratchFixScenario.*
LayeredAttachmentBarrierScenario.*
LayeredAttachmentShapeScenario.*
NonCoreImageFormatScenario.*
OrientationScenario.*
P4aFinalFixScenario.*
P4aSeamAuditScenario.*
PipelineFailureScenario.*
PointSizeDemotionScenario.*
ProgramPipelineScenario.ASamplerUnitRewrittenBetweenDrawsKeepsPaintingTheRightTexture
ProgramPipelineScenario.AStageProgramsUniformBlockBindingReachesThePipelineDraw
ProgramPipelineScenario.ATwoStagePipelinePaintsWhatItsStagesDescribe
ProgramPipelineScenario.AUniformDeclaredInTwoStagesKeepsTheValueTheWrittenStageHolds
ProgramPipelineScenario.ComputeAndGraphicsStagesShareOnePipeline
ProgramPipelineScenario.RebindingAUniformBlockBetweenDrawsReachesTheNextDraw
ProgramPipelineScenario.SwitchingBetweenAPipelineAndAMonolithicProgramLeavesNoError
ProgramPipelineScenario.UniformsGoToTheActiveShaderProgram
RenderbufferBlendFormatScenario.*
ResidentIndexScenario.*
SampleMaskScopeScenario.*
SampleVariablesScenario.*
SnormAttachmentScenario.*
SpirvShaderBinaryScenario.SpecializedModulesLinkAndRenderWithTheirConstantsApplied
SsboArrayDynamicIndexScenario.*
SsboArrayLengthScenario.*
SsboDeclarationFormScenario.*
StorageBufferRegrowScenario.AGrownStoreIsVisibleThroughItsExistingIndexedBinding
StreamedArenaScenario.*
TessellationDrawModeScenario.*
ThreeChannelAttachmentScenario.*
UnboundCounterBlockScenario.*
UnlocatedIoBlockScenario.*
UnwrittenPositionOutputScenario.ARedeclaredButUnwrittenPositionStillDraws
UnwrittenPositionOutputScenario.AShaderWithNoPositionBlockStillDraws
UnwrittenPositionOutputScenario.AWrittenRedeclaredPositionStillDraws
UnwrittenPositionOutputScenario.CapturingAWrittenPositionStillDraws
VertexArrayEnableDisableScenario.*
VertexAttribBindingScenario.*
ViewportArrayScenario.*
)
list(JOIN MGL_P8A_AMBIENT_CASES ":" mglP8aAmbientFilter)
mgl_itest_register_split_arms("${mglP8aAmbientFilter}" "")
# ---- (a') THE SERVER-READ KNOB LANES, EACH WITH A TCP SUPERVISOR OF ITS OWN ------------------
#
# The monolith file pins five whole-process knobs in lanes of their own (PointSizeDemotion.,
# UnlocatedIoBlocks., NoViewportArrayEmulation., WidenedPacked16., ForcedDepthStencilEmulation.),
# because without them the cases are unfalsifiable on llvmpipe or skip. Every one of the five is
# read through ConfigLoader IN THE PROCESS THAT RUNS THE BACKEND - the server. Under inproc that
# is this process and under spawn the child inherits ::environ, so the entry's ENVIRONMENT
# reaches it; under tcp the server is a supervisor started by a fixture, and the lane-wide
# TcpServer.Start supervisor never sees a per-entry variable. MEASURED on this tree before this
# block existed: with the knob in the client's environment only, the Tcp copies of
# PointSizeDemotion's and UnlocatedIoBlocks' arming cases read no arming line and
# NoViewportArrayEmulation's negative control saw the emulation still on - red, where the Split
# and Spawn copies were green.
#
# THE KNOB CANNOT GO ON THE SHARED FIXTURE (the IC TcpServer.Start precedent, which works for
# MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW only because every arm sets that one): it would retarget
# every other tcp case - viewport emulation off breaks ViewportArrayScenario's positive cases,
# the IO-block strip rewrites every interface block. So each lane gets a supervisor of its own,
# on its own port, started by its own fixture with the knob in the fixture's ENVIRONMENT.
#
# THE PORTS: the lane endpoint's port + MOBILEGL_ITEST_TCP_KNOB_PORT_OFFSET + the lane's index.
# The default offset (1000) keeps the five clear of every per-tree endpoint in use (40613 CI,
# 407xx / 409xx split and verify builds, 484x0 package trees) and of each other's trees.
#
# THE LOCK IS STILL mobilegl-tcp: spawn_lane_parity.py / junit_tally.py require it of every
# integration-tcp entry, and one tcp session at a time is what every other tcp entry assumes.
#
# NOT FOR DirectVulkan: all five knobs are Espryt's (Magma routes viewports natively, has no
# IO-block strip, no packed-16 widening, no DS readback emulation; point-size demotion's Magma
# lane is monolith-only and stays so here).
set(MOBILEGL_ITEST_TCP_KNOB_PORT_OFFSET 1000 CACHE STRING
"Port offset (from MOBILEGL_ITEST_TCP_ENDPOINT's) of the per-knob loopback supervisors")
if(NOT MOBILEGL_ITEST_TCP_ENDPOINT MATCHES "^(tcp://.+):([0-9]+)$")
message(FATAL_ERROR "P8-A: MOBILEGL_ITEST_TCP_ENDPOINT '${MOBILEGL_ITEST_TCP_ENDPOINT}' is not "
"tcp://<host>:<port>; the per-knob tcp supervisors derive their ports from it")
endif()
set(mglP8aTcpHost "${CMAKE_MATCH_1}")
set(mglP8aTcpPort "${CMAKE_MATCH_2}")
set(MGL_P8A_KNOB_LANES PointSizeDemotion UnlocatedIoBlocks NoViewportArrayEmulation WidenedPacked16
ForcedDs)
set(MGL_P8A_KNOB_FILTER_PointSizeDemotion "PointSizeDemotionScenario.*")
set(MGL_P8A_KNOB_ENV_PointSizeDemotion "MOBILEGL_POINT_SIZE_DEMOTION=1")
set(MGL_P8A_KNOB_FILTER_UnlocatedIoBlocks "UnlocatedIoBlockScenario.*")
set(MGL_P8A_KNOB_ENV_UnlocatedIoBlocks "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1")
# One case, as on monolith: the three positive cases describe behaviour the backend does not
# have with the emulation off.
set(MGL_P8A_KNOB_FILTER_NoViewportArrayEmulation
"ViewportArrayScenario.WithoutTheEmulationEveryIndexCollapsesOntoViewportZero")
set(MGL_P8A_KNOB_ENV_NoViewportArrayEmulation "MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0")
set(MGL_P8A_KNOB_FILTER_WidenedPacked16 "CopyImagePacked16Scenario.*")
set(MGL_P8A_KNOB_ENV_WidenedPacked16 "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1")
# ForcedDs keeps the spelling the existing Split/Spawn DepthStencilReadbackMatrix block uses. The
# default-framebuffer case is left out on every arm: red on spawn and tcp with or without the
# knob (pending-fix row in the exemption table).
set(MGL_P8A_KNOB_FILTER_ForcedDs
"DepthStencilReadbackScenario.*:DepthStencilReadbackAttachmentShapeScenario.*-DepthStencilReadbackAttachmentShapeScenario.DefaultFramebufferDepthStencilFormatIsBlitCompatible")
set(MGL_P8A_KNOB_ENV_ForcedDs "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1")
# The Matrix scenario's Tcp copy moves HERE from the ForcedDs block (same names): that block
# put the knob in the client's environment only, so its Tcp arm ran the native readback path.
set(MGL_P8A_KNOB_TCP_FILTER_ForcedDs
"DepthStencilReadbackMatrixScenario.*:${MGL_P8A_KNOB_FILTER_ForcedDs}")
set(mglP8aKnobIndex 0)
foreach(mglP8aLane IN LISTS MGL_P8A_KNOB_LANES)
string(TOLOWER "${mglP8aLane}" mglP8aLaneLower)
math(EXPR mglP8aKnobPort "${mglP8aTcpPort} + ${MOBILEGL_ITEST_TCP_KNOB_PORT_OFFSET} + ${mglP8aKnobIndex}")
math(EXPR mglP8aKnobIndex "${mglP8aKnobIndex} + 1")
set(mglP8aKnobEndpoint "${mglP8aTcpHost}:${mglP8aKnobPort}")
set(mglP8aKnobFixture "mobilegl-tcp-p8a-${mglP8aLaneLower}")
set(mglP8aKnobState "${CMAKE_CURRENT_BINARY_DIR}/tcp-server-p8a-${mglP8aLaneLower}.json")
set(mglP8aKnob "${MGL_P8A_KNOB_ENV_${mglP8aLane}}")
add_test(NAME TcpServer.Start.P8a${mglP8aLane} COMMAND "${Python3_EXECUTABLE}" "${MGL_TCP_FIXTURE}"
start --server "${MGL_ITEST_SPLIT_SERVER_PATH}" --endpoint "${mglP8aKnobEndpoint}"
--state "${mglP8aKnobState}")
add_test(NAME TcpServer.Stop.P8a${mglP8aLane} COMMAND "${Python3_EXECUTABLE}" "${MGL_TCP_FIXTURE}"
stop --state "${mglP8aKnobState}")
# The texture-view opt-in as on the shared fixture, so this supervisor differs from it in
# the knob alone.
set_tests_properties(TcpServer.Start.P8a${mglP8aLane} PROPERTIES
FIXTURES_SETUP ${mglP8aKnobFixture} TIMEOUT 20
ENVIRONMENT "MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1;${mglP8aKnob}")
set_tests_properties(TcpServer.Stop.P8a${mglP8aLane} PROPERTIES
FIXTURES_CLEANUP ${mglP8aKnobFixture} TIMEOUT 20)
foreach(mglP8aArm Split Spawn Tcp)
string(TOLOWER "${mglP8aArm}" mglP8aArmLower)
string(TOUPPER "${mglP8aArm}" mglP8aArmUpper)
set(mglP8aEnvironment "${MGL_ITEST_GLES_${mglP8aArmUpper}_ENVIRONMENT}")
set(mglP8aFilter "${MGL_P8A_KNOB_FILTER_${mglP8aLane}}")
set(mglP8aProperties)
if(mglP8aArm STREQUAL "Tcp")
string(REPLACE "MOBILEGL_IPC_CONTROL=${MOBILEGL_ITEST_TCP_ENDPOINT}"
"MOBILEGL_IPC_CONTROL=${mglP8aKnobEndpoint}"
mglP8aEnvironment "${mglP8aEnvironment}")
if(mglP8aEnvironment STREQUAL MGL_ITEST_GLES_TCP_ENVIRONMENT)
message(FATAL_ERROR "P8-A: the Tcp arm environment names no "
"MOBILEGL_IPC_CONTROL=${MOBILEGL_ITEST_TCP_ENDPOINT} to retarget")
endif()
set(mglP8aProperties FIXTURES_REQUIRED ${mglP8aKnobFixture} RESOURCE_LOCK mobilegl-tcp)
if(DEFINED MGL_P8A_KNOB_TCP_FILTER_${mglP8aLane})
set(mglP8aFilter "${MGL_P8A_KNOB_TCP_FILTER_${mglP8aLane}}")
endif()
endif()
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.${mglP8aArm}.${mglP8aLane}."
TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}"
TEST_FILTER "${mglP8aFilter}"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-${mglP8aArmLower}"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${mglP8aEnvironment}\;${mglP8aKnob}"
${mglP8aProperties}
)
list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}")
math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1")
endforeach()
endforeach()
message(STATUS "Integration tests: P8-A registers ${mglP8aKnobIndex} server-knob lane(s) on "
"split/spawn/tcp, each tcp arm on its own supervisor")
+328
View File
@@ -0,0 +1,328 @@
#!/usr/bin/env python3
"""P8-A (ID-P8-2): every monolith integration case runs on a split arm or is exempt BY NAME.
THE QUESTION. The monolith registrations (`DirectGLES.<case>`, `DirectVulkan.<case>`, plus their
knob-pinned lanes such as `DirectGLES.ForcedDepthStencilEmulation.<case>`) define what the suite
tests. A case that exists there and on no split arm is a case the disaggregated runtime was never
asked about - and nothing about a green split lane says so. This asserts, per backend:
monolith cases ⊆ split cases ∪ exempt cases
after the arm and tail segments come off both sides: an entry name is `<Backend>.[<Arm>.][<tail>.]
<Suite>.<Case>`, and only the last two segments (the gtest name) are compared. `Split.F1.`,
`Spawn.ForcedDs.`, `Tcp.MultiDrawTierBaseVertex.` and the monolith's
`ForcedDepthStencilEmulation.` all normalise away.
WHAT COUNTS AS A SPLIT CASE differs by backend, on purpose:
* DirectGLES - an entry with an arm segment (Split/Spawn/Tcp/TcpDevice) that carries a GATED
label. The one informational spelling family is `integration-magma-{all,full}-<arm>`
(MG_IntegrationTest/CMakeLists.txt, P7 package L tiers 2 and 3), and an entry carrying only
those labels is not coverage.
* DirectVulkan - any split-arm entry, the informational tiers included. Magma's split arms are
informational by ruling (ID-P8-2): cases move from tier 2/3 into the gated Magma tier with the
commit that retires their blocker. The report prints how many are gated and how many are
informational-only, so the number is visible without being a gate.
WHAT COUNTS AS A MONOLITH CASE: an entry with no arm segment carrying `integration-gpu` - the label
the monolith registrations and the monolith-control mechanism entries share. That puts
`DirectGLES.PushMonolithArm.<case>` (labelled integration-split, but no arm) on the monolith side,
where it belongs, and leaves build-verify's `DirectGLES.VerifySplit.<case>` (no integration-gpu)
out of both sides.
THE EXEMPTION TABLE (scripts/data/split_coverage_exemptions.txt), one row per pattern:
[<Backend>:]<gtest pattern> <reason-class> <one-line reason>
with reason classes mechanism / single-backend / no-records / inproc-peek / pending-fix. The table
keeps itself honest the way spawn_lane_parity.py's exception lists do - every one of these is an
error, not a warning:
* a pattern that matches no monolith case (an exemption about nothing);
* a pattern every case of which is already on a split arm (stale: a pending-fix that landed must
take its row with it, or the next regression of that case is silently exempt);
* a `pending-fix` row that names neither a package (`P8-B`) nor a DEBTS.md row;
* a `single-backend` row with no `<Backend>:` scope (it exempts the backend the case does NOT
belong to, and unscoped it would also hide a gap on the one it does);
* an unknown reason class, a missing reason, a duplicated pattern.
This script does not compare the arms with each other - spawn_lane_parity.py owns that.
"""
import argparse
import fnmatch
import json
from pathlib import Path
import re
import subprocess
import sys
BACKENDS = ("DirectGLES", "DirectVulkan")
ARMS = ("Split", "Spawn", "Tcp", "TcpDevice")
REASON_CLASSES = ("mechanism", "single-backend", "no-records", "inproc-peek", "pending-fix")
MONOLITH_LABEL = "integration-gpu"
INFORMATIONAL_LABEL = re.compile(r"^integration-magma-(all|full)-(split|spawn|tcp)$")
GTEST_NAME = re.compile(r"^[A-Za-z0-9_]+\.[A-Za-z0-9_]+$")
PATTERN = re.compile(r"^(?:(DirectGLES|DirectVulkan):)?([A-Za-z0-9_*?]+\.[A-Za-z0-9_*?]+)$")
OWNER = re.compile(r"\bP\d+[a-z]?-[0-9A-Z]+\b|DEBTS\.md")
DEFAULT_EXEMPTIONS = Path(__file__).resolve().parents[2] / "scripts/data/split_coverage_exemptions.txt"
def labels_of(test):
for prop in test.get("properties", []):
if prop.get("name") == "LABELS":
value = prop.get("value") or []
return [value] if isinstance(value, str) else list(value)
return []
def classify(document, errors):
"""{backend: {"mono": set, "gated": set, "info": set, "arms": {arm: set}}} from a
`ctest --show-only=json-v1` document. Names under a backend that do not parse are errors."""
out = {b: {"mono": set(), "gated": set(), "info": set(), "arms": {a: set() for a in ARMS}}
for b in BACKENDS}
for test in document.get("tests", []):
name = test.get("name", "")
parts = name.split(".")
if parts[0] not in BACKENDS:
continue
backend = parts[0]
labels = labels_of(test)
arm = parts[1] if len(parts) > 1 and parts[1] in ARMS else None
gtest = ".".join(parts[-2:])
rest = parts[2:] if arm else parts[1:]
if len(rest) < 2 or not GTEST_NAME.match(gtest):
errors.append(f"{name}: does not parse as <Backend>.[<Arm>.][<tail>.]<Suite>.<Case>, so "
f"this gate would silently drop it")
continue
side = out[backend]
if arm is None:
if MONOLITH_LABEL in labels:
side["mono"].add(gtest)
continue
if labels and not all(INFORMATIONAL_LABEL.match(label) for label in labels):
side["gated"].add(gtest)
side["arms"][arm].add(gtest)
else:
side["info"].add(gtest)
return out
def parse_exemptions(text, errors):
rows = []
seen = set()
for number, raw in enumerate(text.splitlines(), 1):
line = raw.strip()
if not line or line.startswith("#"):
continue
fields = line.split(None, 2)
where = f"exemptions:{number}"
if len(fields) < 3 or not fields[2].strip():
errors.append(f"{where}: `{line}` needs <pattern> <reason-class> <reason>")
continue
pattern, reason_class, reason = fields
match = PATTERN.match(pattern)
if not match:
errors.append(f"{where}: `{pattern}` is not [<Backend>:]<Suite>.<Case> (globs * and ? allowed)")
continue
if reason_class not in REASON_CLASSES:
errors.append(f"{where}: reason class `{reason_class}` is not one of {', '.join(REASON_CLASSES)}")
continue
if reason_class == "pending-fix" and not OWNER.search(reason):
errors.append(f"{where}: pending-fix `{pattern}` names no owner - a package (P8-B) or a "
f"DEBTS.md row must be in the reason, or the row outlives everyone's memory of it")
continue
if reason_class == "single-backend" and match.group(1) is None:
errors.append(f"{where}: single-backend `{pattern}` must be scoped `<Backend>:` to the backend "
f"it exempts; unscoped it would also hide a gap on the backend the case belongs to")
continue
if pattern in seen:
errors.append(f"{where}: `{pattern}` is listed twice")
continue
seen.add(pattern)
backends = (match.group(1),) if match.group(1) else BACKENDS
rows.append({"where": where, "pattern": pattern, "glob": match.group(2), "backends": backends,
"class": reason_class, "reason": reason.strip()})
return rows
def check(document, exemption_text, out=print):
"""Returns the exit code; every finding goes through `out`."""
errors = []
sides = classify(document, errors)
rows = parse_exemptions(exemption_text, errors)
covered = {"DirectGLES": sides["DirectGLES"]["gated"],
"DirectVulkan": sides["DirectVulkan"]["gated"] | sides["DirectVulkan"]["info"]}
exempt = {b: {} for b in BACKENDS}
for row in rows:
matched = needed = 0
for backend in row["backends"]:
for case in sorted(sides[backend]["mono"]):
if not fnmatch.fnmatchcase(case, row["glob"]):
continue
matched += 1
if case not in covered[backend]:
needed += 1
exempt[backend].setdefault(case, row)
if matched == 0:
errors.append(f"{row['where']}: `{row['pattern']}` matches no monolith case - an exemption "
f"about nothing (renamed or deleted case?)")
elif needed == 0:
errors.append(f"{row['where']}: `{row['pattern']}` is STALE - every monolith case it names is "
f"on a split arm now; delete the row so the next regression is not exempt")
for backend in BACKENDS:
side = sides[backend]
mono = side["mono"]
if not mono:
errors.append(f"{backend}: no monolith cases in this build - an empty set is covered by "
f"anything, which is the one way this gate could stop meaning something")
continue
missing = sorted(mono - covered[backend] - set(exempt[backend]))
by_class = {}
for row in exempt[backend].values():
by_class[row["class"]] = by_class.get(row["class"], 0) + 1
per_arm = ", ".join(f"{arm.lower()} {len(mono & cases)}" for arm, cases in side["arms"].items() if cases)
classes = ", ".join(f"{name} {count}" for name, count in sorted(by_class.items())) or "none"
if backend == "DirectGLES":
out(f"split coverage {backend}: monolith {len(mono)}, on a gated split arm "
f"{len(mono & side['gated'])} ({per_arm}), exempt {len(exempt[backend])} ({classes}), "
f"missing {len(missing)}")
else:
gated = mono & side["gated"]
out(f"split coverage {backend}: monolith {len(mono)}, on a gated split arm {len(gated)} "
f"({per_arm}), informational tiers only {len(mono & side['info'] - side['gated'])}, "
f"exempt {len(exempt[backend])} ({classes}), missing {len(missing)}")
for case in missing:
errors.append(f"{backend}.{case} is registered on monolith and on no "
f"{'gated ' if backend == 'DirectGLES' else ''}split arm, and no exemption "
f"names it: register it (MG_IntegrationTest/P8Coverage.cmake) or add a row "
f"to scripts/data/split_coverage_exemptions.txt with its reason class")
for message in errors:
out(f"::error::{message}")
return 1 if errors else 0
def ctest_document(build_dir):
text = subprocess.run(["ctest", "--show-only=json-v1"], cwd=build_dir, capture_output=True,
text=True, check=True).stdout
return json.loads(text)
# ---- self-test -------------------------------------------------------------------------------
def _entry(name, *labels):
return {"name": name, "properties": [{"name": "LABELS", "value": list(labels)}]}
def _doc(*entries):
return {"tests": list(entries)}
BASE = [
_entry("DirectGLES.AScenario.One", "integration-gpu"),
_entry("DirectGLES.AScenario.Two", "integration-gpu"),
_entry("DirectGLES.ForcedDepthStencilEmulation.BScenario.Three", "integration-gpu"),
_entry("DirectGLES.Split.AScenario.One", "integration-gpu", "integration-split"),
_entry("DirectGLES.Spawn.F1.AScenario.Two", "integration-gpu", "integration-spawn"),
_entry("DirectGLES.Tcp.ForcedDs.BScenario.Three", "integration-gpu", "integration-tcp"),
_entry("DirectVulkan.AScenario.One", "integration-gpu"),
_entry("DirectVulkan.MagmaOnlyScenario.Four", "integration-gpu"),
_entry("DirectVulkan.Split.AScenario.One", "integration-magma-all-split"),
_entry("DirectVulkan.Tcp.Fm.MagmaOnlyScenario.Four", "integration-magma-tcp"),
_entry("TcpServer.Start"),
]
def _run(document, exemptions):
lines = []
rc = check(document, exemptions, out=lines.append)
return rc, "\n".join(lines)
def self_test():
failures = []
ran = []
def expect(label, document, exemptions, rc, *needles, absent=()):
ran.append(label)
got, text = _run(document, exemptions)
if got != rc or any(n not in text for n in needles) or any(n in text for n in absent):
failures.append(f"{label}: rc {got} (want {rc}); output:\n{text}")
expect("tails normalise on both sides", _doc(*BASE), "", 0,
"DirectGLES: monolith 3, on a gated split arm 3", "missing 0")
expect("a monolith case on no arm reds BY NAME",
_doc(*BASE, _entry("DirectGLES.CScenario.Five", "integration-gpu")), "", 1,
"DirectGLES.CScenario.Five is registered on monolith")
expect("an informational-only arm is not DirectGLES coverage",
_doc(*BASE, _entry("DirectGLES.CScenario.Five", "integration-gpu"),
_entry("DirectGLES.Split.CScenario.Five", "integration-magma-full-split")), "", 1,
"DirectGLES.CScenario.Five is registered on monolith")
expect("an informational tier IS DirectVulkan coverage, and is reported as such", _doc(*BASE), "", 0,
"DirectVulkan: monolith 2, on a gated split arm 1", "informational tiers only 1")
expect("an arm-less entry without integration-gpu is on neither side",
_doc(*BASE, _entry("DirectGLES.VerifySplit.CScenario.Five", "integration-verify-split")), "", 0)
expect("PushMonolithArm (integration-split, no arm) is a MONOLITH case",
_doc(*BASE, _entry("DirectGLES.PushMonolithArm.CScenario.Five", "integration-gpu",
"integration-split")), "", 1,
"DirectGLES.CScenario.Five is registered on monolith")
expect("an exemption covers a missing case",
_doc(*BASE, _entry("DirectGLES.CScenario.Five", "integration-gpu")),
"CScenario.* mechanism toggles a process-local mask", 0, "exempt 1 (mechanism 1)")
expect("a stale exemption reds",
_doc(*BASE), "AScenario.One no-records query only", 1, "is STALE")
expect("an exemption about nothing reds",
_doc(*BASE), "ZScenario.* mechanism gone", 1, "matches no monolith case")
expect("pending-fix without an owner reds",
_doc(*BASE, _entry("DirectGLES.CScenario.Five", "integration-gpu")),
"CScenario.Five pending-fix red on spawn", 1, "names no owner")
expect("pending-fix owned by a package passes",
_doc(*BASE, _entry("DirectGLES.CScenario.Five", "integration-gpu")),
"CScenario.Five pending-fix red on spawn until P8-B lands", 0)
expect("pending-fix owned by a DEBTS.md row passes",
_doc(*BASE, _entry("DirectGLES.CScenario.Five", "integration-gpu")),
"CScenario.Five pending-fix red on spawn (DEBTS.md row: default framebuffer)", 0)
expect("an unknown reason class reds",
_doc(*BASE, _entry("DirectGLES.CScenario.Five", "integration-gpu")),
"CScenario.Five flaky it is slow", 1, "is not one of")
expect("an unscoped single-backend row reds",
_doc(*BASE, _entry("DirectGLES.MagmaOnlyScenario.Four", "integration-gpu")),
"MagmaOnlyScenario.* single-backend Magma's lane", 1, "must be scoped")
expect("a scoped single-backend row exempts only its backend",
_doc(*BASE, _entry("DirectGLES.MagmaOnlyScenario.Four", "integration-gpu")),
"DirectGLES:MagmaOnlyScenario.* single-backend Magma's lane", 0)
expect("a duplicated pattern reds",
_doc(*BASE, _entry("DirectGLES.CScenario.Five", "integration-gpu")),
"CScenario.* mechanism a\nCScenario.* mechanism b", 1, "listed twice")
expect("an unparseable backend entry reds", _doc(*BASE, _entry("DirectGLES.Split.Oops", "integration-split")),
"", 1, "does not parse")
expect("an empty monolith side reds", _doc(*[e for e in BASE if e["name"].startswith("DirectVulkan")]),
"", 1, "DirectGLES: no monolith cases")
expect("a row with no reason reds", _doc(*BASE), "AScenario.Two mechanism", 1, "needs <pattern>")
# The real table must at least parse (its coverage needs a build; the gate run checks that).
if DEFAULT_EXEMPTIONS.exists():
errors = []
parse_exemptions(DEFAULT_EXEMPTIONS.read_text(), errors)
if errors:
failures.append("the checked-in exemption table does not parse:\n" + "\n".join(errors))
for failure in failures:
print(f"::error::self-test: {failure}", file=sys.stderr)
print(f"split_coverage self-test: {len(ran)} cases + the checked-in table's parse, "
f"{len(failures)} failure(s), {'OK' if not failures else 'FAILED'}")
return 1 if failures else 0
def main():
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
parser.add_argument("build_dir", nargs="?", help="a configured build (runs ctest --show-only=json-v1)")
parser.add_argument("--json", help="a saved `ctest --show-only=json-v1` document instead of a build")
parser.add_argument("--exemptions", default=str(DEFAULT_EXEMPTIONS))
parser.add_argument("--self-test", action="store_true")
args = parser.parse_args()
if args.self_test:
return self_test()
if bool(args.build_dir) == bool(args.json):
parser.error("give exactly one of build_dir or --json")
document = json.loads(Path(args.json).read_text()) if args.json else ctest_document(args.build_dir)
return check(document, Path(args.exemptions).read_text())
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,64 @@
# Monolith integration cases that are on NO split arm, and why (P8-A, ID-P8-2).
# Read by scripts/ci/split_coverage.py; its docstring states every rule a row must keep.
#
# [<Backend>:]<gtest pattern> <reason-class> <one-line reason>
#
# Reason classes:
# mechanism the case asserts a process-local mechanism or pins one arm by design
# (MGL_ITEST_MONOLITH_MECHANISM_FILTER in MG_IntegrationTest/CMakeLists.txt)
# single-backend the case belongs to the OTHER backend and skips on this one; scoped to this one
# no-records the workload never reaches a verb (answered from the caps mirror or the
# client's own error surface), so ScenarioFixture's armed split assertion
# (EmitSeq must advance) is red by construction
# inproc-peek reads server state through an in-process peek only an inproc arm can answer
# pending-fix red on a split arm today; the reason names the package or DEBTS.md row that
# fixes it, and the row goes when the case is registered (a stale row is an error)
#
# Evidence for every row: docs/Disaggregated/notes/p8/A.md (per-arm census, junit per case).
# ---- mechanism: MGL_ITEST_MONOLITH_MECHANISM_FILTER, pinned to monolith by construction -------
HandleRecycleScenario.* mechanism pointer-key ABA / slot recycling inside one process (integration-monolith-control)
CsoContentAddressingScenario.* mechanism toggles the process-local CSO content-addressing switch
ResourceSubsystemControlScenario.* mechanism toggles the process-local P3a push mask
ObjectSubsystemControlScenario.* mechanism toggles the process-local P4a push mask
PipeVerifyArmingScenario.* mechanism same-address-space verify comparator; its split arm is build-verify's integration-verify-split
PoisonOmissionScenario.* mechanism withheld-stamp oracle; its split arm is build-verify's VerifySplitPoisonOmitted entries
StorageBufferRegrowScenario.NStorageDefinitionsCostNMapPersistentRoundtripsNotOnePerDraw mechanism counts synchronous stats-slot roundtrips of direct persistent adoption
DirectGLES:MonolithAttachmentClearScenario.* mechanism pins the push-monolith arm (DirectGLES.PushMonolithArm., ID-107); the split clear-attachment path is F1WireScenario's
DirectVulkan:DualBlockScenario.* mechanism needs MOBILEGL_IPC_ROLE_SPLIT_STATE=1; its lane is integration-dualblock-split (Espryt) and the Magma census filters it out
# ---- single-backend: Magma's cases, skipping on Espryt ----------------------------------------
DirectGLES:MagmaRunAheadScenario.* single-backend Magma run-ahead lane only (integration-magma-runahead)
DirectGLES:MagmaWireCacheScenario.* single-backend Magma wire-cache lane only (integration-magma-caches)
DirectGLES:MagmaWireReclaimScenario.* single-backend Magma wire-arm store reclaim
DirectGLES:MagmaVertexLayoutScenario.* single-backend Magma's vertex format map (Espryt hands GL_FIXED to the driver)
DirectGLES:PrimitivesGeneratedNoXfbScenario.* single-backend the stream-query defect and its reroute are DirectVulkan's
DirectGLES:ImageLoadStoreSsoScenario.AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings single-backend DirectVulkan's descriptor binding remap
DirectGLES:UnwrittenPositionOutputScenario.CapturingAnUnwrittenPositionStillDraws single-backend pins DirectVulkan's gl_Position injection (Espryt gap noted in the case)
DirectGLES:F1WireScenario.CopyImageInPlaceOverlapDeclinesAndLeavesTheLevelAlone single-backend Magma wire-arm decline (CONTRACT-P7 3.2 copy-image-in-place)
DirectGLES:F1WireScenario.GenerateMipmapDepthStencilDeclinesAndKeepsTheSession single-backend Magma wire-arm decline (depth-stencil-mipmap)
DirectGLES:F1WireScenario.MultisampleBlitFromASingleSampleSourceDeclinesAndKeepsTheSession single-backend Magma wire-arm decline (multisample-blit-aspect)
DirectGLES:F1WireScenario.MultisampleBlitOntoAMultisampleDestinationDeclinesAndKeepsTheSession single-backend Magma wire-arm decline (multisample-blit-shape)
DirectGLES:F1WireScenario.ShortUniformBufferRangePadsMissingBytes single-backend Magma short-UBO compatibility policy
DirectGLES:F1WireScenario.SubWordUniformBufferRangePadsMissingBytes single-backend Magma short-UBO compatibility policy
DirectGLES:F1WireScenario.UnalignedAtomicCounterRangeDeclinesAndTheSessionLives single-backend Magma descriptor-alignment decline
DirectGLES:F1WireScenario.GenerateMipmapWithoutNativeBlitPixels single-backend Magma shader-mip arm (MGITEST_MAGMA_FORCE_SHADER_MIPMAP, Magma Fm lane)
DirectGLES:F1WireScenario.GenerateMipmapDepthWithoutNativeBlitPixels single-backend Magma depth shader-mip arm (MGITEST_MAGMA_FORCE_SHADER_MIPMAP, Magma Fm lane)
# ---- no-records: EmitSeq does not move, so the armed split assertion is red by construction ----
AdvertisedLimitsScenario.ComputeLocalSizeComesFromTheLinkedProgram no-records limits query answered from the client's CapsMirror
AdvertisedLimitsScenario.ComputeUniformBlocksIsAnAmountAnApplicationCouldActuallyAllocate no-records limits query answered from the client's CapsMirror
AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer no-records limits query answered from the client's CapsMirror
AdvertisedLimitsScenario.EveryBufferLimitIsWithinItsAdvertisedRange no-records limits query answered from the client's CapsMirror
AdvertisedLimitsScenario.EveryGL45CoreMinimumIsMet no-records limits query answered from the client's CapsMirror
AdvertisedLimitsScenario.IndexedTargetSizeSurvivesDivisionByItsBindingCount no-records limits query answered from the client's CapsMirror
AdvertisedLimitsScenario.PerStageBlockCountsFitInTheirBindingPoints no-records limits query answered from the client's CapsMirror
AdvertisedLimitsScenario.ViewportArrayLimitsMeetTheirGL43Floors no-records limits query answered from the client's CapsMirror
SpirvShaderBinaryScenario.AnUnsupportedBinaryFormatIsRejectedInsteadOfSilentlyAccepted no-records shader-binary error surface stays on the client
SpirvShaderBinaryScenario.CompileShaderOnASpirvShaderIsInvalidOperationAndShaderSourceTakesItBack no-records shader-binary error surface stays on the client
SpirvShaderBinaryScenario.ShaderBinaryFormatIsAdvertisedExactlyOnce no-records extension-string query answered from the client's CapsMirror
SpirvShaderBinaryScenario.SpecializeShaderErrorSurfaceMatchesTheExtension no-records shader-binary error surface stays on the client
ProgramPipelineScenario.AReservedNameTakesStateBeforeItIsAProgramPipeline no-records a reserved pipeline name's state is client-side and draws nothing
# ---- pending-fix -------------------------------------------------------------------------------
DirectGLES:DepthStencilReadbackAttachmentShapeScenario.DefaultFramebufferDepthStencilFormatIsBlitCompatible pending-fix Espryt spawn+tcp blit the default framebuffer's depth out as 0 (inproc and all Magma arms green, knob or not); no P8 package owns it - DEBTS.md row requested in notes/p8/A.md