Skip to main content

Module platform_view

Module platform_view 

Source
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::Create then ViewCommand::Update in the same ingest batch, in that order — the native side never sees an Update for 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 call PlatformViewState::commands every frame for free when nothing moved.
  • params_json change (detected via params_generation, bumped by the widget whenever it edits params_json) ⇒ ViewCommand::UpdateParams, independent of the rect/clip/visible comparison above.
  • view_type change on a live slot ⇒ ViewCommand::Dispose followed by a fresh Create + Update, in the same ingest. A different view_type is a different native factory, so the old view cannot be re-parameterized into the new one; emitting only a Create would be ignored by a host that already has a view for that slot id, and the DISPOSE_AFTER_MISSING_FRAMES streak would never fire at all (the slot is still present every pass). See the view-type-swap arm in PlatformViewState::ingest.
  • Missing for HIDE_AFTER_MISSING_FRAMES consecutive ingests (while the slot was last visible) ⇒ Update { visible: false } — a Hide. Only fires once per hide (the slot’s tracked last_visible flips to false, so the same missing streak never re-emits it).
  • Missing for DISPOSE_AFTER_MISSING_FRAMES consecutive ingests ⇒ ViewCommand::Dispose, and the slot is forgotten — a later reappearance of the same slot_id is indistinguishable from a brand-new one and gets a fresh Create. PlatformViewState::retire is 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 ordinary Update — visible flips back to true like any other changed field, no Create.
  • 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 by frust-widgets’ shield(child) wrapper), narrowed to those that overlap the slot’s own rect; 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§

FramePairing
The release gate’s bookkeeping: which frust frame produced each command batch, and therefore which batches may be handed to the native side yet.
PlatformViewState
The differ: per-slot last-seen state plus the accumulated, not-yet-acknowledged ViewCommand backlog. See the module docs for the full semantics.

Enums§

ViewCommand
One native-sibling-compositor instruction — the differ’s whole output vocabulary. Clone + PartialEq + Debug so a golden test can assert an exact command sequence.

Constants§

DISPOSE_AFTER_MISSING_FRAMES
Consecutive ingest calls a slot may be absent from frames before 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 ingest calls a previously-live, previously-visible slot may be absent from frames before 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 FramePairing releases 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.