Skip to main content

SurfaceRenderer

Struct SurfaceRenderer 

Source
pub struct SurfaceRenderer { /* private fields */ }
Expand description

Per-surface renderer and lifecycle state machine.

Holds the current surface state plus the cross-call stashes one frame needs. Constructed empty with SurfaceRenderer::new; the shell brings it online with on_surface_created.

Implementations§

Source§

impl SurfaceRenderer

Source

pub fn new() -> Self

Creates an empty renderer in SurfacePhase::NoSurface.

No GPU work happens here; the surface is created later by the shell via on_surface_created once a window/surface exists (deferred window creation on desktop, surfaceCreated on Android).

Source

pub fn set_initial_pipeline_cache_data(&mut self, data: Option<Vec<u8>>)

Restores the persisted pipeline-cache blob a prior run produced via pipeline_cache_data, to seed the engine’s shader-pipeline compilation at the next surface install and cut warm-start shader/pipeline compilation to near zero on Vulkan.

Builder-style (a setter rather than a new on_surface_created* parameter) so the three surface-creation entry points — and every shell call site — keep their signatures; the shell (persistence lands in tasks 13/14) calls this once after SurfaceRenderer::new and before the first on_surface_created*. Has no effect in practice on adapters without PIPELINE_CACHE (Metal/DX12): the blob is validated against the live adapter at install time and discarded on any mismatch. Passing None clears any restored blob (a cold start).

Caller contract (provenance): data must be bytes this renderer itself previously produced via pipeline_cache_data and the shell persisted verbatim — never bytes from any other source. The framing check downstream (frust_gpu::pipeline_cache::unframe) proves the magic tag and the adapter fingerprint, which is provenance by convention rather than integrity: a deliberately forged blob passing that frame reaches wgpu’s unsafe pipeline-cache seam, whose contract makes foreign data undefined behaviour. This method stays safe because the obligation is a data-handling rule for the shell’s persistence layer, not something a signature can enforce.

Source

pub fn pipeline_cache_data(&self) -> Option<Vec<u8>>

The current pipeline-cache data to persist, framed with the adapter fingerprint so a later launch can validate it before reuse (see frust_gpu::pipeline_cache).

None when there is no live cache to read — no surface installed, or an adapter without PIPELINE_CACHE (Metal/DX12) — or when the driver has produced nothing to hand back yet. The shell (tasks 13/14) writes the returned bytes to disk and feeds them back via set_initial_pipeline_cache_data on the next launch.

Source

pub fn pipeline_warm_up_finished(&self) -> Option<u64>

How many pipelines the engine’s warm-up worker has finished building, or None outside SurfacePhase::SurfaceReady.

Diagnostic: a shell logs it beside the persisted pipeline-cache size so a small blob can be read against how much of the warm-up had landed.

Source

pub fn retained_ledger(&self) -> ResidentPages

The retained pages the renderer holds resident after its last frame (see ResidentPages): the engine’s retained-page store, read as plain data. Empty outside SurfacePhase::SurfaceReady — with no surface there is no engine and no page. Never blocks, and allocates nothing while the store is empty.

Source

pub fn take_retained_ledger(&mut self) -> Option<ResidentPages>

Self::retained_ledger, but only when it changed since the last call — a page added, evicted or re-rendered at another extent, the store cleared, or the surface created, recreated or destroyed (each of which starts the next call over, so a fresh or torn-down surface always publishes its empty ledger) — or when a committed scene recorded a page while the store holds any: the UI draws a page only once a ledger covers the scene that recorded it, and a page re-recorded at the same extent changes nothing else. None when neither happened.

What a shell calls after each frame’s submit to publish the ledger without re-publishing an unchanged one every frame.

Source

pub fn note_scene_serial(&mut self, serial: u64)

Name the paint serial (frust_core::PaintOutcome::paint_serial) of the scene the next Self::encode takes. A shell calls it before every encode, on whichever thread renders, with the serial that travelled with the scene; once that scene’s frame is committed, every ledger this renderer publishes carries the serial as its ResidentPages::frame. A shell that never calls it publishes ledgers stamped 0.

Source

pub fn take_retained_refusal(&mut self) -> bool

Whether a frame was refused since the last call because a DrawRetained named a page the engine did not hold (the engine then empties its store), clearing the flag.

