Skip to main content

Module render_split

Module render_split 

Source
Expand description

Render-thread-split plumbing shared by every shell.

§What lives here

The split moves encode→acquire→blit→present off the UI thread onto a dedicated render thread: the UI thread keeps rebuild→layout→paint, then hands the finished Scene across. This module is the vocabulary for that handoff — the shells own the threads and the wgpu/vello resources, this crate owns the platform-free channel types and the pure lifecycle/kill-switch logic they coordinate through.

  • render_channel — the single UI→render link: a depth-1, latest-wins scene-handoff slot (a newer SceneFrame replaces an un-taken one; the render thread always takes the freshest, dropping stale frames) fused with a FIFO lifecycle-command queue behind one std::sync::Condvar, so the render thread has a single wait point (RenderReceiver::wait_next). Depth 1 is deliberate — Flutter’s merged-mode precedent shows pipeline depth drops to 1 when threads merge; deeper queues add latency for no mobile win.
  • scene_return_channel — the reverse, render→UI give-back link: a non-blocking, depth-1 Mutex-only slot (no Condvar — the UI thread only ever polls it, never parks) the render thread pushes a drained scene back through once it is done reading it, so a shell’s split submit_frame can Scene::reset() and reuse the buffer next frame instead of reallocating one via Scene::new() every frame — restoring frust-scene’s documented reuse contract (scene.rs’s Scene::reset docs) in split mode.
  • RenderCommand / RenderEvent / RenderPhase — the surface lifecycle vocabulary (created/changed/destroyed/pause/resume) as owned commands, modelled on frust-render’s SurfacePhase machine: a pure, host-testable next_render_phase transition table gates whether the render thread may render.
  • Ack / AckWaiter — the cross-thread acknowledgment barrier that makes RenderCommand::Pause and RenderCommand::SurfaceDestroyed synchronous: the UI thread blocks until the render thread has honored the command. This is the correctness anchor for two platform hazards: Android can destroy the ANativeWindow while the render thread still holds the surface, and iOS can kill a process that submits Metal work after the app backgrounds. Both are barriers, not shared mutable flags.
  • SceneFrame / FrameMeta / SurfaceSize — the per-frame payload crossing the handoff: the scene plus the frame clock, the surface dimensions, and (for the single-emitter perf recording) the UI thread’s UiSpans half of the frame timing, which the render thread folds together with its own RenderSpans via FramePasses::from_split.
  • render_thread_enabled / NO_RENDER_THREAD_VAR — the single kill switch the shells consult, parsed exactly like crate::frame_gate’s FRUST_NO_FRAME_GATE (compile-time define or runtime env, any non-"0" value). When engaged, a shell keeps the pre-split single-thread path (kept as an escape hatch until the split’s on-device throughput is fully validated).

§Benchmark scenario markers

The channel carries one thing beside the scene: the benchmark scenario-window edges crate::perf::mark_scenario_start raises, so each is logged stamped with the frame that actually carried it. Three properties of that route are load-bearing here.

  • perf-trace-only. Every marker field, call and queue in this module is behind the feature; a release-lean build has no marker code in the channel at all, and a perf-trace build with FRUST_TRACE off pays one cached bool read per RenderSender::send_scene.
  • Per-thread, not process-wide. RenderSender::send_scene moves the calling thread’s raised markers into the inbox, and [drain] stages the taken frame’s markers onto the calling (render) thread, which is the thread about to record that frame. No shared queue and no flag decide who owns a marker; the thread that raised it does, until it hands a frame off.
  • A marker from a thread that hands no frame off is never emitted. Only the UI thread’s own queue crosses this channel, so a marker raised on, say, a blocking-pool thread stays there and dies with it — see crate::perf::mark_scenario_start, which documents the rule and why the alternative (attaching it to some other thread’s frame) is the cross-thread guess this route exists to remove.

§Layering choice

Like crate::perf and crate::frame_gate, this is shell-owned and platform-free: it takes no frust-render/wgpu/vello dependency, no unsafe, and no frust-reactive, preserving this crate’s compiles-everywhere, reactive-free charter (see docs/ARCHITECTURE.md’s Layer Dependencies). The scene payload (SceneFrame) and the surface handle a RenderCommand::SurfaceCreated carries are therefore generic parameters (S/W): a shell instantiates S = frust_scene::Scene and W = its own raw-window wrapper, while these host tests instantiate cheap stand-ins, so the whole channel is exercised without a GPU or a platform.

§Wiring

This module ships the channel types + pure logic; the desktop, Android, and iOS shells each spawn their own render thread on top of it, gated by render_thread_enabled.

