fix(gpu): positive family gates, a probe that reads accepted state, and a third split doc

**`Unsupported` WAS ACCEPTING LEGACY FRAMES.** I gated the legacy arm
on `!= Mapped`, and `Unsupported` is neither --- so a session below
`PANEL_MIN_VERSION` accepted a band it never negotiated. The
`carries_panel()` check I had in mind guards
`next_geometry_declaration`, a different seam entirely. Both present
arms gate on their POSITIVE family now, which is the shape that cannot
grow this hole again when a fourth family appears.

**AND THE PROBE MEASURED THE PAYLOAD, NOT THE BAND.** It recorded panel
facts before `apply_attach_message` ruled on the message, so once one
valid frame had landed, a REJECTED frame --- wrong family, invalid,
stale generation --- still supplied the expected text while the
retained old frame supplied the rendering. The probe would report the
band showing something it does not show, which is a false positive in
the one place that exists to tell us the band is real. (The false
NEGATIVE, observing only the legacy family, was the previous commit.)

Facts now come from `state.panel.presented()` after the apply, and
`panel_frames` counts only when the retained frame's identity actually
moved: a duplicate or a refusal leaves the band exactly as it was, and
counting either would say the daemon is painting when it is not.

**Third rustdoc split in this slice**, same mechanism each time ---
`screen_size`, `peer_may_send_panel_events`, now
`send_panel_pointer`. I insert a function at what reads as a gap
between declarations, when the lines above it are the NEXT function's
documentation. The check is to look UP from the insertion point, not
just down, and I will apply it rather than keep reporting the same
correction.

Verified by exit status: clippy 0, `-p pmacs-gpu` 0 (271 passed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
Levi Neuwirth 2026-08-15 20:18:12 +02:00
parent 7e28eb8936
commit 4ecf09bf64
No known key found for this signature in database
2 changed files with 57 additions and 35 deletions

View File

@ -1098,14 +1098,6 @@ impl AttachClient {
}) })
} }
/// Send a `FrontendEvent::PanelPointer` (Q#BP16): a gesture
/// hit-tested locally to a panel CELL. Callers gate on
/// [`Self::session_protocol_version`] `>= 21`.
///
/// `buffer_id` and `panel_epoch` close different holes and neither
/// subsumes the other — the first catches an A→B buffer replacement,
/// the second a close/hide/reopen of the *same* buffer — so both are
/// carried rather than one being derived from the other.
/// §5b — the MAPPED gesture, echoing the generation of the frame /// §5b — the MAPPED gesture, echoing the generation of the frame
/// this frontend is displaying. /// this frontend is displaying.
/// ///
@ -1136,6 +1128,14 @@ impl AttachClient {
}) })
} }
/// Send a `FrontendEvent::PanelPointer` (Q#BP16): a gesture
/// hit-tested locally to a panel CELL. Callers gate on
/// [`Self::session_protocol_version`] `>= 21`.
///
/// `buffer_id` and `panel_epoch` close different holes and neither
/// subsumes the other — the first catches an A→B buffer replacement,
/// the second a close/hide/reopen of the *same* buffer — so both are
/// carried rather than one being derived from the other.
pub fn send_panel_pointer( pub fn send_panel_pointer(
&self, &self,
geometry_epoch: u64, geometry_epoch: u64,

View File

@ -849,33 +849,50 @@ fn run_headless_probe(socket: &Path, report: &Path) -> i32 {
facts.last_frame_text = frame_probe_text(frame); facts.last_frame_text = frame_probe_text(frame);
facts.last_title.clone_from(&frame.title); facts.last_title.clone_from(&frame.title);
} }
match msg.as_ref() { // §5b — a panel message is only NOTED here; its facts
// §5b — BOTH families. Observing only the legacy // are read from the retained state AFTER
// one would let mapped production work perfectly // `apply_attach_message` has ruled on it.
// while the live probe reported no panel at all, //
// which is a false negative in the one place that // Reading the raw payload was a false-positive
// exists to tell us the band is real. // generator: once one valid frame had landed, a
InstanceMessage::PanelFrame( // REJECTED frame — wrong family, invalid, stale
pmacs_protocol::panel::PanelFramePayload::Present(frame) // generation — still supplied the expected text while
| pmacs_protocol::panel::PanelFramePayload::PresentMapped { frame, .. }, // the retained old frame supplied the rendering. The
) => { // probe would report the band showing something it does
facts.panel_frames += 1; // not show.
facts.panel_rows = frame.size.rows; let panel_message = matches!(msg.as_ref(), InstanceMessage::PanelFrame(_));
facts.panel_cols = frame.size.cols; let panel_before = state
facts.panel_focused = frame.focused; .panel
facts.panel_frame_text = grid_probe_text(&frame.cells); .presented()
if let Some(expected) = expected_panel_text.as_deref() .map(|frame| (frame.panel_epoch, frame.geometry_epoch, frame.size));
&& facts.panel_frame_text.contains(expected)
{
facts.panel_text_observed = true;
}
}
InstanceMessage::PanelFrame(
pmacs_protocol::panel::PanelFramePayload::Absent,
) => facts.panel_absent_observed = true,
_ => {}
}
state.apply_attach_message(*msg); state.apply_attach_message(*msg);
if panel_message {
match state.panel.presented() {
Some(frame) => {
let identity = (frame.panel_epoch, frame.geometry_epoch, frame.size);
// Counted only when the retained frame
// actually moved: a duplicate or a refusal
// leaves the band exactly as it was, and
// counting either would say the daemon is
// painting when it is not.
if panel_before != Some(identity) {
facts.panel_frames += 1;
}
facts.panel_rows = frame.size.rows;
facts.panel_cols = frame.size.cols;
facts.panel_focused = frame.focused;
facts.panel_frame_text = grid_probe_text(&frame.cells);
if let Some(expected) = expected_panel_text.as_deref()
&& facts.panel_frame_text.contains(expected)
{
facts.panel_text_observed = true;
}
}
None => facts.panel_absent_observed = true,
}
}
if is_snapshot { if is_snapshot {
// The dual declaration: a byte viewport for a // The dual declaration: a byte viewport for a
// document, a cell size for a terminal. The daemon // document, a cell size for a terminal. The daemon
@ -7015,7 +7032,12 @@ impl State {
// frame with no mapping identity is one whose cells cannot // frame with no mapping identity is one whose cells cannot
// be inverted safely, so accepting it would reintroduce the // be inverted safely, so accepting it would reintroduce the
// hole from the receiving side. // hole from the receiving side.
PanelFramePayload::Present(_) if self.panel_family == PanelFamily::Mapped => false, // Gated on the POSITIVE family, not on "not mapped".
// `Unsupported` is neither, and a `!= Mapped` test let it
// through — a session below `PANEL_MIN_VERSION` accepting a
// band it never negotiated. `carries_panel()` guards the
// geometry declaration, not this seam.
PanelFramePayload::Present(_) if self.panel_family != PanelFamily::Legacy => false,
PanelFramePayload::PresentMapped { PanelFramePayload::PresentMapped {
frame, frame,
mapping_generation, mapping_generation,