[Refactor] (P13 W6a): the record arm is the only data arm - MOBILEGL_PIPE_DATA_ARM and its frontend-arm pins go, and the cube-face readback rules move to an integration scenario.

This commit is contained in:
2026-10-06 16:16:29 -04:00
parent 4c3e469ae4
commit d61a4240ee
10 changed files with 172 additions and 396 deletions
+7 -17
View File
@@ -757,23 +757,13 @@ namespace MobileGL::MG_Config {
inline constexpr TransportMode Transport = TransportMode::Monolith;
#endif
// P13 W4: THE DATA-ARM QUESTION - do the backends read RECORDS (the applier's twins, the staged
// stores, the verb's own handles) rather than frontend objects? Under a wire the answer has
// always been yes; P13 makes it yes for monolith too ("monolith is the push variant"), one
// record family at a time. Each family's arm sites ask THIS rather than `Transport`, which
// keeps answering the questions that really are about the transport (lifecycle, roles, whose
// caps, whether a store copies).
//
// MOBILEGL_PIPE_DATA_ARM=record (default) | frontend picks monolith's arm for the families
// already switched, on a backend whose monolith takes the record arm at all (Espryt from W4a,
// Magma at its single flip). It exists for W4-W6 only - bisecting a monolith red to the switch, and
// the per-thread CPU record (ID-P13-2) - and goes in W6 with the frontend arms. Latched once
// by MG_ConfigLoader::Init(): the two arms keep their twins in different places.
extern Bool MonolithTakesRecordArm;
inline Bool DataArmIsRecord() { return Transport != TransportMode::Monolith || MonolithTakesRecordArm; }
// P13: THE DATA-ARM QUESTION, ANSWERED ONCE FOR EVERY BUILD. The backends read RECORDS (the
// applier's twins, the staged stores, the verb's own handles), never frontend objects - over a
// wire and on monolith alike ("monolith is the push variant"). W4 switched monolith family by
// family behind MOBILEGL_PIPE_DATA_ARM; W5 built the record arm into every library; W6 deleted
// the frontend arm and the knob. The predicate stays as a name for the sites that read it.
inline constexpr Bool DataArmIsRecord() { return true; }
// ...and the record arm runs in this process over stores that ALIAS the frontend's shadows
// (monolith) instead of copying them (every wire).
inline Bool RecordArmAliasesFrontend() {
return Transport == TransportMode::Monolith && MonolithTakesRecordArm;
}
inline Bool RecordArmAliasesFrontend() { return Transport == TransportMode::Monolith; }
} // namespace MobileGL::MG_Config
-39
View File
@@ -39,7 +39,6 @@ namespace MobileGL::MG_Config {
Bool SplitTransportRequestedByConfig = false;
IpcTable Ipc;
#endif
Bool MonolithTakesRecordArm = false;
RecordArmTable RecordArm;
} // namespace MobileGL::MG_Config
@@ -676,41 +675,6 @@ namespace MobileGL::MG_ConfigLoader {
}
#endif
// P13 W4 (Config.h DataArmIsRecord). Read after InitTransport: `frontend` names monolith's
// arm only - a wire has no frontend objects on its server half to read, so there it is
// ignored by name rather than half-honoured.
inline void InitDataArm() {
String value;
QueryEnvVariable("MOBILEGL_PIPE_DATA_ARM", value, "record");
String lowered = value;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
// P13 W4: both backends' monolith runs the record arm - Espryt's from W4a, family by family;
// Magma's at its single flip (its verb entry points pick one arm for every family at once).
const Bool backendReady = MG_Config::ActiveBackendType == BackendType::DirectGLES ||
MG_Config::ActiveBackendType == BackendType::DirectVulkan;
MG_Config::MonolithTakesRecordArm = backendReady;
if (lowered.empty() || lowered == "record") {
// The one line a device log can be asked for to show which arm a monolith runs.
MGLOG_I("Config: monolith data arm = %s", backendReady ? "record" : "frontend (backend has no record arm)");
return;
}
if (lowered == "frontend") {
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
MGLOG_W("Config: MOBILEGL_PIPE_DATA_ARM=frontend is ignored under a transport - the "
"record arm is the only arm a wire has");
return;
}
MG_Config::MonolithTakesRecordArm = false;
MGLOG_I("Config: MOBILEGL_PIPE_DATA_ARM=frontend - monolith reads frontend objects in "
"the record families switched so far");
return;
}
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_PIPE_DATA_ARM='%s'; expected "
"record|frontend, using record",
value.c_str());
}
// P13 W5: the record arm's knobs (Config.h RecordArmTable), in every build that has the arm.
inline void InitRecordArm() {
MG_Config::RecordArmTable& ipc = MG_Config::RecordArm;
@@ -750,9 +714,6 @@ namespace MobileGL::MG_ConfigLoader {
// After InitFeatures, so the one line InitIpc logs is the last word on this run's
// configuration, and before the accepted-env map is destroyed just below.
InitTransport();
#endif
InitDataArm();
#if MOBILEGL_BUILD_DISAGGREGATED
InitIpc();
#endif
+7 -22
View File
@@ -72,6 +72,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/DrawParametersScenario.cpp
Scenarios/AsyncCompileScenario.cpp
Scenarios/ArbShaderObjectsScenario.cpp
Scenarios/CubeFaceReadbackScenario.cpp
Scenarios/XfbAfterClipDistanceScenario.cpp
Scenarios/UnwrittenPositionOutputScenario.cpp
Scenarios/SampleMaskScopeScenario.cpp
@@ -1240,9 +1241,8 @@ gtest_discover_tests(MobileGLIntegrationTest
# P13 W3b (ID-P13-3): the pre-handle arm is no longer compiled, so the Handles arm needs neither
# MOBILEGL_PIPE_LEGACY_MEMOS=0 nor a mask that switches anything off: it runs the default mask.
# The Legacy and AbaControl lanes (MOBILEGL_PIPE_PUSH=0, the pre-handle arm) went with that arm;
# AbaControlHandles - the ABA control against the handle arm that ships - stays.
# AbaControlHandles went with the frontend arm in P13 W6.
set(MGL_ITEST_HANDLES_ARM_KNOBS "")
set(MGL_ITEST_ABA_ARM_KNOBS "MOBILEGL_PIPE_HANDLE_ABA_CONTROL=1")
mgl_itest_join_environment(MGL_ITEST_GLES_HANDLE_HANDLES_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith"
@@ -1252,16 +1252,9 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_HANDLE_HANDLES_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith"
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MGITEST_HANDLE_ARM=handles" ${MGL_ITEST_HANDLES_ARM_KNOBS}
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_VULKAN_ENV})
# P13 W4: THE ABA CONTROL'S SUBJECT IS THE FRONTEND ARM. The knob defeats the identity half of
# Magma's frontend-arm keys (MagmaPipeIdentityTables); monolith's record arm keys every object on
# the applier's {slot, gen}, which the knob does not - and must not - reach. So the lane pins the
# frontend arm (MOBILEGL_PIPE_DATA_ARM, a W4-W6 knob) and goes with that arm in W6.
mgl_itest_join_environment(MGL_ITEST_VULKAN_HANDLE_ABA_HANDLES_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith" "MOBILEGL_PIPE_DATA_ARM=frontend"
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MGITEST_HANDLE_ARM=aba"
${MGL_ITEST_HANDLES_ARM_KNOBS} ${MGL_ITEST_ABA_ARM_KNOBS}
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_VULKAN_ENV})
# P13 W6: the AbaControlHandles lane pinned the frontend arm (its knob defeats the identity half
# of Magma's frontend-arm keys); that arm is deleted, and the record arm keys every object on the
# applier's {slot, gen}, which the knob does not reach. Removed (docs/ci/ci-cleanup-log.md).
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.HandleRecycle.Handles."
TEST_FILTER "HandleRecycleScenario.*"
@@ -1280,16 +1273,6 @@ gtest_discover_tests(MobileGLIntegrationTest
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_HANDLE_HANDLES_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.HandleRecycle.AbaControlHandles."
TEST_FILTER "HandleRecycleScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-monolith-control"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_HANDLE_ABA_HANDLES_ENVIRONMENT}"
)
# --- G12: the CSO content-addressing negative control ---------------------------------
#
# PUSH BUILDS ONLY, and that is the honest scope rather than a convenience: the two counters the
@@ -2471,6 +2454,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# GL_ARB_shader_objects (P13): ARB-created shader / program objects travel the record arm like
# core-created ones, so the scenario runs on every split arm as well as both monoliths.
mgl_itest_register_split_arms("ArbShaderObjectsScenario.*" "")
# P13 W6: the cube-face readback rules the deleted frontend-arm unit cases pinned.
mgl_itest_register_split_arms("CubeFaceReadbackScenario.*" "")
mgl_itest_register_split_arms("TriangleScenario.*" "")
mgl_itest_register_split_arms("PersistentCoherentMapScenario.*" "")
# P11 A2 (MG_Remote/CONTRACT-P11.md §2): the >= 16 MiB arena on all three arms. Its tier case
@@ -0,0 +1,149 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CubeFaceReadbackScenario.cpp
// Copyright (c) 2025-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
//
// P13 W6: ONE CUBE FACE OUT, BY TOKEN AND BY Z OFFSET. Two GL 4.6 readback rules that used to be
// pinned by unit cases on the monolith frontend arm (TextureTest / TextureViewTest), which read the
// frontend's level shadow. That arm is gone; every library now answers a readback from the
// backend's own image (the record arm), and a unit process has no backend image to read. So the
// rules are asserted here, on a real context, on every arm the scenarios run on:
//
// * glGetTexImage of ONE cube face packs one face (GL 4.6 8.11.4), so a GL_PIXEL_PACK_BUFFER
// sized for one face is exactly big enough - the validator once measured it against all six
// faces and refused with INVALID_OPERATION - and the face it holds is the one the token named.
// * glGetTextureSubImage of a cube-map VIEW over a 2D array names the face by its z offset
// (GL 4.6 8.11.4, Table 8.25), and face f of a view based at layer L is array layer L + f.
//
// Every face and every layer carries its own value, so a read that lands on the wrong one says
// which one answered instead of merely failing.
#include <cstdint>
#include <vector>
#include "../Harness/HeadlessGL.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 {
class CubeFaceReadbackScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
DrainErrors();
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
}
void TearDown() override {
if (!Ready()) return;
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
for (GLuint buffer : m_buffers) glDeleteBuffers(1, &buffer);
for (GLuint texture : m_textures) glDeleteTextures(1, &texture);
m_buffers.clear();
m_textures.clear();
DrainErrors();
ScenarioTest::TearDown();
}
GLuint NewTexture() {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
return texture;
}
GLuint NewBuffer() {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
m_buffers.push_back(buffer);
return buffer;
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
std::vector<GLuint> m_buffers;
std::vector<GLuint> m_textures;
};
TEST_F(CubeFaceReadbackScenario, GetTexImageOfOneCubeFacePacksIntoAOneFacePixelPackBuffer) {
if (!Ready()) return;
constexpr GLsizei kEdge = 2;
constexpr GLsizeiptr kFaceBytes = kEdge * kEdge * 4;
const GLuint texture = NewTexture();
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, kEdge, kEdge);
for (int face = 0; face < 6; ++face) {
std::vector<GLubyte> seed(static_cast<size_t>(kFaceBytes), static_cast<GLubyte>(10 + face));
glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, 0, 0, kEdge, kEdge, GL_RGBA,
GL_UNSIGNED_BYTE, seed.data());
}
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the six faces failed";
const GLuint buffer = NewBuffer();
glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer);
glBufferData(GL_PIXEL_PACK_BUFFER, kFaceBytes, nullptr, GL_STREAM_READ);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "creating the one-face pack buffer failed";
glGetTexImage(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< "a pixel pack buffer sized for the one face this call packs was refused";
std::vector<GLubyte> packed(static_cast<size_t>(kFaceBytes), 0);
glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, kFaceBytes, packed.data());
for (size_t i = 0; i < packed.size(); ++i) {
ASSERT_EQ(static_cast<int>(packed[i]), 15)
<< "byte " << i << ": the pack buffer holds face " << (static_cast<int>(packed[i]) - 10)
<< ", not -Z (face 5)";
}
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
}
TEST_F(CubeFaceReadbackScenario, CubeMapViewOfAnArrayReadsTheFaceEachZOffsetNames) {
if (!Ready()) return;
constexpr GLint kLayers = 8;
constexpr GLint kViewMinLayer = 2;
const GLuint storage = NewTexture();
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, kLayers);
for (GLint layer = 0; layer < kLayers; ++layer) {
const GLubyte texel[] = {static_cast<GLubyte>(10 + layer), 20, 30, 40};
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, texel);
}
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the array failed";
const GLuint view = NewTexture();
glTextureView(view, GL_TEXTURE_CUBE_MAP, storage, GL_RGBA8, 0, 1, kViewMinLayer, 6);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the cube-map view over the array was refused";
for (GLint face = 0; face < 6; ++face) {
GLubyte output[4] = {};
glGetTextureSubImage(view, 0, 0, 0, face, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, sizeof(output), output);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "reading face " << face << " errored";
EXPECT_EQ(static_cast<GLint>(output[0]), 10 + kViewMinLayer + face)
<< "face " << face << " of a view based at layer " << kViewMinLayer << " answered with layer "
<< (static_cast<GLint>(output[0]) - 10);
}
}
} // namespace
} // namespace MGITest
+3 -88
View File
@@ -20,11 +20,9 @@
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GetProcAddress.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Test/ScopedMonolithFrontendArm.h>
using namespace MobileGL;
using MobileGL::MG_Test::ScopedMonolithFrontendArm;
class BufferTest : public ::testing::Test {
protected:
@@ -235,7 +233,6 @@ TEST_F(BufferTest, AcquireMemoryRangeWithoutExplicit) {
}
TEST_F(BufferTest, AcquireMemoryRangeWithExplicit) {
const ScopedMonolithFrontendArm frontendArm;
auto& slot = MobileGL::MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Uniform);
Vector<Uint> bufferNames;
MobileGL::MG_State::pGLContext->GenBufferNames(1, bufferNames);
@@ -262,9 +259,10 @@ TEST_F(BufferTest, AcquireMemoryRangeWithExplicit) {
const Uint64 flushedSerial = bufObj->GetChangeSerial();
ASSERT_GT(flushedSerial, baseSerial);
// FlushExplicit unmap must not flush the rest of the mapped range.
// FlushExplicit unmap must not flush the rest of the mapped range: the 300 written past the
// flushed range never reaches the store (the content check below). Not asserted on the change
// serial since P13 W6 - the record arm publishes the map's state edge as its own record.
bufObj->ReleaseMemory();
ASSERT_EQ(bufObj->GetChangeSerial(), flushedSerial);
Vector<Int> expected{10, 200, 30, 40, 50};
Vector<Int> actual(5);
@@ -2493,54 +2491,6 @@ TEST_F(BufferTest, MapBufferRangeAndUnmapBufferReseedAnAdoptedShaderStorageBuffe
g_zeroCopyMock = nullptr;
}
// The control: a store the backend declined to adopt keeps the shadow model exactly as
// before - the staging copy is written back into the shadow and the backend's flush op
// carries the range down.
TEST_F(BufferTest, ANonPersistentWriteMapOfAShadowBackedStoreStillFlushesThroughTheBackend) {
const ScopedMonolithFrontendArm frontendArm;
ZeroCopyMockBackend mock;
mock.provideMap = false;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
Vector<GLint> initial(kAdoptedInts);
for (SizeT i = 0; i < kAdoptedInts; ++i) initial[i] = static_cast<GLint>(i);
BufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(kAdoptedInts * sizeof(GLint)), initial.data(),
GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
EXPECT_FALSE(bufferObject->EnsureGpuResidentStorage());
EXPECT_FALSE(bufferObject->IsBackendPersistentMapped());
mock.flushCalls = 0;
mock.subDataCalls = 0;
const Uint64 baseSerial = bufferObject->GetChangeSerial();
constexpr SizeT kFirst = 3;
constexpr SizeT kCount = 4;
const Range1D range{kFirst * sizeof(GLint), (kFirst + kCount) * sizeof(GLint)};
auto* mapped = static_cast<GLint*>(bufferObject->AcquireMemoryRange(range, BufferMappingAccessBit::Write));
ASSERT_NE(mapped, nullptr);
for (SizeT i = 0; i < kCount; ++i) mapped[i] = 600 + static_cast<GLint>(i);
bufferObject->ReleaseMemory();
EXPECT_EQ(mock.flushCalls, 1);
EXPECT_EQ(mock.subDataCalls, 0);
EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial);
const auto* shadow = reinterpret_cast<const GLint*>(bufferObject->MappedData());
for (SizeT i = 0; i < kAdoptedInts; ++i) {
const GLint expected = (i >= kFirst && i < kFirst + kCount) ? 600 + static_cast<GLint>(i - kFirst)
: static_cast<GLint>(i);
EXPECT_EQ(shadow[i], expected) << "int " << i;
}
DeleteBuffers(1, &buffer);
g_zeroCopyMock = nullptr;
}
// ---------------------------------------------------------------------------
// The other three CPU-sourced writes that share the unmap landing's route into an
// adopted store - glBufferSubData, a clear, and a copy - on both kinds of backend: the
@@ -2957,38 +2907,3 @@ TEST_F(BufferTest, RespecifyingAStoreWhileItIsMappedDoesNotLandTheStagedBytesInt
g_zeroCopyMock = nullptr;
}
TEST_F(BufferTest, RespecifyingAShadowBackedStoreWhileItIsMappedPushesNoRangeDown) {
const ScopedMonolithFrontendArm frontendArm;
ZeroCopyMockBackend mock;
mock.provideMap = false;
g_zeroCopyMock = &mock;
ScopedBackendOps scopedOps(&kZeroCopyMockOps);
GLuint buffer = 0;
GenBuffers(1, &buffer);
BindBuffer(GL_ARRAY_BUFFER, buffer);
Vector<GLint> initial(kAdoptedInts, 7);
BufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(kAdoptedInts * sizeof(GLint)), initial.data(),
GL_DYNAMIC_DRAW);
ASSERT_EQ(GetError(), GL_NO_ERROR);
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
ASSERT_NE(bufferObject, nullptr);
mock.flushCalls = 0;
mock.subDataCalls = 0;
mock.respecifyCalls = 0;
auto* mapped = static_cast<GLint*>(
bufferObject->AcquireMemoryRange({0, kAdoptedInts * sizeof(GLint)}, BufferMappingAccessBit::Write));
ASSERT_NE(mapped, nullptr);
for (SizeT i = 0; i < kAdoptedInts; ++i) mapped[i] = 1234;
bufferObject->Respecify(kAdoptedInts * sizeof(GLint), nullptr);
EXPECT_EQ(mock.flushCalls, 0);
EXPECT_EQ(mock.subDataCalls, 0);
EXPECT_EQ(mock.respecifyCalls, 1);
EXPECT_FALSE(bufferObject->IsMapped());
EXPECT_FALSE(bufferObject->HasDefinedContent());
DeleteBuffers(1, &buffer);
g_zeroCopyMock = nullptr;
}
@@ -1,45 +0,0 @@
// MobileGL - MobileGL/MG_Test/ScopedMonolithFrontendArm.h
// Copyright (c) 2025-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
#pragma once
#include <Config.h>
#include <MG_Backend/BackendObjects.h>
namespace MobileGL::MG_Test {
// P13 W4: THE MONOLITH FRONTEND ARM, PINNED FOR ONE CASE. A case that uses this asserts how
// the frontend answers when the backend reads the frontend's objects itself - pushing a map's
// bytes through the object, reading a texture back from the frontend's own level shadow - which
// is the arm W4 retires family by family and W6 deletes. On the record arm (every wire, and
// monolith's from W4) the client tracker and the backend's record readers own those answers,
// and the split and integration lanes cover them. When W6 deletes the frontend arm, the cases
// that use this go with it.
//
// Both halves of the arm: the predicate, and the backend table (the verb port's slots go back
// to the backend's own entry points for the scope).
class ScopedMonolithFrontendArm {
public:
ScopedMonolithFrontendArm() : m_savedTable(MG_Backend::gBackendFunctionsTable) {
m_saved = MG_Config::MonolithTakesRecordArm;
MG_Config::MonolithTakesRecordArm = false;
if (MG_Config::Transport == MG_Config::TransportMode::Monolith && MG_Backend::pActiveBackendObject) {
MG_Backend::gBackendFunctionsTable = MG_Backend::pActiveBackendObject->GetBackendFunctions();
}
}
~ScopedMonolithFrontendArm() {
MG_Config::MonolithTakesRecordArm = m_saved;
MG_Backend::gBackendFunctionsTable = m_savedTable;
}
ScopedMonolithFrontendArm(const ScopedMonolithFrontendArm&) = delete;
ScopedMonolithFrontendArm& operator=(const ScopedMonolithFrontendArm&) = delete;
private:
MG_Backend::GlobalBackendFunctionsTable m_savedTable;
Bool m_saved = false;
};
} // namespace MobileGL::MG_Test
-135
View File
@@ -13,7 +13,6 @@
#include "Includes.h"
#include "Init.h"
#include <MG_Test/ScopedMonolithFrontendArm.h>
#include <Config.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectGLES/Managers.h>
@@ -1061,99 +1060,6 @@ TEST_F(TextureTest, EverySamplerScalarPnameConvertsToTheQueriedType) {
DrainPendingGlErrors();
}
// The CPU-shadow readback path performs no format/type conversion and packs rows tightly. Asking it
// for a layout it cannot produce used to be answered by memcpying the SHADOW's layout into the
// caller's buffer: on glGetTexImage, which has no bufSize argument, that is a heap overflow of
// (shadowTexelSize - clientTexelSize) * texelCount bytes. It must refuse instead.
TEST_F(TextureTest, ShadowReadbackRefusesALayoutItCannotProduceInsteadOfOverrunningTheBuffer) {
const MobileGL::MG_Test::ScopedMonolithFrontendArm frontendArm;
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
ASSERT_NE(texture, 0u);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
const std::vector<GLubyte> source(8 * 8 * 4, 0x5A);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, source.data());
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The positive control first: the matching layout is answered, and answered correctly.
std::vector<GLubyte> matching(8 * 8 * 4, 0);
MG_Impl::GLImpl::GetTextureImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE,
static_cast<GLsizei>(matching.size()), matching.data());
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(matching, source);
// GL_RED against an RGBA8 shadow: 1 client byte per texel against 4 shadow bytes. A verbatim copy
// would write 256 bytes into the 64 GL 4.6 core 8.11 says are required.
std::vector<GLubyte> narrow(8 * 8 * 1, 0xCD);
const std::vector<GLubyte> narrowBefore = narrow;
MG_Impl::GLImpl::GetTextureImage(texture, 0, GL_RED, GL_UNSIGNED_BYTE, static_cast<GLsizei>(narrow.size()),
narrow.data());
ExpectSingleGlError(GL_INVALID_OPERATION);
EXPECT_EQ(narrow, narrowBefore) << "a refused readback must not touch the destination";
// And the widening direction, which is not an overflow but is still the wrong bytes.
std::vector<GLfloat> wide(8 * 8 * 4, 0.0f);
MG_Impl::GLImpl::GetTextureImage(texture, 0, GL_RGBA, GL_FLOAT,
static_cast<GLsizei>(wide.size() * sizeof(GLfloat)), wide.data());
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
MG_Impl::GLImpl::DeleteTextures(1, &texture);
DrainPendingGlErrors();
}
// The same helper honours only GL_PACK_SWAP_BYTES and the bitmap LSB_FIRST path - the pixel-store
// parameters carry a standing TODO in the pack processor. GL_PACK_ALIGNMENT defaults to 4, so a
// 3-byte-per-texel format at an odd width needs row padding that would never be written, and the GPU
// readback path DOES write it. Refusing keeps the two paths from answering the same call with two
// different destination layouts.
TEST_F(TextureTest, ShadowReadbackRefusesAPackStateItCannotHonour) {
const MobileGL::MG_Test::ScopedMonolithFrontendArm frontendArm;
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
ASSERT_NE(texture, 0u);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
// The UNPACK side has the same default alignment of 4, and 5 * 3 = 15 is not a multiple of it -
// so a tightly-packed source would be read back out with a 16-byte row stride and the texture
// would hold the wrong bytes before the readback under test even runs. This is the pack rule
// being pinned below, seen from the upload side.
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 1);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const std::vector<GLubyte> source(5 * 5 * 3, 0x21);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 5, 5, 0, GL_RGB, GL_UNSIGNED_BYTE, source.data());
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// 5 * 3 = 15 bytes per row, which the default GL_PACK_ALIGNMENT of 4 pads to 16.
std::vector<GLubyte> padded(5 * 16, 0);
MG_Impl::GLImpl::GetTextureImage(texture, 0, GL_RGB, GL_UNSIGNED_BYTE, static_cast<GLsizei>(padded.size()),
padded.data());
ExpectSingleGlError(GL_INVALID_OPERATION);
// With the padding removed the rows are tight and the same call is answered.
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 1);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
std::vector<GLubyte> tight(5 * 5 * 3, 0);
MG_Impl::GLImpl::GetTextureImage(texture, 0, GL_RGB, GL_UNSIGNED_BYTE, static_cast<GLsizei>(tight.size()),
tight.data());
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(tight, source);
// A skip offset is ignored outright by the pack processor, so it is refused even when the rows
// themselves are tight.
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_ROWS, 1);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::GetTextureImage(texture, 0, GL_RGB, GL_UNSIGNED_BYTE, static_cast<GLsizei>(tight.size()),
tight.data());
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::PixelStorei(GL_PACK_SKIP_ROWS, 0);
MG_Impl::GLImpl::PixelStorei(GL_PACK_ALIGNMENT, 4);
MG_Impl::GLImpl::PixelStorei(GL_UNPACK_ALIGNMENT, 4);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
MG_Impl::GLImpl::DeleteTextures(1, &texture);
DrainPendingGlErrors();
}
// glTextureParameter* has no target token, so GL 4.6 core 8.10 applies the ten-target list to the
// texture's EFFECTIVE target. The four vector DSA forms reached that gate for free by re-entering
// through the bound-target path; the two scalar forms called the per-object setter directly and
@@ -2141,47 +2047,6 @@ TEST_F(TextureTest, GetTextureSubImageSelectsTheCubeFaceZOffsetNames) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
}
// glGetTexImage of ONE cube face packs one face, so a PIXEL_PACK_BUFFER holding one face is
// exactly the right size for it. The validator used to measure the bound PBO against all SIX
// faces' worth and refuse - INVALID_OPERATION for a buffer the copy that follows would have filled
// precisely. glGetTexImage passes no bufSize, which skips the destination-size branch but NOT the
// PBO one, so this is the only spelling where the six-face sizing was reachable at all.
TEST_F(TextureTest, GetTexImageOfOneCubeFacePacksIntoAOneFacePixelPackBuffer) {
const MobileGL::MG_Test::ScopedMonolithFrontendArm frontendArm;
constexpr GLsizei kEdge = 2;
constexpr SizeT kFaceBytes = static_cast<SizeT>(kEdge) * kEdge * 4;
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP, texture);
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, kEdge, kEdge);
for (int face = 0; face < 6; ++face) {
Uint8 seed[kFaceBytes];
for (SizeT i = 0; i < kFaceBytes; ++i) seed[i] = static_cast<Uint8>(10 + face);
MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, 0, 0, kEdge, kEdge, GL_RGBA,
GL_UNSIGNED_BYTE, seed);
}
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "seeding the six faces failed";
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
MG_Impl::GLImpl::BindBuffer(GL_PIXEL_PACK_BUFFER, buffer);
MG_Impl::GLImpl::BufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(kFaceBytes), nullptr, GL_STREAM_READ);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "creating the one-face pixel pack buffer failed";
MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR)
<< "a pixel pack buffer sized for the one face this call packs was refused";
Uint8 packed[kFaceBytes] = {};
MG_Impl::GLImpl::GetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(kFaceBytes), packed);
EXPECT_EQ(static_cast<int>(packed[0]), 15) << "the PBO holds face " << (static_cast<int>(packed[0]) - 10)
<< ", not -Z";
MG_Impl::GLImpl::BindBuffer(GL_PIXEL_PACK_BUFFER, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(TextureTest, TextureParameteriAndBindTextureUnitAreDirectStateAccess) {
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
@@ -23,7 +23,6 @@
#include "Includes.h"
#include "Init.h"
#include <MG_Test/ScopedMonolithFrontendArm.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
@@ -509,45 +508,4 @@ namespace {
// ======================= which of the owner's layers a face names =======================
// A GL_TEXTURE_CUBE_MAP view over a LAYERED owner - a 2D array here, a cube-map ARRAY behaves
// identically - is the one shape where the face a target names cannot be carried by the choice
// of blob: the owner keeps every layer in ONE blob, so there is nothing for
// ToOwnerUploadTarget to choose between and the face has to land in the byte offset instead.
// It did not. The offset shifted by the view's layer origin alone, so all six face tokens read
// the view's FIRST layer-face - silently, with real texels from a real layer, on every path
// that answers out of the CPU shadow.
//
// The shadow is exactly what this exercises: the fixture's backend is not DirectVulkan, so the
// by-name readback takes the shadow arm rather than asking a backend. (DirectVulkan's own path
// resolves the face into a Vulkan baseArrayLayer and was always right, which is what made this
// a disagreement between the two backends rather than a uniform wrong answer.)
TEST_F(TextureViewTest, CubeMapViewOfAnArrayReadsTheFaceEachTokenNames) {
const MobileGL::MG_Test::ScopedMonolithFrontendArm frontendArm;
constexpr GLint kLayers = 8;
constexpr GLint kViewMinLayer = 2;
const GLuint storage = MakeImmutable2DArray(1, 1, kLayers);
// Every layer carries its own index, so a read that lands on the wrong one says which one
// answered instead of merely failing.
for (GLint layer = 0; layer < kLayers; ++layer) {
const Uint8 texel[] = {static_cast<Uint8>(10 + layer), 20, 30, 40};
MG_Impl::GLImpl::TexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
texel);
}
DrainPendingGlErrors();
const GLuint view = GenTexture();
MG_Impl::GLImpl::TextureView(view, GL_TEXTURE_CUBE_MAP, storage, GL_RGBA8, 0, 1, kViewMinLayer, 6);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the cube-map view over the array was refused";
for (GLint face = 0; face < 6; ++face) {
Uint8 output[4] = {};
MG_Impl::GLImpl::GetTextureSubImage(view, 0, 0, 0, face, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
sizeof(output), output);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "reading face " << face << " errored";
EXPECT_EQ(static_cast<GLint>(output[0]), 10 + kViewMinLayer + face)
<< "face " << face << " of a view based at layer " << kViewMinLayer << " answered with layer "
<< (static_cast<GLint>(output[0]) - 10);
}
}
} // namespace
@@ -461,17 +461,11 @@ TEST(StagedTextureStoreTest, AFollowedCopyMovesTheWindowAndLeavesTheSourceAlone)
#if MOBILEGL_BUILD_DISAGGREGATED
TEST(StagedTextureStoreTest, AnAliasingStoreFollowsACopyIntoAServerOwnedDestination) {
const auto savedTransport = MG_Config::Transport;
const Bool savedArm = MG_Config::MonolithTakesRecordArm;
MG_Config::Transport = MG_Config::TransportMode::Monolith;
MG_Config::MonolithTakesRecordArm = true;
struct Restore {
MG_Config::TransportMode transport;
Bool arm;
~Restore() {
MG_Config::Transport = transport;
MG_Config::MonolithTakesRecordArm = arm;
}
} restore{savedTransport, savedArm};
~Restore() { MG_Config::Transport = transport; }
} restore{savedTransport};
Server::StagedTextureStore store(/*copies=*/false);
ASSERT_TRUE(store.Aliases());
const Uint64 src = Server::StagedTextureStore::KeyForHandle(TestHandle(66, 1));
+4
View File
@@ -11,6 +11,10 @@ them wrong.
| Date | Check | Workflow / job | Reason | Successor |
|---|---|---|---|---|
| 2026-10-06 | unit `BufferTest.{ANonPersistentWriteMapOfAShadowBackedStoreStillFlushesThroughTheBackend, RespecifyingAShadowBackedStoreWhileItIsMappedPushesNoRangeDown}` | Test / unit | P13 W6: they pinned the monolith frontend arm (`ScopedMonolithFrontendArm`) to assert how a shadow-backed map reaches the backend's own flush op - the arm is deleted; the record arm pushes the mapped span itself | `SplitBufferTest` (persistent-map push, live-host-writes), `PersistentCoherentMapScenario` and the buffer integration lanes on monolith's record arm |
| 2026-10-06 | unit `TextureTest.{ShadowReadbackRefusesALayoutItCannotProduceInsteadOfOverrunningTheBuffer, ShadowReadbackRefusesAPackStateItCannotHonour}` | Test / unit | P13 W6: they pinned the frontend arm's CPU-shadow readback, whose pack-state limits they asserted; that path is deleted - the record arm reads the backend's image under a neutral pack and scatters every pack state itself | `PixelStoreSweepScenario`, `PackBufferReadbackScenario` (every pack state, both backends, every arm) |
| 2026-10-06 | unit `TextureTest.GetTexImageOfOneCubeFacePacksIntoAOneFacePixelPackBuffer`, `TextureViewTest.CubeMapViewOfAnArrayReadsTheFaceEachTokenNames` | Test / unit | P13 W6: GL rules, but asserted through the deleted frontend arm's shadow readback; a unit process has no backend image to read | moved, not dropped: `CubeFaceReadbackScenario` (integration-gpu on both backends plus the split arms) asserts the same two rules on a real context |
| 2026-10-06 | `DirectVulkan.HandleRecycle.AbaControlHandles.*` | Test / integration-gpu (`integration-monolith-control`) | P13 W6: it pinned `MOBILEGL_PIPE_DATA_ARM=frontend` - its knob defeats the identity half of Magma's frontend-arm keys, which the deleted arm owned; the record arm keys every object on the applier's {slot, gen} | `DirectVulkan.HandleRecycle.Handles.*` (the same cases on the record arm) |
| 2026-10-06 | `build explicit pull runtime control` (the pull `libMobileGL.so`, `-DMOBILEGL_PIPE_PUSH=OFF`) | Test / `build-linux-monolith-control` | P13 W2 retires the pull build: CMake defaults to push and nothing ships pull (ID-P13-1) | the same job builds the push monolith without MG_Remote (the FCL shape); its `runtime_mode_proof --mode monolith` is the zero-MG_Remote-symbol check |
| 2026-10-06 | `Negative control (pull library)` | Test / `retrace-split` (every leg) | its library was the pull build above | `Negative control (library without a transport)`: same script (renamed `retrace_transport_control.sh`) and evidence sentence, with the push library without MG_Remote; W1 ran both side by side on every leg |
| 2026-10-06 | `DirectGLES.HandleRecycle.Legacy.*`, `DirectVulkan.HandleRecycle.Legacy.*`, `DirectVulkan.HandleRecycle.AbaControl.*` | Test / integration-gpu (`integration-monolith-control`) | they ran `MOBILEGL_PIPE_PUSH=0`, the pre-handle arm, which P13 W3b removed with `MOBILEGL_PIPE_LEGACY_MEMOS` (ID-P13-3) | `HandleRecycle.Handles.*` and `DirectVulkan.HandleRecycle.AbaControlHandles.*` (the ABA control against the handle arm that ships) stay |