diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index e639bf7e..c44a589a 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -3005,7 +3005,43 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # the shared Fm environment would silently retarget GenerateMipmapPixels, # GenerateMipmapPackedFloatPixels, GenerateMipmapDepthPixels and the two base/view cases # as well - four green entries that would stop measuring the arm they were written for. + # ---- P7 wave 2-B2: A SERVER-SIDE KNOB HAS NO TCP ARM, AND THAT IS STRUCTURAL --------- + # + # Every MGITEST_MAGMA_FORCE_* knob in this block is read BY THE SERVER + # (WireFramebuffer.inc's three `std::getenv` readers run on the apply thread). A ctest + # ENVIRONMENT property reaches the process ctest starts - the CLIENT - and the two arms + # differ in where the server comes from: + # inproc the server is a thread of that same process, so it sees the knob; + # spawn ServerSpawn.cpp copies ::environ into the child, so it sees the knob; + # tcp the server is the LANE-WIDE TcpServer.Start fixture, started once from the + # fixture's own os.environ (scripts/ci/tcp_server_fixture.py) long before any + # case runs, and it forks a session child per Hello. Nothing a per-test + # ENVIRONMENT says can reach it. + # + # So a tcp copy of a knob entry does not exercise the arm it names - it runs the + # DEFAULT arm under a name that claims otherwise, which for ShaderMip is precisely what + # that case's own skip message says must not happen ("this case would assert the native + # blit twice over"). MEASURED, not deduced: a temporary fatal in each of the three + # readers killed all five knob=1 entries on split and on spawn and left ALL FIVE GREEN + # on tcp. + # + # Registering them on split+spawn only is therefore a coverage statement, not a gap, and + # it is NAMED in scripts/ci/spawn_lane_parity.py (MAGMA_SERVER_ENV_KNOB_NO_TCP) the way + # MAGMA_INPROC_ONLY and MONOLITH_ONLY are, so the parity gate keeps its teeth for + # everything else. The knob=0 twins (DefaultBlitShape0, MsResolve0) DO stay on tcp: zero + # is the default, so those entries mean the same thing on all three arms. + # + # Making the tcp lane able to carry these would need the fixture to take a per-case + # server environment - a supervisor control, not a ctest property - and that is a debt, + # recorded in notes/p7/magma-b2.md §6. + set(mglItestServerEnvKnobArm TRUE) + if(mglItestArm STREQUAL "Tcp") + set(mglItestServerEnvKnobArm FALSE) + endif() foreach(shaderMipTier 1 2) + if(NOT mglItestServerEnvKnobArm) + continue() + endif() gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectVulkan.${mglItestArm}.ShaderMip${shaderMipTier}." TEST_FILTER "F1WireScenario.GenerateMipmapWithoutNativeBlitPixels" @@ -3017,7 +3053,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # The DEPTH half of the same knob (CONTRACT-P7 §5.1 (A)). ONE TIER, not two: the depth # pass renders into a depth ATTACHMENT view of the destination level, which a depth # image always has usage for - there is no "cannot be attached" shape to force, so - # tier 2 would register a second copy of tier 1's run. + # tier 2 would register a second copy of tier 1's run. Split+spawn only, for the + # server-side-knob reason above. + if(mglItestServerEnvKnobArm) gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectVulkan.${mglItestArm}.DepthMip." TEST_FILTER "F1WireScenario.GenerateMipmapDepthWithoutNativeBlitPixels" @@ -3025,6 +3063,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED) PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} ${mglItestArmProperties} ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MGITEST_MAGMA_FORCE_SHADER_MIPMAP=1\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-depthmip${mglItestArmLogTag}.log") + endif() # NO KNOB on the decline: it is decided on the aspect before the blit-support question # is asked, so the plain Fm environment reaches it on every driver. gtest_discover_tests(MobileGLIntegrationTest @@ -3098,7 +3137,12 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # VK_SURFACE_TRANSFORM_IDENTITY, so the scratch path that exists FOR rotation would # never run. Forced onto an identity surface it must produce the SAME pixels as the # fall-through, which is what the shared assertions check. + # The knob=1 copy is split+spawn only (the server-side-knob note above); knob=0 is the + # default and means the same thing on all three arms, so it keeps its tcp entry. foreach(defaultBlitScratch 0 1) + if(defaultBlitScratch AND NOT mglItestServerEnvKnobArm) + continue() + endif() gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectVulkan.${mglItestArm}.DefaultBlitShape${defaultBlitScratch}." TEST_FILTER "F1WireScenario.ColorBlitToDefaultFromANonSampleableSourceDegrades" @@ -3149,6 +3193,11 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # ITS OWN ENTRIES RATHER THAN A LANE-WIDE FLIP, the shader-mip block's reason: the knob # changes how EVERY depth/stencil resolve in the process is performed. foreach(shaderDepthResolve 0 1) + # Same split+spawn restriction, same reason, same parity exception: the knob is read + # on the server and the tcp server is the lane fixture. MsResolve0 stays everywhere. + if(shaderDepthResolve AND NOT mglItestServerEnvKnobArm) + continue() + endif() gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectVulkan.${mglItestArm}.MsResolve${shaderDepthResolve}." TEST_FILTER "DepthStencilReadbackMatrixScenario.AResolvedMultisampleDepthReadsBackFromTheDestination" diff --git a/scripts/ci/spawn_lane_parity.py b/scripts/ci/spawn_lane_parity.py index 74035881..2e48d630 100644 --- a/scripts/ci/spawn_lane_parity.py +++ b/scripts/ci/spawn_lane_parity.py @@ -57,6 +57,24 @@ FIXTURE_ENTRIES = {"TcpServer.Start", "TcpServer.Stop"} # until they do, naming the two scenarios here keeps the tolerance from widening for anything else. MAGMA_INPROC_ONLY = ("MagmaRunAheadScenario.", "MagmaWireCacheScenario.") +# P7 wave 2-B2: THE SANCTIONED ASYMMETRY THAT IS NOT INPROC-ONLY - these exist on split AND +# spawn and cannot exist on tcp. Every MGITEST_MAGMA_FORCE_* knob is read BY THE SERVER +# (WireFramebuffer.inc's getenv readers run on the apply thread), and a ctest ENVIRONMENT +# property only reaches the process ctest starts, the CLIENT. Under inproc the server is a +# thread of that process and under spawn ServerSpawn.cpp copies ::environ into the child, so +# both see it; under tcp the server is the LANE-WIDE TcpServer.Start fixture, started once from +# the fixture's own os.environ before any case runs, so nothing a per-test property says can +# reach it. A tcp copy would run the DEFAULT arm under a name claiming otherwise. +# +# MEASURED (a temporary fatal in each of the three readers): all five knob=1 entries died on +# split and on spawn and ALL FIVE PASSED on tcp. +# +# These are TAILS, not scenarios - the normaliser above keeps the sub-lane tail for the Magma +# tier on purpose, so `.MsResolve1.` names the knob entry and `.MsResolve0.` (the default, which +# does keep its tcp entry) is untouched. +MAGMA_SERVER_ENV_KNOB_NO_TCP = (".ShaderMip1.", ".ShaderMip2.", ".DepthMip.", + ".DefaultBlitShape1.", ".MsResolve1.") + def lane_names(build_dir, label): """Every ctest entry name under an ANCHORED label (`ctest -L` is a regex, not a name).""" @@ -75,9 +93,15 @@ def lane_cases(build_dir, label): return {m.group(1) for m in CASE.finditer(out)} -def compare_arms(build_dir, tier, labels, inproc_only=()): +def compare_arms(build_dir, tier, labels, inproc_only=(), no_tcp=()): """The three arms of one tier must name the same set after the arm segment comes off. + `inproc_only` drops a key from the comparison for EVERY non-split arm; `no_tcp` drops it for + the tcp arm alone, which is a different shape and needs to be: a server-side test knob does + reach a spawned server and does not reach the tcp lane's shared fixture, so the entry is + required on spawn and forbidden on tcp. Folding it into `inproc_only` would stop requiring + it on spawn, which is where it does most of its work. + Returns True on failure, the way main() below counts them.""" sets = {} for arm, label in labels.items(): @@ -100,10 +124,19 @@ def compare_arms(build_dir, tier, labels, inproc_only=()): if len(comparable) != len(sets[reference]): print(f"{tier}: {len(sets[reference]) - len(comparable)} inproc-only entrie(s) excluded " f"from the comparison by name ({', '.join(inproc_only)})") + if no_tcp: + dropped = {k for k in comparable if any(only in k for only in no_tcp)} + if dropped: + print(f"{tier}: {len(dropped)} server-env-knob entrie(s) excluded from the TCP " + f"comparison only ({', '.join(no_tcp)}) - the knob is read on the server and " + f"the tcp server is the lane fixture, not a per-case process") for arm, keys in sets.items(): if arm == reference: continue - missing, extra = sorted(comparable - keys), sorted(keys - comparable) + expected = comparable + if arm == "tcp" and no_tcp: + expected = {k for k in comparable if not any(only in k for only in no_tcp)} + missing, extra = sorted(expected - keys), sorted(keys - expected) if missing or extra: print(f"::error::{tier}: the {arm} arm does not match the {reference} arm: " f"missing={missing}, extra={extra}. Every arm of a tier is emitted from one " @@ -163,11 +196,15 @@ def main(): # every arm - it is a whole-binary filter, and there they simply SKIP for want of their # lane marker - so excluding them there would turn a present-on-all-arms case into a # spurious "extra". + # The server-env knob entries are gated-tier only for the same per-tier reason: the + # full-suite census is a whole-binary filter with no knob in its environment at all, so + # there is nothing there to exclude. failed |= compare_arms(args.build_dir, "magma gated tier", {"split": "integration-magma-split", "spawn": "integration-magma-spawn", "tcp": "integration-magma-tcp"}, - inproc_only=MAGMA_INPROC_ONLY) + inproc_only=MAGMA_INPROC_ONLY, + no_tcp=MAGMA_SERVER_ENV_KNOB_NO_TCP) failed |= compare_arms(args.build_dir, "magma informational tier", {"split": "integration-magma-all-split", "spawn": "integration-magma-all-spawn",