[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:
2026-09-22 17:31:51 -04:00
parent da30c1301e
commit 5617c5c3b3
6 changed files with 621 additions and 105 deletions
+87 -10
View File
@@ -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
+47
View File
@@ -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 |