[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

This commit is contained in:
Swung0x48
2026-09-08 19:08:12 -04:00
committed by rereview
parent f9c0e7ec43
commit 972dd811d7
4 changed files with 534 additions and 5 deletions
@@ -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
@@ -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 <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/MGPipeTypes.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/DirectGLES.h>
#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<MobileGL::Uint>(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<int>(MobileGL::MG_Util::ConvertTextureSwizzleParamToGLEnum(
static_cast<MobileGL::TextureSwizzleParam>(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<MobileGL::Uint32>(glTextureName)) continue;
out->Slot = static_cast<unsigned>(slot);
out->Gen = static_cast<unsigned>(record.Gen);
out->ParamsSerial = static_cast<unsigned long long>(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<GLenum>(bindingQuery), &previousBinding);
MGB::g_GLESFuncs.glBindTexture(static_cast<GLenum>(glTarget), backendId);
out->BackendTextureId = static_cast<unsigned>(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<GLenum>(glTarget),
kSwizzlePnames[channel], &value);
out->Swizzle[channel] = static_cast<int>(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<GLenum>(glTarget),
GL_DEPTH_STENCIL_TEXTURE_MODE, &mode);
out->DepthStencilModeIsReadable = MGB::g_GLESFuncs.glGetError() == GL_NO_ERROR;
out->DepthStencilMode = static_cast<int>(mode);
MGB::g_GLESFuncs.glBindTexture(static_cast<GLenum>(glTarget),
static_cast<GLuint>(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<unsigned long long>(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
@@ -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
@@ -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 <vector>
#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<unsigned>(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<int>(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<int>(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<unsigned>(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<unsigned>(texture),
static_cast<unsigned>(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<int>(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<int>(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);