Structs§

Ack
The render-thread side of an acknowledgment barrier: the render thread holds this (moved out of a RenderCommand::Pause/RenderCommand::SurfaceDestroyed) while honoring the command, then acknowledges it — unblocking the UI thread’s paired AckWaiter.
AckWaiter
The UI-thread side of an acknowledgment barrier: the UI thread waits on this after sending a RenderCommand::Pause/ RenderCommand::SurfaceDestroyed, blocking until the render thread has acknowledged (or dropped) the paired Ack.
FrameMeta
Per-frame metadata riding the scene-handoff channel alongside the scene itself.
RenderBatch
One wakeup’s worth of work handed to the render thread by RenderReceiver::wait_next/RenderReceiver::try_next: the lifecycle commands to process (FIFO), then the freshest scene to render (if any). A render loop processes commands first (updating its RenderPhase), then renders scene only if the resulting phase can_render.
RenderReceiver
The render-thread handle to the render channel: the single wait point (Self::wait_next) draining pending commands plus the freshest scene each wakeup.
RenderSender
The UI-thread handle to the render channel: sends scenes (latest-wins) and lifecycle commands (FIFO). Single-producer by design (the UI thread), so it is deliberately not Clone.
SceneFrame
One frame handed from the UI thread to the render thread across render_channel: the finished scene, its FrameMeta, and the UI thread’s UiSpans half of the frame timing (the render thread is the single perf emitter).
SceneReturnReceiver
The UI-thread handle to scene_return_channel: polls (never blocks) for a scene the render thread has finished with.
SceneReturnSender
The render-thread handle to scene_return_channel: pushes a drained scene back for the UI thread to reclaim (Scene::reset + reuse) instead of a shell allocating a fresh one every frame — closing the buffer-reuse gap a scene crossing render_channel would otherwise leave (a scene with no way back, so every split submit_frame replaced it with Scene::new()).
SurfaceSize
The surface dimensions a SceneFrame / RenderCommand carries — the physical (device-pixel) swapchain size plus the HiDPI scale factor, so the render thread can (re)configure the surface without consulting the UI thread. Physical-at-the-boundary matches the render thread’s swapchain needs (docs/CODE_STANDARDS.md’s physical-at-FFI, logical-inside rule).

Enums§

RenderCommand
A lifecycle command the UI thread sends to the render thread across render_channel, as an owned value — not a shared mutable flag. Generic over the surface-handle type W a Self::SurfaceCreated carries (frust-render’s raw-window wrapper in a real shell; a stand-in in host tests), keeping this crate render-free.
RenderEvent
A surface-lifecycle event that drives a render-thread RenderPhase transition — the pure, Copy counterpart of a RenderCommand (mirroring frust-render’s SurfaceEvent/callback split, keeping the transition table host-testable without the owned Ack/window payloads).
RenderPhase
The render thread’s view of surface lifecycle state, modelled on frust-render’s SurfacePhase: the render loop renders a handed-off SceneFrame only while can_render — i.e. only in RenderPhase::Active. RenderPhase::Paused is the cross-thread backgrounding barrier (a leftover scene must NOT be submitted after a Pause, per the iOS process-kill hazard).

Constants§

NO_RENDER_THREAD_VAR
The render-thread-split kill-switch environment/compile-time variable: when set to any non-"0" value, render_thread_enabled is false and a shell keeps the pre-split single-thread frame path.

Functions§

ack_pair
Create a linked AckWaiter / Ack barrier pair: the UI thread keeps the waiter, the render thread receives the ack (inside the command). Used by RenderSender::pause/RenderSender::destroy_surface; exposed directly for shells building lifecycle commands by hand.
next_render_phase
Pure render-phase transition table, the analogue of frust-render’s next_phase. Total by design:
render_channel
Create the UI→render channel: a depth-1 latest-wins scene slot fused with a FIFO lifecycle-command queue behind one condvar.
render_thread_enabled
Whether a shell should run the render-thread split — the single switch every shell consults. true unless the NO_RENDER_THREAD_VAR kill switch is engaged (compile-time define or runtime env, any non-"0" value), mirroring crate::perf::enabled’s and crate::frame_gate’s option_env! + runtime-env parsing precedent.
scene_return_channel
Create the render→UI scene give-back channel: a non-blocking, depth-1 return slot — the reverse-direction, pull-based counterpart to render_channel’s UI→render handoff. Mutex<Option<S>> only, no Condvar and no new dependency: nothing should ever park waiting on this slot, so there is no wait point to back — preserving this module’s no-unsafe, no-new-deps, generic-over-S charter (see the module docs’ Layering choice).