From 5617c5c3b3dec93fa79d71a7f31e264222b16aa4 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 22 Sep 2026 16:55:40 -0400 Subject: [PATCH] [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. --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 97 +++- .../Harness/PipeStatsWindow.h | 31 ++ .../Scenarios/TextureUploadShapeScenario.cpp | 452 ++++++++++++++---- docs/Disaggregated/MEASUREMENTS.md | 47 ++ docs/Disaggregated/notes/p34b/espryt-d2.md | 97 ++++ .../notes/p4a/p4a-results/gates-v1.md | 2 +- 6 files changed, 621 insertions(+), 105 deletions(-) diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 5d3326c5..d5a83229 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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__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 diff --git a/MobileGL/MG_IntegrationTest/Harness/PipeStatsWindow.h b/MobileGL/MG_IntegrationTest/Harness/PipeStatsWindow.h index 488f20d7..87915816 100644 --- a/MobileGL/MG_IntegrationTest/Harness/PipeStatsWindow.h +++ b/MobileGL/MG_IntegrationTest/Harness/PipeStatsWindow.h @@ -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 - diff --git a/MobileGL/MG_IntegrationTest/Scenarios/TextureUploadShapeScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/TextureUploadShapeScenario.cpp index 79a8f5a4..1c72129e 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/TextureUploadShapeScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/TextureUploadShapeScenario.cpp @@ -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 #include @@ -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 GreenTexels(int width, int height) { + std::vector texels(static_cast(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 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(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 band(static_cast(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 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(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(shapeWindowCtu)); + RecordProperty("ctu_client_window", static_cast(clientWindowCtu)); + RecordProperty("ctu_is_window_aligned", ctuIsWindowAligned ? 1 : 0); RecordProperty("server_emissions", static_cast(emissions)); RecordProperty("server_box_emissions", static_cast(box)); RecordProperty("server_rect_emissions", static_cast(rect)); RecordProperty("server_upload_jobs", static_cast(jobs)); RecordProperty("client_emissions", static_cast(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 diff --git a/docs/Disaggregated/MEASUREMENTS.md b/docs/Disaggregated/MEASUREMENTS.md index f5efe026..83777b11 100644 --- a/docs/Disaggregated/MEASUREMENTS.md +++ b/docs/Disaggregated/MEASUREMENTS.md @@ -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 年那次测量的条件推算。形状是悬崖的**因**,帧时是它在某一家硬件 + 上的**果**——门守的是因。 diff --git a/docs/Disaggregated/notes/p34b/espryt-d2.md b/docs/Disaggregated/notes/p34b/espryt-d2.md index dfe894fc..45040f2d 100644 --- a/docs/Disaggregated/notes/p34b/espryt-d2.md +++ b/docs/Disaggregated/notes/p34b/espryt-d2.md @@ -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 diff --git a/docs/Disaggregated/notes/p4a/p4a-results/gates-v1.md b/docs/Disaggregated/notes/p4a/p4a-results/gates-v1.md index afe45cc3..ff56177b 100644 --- a/docs/Disaggregated/notes/p4a/p4a-results/gates-v1.md +++ b/docs/Disaggregated/notes/p4a/p4a-results/gates-v1.md @@ -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 |