The refused frame presented nothing, so a shell seeing true owes the widget tree one more paint, against the emptied ledger it publishes alongside — otherwise an idle screen would keep the frame before the refused one until something else asked for a paint.

Source

pub fn clear_retained(&mut self)

Drop every retained page the engine holds — what a device-scale change calls for, since every body is about to be laid out again. The next Self::take_retained_ledger reports the empty store. A no-op with no surface.

Source

pub fn note_device_scale(&mut self, scale: f64) -> bool

Note the device scale (physical / logical, already sanitized by the shell) the coming frames are painted at — the one place a shell reports a scale change to the retained store.

When scale differs from the last noted one (or none was noted yet), every resident page was rasterized for a scale no frame will name again, so the store is cleared (Self::clear_retained) and the next Self::take_retained_ledger publishes the empty ledger even when the store was empty already — the UI thread then paints every retained body again instead of naming pages that are gone. The same scale again is a no-op, so a shell may call this at every surface create and resize. Returns whether the scale changed.

Source

pub fn surface_resolved_translucent(&self) -> bool

Whether the live surface actually resolved to a translucent (alpha-compositing) mode — the truth a shell’s Mode B paint contract must key off, replacing the SurfaceAlphaRequest it asked for.

SurfaceAlphaRequest::TranslucentPreferred is a preference: the configure step resolves it against the platform’s advertised alpha modes and silently degrades to an opaque swapchain (with a log::warn!) when none is available. A shell that clears its base color to TRANSPARENT and lets platform_view slots punch their rects on the strength of the request alone then presents black rectangles on that opaque swapchain. Reading this after every successful on_surface_created*/ on_surface_installed — and re-reading it after every recreate — is what makes a fallback degrade to the Mode A contract (opaque base, no punch) instead.

false outside SurfacePhase::SurfaceReady: with no live surface there is nothing translucent to composite against, and false is the safe (Mode A) default — never punch a hole you can’t prove is a window.

Deliberately not named is_surface_translucent: RenderRoot (which sits on the other side of this same call chain) already owns a method by that name for the pushed flag, and a shell threads this value straight into it. The signature is wgpu-free, like every other value crossing this crate’s boundary (docs/CODE_STANDARDS.md’s wgpu-leak anti-pattern).

Source

pub fn gpu_pass_timings(&self) -> Option<GpuPassTimings>

The most recent frame’s real GPU time per pass, or None when this surface produces no such measurement.

None — the gpu_q=0 case a shell reports — for every reason there is: no live surface, a device created without wgpu::Features::TIMESTAMP_QUERY (which a build that did not compile perf-trace in never asks for), and the first few frames of a surface, before the ring’s first readback has landed.

The reading lags the calling frame: a frame’s queries are mapped without ever blocking the frame path, so what comes back is a recent frame’s GPU cost rather than the one being recorded. In steady state one fresh reading lands per frame, so the series is complete and offset, not sparse — see frust_gpu::diag::TimestampRing.

Source

pub fn set_overlay_split(&mut self, armed: bool) -> bool

Arms (or disarms) the overlay spike’s split on this surface: while armed, Self::encode keeps only the commands before the scene’s spike_cut for this surface’s frame, leaving the commands from the cut on to the host’s OverlaySurface, which draws them from the same scene. A scene with no cut is drawn whole.

A host arms it exactly while it has a live overlay to hand the range to — armed with none, those commands would be drawn nowhere. Refused (a no-op answering false) unless the spike is switched on (crate::overlay::OVERLAY_SPIKE_ENV_VAR), so no production path can arm it; answers whether the split is now armed.

The surface keeps its single-target shape either way: the cut changes which commands this surface’s one frame draws, never where it draws. The bracket rule a cut obeys is frust_engine::renderer::project_command_range’s.

Source

pub fn phase(&self) -> SurfacePhase

The current lifecycle phase.

Source

