diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index fca8f395..034f3612 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -979,6 +979,22 @@ namespace MobileGL::MG_Backend::DirectGLES { struct ResolvedDrawBuffers { struct Entry { MG_State::GLState::BufferObject* frontend = nullptr; + // A RAW TWIN POINTER, AND IT MAY DANGLE - the invariant that makes that safe + // is stated here rather than left in the two callers (espryt-v3 ยง8, m8). + // + // Nothing tells this memo when a twin dies: on the handle arm a + // resource_destroy takes the twin out of the slot table (ReleaseByHandle) + // while this entry still holds its address, and on the legacy arm the same + // is true of the registry's own release. So the rule is: THIS POINTER IS + // ONLY EVER DEREFERENCED AFTER THE ENTRY'S IDENTITY HAS BEEN RE-RESOLVED IN + // THE SAME PASS - FindByHandle(handle) on the handle arm, the frontend + // identity compare on the legacy one - and a miss re-resolves through + // EnsureBufferResource rather than trusting what is stored here. Both + // consumers do that today; a third one that read `resource` straight out of + // a "valid" memo would be reading freed memory, and no compare in this + // struct would catch it. The pointer stays raw because the alternative - + // owning a reference from a per-draw memo - is what keeps a dead driver + // buffer alive, which is the leak class P2's death notice exists to remove. BufferImpl::GLESBufferResource* resource = nullptr; Uint8 attribIndex = 0; #if MOBILEGL_PIPE_PUSH diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index d63d737b..cf7602b1 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -1587,10 +1587,12 @@ namespace MobileGL::MG_Pipe { } // P3a's vertex segment, in the order the design fixes: vertex elements, then the - // vertex buffers that fill them, then the index binding. All three still resolve to - // false today - their dirty bits map to no subsystem until the tracker's arms land - - // and all three emitters are stubs; the call sites are here so the commit that gives - // them bodies does not also have to edit the validate point. + // vertex buffers that fill them, then the index binding. All three are LIVE now (m1): + // bits 5 / 9 / 10 map onto kMGPipeSubsystemVertexInput in Tracker.h:145-148 and all + // three emitters have bodies, so `wants()` answers true whenever bit 8 is in the push + // mask - which the phase default 0x1ff sets, on every backend. The sentence that used + // to stand here ("all three still resolve to false today - their dirty bits map to no + // subsystem") was the contract commit's and stopped being true when the client landed. if (wants(MGPipeDirty::NewVertexElements)) { payloadBytes += EmitVertexElements(*ctx); } diff --git a/scripts/p3a_vertex_input_negative_control.sh b/scripts/p3a_vertex_input_negative_control.sh index 4688da45..66689ba5 100644 --- a/scripts/p3a_vertex_input_negative_control.sh +++ b/scripts/p3a_vertex_input_negative_control.sh @@ -171,7 +171,13 @@ say "$TEST_NAME is green before the patch" # --- 2. stop copying IsBgra ------------------------------------------------------------------ cp -f "$HEADER" "$BACKUP" || exit 2 +# m3: INT and TERM as well as EXIT. A Ctrl-C during the rebuild used to leave the patched header +# in the tree - bash runs no EXIT trap for an uncaught SIGINT - and the next thing that reader +# does is build, from a hard-zeroed field, with nothing saying so. The two extra traps repair and +# then re-raise with the default disposition, so the exit status still reports the signal. trap 'repair' EXIT +trap 'repair; trap - INT; kill -INT $$' INT +trap 'repair; trap - TERM; kill -TERM $$' TERM # Armed BEFORE the patcher runs, not after: a python that died half-way through the write must # still be repaired. The cost of arming it early is one unnecessary rebuild in the case where the # patcher matched nothing and the file is byte-identical (cp refreshes its mtime). @@ -216,9 +222,18 @@ say "running $TEST_NAME against the dropped field" if ctest --test-dir "$BUILD_DIR" -R "$TEST_NAME" --no-tests=error --output-on-failure \ > "$LOG_DIR/ctest-after.log" 2>&1; then VERDICT=did-not-trip -elif grep -q "$FIELD" "$LOG_DIR/ctest-after.log"; then +elif grep -qE "^.*(Failure|error:|Expected).*$FIELD|$FIELD.*(Failure|Which is|Expected)" \ + "$LOG_DIR/ctest-after.log"; then # A red is not yet a pass: a suite that had started failing for an unrelated reason satisfies the # first half of the claim and none of the second. + # + # m3: matched against the FAILING ASSERTION's own lines rather than against the whole ctest log. + # A bare `grep -q IsBgra` over the log was exact today only because the field name happens to + # appear exactly once in the tree, inside the case that fails; a future case NAMED after the + # field, a skip reason quoting it, or a compiler note echoed into the log would all have made + # "tripped" mean "the string exists somewhere". The alternation keeps both orders because gtest + # prints the field on the `Failure`/`Expected` line for an EXPECT_EQ and on the following + # `Which is` line for a streamed message. VERDICT=tripped else VERDICT=wrong-reason