mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_IntegrationTest, docs] (P7): the texture upload shape becomes a gate, with the view remap inside the workload
P3b/P4b R-11 / D-D4, landing under the P7 program (PLAN-PH-P34B-P7.md 1.2, wave 2-D
package D2). P4a recorded the emission shape rather than gating it because the
per-storage-owner cursor with view/owner remapping was the next phase's and gating an
unfinished shape pins a non-answer. That work landed, so the RecordProperty block is
now EXPECT_EQ against a gold row MEASURED on this cursor, and the workload carries a
third texture written through a glTextureView so the remap is inside the counted
window.
The gold row, identical on monolith / inproc / spawn / tcp:
emit=9 box=9 rect=0 jobs=9, client emitter 9
keyed on the workload constants by static_assert. P4a's emit=6 box=6 rect=0 jobs=6
was a two-texture workload with no client emitter and is not comparable.
The client half is read from MGPipeTextureEmitter::SubDataCount() rather than from
ctu=, and that is a correction: under spawn and tcp the two roles are two processes
whose PipeStats windows close on different Present cadences (measured ctu=13 against
emit=9, and usually not yet written when the case reads), so ctu == emit is not a
well-posed comparison there. The log field is kept as a record and asserted only
where the transport says the two are window-aligned - asking the NUMBER was the first
mistake, because the server publishes a readable ctu=0 under spawn and the comparison
silently became 0 == 9. PipeStatsWindow gains LastFromClientLog/LastFromServerLog.
Split/spawn/tcp arms added as explicit per-arm blocks rather than through
mgl_itest_register_split_arms: the macro hands every arm the shared environment, and
this case needs a lane marker, PipeStats period 1, a push mask and above all a
MOBILEGL_LOG_FILE_PATH private to the arm - the library opens the log "w", so two
entries sharing a path truncate each other and the failure looks exactly like "the
counter was never emitted".
Red-once (R-16): the prescribed flip of summedArea*4 >= unionArea*3 leaves the gate
GREEN, and that is a finding. Two gates downstream dominate it - the server needs
dirtyRectCount >= 2, and UnpackRingAvailable() zeroes the rect list by design ("one
box, one job", the existing Mali-cliff mitigation). So rect=0 is the policy's
intended output, not an accident of the scatter pattern. The red-once that bites
splits the union box in two AND disables the unpack ring: box=6 rect=3 jobs=12
against the gold, while 26 pixel cases stay green. Four attempts and their reasons
are in the note.
The Mali frame-time delta D-D4 asks for beside the gold is NOT schedulable - one
Redmi, an Adreno, no Mali part - and is recorded as a declared deviation in the
scenario header and MEASUREMENTS.md 13.3 rather than modelled.
Also corrects the three places that said the client emitter had not landed: the
scenario header, the CMake probe's comment, and gates-v1.md's re-run table.
TextureEmit.h's EmitOneLevel has been incrementing it.
This commit is contained in:
@@ -640,9 +640,16 @@ if (MOBILEGL_PIPE_PUSH)
|
||||
# question with a different owner's file behind it (review F-m7). ctu= is emitted by PipeStats
|
||||
# in EVERY push build whether or not anything increments it, so "the counter read zero" and
|
||||
# "no client emitter exists" are the same number and TextureUploadShape cannot tell them apart
|
||||
# from the summary line alone. Once package B's texture emitter lands, an emitter that stopped
|
||||
# emitting would read exactly like no emitter at all and the two-sided assertion the scenario
|
||||
# exists for would pass while comparing nothing. This probe is what separates them.
|
||||
# from the summary line alone. An emitter that STOPPED emitting reads exactly like no emitter
|
||||
# at all in that field, and the two-sided assertion the scenario exists for would pass while
|
||||
# comparing nothing. This probe is what separates them.
|
||||
#
|
||||
# THAT EMITTER HAS LANDED (P3b/P4b wave 2-D corrects this comment's tense):
|
||||
# MG_Impl/Pipe/TextureEmit.h's EmitOneLevel increments
|
||||
# CallClass::ClientTextureUploadEmissions on every piece it emits, so the probe hits and the
|
||||
# else() arm below is unreachable on this tree. It is KEPT rather than deleted because it is
|
||||
# what makes the arming decision falsifiable in both directions - a probe that cannot report
|
||||
# "absent" cannot be shown to be looking for the right symbol.
|
||||
mgl_itest_probe_for_symbol(MGL_ITEST_CLIENT_TEXTURE_UPLOAD_EMITTER
|
||||
"${MGL_ITEST_ROOT}/MobileGL/MG_Impl/Pipe" "ClientTextureUploadEmissions")
|
||||
if (MGL_ITEST_CLIENT_TEXTURE_UPLOAD_EMITTER)
|
||||
@@ -1646,21 +1653,32 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_OBJECT_SUBSYSTEM_NO_CONSUMER_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# --- The texture upload shape: RECORDED, NOT GATED in P4a (D-D4) ----------------------
|
||||
# --- The texture upload shape: A GATE as of P3b/P4b R-11 (D-D4) -----------------------
|
||||
#
|
||||
# SSIM is blind to the box-versus-rect upload shape and the Mali cliff it hides is ~+6 ms/frame, so
|
||||
# the shape needs a number - two numbers, in fact, the server's tex[emit= box= rect= jobs=] and the
|
||||
# client's emit[ctu=], which agreeing is the whole reason both are published (D-L). What this lane
|
||||
# asserts is that the numbers could be READ, that the server bracket's own arithmetic holds and that
|
||||
# the two sides agree when both are non-zero; WHICH shape each texture took is RecordProperty'd and
|
||||
# printed for MEASUREMENTS.md. P3b/P4b turns it into a gate against a gold standard, with the Mali
|
||||
# frame-time delta published beside it - gating a shape this phase has not finished deciding would
|
||||
# either pin today's shape as the answer or fail on the change that is the next phase's point.
|
||||
# client's emit[ctu=], which agreeing is the whole reason both are published (D-L).
|
||||
#
|
||||
# P4a RECORDED those numbers rather than gating them, because the per-storage-owner emission cursor
|
||||
# with view/owner index remapping - what actually decides the shape for a texture uploaded through a
|
||||
# VIEW - was the next phase's and gating an unfinished shape pins a non-answer. THAT WORK LANDED
|
||||
# (MG_Impl/Pipe/TextureEmit.h's owner-keyed drain list; TextureObjectView::ToOwner*), so this lane's
|
||||
# RecordProperty block is now EXPECT_EQ against a gold row measured on this cursor, and the workload
|
||||
# carries a third texture written through a glTextureView so the remap is inside the counted window.
|
||||
#
|
||||
# MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 IS THEREFORE MANDATORY HERE, not decorative: without it the
|
||||
# case SKIPS rather than dropping the third texture, because a conditional workload behind a fixed
|
||||
# gold row is a gate that passes by shrinking.
|
||||
#
|
||||
# THE MALI FRAME-TIME DELTA D-D4 asks for beside the gold is NOT SCHEDULABLE in this phase - one
|
||||
# Redmi, an Adreno, no Mali part - and is recorded as a declared deviation in the scenario header
|
||||
# and in docs/Disaggregated/MEASUREMENTS.md rather than modelled.
|
||||
#
|
||||
# Its own log path and its own single-case filter, for the per-lane-log reason above.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_TEXTURE_UPLOAD_SHAPE_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_TEXTURE_UPLOAD_SHAPE_LANE=1"
|
||||
"MOBILEGL_PIPE_PUSH=0x1fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/texture-upload-shape-DirectGLES.log"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
|
||||
@@ -1671,6 +1689,7 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
RESOURCE_LOCK texture-upload-shape-DirectGLES.log
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_TEXTURE_UPLOAD_SHAPE_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
@@ -2661,6 +2680,64 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
"TextureViewAliasScenario.*-TextureViewAliasScenario.TheOwnerUploadDrainRunsAgainAfterADrawBoundary"
|
||||
"")
|
||||
|
||||
# ---- D2: THE UPLOAD-SHAPE GOLD, UNDER ALL THREE TRANSPORTS ------------------------------
|
||||
#
|
||||
# WRITTEN OUT PER ARM RATHER THAN THROUGH mgl_itest_register_split_arms, and the reason is
|
||||
# structural rather than stylistic: the macro hands every arm the SHARED
|
||||
# MGL_ITEST_GLES_<ARM>_ENVIRONMENT, and this case needs four things no shared list carries -
|
||||
# MGITEST_TEXTURE_UPLOAD_SHAPE_LANE=1 (without it the case skips), MOBILEGL_PIPE_STATS=1 with
|
||||
# PERIOD=1, MOBILEGL_PIPE_PUSH=0x1fff, and a MOBILEGL_LOG_FILE_PATH PRIVATE TO THE ARM. That
|
||||
# last one is not negotiable: the library opens the log fopen(path, "w"), so two entries
|
||||
# sharing a path truncate each other under `ctest -j` and the failure looks exactly like "the
|
||||
# counter was never emitted". The PersistentMapArm pair above is the same shape for the same
|
||||
# reason. A macro call here would produce three entries that all green-skip.
|
||||
#
|
||||
# WHAT THE SPLIT ARMS ADD OVER THE MONOLITH LANE. The monolith lane reads one log, because
|
||||
# there is one role. Under SPAWN and TCP the client and the server are two PROCESSES with two
|
||||
# independent counter sets - the client's line carries ctu= and the server's carries tex[] -
|
||||
# and the `ctu == emit` comparison the whole scenario exists for is only a real comparison
|
||||
# there. Under inproc it is one process with two log FILES, which is the arm that proves the
|
||||
# role-derived path reading without needing two counter sets.
|
||||
set(MGL_D2_TUS_FILTER "TextureUploadShapeScenario.TheEmittedUploadShapeIsRecordedAndTheTwoSidesAgree")
|
||||
foreach(mglD2Arm Split Spawn Tcp)
|
||||
string(TOLOWER "${mglD2Arm}" mglD2ArmLower)
|
||||
set(mglD2Log "${CMAKE_CURRENT_BINARY_DIR}/texture-upload-shape-${mglD2ArmLower}-DirectGLES.log")
|
||||
set(mglD2Extra)
|
||||
if(mglD2Arm STREQUAL "Split")
|
||||
set(mglD2Transport "MOBILEGL_TRANSPORT=inproc"
|
||||
"MOBILEGL_IPC_SERVER_PATH=${MGL_ITEST_SPLIT_SERVER_PATH}")
|
||||
elseif(mglD2Arm STREQUAL "Spawn")
|
||||
set(mglD2Transport "MOBILEGL_TRANSPORT=spawn"
|
||||
"MOBILEGL_IPC_SERVER_PATH=${MGL_ITEST_SPLIT_SERVER_PATH}")
|
||||
else()
|
||||
set(mglD2Transport "MOBILEGL_TRANSPORT=spawn"
|
||||
"MOBILEGL_IPC_CONTROL=${MOBILEGL_ITEST_TCP_ENDPOINT}"
|
||||
"MOBILEGL_IPC_DATA=stream" "MOBILEGL_IPC_REQUIRE_SAME_BUILD=1"
|
||||
"MOBILEGL_IPC_LOG_FORWARD=1" "MOBILEGL_IPC_TOKEN="
|
||||
"MGITEST_TCP_LANE=1" "MOBILEGL_ITEST_REQUIRE_GPU=1")
|
||||
set(mglD2Extra FIXTURES_REQUIRED mobilegl-tcp)
|
||||
endif()
|
||||
mgl_itest_join_environment(MGL_D2_TUS_ENV_${mglD2Arm}
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" ${mglD2Transport} "MGITEST_SPLIT_LANE=1"
|
||||
"MGITEST_TEXTURE_UPLOAD_SHAPE_LANE=1" "MOBILEGL_PIPE_PUSH=0x1fff"
|
||||
"MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1"
|
||||
"MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${mglD2Log}"
|
||||
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
|
||||
get_filename_component(mglD2LogName "${mglD2Log}" NAME)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.${mglD2Arm}.TextureUploadShape."
|
||||
TEST_FILTER "${MGL_D2_TUS_FILTER}"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-${mglD2ArmLower}"
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
RESOURCE_LOCK "${mglD2LogName}"
|
||||
${mglD2Extra}
|
||||
ENVIRONMENT "${MGL_D2_TUS_ENV_${mglD2Arm}}"
|
||||
)
|
||||
endforeach()
|
||||
|
||||
# The split half of the counting pair. MGITEST_PERSISTENT_MAP_ARM=emulated is R-6: under split
|
||||
# the adopt tier is pinned at T2, the resource owner declines every acquisition and the client
|
||||
# pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith
|
||||
|
||||
@@ -177,6 +177,37 @@ namespace MGITest::PipeStatsWindow {
|
||||
return Last(ReadLaneLog());
|
||||
}
|
||||
|
||||
// ---- ONE ROLE AT A TIME, and P3b/P4b wave 2-D package D2 is why it had to exist -----------
|
||||
//
|
||||
// LastFromLaneLog() concatenates and takes the LAST window, which is the right answer for a
|
||||
// counter only ONE role ever increments. It is the wrong answer for a claim that compares two
|
||||
// roles' readings of the SAME records - TextureUploadShape's `tex[emit=]` (the server's) and
|
||||
// `ctu=` (the client's) - because under SPAWN and TCP the two roles are two PROCESSES with two
|
||||
// independent sets of counters. The client's line then carries `ctu=N tex[emit=0]` and the
|
||||
// server's carries `ctu=0 tex[emit=N]`, and whichever line happens to be last answers BOTH
|
||||
// questions with one role's numbers. The comparison silently becomes `N == 0` or `0 == N`.
|
||||
//
|
||||
// Under INPROC it happens to work either way - one process, one set of atomic counters, two
|
||||
// log FILES - and that is exactly why this had to be written against the spawn shape rather
|
||||
// than discovered by reading the inproc lane.
|
||||
//
|
||||
// Both return `found == false` where that role has no log (the pull build has no server half;
|
||||
// a lane that configured no MOBILEGL_LOG_FILE_PATH has neither), which a caller must treat as
|
||||
// "could not look" rather than as a zero.
|
||||
inline Window LastFromClientLog() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MGPipeSyncPeerLog();
|
||||
#endif
|
||||
return Last(ReadWholeFile(LibraryLogPath()));
|
||||
}
|
||||
|
||||
inline Window LastFromServerLog() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MGPipeSyncPeerLog();
|
||||
#endif
|
||||
return Last(ReadWholeFile(ServerLibraryLogPath()));
|
||||
}
|
||||
|
||||
// One counter out of that line, by its short name ("mpr", "draws", "csom"), or -1 when the
|
||||
// line does not carry it. The search includes the SEPARATOR before the name and the `=` after
|
||||
// it, so "draws" cannot match "draws/f=" and "mpr" cannot match a longer name ending in it -
|
||||
|
||||
@@ -6,55 +6,76 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - THE TEXTURE UPLOAD SHAPE, RECORDED (BRIEF-P4A.md D-D4). NOT A GATE IN P4a.
|
||||
// Scenario - THE TEXTURE UPLOAD SHAPE, GATED (BRIEF-P4A.md D-D4; P3b/P4b R-11, wave 2-D).
|
||||
//
|
||||
// WHAT IT IS FOR. SSIM is completely blind to the difference between "one union box" and "N
|
||||
// separate rects", and that difference is the Mali upload cliff: Mali prices an upload by the
|
||||
// number of JOBS, and ~100 one-rect sprite jobs against one union box measured +6 ms/frame
|
||||
// (ARCHITECTURE.md:249-251). Every P4a gate can be green while the emission shape has silently
|
||||
// inverted, so the shape needs a number - and there are TWO numbers, deliberately:
|
||||
// (ARCHITECTURE.md:249-251). Every gate in the phase can be green while the emission shape has
|
||||
// silently inverted, so the shape needs a number - and there are TWO numbers, deliberately:
|
||||
//
|
||||
// tex[emit= box= rect= jobs=] the SERVER's count, Espryt's own, which has existed since P2
|
||||
// emit[ctu=] the CLIENT's count of the same records (CallClass::
|
||||
// ClientTextureUploadEmissions, minted by the P4a contract commit)
|
||||
// ClientTextureUploadEmissions)
|
||||
//
|
||||
// The two agreeing is the whole reason both are printed (D-L). An emission-shape divergence between
|
||||
// the client that decides the rect model and the server that pays the GPU cost is then a difference
|
||||
// of two published numbers rather than something only a device can see.
|
||||
// The two agreeing is the whole reason both are printed (D-L). An emission-shape divergence
|
||||
// between the client that decides the rect model and the server that pays the GPU cost is then a
|
||||
// difference of two published numbers rather than something only a device can see.
|
||||
//
|
||||
// WHY IT IS RECORDED AND NOT GATED, and this is a scope decision rather than a hedge. ROADMAP.md:23
|
||||
// puts "dirty 归属反转(按存储属主键控的发射游标)" in the P3b/P4b cell: P4a lands the flat drain
|
||||
// list, and the per-storage-owner emission cursor with view/owner index remapping - which is what
|
||||
// actually decides the shape for a texture uploaded through a VIEW - is the next phase's. Gating a
|
||||
// shape the phase has not finished deciding would either pin today's shape as if it were the
|
||||
// answer, or fail on a change that is the point of the next phase. So P4a BUILDS the scenario (it
|
||||
// is meaningless without P4a's records) and runs it as a RECORDED comparison; P3b/P4b turns it into
|
||||
// a gate with the Mali frame-time delta published beside it (D-D4).
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// WHY IT IS A GATE NOW, AND WHAT CHANGED SINCE P4a. The P4a header said, correctly for its tree:
|
||||
// "the per-storage-owner emission cursor with view/owner index remapping - which is what actually
|
||||
// decides the shape for a texture uploaded through a VIEW - is the next phase's", and gating a
|
||||
// shape the phase had not finished deciding would have pinned a non-answer. THAT WORK LANDED:
|
||||
// the cursor is keyed on the storage owner (TextureEmit.h's drain list), the remap is
|
||||
// TextureObjectView::ToOwner*, and TextureViewAliasScenario is its behavioural gate. So the
|
||||
// numbers are no longer provisional and the RecordProperty block became EXPECT_EQ against the
|
||||
// table below.
|
||||
//
|
||||
// WHAT IT THEREFORE ASSERTS, and it is not nothing:
|
||||
// THE CLIENT EMITTER LANDED TOO, and three places in the tree said otherwise until this commit:
|
||||
// this header, the CMake probe's comment, and notes/p4a/p4a-results/gates-v1.md's re-run table.
|
||||
// MG_Impl/Pipe/TextureEmit.h's EmitOneLevel increments CallClass::ClientTextureUploadEmissions on
|
||||
// every piece it emits, so `ctu=` is a live reading and `ctu == emit` is a live assertion. The
|
||||
// CMake probe's `else()` arm is kept - it is what makes the arming decision falsifiable - but it
|
||||
// is unreachable on this tree.
|
||||
//
|
||||
// THE MALI FRAME-TIME DELTA IS NOT PUBLISHED BESIDE THESE NUMBERS AND WILL NOT BE IN THIS PHASE.
|
||||
// D-D4 asks for the +6 ms/frame reading next to the shape gold. It is NOT SCHEDULABLE: the phase
|
||||
// has one device, a Redmi with an Adreno, and no Mali part at all. Recorded as a DECLARED
|
||||
// DEVIATION here and in docs/Disaggregated/MEASUREMENTS.md ("Adreno-only; Mali delta deferred")
|
||||
// rather than faked from a model or copied from the 2026 measurement's conditions. What the gate
|
||||
// therefore pins is the SHAPE, on the reasoning that the shape is the cliff's cause and the frame
|
||||
// time is its consequence on one vendor's hardware.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
//
|
||||
// WHAT IT ASSERTS:
|
||||
//
|
||||
// 1. the numbers could be READ AT ALL - the counters exist, the window covers the workload, and
|
||||
// the workload really uploaded (a run that uploaded nothing would record four zeroes and look
|
||||
// the workload really uploaded (a run that uploaded nothing would read four zeroes and look
|
||||
// exactly like a healthy run whose emitter had been switched off);
|
||||
// 2. the internal ARITHMETIC of the server's own bracket holds: emit == box + rect, and
|
||||
// jobs >= emit, because a box emission is one job and a rect-list emission is N;
|
||||
// 3. the client and the server agree on the RECORD COUNT when both are non-zero (ctu == emit).
|
||||
// Not "when the client is non-zero": on the P4a contract tree the client emits nothing and
|
||||
// that is a SKIP-shaped observation, not a divergence.
|
||||
// 3. the client and the server agree on the RECORD COUNT (ctu == emit);
|
||||
// 4. THE GOLD TABLE: emit / box / rect / jobs are exactly kGold's, which is the assertion the
|
||||
// other three cannot make - they are all satisfied by a shape that inverted wholesale.
|
||||
//
|
||||
// Everything else - which shape each texture took, and whether that is the right shape - is
|
||||
// RECORDED with RecordProperty and printed, for MEASUREMENTS.md and for the P3b/P4b gate to be
|
||||
// written against.
|
||||
//
|
||||
// THE WORKLOAD is the shape the decision is about: one texture receiving MANY SMALL SCATTERED
|
||||
// SUB-REGIONS per frame (the sprite-atlas / chunk-renderer shape), and one receiving a single large
|
||||
// contiguous one. The first is where the box-versus-rect choice is made - MipmapStorage's 96-rect
|
||||
// cascade and the summedArea*4 >= unionArea*3 union-box fallback - and the second is the control
|
||||
// that must always be one box whatever the policy is.
|
||||
// THE WORKLOAD is the shape the decision is about, in three textures:
|
||||
// * MANY SMALL SCATTERED SUB-REGIONS per frame (the sprite-atlas / chunk-renderer shape). This
|
||||
// is where the box-versus-rect choice is made - MipmapStorage's 96-rect cascade and the
|
||||
// summedArea*4 >= unionArea*3 union-box fallback;
|
||||
// * one large CONTIGUOUS region, the control that must always be one box whatever the policy is;
|
||||
// * the same scattered pattern written THROUGH A glTextureView of the third texture's level 1,
|
||||
// so the view/owner index remap is inside the counted window. Without it the gold table would
|
||||
// pin a shape for exactly the input the P3b/P4b work changed and say nothing about the input
|
||||
// it changed it for.
|
||||
//
|
||||
// DIRECTGLES ONLY. The server-side counters are Espryt's (Managers.cpp:6360-6395); Magma's upload
|
||||
// path is P7 and contributes nothing to them, so a DirectVulkan lane would record a bracket of
|
||||
// zeroes and call it a shape.
|
||||
//
|
||||
// VIEWS ARE OPT-IN ON ESPRYT, so every lane that runs this case sets
|
||||
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1. A lane without it SKIPS rather than dropping the third
|
||||
// texture: a conditional workload behind a fixed gold table is a gate that passes by shrinking.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
@@ -66,6 +87,7 @@
|
||||
#include "../Harness/PipeStatsWindow.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
#include "../Harness/TextureEmitPeek.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
@@ -78,8 +100,8 @@
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// Set by the TextureUploadShape. ctest entry and by nothing else; a harness marker, never
|
||||
// read by the library.
|
||||
// Set by the TextureUploadShape. ctest entries and by nothing else; a harness marker,
|
||||
// never read by the library.
|
||||
constexpr const char* kLaneMarker = "MGITEST_TEXTURE_UPLOAD_SHAPE_LANE";
|
||||
|
||||
constexpr int kInset = 2;
|
||||
@@ -91,6 +113,47 @@ namespace MGITest {
|
||||
constexpr int kScatteredRects = 40;
|
||||
constexpr int kRectSize = 2;
|
||||
constexpr int kFrames = 3;
|
||||
// The viewed texture's OWNER is twice the atlas so that its level 1 - which is what the
|
||||
// view opens onto - is exactly kAtlasSize and the same scattered pattern applies to it.
|
||||
constexpr int kViewedOwnerSize = kAtlasSize * 2;
|
||||
constexpr int kViewedOwnerLevels = 2;
|
||||
constexpr int kViewMinLevel = 1;
|
||||
|
||||
// ---- THE GOLD TABLE -------------------------------------------------------------------
|
||||
//
|
||||
// MEASURED ON THIS CURSOR, not inherited. The P4a record for the two-texture workload was
|
||||
// `emit=6 box=6 rect=0 jobs=6` with `ctu=0`; this row is the three-texture workload's own
|
||||
// reading taken after the client emitter and the owner-keyed cursor landed, and the P4a
|
||||
// numbers are quoted only so that a reader can see they are not the same claim.
|
||||
//
|
||||
// KEYED ON THE WORKLOAD CONSTANTS, and the static_assert below is the key: change
|
||||
// kScatteredRects, kFrames, the number of contiguous bands or the number of textures and
|
||||
// these numbers describe a workload that no longer exists. The assert makes that a compile
|
||||
// error rather than a mysterious red.
|
||||
//
|
||||
// WHY box IS THE WHOLE OF emit ON THIS TREE. GetDirtyRects (MipmapStorage.cpp:430-457)
|
||||
// hands the emitter a rect list only when the list has 2..maxRects entries AND the rects'
|
||||
// summed area is under 3/4 of the union box. This workload's rects are spread on a coarse
|
||||
// stride and the cascade merges every pair that touches, so the emitter takes the box arm
|
||||
// for all three textures in all three frames. That is the SHAPE being pinned: a change
|
||||
// that starts emitting rect lists here is exactly the +6 ms/frame inversion, and it is
|
||||
// what the red-once for this gate produces on purpose.
|
||||
struct UploadShapeGold {
|
||||
long long Emissions;
|
||||
long long BoxEmissions;
|
||||
long long RectEmissions;
|
||||
long long Jobs;
|
||||
};
|
||||
constexpr int kTexturesInTheWindow = 3;
|
||||
constexpr UploadShapeGold kGold{9, 9, 0, 9};
|
||||
static_assert(kScatteredRects == 40 && kFrames == 3 && kTexturesInTheWindow == 3,
|
||||
"the gold table is keyed on the workload constants: re-measure before "
|
||||
"changing kScatteredRects, kFrames or the texture count");
|
||||
static_assert(kGold.Emissions == kGold.BoxEmissions + kGold.RectEmissions,
|
||||
"the gold row must itself satisfy the arithmetic the case asserts");
|
||||
static_assert(kGold.Emissions == kTexturesInTheWindow * kFrames,
|
||||
"one emission per texture per frame is the shape; a gold row that does not "
|
||||
"say so is pinning an accident");
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
@@ -146,6 +209,8 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
if (m_scattered != 0) glDeleteTextures(1, &m_scattered);
|
||||
if (m_contiguous != 0) glDeleteTextures(1, &m_contiguous);
|
||||
if (m_view != 0) glDeleteTextures(1, &m_view);
|
||||
if (m_viewedOwner != 0) glDeleteTextures(1, &m_viewedOwner);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer);
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
@@ -184,6 +249,46 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
"PipeStats";
|
||||
return;
|
||||
}
|
||||
if (!TextureViewUsable()) {
|
||||
// A SKIP AND NOT A SMALLER WORKLOAD. The gold table has one row; dropping the
|
||||
// third texture here would leave a case that asserts emit==9 against a
|
||||
// six-emission workload, i.e. a gate that fails for the wrong reason - or,
|
||||
// worse, a second gold row that nobody re-measures.
|
||||
GTEST_SKIP() << "GL_ARB_texture_view is not advertised on this backend, so the "
|
||||
"third texture - the one whose upload goes through a "
|
||||
"glTextureView, which is the input the P3b/P4b emission cursor "
|
||||
"work changed - cannot be built. Every lane this case is "
|
||||
"registered in sets MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool TextureViewUsable() {
|
||||
GLuint storage = 0;
|
||||
glGenTextures(1, &storage);
|
||||
glBindTexture(GL_TEXTURE_2D, storage);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
GLuint view = 0;
|
||||
glGenTextures(1, &view);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
|
||||
const bool usable = glGetError() == GL_NO_ERROR;
|
||||
glDeleteTextures(1, &view);
|
||||
glDeleteTextures(1, &storage);
|
||||
return usable;
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t> GreenTexels(int width, int height) {
|
||||
std::vector<std::uint8_t> texels(static_cast<std::size_t>(width) * height * 4);
|
||||
for (std::size_t i = 0; i < texels.size(); i += 4) {
|
||||
texels[i] = 0;
|
||||
texels[i + 1] = 255;
|
||||
texels[i + 2] = 0;
|
||||
texels[i + 3] = 255;
|
||||
}
|
||||
return texels;
|
||||
}
|
||||
|
||||
GLuint MakeAtlas(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||
@@ -206,6 +311,57 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
return texture;
|
||||
}
|
||||
|
||||
// The third texture's OWNER: immutable, two levels, seeded green on both. glTextureView
|
||||
// requires immutable storage, so this one cannot be a MakeAtlas.
|
||||
GLuint MakeViewedOwner() {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, kViewedOwnerLevels, GL_RGBA8, kViewedOwnerSize,
|
||||
kViewedOwnerSize);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
for (int level = 0; level < kViewedOwnerLevels; ++level) {
|
||||
const int size = kViewedOwnerSize >> level;
|
||||
const std::vector<std::uint8_t> green = GreenTexels(size, size);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, level, 0, 0, size, size, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
green.data());
|
||||
}
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A view of the owner's LEVEL 1, which is kAtlasSize square: the view's own level 0.
|
||||
// Every glTexSubImage2D through this name has to be remapped to the owner's level 1 by
|
||||
// TextureObjectView::ToOwnerLevel before it reaches the drain list, and the emission it
|
||||
// produces has to land on the OWNER's cursor.
|
||||
GLuint MakeLevelView(GLuint owner) {
|
||||
GLuint view = 0;
|
||||
glGenTextures(1, &view);
|
||||
glTextureView(view, GL_TEXTURE_2D, owner, GL_RGBA8, kViewMinLevel, 1, 0, 1);
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
||||
<< "a single-level 2D view of a 2D texture is a legal view pair";
|
||||
glBindTexture(GL_TEXTURE_2D, view);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return view;
|
||||
}
|
||||
|
||||
void ScatterRectsInto(GLuint texture) {
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
const std::uint8_t patch[kRectSize * kRectSize * 4] = {
|
||||
0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255};
|
||||
for (int rect = 0; rect < kScatteredRects; ++rect) {
|
||||
const int x = ((rect * 7) % (kAtlasSize / kRectSize)) * kRectSize;
|
||||
const int y = ((rect * 5) % (kAtlasSize / kRectSize)) * kRectSize;
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, kRectSize, kRectSize, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, patch);
|
||||
}
|
||||
}
|
||||
|
||||
Image DrawSampled(GLuint texture) {
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
@@ -223,6 +379,8 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
GLuint m_quadBuffer = 0;
|
||||
GLuint m_scattered = 0;
|
||||
GLuint m_contiguous = 0;
|
||||
GLuint m_viewedOwner = 0;
|
||||
GLuint m_view = 0;
|
||||
};
|
||||
|
||||
TEST_F(TextureUploadShapeScenario, TheEmittedUploadShapeIsRecordedAndTheTwoSidesAgree) {
|
||||
@@ -232,10 +390,13 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
|
||||
m_scattered = MakeAtlas(0, 255, 0);
|
||||
m_contiguous = MakeAtlas(0, 255, 0);
|
||||
m_viewedOwner = MakeViewedOwner();
|
||||
m_view = MakeLevelView(m_viewedOwner);
|
||||
// The first draw of each texture uploads its whole level, which is not the shape this
|
||||
// scenario is about; it happens in the SETUP window, before the one that is read.
|
||||
DrawSampled(m_scattered);
|
||||
DrawSampled(m_contiguous);
|
||||
DrawSampled(m_view);
|
||||
BindDefaultFramebuffer();
|
||||
Gl().EndFrame();
|
||||
// The server closes/resets the stats window inside Present. Under
|
||||
@@ -247,118 +408,220 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
}
|
||||
|
||||
// ---- the counted window ----
|
||||
//
|
||||
// THE CLIENT HALF IS READ FROM THE EMITTER, NOT FROM ctu=, and that is a correction
|
||||
// this package had to make rather than a preference. MEASURED on all four arms:
|
||||
//
|
||||
// monolith one role, one log: the window line carries tex[emit=9] AND ctu=9.
|
||||
// inproc two log FILES, one process, one set of counters: the SERVER's line
|
||||
// carries both, and the client's log holds only the "counters ON" banner
|
||||
// at the moment this case reads it.
|
||||
// spawn/tcp two PROCESSES with two counter sets. The server's line carries
|
||||
// tex[emit=9] ctu=0; the client process emits its own summary on a
|
||||
// DIFFERENT Present cadence and had written only the banner by the time
|
||||
// this case reads - and when it does write one, its window covers the
|
||||
// setup emissions too (measured ctu=13 against emit=9), so the two
|
||||
// numbers are not windowed on the same work and `ctu == emit` is not a
|
||||
// well-posed comparison there at all.
|
||||
//
|
||||
// So the log's ctu= is kept as a RECORD (printed, and asserted where it is
|
||||
// window-aligned) and the ASSERTION reads MGPipeTextureEmitter's own counter through
|
||||
// the in-process peek. That is the same counter's SOURCE - EmitOneLevel increments
|
||||
// both on the same line - and the test process is the CLIENT under every one of
|
||||
// monolith / inproc / spawn / tcp, so the delta below is the client's true count of
|
||||
// the records this workload produced, in every arm. D-L asks for the two sides to
|
||||
// agree; this is what makes that a real question outside monolith.
|
||||
TextureEmitCountersPeek emitBefore{};
|
||||
const bool emitPeekLive = PeekTextureEmitCounters(&emitBefore);
|
||||
|
||||
Image lastScattered;
|
||||
Image lastContiguous;
|
||||
Image lastViewed;
|
||||
for (int frame = 0; frame < kFrames; ++frame) {
|
||||
// MANY SMALL SCATTERED RECTS: the shape whose box-versus-rect decision is the whole
|
||||
// subject. They are spread over the atlas on a coarse stride so that their union
|
||||
// box is most of the texture and their summed area is a small fraction of it -
|
||||
// which is the input the summedArea*4 >= unionArea*3 fallback is written for.
|
||||
glBindTexture(GL_TEXTURE_2D, m_scattered);
|
||||
const std::uint8_t patch[kRectSize * kRectSize * 4] = {
|
||||
0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255};
|
||||
for (int rect = 0; rect < kScatteredRects; ++rect) {
|
||||
const int x = ((rect * 7) % (kAtlasSize / kRectSize)) * kRectSize;
|
||||
const int y = ((rect * 5) % (kAtlasSize / kRectSize)) * kRectSize;
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, kRectSize, kRectSize, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, patch);
|
||||
}
|
||||
ScatterRectsInto(m_scattered);
|
||||
lastScattered = DrawSampled(m_scattered);
|
||||
|
||||
// ONE LARGE CONTIGUOUS REGION: the control. Whatever the policy is, this is one
|
||||
// box and one job, and a reading where it is not says the policy has stopped
|
||||
// looking at the region at all.
|
||||
glBindTexture(GL_TEXTURE_2D, m_contiguous);
|
||||
std::vector<std::uint8_t> band(static_cast<std::size_t>(kAtlasSize) * 8 * 4);
|
||||
for (std::size_t i = 0; i < band.size(); i += 4) {
|
||||
band[i] = 0;
|
||||
band[i + 1] = 255;
|
||||
band[i + 2] = 0;
|
||||
band[i + 3] = 255;
|
||||
}
|
||||
const std::vector<std::uint8_t> band = GreenTexels(kAtlasSize, 8);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kAtlasSize, 8, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
band.data());
|
||||
lastContiguous = DrawSampled(m_contiguous);
|
||||
|
||||
// THE SAME SCATTERED PATTERN THROUGH A VIEW. The writes name the view's level 0;
|
||||
// the drain has to key them on the OWNER at the owner's level 1, and the shape the
|
||||
// emitter then chooses has to be the same one the owner's own name would have got.
|
||||
// A cursor that keyed on the view would show up here as a DIFFERENT emit count,
|
||||
// not as a wrong picture.
|
||||
ScatterRectsInto(m_view);
|
||||
lastViewed = DrawSampled(m_view);
|
||||
}
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the upload workload left a GL error behind";
|
||||
TextureEmitCountersPeek emitAfter{};
|
||||
const bool emitPeekClosed = emitPeekLive && PeekTextureEmitCounters(&emitAfter);
|
||||
Gl().EndFrame(); // the swap that emits the window covering exactly the work above
|
||||
if (const auto runtime = PeekSplitRuntime(); runtime.transportResolved) {
|
||||
ASSERT_TRUE(runtime.sessionActive);
|
||||
ASSERT_TRUE(WaitForSplitAppliedForTesting(runtime.emitSeq));
|
||||
}
|
||||
|
||||
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog();
|
||||
ASSERT_TRUE(window.found)
|
||||
// ---- READING TWO ROLES, NOT ONE LINE --------------------------------------------
|
||||
//
|
||||
// Under SPAWN and TCP the client and the server are two PROCESSES with two independent
|
||||
// sets of counters: the client's summary line carries `ctu=N tex[emit=0]` and the
|
||||
// server's carries `ctu=0 tex[emit=N]`. A single concatenated read takes whichever line
|
||||
// came last and answers BOTH questions with one role's numbers, which turns the
|
||||
// two-sided comparison into `N == 0`. So each side is read from ITS OWN role's log, and
|
||||
// the server bracket falls back to the client log only where there is no server half -
|
||||
// the pull build and the monolith lane, where one file IS the whole run.
|
||||
const PipeStatsWindow::Window clientWindow = PipeStatsWindow::LastFromClientLog();
|
||||
const PipeStatsWindow::Window serverWindow = PipeStatsWindow::LastFromServerLog();
|
||||
ASSERT_TRUE(clientWindow.found)
|
||||
<< "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath()
|
||||
<< ", so the shape could not be read at all";
|
||||
RecordProperty("stats_line", window.line.c_str());
|
||||
const PipeStatsWindow::Window& shapeWindow =
|
||||
(serverWindow.found && PipeStatsWindow::CounterOrAbsent(serverWindow, "emit") > 0)
|
||||
? serverWindow
|
||||
: clientWindow;
|
||||
RecordProperty("stats_line_client", clientWindow.line.c_str());
|
||||
if (serverWindow.found) RecordProperty("stats_line_server", serverWindow.line.c_str());
|
||||
|
||||
const long long emissions = PipeStatsWindow::CounterOrAbsent(window, "emit");
|
||||
const long long box = PipeStatsWindow::CounterOrAbsent(window, "box");
|
||||
const long long rect = PipeStatsWindow::CounterOrAbsent(window, "rect");
|
||||
const long long jobs = PipeStatsWindow::CounterOrAbsent(window, "jobs");
|
||||
const long long clientEmissions = PipeStatsWindow::CounterOrAbsent(window, "ctu");
|
||||
ASSERT_GE(emissions, 0) << "the summary line carries no tex[emit=]: " << window.line;
|
||||
ASSERT_GE(box, 0) << "no box=: " << window.line;
|
||||
ASSERT_GE(rect, 0) << "no rect=: " << window.line;
|
||||
ASSERT_GE(jobs, 0) << "no jobs=: " << window.line;
|
||||
const long long emissions = PipeStatsWindow::CounterOrAbsent(shapeWindow, "emit");
|
||||
const long long box = PipeStatsWindow::CounterOrAbsent(shapeWindow, "box");
|
||||
const long long rect = PipeStatsWindow::CounterOrAbsent(shapeWindow, "rect");
|
||||
const long long jobs = PipeStatsWindow::CounterOrAbsent(shapeWindow, "jobs");
|
||||
// The log's ctu=, from whichever role's window carries it. Under monolith and inproc
|
||||
// that is the same window the tex[] bracket came from; under spawn and tcp it is a
|
||||
// different process's differently-windowed counter, so it is RECORDED and only
|
||||
// asserted where the two are window-aligned (see the note over the counted window).
|
||||
const long long shapeWindowCtu = PipeStatsWindow::CounterOrAbsent(shapeWindow, "ctu");
|
||||
const long long clientWindowCtu = PipeStatsWindow::CounterOrAbsent(clientWindow, "ctu");
|
||||
// ALIGNED IS A PROPERTY OF THE TRANSPORT, NOT OF THE NUMBER, and asking the number
|
||||
// was this case's first mistake: `shapeWindowCtu >= 0` is TRUE under spawn, where the
|
||||
// server's line publishes a perfectly readable ctu=0 that belongs to a process which
|
||||
// emitted nothing, and the comparison became 0 == 9. Asking the process which
|
||||
// transport it resolved cannot be satisfied by a counter that happens to read zero -
|
||||
// and a single-process arm whose ctu= really did collapse to 0 still reds, which is
|
||||
// the failure the alignment check must not swallow.
|
||||
const SplitRuntimeState statsRuntime = PeekSplitRuntime();
|
||||
const bool oneProcessForCounters =
|
||||
!statsRuntime.transportResolved || statsRuntime.transportName == "inproc";
|
||||
const bool ctuIsWindowAligned = oneProcessForCounters && shapeWindowCtu >= 0;
|
||||
const long long clientEmissions =
|
||||
emitPeekClosed
|
||||
? static_cast<long long>(emitAfter.SubDataCount - emitBefore.SubDataCount)
|
||||
: shapeWindowCtu;
|
||||
ASSERT_GE(emissions, 0) << "the summary line carries no tex[emit=]: " << shapeWindow.line;
|
||||
ASSERT_GE(box, 0) << "no box=: " << shapeWindow.line;
|
||||
ASSERT_GE(rect, 0) << "no rect=: " << shapeWindow.line;
|
||||
ASSERT_GE(jobs, 0) << "no jobs=: " << shapeWindow.line;
|
||||
|
||||
// THE RECORD. This is the deliverable of this scenario in P4a: a number, in the ctest
|
||||
// XML and in the log, for MEASUREMENTS.md and for the P3b/P4b gate to be written
|
||||
// against. Printed as well as recorded, because a RecordProperty is invisible in a
|
||||
// console run.
|
||||
std::cout << "[ TextureUploadShape ] backend=" << Gl().BackendName() << " frames=" << kFrames
|
||||
<< " scattered_rects_per_frame=" << kScatteredRects
|
||||
<< " textures=" << kTexturesInTheWindow
|
||||
<< " server[emit=" << emissions << " box=" << box << " rect=" << rect
|
||||
<< " jobs=" << jobs << "] client[ctu=" << clientEmissions << "]" << std::endl;
|
||||
<< " jobs=" << jobs << "] client[emitter=" << clientEmissions
|
||||
<< " ctu_shape_window=" << shapeWindowCtu
|
||||
<< " ctu_client_window=" << clientWindowCtu << "]" << std::endl;
|
||||
RecordProperty("ctu_shape_window", static_cast<int>(shapeWindowCtu));
|
||||
RecordProperty("ctu_client_window", static_cast<int>(clientWindowCtu));
|
||||
RecordProperty("ctu_is_window_aligned", ctuIsWindowAligned ? 1 : 0);
|
||||
RecordProperty("server_emissions", static_cast<int>(emissions));
|
||||
RecordProperty("server_box_emissions", static_cast<int>(box));
|
||||
RecordProperty("server_rect_emissions", static_cast<int>(rect));
|
||||
RecordProperty("server_upload_jobs", static_cast<int>(jobs));
|
||||
RecordProperty("client_emissions", static_cast<int>(clientEmissions));
|
||||
|
||||
// 1. the workload really uploaded. Without this the three assertions below are all
|
||||
// 0 == 0 and a run whose emitter was switched off records the same "healthy" shape
|
||||
// as one that worked.
|
||||
// 1. the workload really uploaded. Without this the assertions below are all 0 == 0 and
|
||||
// a run whose emitter was switched off records the same "healthy" shape as one that
|
||||
// worked.
|
||||
ASSERT_GT(emissions, 0)
|
||||
<< "the server counted no texture upload emission at all over " << kFrames
|
||||
<< " frames of " << kScatteredRects
|
||||
<< " sub-regions each plus a contiguous band. Either the uploads never reached the "
|
||||
"backend or the counter stopped counting; in both cases every shape number below "
|
||||
"would be a zero that means nothing. "
|
||||
<< window.line;
|
||||
<< " sub-regions each, plus a contiguous band and a view write. Either the uploads "
|
||||
"never reached the backend or the counter stopped counting; in both cases every "
|
||||
"shape number below would be a zero that means nothing. "
|
||||
<< shapeWindow.line;
|
||||
|
||||
// 2. the server bracket's own arithmetic.
|
||||
EXPECT_EQ(box + rect, emissions)
|
||||
<< "tex[box=] + tex[rect=] must be tex[emit=]: every emission takes exactly one of "
|
||||
"the two shapes. " << window.line;
|
||||
"the two shapes. " << shapeWindow.line;
|
||||
EXPECT_GE(jobs, emissions)
|
||||
<< "tex[jobs=] must be at least tex[emit=]: a box emission is one driver upload job "
|
||||
"and a rect-list emission is N. " << window.line;
|
||||
"and a rect-list emission is N. " << shapeWindow.line;
|
||||
|
||||
// 3. the two sides agree, WHEN THERE ARE TWO SIDES - and "there are two sides" is
|
||||
// 3. THE GOLD TABLE (R-11 / D-D4). This is the assertion the three around it cannot
|
||||
// make: emit == box + rect and jobs >= emit are both satisfied by a shape that
|
||||
// inverted wholesale from N boxes to N rect lists, which is the +6 ms/frame
|
||||
// direction on Mali and is invisible to SSIM.
|
||||
EXPECT_EQ(emissions, kGold.Emissions)
|
||||
<< "the emission COUNT moved. One resource_subdata per (storage owner, upload "
|
||||
"target, level) per frame is " << kTexturesInTheWindow << " textures x " << kFrames
|
||||
<< " frames = " << kGold.Emissions
|
||||
<< ". A count above this says a level is being emitted more than once per validate "
|
||||
"point - the two-cursor shape, if the extra one is the viewed texture; a count "
|
||||
"below says a level's record was dropped. " << shapeWindow.line;
|
||||
EXPECT_EQ(box, kGold.BoxEmissions)
|
||||
<< "the BOX/RECT split moved: " << box << " box emissions against a gold of "
|
||||
<< kGold.BoxEmissions << ". " << shapeWindow.line;
|
||||
EXPECT_EQ(rect, kGold.RectEmissions)
|
||||
<< "the BOX/RECT split moved: " << rect << " rect-list emissions against a gold of "
|
||||
<< kGold.RectEmissions
|
||||
<< ". This is the direction that costs ~+6 ms/frame on Mali (ARCHITECTURE.md:249-"
|
||||
"251): the driver is handed N upload jobs where it used to be handed one, and no "
|
||||
"SSIM case can see it. If this is intended, re-measure the row and say so in "
|
||||
"docs/Disaggregated/MEASUREMENTS.md. " << shapeWindow.line;
|
||||
EXPECT_EQ(jobs, kGold.Jobs)
|
||||
<< "the JOB count moved: " << jobs << " against a gold of " << kGold.Jobs
|
||||
<< ". Jobs are what Mali prices the upload by. " << shapeWindow.line;
|
||||
|
||||
// 4. the two sides agree, WHEN THERE ARE TWO SIDES - and "there are two sides" is
|
||||
// answered by the BUILD, not by the number (review F-m7).
|
||||
//
|
||||
// ctu= IS ALWAYS PRESENT IN A PUSH BUILD: PipeStats.cpp writes the field whether or
|
||||
// not anything ever incremented the counter, so `clientEmissions > 0` conflated
|
||||
// three different trees - "no client emitter exists", "the emitter exists and
|
||||
// emitted nothing", and "the counter was not published at all" - into one branch
|
||||
// that asserts nothing and prints a sentence that is only true of the first. Once
|
||||
// package B's texture emitter lands, an emitter that STOPPED emitting would read
|
||||
// exactly like no emitter at all and this case would have gone green over it, which
|
||||
// is the failure mode the whole scenario exists to make impossible.
|
||||
// that asserts nothing. So the discriminator is
|
||||
// MGITEST_PIPE_CLIENT_TEXTURE_UPLOAD_EMITTER_PRESENT, the build's own content probe
|
||||
// for a MG_Impl/Pipe source that emits CallClass::ClientTextureUploadEmissions.
|
||||
//
|
||||
// So the discriminator is MGITEST_PIPE_CLIENT_TEXTURE_UPLOAD_EMITTER_PRESENT, the
|
||||
// build's own content probe for a MG_Impl/Pipe source that emits
|
||||
// CallClass::ClientTextureUploadEmissions - the same mechanism as every other arming
|
||||
// decision in this package - and each side of it asserts something real.
|
||||
// THAT PROBE HITS ON THIS TREE: MG_Impl/Pipe/TextureEmit.h's EmitOneLevel increments
|
||||
// it on every piece. The else-arm below is retained because it is what makes the
|
||||
// arming decision falsifiable in both directions, not because it is reachable here.
|
||||
const bool clientEmitterExists =
|
||||
BuildMarkerIsSet("MGITEST_PIPE_CLIENT_TEXTURE_UPLOAD_EMITTER_PRESENT");
|
||||
ASSERT_GE(clientEmissions, 0)
|
||||
<< "the summary line carries no ctu= field at all, in a push build, where PipeStats "
|
||||
"publishes it unconditionally. The client half of the comparison cannot be read: "
|
||||
<< window.line;
|
||||
<< "the client half of the comparison could not be read at all. The in-process "
|
||||
"emitter peek "
|
||||
<< (emitPeekLive ? "opened but did not close" : TextureEmitPeekSkipReason())
|
||||
<< ", and no role's summary line carried a ctu= field either. client="
|
||||
<< clientWindow.line << " shape=" << shapeWindow.line;
|
||||
RecordProperty("client_emitter_present", clientEmitterExists ? 1 : 0);
|
||||
RecordProperty("client_half_source", emitPeekClosed ? "emitter peek" : "ctu= field");
|
||||
if (ctuIsWindowAligned) {
|
||||
// Where the two roles ARE one window, the published field must agree with the
|
||||
// emitter it is published from. A divergence here is PipeStats mis-publishing,
|
||||
// which is a different defect from an emission-shape divergence and has to stay
|
||||
// distinguishable from it.
|
||||
EXPECT_EQ(shapeWindowCtu, clientEmissions)
|
||||
<< "the published ctu= field and MGPipeTextureEmitter's own SubDataCount "
|
||||
"disagree in a window that covers the same work. EmitOneLevel increments "
|
||||
"both on the same line, so this is the counter's publication and not the "
|
||||
"emission shape. " << shapeWindow.line;
|
||||
} else {
|
||||
RecordProperty("ctu_note",
|
||||
"the client's summary window is not aligned with the server's in "
|
||||
"this arm (two processes, two Present cadences), so ctu= is "
|
||||
"recorded and the assertion reads the in-process emitter instead");
|
||||
}
|
||||
if (clientEmitterExists) {
|
||||
EXPECT_GT(clientEmissions, 0)
|
||||
<< "a MG_Impl/Pipe source emits CallClass::ClientTextureUploadEmissions on this "
|
||||
@@ -366,15 +629,18 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
<< " texture upload emissions for this workload, but the client counted NONE. An "
|
||||
"emitter that has stopped emitting reads exactly like no emitter at all in "
|
||||
"this field, which is why this case asks the build rather than the number. "
|
||||
<< window.line;
|
||||
<< clientWindow.line;
|
||||
EXPECT_EQ(clientEmissions, emissions)
|
||||
<< "the CLIENT counted " << clientEmissions
|
||||
<< " texture upload records and the SERVER counted " << emissions
|
||||
<< " for the same workload in the same window. The two counting the same records "
|
||||
"is the entire reason both are published (D-L): a divergence here is an "
|
||||
"emission-shape divergence that SSIM is blind to and that costs ~+6 ms/frame "
|
||||
"on Mali when it goes the wrong way. "
|
||||
<< window.line;
|
||||
"on Mali when it goes the wrong way. client=" << clientWindow.line
|
||||
<< " server=" << shapeWindow.line;
|
||||
EXPECT_EQ(clientEmissions, kGold.Emissions)
|
||||
<< "the client's own count against the gold row, so that a run where BOTH sides "
|
||||
"moved together is still red. " << clientWindow.line;
|
||||
} else {
|
||||
// Not merely "not asserted": on a tree with no client emitter the counter must be
|
||||
// ZERO, and a non-zero one would mean the probe is looking for the wrong symbol -
|
||||
@@ -385,12 +651,7 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
"tree, yet the client counted " << clientEmissions
|
||||
<< " of them. Something is incrementing that counter which this build's probe "
|
||||
"cannot see, so the probe is looking for the wrong symbol and this case's "
|
||||
"arming decision is unreliable in both directions. " << window.line;
|
||||
std::cout << "[ TextureUploadShape ] no P4a client emitter has landed on this tree "
|
||||
"(the build's ClientTextureUploadEmissions probe found none), so this "
|
||||
"run records the SERVER shape only and pins ctu=0. That is the expected "
|
||||
"reading on the contract tree and it is not a divergence."
|
||||
<< std::endl;
|
||||
"arming decision is unreliable in both directions. " << clientWindow.line;
|
||||
RecordProperty("client_side", "absent");
|
||||
}
|
||||
|
||||
@@ -402,6 +663,9 @@ void main() { oColor = texture(uTex, vUv); }
|
||||
EXPECT_TRUE(RegionIsMostly(lastContiguous, kInset, lastContiguous.Width() - kInset, kInset,
|
||||
lastContiguous.Height() - kInset, "green", 0.0,
|
||||
"the contiguous-band atlas"));
|
||||
EXPECT_TRUE(RegionIsMostly(lastViewed, kInset, lastViewed.Width() - kInset, kInset,
|
||||
lastViewed.Height() - kInset, "green", 0.0,
|
||||
"the atlas written through a glTextureView"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -772,3 +772,50 @@ P6(spawn transport)的实现包(a6 / c6 / lk / so / sm / cp / hs / dl / st
|
||||
- **门 8① 的两种门铃只在一条负载、一台设备上比过**,且参照注记里的 1600 spins/帧 属于更重的场景;若存在差异,应在每帧会合次数更多的负载上才看得见。
|
||||
- **spawn client 的窗口化字段仍不可信**,而且这一项是**结构**不是缺陷:要让 spawn client 推进窗口,得让 client 进程到达一次 `PipeStats::OnPresent`,而 client 的帧边界在 spawn 下是 `EmitPresent`(每条 present 记录一次)——接上去会与 backend 的帧边界重复计数。**不要顺手接**。
|
||||
- **`MOBILEGL_IPC_STAGE_MB` 同时是 arena 大小与 chunk 预算的来源**,所以分块的净效果没有被单变量隔离(§12.5 的 `tex` +194%)。
|
||||
|
||||
## 13. P3b/P4b wave 2-D 包 D2(`7ed5da52`,WSL 桌面 lavapipe/llvmpipe)
|
||||
|
||||
### 13.1 上传形状金标(R-11 / D-D4,`TextureUploadShapeScenario` 由记录升为门)
|
||||
|
||||
工作负载常量 `kScatteredRects=40`、`kFrames=3`、一条连续带、**三张**纹理(第三张经
|
||||
`glTextureView` 的 level 1 上传,使 view/属主重映射落在被计数的窗口内)。
|
||||
|
||||
| 臂 | emit | box | rect | jobs | client emitter |
|
||||
|---|---|---|---|---|---|
|
||||
| `DirectGLES.TextureUploadShape.`(monolith) | 9 | 9 | 0 | 9 | 9 |
|
||||
| `DirectGLES.Split.`(inproc) | 9 | 9 | 0 | 9 | 9 |
|
||||
| `DirectGLES.Spawn.` | 9 | 9 | 0 | 9 | 9 |
|
||||
| `DirectGLES.Tcp.` | 9 | 9 | 0 | 9 | 9 |
|
||||
|
||||
P4a 的记录是两张纹理、无 client emitter 的 `emit=6 box=6 rect=0 jobs=6`(`ctu=0`),与本行
|
||||
**不可比**:工作负载不同。
|
||||
|
||||
`rect=0` 不是散点图案的偶然,而是 unpack-ring 策略的既定输出:`Managers.cpp:8652` 在
|
||||
`UnpackRingAvailable()` 时把 `dirtyRectCount` 清零("One box, one job",即 Mali 悬崖的既有缓解),
|
||||
`:8764` 的 `rectShape` 还要求 `dirtyRectCount >= 2`。因此**客户端任何谓词翻转都动不了这四个数**
|
||||
——翻转 `summedArea*4 >= unionArea*3` 全绿,见 `notes/p34b/espryt-d2.md` 的四次尝试。红一次由
|
||||
"把 union box 切成两半 + `MOBILEGL_ESPRYT_DISABLE_UNPACK_RING=1`"做到:`box=6 rect=3 jobs=12`,
|
||||
而 26 个像素用例全绿。
|
||||
|
||||
### 13.2 client 半边的读法(本包的修正)
|
||||
|
||||
`ctu=` 与 `tex[]` 是否在同一个窗口里,按传输分:
|
||||
|
||||
| 臂 | `tex[]` 在哪 | `ctu=` 在哪 |
|
||||
|---|---|---|
|
||||
| monolith | 唯一日志 | 同一行 |
|
||||
| inproc | **server** 日志(两个文件、一个进程、一套计数器) | 同一行 |
|
||||
| spawn / tcp | **server** 日志 | client 进程自己的计数器,**Present 节奏不同**——实测 `ctu=13` 对 `emit=9`,且用例读取时通常还没写出 |
|
||||
|
||||
这与 §12.7 最后一条"spawn client 的窗口化字段仍不可信(是结构不是缺陷)"是同一件事,从集成用例
|
||||
这一侧再次证实。因此断言改读**进程内的 `MGPipeTextureEmitter::SubDataCount()`**(同一个计数器的源
|
||||
头,测试进程在四个臂下都是 client),`ctu=` 只在窗口对齐时断言;是否对齐由
|
||||
`PeekSplitRuntime()` 回答的传输决定,**不是**由数值是否 >= 0 决定——spawn 下 server 会发布一个
|
||||
完全可读的 `ctu=0`,按数值判会把比较悄悄变成 `0 == 9`。
|
||||
|
||||
### 13.3 未跑项 / 不可调度项
|
||||
|
||||
- **Mali 帧时增量(D-D4 要求与金标并列)不可调度。** 本阶段只有一台 Redmi(Adreno 830v2),
|
||||
**无任何 Mali 设备**。按 Adreno 记录形状,偏差在此与场景头部注明:**Adreno-only; Mali delta
|
||||
deferred**。不建模、不从 2026 年那次测量的条件推算。形状是悬崖的**因**,帧时是它在某一家硬件
|
||||
上的**果**——门守的是因。
|
||||
|
||||
@@ -98,3 +98,100 @@ from `os.environ` and strips only the transport keys). A feature knob that reach
|
||||
three is the worst of the three states: the lane is green and tests nothing.
|
||||
|
||||
---
|
||||
|
||||
## Slice 2 — `TextureUploadShapeScenario` promoted from record to gate (R-11 / D-D4)
|
||||
|
||||
`RecordProperty` → `EXPECT_EQ` against a gold row measured on this cursor, plus a third texture
|
||||
uploaded through a `glTextureView` so the view/owner remap is inside the counted workload.
|
||||
|
||||
### The gold row
|
||||
|
||||
Workload constants `kScatteredRects=40`, `kFrames=3`, one contiguous band, three textures.
|
||||
|
||||
| | emit | box | rect | jobs | client emitter |
|
||||
|---|---|---|---|---|---|
|
||||
| **gold (this cursor)** | **9** | **9** | **0** | **9** | **9** |
|
||||
| P4a's record (two textures, no client emitter) | 6 | 6 | 0 | 6 | ctu=0 |
|
||||
|
||||
Identical on all four arms — `DirectGLES.TextureUploadShape.` (monolith), `.Split.`, `.Spawn.`,
|
||||
`.Tcp.`.
|
||||
|
||||
### The client half is read from the emitter, not from `ctu=`
|
||||
|
||||
Measured, and it is a correction rather than a preference:
|
||||
|
||||
| arm | where `tex[]` is | where `ctu=` is |
|
||||
|---|---|---|
|
||||
| monolith | the one log | same line |
|
||||
| inproc | the SERVER's log (two files, one process, one counter set) | same line |
|
||||
| spawn / tcp | the SERVER's log | the CLIENT process's own counters, on a **different Present cadence** — measured `ctu=13` against `emit=9`, and usually not yet written at all when the case reads |
|
||||
|
||||
So `ctu == emit` is not a well-posed comparison under spawn/tcp from the summary lines. The log
|
||||
field is kept as a RECORD and asserted only where the two are window-aligned (decided by asking
|
||||
`PeekSplitRuntime()` which transport resolved — **not** by testing whether the number is >= 0,
|
||||
which was this case's first mistake: the server publishes a perfectly readable `ctu=0` under
|
||||
spawn and the comparison silently became `0 == 9`). The ASSERTION reads
|
||||
`MGPipeTextureEmitter::SubDataCount()` in-process, which is the same counter's source and is the
|
||||
client's true count in every arm, because the test process is the client under all four.
|
||||
|
||||
`Harness/PipeStatsWindow.h` gains `LastFromClientLog()` / `LastFromServerLog()` for this.
|
||||
|
||||
### Red-once (R-16)
|
||||
|
||||
**The prescribed flip does not reach this workload, and that is a finding.** Flipping
|
||||
`summedArea * 4 >= unionArea * 3` in `MipmapStorage::GetDirtyRects` leaves the gate GREEN. Four
|
||||
attempts, in order, all green:
|
||||
|
||||
1. flip `summedArea*4 >= unionArea*3` → green
|
||||
2. `rects.size() < 2` → `rects.empty()` → green
|
||||
3. remove the union-box re-seed in `MarkDirtyRegion` → green
|
||||
4. `MOBILEGL_ESPRYT_DISABLE_UNPACK_RING=1` alone → green
|
||||
|
||||
The reason is two gates downstream of the predicate, either of which alone forces the box arm:
|
||||
|
||||
* `Managers.cpp:8764` `rectShape = subRectEligible && dirtyRectCount >= 2` — the SERVER needs at
|
||||
least two rects, and the client's list collapses to one for this workload; and
|
||||
* `Managers.cpp:8652` `if (BufferImpl::UnpackRingAvailable()) dirtyRectCount = 0;` — **by design**:
|
||||
"through the unpack ring every glTexSubImage is a GPU copy job (Mali), so ~100 sprite rects
|
||||
become ~100 jobs whose fixed cost dwarfs the union box's extra bytes… One box, one job."
|
||||
|
||||
So `box=9 rect=0` is not an accident of the scatter pattern: it is the unpack-ring policy's
|
||||
intended output, and no client-side predicate can move it while the ring is available. The gold
|
||||
row pins the policy's output, which is the right thing to pin.
|
||||
|
||||
The red-once that DOES bite makes `GetDirtyRects` hand back the union box cut in two AND disables
|
||||
the unpack ring:
|
||||
|
||||
```
|
||||
DirectGLES.TextureUploadShape. ... FAILED
|
||||
the BOX/RECT split moved: 6 box emissions against a gold of 9
|
||||
the BOX/RECT split moved: 3 rect-list emissions against a gold of 0
|
||||
the JOB count moved: 12 against a gold of 9
|
||||
tex[emit=9 box=6 rect=3 jobs=12]
|
||||
```
|
||||
|
||||
and all 26 pixel cases of `TextureView` / `TextureViewAlias` / `SampledSetStaleness` /
|
||||
`ImageSizeAfterRespec` / `CopyImageLevelRange` / `ClearThenReadPixels` stay GREEN under the same
|
||||
patch — the required property, and the point of the gate: SSIM and every pixel assertion are blind
|
||||
to a shape that inverted.
|
||||
|
||||
### The Mali frame-time delta is NOT published
|
||||
|
||||
D-D4 asks for the +6 ms/frame reading beside the gold. **Not schedulable**: this phase has one
|
||||
device, a Redmi with an Adreno, and no Mali part. Recorded as a declared deviation in the scenario
|
||||
header and in `docs/Disaggregated/MEASUREMENTS.md` as "Adreno-only; Mali delta deferred" rather
|
||||
than modelled or copied from the 2026 measurement's conditions. What the gate pins is the SHAPE,
|
||||
on the reasoning that the shape is the cliff's cause and the frame time is its consequence on one
|
||||
vendor's hardware.
|
||||
|
||||
### Stale prose corrected
|
||||
|
||||
All three said the client emitter had not landed. `MG_Impl/Pipe/TextureEmit.h`'s `EmitOneLevel`
|
||||
increments `CallClass::ClientTextureUploadEmissions` on every piece it emits, and the build's own
|
||||
probe hits (`MGITEST_PIPE_CLIENT_TEXTURE_UPLOAD_EMITTER_PRESENT=1` is in every lane's environment).
|
||||
|
||||
* `Scenarios/TextureUploadShapeScenario.cpp` header and the client-half comment block
|
||||
* `MG_IntegrationTest/CMakeLists.txt`, the probe's comment ("Once package B's texture emitter
|
||||
lands"). The `else()` arm is KEPT — it is what makes the arming decision falsifiable in both
|
||||
directions — but it is unreachable on this tree and now says so.
|
||||
* `notes/p4a/p4a-results/gates-v1.md`'s re-run table
|
||||
|
||||
@@ -446,7 +446,7 @@ run against the finished code):
|
||||
|---|---|
|
||||
| `ctest --test-dir build-verify -R 'HandleRecycle'` | the six ABA cases' `Handles` arm and the seven leak cases **arm themselves** when D and E land. The leak cases are the phase's only defence against P3a's C-1 shape, on both backends |
|
||||
| `ctest --test-dir build-push -R 'ObjectSubsystemControl'` and `MOBILEGL_PIPE_PUSH=0x9ff ctest -R 'ObjectSubsystemControl'` | both cases SKIP today; the On/Off A/B arms when B and C land, the refusal when D lands. The refusal lane already pins `0x9ff` itself, so D.3's separate `0x9ff` invocation is a second reading of the same thing rather than the only one |
|
||||
| `ctest --test-dir build-push -R 'TextureUploadShape'` | the client half (`ctu=`) is zero today; once B lands, the `ctu == emit` assertion is live and is the client/server shape comparison D-L exists for. **Record the new numbers** — the P4a baseline is `emit=6 box=6 rect=0 jobs=6` |
|
||||
| `ctest --test-dir build-push -R 'TextureUploadShape'` | ~~the client half (`ctu=`) is zero today; once B lands, the `ctu == emit` assertion is live~~ **DONE, P3b/P4b wave 2-D (R-11).** The client emitter landed (`MG_Impl/Pipe/TextureEmit.h`'s `EmitOneLevel`), the `ctu == emit` comparison is live, and the scenario is now a GATE against a gold row rather than a record. The workload gained a third texture written through a `glTextureView`, so the numbers are not comparable to the P4a baseline `emit=6 box=6 rect=0 jobs=6`: the row measured on `7ed5da52` is **`emit=9 box=9 rect=0 jobs=9`, client emitter 9**, identical on monolith / inproc / spawn / tcp. See `notes/p34b/espryt-d2.md` |
|
||||
| `bash scripts/p4a_descriptor_negative_control.sh build-push` | exits 2 today because both emit headers are the contract's stubs. It becomes a real G7 control the moment B and C fill them in, and it is **not** a CI lane (it rebuilds the library up to four times) |
|
||||
| `bash scripts/p4a_untouched_regions.sh 37da3c3a HEAD` | this is the gate D and E can break. It is rc 0 here because nothing on the list has been touched yet |
|
||||
| `ctest --test-dir build-push -R 'TextureParamsWithoutASamplerView'` | green here; after `esprytobj` it is the regression net that says the parameter sync did not move onto the sampler view |
|
||||
|
||||
Reference in New Issue
Block a user