[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

This commit is contained in:
2026-09-29 12:20:15 -04:00
parent e2333d98a8
commit bd2e43ede3
4 changed files with 152 additions and 31 deletions
@@ -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.<arm>.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.
@@ -36,6 +36,7 @@
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#if !defined(__ANDROID__)
#include <MG_Backend/DirectVulkan/DescriptorPoolCensus.h>
@@ -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<std::uint64_t>(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<std::uint64_t>(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);
}
@@ -41,6 +41,7 @@
#include <vector>
#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<std::streamoff>(offset));
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
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;
+21 -2
View File
@@ -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