cranpose_render_wgpu/frame_packet.rs
1//! The producer→present frame boundary (pipeline step 4).
2//!
3//! A [`FramePacket`] is everything the present stage needs to render one
4//! frame: the fully lowered, owned scene tree plus the frame scalars. The
5//! producer builds it after lowering; the present side consumes it and
6//! returns the scene buffers for recycling. Today both happen synchronously
7//! on one thread; the packet type is the contract that lets a later step
8//! move consumption to a present thread without changing what crosses the
9//! boundary.
10//!
11//! Every payload member is proven `Send` at compile time below — a
12//! regression that reintroduces a thread-bound member (an `Rc`, a raw
13//! pointer, a borrowed graph node) fails the build here rather than at the
14//! future channel.
15
16use crate::normalized_scene::{ChildLayerComposite, CollectedLayer, LoweredChildSource};
17use crate::scene::{
18 BackdropLayer, CompositorScene, DrawOp, DrawShape, EffectLayer, ImageDraw, RetainedDraw,
19 ShadowDraw, TextDraw,
20};
21#[cfg(not(target_arch = "wasm32"))]
22use crate::scene::{ColorPatch, PendingFeedCapture};
23use cranpose_core::NodeId;
24use cranpose_render_common::graph::{DrawCommandId, ProjectiveTransform};
25use cranpose_ui_graphics::{GraphicsLayer, Rect, RenderEffect};
26
27/// One frame's replay plan, emitted by the producer-side planner
28/// ([`ShapeReplayState::take_frame_ops`](crate::shape_replay::ShapeReplayState))
29/// when the packet is built and consumed by the present-side store
30/// (`GpuRenderer::consume_replay_ops`) just before the packet renders. This
31/// is the ONLY producer→store replay channel; the store answers with a
32/// [`ReplayAck`].
33#[cfg(not(target_arch = "wasm32"))]
34#[derive(Default)]
35pub(crate) struct ReplayFrameOps {
36 /// The retained-feed generation the plan was made under. The store
37 /// drops the batch whole on a mismatch: every capture/patch/release in
38 /// it names slots of a universe the store no longer holds.
39 pub(crate) generation: u64,
40 /// The planner's frame ordinal at plan time; the store's defensive
41 /// staleness reference for `captures` (each capture is stamped with the
42 /// ordinal it was queued on).
43 pub(crate) frame: u64,
44 pub(crate) captures: Vec<PendingFeedCapture>,
45 pub(crate) color_patches: Vec<ColorPatch>,
46 pub(crate) releases: Vec<u32>,
47}
48
49#[cfg(target_arch = "wasm32")]
50#[derive(Default)]
51pub(crate) struct ReplayFrameOps;
52
53/// One confirmed capture: the span's identity key `(command, span slot)`
54/// mapped to the physical GPU slot the store retained it in.
55pub(crate) type ReplayConfirmation = ((DrawCommandId, u32), u32);
56
57/// Why the present stage refused a packet without drawing it. Each reason
58/// names the expectation the packet no longer matches; the frame is not an
59/// error — its buffers travel back through [`RenderReturns`] for re-queue.
60/// `pub` (not `pub(crate)`) because the cancellation-protocol tests in
61/// `tests/` observe outcomes through `#[doc(hidden)]` hooks; the module is
62/// private, so the only public path is the hidden re-export in `lib.rs`.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum CancelReason {
65 /// The packet was built against a different `GpuRenderer` instance.
66 RendererEpoch,
67 /// The packet was built against a different surface configuration.
68 SurfaceEpoch,
69 /// The packet was lowered for a different surface size.
70 Viewport,
71 /// The present stage had no usable surface to draw the packet on: the
72 /// surface was dropped, or acquire failed past its one reconfigure
73 /// retry. Producer state (epochs, viewport) still matched — only the
74 /// swapchain was missing.
75 SurfaceUnavailable,
76 /// The device reported an uncaptured error (validation/OOM/internal)
77 /// since the previous frame: nothing of this packet is encoded on the
78 /// suspect device. One cancel per poisoning — the gate clears as it
79 /// fires, so the next packet renders (see `DeviceErrorSentry`).
80 DeviceError,
81}
82
83/// What the present stage did with a packet. `NotRun` is the `Default` so a
84/// draw that never happened can never be reported `Presented`.
85/// `pub` like [`CancelReason`], for the same hidden test re-export.
86#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
87pub enum PresentOutcome {
88 /// No packet was consumed (the default of an untouched returns value).
89 #[default]
90 NotRun,
91 /// The packet was drawn and presented.
92 Presented,
93 /// The packet was refused before any encoding; buffers returned whole.
94 Cancelled(CancelReason),
95}
96
97/// The store's answer to one [`ReplayFrameOps`] batch, applied by the
98/// planner ([`ShapeReplayState::apply_ack`](crate::shape_replay::ShapeReplayState))
99/// before the next frame's planning. Travels with the batch's emptied
100/// buffers (capacity intact) so neither side allocates per frame.
101#[cfg(not(target_arch = "wasm32"))]
102pub(crate) struct ReplayAck {
103 /// The generation the confirmations are stamped with — the slot
104 /// universe they verifiably exist in.
105 pub(crate) generation: u64,
106 /// The staleness ordinal of the batch this answers, echoed back so the
107 /// planner purges exactly THIS batch's unconfirmed requests. With a
108 /// packet rendering and another already published, a later batch's
109 /// requests are live when this ack lands and must survive it.
110 pub(crate) frame: u64,
111 pub(crate) confirmations: Vec<ReplayConfirmation>,
112}
113
114#[cfg(target_arch = "wasm32")]
115pub(crate) struct ReplayAck;
116
117/// The lowered root a [`FramePacket`] carries: either today's direct path
118/// (the root renders straight to the surface) or a root layer surface (the
119/// old non-direct fallback, now lowered producer-side too).
120pub(crate) enum PacketRoot {
121 /// The lowered root scene plus owned child-layer composites, rendered
122 /// by `render_root_direct`. Boxed (like `Surface`) so moving the packet
123 /// moves one pointer instead of the payload struct.
124 Direct(Box<CollectedLayer>),
125 /// A root that needs its own layer surface (effects, backdrops or
126 /// shadows on the root), rendered by the snapshot-consuming
127 /// `render_layer_surface` body plus the root composite tail.
128 Surface(Box<RootSurfacePacket>),
129}
130
131/// Producer lowering of a non-direct root plus a snapshot of every value
132/// the present-side root composite tail used to read off `graph.root` —
133/// the present backend must never touch the retained graph.
134pub(crate) struct RootSurfacePacket {
135 /// The root's collection-time snapshot, from `lower_layer_node` (the
136 /// same lowering the render-side root path used to run).
137 pub(crate) lowered: ChildLayerComposite,
138 /// The root's collected content; dropped unused on a raster-cache hit.
139 pub(crate) source: LoweredChildSource,
140 /// `graph.root.transform_to_parent` — maps the root surface and the
141 /// backdrop/shadow rects into surface space.
142 pub(crate) transform_to_parent: ProjectiveTransform,
143 /// `graph.root.node_id`, for the root backdrop layer.
144 pub(crate) node_id: Option<NodeId>,
145 /// `graph.root.backdrop().cloned()` — drives the composite-target
146 /// decision and the root backdrop apply.
147 pub(crate) backdrop: Option<RenderEffect>,
148 /// `graph.root.graphics_layer` — read for `shadow_elevation` and by
149 /// `push_layer_shadow`.
150 pub(crate) graphics_layer: GraphicsLayer,
151 /// `graph.root.local_bounds` — the backdrop/shadow source rect.
152 pub(crate) local_bounds: Rect,
153 /// `graph.root.clip_rect()` — the root backdrop layer's clip.
154 pub(crate) clip_rect: Option<Rect>,
155 /// `graph.root.shadow_clip` — the root shadow's clip.
156 pub(crate) shadow_clip: Option<Rect>,
157}
158
159/// One frame's producer output.
160pub(crate) struct FramePacket {
161 /// Monotone frame sequence number, stamped by the producer. Consumed by
162 /// present-side telemetry today; the lease/ack replay protocol keys off
163 /// it when the stages split.
164 pub(crate) frame_id: u64,
165 /// Physical surface size the payload was lowered for.
166 pub(crate) viewport: (u32, u32),
167 /// The `GpuRenderer` instance the packet was built against; a packet
168 /// that outlives its renderer is cancelled, never drawn.
169 pub(crate) renderer_epoch: u64,
170 /// The surface configuration the packet was built against; a packet
171 /// that straddles a reconfigure is cancelled, never drawn.
172 pub(crate) surface_epoch: u64,
173 /// Root scale the payload was lowered for.
174 pub(crate) root_scale: f32,
175 /// The lowered root (direct or root-surface).
176 pub(crate) root: PacketRoot,
177 /// The dev overlay, lowered producer-side when a dev overlay graph is
178 /// set; the present backend only renders it.
179 pub(crate) overlay: Option<CollectedLayer>,
180 /// The frame's replay plan. Unconditional so the packet has one
181 /// architecture; wasm has no retained replay path, and Surface frames
182 /// never touch the planner — both carry the empty default, which the
183 /// present store must NOT consume (its generation 0 would count a
184 /// false generation drop).
185 pub(crate) replay: ReplayFrameOps,
186 /// Producer-side text layout cache size at packet build time, carried
187 /// for the present backend's frame stats — the present call tree holds
188 /// no text layout state to read it from.
189 pub(crate) text_cache_len: usize,
190 /// Threaded mode only: the emptied confirmations vec from a previous
191 /// frame's [`ReplayAck`], riding back to the present-side store so it
192 /// recycles the capacity instead of allocating per frame. The sync path
193 /// (and wasm) hands the vec straight back via
194 /// `restore_replay_ack_confirmations` and leaves this `None`.
195 pub(crate) recycled_confirmations: Option<Vec<ReplayConfirmation>>,
196 /// Threaded mode only: set by the present stage's early replay-ops
197 /// consumption (`GpuRenderer::take_replay_ack_early`), which runs
198 /// BEFORE surface acquire so the [`ReplayAck`] reaches the producer in
199 /// time for the very next frame's planning. When set, `replay` has
200 /// been taken (it holds the empty default), the render path must not
201 /// consume it again, and a later cancel must not reclaim it. Always
202 /// `false` as built by the producer and on the sync path.
203 pub(crate) replay_preconsumed: bool,
204}
205
206/// Present-stage timestamps for one consumed packet, in nanoseconds on the
207/// clock the producer injected at runtime start (`0` = stage did not run or
208/// no clock was injected). Carried back in [`RenderReturns`] so the
209/// producer's frame telemetry keeps recording acquire/render/present phases
210/// after those stages move to the present thread. Plain integers so the
211/// packet types stay clock-library-free on every target.
212#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
213pub struct PresentTimings {
214 pub after_acquire_ns: i64,
215 pub after_render_ns: i64,
216 pub after_present_ns: i64,
217}
218
219/// What the present stage hands back to the producer after consuming a
220/// frame: the rendered packet's scene buffers for recycling and the store's
221/// [`ReplayAck`] (with the batch's emptied op buffers) for the planner.
222/// The producer folds it in via `RendererFrontend::apply_returns`; the
223/// present backend fills it instead of writing producer state itself.
224#[derive(Default)]
225pub(crate) struct RenderReturns {
226 /// The rendered direct-root scene, returned so its draw vectors are
227 /// reused instead of reallocated every frame. `None` when the frame
228 /// rendered a Surface root (its scene is not pooled, as before) or the
229 /// direct draw failed.
230 pub(crate) scene: Option<CompositorScene>,
231 /// The store's answer to the packet's replay plan plus the recycled
232 /// op buffers. `None` when no packet was consumed; always `None` on
233 /// wasm, which has no retained replay path.
234 pub(crate) ack: Option<(ReplayAck, ReplayFrameOps)>,
235 /// The `frame_id` of the packet these returns describe; 0 when no
236 /// packet was consumed.
237 pub(crate) frame_id: u64,
238 /// What the present stage did with the packet. Never `Presented`
239 /// unless a draw actually ran.
240 pub(crate) outcome: PresentOutcome,
241 /// A cancelled packet's replay plan, returned unconsumed so the
242 /// planner can re-queue its releases and recycle its buffers. `None`
243 /// on the presented path (the ops travel back through `ack` there).
244 pub(crate) cancelled_replay: Option<ReplayFrameOps>,
245 /// Present-thread stage timestamps for this packet; all-zero on the
246 /// sync path (the producer already holds the clock there) and on any
247 /// outcome that never reached the swapchain.
248 pub(crate) timings: PresentTimings,
249}
250
251/// Compile-time proof that the packet and every member chain can cross a
252/// thread boundary. Listed individually so a regression names the exact
253/// type that broke instead of one opaque `FramePacket: !Send` error.
254const _: () = {
255 const fn assert_send<T: Send>() {}
256 assert_send::<FramePacket>();
257 assert_send::<PacketRoot>();
258 assert_send::<RootSurfacePacket>();
259 assert_send::<CollectedLayer>();
260 assert_send::<ChildLayerComposite>();
261 assert_send::<LoweredChildSource>();
262 assert_send::<CompositorScene>();
263 assert_send::<DrawShape>();
264 assert_send::<ImageDraw>();
265 assert_send::<TextDraw>();
266 assert_send::<ShadowDraw>();
267 assert_send::<DrawOp>();
268 assert_send::<EffectLayer>();
269 assert_send::<BackdropLayer>();
270 assert_send::<RetainedDraw>();
271 assert_send::<ReplayFrameOps>();
272 assert_send::<ReplayAck>();
273 assert_send::<RenderReturns>();
274 assert_send::<CancelReason>();
275 assert_send::<PresentOutcome>();
276 assert_send::<PresentTimings>();
277 assert_send::<Option<Vec<ReplayConfirmation>>>();
278};
279
280#[cfg(not(target_arch = "wasm32"))]
281const _: () = {
282 const fn assert_send<T: Send>() {}
283 assert_send::<ColorPatch>();
284 assert_send::<PendingFeedCapture>();
285};