diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 5fd335f8..e9f1893c 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -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* 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& 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 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 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* 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& 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 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(); - } - 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(MG_Pipe::MGPipeResourceTarget::Tex2D); - desc.StorageKind = static_cast(TextureStorageType::Mipmap); - desc.InternalFormat = static_cast(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(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(TextureSwizzleParam::Green); - pushed.Swizzle[1] = static_cast(TextureSwizzleParam::Green); - pushed.Swizzle[2] = static_cast(TextureSwizzleParam::Blue); - pushed.Swizzle[3] = static_cast(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(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; diff --git a/docs/ci/ci-cleanup-log.md b/docs/ci/ci-cleanup-log.md index 589c5a0b..b780712e 100644 --- a/docs/ci/ci-cleanup-log.md +++ b/docs/ci/ci-cleanup-log.md @@ -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, `.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