[Test] (P13 W3b): retire the two P2-mask texture-sync probes and log every W3b removal

This commit is contained in:
2026-10-06 04:04:49 -04:00
parent 06362251b0
commit 640c73243d
2 changed files with 8 additions and 304 deletions
+4 -304
View File
@@ -3040,144 +3040,11 @@ TEST(UnorderedMapSanity, ErasingTheOnlyElementReturnsEnd) {
EXPECT_TRUE(map.empty());
}
namespace {
// Records what the per-unit texture sync actually pushed at the driver: which backend
// texture id was current when each glTexImage2D landed, and the shape it was given.
struct TexSpecCall {
GLuint texture;
GLsizei width;
GLsizei height;
};
MobileGL::Vector<TexSpecCall>* g_texSpecCalls = nullptr;
GLuint g_texSpecBoundTexture = 0;
void TS_BindTexture(GLenum, GLuint texture) { g_texSpecBoundTexture = texture; }
void TS_ActiveTexture(GLenum) {}
void TS_TexParameteri(GLenum, GLenum, GLint) {}
void TS_TexParameterf(GLenum, GLenum, GLfloat) {}
void TS_TexParameterfv(GLenum, GLenum, const GLfloat*) {}
void TS_PixelStorei(GLenum, GLint) {}
void TS_BindBuffer(GLenum, GLuint) {}
void TS_TexImage2D(GLenum, GLint level, GLint, GLsizei width, GLsizei height, GLint, GLenum, GLenum,
const void*) {
if (g_texSpecCalls && level == 0) {
g_texSpecCalls->push_back({g_texSpecBoundTexture, width, height});
}
}
// Clears the recording hook even when a gtest assertion unwinds the test body.
struct ScopedTexSpecRecording {
explicit ScopedTexSpecRecording(MobileGL::Vector<TexSpecCall>& sink) {
g_texSpecCalls = &sink;
g_texSpecBoundTexture = 0;
}
~ScopedTexSpecRecording() { g_texSpecCalls = nullptr; }
ScopedTexSpecRecording(const ScopedTexSpecRecording&) = delete;
ScopedTexSpecRecording& operator=(const ScopedTexSpecRecording&) = delete;
};
// Gives `name` a complete single-level 2D image of the requested size without going through
// the frontend upload path (the mock table below wires only the state-pushing entry points).
MobileGL::SharedPtr<MobileGL::MG_State::GLState::ITextureObject> MakeComplete2DTexture(GLuint name,
MobileGL::Int size) {
using namespace MobileGL;
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, name);
auto object = MG_State::pGLContext->GetTextureUnitObject(0)
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject();
object->SetInternalFormat(TextureInternalFormat::RGBA8);
MG_State::GLState::AsMipmapTexture(object.get())
->AllocateStorage(TextureUploadTarget::Texture2D, 0, {{size, size, 1}, 4});
return object;
}
} // namespace
// The per-unit texture sync memo BORROWS the binding slot: an entry holds a pointer to the
// slot's shared_ptr plus the backend twin of whatever was in it when the entry was built. Its
// keys (context id, bind-generation epoch, high-water mark, sampling generation) are the primary
// guard, but they are all derived state - so the memo also has to survive a slot swap that never
// reached them.
// P5e (tx2), CONTRACT-P5E §4.3: THE HANDLE ARM'S SIBLING OF UnitMemoRefusesToDriveATwinFromAnotherTexture,
// WITH ABA AS THE HAZARD INSTEAD OF A SILENT SLOT SWAP. (P13 W3b retired that case: it ran only at
// the P2 mask, whose legacy unit arm is gone now that subsystem bits 0-13 are fixed on.)
//
// It did not. The DSA by-name emulation swapped a slot silently, every key still matched, and
// the replay drove texture A's backend twin from texture B's frontend object: A's backend
// storage was re-specified with B's shape, destroying anything A only ever had on the GPU. On
// Espryt + Iris/BSL that blanked Minecraft's 16x16 lightmap the moment a 2048x2048 shadow map
// was uploaded through a by-name call, and since the text shader multiplies by the lightmap,
// `if (color.a < 0.1) discard` then threw away every glyph in the process - HUD, menus and the
// vanilla title screen alike.
TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) {
using namespace MobileGL;
ScopedDirectGLESTextureBindings scoped; // fresh GLContext + registry + binding caches
Vector<TexSpecCall> specs;
ScopedTexSpecRecording recording(specs);
auto functions = MG_Backend::DirectGLES::g_GLESFuncs;
functions.glBindTexture = TS_BindTexture;
functions.glActiveTexture = TS_ActiveTexture;
functions.glTexImage2D = TS_TexImage2D;
functions.glTexParameteri = TS_TexParameteri;
functions.glTexParameterf = TS_TexParameterf;
functions.glTexParameterfv = TS_TexParameterfv;
functions.glPixelStorei = TS_PixelStorei;
functions.glBindBuffer = TS_BindBuffer;
MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
GLuint names[2] = {};
MG_Impl::GLImpl::GenTextures(2, names);
// `foreign` stands in for the shadow map, `resident` for the lightmap. Both are fully
// specified BEFORE the first sync so that nothing between the two syncs can move the
// sampling-resolution generation and invalidate the memo for an unrelated reason.
const auto foreign = MakeComplete2DTexture(names[1], 32);
const auto resident = MakeComplete2DTexture(names[0], 16);
ASSERT_NE(foreign, nullptr);
ASSERT_NE(resident, nullptr);
// First sync: builds the memo with unit 0 -> `resident`, and gives `resident`'s twin its
// 16x16 backend storage. The unit walk is the per-draw one, so both syncs below stand in a
// draw - the fill is what a real glDraw* would have done before reaching this helper.
MG_Test::ScopedPipeVerb draw(MG_Pipe::MGPipeVerb::DrawArrays);
MG_Backend::DirectGLES::TextureImpl::SyncNeccessaryTextures();
auto* residentSlot = MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects.Find(resident.get());
ASSERT_NE(residentSlot, nullptr);
ASSERT_NE(*residentSlot, nullptr);
const GLuint residentBackendId = (*residentSlot)->GetBackendTextureId();
ASSERT_NE(residentBackendId, 0u);
ASSERT_FALSE(specs.empty());
EXPECT_EQ(specs.back().texture, residentBackendId);
EXPECT_EQ(specs.back().width, 16);
// The hazard, reproduced at the state level: put `foreign` on the slot the memo borrows
// WITHOUT telling the binding accounting, exactly as the by-name emulation used to.
MG_State::pGLContext->GetTextureUnitObject(0).GetBindingSlot(TextureTarget::Texture2D).Bind(foreign);
const SizeT specsBeforeReplay = specs.size();
// The second draw. Its fill re-reads the memo's keys off the live context, which is the
// premise being tested: the silent slot swap moved none of them.
draw.Renew();
MG_Backend::DirectGLES::TextureImpl::SyncNeccessaryTextures();
// `foreign` must have been synced through its OWN twin...
auto* foreignSlot = MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects.Find(foreign.get());
ASSERT_NE(foreignSlot, nullptr);
ASSERT_NE(*foreignSlot, nullptr);
const GLuint foreignBackendId = (*foreignSlot)->GetBackendTextureId();
EXPECT_NE(foreignBackendId, residentBackendId);
// ...and above all, nothing may have re-specified the RESIDENT texture's backend storage.
// That single call is what destroyed the lightmap.
for (SizeT i = specsBeforeReplay; i < specs.size(); ++i) {
EXPECT_NE(specs[i].texture, residentBackendId)
<< "the stale memo entry re-specified the resident texture's backend storage with "
<< specs[i].width << "x" << specs[i].height;
}
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
}
// P5e (tx2), CONTRACT-P5E §4.3: THE HANDLE ARM'S SIBLING OF THE CASE ABOVE, WITH ABA AS THE
// HAZARD INSTEAD OF A SILENT SLOT SWAP.
//
// The case above pins what the BORROWED-SLOT work list needs: a pointer compare per entry,
// That case pinned what the BORROWED-SLOT work list needs: a pointer compare per entry,
// because its key is derived state and a slot swap that never reached the key would drive
// texture A's twin from texture B. The by-handle work list carries no such pointer and no
// PairingsIntact, and this is the argument for why it does not have to:
@@ -3256,173 +3123,6 @@ TEST(DirectGLESTextureSync, ARecycledTextureSlotReMintsTheTwinOnTheHandleArm) {
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Texture, second);
}
namespace {
// What glTexParameteri actually reached the driver, and which backend texture was bound
// when it did. The G9 probe below is a WHITE-BOX assertion (ID-19): the parameter push is
// not observable through public GL without creating the very sampler view whose absence is
// the subject, so the observation is taken at the driver boundary instead.
struct TexParamCall {
GLuint texture;
GLenum pname;
GLint value;
};
MobileGL::Vector<TexParamCall>* g_texParamCalls = nullptr;
GLuint g_texParamBoundTexture = 0;
void TP_BindTexture(GLenum, GLuint texture) { g_texParamBoundTexture = texture; }
void TP_ActiveTexture(GLenum) {}
void TP_TexParameteri(GLenum, GLenum pname, GLint value) {
if (g_texParamCalls) {
g_texParamCalls->push_back({g_texParamBoundTexture, pname, value});
}
}
void TP_TexParameterf(GLenum, GLenum, GLfloat) {}
void TP_TexParameterfv(GLenum, GLenum, const GLfloat*) {}
void TP_PixelStorei(GLenum, GLint) {}
void TP_BindBuffer(GLenum, GLuint) {}
struct ScopedTexParamRecording {
explicit ScopedTexParamRecording(MobileGL::Vector<TexParamCall>& sink) {
g_texParamCalls = &sink;
g_texParamBoundTexture = 0;
}
~ScopedTexParamRecording() { g_texParamCalls = nullptr; }
ScopedTexParamRecording(const ScopedTexParamRecording&) = delete;
ScopedTexParamRecording& operator=(const ScopedTexParamRecording&) = delete;
};
} // namespace
// G9 AS A WHITE-BOX ASSERTION (gates review R1, ID-19), AND IT COVERS THE HALF THE PUBLIC-GL
// SCENARIO CANNOT.
//
// TextureParamsWithoutASamplerViewScenario catches "the parameters were EMITTED and marked
// synced but never applied": its observation is a sample, the sample creates the sampler view,
// and IsDrawSyncClean then skips the sync. What it cannot catch is a backend that merely DEFERS
// the apply to the first sampler view - the observation creates that view, the parameters land
// at that moment, and the case is green. Here the reading is taken while the texture is still
// attachment-only: nothing is ever bound to a unit, no sampler view is minted, and the
// assertion is that the parameter reached the driver ANYWAY.
//
// RED ON THE PRE-P4a BEHAVIOUR: with set_texture_params addressed by resource, this twin's
// parameter push no longer needs anything to be bound. A backend that reinstated the deferral -
// resolving the params through a sampler view, or gating the push on a unit binding - leaves
// the recording empty and this case fails, which is exactly the regression the scenario's
// self-repair hides.
//
// WHAT IT DOES NOT COVER, AND THE NEXT READER MUST NOT OVER-TRUST IT (review N-9): the probe
// drives SyncTextureParamsToBackend DIRECTLY, so the only deferral shape it can see is one
// INSIDE that function. A regression that gates the CALL on a sampler view existing - in
// SyncNeccessaryTextures, or in package E's per-unit walk - leaves this case green. That
// caller-level half is a scenario's job and the scenario is package F's (G9's scenario half,
// gates review R1); this is the backend-side probe R1 asked for and nothing wider.
TEST(DirectGLESTextureSync, AnAttachmentOnlyTexturesParametersReachTheDriverWithNoSamplerView) {
using namespace MobileGL;
ScopedDirectGLESTextureBindings scoped; // fresh GLContext + registry + binding caches
Vector<TexParamCall> params;
ScopedTexParamRecording recording(params);
auto functions = MG_Backend::DirectGLES::g_GLESFuncs;
functions.glBindTexture = TP_BindTexture;
functions.glActiveTexture = TP_ActiveTexture;
functions.glTexParameteri = TP_TexParameteri;
functions.glTexParameterf = TP_TexParameterf;
functions.glTexParameterfv = TP_TexParameterfv;
functions.glPixelStorei = TP_PixelStorei;
functions.glBindBuffer = TP_BindBuffer;
MG_Backend::DirectGLES::SetGLESFuncsTable(functions);
GLuint name = 0;
MG_Impl::GLImpl::GenTextures(1, &name);
const auto texture = MakeComplete2DTexture(name, 8);
ASSERT_NE(texture, nullptr);
// ATTACHMENT-ONLY from here on: the unit that specified it is released, so nothing in this
// test ever binds this texture for sampling and nothing mints a sampler view for it.
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
// The parameter that has to travel. Red -> Green is not any texture's default, so a driver
// that never hears about it is distinguishable from one that does.
texture->SetSwizzleParam(TextureSwizzleParam::Red, TextureSwizzleParam::Green);
auto& registry = MG_Backend::DirectGLES::TextureImpl::g_backendTextureObjects;
auto& twin = registry.GetOrCreate(texture);
if (!twin) {
twin = MakeShared<MG_Backend::DirectGLES::TextureImpl::BackendTextureObject>();
}
ASSERT_NE(twin, nullptr);
const MG_Pipe::MGPipeHandle res = registry.HandleOf(texture.get());
MG_Pipe::MGPipeHandle builtinSampler = MG_Pipe::kMGPipeNullHandle;
if (MG_Backend::DirectGLES::TextureResourceSubsystemEnabled()) {
// The handle arm reads the applier, so the applier is what this probe writes - which is
// also what makes it a white-box test rather than a scenario: no client emitter exists
// on this tree, and the point is Espryt's behaviour given a record, not the client's.
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(res));
MG_Pipe::MGPResourceDesc desc{};
desc.Resource = res;
desc.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
desc.StorageKind = static_cast<Uint8>(TextureStorageType::Mipmap);
desc.InternalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8);
desc.Width = 8;
desc.Height = 8;
desc.Depth = 1;
desc.ArrayLayers = 1;
desc.Levels = 1;
EXPECT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc));
// Every ITextureObject owns a sampler object, so the built-in sampler CSO is not
// optional (a null one is Fatal{ProtocolCorruption} in the applier). It is a SAMPLER
// CSO and not a sampler VIEW - the distinction this case exists for.
auto samplerOwner = MakeShared<MG_State::GLState::SamplerObject>(0u);
builtinSampler = MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::SamplerCso,
samplerOwner->GetLifetimeId());
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(builtinSampler));
MG_Pipe::MGPSamplerDesc samplerDesc{};
samplerDesc.Cso = builtinSampler;
SamplerParameters samplerParams{};
MG_Pipe::MGPipeApplyCreateSamplerState(samplerDesc, &samplerParams);
MG_Pipe::MGPTextureParams pushed{};
pushed.Res = res;
pushed.BuiltinSampler = builtinSampler;
pushed.MaxLevel = 0;
pushed.Swizzle[0] = static_cast<Uint8>(TextureSwizzleParam::Green);
pushed.Swizzle[1] = static_cast<Uint8>(TextureSwizzleParam::Green);
pushed.Swizzle[2] = static_cast<Uint8>(TextureSwizzleParam::Blue);
pushed.Swizzle[3] = static_cast<Uint8>(TextureSwizzleParam::Alpha);
MG_Pipe::MGPipeApplySetTextureParams(pushed);
// THE OBSERVATION IS TAKEN WHILE THE TEXTURE IS STILL ATTACHMENT-ONLY: no sampler view
// record exists, and no sampler-view twin does either. This is the assertion the public
// scenario cannot make, because making it there would create the view.
EXPECT_EQ(MG_Backend::DirectGLES::SamplerViewImpl::g_backendSamplerViews.FindByHandle(
MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::SamplerViewCso,
texture->GetLifetimeId())),
nullptr)
<< "a sampler view was minted for a texture nothing sampled";
}
const SizeT before = params.size();
twin->SyncTextureParamsToBackend(texture);
Bool sawSwizzleR = false;
for (SizeT i = before; i < params.size(); ++i) {
if (params[i].pname == GL_TEXTURE_SWIZZLE_R) {
sawSwizzleR = true;
EXPECT_EQ(params[i].value, static_cast<GLint>(GL_GREEN))
<< "the swizzle reached the driver with the wrong value";
}
}
EXPECT_TRUE(sawSwizzleR)
<< "an attachment-only texture's GL_TEXTURE_SWIZZLE_R never reached the driver: the "
"parameter push is gated on something being bound, which is the deferral G9's public "
"scenario cannot observe";
if (!MG_Pipe::MGPipeHandleIsNull(builtinSampler)) {
MG_Pipe::MGPipeApplyDeleteSamplerState(MG_Pipe::MGPHandleOnly{builtinSampler});
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerCso, builtinSampler);
}
}
TEST(DirectVulkanSanity, GraphicsSamplerFeedbackOnlyAliasesWritableOverlappingMip) {
using MobileGL::MG_Backend::DirectVulkan::UniformManager;
+4
View File
@@ -13,6 +13,10 @@ them wrong.
|---|---|---|---|---|
| 2026-10-06 | `build explicit pull runtime control` (the pull `libMobileGL.so`, `-DMOBILEGL_PIPE_PUSH=OFF`) | Test / `build-linux-monolith-control` | P13 W2 retires the pull build: CMake defaults to push and nothing ships pull (ID-P13-1) | the same job builds the push monolith without MG_Remote (the FCL shape); its `runtime_mode_proof --mode monolith` is the zero-MG_Remote-symbol check |
| 2026-10-06 | `Negative control (pull library)` | Test / `retrace-split` (every leg) | its library was the pull build above | `Negative control (library without a transport)`: same script (renamed `retrace_transport_control.sh`) and evidence sentence, with the push library without MG_Remote; W1 ran both side by side on every leg |
| 2026-10-06 | `DirectGLES.HandleRecycle.Legacy.*`, `DirectVulkan.HandleRecycle.Legacy.*`, `DirectVulkan.HandleRecycle.AbaControl.*` | Test / integration-gpu (`integration-monolith-control`) | they ran `MOBILEGL_PIPE_PUSH=0`, the pre-handle arm, which P13 W3b removed with `MOBILEGL_PIPE_LEGACY_MEMOS` (ID-P13-3) | `HandleRecycle.Handles.*` and `DirectVulkan.HandleRecycle.AbaControlHandles.*` (the ABA control against the handle arm that ships) stay |
| 2026-10-06 | `DirectGLES.ResourceSubsystemControl.Off.*`, `DirectGLES.ResourceSubsystemOff.*`, `DirectGLES.ObjectSubsystemControl.{Off,Refused,RefusedTexture}.*` | Test / integration-gpu | they cleared subsystem bits (masks 0x7f / 0x1ff / 0x9ff / 0x5ff) to run legacy arms; since W3b bits 0-13 are fixed on (ID-P13-3) | one named-refusal control per lane, `<lane>.RefusedAtStartup`: the same scenario and mask must go red with `Fatal{PipeSubsystemsFixedOn}` naming that mask (`Harness/RefusedMaskControl.cmake`); the `On` lanes now run 0x3fff |
| 2026-10-06 | `SanityTest`: `DirectGLESSlotTable.{AnArmlessKnobCombinationStopsInsteadOfSkippingTheLane, TheArmlessCasesLeaveTheLogPathAndTheConfigAsTheyFoundThem, EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping}` | Test / unit | they pinned the `MOBILEGL_PIPE_LEGACY_MEMOS=0` + cleared-bit knob pair; the knob is gone and the mask is refused at startup | the RefusedAtStartup controls above |
| 2026-10-06 | `SanityTest`: `DirectGLESTextureSync.{UnitMemoRefusesToDriveATwinFromAnotherTexture, AnAttachmentOnlyTexturesParametersReachTheDriverWithNoSamplerView}` | Test / unit | white-box probes that only ever ran at the P2 mask (0x7f): the borrowed-slot unit memo and the pre-handle texture-parameter push; both arms are gone with the legacy memos, and at the shipping mask the fixture cannot produce the applier records the handle arm reads | `DirectGLESTextureSync`'s handle-arm ABA sibling (P5e tx2) and `TextureParamsWithoutASamplerViewScenario` (G9, every lane) |
## Kept after investigation