diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 63003bec..5849b06f 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -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 diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 2636788d..3f259e6b 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -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(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 diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 14b18a9d..fb822d62 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_IntegrationTest/Scenarios/CubeFaceReadbackScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/CubeFaceReadbackScenario.cpp new file mode 100644 index 00000000..c12765d3 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/CubeFaceReadbackScenario.cpp @@ -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 +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#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 m_buffers; + std::vector 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 seed(static_cast(kFaceBytes), static_cast(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(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(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(GL_NO_ERROR)) + << "a pixel pack buffer sized for the one face this call packs was refused"; + + std::vector packed(static_cast(kFaceBytes), 0); + glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, kFaceBytes, packed.data()); + for (size_t i = 0; i < packed.size(); ++i) { + ASSERT_EQ(static_cast(packed[i]), 15) + << "byte " << i << ": the pack buffer holds face " << (static_cast(packed[i]) - 10) + << ", not -Z (face 5)"; + } + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + EXPECT_EQ(glGetError(), static_cast(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(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(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(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(GL_NO_ERROR)) << "reading face " << face << " errored"; + EXPECT_EQ(static_cast(output[0]), 10 + kViewMinLayer + face) + << "face " << face << " of a view based at layer " << kViewMinLayer << " answered with layer " + << (static_cast(output[0]) - 10); + } + } + + } // namespace +} // namespace MGITest diff --git a/MobileGL/MG_Test/Buffer/BufferTest.cpp b/MobileGL/MG_Test/Buffer/BufferTest.cpp index 714e4a1b..eb98883f 100644 --- a/MobileGL/MG_Test/Buffer/BufferTest.cpp +++ b/MobileGL/MG_Test/Buffer/BufferTest.cpp @@ -20,11 +20,9 @@ #include #include #include -#include 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 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 expected{10, 200, 30, 40, 50}; Vector 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 initial(kAdoptedInts); - for (SizeT i = 0; i < kAdoptedInts; ++i) initial[i] = static_cast(i); - BufferData(GL_SHADER_STORAGE_BUFFER, static_cast(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(bufferObject->AcquireMemoryRange(range, BufferMappingAccessBit::Write)); - ASSERT_NE(mapped, nullptr); - for (SizeT i = 0; i < kCount; ++i) mapped[i] = 600 + static_cast(i); - bufferObject->ReleaseMemory(); - - EXPECT_EQ(mock.flushCalls, 1); - EXPECT_EQ(mock.subDataCalls, 0); - EXPECT_GT(bufferObject->GetChangeSerial(), baseSerial); - const auto* shadow = reinterpret_cast(bufferObject->MappedData()); - for (SizeT i = 0; i < kAdoptedInts; ++i) { - const GLint expected = (i >= kFirst && i < kFirst + kCount) ? 600 + static_cast(i - kFirst) - : static_cast(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 initial(kAdoptedInts, 7); - BufferData(GL_ARRAY_BUFFER, static_cast(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( - 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; -} diff --git a/MobileGL/MG_Test/ScopedMonolithFrontendArm.h b/MobileGL/MG_Test/ScopedMonolithFrontendArm.h deleted file mode 100644 index 5f86adff..00000000 --- a/MobileGL/MG_Test/ScopedMonolithFrontendArm.h +++ /dev/null @@ -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 -#include - -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 diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 5efd099a..d2ff79ee 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -13,7 +13,6 @@ #include "Includes.h" #include "Init.h" -#include #include #include #include @@ -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 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 matching(8 * 8 * 4, 0); - MG_Impl::GLImpl::GetTextureImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, - static_cast(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 narrow(8 * 8 * 1, 0xCD); - const std::vector narrowBefore = narrow; - MG_Impl::GLImpl::GetTextureImage(texture, 0, GL_RED, GL_UNSIGNED_BYTE, static_cast(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 wide(8 * 8 * 4, 0.0f); - MG_Impl::GLImpl::GetTextureImage(texture, 0, GL_RGBA, GL_FLOAT, - static_cast(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 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 padded(5 * 16, 0); - MG_Impl::GLImpl::GetTextureImage(texture, 0, GL_RGB, GL_UNSIGNED_BYTE, static_cast(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 tight(5 * 5 * 3, 0); - MG_Impl::GLImpl::GetTextureImage(texture, 0, GL_RGB, GL_UNSIGNED_BYTE, static_cast(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(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(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(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(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(kFaceBytes), packed); - EXPECT_EQ(static_cast(packed[0]), 15) << "the PBO holds face " << (static_cast(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); diff --git a/MobileGL/MG_Test/Texture/TextureViewTest.cpp b/MobileGL/MG_Test/Texture/TextureViewTest.cpp index 8081b1dc..3600934f 100644 --- a/MobileGL/MG_Test/Texture/TextureViewTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureViewTest.cpp @@ -23,7 +23,6 @@ #include "Includes.h" #include "Init.h" -#include #include #include #include @@ -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(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(output[0]), 10 + kViewMinLayer + face) - << "face " << face << " of a view based at layer " << kViewMinLayer << " answered with layer " - << (static_cast(output[0]) - 10); - } - } } // namespace diff --git a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp index e46e46ef..b374a769 100644 --- a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp +++ b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp @@ -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)); diff --git a/docs/ci/ci-cleanup-log.md b/docs/ci/ci-cleanup-log.md index ce2f5b72..54a1b32f 100644 --- a/docs/ci/ci-cleanup-log.md +++ b/docs/ci/ci-cleanup-log.md @@ -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 |