From bd2e43ede3f6967af9f23b09256d0f4383db0497 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 29 Sep 2026 12:20:15 -0400 Subject: [PATCH] [Test] (P11 M2): dev's two Magma scenarios on the split arms - both on Espryt's three arms, OversizedRecordingSplitScenario on Magma's three (and pinned to a 64-draw budget on split and spawn, where the server reads it), DescriptorPoolGrowthScenario's census cases on Magma's split arm only; the split-recording arming reads both role logs and dev's pinned lane holds its log's RESOURCE_LOCK, and the census cases count the sets the wire arm holds between its descriptor rewinds --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 92 +++++++++++++++++++ .../DescriptorPoolGrowthScenario.cpp | 31 +++++-- .../OversizedRecordingSplitScenario.cpp | 37 +++----- scripts/ci/spawn_lane_parity.py | 23 ++++- 4 files changed, 152 insertions(+), 31 deletions(-) diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 3b992f54..74f1dc3f 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -1068,6 +1068,11 @@ gtest_discover_tests(MobileGLIntegrationTest # renderer's latched MGLOG_I) proves the split happened at all - on a renderer that records the # whole frame into one command buffer, it is the assertion that goes red. DirectVulkan only: # the budget steers nothing on DirectGLES, whose driver flushes on its own. +# +# P11 M2: RESOURCE_LOCK, for the UnlocatedIoBlocks lane's reason above - EVERY case here reads the +# lane's one log back, and the library truncates it at each process's start, so under `ctest -j` a +# sibling case wipes the line a reading case is looking for (measured on this tree: 5 of 8 red at +# -j 6, a different five on the monolith and inproc runs). gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectVulkan.SplitRecording." TEST_FILTER "OversizedRecordingSplitScenario.*" @@ -1075,6 +1080,7 @@ gtest_discover_tests(MobileGLIntegrationTest PROPERTIES LABELS integration-gpu TIMEOUT ${MGL_ITEST_TIMEOUT} + RESOURCE_LOCK oversized-recording-split.log ENVIRONMENT "${MGL_ITEST_VULKAN_SPLIT_RECORDING_ENVIRONMENT}" ) @@ -2870,6 +2876,18 @@ if (MOBILEGL_BUILD_DISAGGREGATED) mgl_itest_register_split_arms("ImageBindableRemintScenario.*" "") mgl_itest_register_split_arms("PartialUploadOverGpuContentScenario.*" "") + # ---- P11 M2: DEV'S TWO MAGMA SCENARIOS ON THE SPLIT ARMS --------------------------------- + # + # Merged from dev@c14283ee with their monolith entries. Both are plain GL and check pixels on + # every backend: OversizedRecordingSplitScenario draws frames whose state spans a command-buffer + # split (d20c2d01), DescriptorPoolGrowthScenario draws a 4096-distinct-set frame and gives it + # back (c2de6576/53fa16c2). On Espryt neither has anything else to check - the draw budget and + # the pool census are Magma's - so every case runs on all three arms. The Magma arms are laid by + # hand below (`DirectVulkan..Oversized.`, `.SplitRecording64.`, `DirectVulkan.Split.DescPool.`), + # where the gated flavour and the per-arm exceptions live. + mgl_itest_register_split_arms("OversizedRecordingSplitScenario.*" "") + mgl_itest_register_split_arms("DescriptorPoolGrowthScenario.*" "") + # ---- P5b t2: the transform-feedback spans, the XFB object bind, the patch parameter ------- # # MG_Remote/CONTRACT-P5B.md ยง2 t2. The three P5 lanes above are the REDUCED PATH's targets @@ -3923,6 +3941,80 @@ if (MOBILEGL_BUILD_DISAGGREGATED) endforeach() endif() +# P11 M2: DEV'S TWO MAGMA SCENARIOS ON MAGMA'S ARMS, GATED, in the band flavour. Every entry gets a +# private log through MGL_SPLIT_ARM_TEST_LISTS. +# +# Oversized. OversizedRecordingSplitScenario under the default draw budget (16384), all three +# arms: every case's pixels, unsplit - dev's control. Its arming is not checked here (no +# budget pinned). +# SplitRecording64. The same cases with MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER=64, so every +# case splits (d20c2d01's SplitOversizedRecording, which runs ahead of the wire arm's draw and +# dispatch too) and asserts the renderer's latched MGLOG_I. SPLIT AND SPAWN ONLY, one entry per +# case. The budget and the log line are the SERVER renderer's - the apply thread inproc, the +# spawned server (which copies this entry's environment, log base included) under spawn - and +# the scenario reads both role logs of its base (PipeStatsWindow). The tcp arm's server is +# the lane fixture, which no per-entry property reaches: a tcp copy would run the default +# budget under a name claiming 64 and its arming would find nothing (spawn_lane_parity.py, +# MAGMA_SERVER_ENV_KNOB_NO_TCP). The line is latched once per process, hence one per case. +# DescPool. DescriptorPoolGrowthScenario, SPLIT ONLY. Both cases read the pool census +# (DirectVulkan::GetDescriptorPoolCensus), which is in-process: inproc it is taken on the +# apply thread (the test probe, between drain batches); on spawn and tcp the pools live in the +# server process and the census answers unavailable, so a copy there would check pixels +# under a name claiming the pools (spawn_lane_parity.py, MAGMA_INPROC_ONLY). Those pixels run +# on every arm in the Espryt entries above and in the Magma informational and full-suite tiers. +if (MOBILEGL_BUILD_DISAGGREGATED) + set(MGL_ITEST_OVERSIZED_CASES + EveryDrawOfTheFrameLandsAcrossCommandBufferSplits + ADepthOccluderDrawnBeforeTheSplitHidesADrawAfterIt + AStencilMarkMadeBeforeTheSplitGatesADrawAfterIt + BlendingAccumulatesOntoPixelsWrittenBeforeTheSplit + AMultisampleTargetKeepsItsSamplesAcrossTheSplit + OcclusionQueriesOpenAcrossTheSplitCountBothSides + APrimitivesGeneratedQueryOpenAcrossTheSplitCountsBothSides + ATransformFeedbackCaptureActiveAcrossTheSplitAppendsEveryDraw) + foreach(mglItestArm IN LISTS MGL_ITEST_MAGMA_ARMS) + mgl_itest_magma_arm_shape(${mglItestArm}) + mgl_itest_join_environment(mglItestMagmaM2Environment + ${MGL_ITEST_MAGMA_BAND_FLAVOUR_ENV} ${mglItestArmTransportEnv}) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.Oversized." + TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}" + TEST_FILTER "OversizedRecordingSplitScenario.*" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${mglItestMagmaM2Environment}") + 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") + if(NOT mglItestArm STREQUAL "Tcp") + foreach(mglItestOversizedCase IN LISTS MGL_ITEST_OVERSIZED_CASES) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.SplitRecording64." + TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}" + TEST_FILTER "OversizedRecordingSplitScenario.${mglItestOversizedCase}" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${mglItestMagmaM2Environment}\;MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER=64") + 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() + endif() + if(mglItestArm STREQUAL "Split") + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.Split.DescPool." + TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}" + TEST_FILTER "DescriptorPoolGrowthScenario.*" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${mglItestMagmaM2Environment}") + 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") + endif() + endforeach() +endif() + # P10 B: ASYNC COMPILATION, THE XFB QUERY PAIR AND GL_PRIMITIVES_GENERATED ON MAGMA, all three # arms, GATED, in the band flavour (role-split state, strict errors, run-ahead). The DirectGLES # half of the first two is registered beside the other Espryt arms; the third is Magma's alone. diff --git a/MobileGL/MG_IntegrationTest/Scenarios/DescriptorPoolGrowthScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/DescriptorPoolGrowthScenario.cpp index 8cc3c342..62bbcb1f 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/DescriptorPoolGrowthScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/DescriptorPoolGrowthScenario.cpp @@ -36,6 +36,7 @@ #include "../Harness/HeadlessGL.h" #include "../Harness/ScenarioFixture.h" +#include "../Harness/SplitRuntimePeek.h" #if !defined(__ANDROID__) #include @@ -97,6 +98,20 @@ void main() { o_color = texture(uTex, vec2(0.5)); } return (end != nullptr && *end == '\0') ? parsed : fallback; } + // P11 M2: HOW MANY OF THE BURST'S SETS ONE FRAME SLOT HOLDS AT ONCE. All kDraws of them on + // the monolith arm. Under a split transport the server draws through the WIRE arm, which + // rewinds a slot's set cursors every UniformManager::kWireDescriptorSetBudget (2048) sets once + // it has proved the GPU done with them (VulkanRenderer::RewindWireDescriptorSetsIfDue), so its + // cache holds the burst 2048 sets at a time: the burst still needs a set per draw, grows the + // slot geometrically to that and is given back by the same trim, only the census counts the + // sets held rather than the draws. (The census itself is in-process - inproc it is read on the + // server's apply thread; spawn and tcp answer unavailable - see GetDescriptorPoolCensus.) + constexpr std::uint64_t kWireDescriptorSetBudget = 2048; + std::uint64_t BurstSetsHeldAtOnce() { + const std::uint64_t draws = static_cast(kDraws); + return PeekSplitRuntime().transportResolved ? std::min(draws, kWireDescriptorSetBudget) : draws; + } + // Pools a geometric policy adds for n sets from an empty flavour: 64, 128, ... up to the cap, // then one per cap. The old first-pool rule needed n/128 or n/256 of them. std::uint64_t GeometricPoolBound(std::uint64_t n) { @@ -269,15 +284,16 @@ void main() { o_color = texture(uTex, vec2(0.5)); } if (!IsVulkan() || !after.available) return; // Falsifiability first: had the renderer's memos collapsed the draws onto shared sets, the // slot would never have needed to grow and the bound below would pass on any policy. - ASSERT_GE(after.cachedSets, before.cachedSets + kDraws) + const std::uint64_t held = BurstSetsHeldAtOnce(); + ASSERT_GE(after.cachedSets, before.cachedSets + held) << "the frame did not allocate one descriptor set per draw, so it does not exercise growth. Before: " << Describe(before) << "; after: " << Describe(after); const std::uint64_t added = after.pools - before.pools; RecordProperty("census_before", Describe(before)); RecordProperty("census_after", Describe(after)); - EXPECT_LE(added, GeometricPoolBound(kDraws + 1)) - << "a frame needing " << kDraws << " descriptor sets added " << added << " pools; doubling the slot's " - << "largest pool needs at most " << GeometricPoolBound(kDraws + 1) << ". Before: " << Describe(before) + EXPECT_LE(added, GeometricPoolBound(held + 1)) + << "a frame needing " << held << " descriptor sets at once added " << added << " pools; doubling the " + << "slot's largest pool needs at most " << GeometricPoolBound(held + 1) << ". Before: " << Describe(before) << "; after: " << Describe(after); } @@ -327,19 +343,20 @@ void main() { o_color = texture(uTex, vec2(0.5)); } RecordProperty("quiet_frames", std::to_string(quietFrames)); RecordProperty("census_after_quiet", Describe(afterQuiet)); RecordProperty("census_after_second_burst", Describe(afterSecondBurst)); - ASSERT_GE(afterBurst.cachedSets, static_cast(kDraws)) + const std::uint64_t held = BurstSetsHeldAtOnce(); + ASSERT_GE(afterBurst.cachedSets, held) << "the burst did not allocate one descriptor set per draw: " << Describe(afterBurst); EXPECT_GT(afterQuiet.slotTrims, afterBurst.slotTrims) << quietFrames << " frames of one draw each followed a " << kDraws << "-set frame, but no frame slot was " << "trimmed (MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES=" << trimFrames << "). After the burst: " << Describe(afterBurst) << "; after the quiet frames: " << Describe(afterQuiet); - EXPECT_LE(afterQuiet.cachedSets + kDraws / 2, afterBurst.cachedSets) + EXPECT_LE(afterQuiet.cachedSets + held / 2, afterBurst.cachedSets) << "the trim did not give the burst's sets back. After the burst: " << Describe(afterBurst) << "; after the quiet frames: " << Describe(afterQuiet); EXPECT_LT(afterQuiet.pools, afterBurst.pools) << "the trim did not give the burst's pools back. After the burst: " << Describe(afterBurst) << "; after the quiet frames: " << Describe(afterQuiet); - EXPECT_GE(afterSecondBurst.cachedSets, afterQuiet.cachedSets + kDraws) + EXPECT_GE(afterSecondBurst.cachedSets, afterQuiet.cachedSets + held) << "the second burst was not served by freshly allocated sets: " << Describe(afterSecondBurst); } diff --git a/MobileGL/MG_IntegrationTest/Scenarios/OversizedRecordingSplitScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/OversizedRecordingSplitScenario.cpp index b6e7deec..2be186c3 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/OversizedRecordingSplitScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/OversizedRecordingSplitScenario.cpp @@ -41,6 +41,7 @@ #include #include "../Harness/HeadlessGL.h" +#include "../Harness/PipeStatsWindow.h" #include "../Harness/ScenarioFixture.h" #ifdef GLAPI @@ -123,29 +124,21 @@ void main() { o_color = vec4(1.0); } return (end != nullptr && *end == '\0') ? parsed : 0; } - // The library log, for the arming assertion. Same machinery and reasoning as - // UnlocatedIoBlockScenario: MOBILEGL_LOG_FILE_PATH is read at log-init, every process in - // the lane appends to the file, and only bytes appended after the snapshot count. - std::filesystem::path LibraryLogPath() { - const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH"); - return (path != nullptr && *path != '\0') ? std::filesystem::path(path) : std::filesystem::path(); - } + // The library log, for the arming assertion. MOBILEGL_LOG_FILE_PATH is read at log-init, + // every process in the lane appends to it, and only bytes appended after the snapshot + // count. P11 M2: ON A SPLIT BUILD IT IS A BASE NAME - the library writes one file per role + // (PipeStatsWindow.h) - and the latched line comes from the RENDERER, which under a + // transport is the server role: the apply thread under inproc, the spawned server process + // under spawn (it inherits this entry's environment, budget and log base included). So the + // mark and the read cover both roles, as PrimitivesGeneratedNoXfbScenario's arming does; a + // read of the literal base found no file and reported an unsplit renderer. In the pull + // build the client role is the whole log. + std::string LibraryLogPath() { return PipeStatsWindow::LibraryLogPath(); } - std::uintmax_t LibraryLogSize() { - std::error_code ec; - const std::filesystem::path path = LibraryLogPath(); - if (path.empty()) return 0; - const std::uintmax_t size = std::filesystem::file_size(path, ec); - return ec ? 0 : size; - } + PipeStatsWindow::LogMark LibraryLogSize() { return PipeStatsWindow::MarkLaneLog(); } - std::string LibraryLogSince(std::uintmax_t offset) { - const std::filesystem::path path = LibraryLogPath(); - if (path.empty()) return {}; - std::ifstream file(path, std::ios::binary); - if (!file.good()) return {}; - file.seekg(static_cast(offset)); - return std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + std::string LibraryLogSince(const PipeStatsWindow::LogMark& mark) { + return PipeStatsWindow::ReadLaneLogSince(mark); } std::string Describe(const Rgba8& c) { @@ -338,7 +331,7 @@ void main() { o_color = vec4(1.0); } return ReadPixels(kSide, kSide); } - std::uintmax_t m_logBefore = 0; + PipeStatsWindow::LogMark m_logBefore{}; GLuint m_vao = 0; GLuint m_vbo = 0; GLuint m_rectProgram = 0; diff --git a/scripts/ci/spawn_lane_parity.py b/scripts/ci/spawn_lane_parity.py index 7bff0bf8..425ed80a 100644 --- a/scripts/ci/spawn_lane_parity.py +++ b/scripts/ci/spawn_lane_parity.py @@ -58,7 +58,13 @@ FIXTURE_ENTRIES = {"TcpServer.Start", "TcpServer.Stop"} # the server's own view/attachment identity out of this process. # A cross-process hold and a cross-process cache peek are server-side knobs neither exists yet; # until they do, naming the two scenarios here keeps the tolerance from widening for anything else. -MAGMA_INPROC_ONLY = ("MagmaRunAheadScenario.", "MagmaWireCacheScenario.") +# DescriptorPoolGrowthScenario (P11 M2, the `.DescPool.` entry): both cases read the renderer's +# pool census (DirectVulkan::GetDescriptorPoolCensus), an in-process read - +# on spawn and tcp the pools live in the server process and the census +# answers unavailable, so a copy there checks pixels under a name claiming +# the pools. The pixels do run on every arm: the Espryt entries and the +# Magma informational and full-suite tiers. +MAGMA_INPROC_ONLY = ("MagmaRunAheadScenario.", "MagmaWireCacheScenario.", "DescriptorPoolGrowthScenario.") # 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 @@ -101,6 +107,18 @@ MAGMA_INPROC_ONLY = ("MagmaRunAheadScenario.", "MagmaWireCacheScenario.") # SERVER's VulkanRenderer reads to pin the GL_PRIMITIVES_GENERATED reroute on - the knob-free # `.PrimGen.` copies (the bring-up probe's Auto verdict) keep their tcp entries. Eight cases, eight # tails. +# P11 M2: the `.SplitRecording64.` entries set MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER=64, which +# the SERVER's VulkanRenderer reads (SplitOversizedRecording), and assert the line it latches - the +# knob-free `.Oversized.` copies (the default budget, dev's control) keep their tcp entries. Eight +# cases, eight tails. +SPLIT_RECORDING_CASES = ("EveryDrawOfTheFrameLandsAcrossCommandBufferSplits", + "ADepthOccluderDrawnBeforeTheSplitHidesADrawAfterIt", + "AStencilMarkMadeBeforeTheSplitGatesADrawAfterIt", + "BlendingAccumulatesOntoPixelsWrittenBeforeTheSplit", + "AMultisampleTargetKeepsItsSamplesAcrossTheSplit", + "OcclusionQueriesOpenAcrossTheSplitCountBothSides", + "APrimitivesGeneratedQueryOpenAcrossTheSplitCountsBothSides", + "ATransformFeedbackCaptureActiveAcrossTheSplitAppendsEveryDraw") PRIMGEN_REROUTE_CASES = ("CountsADrawMadeWithNoCaptureSpan", "CountsUnderRasterizerDiscardWithNoCaptureSpan", "CountsATessellatedPatchWithNoCaptureSpan", @@ -119,7 +137,8 @@ MAGMA_SERVER_ENV_KNOB_NO_TCP = (".ShaderMip1.", ".ShaderMip2.", ".DepthMip.", ".Reclaim.MagmaWireReclaimScenario.ManySmallRespecifyAndDrawRoundsStayUnderTheStoreCountCeiling", ".Reclaim.MagmaWireReclaimScenario.ADrawAfterTheEarlyReclaimFollowsTheNewStoreNotTheMemoizedHandle", ".Reclaim.MagmaWireReclaimScenario.DescriptorSetsRewindInsideOneLongFrameAfterTheirSubmitRetires") + tuple( - f".PrimGenReroute.PrimitivesGeneratedNoXfbScenario.{case}" for case in PRIMGEN_REROUTE_CASES) + f".PrimGenReroute.PrimitivesGeneratedNoXfbScenario.{case}" for case in PRIMGEN_REROUTE_CASES) + tuple( + f".SplitRecording64.OversizedRecordingSplitScenario.{case}" for case in SPLIT_RECORDING_CASES) # P11 A1: THE MIRROR SHAPE - entries that exist on tcp ALONE, by construction. `.AdoptTier0.` runs