pub async fn on_surface_created( &mut self, ctx: &mut RenderContext, window: impl Into<SurfaceTarget<'static>>, width: u32, height: u32, alpha: SurfaceAlphaRequest, ) -> Result<()>

Brings the surface online (surfaceCreated/resumed): creates the swapchain and a device-bound renderer, transitioning to SurfacePhase::SurfaceReady.

Valid from any phase — calling it in SurfaceLost is how the shell recovers, and calling it in SurfaceReady replaces the surface (the old one is dropped first). window is any raw window handle the shell owns (wgpu::SurfaceTarget); no winit dependency is imposed here.

Presentation uses vsync (PresentMode::AutoVsync), matching the vsync-driven frame pacing the platform shells provide.

Source

pub async fn on_surface_installed( &mut self, ctx: &mut RenderContext, surface: DetachedSurface, width: u32, height: u32, alpha: SurfaceAlphaRequest, ) -> Result<()>

Brings the surface online from a DetachedSurface that was created on the windowing/main thread via RenderContext::surface_factory, transitioning to SurfacePhase::SurfaceReady (the render-thread split).

The desktop counterpart of on_surface_created for the split: on_surface_created reads the window handle and configures on one thread, but winit only yields that handle on the main thread — so the split creates the surface there (SurfaceFactory::create_detached_surface) and hands the Send surface here, where the render thread that owns this renderer/context does the device + swapchain + blitter work. Presentation uses vsync (PresentMode::AutoVsync), matching on_surface_created.

Valid from any phase (recreation after SurfaceLost/resume replaces the old surface — dropped first).

Source

pub async unsafe fn on_surface_created_from_android_window( &mut self, ctx: &mut RenderContext, window_ptr: *mut c_void, width: u32, height: u32, alpha: SurfaceAlphaRequest, ) -> Result<()>

Brings the surface online from a raw ANativeWindow pointer (Android surfaceCreated), transitioning to SurfacePhase::SurfaceReady.

Compiled unconditionally so a host cargo check --target aarch64-linux-android covers it. This is one of the framework’s sanctioned unsafe entry points (see also on_surface_created_from_metal_layer); the raw-pointer handling is isolated in frust_gpu::lifecycle::create_android_surface.

§Safety

window_ptr must be a valid, acquired ANativeWindow* that outlives the surface (and all its SurfaceTextures). See frust_gpu::lifecycle::create_android_surface for the full contract.

Source

pub fn on_surface_changed( &mut self, ctx: &RenderContext, width: u32, height: u32, )

Resizes the swapchain (surfaceChanged/Resized).

Only acts in SurfacePhase::SurfaceReady; a resize with no surface is dropped. The renderer (and its compiled pipelines) is preserved — only the surface config and its sized attachments are recreated. Zero dimensions are ignored (a minimized window keeps its last valid size).

Source

pub fn on_surface_destroyed(&mut self)

Tears the surface down (surfaceDestroyed/suspended), transitioning to SurfacePhase::NoSurface.

Drops the RenderSurface (and its renderer) so no surface or texture outlives the platform’s underlying window. Idempotent.

Source

pub fn render( &mut self, ctx: &RenderContext, scene: &Scene, base_color: Color, ) -> Result<FrameOutcome>

Encodes scene and presents it, clearing to base_color; returns the FrameOutcome so the shell can react.

This is a thin convenience wrapper over the two-phase seam Self::encode + Self::present: it encodes, and — unless the frame was skipped for want of a renderable surface — presents. A caller that wants to attribute GPU encode cost separately from the swapchain-acquire (vsync) wait — the render-thread-split decision hinges on that split — calls the two entry points directly and times each with its own clock (timing stays shell-owned; this crate reads no clock — see frust-shell-common::perf’s layering note).

In SurfacePhase::NoSurface/SurfacePhase::SurfaceLost the frame is dropped (FrameOutcome::Skipped) — never panicking, never queueing. On an Outdated acquire the surface is reconfigured and FrameOutcome::Redraw asks the shell to try again; on Lost the surface is dropped, the machine moves to SurfacePhase::SurfaceLost, and FrameOutcome::SurfaceLost tells the shell to recreate it.

Nothing accumulates across calls: each frame re-encodes from scene.

Source

pub fn encode( &mut self, ctx: &RenderContext, scene: &Scene, base_color: Color, ) -> Result<EncodeOutcome>

Phase 1 of the frame — the encode span: take the frame’s scene. It does not touch the swapchain, so a caller timing this call in isolation measures encode cost with no vsync wait folded in. This copies the display list and stashes base_color; the GPU render moves to Self::submit (it needs the acquired swapchain texture), so encode_us is a memcpy and nothing else.

Returns EncodeOutcome::Skipped (no work done, nothing queued) in any phase but SurfacePhase::SurfaceReady; otherwise EncodeOutcome::Encoded, after which Self::present finishes the frame. Nothing accumulates across frames.

Source

pub fn encode_segmented( &mut self, ctx: &RenderContext, scene: &Scene, base_color: Color, cuts: &[Range<usize>], ) -> Result<EncodeOutcome>

Self::encode for a segmented frame (render-model Phase 4, decision D10): the overlays above hosted native views draw the command ranges cuts of scene — the differ’s ViewCommand::Overlay.command_ranges, ascending and disjoint — and this surface draws everything else.

The main segment is the complement of the cuts (crate::overlay::main_segment_ranges): 0..first.start, every gap between two cuts, and whatever follows the last one, each gap projected on its own through frust_engine::renderer::project_command_ranges so its clips and layers are closed and reopened around the cuts (the bracket rule of frust_engine::renderer::project_command_range). The differ’s cuts tile the tail from the first cut whenever every visible slot after it has content above it, so the main segment is usually 0..first.start; a slot with nothing above it leaves a gap (its hole-punch, say) that the main surface keeps drawing.

Only servable cuts are taken (crate::overlay::cut_is_servable): a cut whose range holds content an overlay cannot draw — a retained page’s composite or recording, a retained bracket open across the cut, an external texture, a backdrop blur — stays in the main segment, drawn exactly as an uncut frame draws it, and its overlay records an empty range. Self::submit_segmented reports the classification per cut (OverlayCutOutcome::serviceability).

The whole scene is copied once more, for Self::submit_segmented to cut each overlay’s range from — ctx stays unused, as in encode. With no cut this is exactly Self::encode (the whole frame, copied whole), and so is a frame the overlay spike’s split (Self::set_overlay_split) is armed on: explicit cuts replace the spike’s fixed one here, never combine with it.

Follow with Self::acquire and Self::submit_segmented (or Self::submit_segmented_deferred) with an OverlayCut per cut, in the same order. A plain Self::submit after a segmented encode draws the main segment alone.

Source

pub fn present(&mut self, ctx: &RenderContext) -> Result<FrameOutcome>

Phase 2 of the frame — the present span: a thin wrapper over the two-phase Self::acquire + Self::submit seam, kept for callers (and the Self::render convenience wrapper) that time present as one span. It acquires the swapchain texture (the blocking vsync/present wait) and, on success, renders/submits/presents it.

A caller wanting the finer acquire (blocking vsync wait) vs submit (GPU render + queue-submit + present) attribution — to separate GPU saturation from render cost — calls Self::acquire and Self::submit directly, timing each with its own clock (timing stays shell-owned; this crate reads no clock — see frust-shell-common::perf’s layering note). present’s combined span equals acquire + submit by construction.

Assumes Self::encode has already copied this frame’s display list. In any phase but SurfacePhase::SurfaceReady the call is a no-op returning FrameOutcome::Skipped. On an Outdated acquire the surface is reconfigured and FrameOutcome::Redraw asks the shell to try again; on Lost the surface is dropped, the machine moves to SurfacePhase::SurfaceLost, and FrameOutcome::SurfaceLost tells the shell to recreate it.

Source

pub fn acquire(&mut self, ctx: &RenderContext) -> Result<AcquireOutcome>

Phase 2a of the frame — the acquire sub-span: acquire the swapchain texture (the blocking vsync/present wait, per the surface’s present mode), classify the result, and — on a usable acquire — stash the texture for Self::submit to blit into. Timing this call in isolation attributes the blocking present/vsync wait separately from Self::submit’s blit/queue-submit work — the split needed to separate GPU saturation from blit cost.

Returns AcquireOutcome::Acquired when a texture was stashed (the caller must follow with Self::submit); otherwise a terminal outcome — AcquireOutcome::Reconfigured (Outdated acquire, surface reconfigured), AcquireOutcome::Lost (surface dropped, now SurfaceLost), or AcquireOutcome::Skipped (no renderable surface or a transient failure). In any phase but SurfacePhase::SurfaceReady it is a no-op returning AcquireOutcome::Skipped.

Source

pub fn submit(&mut self, ctx: &RenderContext) -> Result<FrameOutcome>

Phase 2b of the frame — the submit sub-span: turn the swapchain texture Self::acquire stashed into a presented frame, then present it. Timing this call in isolation attributes the submit work separately from Self::acquire’s blocking vsync wait.

What the submit span contains: the whole GPU render runs HERE, targeting the acquired swapchain texture (which does not exist until Self::acquire) or the surface’s intermediate, then present. So the frame’s GPU cost lands in submit_us; encode_us is a memcpy.

v3 wire mapping. The acquire_us/submit_us field names and the wire format are unchanged (perf.rs is untouched). The swapchain acquire (the blocking vsync wait) happens in Self::acquire and is recorded in acquire_us, so acquire precedes the GPU render, which lives entirely in this span.

Must follow an AcquireOutcome::Acquired result from Self::acquire on the same frame — it consumes the stashed texture. With nothing stashed (no prior Acquired, or the surface vanished between the two calls) it is a no-op returning FrameOutcome::Skipped; otherwise FrameOutcome::Rendered.

A caller that must issue the present itself, on another thread, calls Self::submit_deferred instead.

Source

pub fn submit_deferred( &mut self, ctx: &RenderContext, ) -> Result<(FrameOutcome, Option<DeferredPresent>)>

Self::submit with the final present step handed back to the caller instead of issued here: identical GPU work (render + queue-submit), but the acquired swapchain frame is returned as an opaque, Send DeferredPresent the caller presents on a thread of its choosing.

The iOS presentsWithTransaction contract is why this exists — see DeferredPresent’s docs for the full mechanism. Everything else is unchanged: same outcomes, same stash discipline, and Some(DeferredPresent) accompanies exactly the FrameOutcome::Rendered case (every other outcome carries None). Dropping the returned handle instead of presenting it skips that frame’s present without error.

Source

pub fn submit_segmented( &mut self, ctx: &RenderContext, overlays: &mut [OverlayCut<'_>], ) -> Result<SegmentedOutcome>

Self::submit for a frame Self::encode_segmented cut: the main segment AND every overlay’s range recorded into one encoder — the external passes, the main segment’s passes, then each OverlayCut’s OverlaySurface::record in the order given — and one Queue::submit, so the GPU runs every target’s work in order and no overlay waits on a submit of its own. Then the presents, on the calling thread: the main surface first, then each overlay that rendered, in cut order (bottom to top).

The engine records and this crate submits once per frame, per the frame-recording invariant (its atlas-growth and glyph-replay exceptions aside) — the overlays’ engines included. Each target still compiles its own segment: a compiled frame is specific to its target’s size and root, so a segmented frame costs one compile per target (the main segment’s plus one per overlay).

The overlays share nothing with the main surface but its device and queue: their engines keep retention off, which is why a cut holding retained content (or an external texture, or a backdrop blur) was kept on the main surface by the encode — its overlay records an empty range and draws nothing, see crate::overlay::cut_is_servable — and each schedules its own budget.

When the main frame does not render (nothing acquired, a surface gone, or an engine refusal) no overlay records either: every OverlayCutOutcome is OverlayFrame::Skipped, nothing is submitted, and every overlay keeps its previous frame on screen. An overlay the engine refuses on its own is reported in its outcome and presents nothing, while the main frame and the other overlays still land. After a plain Self::encode the overlays are skipped the same way — there is no cut scene to draw them from.

Source

pub fn submit_segmented_deferred( &mut self, ctx: &RenderContext, overlays: &mut [OverlayCut<'_>], ) -> Result<(SegmentedOutcome, SegmentedPresents)>

Self::submit_segmented with every present handed back instead of issued: identical GPU work, and the main frame plus each rendered overlay’s frame returned as a SegmentedPresents for a host that lands them together itself — on Apple, inside the one CATransaction that also carries the native views’ geometry (SegmentedPresents::present_all on the transaction’s thread; see DeferredPresent for why the thread matters).

Trait Implementations§

Source§

impl Default for SurfaceRenderer

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T, S> SimdFrom<T, S> for T
where S: Simd,

Source§

fn simd_from(_simd: S, value: T) -> T

Source§

impl<F, T, S> SimdInto<T, S> for F
where T: SimdFrom<F, S>, S: Simd,

Source§

fn simd_into(self, simd: S) -> T

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WasmNotSend for T
where T: Send,

Source§

impl<T> WasmNotSendSync for T

Source§

impl<T> WasmNotSync for T
where T: Sync,