From 9bd6d3940365073a6f3412b1dafca8b1c42ff8bb Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 6 Sep 2026 05:32:25 -0400 Subject: [PATCH] [Test] (Pipe): pin the push-on-mutation shape - a frontend write inside a verb refreshes the pushed field, and only its value - AFrontendMutationInsideAVerbRefreshesThePushedField: fills DrawArrays, then does what UniformManager's fallback path does mid-draw (a SamplerObject filter change) and what a bind reached from inside a verb does (NoteTextureUnitTouched), and asserts the block still equals the live context for GetSamplingResolutionGeneration, GetTextureBindGeneration and GetMaxTouchedTextureUnit. - AFrontendMutationInsideAVerbDoesNotDivergeAtRead: the lane failure end to end, under the armed comparator in a forked child - the read after the mutation must complete and the log must carry no Fatal{. - TheMutationNoticeRefreshesTheValueButNotTheStamp: the notice must not restamp a field whose stamp the fill withheld (negative control B), and must not stamp a field the verb class never fills (the generation under a kQuery verb). - Falsified: with the notice's body short-circuited, the first two fail (the read-side one by SIGABRT on Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays", where=read}) and the third stays green, which is what a guard case should do. - The three names also exist as visible GTEST_SKIPs in the pull build, as every other case in this file does. --- MobileGL/MG_Test/Pipe/PipeInputsTest.cpp | 105 +++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp b/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp index 69be8437..3d2df3a1 100644 --- a/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp @@ -170,6 +170,15 @@ TEST(PipeInputsTest,BadVerifyCorruptKnobIsFatalNamingTheKnob) { TEST(PipeInputsTest,VerifyFatalOffLogsTheDivergenceAndContinues) { GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; } +TEST(PipeInputsTest,AFrontendMutationInsideAVerbRefreshesThePushedField) { + GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; +} +TEST(PipeInputsTest,TheMutationNoticeRefreshesTheValueButNotTheStamp) { + GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; +} +TEST(PipeInputsTest,AFrontendMutationInsideAVerbDoesNotDivergeAtRead) { + GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; +} #else // MOBILEGL_PIPE_PUSH @@ -515,6 +524,102 @@ TEST_F(PipeInputsTest, EveryVerbFillsItsClassAndNothingElse) { #endif } +// Push on mutation (P1 lane finding F2, MG_Pipe/PipeMutation.h). The backends write into +// frontend objects inside their own verb - Magma synthesises a fallback texture for an +// unbound sampler and overrides a unit's sampler filter (VulkanRenderer/UniformManager) - +// and every such write moves a counter the verb boundary already copied. The frontend +// mutator refreshes that field, so the block still equals the live context at every read. +// Without the notice the two reads below differ and the pushed value is the stale one. +TEST_F(PipeInputsTest, AFrontendMutationInsideAVerbRefreshesThePushedField) { + auto& ctx = *MG_State::pGLContext; + MGPipeFillForVerb(MGPipeVerb::DrawArrays); + ASSERT_EQ(gPipeInputs.GetSamplingResolutionGeneration(), ctx.GetSamplingResolutionGeneration()); + ASSERT_EQ(gPipeInputs.GetTextureBindGeneration(), ctx.GetTextureBindGeneration()); + ASSERT_EQ(gPipeInputs.GetMaxTouchedTextureUnit(), ctx.GetMaxTouchedTextureUnit()); + + // What UniformManager's fallback path does mid-draw: change a sampler object's filter, + // which bumps the context-wide sampling-resolution generation (SamplerObject.cpp). + const Uint64 samplingBefore = ctx.GetSamplingResolutionGeneration(); + auto sampler = MakeShared(0u); + sampler->SetMinFilter(sampler->GetMinFilter() == SamplerFilterMode::Nearest ? SamplerFilterMode::Linear + : SamplerFilterMode::Nearest); + ASSERT_NE(ctx.GetSamplingResolutionGeneration(), samplingBefore) << "the mutation did not move the counter"; + EXPECT_EQ(gPipeInputs.GetSamplingResolutionGeneration(), ctx.GetSamplingResolutionGeneration()); + + // And a bind reached from inside a verb moves both the bind generation and the + // high-water mark of touched units (TextureState::NoteUnitTouched). + const Uint64 bindBefore = ctx.GetTextureBindGeneration(); + const Int unit = ctx.GetMaxTouchedTextureUnit() + 1; + ctx.NoteTextureUnitTouched(unit); + ASSERT_NE(ctx.GetTextureBindGeneration(), bindBefore) << "the bind did not move the counter"; + ASSERT_EQ(ctx.GetMaxTouchedTextureUnit(), unit); + EXPECT_EQ(gPipeInputs.GetTextureBindGeneration(), ctx.GetTextureBindGeneration()); + EXPECT_EQ(gPipeInputs.GetMaxTouchedTextureUnit(), ctx.GetMaxTouchedTextureUnit()); +} + +// The notice refreshes the VALUE and never the stamp: a field this verb withheld the stamp +// of (negative control B) must stay stale, and a field outside the verb class's mask must +// stay unfilled rather than be healed by an unrelated frontend write. +TEST_F(PipeInputsTest, TheMutationNoticeRefreshesTheValueButNotTheStamp) { +#if !MOBILEGL_PIPE_POISON + GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)"; +#else + auto& ctx = *MG_State::pGLContext; + MGPipeSetPoisonOmission("DrawArrays", "GetSamplingResolutionGeneration"); + MGPipeFillForVerb(MGPipeVerb::DrawArrays); + ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration)); + ctx.BumpSamplingResolutionGeneration(); + EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration)) + << "the notice restamped a field whose stamp the fill withheld"; + + // FenceSync is a kQuery verb: its mask holds no texture field at all, so the notice must + // leave the generation unfilled and a read of it Fatal{UnmigratedPipeInput}. + MGPipeSetPoisonOmission(nullptr, nullptr); + MGPipeFillForVerb(MGPipeVerb::FenceSync); + ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration)); + ctx.BumpSamplingResolutionGeneration(); + EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration)) + << "the notice stamped a field the verb class never fills"; +#endif +} + +// The lane failure itself (six DirectVulkan.Verify.UnboundImageDescriptorScenario entries, +// two SampledSetStalenessScenario, two retrace cases): a frontend write made inside a draw +// used to make the very next read of the pushed block differ from the live context. The +// child reproduces it end to end under the armed comparator and must survive; without the +// notice it dies of Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays", +// where=read}. +TEST_F(PipeInputsTest, AFrontendMutationInsideAVerbDoesNotDivergeAtRead) { +#if !MOBILEGL_PIPE_VERIFY + GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)"; +#elif !MGTEST_HAVE_FORK + GTEST_SKIP() << "no fork() on this platform"; +#else + ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH"; + const ChildResult r = RunInChild([] { + MG_Config::Features.PipeVerify = true; + MGPipeFillForVerb(MGPipeVerb::DrawArrays); + (void)gPipeInputs.GetSamplingResolutionGeneration(); // boundary == live: completes + auto& ctx = *MG_State::pGLContext; + const Uint64 before = ctx.GetSamplingResolutionGeneration(); + auto sampler = MakeShared(0u); + sampler->SetMinFilter(sampler->GetMinFilter() == SamplerFilterMode::Nearest ? SamplerFilterMode::Linear + : SamplerFilterMode::Nearest); + if (ctx.GetSamplingResolutionGeneration() == before) { + ::_exit(7); // the mutation did not take: the case would pass for the wrong reason + } + (void)gPipeInputs.GetSamplingResolutionGeneration(); // Fatal{PipeVerifyDiffer} without the notice + ctx.NoteTextureUnitTouched(ctx.GetMaxTouchedTextureUnit() + 1); + (void)gPipeInputs.GetTextureBindGeneration(); + (void)gPipeInputs.GetMaxTouchedTextureUnit(); + }); + ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_NE(r.Log.find("MGPipe: verify armed"), std::string::npos) << r.Log; + EXPECT_EQ(r.Log.find("Fatal{"), std::string::npos) << r.Log; + EXPECT_EQ(r.Log.find("PipeVerifyDiffer"), std::string::npos) << r.Log; +#endif +} + #endif // MOBILEGL_PIPE_PUSH int main(int argc, char** argv) {