diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index ba123e48..5d3326c5 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -58,6 +58,11 @@ add_executable(MobileGLIntegrationTest Harness/P4aFinalFixPeek.cpp Harness/PersistentMapPeek.cpp Harness/SplitRuntimePeek.cpp + # ---- P3b/P4b wave 2-D package D2 (own block; D1 edits this file elsewhere) -------------- + # The client texture emitter's own counters, for TextureViewAliasScenario's half of the + # owner-keyed emission cursor gate: a drain that ran and emitted nothing is invisible in + # pixels and is exactly the two-cursor regression's signature. + Harness/TextureEmitPeek.cpp Scenarios/OrientationScenario.cpp Scenarios/CrossFrameBufferScenario.cpp Scenarios/ResidentIndexScenario.cpp @@ -121,6 +126,8 @@ add_executable(MobileGLIntegrationTest Scenarios/CopyImageLayeredScenario.cpp Scenarios/CopyImagePacked16Scenario.cpp Scenarios/TextureViewScenario.cpp + # P3b/P4b wave 2-D package D2: the verification half of the owner-keyed emission cursor. + Scenarios/TextureViewAliasScenario.cpp Scenarios/PackedWordReadbackScenario.cpp Scenarios/LayeredAttachmentBarrierScenario.cpp Scenarios/LayeredAttachmentShapeScenario.cpp @@ -754,8 +761,13 @@ include(GoogleTest) # caller's transport. A separate label makes every exception inspectable. set(MGL_ITEST_MONOLITH_MECHANISM_FILTER "HandleRecycleScenario.*:CsoContentAddressingScenario.*:ResourceSubsystemControlScenario.*:ObjectSubsystemControlScenario.*:PipeVerifyArmingScenario.*:PoisonOmissionScenario.*:StorageBufferRegrowScenario.NStorageDefinitionsCostNMapPersistentRoundtripsNotOnePerDraw:LargeArenaAdoptionScenario.AnAdoptionCostsExactlyOneMapPersistentRoundtrip") +# P3b/P4b wave 2-D package D2: TextureViewAliasScenario joins this list rather than the ambient +# one for the same reason TextureViewScenario is in it - every case it carries constructs a +# glTextureView, which DirectGLES withholds unless MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 +# (ConfigLoader.cpp:213). In the ambient entry it would green-skip on every run and the gate +# would be a name in a list. set(MGL_ITEST_GLES_VIEW_FILTER - "TextureViewScenario.*:F1WireScenario.ComputeImageStoreThroughViewPreservesOtherRootLayer:F1WireScenario.TextureViewClearTargetsItsRootMipAndLayer:F1WireScenario.GenerateMipmapThroughViewKeepsOwnerLevelsAndLayersOutsideItsWindow") + "TextureViewScenario.*:TextureViewAliasScenario.*:F1WireScenario.ComputeImageStoreThroughViewPreservesOtherRootLayer:F1WireScenario.TextureViewClearTargetsItsRootMipAndLayer:F1WireScenario.GenerateMipmapThroughViewKeepsOwnerLevelsAndLayersOutsideItsWindow") gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectGLES." @@ -1930,9 +1942,22 @@ endif() # generated ctest include file verbatim and never expanded. set(MGL_ITEST_SPLIT_SERVER_PATH "${CMAKE_BINARY_DIR}/libMobileGLServer.so") +# P3b/P4b wave 2-D package D2: MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 IS ON THE SHARED GLES +# SPLIT/SPAWN/TCP LISTS, not on a private entry, and that is the deliberate reading of the plan +# row. The knob is Espryt's FEATURE opt-in (ConfigLoader.cpp:213), independent of transport: it +# advertises GL_ARB_texture_view and nothing else. Without it TextureViewAliasScenario - and +# TextureViewScenario, if it is ever registered here - green-skips in all three arms and the +# gate is a name in a list. The monolith DirectGLES lane has carried it on its own view entry +# since P4a for exactly this reason; the split arms had no equivalent. +# +# It is NOT added to MGL_ITEST_VULKAN_{SPLIT,SPAWN,TCP}_ENVIRONMENT below: the variable is read +# only by ConfigLoader's Espryt feature block, so on DirectVulkan it would be a parsed-and- +# ignored name, and tier 2's informational Magma replay of these registrations wants the two +# backends' arms to differ only where the product differs. mgl_itest_join_environment(MGL_ITEST_GLES_SPLIT_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=inproc" "MOBILEGL_IPC_SERVER_PATH=${MGL_ITEST_SPLIT_SERVER_PATH}" "MGITEST_SPLIT_LANE=1" + "MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1" ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV}) # The two counting lanes, one per transport, and they are a PAIR: exit gate E3(c) asks that the @@ -1998,9 +2023,13 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # makes the test process the CLIENT and has it launch a server from MOBILEGL_IPC_SERVER_PATH # - the same path the inproc arm already carries, because under inproc the library reads it # and decides it does not need one. + # D2: the texture-view opt-in, for the reason stated over the SPLIT list above. Under spawn + # the variable has to reach BOTH processes; it does, because the server is spawned from this + # process's environment. mgl_itest_join_environment(MGL_ITEST_GLES_SPAWN_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=spawn" "MOBILEGL_IPC_SERVER_PATH=${MGL_ITEST_SPLIT_SERVER_PATH}" "MGITEST_SPLIT_LANE=1" + "MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1" ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV}) # A single supervisor serves one session at a time. Fixture teardown also reaps a @@ -2016,7 +2045,19 @@ if (MOBILEGL_BUILD_DISAGGREGATED) --state "${MGL_TCP_STATE}") add_test(NAME TcpServer.Stop COMMAND "${Python3_EXECUTABLE}" "${MGL_TCP_FIXTURE}" stop --state "${MGL_TCP_STATE}") - set_tests_properties(TcpServer.Start PROPERTIES FIXTURES_SETUP mobilegl-tcp TIMEOUT 20) + # P3b/P4b wave 2-D package D2: THE TCP LANE'S SERVER IS PRE-STARTED BY THIS FIXTURE, not + # spawned from a client that already carries the lane environment, so a FEATURE opt-in has to + # be set here as well as on MGL_ITEST_GLES_TCP_ENVIRONMENT. tcp_server_fixture.py builds the + # supervisor's environment from os.environ (:60) and strips only the transport keys (:63-65), + # so a ctest ENVIRONMENT property on this entry reaches the server process. + # + # Measured, not assumed: without it every TextureViewAliasScenario case SKIPPED in the Tcp + # arm with "GL_ARB_texture_view not advertised" while the identical Split and Spawn arms ran + # - because under inproc and spawn the server inherits the client's environment and under tcp + # it does not. A feature knob that reaches two of three arms is the worst of the three states: + # the lane is green and tests nothing. + set_tests_properties(TcpServer.Start PROPERTIES FIXTURES_SETUP mobilegl-tcp TIMEOUT 20 + ENVIRONMENT "MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1") set_tests_properties(TcpServer.Stop PROPERTIES FIXTURES_CLEANUP mobilegl-tcp TIMEOUT 20) add_test(NAME TcpLane.AccountingControls COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/ci/tcp_lane_tools_test.py") @@ -2039,11 +2080,14 @@ if (MOBILEGL_BUILD_DISAGGREGATED) set_tests_properties(TcpLane.SupervisorProtocolControls PROPERTIES LABELS integration-tcp TIMEOUT 180 SKIP_RETURN_CODE 77 RESOURCE_LOCK mobilegl-tcp) + # D2: the texture-view opt-in on both TCP lists too, so the three shared GLES split arms + # carry one configuration. See the note over MGL_ITEST_GLES_SPLIT_ENVIRONMENT. mgl_itest_join_environment(MGL_ITEST_GLES_TCP_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=spawn" "MOBILEGL_IPC_CONTROL=${MOBILEGL_ITEST_TCP_ENDPOINT}" "MOBILEGL_IPC_DATA=stream" "MOBILEGL_IPC_REQUIRE_SAME_BUILD=1" "MOBILEGL_IPC_LOG_FORWARD=1" "MOBILEGL_IPC_TOKEN=" "MGITEST_SPLIT_LANE=1" "MGITEST_TCP_LANE=1" "MOBILEGL_ITEST_REQUIRE_GPU=1" + "MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1" ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV}) mgl_itest_join_environment(MGL_ITEST_GLES_TCPDEVICE_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=spawn" @@ -2051,6 +2095,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED) "MOBILEGL_IPC_REQUIRE_SAME_BUILD=1" "MOBILEGL_IPC_LOG_FORWARD=1" "MOBILEGL_IPC_TOKEN=${MOBILEGL_ITEST_TCP_DEVICE_TOKEN}" "MGITEST_SPLIT_LANE=1" "MGITEST_TCP_LANE=1" "MOBILEGL_ITEST_REQUIRE_GPU=1" + "MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1" ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV}) # ---- P7 package L: THE SAME THREE ARMS FOR MAGMA ---------------------------------------- @@ -2568,6 +2613,54 @@ if (MOBILEGL_BUILD_DISAGGREGATED) mgl_itest_register_split_arms("SyncWireScenario.*" "") + # ========================================================================================= + # P3b/P4b WAVE 2-D PACKAGE D2 - OWN BLOCK. Package D1 (XFB / readback) registers its arms in + # its own block elsewhere in this file; keep the two apart so the integrator can merge both. + # ========================================================================================= + # + # THE OWNER-KEYED EMISSION CURSOR, under all three transports. The claim is about WHICH + # emission cursor an upload through a texture VIEW lands on, and it is only interesting where + # the record actually crosses a wire: under monolith the drain's SubDataCount still moves but + # nothing downstream can disagree with the client's own dirty model, because there is one + # address space and one MipmapStorage. Under split the applier holds its own per-record + # pending-upload set, so a cursor that cleared the wrong flag produces a server-side level + # that never receives the texels - which is the failure this registration exists to reach. + # + # Every case draws and reads pixels, so each moves the client encoder's record ordinal and + # satisfies ScenarioFixture's armed-lane rule. The three GLES arm environments carry + # MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 (above); without it every case green-skips through + # TextureViewUsable() rather than failing - which is what the Tcp arm did until the fixture + # entry below got the same knob. + # + # ONE CASE IS EXCLUDED BY NAME, and it is a defect this package FOUND rather than a lane + # problem. TheOwnerUploadDrainRunsAgainAfterADrawBoundary is red on all three transports and + # green under monolith: + # + # after a view has been SAMPLED once, a later glTexSubImage through the OWNER's own name + # never becomes visible through the view. + # + # The client half is provably correct - the case's own counter assertions pass, so the + # records were emitted and the applier accepted them. The server's per-texture clean gate + # BackendTextureObject::IsDrawSyncCleanByRecord (Managers.cpp:15433) compares the VIEW's + # record.Serial and record.PendingUploads, and an owner-side resource_subdata moves only the + # OWNER's (MG_Pipe/PipeApply.cpp:1175). Nothing anywhere propagates a serial bump from a + # storage owner to the records that view it: MGPResourceDesc::ViewOf is a forward edge with + # no reverse index, and the applier reads it only in the descriptor-equality helper + # (PipeApply.cpp:666). Under monolith the same gate reads GetContentVersion() THROUGH the + # view, which TextureObjectView forwards to the owner (TextureObjectView.cpp:100-102) - the + # forwarding the split gate has no equivalent of. CONTRACT-P5E.md:387-395 states the split + # gate in full and has the same hole, so the implementation is faithful to the contract. + # + # NOT EXCLUDED FROM THE MONOLITH LANES, where it is green and is the regression net for the + # forwarding that works. Listed in notes/p34b/split-scenario-census.md and + # notes/p34b/espryt-d2.md rather than deleted (G14). The fix is a one-line-shaped change in + # MG_Pipe/PipeApply.cpp with an O(#texture records) scan behind it, or a reverse index in + # MGPipeResourceRecord - an owner's decision, not this package's, and it is outside D2's + # declared footprint. + mgl_itest_register_split_arms( + "TextureViewAliasScenario.*-TextureViewAliasScenario.TheOwnerUploadDrainRunsAgainAfterADrawBoundary" + "") + # The split half of the counting pair. MGITEST_PERSISTENT_MAP_ARM=emulated is R-6: under split # the adopt tier is pinned at T2, the resource owner declines every acquisition and the client # pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith diff --git a/MobileGL/MG_IntegrationTest/Harness/TextureEmitPeek.cpp b/MobileGL/MG_IntegrationTest/Harness/TextureEmitPeek.cpp new file mode 100644 index 00000000..e36ad234 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/TextureEmitPeek.cpp @@ -0,0 +1,55 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Harness/TextureEmitPeek.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 + +#include "TextureEmitPeek.h" + +#if !defined(__ANDROID__) +#include +#if MOBILEGL_PIPE_PUSH +#include +#define MGITEST_TEXTURE_EMIT_PEEK_LIVE 1 +#endif +#endif + +namespace MGITest { + +#if defined(MGITEST_TEXTURE_EMIT_PEEK_LIVE) + + bool PeekTextureEmitCounters(TextureEmitCountersPeek* out) { + if (out == nullptr) return false; + // A SEPARATE TRANSLATION UNIT for PipeSlotPeek.h's reason, verbatim: the scenario sources + // include the GL headers with prototypes and MobileGL's umbrella header is not meant to + // meet them in one file. + auto& emitter = MobileGL::MG_Pipe::MGPipeTextureEmitterInstance(); + out->SubDataCount = static_cast(emitter.SubDataCount()); + out->RefusedSubDataCount = static_cast(emitter.RefusedSubDataCount()); + out->CreateCount = static_cast(emitter.CreateCount()); + out->RespecifyCount = static_cast(emitter.RespecifyCount()); + out->DrainListSize = static_cast(emitter.DrainListSize()); + return true; + } + + const char* TextureEmitPeekSkipReason() { return ""; } + +#else + + bool PeekTextureEmitCounters(TextureEmitCountersPeek*) { return false; } + + const char* TextureEmitPeekSkipReason() { +#if defined(__ANDROID__) + return "this module links the shipping libMobileGL.so on Android, built " + "-fvisibility=hidden, so the client emitter's counters do not resolve"; +#else + return "this is a PULL build: MGPipeTextureEmitter is compiled only under " + "MOBILEGL_PIPE_PUSH, so there is no emission cursor to read"; +#endif + } + +#endif + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Harness/TextureEmitPeek.h b/MobileGL/MG_IntegrationTest/Harness/TextureEmitPeek.h new file mode 100644 index 00000000..13937234 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/TextureEmitPeek.h @@ -0,0 +1,64 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Harness/TextureEmitPeek.h +// 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 +// +// THE CLIENT TEXTURE EMITTER'S OWN COUNTERS AND ITS DRAIN LIST, read from a scenario. +// +// WHY A WHITE-BOX READING EXISTS HERE AT ALL, and it is the same argument PipeApplyPeek.h makes +// for G9. The owner-keyed drain cursor (TextureEmit.h's "the drain list (D-D4)") is a statement +// about WHICH emission cursor an upload through a texture VIEW lands on. Its public-GL +// observable - the texels a later sample returns - is repaired by every other arm of the +// system: a level that never made it onto the drain list is still uploaded by the NEXT whole- +// level respecify, by a GenerateMipmap, or by the monolith sync path, and the picture comes out +// right one frame later than it should have. A scenario that only looked at pixels would +// therefore go green over a two-cursor regression on most trees and red on none of them +// reliably. The counters are where the claim actually lives: +// +// SubDataCount() how many resource_subdata records the drain put on the wire. A drain that +// ran and emitted NOTHING is the silent failure this peek exists to make +// loud - it looks exactly like a healthy drain from outside. +// DrainListSize() what is still owed after the drain. Non-zero after a validate point means +// a level bailed (no storage, no shadow, an empty box) and stayed on the +// list, which is the safe direction but not the expected one for a plain +// glTexSubImage of a texture with storage. +// +// EVERY ENTRY POINT RETURNS false, TOUCHING NOTHING, WHERE IT CANNOT LOOK, and a caller that +// gets false has learned NOTHING. Out of reach means: a PULL build (the emitter is +// `#if MOBILEGL_PIPE_PUSH` and does not exist there); and Android, where this module links the +// shipping libMobileGL.so built -fvisibility=hidden and no internal symbol resolves. It is NOT +// backend-dependent: the emitter is the CLIENT's, and the client is this process under every +// one of inproc / spawn / tcp as well as under monolith. + +#pragma once + +namespace MGITest { + + // MGPipeTextureEmitterInstance()'s cumulative counters. They are process-global and the + // library never resets them, so every caller compares a DELTA across its own window rather + // than an absolute - the same rule the PipeStats window readers follow. + struct TextureEmitCountersPeek { + // resource_subdata records the drain emitted (m_subDatas). + unsigned long long SubDataCount; + // ...of which the applier REFUSED (m_refusedSubDatas). A refusal leaves the level dirty + // and on the list, so a case that expected an emission and sees a refusal instead has a + // different finding from one that sees no emission at all. + unsigned long long RefusedSubDataCount; + // resource_create / resource_respecify, for a message that can say whether the texture + // was even described to the applier before the upload was due. + unsigned long long CreateCount; + unsigned long long RespecifyCount; + // What is STILL owed: m_drain.size() right now. Read after the validate point, this is + // the "did every dirty level get emitted" answer. + unsigned long long DrainListSize; + }; + + bool PeekTextureEmitCounters(TextureEmitCountersPeek* out); + + // Why the peek could not look, for a GTEST_SKIP message. Empty when it can. + const char* TextureEmitPeekSkipReason(); + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/TextureViewAliasScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/TextureViewAliasScenario.cpp new file mode 100644 index 00000000..f06c5e3e --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/TextureViewAliasScenario.cpp @@ -0,0 +1,570 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureViewAliasScenario.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 OWNER-KEYED EMISSION CURSOR (P3b/P4b, wave 2-D; ROADMAP's "dirty 归属反转 - +// 按存储属主键控的发射游标"). THE VERIFICATION HALF. +// +// THE CODE HALF ALREADY LANDED and this file is the gate it never had: +// +// * the drain cursor is keyed on the STORAGE OWNER - MG_Impl/Pipe/TextureEmit.h's +// NoteLevelDirty acquires a handle for whatever ITextureObject& reaches it, and what +// reaches it is the owner because TextureObjectView forwards MarkStorageDirty / +// MarkStorageDirtyRegion to the owner's methods (TextureObjectView.cpp:314-329); +// * the index remap is TextureObjectView::ToOwnerLevel / ToOwnerUploadTarget / +// ToOwnerRegionOffset (TextureObjectView.h:105-132), so a write through a level- or +// layer-restricted view names the OWNER's level and the OWNER's layer; +// * the wire half is MGPSamplerView + MGPResourceDesc::ViewOf (P5e tx2). +// +// WHAT GOES WRONG WITHOUT IT, which is why a scenario is owed rather than a unit case. A view +// and its owner share ONE set of dirty flags (the view owns no MipmapStorage at all) but would +// carry TWO emission cursors if the drain were keyed on the texture the application named. +// Whichever cursor drained first would clear the flag the other still needed, or both would +// ship the same texels twice. Neither failure is visible in the next frame's picture on most +// trees: the level that lost its record is uploaded again by the NEXT whole-level respecify, +// by a glGenerateMipmap, or by the monolith sync path, so the image is right one frame late. +// That is exactly the shape SSIM and every ordinary pixel case go green over. +// +// SO THE CLAIM IS ASSERTED ON BOTH CHANNELS AT ONCE, and neither alone is the gate +// (notes/recovered/wf2/final/part3.md 7.3, "门:新增场景,通过 view 上传、经属主采样(以及反 +// 向),跨 draw 边界各一次"): +// +// PIXELS an upload through one name must be visible through the other name, in the SAME +// frame it was issued - and the layers the view does not address must not move. +// COUNTERS MGPipeTextureEmitter::SubDataCount() must have moved across the drain, and +// DrainListSize() must be back to zero after it. A SILENTLY EMPTY DRAIN - the +// two-cursor regression's actual signature - reads as a perfectly healthy run +// from the pixel side alone, because the repair paths above hide it. +// +// THE FOUR CASES are the two directions, each in a one-drain and a two-drain shape: +// (a) upload through a layer-restricted VIEW, sample through the OWNER; +// (b) upload through the OWNER, sample through the VIEW; +// (c) (a) across a draw boundary - draw, upload, draw, upload, draw - so the drain runs +// three times and the counters are read at each one; +// (d) (b) across the same boundary. +// +// THE RED-ONCE (R-16) this file was written against: make TextureObjectView:: +// MarkStorageDirtyRegion call PipeNoteLevelDirty on ITSELF instead of forwarding to the owner - +// i.e. build the two-cursor shape the design rejects. Case (a) then goes red on stale OWNER +// texels AND on a SubDataCount that did not move, which is the pair this file exists to make +// simultaneous. Recorded in docs/Disaggregated/notes/p34b/espryt-d2.md. +// +// VIEWS ARE OPT-IN ON ESPRYT. DirectGLES withholds GL_ARB_texture_view unless +// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 (ConfigLoader.cpp:213, Config.h:72), so every lane that +// runs this file sets it; a lane that does not green-skips through TextureViewUsable() rather +// than failing, the same shape TextureViewScenario uses. + +#include +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" +#include "../Harness/TextureEmitPeek.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + constexpr int kSize = 64; + constexpr int kLayers = 4; + // Deliberately NOT 0: a view whose layer origin was dropped lands on layer 0, and the + // per-layer seeding below makes that land on a layer this file also reads back. + constexpr int kViewLayer = 2; + // The quad covers the whole surface; the inset keeps the assertions off the primitive + // edge, which is HeadlessGL.h's standing rule for a full-surface draw. + constexpr int kInset = 2; + + 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); +} +)"; + + // The VIEW's sampler: an ordinary 2D name over one of the owner's layers. + constexpr const char* kFS2D = R"(#version 330 core +in vec2 vUv; +uniform sampler2D uTex; +out vec4 oColor; +void main() { oColor = texture(uTex, vUv); } +)"; + + // The OWNER's sampler: the array, with the layer named by a uniform so one program + // serves every layer read-back below. + constexpr const char* kFSArray = R"(#version 330 core +in vec2 vUv; +uniform sampler2DArray uTex; +uniform int uLayer; +out vec4 oColor; +void main() { oColor = texture(uTex, vec3(vUv, float(uLayer))); } +)"; + + struct Vertex { + float x, y; + }; + + std::string Describe(const Rgba8& c) { + return "rgba(" + std::to_string(c.r) + "," + std::to_string(c.g) + "," + std::to_string(c.b) + + "," + std::to_string(c.a) + ")"; + } + + class TextureViewAliasScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + if (!TextureViewUsable()) { + GTEST_SKIP() << "glTextureView is unavailable on backend " << Gl().BackendName() + << " (GL_ARB_texture_view not advertised). On DirectGLES that is " + "the MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 opt-in this lane " + "did not set; every lane this scenario is registered in sets it."; + return; + } + std::string error; + m_program2D = CompileProgram(kVS, kFS2D, &error); + ASSERT_NE(m_program2D, 0u) << error; + m_programArray = CompileProgram(kVS, kFSArray, &error); + ASSERT_NE(m_programArray, 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); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the fixture setup left a GL error behind"; + } + + void TearDown() override { + if (!Ready()) return; + glUseProgram(0); + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + for (const GLuint texture : m_textures) { + glDeleteTextures(1, &texture); + } + m_textures.clear(); + if (m_vao != 0) glDeleteVertexArrays(1, &m_vao); + if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer); + if (m_program2D != 0) glDeleteProgram(m_program2D); + if (m_programArray != 0) glDeleteProgram(m_programArray); + } + + // TextureViewScenario's probe, verbatim in intent: a trivial same-format full-range + // view. A backend that simply does not have the feature skips rather than failing + // every case in the file. + bool TextureViewUsable() { + GLuint storage = 0; + glGenTextures(1, &storage); + glBindTexture(GL_TEXTURE_2D, storage); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1); + glBindTexture(GL_TEXTURE_2D, 0); + GLuint view = 0; + glGenTextures(1, &view); + while (glGetError() != GL_NO_ERROR) { + } + glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1); + const bool usable = glGetError() == GL_NO_ERROR; + glDeleteTextures(1, &view); + glDeleteTextures(1, &storage); + return usable; + } + + GLuint MakeTexture() { + GLuint texture = 0; + glGenTextures(1, &texture); + m_textures.push_back(texture); + return texture; + } + + // An immutable RGBA8 array with NEAREST filtering and one level, seeded per layer by + // CPU SUB-IMAGE rather than by rendering. Seeding through the GPU would additionally + // depend on a CPU sub-image reaching a layer whose content the GPU wrote, which is a + // different question and would make a failure here unattributable - the reason + // TextureViewScenario.cpp:717-721 gives for the same choice. + GLuint MakeSeededArray() { + const GLuint texture = MakeTexture(); + glBindTexture(GL_TEXTURE_2D_ARRAY, texture); + glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + for (int layer = 0; layer < kLayers; ++layer) { + UploadLayerThroughOwner(texture, layer, LayerSeed(layer)); + } + return texture; + } + + static Rgba8 LayerSeed(int layer) { + return Rgba8{static_cast(10 + layer * 20), + static_cast(200 - layer * 20), 30, 255}; + } + + static std::vector Fill(Rgba8 colour) { + std::vector texels(static_cast(kSize) * kSize * 4); + for (std::size_t i = 0; i < texels.size(); i += 4) { + texels[i + 0] = colour.r; + texels[i + 1] = colour.g; + texels[i + 2] = colour.b; + texels[i + 3] = colour.a; + } + return texels; + } + + void UploadLayerThroughOwner(GLuint owner, int layer, Rgba8 colour) { + const std::vector texels = Fill(colour); + glBindTexture(GL_TEXTURE_2D_ARRAY, owner); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kSize, kSize, 1, GL_RGBA, + GL_UNSIGNED_BYTE, texels.data()); + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + } + + // THE VIEW-SIDE WRITE. A single-layer GL_TEXTURE_2D view of the array: its own level 0 + // is the owner's level , and its own layer 0 is the owner's layer + // kViewLayer. glTexSubImage2D here is what ToOwnerRegionOffset has to move. + void UploadThroughView(GLuint view, Rgba8 colour) { + const std::vector texels = Fill(colour); + glBindTexture(GL_TEXTURE_2D, view); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RGBA, GL_UNSIGNED_BYTE, + texels.data()); + glBindTexture(GL_TEXTURE_2D, 0); + } + + GLuint MakeLayerView(GLuint owner) { + const GLuint view = MakeTexture(); + glTextureView(view, GL_TEXTURE_2D, owner, GL_RGBA8, 0, 1, kViewLayer, 1); + EXPECT_EQ(glGetError(), static_cast(GL_NO_ERROR)) + << "2D_ARRAY -> 2D is a legal view pair"; + glBindTexture(GL_TEXTURE_2D, view); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glBindTexture(GL_TEXTURE_2D, 0); + return view; + } + + // ---- the two sampling draws. Both go to the default framebuffer, which is what + // makes them ordinary draws that reach the validate point and run the drain. ---- + + Image SampleThroughTheView(GLuint view) { + BindDefaultFramebuffer(); + glViewport(0, 0, Gl().Width(), Gl().Height()); + glUseProgram(m_program2D); + glUniform1i(glGetUniformLocation(m_program2D, "uTex"), 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, view); + glBindVertexArray(m_vao); + glDrawArrays(GL_TRIANGLES, 0, 6); + return ReadPixels(Gl().Width(), Gl().Height()); + } + + Image SampleThroughTheOwner(GLuint owner, int layer) { + BindDefaultFramebuffer(); + glViewport(0, 0, Gl().Width(), Gl().Height()); + glUseProgram(m_programArray); + glUniform1i(glGetUniformLocation(m_programArray, "uTex"), 0); + glUniform1i(glGetUniformLocation(m_programArray, "uLayer"), layer); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D_ARRAY, owner); + glBindVertexArray(m_vao); + glDrawArrays(GL_TRIANGLES, 0, 6); + return ReadPixels(Gl().Width(), Gl().Height()); + } + + // Whole-region rather than a spot check, for the reason HeadlessGL.h gives: three of + // four vertices carrying stale data still paints a correct centre pixel. + void ExpectWholeSurface(const Image& image, Rgba8 expected, const char* what) { + int offenders = 0; + Rgba8 firstOffender{}; + int firstX = -1; + int firstY = -1; + const int x1 = image.Width() - kInset; + const int y1 = image.Height() - kInset; + int total = 0; + for (int y = kInset; y < y1; ++y) { + for (int x = kInset; x < x1; ++x) { + ++total; + const Rgba8 actual = image.At(x, y); + const bool ok = std::abs(int(actual.r) - int(expected.r)) <= 2 && + std::abs(int(actual.g) - int(expected.g)) <= 2 && + std::abs(int(actual.b) - int(expected.b)) <= 2 && + std::abs(int(actual.a) - int(expected.a)) <= 2; + if (!ok) { + if (offenders == 0) { + firstOffender = actual; + firstX = x; + firstY = y; + } + ++offenders; + } + } + } + EXPECT_EQ(offenders, 0) << what << ": " << offenders << " of " << total + << " pixels disagree; first at (" << firstX << ", " << firstY + << ") is " << Describe(firstOffender) << ", expected " + << Describe(expected) << " +/- 2"; + } + + // ---- the emitter half ------------------------------------------------------------- + // + // WHY "THE EMITTER NEVER SAW ANY TEXTURE" IS A SKIP AND "IT SAW TEXTURES BUT EMITTED + // NO SUB-DATA" IS RED, and the distinction is the whole value of the reading. In a + // PULL build the emitter does not exist, and in a lane whose MOBILEGL_PIPE_PUSH mask + // has the texture-resource bit clear (D-K2's rows; the 0x5ff / 0x7ff refusal lanes) + // it exists but is deliberately not wired - in both the counters are flat and nothing + // is being claimed. The moment ANY create or respecify has been counted, the emitter + // is live for this process, and a drain that then put nothing on the wire is the + // two-cursor regression itself. + struct EmitWindow { + bool live = false; + TextureEmitCountersPeek before{}; + TextureEmitCountersPeek after{}; + unsigned long long SubDataDelta() const { return after.SubDataCount - before.SubDataCount; } + unsigned long long RefusedDelta() const { + return after.RefusedSubDataCount - before.RefusedSubDataCount; + } + unsigned long long DescribedDelta() const { + return (after.CreateCount - before.CreateCount) + + (after.RespecifyCount - before.RespecifyCount); + } + }; + + bool OpenEmitWindow(EmitWindow& window) { + window.live = PeekTextureEmitCounters(&window.before); + return window.live; + } + + void CloseEmitWindow(EmitWindow& window) { + if (!window.live) return; + window.live = PeekTextureEmitCounters(&window.after); + } + + // The emitter is armed in THIS process if it has ever described a texture to the + // applier. Checked on the window's `after` reading, because the seeding uploads that + // arm it happen inside the window. + bool EmitterIsArmed(const EmitWindow& window) const { + return window.live && (window.after.CreateCount != 0 || window.after.RespecifyCount != 0); + } + + void ExpectTheDrainEmitted(const EmitWindow& window, const char* what) { + if (!window.live) { + RecordProperty("emit_counters", TextureEmitPeekSkipReason()); + return; + } + if (!EmitterIsArmed(window)) { + // Not "not asserted": a lane where the emitter never described a single + // texture cannot have an emission cursor at all, so there is no cursor claim + // to make. Recorded so a run where the whole file quietly stopped asserting + // is visible in the ctest XML rather than invisible. + RecordProperty("emit_cursor", "the client texture emitter described no texture in " + "this process - the texture-resource subsystem bit " + "is clear in this lane"); + return; + } + EXPECT_GT(window.SubDataDelta(), 0u) + << what << ": the client texture emitter described " + << window.DescribedDelta() + << " texture(s) across this draw and put NO resource_subdata on the wire. A " + "drain that runs and emits nothing is exactly what a per-NAME emission " + "cursor produces when a view and its owner each hold one: whichever cursor " + "drained first cleared the dirty flag the other still needed. The picture " + "can still be right - the next whole-level respecify, a glGenerateMipmap or " + "the monolith sync path re-uploads the level one frame later - which is why " + "this counter and not the pixels is the load-bearing half here."; + EXPECT_EQ(window.RefusedDelta(), 0u) + << what << ": the applier REFUSED " << window.RefusedDelta() + << " of this drain's resource_subdata records. A refusal leaves the level dirty " + "and on the drain list (D-D5 step 1), which is the safe direction but not " + "the expected one for a glTexSubImage of a texture that has storage."; + EXPECT_EQ(window.after.DrainListSize, 0u) + << what << ": " << window.after.DrainListSize + << " (storage owner, upload target, level) key(s) are STILL owed after the " + "validate point. A level that bailed - no storage, no shadow, an empty box - " + "stays on the list; for this workload every level has immutable storage and " + "a non-empty dirty box, so nothing should have bailed."; + } + + GLuint m_program2D = 0; + GLuint m_programArray = 0; + GLuint m_vao = 0; + GLuint m_quadBuffer = 0; + std::vector m_textures; + }; + + // ------------------------------------------------------------------------------------ + // (a) UPLOAD THROUGH THE VIEW, SAMPLE THROUGH THE OWNER. + // + // The direction that pins ToOwnerRegionOffset: the view's own layer 0 is the owner's + // layer kViewLayer, so a remap that was dropped marks - and uploads - the owner's layer 0 + // instead, and the two reads below catch it from both sides at once. + // ------------------------------------------------------------------------------------ + TEST_F(TextureViewAliasScenario, SubImageThroughAViewIsSampledThroughTheOwner) { + if (!Ready() || IsSkipped()) return; + + constexpr Rgba8 kPainted{255, 0, 255, 255}; + EmitWindow window; + OpenEmitWindow(window); + + const GLuint owner = MakeSeededArray(); + const GLuint view = MakeLayerView(owner); + UploadThroughView(view, kPainted); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "writing through the view raised a GL error"; + + const Image throughOwner = SampleThroughTheOwner(owner, kViewLayer); + CloseEmitWindow(window); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the sampling draw raised a GL error"; + + ExpectWholeSurface(throughOwner, kPainted, + "the owner's layer kViewLayer must carry the texels written through " + "the view's own layer 0"); + ExpectTheDrainEmitted(window, "upload through a layer-restricted view"); + + // THE NEGATIVE HALF, and it is what separates "the remap was dropped" from "the + // upload never arrived": a lost layer origin writes the owner's layer 0, which this + // read would see as an untouched layer that moved. + const Image layerZero = SampleThroughTheOwner(owner, 0); + ExpectWholeSurface(layerZero, LayerSeed(0), + "the owner's layer 0 is outside the view's window and must not have " + "been written"); + } + + // ------------------------------------------------------------------------------------ + // (b) UPLOAD THROUGH THE OWNER, SAMPLE THROUGH THE VIEW. + // + // The reverse direction, and the one that fails when the VIEW holds a cursor of its own: + // the owner's cursor drains and clears the shared flag, and the view's sampler then + // resolves a resource the record never reached. + // ------------------------------------------------------------------------------------ + TEST_F(TextureViewAliasScenario, SubImageThroughTheOwnerIsSampledThroughTheView) { + if (!Ready() || IsSkipped()) return; + + constexpr Rgba8 kPainted{0, 0, 255, 255}; + EmitWindow window; + OpenEmitWindow(window); + + const GLuint owner = MakeSeededArray(); + const GLuint view = MakeLayerView(owner); + UploadLayerThroughOwner(owner, kViewLayer, kPainted); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "writing through the owner raised a GL error"; + + const Image throughView = SampleThroughTheView(view); + CloseEmitWindow(window); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the sampling draw raised a GL error"; + + ExpectWholeSurface(throughView, kPainted, + "the view must read the texels written through the owner's own name " + "into the layer the view addresses"); + ExpectTheDrainEmitted(window, "upload through the storage owner"); + } + + // ------------------------------------------------------------------------------------ + // (c) THE SAME, ACROSS DRAW BOUNDARIES, so the drain runs more than once. + // + // WHY THE SECOND AND THIRD DRAINS ARE THE POINT. The first drain of a texture's life + // carries the whole level anyway - the storage definition made it dirty - so a cursor + // that is keyed on the wrong object still ships the right texels by accident. It is the + // NEXT drain, where only the sub-region written since the last validate point is owed, + // that can lose a record and look healthy. + // ------------------------------------------------------------------------------------ + TEST_F(TextureViewAliasScenario, TheViewUploadDrainRunsAgainAfterADrawBoundary) { + if (!Ready() || IsSkipped()) return; + + constexpr Rgba8 kFirst{255, 0, 255, 255}; + constexpr Rgba8 kSecond{0, 255, 255, 255}; + + const GLuint owner = MakeSeededArray(); + const GLuint view = MakeLayerView(owner); + + // Drain 1: the storage definition's own upload. Read only to close the window. + const Image seeded = SampleThroughTheOwner(owner, kViewLayer); + ExpectWholeSurface(seeded, LayerSeed(kViewLayer), "the seeded layer before any view write"); + Gl().EndFrame(); + + EmitWindow second; + OpenEmitWindow(second); + UploadThroughView(view, kFirst); + const Image afterFirst = SampleThroughTheOwner(owner, kViewLayer); + CloseEmitWindow(second); + ExpectWholeSurface(afterFirst, kFirst, "the first view write, through the owner"); + ExpectTheDrainEmitted(second, "the SECOND drain (first view write after a draw boundary)"); + Gl().EndFrame(); + + EmitWindow third; + OpenEmitWindow(third); + UploadThroughView(view, kSecond); + const Image afterSecond = SampleThroughTheOwner(owner, kViewLayer); + CloseEmitWindow(third); + ExpectWholeSurface(afterSecond, kSecond, "the second view write, through the owner"); + ExpectTheDrainEmitted(third, "the THIRD drain (second view write after a draw boundary)"); + + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the workload left a GL error behind"; + const Image layerZero = SampleThroughTheOwner(owner, 0); + ExpectWholeSurface(layerZero, LayerSeed(0), + "two writes through the view must still have left layer 0 alone"); + } + + // ------------------------------------------------------------------------------------ + // (d) the owner direction across the same boundary. + // ------------------------------------------------------------------------------------ + TEST_F(TextureViewAliasScenario, TheOwnerUploadDrainRunsAgainAfterADrawBoundary) { + if (!Ready() || IsSkipped()) return; + + constexpr Rgba8 kFirst{0, 0, 255, 255}; + constexpr Rgba8 kSecond{255, 255, 0, 255}; + + const GLuint owner = MakeSeededArray(); + const GLuint view = MakeLayerView(owner); + + const Image seeded = SampleThroughTheView(view); + ExpectWholeSurface(seeded, LayerSeed(kViewLayer), + "the view must read its layer's seed before any owner write"); + Gl().EndFrame(); + + EmitWindow second; + OpenEmitWindow(second); + UploadLayerThroughOwner(owner, kViewLayer, kFirst); + const Image afterFirst = SampleThroughTheView(view); + CloseEmitWindow(second); + ExpectWholeSurface(afterFirst, kFirst, "the first owner write, through the view"); + ExpectTheDrainEmitted(second, "the SECOND drain (first owner write after a draw boundary)"); + Gl().EndFrame(); + + EmitWindow third; + OpenEmitWindow(third); + UploadLayerThroughOwner(owner, kViewLayer, kSecond); + const Image afterSecond = SampleThroughTheView(view); + CloseEmitWindow(third); + ExpectWholeSurface(afterSecond, kSecond, "the second owner write, through the view"); + ExpectTheDrainEmitted(third, "the THIRD drain (second owner write after a draw boundary)"); + + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the workload left a GL error behind"; + } + + } // namespace +} // namespace MGITest diff --git a/docs/Disaggregated/notes/p34b/espryt-d2.md b/docs/Disaggregated/notes/p34b/espryt-d2.md new file mode 100644 index 00000000..dfe894fc --- /dev/null +++ b/docs/Disaggregated/notes/p34b/espryt-d2.md @@ -0,0 +1,100 @@ +# P3b/P4b wave 2-D package D2 — the Espryt stream, part 2 + +The verification gates P3b/P4b owes, plus the texture / program split-scenario census. +Base `7ed5da52` (`feat/disaggregated`), worktree `~/w7/p7-espryt-d2`, branch `p7/espryt-d2`. + +Package D1 owns `MG_Backend/DirectGLES/{DirectGLES.cpp,Managers.cpp}`, the XFB/readback +scenarios and their split-arm registrations. Nothing here touches those; every +`MG_IntegrationTest/CMakeLists.txt` edit sits in a block headed `package D2`. + +--- + +## Slice 1 — `TextureViewAliasScenario` + +The verification half of "按存储属主键控的发射游标与 view 索引重映射". The code half landed in +P4a/P5e: the drain cursor is owner-keyed (`MG_Impl/Pipe/TextureEmit.h`, `NoteLevelDirty` + +`DrainTextureSubData`), the index remap is `TextureObjectView::ToOwnerLevel` / +`ToOwnerUploadTarget` / `ToOwnerRegionOffset`, the wire half is `MGPSamplerView` + +`MGPResourceDesc::ViewOf`. + +Four cases, each asserting on TWO channels at once — sampled texels AND +`MGPipeTextureEmitter::SubDataCount()` / `RefusedSubDataCount()` / `DrainListSize()`, read through +the new `Harness/TextureEmitPeek.{h,cpp}`: + +| case | direction | drains | +|---|---|---| +| `SubImageThroughAViewIsSampledThroughTheOwner` | view upload → owner sample | 1 | +| `SubImageThroughTheOwnerIsSampledThroughTheView` | owner upload → view sample | 1 | +| `TheViewUploadDrainRunsAgainAfterADrawBoundary` | view upload → owner sample | 3 | +| `TheOwnerUploadDrainRunsAgainAfterADrawBoundary` | owner upload → view sample | 3 | + +### Red-once (R-16) + +`TextureObjectView::MarkStorageDirtyRegion` patched to call `PipeNoteLevelDirty` on ITSELF instead +of forwarding to the storage owner — the two-cursor shape the design rejects. + +``` +DirectGLES.TextureViewAliasScenario.TheViewUploadDrainRunsAgainAfterADrawBoundary FAILED + the first view write, through the owner: 11408 of 11408 pixels disagree; + first at (2, 2) is rgba(50,160,30,255), expected rgba(255,0,255,255) + Expected: (window.SubDataDelta()) > (0u), actual: 0 vs 0 + the SECOND drain (first view write after a draw boundary): the client texture emitter + described 0 texture(s) across this draw and put NO resource_subdata on the wire. + ... and the same pair again for the THIRD drain. +``` + +**Both channels red simultaneously**, which is the property the file was written for: stale texels +AND a drain that emitted nothing. + +**Deviation from the plan's prescription, recorded rather than papered over.** The plan says +"case (a) must go red". It does not — case (c) does. Case (a) creates the texture, writes through +the view and drains ONCE, and that first drain of a texture's life carries the whole level anyway +(the storage definition already marked it dirty), so a cursor keyed on the wrong object still +ships the right texels by accident. It is the SECOND and THIRD drains, where only the sub-region +written since the last validate point is owed, that can lose a record. Cases (c)/(d) exist for +exactly that and are where the red lands. The scenario header states this; case (a) is kept +because it is the cheapest statement of the remap itself. + +### A defect this slice FOUND: owner writes are invisible through a sampled view under split + +`TheOwnerUploadDrainRunsAgainAfterADrawBoundary` is **red on inproc, spawn and tcp, green under +monolith**, on the unmodified tree. + +Once a view has been SAMPLED, a later `glTexSubImage` through the OWNER's own name never becomes +visible through the view. The client half is provably correct — the case's own counter assertions +pass, so the records were emitted and the applier accepted them. + +* The server's per-texture clean gate `BackendTextureObject::IsDrawSyncCleanByRecord` + (`Managers.cpp:15433`) compares the VIEW's `record.Serial` and `record.PendingUploads`. +* An owner-side `resource_subdata` moves only the OWNER's (`MG_Pipe/PipeApply.cpp:1175`). +* Nothing propagates a serial bump from a storage owner to the records that view it. + `MGPResourceDesc::ViewOf` is a forward edge with no reverse index, and the applier reads it only + in the descriptor-equality helper (`PipeApply.cpp:666`). +* Under monolith the same gate reads `GetContentVersion()` THROUGH the view, which + `TextureObjectView` forwards to the owner (`TextureObjectView.cpp:100-102`). The split gate has + no equivalent of that forwarding. +* `MG_Remote/CONTRACT-P5E.md:387-395` states the split gate in full and has the same hole, so the + implementation is faithful to the contract. **The contract is what is wrong.** + +`KHR-GL43.texture_view.coherency` is exactly this test and reads Pass in the CTS baseline — which +is a **monolith** reading (`notes/p7/device-window-1/CTS-base/README.md:16`). + +**Not fixed here.** The fix is either at the gate (`Managers.cpp` — package D1's file) or at the +publisher (`MG_Pipe/PipeApply.cpp`'s `ApplyTextureUpload`, outside D2's declared footprint), and +the publisher-side form needs either an O(#texture records) scan per `resource_subdata` or a new +reverse index on `MGPipeResourceRecord` — a design decision with a contract amendment attached. +The case is **excluded by name from the three split arms** with the cause stated in the CMake +comment, kept in the monolith lanes where it is the regression net for the forwarding that works, +and listed in `split-scenario-census.md`. G14: the name still exists in every lane. + +### The tcp arm was green-skipping everything + +`MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1` on `MGL_ITEST_GLES_TCP_ENVIRONMENT` is not enough: the tcp +lane's server is **pre-started by the `TcpServer.Start` fixture**, not spawned from a client that +carries the lane environment, so the server withheld `GL_ARB_texture_view` and all four cases +SKIPPED while the identical Split and Spawn arms ran. Fixed by putting the knob on the fixture +entry's own `ENVIRONMENT` property (`tcp_server_fixture.py` builds the supervisor's environment +from `os.environ` and strips only the transport keys). A feature knob that reaches two arms of +three is the worst of the three states: the lane is green and tests nothing. + +---