From 972dd811d74bcc8501805e1e56e465d7c48aa921 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 8 Sep 2026 18:11:04 -0400 Subject: [PATCH] [Test] (Espryt): take G9's reading while the texture is still attachment-only - the applier's params record, Espryt's applied value and the absence of a sampler view, before the sample that would repair all three --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 1 + .../Harness/PipeApplyPeek.cpp | 188 ++++++++++++++ .../Harness/PipeApplyPeek.h | 110 ++++++++ ...xtureParamsWithoutASamplerViewScenario.cpp | 240 +++++++++++++++++- 4 files changed, 534 insertions(+), 5 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.cpp create mode 100644 MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.h diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 50cf763f..80e28799 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -53,6 +53,7 @@ add_executable(MobileGLIntegrationTest Harness/HeadlessGL.cpp Harness/BackendCapsPeek.cpp Harness/PipeSlotPeek.cpp + Harness/PipeApplyPeek.cpp Scenarios/OrientationScenario.cpp Scenarios/CrossFrameBufferScenario.cpp Scenarios/ResidentIndexScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.cpp b/MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.cpp new file mode 100644 index 00000000..d2d09e68 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.cpp @@ -0,0 +1,188 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.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 "PipeApplyPeek.h" + +#if !defined(__ANDROID__) +#include +#if MOBILEGL_PIPE_PUSH +#include +#include +#include +#include +#include +#include +#define MGITEST_PIPE_APPLY_PEEK_LIVE 1 +#endif +#endif + +namespace MGITest { + +#if defined(MGITEST_PIPE_APPLY_PEEK_LIVE) + namespace { + namespace MGP = MobileGL::MG_Pipe; + namespace MGB = MobileGL::MG_Backend::DirectGLES; + + // The frontend texture object a GL name denotes in the CURRENT context, or null. This is + // a LOOKUP KEY and nothing else: every value this file reports comes from the applier or + // from Espryt, never from the object found here. (Reading the frontend's own parameter + // state would answer the question the scenario is asking with the input to it.) + MobileGL::MG_State::GLState::ITextureObject* FrontendTexture(unsigned glTextureName) { + if (!MobileGL::MG_State::pGLContext) return nullptr; + const auto& object = MobileGL::MG_State::pGLContext->GetTextureObject( + static_cast(glTextureName)); + return object ? object.get() : nullptr; + } + + // Espryt's twin for that texture, or null - which is also this file's "is Espryt even the + // backend running" answer. On Magma no Espryt twin was ever built, so every entry point + // below stops here rather than reaching for g_GLESFuncs, whose members are null there. + MGB::TextureImpl::BackendTextureObject* EsprytTwin(unsigned glTextureName) { + MobileGL::MG_State::GLState::ITextureObject* const object = FrontendTexture(glTextureName); + if (object == nullptr) return nullptr; + auto* const found = MGB::TextureImpl::g_backendTextureObjects.Find(object); + if (found == nullptr || !*found) return nullptr; + return found->get(); + } + + int SwizzleToGLEnum(MobileGL::Uint8 encoded) { + return static_cast(MobileGL::MG_Util::ConvertTextureSwizzleParamToGLEnum( + static_cast(encoded))); + } + + // MGPipeTypes.h owns the two numbers and says why depth is 0 (a zeroed record must decode + // to what an untouched texture already has). This is that decode, and nothing else in + // this module may open-code it. + int DepthStencilModeToGLEnum(MobileGL::Uint8 encoded) { + return encoded == MGP::kMGPipeDepthStencilModeStencil ? GL_STENCIL_INDEX + : GL_DEPTH_COMPONENT; + } + + // The GL_TEXTURE_BINDING_* query for a target, or 0 where this file has no answer. A + // guess would be worse than a refusal: the binding is what gets RESTORED, so a wrong + // pname would leave the driver bound to this test's texture. + int BindingQueryFor(unsigned glTarget) { + switch (glTarget) { + case GL_TEXTURE_2D: return GL_TEXTURE_BINDING_2D; + default: return 0; + } + } + } // namespace + + bool PeekPipeTextureParamsRecord(unsigned glTextureName, PipeTextureParamsRecordPeek* out) { + if (out == nullptr) return false; + const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier(); + // Slot 0 is the reserved null handle and is never live (MGPipeHandles.h), so the scan + // starts at 1 and a match at 0 is impossible rather than merely unlikely. + for (MobileGL::SizeT slot = 1; slot < applier.TextureResources.size(); ++slot) { + const MGP::MGPipeResourceRecord& record = applier.TextureResources[slot]; + if (!record.Live) continue; + if (record.Desc.GlNameForDiag != static_cast(glTextureName)) continue; + out->Slot = static_cast(slot); + out->Gen = static_cast(record.Gen); + out->ParamsSerial = static_cast(record.ParamsSerial); + for (int channel = 0; channel < 4; ++channel) { + out->Swizzle[channel] = SwizzleToGLEnum(record.Params.Swizzle[channel]); + } + out->DepthStencilMode = DepthStencilModeToGLEnum(record.Params.DepthStencilMode); + return true; + } + return false; + } + + bool PeekEsprytAppliedTextureParams(unsigned glTextureName, unsigned glTarget, + EsprytAppliedTextureParamsPeek* out) { + if (out == nullptr) return false; + const int bindingQuery = BindingQueryFor(glTarget); + if (bindingQuery == 0) return false; + MGB::TextureImpl::BackendTextureObject* const twin = EsprytTwin(glTextureName); + if (twin == nullptr) return false; + const MobileGL::Uint backendId = twin->GetBackendTextureId(); + if (backendId == 0) return false; + if (MGB::g_GLESFuncs.glGetTexParameteriv == nullptr || + MGB::g_GLESFuncs.glBindTexture == nullptr || MGB::g_GLESFuncs.glGetIntegerv == nullptr || + MGB::g_GLESFuncs.glGetError == nullptr) { + return false; + } + + // SAVE / QUERY / RESTORE ON THE UNIT THAT IS ALREADY ACTIVE. No glActiveTexture, so the + // only driver state this touches is one unit's binding, and it is put back byte for byte + // - which is what keeps Espryt's own g_boundTexturesCache true rather than merely + // consistent. (Binding through the twin's own Bind() would update that shadow and would + // therefore CHANGE what the scenario measures next; this does not.) + GLint previousBinding = 0; + MGB::g_GLESFuncs.glGetIntegerv(static_cast(bindingQuery), &previousBinding); + MGB::g_GLESFuncs.glBindTexture(static_cast(glTarget), backendId); + + out->BackendTextureId = static_cast(backendId); + static const GLenum kSwizzlePnames[4] = {GL_TEXTURE_SWIZZLE_R, GL_TEXTURE_SWIZZLE_G, + GL_TEXTURE_SWIZZLE_B, GL_TEXTURE_SWIZZLE_A}; + for (int channel = 0; channel < 4; ++channel) { + GLint value = 0; + MGB::g_GLESFuncs.glGetTexParameteriv(static_cast(glTarget), + kSwizzlePnames[channel], &value); + out->Swizzle[channel] = static_cast(value); + } + + // The depth/stencil aspect mode is ES 3.1 and is INVALID_ENUM on a driver without it, so + // it is asked for last and its own error decides whether the answer is usable. The queue + // is drained first because a stale error from anywhere else would be indistinguishable + // from this call's - Espryt drains it the same way at every one of its own sync sites + // (DebugImpl::ErrorLopper), and this module's own GL errors are read from the FRONTEND + // state (ScenarioTest::FirstGLError), which none of this touches. + while (MGB::g_GLESFuncs.glGetError() != GL_NO_ERROR) { + } + GLint mode = 0; + MGB::g_GLESFuncs.glGetTexParameteriv(static_cast(glTarget), + GL_DEPTH_STENCIL_TEXTURE_MODE, &mode); + out->DepthStencilModeIsReadable = MGB::g_GLESFuncs.glGetError() == GL_NO_ERROR; + out->DepthStencilMode = static_cast(mode); + + MGB::g_GLESFuncs.glBindTexture(static_cast(glTarget), + static_cast(previousBinding)); + while (MGB::g_GLESFuncs.glGetError() != GL_NO_ERROR) { + } + return true; + } + + bool PeekEsprytHasSamplerViewForTexture(unsigned glTextureName, bool* outExists) { + if (outExists == nullptr) return false; + MobileGL::MG_State::GLState::ITextureObject* const object = FrontendTexture(glTextureName); + if (object == nullptr) return false; + // Espryt must be the backend running, or "no view" would be true of every texture on + // every other backend and the assertion would be vacuous where it is loudest. + if (EsprytTwin(glTextureName) == nullptr) return false; + // HandleOfSamplerViewForTexture is the monolith glue that derives the view's handle from + // the TEXTURE's lifetime id (D-F2: one view per ITextureObject), so this asks Espryt's + // own table the same way Espryt asks it - it does not consult the applier record's + // ViewCso, which is the client's statement about the same fact and would make one side + // of the seam vouch for the other. + const MGP::MGPipeHandle view = MGB::SamplerViewImpl::HandleOfSamplerViewForTexture(object); + if (MGP::MGPipeHandleIsNull(view)) { + *outExists = false; + return true; + } + *outExists = MGB::SamplerViewImpl::FindSamplerViewForHandle(view) != nullptr; + return true; + } + + bool PeekPipeApplierRefusedNoConsumer(unsigned long long* outCount) { + if (outCount == nullptr) return false; + *outCount = static_cast(MGP::MGPipeApplier().RefusedNoConsumer); + return true; + } +#else + bool PeekPipeTextureParamsRecord(unsigned, PipeTextureParamsRecordPeek*) { return false; } + bool PeekEsprytAppliedTextureParams(unsigned, unsigned, EsprytAppliedTextureParamsPeek*) { + return false; + } + bool PeekEsprytHasSamplerViewForTexture(unsigned, bool*) { return false; } + bool PeekPipeApplierRefusedNoConsumer(unsigned long long*) { return false; } +#endif + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.h b/MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.h new file mode 100644 index 00000000..233e0982 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.h @@ -0,0 +1,110 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.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 APPLIER's texture-parameter record, ESPRYT's applied value for the same texture, and +// whether that texture has a sampler view yet. Three readings taken from a scenario, for gate +// G9's WHITE-BOX half. +// +// WHY A WHITE-BOX HALF EXISTS AT ALL (ID-19, brief section F, gates review R1). G9's public-GL +// cases in TextureParamsWithoutASamplerViewScenario.cpp catch "the parameter never reached the +// driver". They CANNOT catch "the parameter reached the driver LATE", because a texture +// parameter's only public-GL observable is a SAMPLE and the sample is itself what repairs an +// unsynced parameter: it puts the texture on the unit list, and that walk pushes the parameters +// for anything whose params serial moved. A backend that deferred every attachment-only +// texture's parameters to the first sampler view would be green on all four of those cases, +// forever, on every tree. The distinction only exists on the inside, so the reading has to be +// taken there - while the texture is still attachment-only, before any sample. +// +// 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. This one goes further than PipeSlotPeek and includes Espryt's own +// Managers.h, which is exactly why it may not be anywhere near a scenario's GL headers. +// +// EVERY ENTRY POINT RETURNS false, TOUCHING NOTHING, WHERE IT CANNOT LOOK, and a caller that +// gets false has learned NOTHING - "could not look" is not "was applied". Out of reach means: +// a PULL build (there is no applier: it is `#if MOBILEGL_PIPE_PUSH`); Android, where this module +// links the shipping libMobileGL.so built -fvisibility=hidden and no internal symbol resolves; +// a backend that is not DirectGLES (Espryt is the subject; Magma answers the same GL question +// through P7's own paths); and, for the record peek, a mask whose texture-resource bit is off, +// where no record exists to find because nothing was ever emitted. + +#pragma once + +namespace MGITest { + + // ---- the applier's set_texture_params record for one GL texture name ------------------ + // + // ADDRESSED BY GL NAME, and the search key is MGPResourceDesc::GlNameForDiag. That field is + // diagnostics-only by contract - never an identity, never a memo key (MGPipeTypes.h) - and + // this is a diagnostic: a test harness looking for the record a named GL object produced. + // The alternative would be to ask the CLIENT emitter for the texture's handle, and the + // review is explicit that this probe must arm on package D's applier/backend state and not + // on the emitter markers B and C set: they are different questions, and a shared marker + // would re-create the shape review F-M5 was raised about. + struct PipeTextureParamsRecordPeek { + // The handle the record sits at, so a caller can print it. + unsigned Slot; + unsigned Gen; + // set_texture_params' own serial. 0 means the record exists (the resource was created) + // but NO set_texture_params has ever been applied to it - which is a different finding + // from "no record", and the two must not be merged. + unsigned long long ParamsSerial; + // MGPTextureParams::Swizzle[4], translated to the GL enums the application passed to + // glTextureParameteri (GL_ZERO / GL_ONE / GL_RED / GL_GREEN / GL_BLUE / GL_ALPHA), so + // the scenario compares what it set against what the record carries in ONE vocabulary + // and neither side has to know the other's encoding. + int Swizzle[4]; + // MGPTextureParams::DepthStencilMode, translated the same way: GL_DEPTH_COMPONENT or + // GL_STENCIL_INDEX. + int DepthStencilMode; + }; + + bool PeekPipeTextureParamsRecord(unsigned glTextureName, PipeTextureParamsRecordPeek* out); + + // ---- Espryt's APPLIED value for the same texture -------------------------------------- + // + // Read from the DRIVER, through the twin's own ES name, because "applied" means the driver + // was told - the same thing package D's white-box unit probe asserts against its mocked + // driver (esprytobj-v2 (9)). The current binding on the ACTIVE unit is saved and restored + // around the query and no unit is switched, so Espryt's binding shadow still describes + // reality afterwards: nothing is perturbed for it to be stale about. + // + // `glTarget` is the texture's GL target (only GL_TEXTURE_2D is supported today; any other + // target returns false rather than guessing a binding query). + struct EsprytAppliedTextureParamsPeek { + // The driver name Espryt minted for this texture, for the caller's message. + unsigned BackendTextureId; + int Swizzle[4]; + int DepthStencilMode; + // False when the driver rejected the depth/stencil query - a non-depth texture, or an ES + // level without GL_DEPTH_STENCIL_TEXTURE_MODE. The swizzle half is still valid. + bool DepthStencilModeIsReadable; + }; + + bool PeekEsprytAppliedTextureParams(unsigned glTextureName, unsigned glTarget, + EsprytAppliedTextureParamsPeek* out); + + // ---- and the claim that makes the two above mean anything ------------------------------ + // + // Whether Espryt holds a SAMPLER VIEW twin for this texture. This is the assertion the + // public-GL cases cannot make, because making it there would create the view. `*outExists` + // is written only on true. + bool PeekEsprytHasSamplerViewForTexture(unsigned glTextureName, bool* outExists); + + // ---- c0f's belt, for the ObjectSubsystemControl arms ----------------------------------- + // + // MGPipeApplierState::RefusedNoConsumer: the number of P4a-family entry points that were + // refused because no backend had registered MGPipeResourceOps. On a backend WITH a consumer + // it must never move; on one without (Magma, ID-39/ID-40) the client's own gate is supposed + // to stop the emission before the belt is reached, so it must never move there either. A + // non-zero delta says the gate and the belt disagreed, which is the whole point of having + // both. Reset by MGPipeApplierReset, so a caller reads it as a DELTA and treats a value that + // went DOWN as "the applier was reset, count everything since as `after`". + bool PeekPipeApplierRefusedNoConsumer(unsigned long long* outCount); + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/TextureParamsWithoutASamplerViewScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/TextureParamsWithoutASamplerViewScenario.cpp index 4bd44023..150ebd66 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/TextureParamsWithoutASamplerViewScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/TextureParamsWithoutASamplerViewScenario.cpp @@ -48,11 +48,46 @@ // in particular they would go RED if any of the four reachability paths were ever made to // depend on the texture having a sampler VIEW - which is exactly the coupling P4a's // resource-addressed set_texture_params removes and the thing a later phase could reintroduce; -// * the "落地前必须红" artefact ROADMAP.md:20 asks for is NOT produced by this file, and no -// public-GL integration scenario on a monolith tree can produce it. Producing it needs an -// observation of the DRIVER's texture object taken while the texture is still -// read-attachment-only, which means a probe inside MG_Backend/DirectGLES - package D's files -// (C.7), not this package's. The integrator's ruling is recorded in the gates result document. +// * the "落地前必须红" artefact ROADMAP.md:20 asks for is NOT produced by the public-GL half +// of this file, and no public-GL integration scenario on a monolith tree can produce it. +// Producing it needs an observation of the DRIVER's texture object taken while the texture is +// still read-attachment-only. ID-19 rules that G9 is therefore a WHITE-BOX assertion, and this +// file now carries the SCENARIO half of it (the unit half is package D's, +// MG_Test/SanityTest.cpp's DirectGLESTextureSync.AnAttachmentOnlyTexturesParametersReachThe +// DriverWithNoSamplerView). +// +// THE WHITE-BOX HALF, and what it adds to the four cases below. Each case, at the point where its +// texture is reachable ONLY its own way and BEFORE the observing sample, takes three readings +// through MG_IntegrationTest/Harness/PipeApplyPeek.h and asserts all three: +// +// (a) the APPLIER holds a set_texture_params record for this texture, at a non-zero ParamsSerial, +// carrying the field the case moved; +// (b) ESPRYT's applied value for the same texture - read back from the DRIVER, through the twin's +// own ES name - is that value ALREADY, not after the first sampler view; +// (c) Espryt holds NO SAMPLER VIEW for this texture yet, which is what turns (b) from "applied" +// into "applied WITHOUT one" and is the whole claim D10 makes. +// +// (c) is the assertion the public-GL half structurally cannot make: making it there would create +// the view. (b) is the half that goes red on a backend that DEFERS - a tree where the parameter +// push is gated on a sampler view existing is green on all four public-GL cases forever, because +// the sample that observes the parameter is also what mints the view and repairs the state. +// +// WHAT THE THREE READINGS DO **NOT** COVER, so the next reader does not over-trust them +// (esprytobj re-review N-9, carried here by request). D's unit probe drives +// SyncTextureParamsToBackend directly, so the only deferral shape IT can see is one INSIDE that +// function. These three run through the real per-frame paths and therefore also see a deferral +// introduced ABOVE it - in SyncNeccessaryTextures, in the attachment walk, or in E's per-unit walk. +// Between them the two halves cover both, and neither covers both alone. +// +// A READING THAT CANNOT BE TAKEN IS DECLINED BY NAME AND THE CASE CONTINUES - it is not a +// GTEST_SKIP, and that is a deliberate departure from the shape the review sketched. These four +// cases are dual-purpose: they are also the END-TO-END regression net around D10, and that net is +// the ONLY thing measuring D10 on exactly the arms where the peek cannot look (the pull build, +// which has no applier at all; the 0x1ff and 0 lanes, where the texture family is switched off; +// Magma, which has no Espryt twin). Skipping the case there would delete the one verdict those +// lanes carry in order to report the absence of a second one. The decline is printed, recorded as +// a test property and named, so a lane that silently stopped taking the reading is visible in the +// log rather than in a count. // // WHY THE SECOND ONE IS THE RED, mechanically (scout-espryt-framebuffer.md 2.6, re-opened at the // base ref). Today a texture's parameters ride on the UNIT BINDING and on the DRAW attachment set: @@ -102,6 +137,7 @@ #include #include "../Harness/HeadlessGL.h" +#include "../Harness/PipeApplyPeek.h" #include "../Harness/ScenarioFixture.h" #ifdef GLAPI @@ -271,11 +307,172 @@ void main() { glTextureParameteri(texture, GL_TEXTURE_SWIZZLE_B, GL_ZERO); } + // Which of the two parameters a case moved, and therefore which one the white-box + // reading has to find on both sides of the seam. Two, because they are the two the + // four cases use and because a peek that reported "some parameter" would be green for + // a backend that applied the wrong one. + enum class MovedParameter { Swizzle, DepthStencilMode }; + + // ------------------------------------------------------------------------------ + // G9's WHITE-BOX READING (ID-19). Called by every case at the point where its + // texture is reachable only its own way and BEFORE the observing sample - which is + // the whole of the design, because the sample repairs what it observes. + // + // `expectedSwizzle` is the four GL enums the case set (or left at their defaults); + // `expectedDepthStencilMode` is GL_DEPTH_COMPONENT or GL_STENCIL_INDEX. Both are + // always passed and `moved` says which one is the case's subject, so a reader of a + // failure can see the untouched half beside the moved one. + void TakeTheWhiteBoxReadingBeforeAnySample(GLuint texture, GLenum target, + MovedParameter moved, + const GLint expectedSwizzle[4], + GLint expectedDepthStencilMode, + const char* whatMadeItReachable) { + const char* const movedName = + moved == MovedParameter::Swizzle ? "GL_TEXTURE_SWIZZLE_*" + : "GL_DEPTH_STENCIL_TEXTURE_MODE"; + + PipeTextureParamsRecordPeek record{}; + if (!PeekPipeTextureParamsRecord(static_cast(texture), &record)) { + DeclineTheWhiteBoxReading( + "no set_texture_params record for this texture in the applier. Either " + "there is no applier here (a PULL build: MGPipeApplierState is " + "#if MOBILEGL_PIPE_PUSH), or this lane's MOBILEGL_PIPE_PUSH leaves " + "kMGPipeSubsystemTextureResources (bit 10) clear, or no backend " + "registered MGPipeResourceOps so the client never emitted (c0f). The " + "end-to-end half of this case below is unaffected and still decides it."); + return; + } + + // From here the reading WAS taken, so everything is a hard assertion: a record + // that exists and does not carry the parameter is exactly the finding. + EXPECT_NE(record.ParamsSerial, 0u) + << "the applier holds a resource record for texture " << texture + << " at handle {" << record.Slot << ", " << record.Gen + << "} but its ParamsSerial is 0, i.e. NO set_texture_params has ever been " + "applied to it - and this case moved " << movedName << " while the texture " + "was " << whatMadeItReachable + << ". A parameter change on a texture with no sampler view has to produce a " + "record addressed BY RESOURCE (D10, D-E1); a zero here means the client " + "never emitted one, which is the coupling P4a exists to remove reappearing " + "on the emitter's side of the seam."; + + if (moved == MovedParameter::Swizzle) { + for (int channel = 0; channel < 4; ++channel) { + EXPECT_EQ(record.Swizzle[channel], static_cast(expectedSwizzle[channel])) + << "the applier's set_texture_params record for texture " << texture + << " carries the wrong swizzle in channel " << channel + << " (record 0x" << std::hex << record.Swizzle[channel] << ", expected 0x" + << expectedSwizzle[channel] << std::dec + << "). The record is what Espryt reads, so a wrong value here is a " + "wrong value everywhere downstream of it."; + } + } else { + EXPECT_EQ(record.DepthStencilMode, static_cast(expectedDepthStencilMode)) + << "the applier's set_texture_params record for texture " << texture + << " carries GL_DEPTH_STENCIL_TEXTURE_MODE 0x" << std::hex + << record.DepthStencilMode << ", expected 0x" << expectedDepthStencilMode + << std::dec << "."; + } + + // (c) - and it is checked BEFORE (b) is read, because (b) reads the driver and a + // reader of a failure needs to know the view question was answered on the state + // this case built rather than on anything the peek did. + bool hasSamplerView = true; + if (!PeekEsprytHasSamplerViewForTexture(static_cast(texture), + &hasSamplerView)) { + DeclineTheWhiteBoxReading( + "Espryt holds no twin for this texture, so neither the sampler-view " + "question nor the applied-value one can be asked here. On a backend other " + "than DirectGLES that is the designed state (P4a touches no DirectVulkan " + "source but MagmaPipeArms.h, D-Q)."); + return; + } + EXPECT_FALSE(hasSamplerView) + << "Espryt already holds a SAMPLER VIEW for texture " << texture + << ", which was " << whatMadeItReachable + << " and has never been bound to a sampler unit in this case. The whole claim " + "of D10 is that a texture reached this way has no view, so if one exists " + "the reading below cannot separate 'applied by resource' from 'applied " + "through the view' and this case has stopped measuring G9."; + + EsprytAppliedTextureParamsPeek applied{}; + if (!PeekEsprytAppliedTextureParams(static_cast(texture), + static_cast(target), &applied)) { + DeclineTheWhiteBoxReading( + "Espryt's applied value could not be read back from the driver (no twin, " + "no ES name yet, or a target this peek has no binding query for)."); + return; + } + + std::cout << "[ TextureParamsWithoutASamplerView ] white-box: texture " << texture + << " -> applier handle {" << record.Slot << ", " << record.Gen + << "} paramsSerial " << record.ParamsSerial << ", Espryt ES name " + << applied.BackendTextureId << ", sampler view: none, " << movedName + << " applied before any sample" << std::endl; + + if (moved == MovedParameter::Swizzle) { + for (int channel = 0; channel < 4; ++channel) { + EXPECT_EQ(applied.Swizzle[channel], static_cast(expectedSwizzle[channel])) + << "ESPRYT HAS NOT APPLIED THE SWIZZLE YET. Channel " << channel + << " of the driver texture (ES name " << applied.BackendTextureId + << ") reads 0x" << std::hex << applied.Swizzle[channel] << ", the " + << "application set 0x" << expectedSwizzle[channel] << std::dec + << ", and the applier's record already carries the right value - so " + "the record reached the server and the server has not pushed it. " + "The texture was " << whatMadeItReachable + << " and has NO sampler view (asserted above), which makes this " + "exactly the deferred-to-first-view shape G9 exists to catch: the " + "sample at the end of this case would repair it, and the " + "end-to-end assertion below would then pass on a driver that was " + "told late. That is the half no public-GL case can see."; + } + } else { + if (!applied.DepthStencilModeIsReadable) { + DeclineTheWhiteBoxReading( + "this driver would not answer glGetTexParameteriv(" + "GL_DEPTH_STENCIL_TEXTURE_MODE), so the applied aspect mode cannot be " + "read back. The record half above was still asserted."); + return; + } + EXPECT_EQ(applied.DepthStencilMode, static_cast(expectedDepthStencilMode)) + << "ESPRYT HAS NOT APPLIED THE DEPTH/STENCIL ASPECT MODE YET. The driver " + "texture (ES name " << applied.BackendTextureId << ") reads 0x" + << std::hex << applied.DepthStencilMode << ", the application set 0x" + << expectedDepthStencilMode << std::dec + << ", and the applier's record already carries the right value. The " + "texture was " << whatMadeItReachable + << " and has no sampler view, so this is D-E3's gap measured directly " + "rather than through a sample that would repair it: a driver left at " + "GL_DEPTH_COMPONENT samples the DEPTH bits where the application asked " + "for stencil."; + } + } + + // Printed, recorded and named, never silent - a lane that stopped taking the reading + // must be visible in the log. See this file's header for why it is not a GTEST_SKIP. + void DeclineTheWhiteBoxReading(const std::string& why) { + std::cout << "[ TextureParamsWithoutASamplerView ] white-box reading DECLINED: " + << why << std::endl; + RecordProperty("g9_white_box", "declined"); + RecordProperty("g9_white_box_reason", why.c_str()); + } + GLuint m_fetchProgram = 0; GLuint m_vao = 0; GLuint m_quadBuffer = 0; }; + // The swizzle SwizzleRedIntoGreen leaves behind, as GL enums: R -> ZERO, G -> ONE, + // B -> ZERO and A untouched at its GL default. Written once here because both the + // applier record and the driver read-back are compared against it. + constexpr GLint kRedIntoGreenSwizzle[4] = {GL_ZERO, GL_ONE, GL_ZERO, GL_ALPHA}; + // A texture whose aspect mode was never touched, i.e. the GL initial value - which is + // also what a zeroed MGPTextureParams::DepthStencilMode decodes to (MGPipeTypes.h). + constexpr GLint kUntouchedDepthStencilMode = GL_DEPTH_COMPONENT; + // ...and the identity swizzle, for the case whose subject is the aspect mode: the moved + // half is asserted, and the untouched half is carried so a failure prints both. + constexpr GLint kUntouchedSwizzle[4] = {GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA}; + // ------------------------------------------------------------------------------------ // 1. DRAW ATTACHMENT ONLY. Green today (SyncNeccessaryTextures' FBO list walks the draw // slot and calls SyncTextureObjectToBackend, which syncs parameters) and green after. @@ -307,6 +504,13 @@ void main() { glBindFramebuffer(GL_FRAMEBUFFER, 0); glDeleteFramebuffers(1, &fbo); + // G9's white-box reading, taken here: the texture has been a draw attachment and + // nothing else, and the sample below has not happened yet. + TakeTheWhiteBoxReadingBeforeAnySample(texture, GL_TEXTURE_2D, MovedParameter::Swizzle, + kRedIntoGreenSwizzle, kUntouchedDepthStencilMode, + "an attachment of the DRAW framebuffer and " + "nothing else"); + const Image image = SampleAndRead(m_fetchProgram, texture); EXPECT_TRUE(WholeViewportIs(image, "green", "a texture that was only ever a DRAW attachment, sampled " @@ -406,6 +610,16 @@ void main() { glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the read-attachment frame left a GL error"; + // G9's white-box reading, and this is the case it matters most for: the aspect + // mode was set while the texture was reachable ONLY as a read attachment, and the + // observation below is a sample that would repair an unsynced parameter on its way + // to reporting it. + TakeTheWhiteBoxReadingBeforeAnySample(texture, GL_TEXTURE_2D, + MovedParameter::DepthStencilMode, + kUntouchedSwizzle, GL_STENCIL_INDEX, + "an attachment of the READ framebuffer and " + "nothing else"); + // ---- the observation ---- BindDefaultFramebuffer(); glViewport(0, 0, Gl().Width(), Gl().Height()); @@ -484,6 +698,15 @@ void main() { Gl().EndFrame(); glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); + // G9's white-box reading. RGBA8 is a core image format, so no widening carrier is + // minted and the driver's swizzle is the application's own - the composition + // RecreateBackendTexture applies for a NON-core format would show up here as a + // legitimate difference and this case deliberately does not use one. + TakeTheWhiteBoxReadingBeforeAnySample(texture, GL_TEXTURE_2D, MovedParameter::Swizzle, + kRedIntoGreenSwizzle, kUntouchedDepthStencilMode, + "an image-unit binding and nothing else, across " + "a RequireImageBindableStorage re-mint"); + const Image image = SampleAndRead(m_fetchProgram, texture); EXPECT_TRUE(WholeViewportIs(image, "green", "a texture that was only ever an image-unit binding, sampled " @@ -525,6 +748,13 @@ void main() { } Gl().EndFrame(); + // G9's white-box reading, on the DESTINATION - the endpoint whose parameters moved. + TakeTheWhiteBoxReadingBeforeAnySample(destination, GL_TEXTURE_2D, + MovedParameter::Swizzle, kRedIntoGreenSwizzle, + kUntouchedDepthStencilMode, + "a glCopyImageSubData destination and nothing " + "else"); + // ...and the destination now holds the source's RED texel, which the swizzle must turn // into GREEN when it is finally sampled. const Image image = SampleAndRead(m_fetchProgram, destination);