From 976fbe4cd4876f89cf636561d2415b03d233b418 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 29 Sep 2026 22:14:31 -0400 Subject: [PATCH] [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 --- .github/workflows/test.yml | 9 + MobileGL/MG_IntegrationTest/CMakeLists.txt | 44 ++- MobileGL/MG_IntegrationTest/P8Coverage.cmake | 239 ++++++++++++++ scripts/ci/split_coverage.py | 328 +++++++++++++++++++ scripts/data/split_coverage_exemptions.txt | 64 ++++ 5 files changed, 668 insertions(+), 16 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/P8Coverage.cmake create mode 100755 scripts/ci/split_coverage.py create mode 100644 scripts/data/split_coverage_exemptions.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c4c813e7..1a281348 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 9929f305..faadb8ed 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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 # ===================================================================================== diff --git a/MobileGL/MG_IntegrationTest/P8Coverage.cmake b/MobileGL/MG_IntegrationTest/P8Coverage.cmake new file mode 100644 index 00000000..40612e39 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/P8Coverage.cmake @@ -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://:; 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") diff --git a/scripts/ci/split_coverage.py b/scripts/ci/split_coverage.py new file mode 100755 index 00000000..c58273d0 --- /dev/null +++ b/scripts/ci/split_coverage.py @@ -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.`, `DirectVulkan.`, plus their +knob-pinned lanes such as `DirectGLES.ForcedDepthStencilEmulation.`) 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 `.[.][.] +.`, 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}-` + (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.` (labelled integration-split, but no arm) on the monolith side, +where it belongs, and leaves build-verify's `DirectGLES.VerifySplit.` (no integration-gpu) +out of both sides. + +THE EXEMPTION TABLE (scripts/data/split_coverage_exemptions.txt), one row per pattern: + + [:] + +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 `:` 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 .[.][.]., 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 ") + continue + pattern, reason_class, reason = fields + match = PATTERN.match(pattern) + if not match: + errors.append(f"{where}: `{pattern}` is not [:]. (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 `:` 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 ") + # 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()) diff --git a/scripts/data/split_coverage_exemptions.txt b/scripts/data/split_coverage_exemptions.txt new file mode 100644 index 00000000..522df799 --- /dev/null +++ b/scripts/data/split_coverage_exemptions.txt @@ -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. +# +# [:] +# +# 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