pub struct FramePairing { /* private fields */ }Expand description
The release gate’s bookkeeping: which frust frame produced each command batch, and therefore which batches may be handed to the native side yet.
A shell records (generation, frame_id) for every batch
PlatformViewState::ingest produces (the frame that is about to be
submitted carries the geometry the batch describes), reads back the id of
the last frame the render side actually presented, and serves
PlatformViewState::commands_up_to the resulting boundary. Pure logic:
no clock, no platform types, no knowledge of how a shell obtains the two
cursors — which is what makes the ordering testable on the host, since a
mobile shell’s own frame loop is not.
§Why the frame id, not a count
The UI→render scene channel is depth-1 latest-wins, so submissions and
presents are not the same clock — under the measured 120 Hz-submit /
60 Hz-present regime they diverge by half the frames. Pairing against a
presented count over-delays by exactly the dropped frames; pairing against
the id of the frame that actually presented does not. The
price of the id is that a dropped frame’s id never arrives, which is what
MAX_FRAMES_IN_FLIGHT exists for.
§Lifecycle
The pairing is a smoothing device, not a correctness barrier: a shell
clears it whenever the frames it refers to stop being
meaningful (backgrounding, surface recreation), after which the whole
backlog releases immediately — a hide or a full replay must reach the native
side even though no further frame will ever present to unlock it.
Going idle is the third such moment, and the only one with no lifecycle
callback to hang a clear on: the loop simply stops producing frames while
the display keeps ticking. A shell reports those ticks
(note_idle_tick) so the staleness hatch stays
reachable there — without them a batch whose frame never presented is held
for the process lifetime (see that method for the full failure mode).
Implementations§
Source§impl FramePairing
impl FramePairing
Sourcepub fn record(&mut self, generation: u64, frame_id: u64)
pub fn record(&mut self, generation: u64, frame_id: u64)
Remember that the batch published under generation describes geometry
painted by frust frame frame_id.
Sourcepub fn note_idle_tick(&mut self)
pub fn note_idle_tick(&mut self)
Report one display tick on which the frame loop produced no frust
frame — the tick a shell’s frame gate skipped. Ages every held batch
exactly as a submission does (see
releasable_generation).
Why the release gate needs an idle clock at all. A batch is released
on one of two events: its own frame is confirmed presented, or the
submission cursor climbs MAX_FRAMES_IN_FLIGHT past it. A present is
recorded only for a Rendered render outcome, so any other one — an
encode or acquire skipped against a surface that is not ready, a
swapchain reconfigure, a lost surface, an encode/acquire error — leaves
the batch’s frame permanently unconfirmed. That is survivable while
frames keep flowing, because the submission cursor walks past it within
twelve frames. Once the app settles, though, the submission cursor stops
too, and neither arm can ever fire again: the settled geometry is held
for the process lifetime and the native sibling stays parked at whatever
mid-animation rect it last applied — device-observed as a camera preview
stuck black behind correct-but-never-delivered geometry, healed only by a
surface recreate (which clears the pairing). The display clock is the
one cursor still moving at idle, and an idle tick carries exactly the
evidence the submission cursor does: that frame is not coming.
Why an idle bound and not an immediate release. Releasing the whole
backlog on the last painted frame is not expressible: a touch-driven drag
paints with needs_frame == false every frame, so “this paint asked for
no continuation frame” cannot tell a settle frame from a mid-drag one,
and keying the release on it would turn the gate off for exactly the
scrolling case it was built to smooth. Aging by idle ticks costs nothing
on any path where frames still flow — a present that does arrive still
releases the batch first, unchanged — and bounds the broken path to
MAX_FRAMES_IN_FLIGHT display ticks (~100 ms at 120 Hz).
Sourcepub fn releasable_generation(
&self,
presented_frame_id: u64,
submitted_frame_id: u64,
) -> u64
pub fn releasable_generation( &self, presented_frame_id: u64, submitted_frame_id: u64, ) -> u64
The highest generation releasable right now, given the id of the last presented frame and of the last submitted one.
A batch is releasable once its own frame is on screen, or once that
frame has fallen MAX_FRAMES_IN_FLIGHT behind the staleness cursor
(it was dropped by the latest-wins channel, or never presented at all,
and will never reach the screen). The first batch that is neither caps
the boundary at its own generation minus one, so everything published
before it — including a lifecycle batch that was never paired with a
frame at all — still goes out; an empty queue releases everything.
The staleness cursor is the submission cursor plus the current idle
stretch (note_idle_tick): the two are the same
“frames have moved on past this one” evidence, and with no idle ticks
reported this is bit-for-bit the submission-only rule.
Sourcepub fn acknowledge(&mut self, generation: u64)
pub fn acknowledge(&mut self, generation: u64)
Drop the bookkeeping for every batch the native side has acknowledged —
the same generation the shell hands
PlatformViewState::acknowledge, so the two stay in step.