[Test] (Pipe): switch P4a's four object subsystems off against the shipping mask and record the texture upload shape the two counters make visible

This commit is contained in:
2026-09-08 19:08:12 -04:00
committed by rereview
parent b6e5c2bb39
commit 8c8d460e14
3 changed files with 985 additions and 2 deletions
+124 -2
View File
@@ -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
@@ -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 <cstdint>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#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<std::uint8_t> 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
@@ -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 <cstdint>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#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<std::uint8_t> texels(static_cast<std::size_t>(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<std::uint8_t> band(static_cast<std::size_t>(kAtlasSize) * 8 * 4);
for (std::size_t i = 0; i < band.size(); i += 4) {
band[i] = 0;
band[i + 1] = 255;
band[i + 2] = 0;
band[i + 3] = 255;
}
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<int>(emissions));
RecordProperty("server_box_emissions", static_cast<int>(box));
RecordProperty("server_rect_emissions", static_cast<int>(rect));
RecordProperty("server_upload_jobs", static_cast<int>(jobs));
RecordProperty("client_emissions", static_cast<int>(clientEmissions));
// 1. the workload really uploaded. Without this the three assertions below are all
// 0 == 0 and a run whose emitter was switched off records the same "healthy" shape
// as one that worked.
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