diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 3fa68516..d2e295c5 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -134,6 +134,8 @@ add_executable(MobileGLIntegrationTest Scenarios/CsoContentAddressingScenario.cpp Scenarios/ResourceSubsystemControlScenario.cpp Scenarios/TextureParamsWithoutASamplerViewScenario.cpp + Scenarios/TextureUploadShapeScenario.cpp + Scenarios/ObjectSubsystemControlScenario.cpp ) target_include_directories(MobileGLIntegrationTest PRIVATE @@ -564,6 +566,23 @@ if (MOBILEGL_PIPE_PUSH) endif() endforeach() + # The client-side emitter of P4a's four suppressor-visible emission counters, which is what + # ObjectSubsystemControl reads. Same shape and same reason as the CSO and map-persistent + # emitter probes above: the emitters are minted in MG_Impl/Pipe (packages B and C), header-only + # by design (D-P), so the question is "does any source there emit it", not "does a named file + # exist". Until one does, every counter in the emit[] bracket is structurally zero in BOTH arms + # of the A/B and an assertion about their difference would be a statement about nothing. + mgl_itest_probe_for_symbol(MGL_ITEST_OBJECT_EMITTER + "${MGL_ITEST_ROOT}/MobileGL/MG_Impl/Pipe" "FramebufferEmissions") + if (MGL_ITEST_OBJECT_EMITTER) + message(STATUS "Integration tests: P4a's object emissions have an emitter " + "(${MGL_ITEST_OBJECT_EMITTER})") + list(APPEND MGL_ITEST_CAPABILITY_ENV "MGITEST_PIPE_OBJECT_EMITTER_PRESENT=1") + else() + message(STATUS "Integration tests: no MG_Impl/Pipe source emits FramebufferEmissions - " + "ObjectSubsystemControl's emission case will SKIP") + endif() + mgl_itest_probe_for_symbol(MGL_ITEST_MAGMA_ABA "${MGL_ITEST_ROOT}/MobileGL/MG_Backend/DirectVulkan" "PipeHandleAbaControl") if (MGL_ITEST_MAGMA_ABA) @@ -576,8 +595,9 @@ if (MOBILEGL_PIPE_PUSH) endif() else() message(STATUS "Integration tests: pull build - HandleRecycle.{Handles,AbaControl}, " - "CsoContentAddressing and ResourceSubsystemControl stay registered (G2) and SKIP: " - "every arm they assert is compiled only under MOBILEGL_PIPE_PUSH") + "CsoContentAddressing, ResourceSubsystemControl, ObjectSubsystemControl and " + "TextureUploadShape stay registered (G2) and SKIP: every arm they assert is " + "compiled only under MOBILEGL_PIPE_PUSH") endif() mgl_itest_join_environment(MGL_ITEST_GLES_ENVIRONMENT @@ -1322,6 +1342,108 @@ gtest_discover_tests(MobileGLIntegrationTest ENVIRONMENT "${MGL_ITEST_GLES_ARENA_SUBSYSTEM_OFF_ENVIRONMENT}" ) +# --- G12: the P4a subsystem A/B, and its dependency refusal --------------------------- +# +# THE MASKS ARE THE TWO PHASE CONSTANTS, exactly as the P3a block above: 0x1fff is +# kMGPipeSubsystemsMigratedAtP4a (the push build's default) and 0x1ff is +# kMGPipeSubsystemsMigratedAtP3a, which is that default with bits 9 (framebuffer), 10 (texture +# resources), 11 (samplers) and 12 (programs) cleared. THIS is the phase's "new 0x1ff off-lane": the +# constant P3a shipped survives as P4a's A/B control, and a lane that spelled its own bit pattern +# would stop being the shape that ships the first time the default moved. +# +# THE THIRD LANE IS THE DEPENDENCY REFUSAL (D-K2) and it is the one entry in this family that is not +# vacuous before the emitters land: 0x9ff sets the sampler subsystem (bit 11) WITHOUT the texture +# resource subsystem (bit 10) that every MGPBoundView::Texture and MGPImageView::Res depends on, and +# the bring-up must log ONE error naming both bits and run the legacy sampler arm. That is a +# decision made from the bitmask alone, so it is assertable on a tree where nothing emits yet - and +# a half-honoured mask is invisible in the pixels by construction, which is why it needs an entry +# rather than a code comment. +# +# ONE CASE PER LANE, through TEST_FILTER, and it is a constraint rather than a preference: each case +# READS the library's log and the log is a per-LANE resource (the library opens it fopen(path, "w"), +# so every process in a lane truncates it). Two entries in one lane race under `ctest -j` with a +# failure indistinguishable from "the counter was never emitted". Same rule as the CSO lanes, the +# map-persistent lanes and the verify arming lane. +# +# DirectGLES only. P4a migrates Espryt's framebuffer, texture, sampler and program paths; Magma's +# are P7 and register nothing here, so a DirectVulkan lane would be measuring the client emitters +# against a backend nobody asked to change. +# +# Registered in EVERY build, including the pull build where none of the counters exists, so that +# `ctest -L integration-gpu` stays name-for-name identical between pull and push (gate G2). In a +# pull build MGITEST_PIPE_PUSH_BUILD is absent and every one of these cases skips saying so. +mgl_itest_join_environment(MGL_ITEST_GLES_OBJECT_SUBSYSTEM_ON_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_OBJECT_SUBSYSTEM_LANE=on" + "MOBILEGL_PIPE_PUSH=0x1fff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1" + "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/object-subsystem-on-DirectGLES.log" + ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV}) +mgl_itest_join_environment(MGL_ITEST_GLES_OBJECT_SUBSYSTEM_OFF_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_OBJECT_SUBSYSTEM_LANE=off" + "MOBILEGL_PIPE_PUSH=0x1ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1" + "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/object-subsystem-off-DirectGLES.log" + ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV}) +mgl_itest_join_environment(MGL_ITEST_GLES_OBJECT_SUBSYSTEM_REFUSED_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MGITEST_OBJECT_SUBSYSTEM_LANE=refused" + "MOBILEGL_PIPE_PUSH=0x9ff" "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1" + "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/object-subsystem-refused-DirectGLES.log" + ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV}) + +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.ObjectSubsystemControl.On." + TEST_FILTER "ObjectSubsystemControlScenario.ClearingTheP4aBitsStopsTheEmissionsAndNotThePixels" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_ON_ENVIRONMENT}" +) +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.ObjectSubsystemControl.Off." + TEST_FILTER "ObjectSubsystemControlScenario.ClearingTheP4aBitsStopsTheEmissionsAndNotThePixels" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_OFF_ENVIRONMENT}" +) +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.ObjectSubsystemControl.Refused." + TEST_FILTER "ObjectSubsystemControlScenario.ASamplerBitWithoutTheTextureBitIsRefusedAndNamed" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_OBJECT_SUBSYSTEM_REFUSED_ENVIRONMENT}" +) + +# --- The texture upload shape: RECORDED, NOT GATED in P4a (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. +# +# 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_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/texture-upload-shape-DirectGLES.log" + ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV}) + +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.TextureUploadShape." + TEST_FILTER "TextureUploadShapeScenario.TheEmittedUploadShapeIsRecordedAndTheTwoSidesAgree" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_TEXTURE_UPLOAD_SHAPE_ENVIRONMENT}" +) + # --- G9: the mandatory red-before scenario ------------------------------------------- # # TextureParamsWithoutASamplerViewScenario needs NO lane of its own, and that is deliberate rather diff --git a/MobileGL/MG_IntegrationTest/Scenarios/ObjectSubsystemControlScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/ObjectSubsystemControlScenario.cpp new file mode 100644 index 00000000..e934717f --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/ObjectSubsystemControlScenario.cpp @@ -0,0 +1,501 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ObjectSubsystemControlScenario.cpp +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Scenario - THE P4a SUBSYSTEM A/B IS REAL, AND ITS DEPENDENCY REFUSALS ARE EXERCISED (gate G12). +// +// P4a migrates FOUR subsystems (D-K1, MG_Pipe/MGPipe.h): +// +// bit 9 kMGPipeSubsystemFramebuffer set_framebuffer_state +// bit 10 kMGPipeSubsystemTextureResources texture + renderbuffer resource_*, set_texture_params +// bit 11 kMGPipeSubsystemSamplers sampler CSO, sampler view, the three unit sets +// bit 12 kMGPipeSubsystemPrograms shader CSO, draw/dispatch program, global constants +// +// so the push build's default mask becomes kMGPipeSubsystemsMigratedAtP4a = 0x1fff, and P3a's +// 0x1ff survives as the control that clears exactly those four - MGPipe.h's rule that every phase's +// constant keeps meaning what it meant, so an operator's recorded mask is still readable a phase +// later. THE OFF LANE IS 0x1ff AND NOT A HAND-PICKED PATTERN, for that reason. +// +// That A/B is what every "push vs pull" number in MEASUREMENTS.md is taken against, and it has one +// characteristic failure mode: the bits stop steering anything, both arms run the same code, and +// every later comparison is quietly taken against a switch that does nothing. This file is the +// entry that cannot let that happen. It is the P4a analogue of ResourceSubsystemControlScenario and +// deliberately its twin in shape. +// +// WHAT IT ASSERTS, per lane: +// +// on (MOBILEGL_PIPE_PUSH=0x1fff) +// The client emits P4a's records for the workload: a framebuffer state per bound target that +// moved, the three unit sets, and the client-side texture upload record. The window's +// emit[fbe= sve= sse= sie= ctu=] bracket therefore carries a NON-ZERO total. +// +// off (MOBILEGL_PIPE_PUSH=0x1ff, P3a's default = P4a's four subsystems cleared) +// The frontend dispatch falls through to the legacy MGB_CTX-reading arms, nothing is emitted +// through any of the four families, and every one of those five counters must read ZERO. +// This is the reading a dead switch fails: with the bits ignored, this lane would report the +// same non-zero counts as the other one. +// +// refused (MOBILEGL_PIPE_PUSH=0x9ff = bits 0..8 plus bit 11, samplers, WITHOUT bit 10) +// D-K2's dependency refusal. Every MGPBoundView::Texture and MGPImageView::Res names a +// Texture handle and only bit 10 populates the texture slot table, so a sampler subsystem +// without it would miss every lookup and walk on without unbinding. The bring-up logs ONE +// error naming BOTH bits, refuses bit 11 and runs the legacy sampler arm - modelled on the +// bit-8-requires-bit-7 refusal that already ships (Managers.cpp:2393-2410). The assertion is +// that the refusal is NAMED and that the run then produces the same pixels as any other +// lane: a refusal that half-ran, or that aborted, would both be failures here. +// +// every lane +// THE PIXELS MUST NOT MOVE. The workload draws one solid-colour quad through a texture, an +// explicit sampler object and a user framebuffer, and every lane must read back that colour. +// "The counters moved and the picture did not" is the whole claim - a switch that changed +// what is drawn would not be an A/B, it would be a bug. +// +// WHY IT CAN SKIP. The counters are emitted by the client-side emitters P4a packages B and C own, +// and this file is written against the P4a contract commit, before either lands. Until then nothing +// emits, the five counters are structurally zero in BOTH lanes, and an assertion about the +// difference would be a statement about nothing. The build answers the question rather than a +// hand-maintained list: MG_IntegrationTest/CMakeLists.txt greps every source under MG_Impl/Pipe/ +// for the counters' names and passes the answer in as MGITEST_PIPE_OBJECT_EMITTER_PRESENT, with a +// CONFIGURE_DEPENDS on that directory and on each file it finds so the answer cannot go stale. It +// is a CONTENT probe, not a filename probe, so the owning packages keep control of their own file +// layout - P4a's new client files are headers (D-P), and a glob for a named .cpp would have kept +// this control skipping forever with a reason that had become false. +// +// DIRECTGLES ONLY, and that is the honest scope: P4a migrates Espryt's framebuffer, texture, +// sampler and program paths. Magma's are P7 (D-Q) and register nothing here, so a DirectVulkan lane +// would be measuring the client emitters against a backend nobody asked to change. + +#include +#include +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/PipeStatsWindow.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + // Set by the three ObjectSubsystemControl. ctest entries and by nothing else; a harness + // marker, never read by the library. Its absence means an ambient entry, where neither the + // stats channel nor a private log path is configured. + constexpr const char* kLaneMarker = "MGITEST_OBJECT_SUBSYSTEM_LANE"; + constexpr const char* kLaneOn = "on"; + constexpr const char* kLaneOff = "off"; + constexpr const char* kLaneRefused = "refused"; + + constexpr int kInset = 2; + constexpr int kTextureSize = 4; + // Enough frames that a per-frame emitter and a per-draw emitter read differently, and few + // enough that one summary window covers exactly this. + constexpr int kDrawsInTheWindow = 4; + + constexpr const char* kVS = R"(#version 330 core +in vec2 aPos; +out vec2 vUv; +void main() { + vUv = aPos * 0.5 + 0.5; + gl_Position = vec4(aPos, 0.0, 1.0); +} +)"; + + constexpr const char* kFS = R"(#version 330 core +in vec2 vUv; +uniform sampler2D uTex; +out vec4 oColor; +void main() { oColor = texture(uTex, vUv); } +)"; + + struct Vertex { + float x, y; + }; + + bool BuildMarkerIsSet(const char* name) { + const char* value = std::getenv(name); + return value != nullptr && value[0] == '1' && value[1] == '\0'; + } + + std::string LaneName() { + const char* lane = std::getenv(kLaneMarker); + return lane != nullptr ? std::string(lane) : std::string(); + } + + class ObjectSubsystemControlScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + m_lane = LaneName(); + std::string error; + m_program = CompileProgram(kVS, kFS, &error); + ASSERT_NE(m_program, 0u) << error; + + static const Vertex quad[6] = {{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}, + {-1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}}; + glGenBuffers(1, &m_quadBuffer); + glBindBuffer(GL_ARRAY_BUFFER, m_quadBuffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW); + glGenVertexArrays(1, &m_vao); + glBindVertexArray(m_vao); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr); + glBindVertexArray(0); + RecordProperty("lane", m_lane.empty() ? "ambient" : m_lane.c_str()); + } + + void TearDown() override { + if (!Ready()) return; + glUseProgram(0); + glBindVertexArray(0); + glBindSampler(0, 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + if (m_vao != 0) glDeleteVertexArrays(1, &m_vao); + if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer); + if (m_program != 0) glDeleteProgram(m_program); + } + + // GTEST_SKIP() returns from the function it is written in, so this cannot report + // through a return value; every caller pairs it with `if (IsSkipped()) return;`. + void SkipUnlessTheLaneIsAssertableHere(bool needsTheEmitters) { + if (m_lane.empty()) { + GTEST_SKIP() << "runs only in its own lane: the three ObjectSubsystemControl. " + "ctest entries set " << kLaneMarker + << " together with the MOBILEGL_PIPE_PUSH bitmask that arm means, " + "MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 and a " + "private MOBILEGL_LOG_FILE_PATH. None of that is configured in " + "the ambient entries, and the ambient log is shared, so a read " + "here would race."; + return; + } + if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) { + GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH: there are no " + "subsystem bits to clear, P4a's five CallClass members do not " + "exist and the summary line carries no emit[...] bracket. The " + "entry is registered here anyway so that `ctest -L " + "integration-gpu` names the same tests in the pull build and the " + "push build (gate G2)."; + return; + } + if (needsTheEmitters && !BuildMarkerIsSet("MGITEST_PIPE_OBJECT_EMITTER_PRESENT")) { + GTEST_SKIP() << "subsystem not implemented on this tree: no source under " + "MobileGL/MG_Impl/Pipe/ emits FramebufferEmissions, so nothing " + "sends a P4a record, every counter in the emit[] bracket is " + "structurally zero in BOTH lanes and the difference between them " + "is not observable yet. P4a packages B (framebuffer, texture) " + "and C (sampler, image, program) own those emitters; this " + "control arms itself when they land, whatever files they use."; + return; + } + if (PipeStatsWindow::LibraryLogPath().empty()) { + GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the library's " + "own log is the only channel this module has for reading " + "PipeStats and the bring-up's refusal line"; + return; + } + } + + // The workload, and every one of P4a's four families is in it exactly once per draw: + // a USER FRAMEBUFFER with a texture attachment (bit 9), a TEXTURE with parameters and + // an upload (bit 10), an explicit SAMPLER OBJECT on the unit (bit 11) and a PROGRAM + // with a default-uniform-block write (bit 12). A lane that steered only one of the four + // would move only its own counter, which is why they are counted separately. + void RunTheWorkload() { + std::vector texels(kTextureSize * kTextureSize * 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; + } + glGenTextures(1, &m_texture); + glBindTexture(GL_TEXTURE_2D, m_texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextureSize, kTextureSize, 0, GL_RGBA, + GL_UNSIGNED_BYTE, texels.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + + glGenSamplers(1, &m_sampler); + glSamplerParameteri(m_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glSamplerParameteri(m_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // The user framebuffer, drawn into once per iteration so that the framebuffer + // record has a reason to move: the binding alternates between it and the default + // framebuffer, which is exactly what a per-target set_framebuffer_state counts. + glGenTextures(1, &m_attachment); + glBindTexture(GL_TEXTURE_2D, m_attachment); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextureSize, kTextureSize, 0, GL_RGBA, + GL_UNSIGNED_BYTE, nullptr); + glBindTexture(GL_TEXTURE_2D, 0); + glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + m_attachment, 0); + BindDefaultFramebuffer(); + + for (int draw = 0; draw < kDrawsInTheWindow; ++draw) { + // Into the user framebuffer... + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glViewport(0, 0, kTextureSize, kTextureSize); + glUseProgram(m_program); + glUniform1i(glGetUniformLocation(m_program, "uTex"), 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_texture); + glBindSampler(0, m_sampler); + glBindVertexArray(m_vao); + glDrawArrays(GL_TRIANGLES, 0, 6); + // ...and into the default one, which is what the case reads back. + BindDefaultFramebuffer(); + glViewport(0, 0, Gl().Width(), Gl().Height()); + glDrawArrays(GL_TRIANGLES, 0, 6); + // One sub-region upload per iteration, so the client-side texture upload + // counter (ctu) has something to count and the server's tex[emit=] has the + // same something. + const std::uint8_t green[4] = {0, 255, 0, 255}; + glBindTexture(GL_TEXTURE_2D, m_texture); + glTexSubImage2D(GL_TEXTURE_2D, 0, draw % kTextureSize, 0, 1, 1, GL_RGBA, + GL_UNSIGNED_BYTE, green); + } + } + + void ReleaseTheWorkload() { + glBindSampler(0, 0); + glBindTexture(GL_TEXTURE_2D, 0); + BindDefaultFramebuffer(); + if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo); + if (m_sampler != 0) glDeleteSamplers(1, &m_sampler); + if (m_texture != 0) glDeleteTextures(1, &m_texture); + if (m_attachment != 0) glDeleteTextures(1, &m_attachment); + m_fbo = m_sampler = m_texture = m_attachment = 0; + } + + std::string m_lane; + GLuint m_program = 0; + GLuint m_vao = 0; + GLuint m_quadBuffer = 0; + GLuint m_texture = 0; + GLuint m_attachment = 0; + GLuint m_sampler = 0; + GLuint m_fbo = 0; + }; + + // ONE case per lane, and it is a constraint rather than a preference: this case READS the + // library log, the log is a per-LANE resource (the library opens it fopen(path, "w"), so + // every process in a lane truncates it), and a second case in the same lane would race this + // one under `ctest -j` with a failure indistinguishable from "the counter was never + // emitted". The CMake registration gives each lane a TEST_FILTER naming one case. + TEST_F(ObjectSubsystemControlScenario, ClearingTheP4aBitsStopsTheEmissionsAndNotThePixels) { + if (!Ready()) return; + SkipUnlessTheLaneIsAssertableHere(/*needsTheEmitters=*/true); + if (IsSkipped()) return; + if (m_lane == kLaneRefused) { + GTEST_SKIP() << "the refusal lane runs ASamplerBitWithoutTheTextureBitIsRefusedAndNamed " + "instead: at 0x9ff the sampler subsystem is refused at bring-up, so " + "the emission counts are neither the on-lane's nor the off-lane's and " + "asserting either would be reading a third arm as if it were one of " + "the two."; + } + + BindDefaultFramebuffer(); + Gl().EndFrame(); // close the setup window: everything below is one window + + ClearTo(0.0f, 0.0f, 0.0f, 1.0f); + RunTheWorkload(); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the workload left a GL error behind"; + const Image image = ReadPixels(Gl().Width(), Gl().Height()); + Gl().EndFrame(); // the swap that emits the window covering exactly the work above + + const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog(); + ASSERT_TRUE(window.found) + << "no 'MGPipe stats:' line in " << PipeStatsWindow::LibraryLogPath() + << ". This IS a push build (the lane checked MGITEST_PIPE_PUSH_BUILD before getting " + "here), so either MOBILEGL_PIPE_STATS / MOBILEGL_PIPE_STATS_PERIOD did not reach " + "the process, or no summary line was emitted at all because nothing reached " + "PipeStats::OnPresent."; + RecordProperty("stats_line", window.line.c_str()); + + // The five counters of the emit[] bracket, read individually so that a lane which + // steered one family and not another says WHICH. + const long long framebuffer = PipeStatsWindow::CounterOrAbsent(window, "fbe"); + const long long samplerViews = PipeStatsWindow::CounterOrAbsent(window, "sve"); + const long long samplerStates = PipeStatsWindow::CounterOrAbsent(window, "sse"); + const long long shaderImages = PipeStatsWindow::CounterOrAbsent(window, "sie"); + const long long clientUploads = PipeStatsWindow::CounterOrAbsent(window, "ctu"); + ASSERT_GE(framebuffer, 0) + << "the summary line carries no fbe= field, so this build's PipeStats has no P4a " + "emission counters to read: " + << window.line; + ASSERT_GE(samplerViews, 0) << "no sve= field: " << window.line; + ASSERT_GE(samplerStates, 0) << "no sse= field: " << window.line; + ASSERT_GE(shaderImages, 0) << "no sie= field: " << window.line; + ASSERT_GE(clientUploads, 0) << "no ctu= field: " << window.line; + const long long total = framebuffer + samplerViews + samplerStates + shaderImages + + clientUploads; + + if (m_lane == kLaneOn) { + EXPECT_GT(total, 0) + << "with bits 9|10|11|12 SET the four P4a families are the path this workload " + "takes - a user framebuffer bound and unbound " + << kDrawsInTheWindow + << " times, a texture with parameters and a sub-region upload per iteration, an " + "explicit sampler object on the unit and a program with a default-uniform " + "write - so the window's emit[] bracket must carry something. All five " + "reading zero means the emitters never ran on the arm that is supposed to run " + "them. It reported: " + << window.line; + // The framebuffer family on its own, because it is the one that would be hidden by + // a large upload count: a suppressor that stopped suppressing shows up as fbe + // tracking the DRAW count, and a family that never emitted shows up as zero. + EXPECT_GT(framebuffer, 0) + << "fbe= is zero on the ON lane: set_framebuffer_state never went out even " + "though the workload bound a user framebuffer and the default framebuffer " + << kDrawsInTheWindow << " times each. " << window.line; + } else if (m_lane == kLaneOff) { + EXPECT_EQ(total, 0) + << "with bits 9|10|11|12 CLEARED (MOBILEGL_PIPE_PUSH=0x1ff, P3a's default) the " + "frontend dispatch must fall through to the legacy MGB_CTX-reading arms and " + "emit nothing through any of the four P4a families, so every counter in the " + "emit[] bracket must be zero. A non-zero count here is the dead-switch " + "reading: the bits are being ignored, both arms run the same code, and every " + "push-vs-pull number taken against this A/B is measuring one arm twice. It " + "reported: " + << window.line; + } else { + FAIL() << "unknown " << kLaneMarker << " value '" << m_lane + << "': the arms are on / off / refused. Reading an unrecognised name as " + "either would make this lane assert the other arm's expectation while " + "claiming to test this one."; + } + + // ... and the picture is the same whichever arm ran. + EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset, + image.Height() - kInset, "green", 0.0, + "the sampled draw [" + m_lane + "]")) + << "the subsystem bits changed what is DRAWN, which is not an A/B - the handle path " + "and the legacy path must produce the same pixels from the same texture, sampler " + "and framebuffer."; + + ReleaseTheWorkload(); + } + + // ------------------------------------------------------------------------------------ + // D-K2's dependency refusal, in the direction that has to be refused. + // + // 0x9ff is bits 0..8 (everything P3a shipped) plus bit 11 (samplers) and WITHOUT bit 10 + // (texture resources). Every MGPBoundView::Texture and every MGPImageView::Res names a + // Texture handle, and only bit 10 populates the texture slot table, so with bit 11 alone + // every lookup would miss and the unit walk would `continue` without unbinding - a + // half-run subsystem, which ROADMAP.md:7 forbids as loudly as a dead switch. The bring-up + // logs ONE error naming both bits, refuses bit 11, and runs the legacy sampler arm. + // + // TWO ASSERTIONS, and the second is the one that stops this from being a log-scraping test: + // the refusal is NAMED in the library's own log, and the run then draws the same picture as + // every other lane. A refusal that aborted the process, and a refusal that silently let the + // half-configured arm run, are both failures - and they look completely different here. + // ------------------------------------------------------------------------------------ + TEST_F(ObjectSubsystemControlScenario, ASamplerBitWithoutTheTextureBitIsRefusedAndNamed) { + if (!Ready()) return; + // needsTheEmitters=false: the refusal is a BRING-UP decision made from the bitmask + // alone, so it is assertable before any emitter exists - which is exactly what makes it + // the one P4a control that is not vacuous on the contract tree. + SkipUnlessTheLaneIsAssertableHere(/*needsTheEmitters=*/false); + if (IsSkipped()) return; + if (m_lane != kLaneRefused) { + GTEST_SKIP() << "runs only in the refusal lane (MOBILEGL_PIPE_PUSH=0x9ff): the " + "on/off lanes configure a mask whose dependencies are all satisfied, " + "so there is no refusal there to find and a search for one would " + "report a healthy lane as red."; + } + // The refusal is decided from the bitmask, but it is a BACKEND's decision: D-K2 puts it + // in ResolveSamplersSubsystemArm(), beside the bit-8-requires-bit-7 refusal that + // already ships, and that function is package D's (Managers.cpp). A backend that does + // not yet honour P4a's mask at all cannot refuse a dependency inside it, so on such a + // tree there is nothing here to find and this case SKIPS rather than reporting the + // absence of an unimplemented subsystem as a failure. The marker is the same one + // HandleRecycle's P4a cases read - "does any source under this backend name one of the + // four P4a subsystem constants" - because naming the constant is exactly what honouring + // the mask means. + { + const std::string& backend = Gl().BackendName(); + const std::string marker = + "MGITEST_HANDLE_REKEY_OBJECTS_" + (backend == "DirectVulkan" + ? std::string("DirectVulkan") + : std::string("DirectGLES")); + if (!BuildMarkerIsSet(marker.c_str())) { + GTEST_SKIP() << "subsystem not implemented on this tree: no source under " + "MobileGL/MG_Backend/" + << backend + << " names any of kMGPipeSubsystem{Framebuffer, TextureResources, " + "Samplers, Programs}, so this backend does not honour P4a's mask " + "and cannot refuse a dependency inside it. D-K2's refusal lives " + "in ResolveSamplersSubsystemArm() beside the bit-8-requires-bit-7 " + "one that already ships (Managers.cpp:2393-2410), which is P4a " + "package D's file; this control arms itself when that lands. The " + "lane itself is not wasted: the library came up under 0x9ff, " + "which on a tree with no P4a arm is P3a's mask plus one inert " + "bit, and a mask that aborted a bring-up would have failed this " + "entry before the skip."; + } + } + + BindDefaultFramebuffer(); + ClearTo(0.0f, 0.0f, 0.0f, 1.0f); + RunTheWorkload(); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) + << "the workload left a GL error behind on the refused lane, which would mean the " + "refusal did not fall back cleanly to the legacy arm"; + const Image image = ReadPixels(Gl().Width(), Gl().Height()); + Gl().EndFrame(); + + const std::string log = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::LibraryLogPath()); + ASSERT_FALSE(log.empty()) + << "the library wrote nothing to " << PipeStatsWindow::LibraryLogPath() + << ", so the refusal cannot be read back. MOBILEGL_LOG_FILE_PATH is the only channel " + "this module has for the library's own report."; + // Named, not merely present: the refusal has to say which bit it refused AND which bit + // it needed, because "a sampler bit was ignored" without the dependency is a message an + // operator cannot act on. Both spellings are accepted - the constant's name and the + // hexadecimal mask - so the assertion does not pin the message's wording. + const bool namesTheSampler = log.find("Sampler") != std::string::npos || + log.find("sampler") != std::string::npos || + log.find("0x800") != std::string::npos; + const bool namesTheTexture = log.find("TextureResources") != std::string::npos || + log.find("texture resource") != std::string::npos || + log.find("0x400") != std::string::npos; + EXPECT_TRUE(namesTheSampler && namesTheTexture) + << "MOBILEGL_PIPE_PUSH=0x9ff sets the sampler subsystem (bit 11) without the texture " + "resource subsystem (bit 10) it depends on, and the library's log names neither " + "of them. D-K2 requires ONE MGLOG_E naming both bits and a fall back to the " + "legacy sampler arm; a mask that is silently half-honoured is the failure this " + "case exists to catch, and it is invisible in the pixels by construction. The log " + "was " << log.size() << " bytes."; + + EXPECT_TRUE(RegionIsMostly(image, kInset, image.Width() - kInset, kInset, + image.Height() - kInset, "green", 0.0, + "the sampled draw [refused]")) + << "the refused configuration did not draw what every other lane draws. A refusal is " + "supposed to run the LEGACY arm, which is the arm that ships in a pull build - so " + "the pixels are the one thing it may not change."; + + ReleaseTheWorkload(); + } + + } // namespace +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/TextureUploadShapeScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/TextureUploadShapeScenario.cpp new file mode 100644 index 00000000..86aa7fb1 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/TextureUploadShapeScenario.cpp @@ -0,0 +1,360 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureUploadShapeScenario.cpp +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// 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. +// +// 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: +// +// 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) +// +// 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). +// +// WHAT IT THEREFORE ASSERTS, and it is not nothing: +// +// 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 +// 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. +// +// 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. +// +// 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. + +#include +#include +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/PipeStatsWindow.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + // Set by the TextureUploadShape. ctest entry 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; + constexpr int kAtlasSize = 64; + // Enough scattered rects that the box-versus-rect policy has a real decision to make: the + // rect cascade caps at MipmapStorage::kMaxDirtyRects = 96, and the union-box fallback fires + // on summedArea*4 >= unionArea*3, so a handful of rects would take neither branch + // interestingly. + constexpr int kScatteredRects = 40; + constexpr int kRectSize = 2; + constexpr int kFrames = 3; + + constexpr const char* kVS = R"(#version 330 core +in vec2 aPos; +out vec2 vUv; +void main() { + vUv = aPos * 0.5 + 0.5; + gl_Position = vec4(aPos, 0.0, 1.0); +} +)"; + + constexpr const char* kFS = R"(#version 330 core +in vec2 vUv; +uniform sampler2D uTex; +out vec4 oColor; +void main() { oColor = texture(uTex, vUv); } +)"; + + struct Vertex { + float x, y; + }; + + bool BuildMarkerIsSet(const char* name) { + const char* value = std::getenv(name); + return value != nullptr && value[0] == '1' && value[1] == '\0'; + } + + class TextureUploadShapeScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + std::string error; + m_program = CompileProgram(kVS, kFS, &error); + ASSERT_NE(m_program, 0u) << error; + + static const Vertex quad[6] = {{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}, + {-1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}}; + glGenBuffers(1, &m_quadBuffer); + glBindBuffer(GL_ARRAY_BUFFER, m_quadBuffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW); + glGenVertexArrays(1, &m_vao); + glBindVertexArray(m_vao); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr); + glBindVertexArray(0); + } + + void TearDown() override { + if (!Ready()) return; + glUseProgram(0); + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + if (m_scattered != 0) glDeleteTextures(1, &m_scattered); + if (m_contiguous != 0) glDeleteTextures(1, &m_contiguous); + if (m_vao != 0) glDeleteVertexArrays(1, &m_vao); + if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer); + if (m_program != 0) glDeleteProgram(m_program); + } + + void SkipUnlessTheLaneIsAssertableHere() { + if (std::getenv(kLaneMarker) == nullptr) { + GTEST_SKIP() << "runs only in its own lane: the TextureUploadShape. ctest entry " + "sets " << kLaneMarker + << " together with MOBILEGL_PIPE_STATS=1, " + "MOBILEGL_PIPE_STATS_PERIOD=1 and a private " + "MOBILEGL_LOG_FILE_PATH. None of that is configured in the " + "ambient entries, and the ambient log is shared, so a read here " + "would race."; + return; + } + if (Gl().BackendName() != "DirectGLES") { + GTEST_SKIP() << "DirectGLES only: the upload-shape counters are Espryt's " + "(Managers.cpp:6360-6395) and " << Gl().BackendName() + << " contributes nothing to them, so this lane would record a " + "bracket of zeroes and call it a shape."; + return; + } + if (!BuildMarkerIsSet("MGITEST_PIPE_PUSH_BUILD")) { + GTEST_SKIP() << "this library was built without MOBILEGL_PIPE_PUSH: the client's " + "half of the comparison (CallClass::ClientTextureUploadEmissions, " + "the ctu= field) does not exist there, and a one-sided reading is " + "not the comparison this scenario is for. The entry is registered " + "in every build so that `ctest -L integration-gpu` names the same " + "tests in the pull build and the push build (gate G2)."; + return; + } + if (PipeStatsWindow::LibraryLogPath().empty()) { + GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the library's " + "summary line is the only channel this module has for reading " + "PipeStats"; + return; + } + } + + GLuint MakeAtlas(std::uint8_t r, std::uint8_t g, std::uint8_t b) { + std::vector texels(static_cast(kAtlasSize) * kAtlasSize * 4); + for (std::size_t i = 0; i < texels.size(); i += 4) { + texels[i] = r; + texels[i + 1] = g; + texels[i + 2] = b; + texels[i + 3] = 255; + } + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kAtlasSize, kAtlasSize, 0, GL_RGBA, + GL_UNSIGNED_BYTE, texels.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + return texture; + } + + Image DrawSampled(GLuint texture) { + BindDefaultFramebuffer(); + glViewport(0, 0, Gl().Width(), Gl().Height()); + glUseProgram(m_program); + glUniform1i(glGetUniformLocation(m_program, "uTex"), 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture); + glBindVertexArray(m_vao); + glDrawArrays(GL_TRIANGLES, 0, 6); + return ReadPixels(Gl().Width(), Gl().Height()); + } + + GLuint m_program = 0; + GLuint m_vao = 0; + GLuint m_quadBuffer = 0; + GLuint m_scattered = 0; + GLuint m_contiguous = 0; + }; + + TEST_F(TextureUploadShapeScenario, TheEmittedUploadShapeIsRecordedAndTheTwoSidesAgree) { + if (!Ready()) return; + SkipUnlessTheLaneIsAssertableHere(); + if (IsSkipped()) return; + + m_scattered = MakeAtlas(0, 255, 0); + m_contiguous = MakeAtlas(0, 255, 0); + // 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); + BindDefaultFramebuffer(); + Gl().EndFrame(); + + // ---- the counted window ---- + Image lastScattered; + Image lastContiguous; + 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); + } + 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; + } + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kAtlasSize, 8, GL_RGBA, GL_UNSIGNED_BYTE, + band.data()); + lastContiguous = DrawSampled(m_contiguous); + } + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the upload workload left a GL error behind"; + Gl().EndFrame(); // the swap that emits the window covering exactly the work above + + const PipeStatsWindow::Window window = PipeStatsWindow::LastFromLaneLog(); + ASSERT_TRUE(window.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 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; + + // 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 + << " server[emit=" << emissions << " box=" << box << " rect=" << rect + << " jobs=" << jobs << "] client[ctu=" << clientEmissions << "]" << std::endl; + 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. + 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; + + // 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; + 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; + + // 3. the two sides agree, WHEN THERE ARE TWO SIDES. On the P4a contract tree the client + // emits nothing (the emit headers are the contract's stubs), so ctu= is zero and the + // honest reading is "one side only", recorded and not asserted - never a divergence + // reported against a client that has not been written yet. + if (clientEmissions > 0) { + 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; + } else { + std::cout << "[ TextureUploadShape ] the client side reads zero: no P4a client " + "emitter has landed on this tree, so this run records the SERVER shape " + "only. That is the expected reading on the contract tree and it is not " + "a divergence." + << std::endl; + RecordProperty("client_side", "absent"); + } + + // The pixels, so that a recorded shape cannot be the shape of a workload that drew + // nothing. + EXPECT_TRUE(RegionIsMostly(lastScattered, kInset, lastScattered.Width() - kInset, kInset, + lastScattered.Height() - kInset, "green", 0.0, + "the scattered-rect atlas")); + EXPECT_TRUE(RegionIsMostly(lastContiguous, kInset, lastContiguous.Width() - kInset, kInset, + lastContiguous.Height() - kInset, "green", 0.0, + "the contiguous-band atlas")); + } + + } // namespace +} // namespace MGITest