Expand description
Platform-agnostic native-sibling compositor logic: turns the raw,
per-paint-pass PlatformViewFrame collection (frust-core) into an
idempotent, generation-stamped command list — ViewCommand — both mobile
shells’ FFI peek getters serve to their platform side
(docs/SHELLS_ARCHITECTURE.md’s platform-view embedding flow).
Pure diffing logic with no FFI, no JSON, and no platform types — the same
“platform-agnostic brain, shell-owned wire format” split this crate draws
elsewhere (frame_gate’s skip decision, resample’s pointer
interpolation). JSON encoding of a ViewCommand batch stays hand-rolled
in each shell’s own FFI glue (docs/CODE_STANDARDS.md’s “hand-roll JSON at
the mobile FFI boundary” rule); this module owns only the typed command
vocabulary, never serde or any wire format.
§The frust-core → differ contract
frust-core::app::RenderRoot::platform_view_frames() replaces its whole
Vec<PlatformViewFrame> every paint pass and stays deliberately dumb: a
slot absent from one pass’s frames might be culled-but-still-alive,
momentarily not repainting, or genuinely torn down — core has no teardown
hook to tell those apart. PlatformViewState resolves that ambiguity by
watching how long a slot stays missing (missing_streak).
§Command semantics
- New
slot_id⇒ViewCommand::CreatethenViewCommand::Updatein the same ingest batch, in that order — the native side never sees anUpdatefor a view it hasn’t been told to create yet. - Rect/clip/visible change past
EPSILON_PX⇒Update; a smaller change (or none at all) emits nothing, so a shell can callPlatformViewState::commandsevery frame for free when nothing moved. params_jsonchange (detected viaparams_generation, bumped by the widget whenever it editsparams_json) ⇒ViewCommand::UpdateParams, independent of the rect/clip/visible comparison above.view_typechange on a live slot ⇒ViewCommand::Disposefollowed by a freshCreate+Update, in the same ingest. A differentview_typeis a different native factory, so the old view cannot be re-parameterized into the new one; emitting only aCreatewould be ignored by a host that already has a view for that slot id, and theDISPOSE_AFTER_MISSING_FRAMESstreak would never fire at all (the slot is still present every pass). See the view-type-swap arm inPlatformViewState::ingest.- Missing for
HIDE_AFTER_MISSING_FRAMESconsecutive ingests (while the slot was last visible) ⇒Update { visible: false }— a Hide. Only fires once per hide (the slot’s trackedlast_visibleflips tofalse, so the same missing streak never re-emits it). - Missing for
DISPOSE_AFTER_MISSING_FRAMESconsecutive ingests ⇒ViewCommand::Dispose, and the slot is forgotten — a later reappearance of the sameslot_idis indistinguishable from a brand-new one and gets a freshCreate.PlatformViewState::retireis the second, explicit path to the same outcome — the one a real widget teardown takes, immediately — and both are kept deliberately (Widget teardown detection, below). - Revive after Hide (slot reappears in
ingest’s frames before the dispose threshold): since the slot is still tracked, this is just an ordinaryUpdate—visibleflips back totruelike any other changed field, noCreate. - Revive after Dispose: the slot was forgotten, so this is
indistinguishable from new — fresh
Create+Update.
§Z-shields (interactive slots only)
An interactive slot’s shields list — the regions where frust content
painted OVER the slot keeps winning input — is assembled here, not by
the widget, from two sources:
- the pass’s auto-collected shield rects
(
RenderRoot::input_shields(), reported byfrust-widgets’shield(child)wrapper), narrowed to those that overlap the slot’s ownrect; plus - the slot’s own manually declared rects (
PlatformViewView::shield_local, the escape hatch), which arrive on the frame and are always kept.
A non-interactive slot always ships an empty list: shields only mean
anything to a host that is forwarding touches to the native view in the
first place, so carrying them would be noise the host must ignore. The
resulting ViewCommand::Update shape is the same either way.
Comparison is epsilon-based, like rect/clip (and order-sensitive: the
collection order is paint order, which is deterministic for an unchanged
tree), so a shield drifting sub-pixel with its chrome emits nothing.
§Widget teardown detection
Two paths converge on the same Dispose. A torn-down platform_view
widget reports its slot id to frust-core’s pending-retire list
(RenderRoot::take_retired_platform_views), which each shell drains after
its rebuild and feeds to PlatformViewState::retire — an immediate
Dispose, no streak. DISPOSE_AFTER_MISSING_FRAMES is the backstop
for what that hook cannot see (a widget dropped without View::teardown
running): a heuristic streak, since ingest alone cannot tell a dropped
widget from a culled or transiently-not-repainting one. Both paths give the
same command and the same “next Create is fresh” semantics, and a merely
culled slot reports no retire, so it correctly keeps living behind the
streak.
§Generation / acknowledgement / compaction
PlatformViewState::commands returns (generation, &[ViewCommand]) — the
entire not-yet-acknowledged command backlog, not just the latest
batch. generation only advances when PlatformViewState::ingest (or
PlatformViewState::reset_for_surface_recreate/PlatformViewState::retire)
actually produces at least one command; a no-change ingest leaves it
untouched, so a shell polling every frame can cheaply tell “nothing new”
apart from “here’s more to apply” without diffing the slice itself.
PlatformViewState::acknowledge tells the state that the native side has
finished applying everything up through a given generation, letting it
compact (drop) those entries from the backlog — this is what makes a
missed poll during surface recreation safe: the native side just re-polls
commands and gets the same backlog again
(nothing was dropped until acknowledged), and re-applying an already-applied
prefix is safe because the command stream is a replay of state transitions,
not one-shot deltas.
§Backlog cap
The backlog only shrinks on acknowledge,
so a native side that stops acking (a wedged host, a lost view hierarchy)
would otherwise grow it for the process lifetime — reachable, since a camera
preview is a genuinely long-lived slot. Past MAX_PENDING_COMMANDS
entries the backlog is compacted into its own net effect: one Dispose
per slot the dropped entries tore down, then a full Create + Update
replay of every live slot — exactly the surface-recreate replay
(reset_for_surface_recreate),
which is already the established “the native side must rebuild from this
alone” batch. Every dropped intermediate is a state transition the replay
supersedes, so a native side that applies only the compacted batch lands in
the same place. The whole compacted batch carries the current generation, so
an ack of an older one drops none of it.
§Frame pairing (the release gate)
commands hands the native side the whole
backlog the instant it exists — which is earlier than the frust frame that
produced the geometry reaches the screen, so a scrolling hosted view runs
visibly ahead of the frust content it is supposed to be pinned to. A shell
that knows which frust frame each batch came from closes that gap by
releasing only the prefix whose frame is already presented:
FramePairing keeps the (generation, frame_id) bookkeeping and
commands_up_to serves the prefix.
Holding geometry for a presentation that never comes is the gate’s one
failure mode, so it releases anyway once the frame it waits on has fallen far
enough behind — counted in submissions while frames flow and in idle display
ticks (FramePairing::note_idle_tick) once they stop.
§Skip-safety
Nothing here special-cases a gate-skipped frame
(docs/SHELLS_ARCHITECTURE.md’s frame_gate module); the contract is
entirely “don’t call ingest on a Skip”.
Paint doesn’t run on a skip, so no rect can appear to “move” either
(PaintCtx::visible_rect/scroll state can’t have changed).
Structs§
- Frame
Pairing - The release gate’s bookkeeping: which frust frame produced each command batch, and therefore which batches may be handed to the native side yet.
- Platform
View State - The differ: per-slot last-seen state plus the accumulated, not-yet-acknowledged
ViewCommandbacklog. See the module docs for the full semantics.
Enums§
- View
Command - One native-sibling-compositor instruction — the differ’s whole output
vocabulary.
Clone + PartialEq + Debugso a golden test can assert an exact command sequence.
Constants§
- DISPOSE_
AFTER_ MISSING_ FRAMES - Consecutive
ingestcalls a slot may be absent fromframesbefore it is Disposed outright. A heuristic streak, not a real teardown signal — see the module docs’ Widget teardown detection. - EPSILON_
PX - Below this many logical px of difference on every edge, a rect/clip change
is not worth an
ViewCommand::Update— see the module docs’ Command semantics section. Chosen to absorb floating-point layout jitter (e.g. a scroll offset accumulating sub-pixel drift) without visibly lagging a genuinely moving native sibling view. - HIDE_
AFTER_ MISSING_ FRAMES - Consecutive
ingestcalls a previously-live, previously-visible slot may be absent fromframesbefore it is Hidden (Update { visible: false }). See the module docs’ Command semantics section. - MAX_
FRAMES_ IN_ FLIGHT - How far a batch’s frust frame may fall behind the submission cursor before
FramePairingreleases the batch anyway — the release gate’s staleness escape hatch. Required, not defensive: the UI→render scene channel is depth-1 latest-wins, so a scene the UI thread submitted may be overtaken and never rendered at all, and a dropped frame’s id never presents. Without this arm one dropped scene strands every later batch forever — a submission counter is not a presented counter. - MAX_
PENDING_ COMMANDS - Upper bound on the not-yet-acknowledged command backlog before it is compacted into its own net effect — see the module docs’ Backlog cap.