diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 6537838b..e298c8f1 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -338,6 +338,13 @@ namespace MobileGL::MG_Pipe { PipeInputs g_snapshot{}; // the second arm PipeInputs g_readScratch{}; // where the compare-at-read re-read lands + // The read hook arms at the first fill (ArmVerify below), so it cannot see a read + // made before that. That window is covered by the poison instead: MGP_INPUT_CHECK + // precedes MGP_INPUT_VERIFY_READ in every accessor and a stamp of 0 is never fresh, + // so such a read is Fatal{UnmigratedPipeInput, "@"} before the hook + // could matter - which holds only while a verify build always carries the poison. + static_assert(MOBILEGL_PIPE_POISON, "the compare-at-read hook relies on the poison for reads before the first fill"); + struct VerifyState { Bool Parsed = false; Bool Enabled = false; @@ -515,7 +522,9 @@ namespace MobileGL::MG_Pipe { #endif #if MOBILEGL_PIPE_POISON MGPipeFilledState& filled = MGPipeFillAccess::Filled(inputs); - // Starts at 1, so FilledGen == 0 means "never filled". + // Starts at 1: FilledGen == 0 is "never filled", and MGPipeInputFieldIsFresh refuses + // it on both branches, so a read before this first bump is + // Fatal{UnmigratedPipeInput, "@"} rather than default storage. ++filled.CurrentVerbSerial; #endif MGPipeFillAccess::SetVerb(inputs, verb); diff --git a/MobileGL/MG_Pipe/generated/PipeFilled.inc b/MobileGL/MG_Pipe/generated/PipeFilled.inc index 91e82514..4c485cfd 100644 --- a/MobileGL/MG_Pipe/generated/PipeFilled.inc +++ b/MobileGL/MG_Pipe/generated/PipeFilled.inc @@ -310,8 +310,12 @@ struct MGPipeFilledState { std::abort(); } +// FilledGen == 0 is "never filled" on BOTH branches: before the first MGPipeFillForVerb the +// serial is 0 as well, and a read in that window is the poison's "@" case +// (P1 brief D6), never a fresh read of default-constructed storage. inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) { const SizeT index = static_cast(field); - return kMGPipeInputFieldSticky[index] ? state.FilledGen[index] != 0 - : state.FilledGen[index] == state.CurrentVerbSerial; + const Uint64 gen = state.FilledGen[index]; + if (gen == 0) return false; + return kMGPipeInputFieldSticky[index] || gen == state.CurrentVerbSerial; } diff --git a/scripts/gen_pipe.py b/scripts/gen_pipe.py index a26d1ad9..7ae0fe56 100644 --- a/scripts/gen_pipe.py +++ b/scripts/gen_pipe.py @@ -807,10 +807,14 @@ def gen_filled(accessors, calls, sticky): std::abort(); } +// FilledGen == 0 is "never filled" on BOTH branches: before the first MGPipeFillForVerb the +// serial is 0 as well, and a read in that window is the poison's "@" case +// (P1 brief D6), never a fresh read of default-constructed storage. inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) { const SizeT index = static_cast(field); - return kMGPipeInputFieldSticky[index] ? state.FilledGen[index] != 0 - : state.FilledGen[index] == state.CurrentVerbSerial; + const Uint64 gen = state.FilledGen[index]; + if (gen == 0) return false; + return kMGPipeInputFieldSticky[index] || gen == state.CurrentVerbSerial; }""") return "\n".join(out) + "\n"