euv_engine/renderer/webgpu/struct.rs
1use super::*;
2
3/// Constructor inputs for [`WebGpuRenderer`]. Grouped into one struct so the
4/// constructor stays a single-argument call (the renderer carries enough GPU
5/// handles and canvas parameters that a positional parameter list would be
6/// unwieldy). All fields are public so integration tests can assemble a
7/// renderer around `Reflect` stand-in objects.
8#[derive(Clone, Data)]
9pub struct WebGpuRendererInit {
10 /// The WebGPU device (`GpuDevice`).
11 pub device: JsValue,
12 /// The device's command queue (`GpuQueue`).
13 pub queue: JsValue,
14 /// The WebGPU canvas rendering context (`GpuCanvasContext`).
15 pub context: JsValue,
16 /// The HTML canvas element backing the context.
17 pub canvas: HtmlCanvasElement,
18 /// The texture format string used by the swap chain (e.g., `"bgra8unorm"`).
19 pub format: String,
20 /// The physical pixel width of the canvas backing store.
21 pub width: u32,
22 /// The physical pixel height of the canvas backing store.
23 pub height: u32,
24 /// Whether MSAA anti-aliasing is enabled for render pipelines.
25 pub antialias: bool,
26}
27
28/// A WebGPU rendering backend wrapping the GPU device, queue, and canvas context
29/// for GPU-accelerated rendering on the web.
30///
31/// Created asynchronously via `WebGpuRenderer::init` because adapter and
32/// device acquisition returns JavaScript Promises that must be awaited.
33/// Once initialized, the renderer provides methods to create GPU resources
34/// (buffers, shader modules, command encoders) and execute render passes.
35///
36/// WebGPU types are stored as `JsValue` to avoid feature-gated import issues
37/// with `web_sys`. Method calls are performed via `Reflect` and `JsCast`.
38#[derive(Clone, Data)]
39pub struct WebGpuRenderer {
40 /// The WebGPU device (`GpuDevice`) used to create GPU resources.
41 pub(crate) device: JsValue,
42 /// The device's command queue (`GpuQueue`) for submitting command buffers.
43 pub(crate) queue: JsValue,
44 /// The WebGPU canvas rendering context (`GpuCanvasContext`).
45 pub(crate) context: JsValue,
46 /// The HTML canvas element backing the WebGPU context.
47 pub(crate) canvas: HtmlCanvasElement,
48 /// The texture format string used by the canvas's swap chain (e.g., `"bgra8unorm"`).
49 #[get(type(clone))]
50 pub(crate) format: String,
51 /// The physical pixel width of the canvas backing store.
52 #[get(type(copy))]
53 pub(crate) width: u32,
54 /// The physical pixel height of the canvas backing store.
55 #[get(type(copy))]
56 pub(crate) height: u32,
57 /// Whether MSAA anti-aliasing is enabled for render pipelines.
58 ///
59 /// When `true`, the renderer allocates a multisampled intermediate texture
60 /// (`sampleCount: 4`) and resolves into the swap chain each frame; when
61 /// `false`, render passes attach directly to the swap chain view at
62 /// `sampleCount: 1`.
63 #[get(type(copy))]
64 pub(crate) antialias: bool,
65 /// The multisampled color texture used when `antialias` is `true`.
66 ///
67 /// `None` when MSAA is disabled. Rebuilt on every resize because the
68 /// `width`/`height` are immutable for a given `GpuTexture`.
69 #[get(type(clone))]
70 pub(crate) multisample_texture: Option<JsValue>,
71 /// The default `GpuTextureView` into `multisample_texture`.
72 ///
73 /// Cached at texture-create time so `begin_render_pass` does not have to
74 /// recreate the view each frame. `None` when MSAA is disabled.
75 #[get(type(clone))]
76 pub(crate) multisample_view: Option<JsValue>,
77 /// The depth-stencil texture used for depth-tested passes.
78 ///
79 /// Created lazily on the first call to [`WebGpuRenderer::begin_render_pass`]
80 /// that includes a `depthStencil` attachment. Rebuilt on every resize
81 /// because the dimensions are immutable for a given `GpuTexture`. The
82 /// matching default view is cached in `depth_view`.
83 ///
84 /// `None` until the first depth-tested render pass is opened.
85 #[get(type(clone))]
86 pub(crate) depth_texture: Option<JsValue>,
87 /// The default `GpuTextureView` into `depth_texture`.
88 ///
89 /// `None` when no depth texture has been allocated.
90 #[get(type(clone))]
91 pub(crate) depth_view: Option<JsValue>,
92 /// The depth-stencil format used for `depth_texture`.
93 ///
94 /// Stored so subsequent render-pass openers can pass the same format
95 /// to the pipeline layout without having to remember it externally.
96 /// `None` until the first depth texture is allocated.
97 #[get(type(clone))]
98 pub(crate) depth_format: Option<String>,
99 /// User-supplied closure fired when the underlying `GpuDevice` enters
100 /// the `lost` state (browser-initiated context loss, OS driver crash,
101 /// `device.destroy()`, ...).
102 ///
103 /// `None` until the caller calls [`WebGpuRenderer::on_device_lost`].
104 /// The renderer also stores a separate `device_lost_handle` that
105 /// forwards the `GPUDeviceLostInfo` JS value into this callback.
106 #[get(type(clone))]
107 pub(crate) device_lost_callback: Option<js_sys::Function>,
108 /// Whether the device is currently in the `lost` state.
109 ///
110 /// Once flipped to `true`, every GPU operation returns
111 /// `Err(WebGpuError::RendererDisposed)` until the caller destroys the
112 /// renderer and creates a new one (WebGPU has no "recover from lost
113 /// device" API).
114 #[get(type(copy))]
115 pub(crate) device_lost: bool,
116 /// Shared slot for the most recent popped error-scope value.
117 ///
118 /// `device.popErrorScope()` returns a `Promise<GPUError?>`; we
119 /// cannot `.await` it from a sync call site. Instead, every
120 /// `push_error_scope` + `pop_error_scope` pair registers a
121 /// microtask via `wasm_bindgen_futures::spawn_local` that stores
122 /// the resolved value here. Callers that want the error
123 /// synchronously call [`WebGpuRenderer::take_last_error`] to
124 /// drain the slot.
125 ///
126 /// Holding a `Rc<PendingErrorCell>` lets the spawn_local future
127 /// own its own handle independently of `&self`, so the
128 /// renderer's borrow checker stays happy. The slot is empty
129 /// (`None`) by default and after each successful take.
130 ///
131 /// The cell is intentionally `PendingErrorCell` (a `Sync`
132 /// `UnsafeCell` newtype, see [`crate::renderer::static`]) rather
133 /// than `Rc<RefCell<...>>` - the WASM single-threaded scheduler
134 /// makes the runtime borrow check `RefCell` provides unreachable
135 /// in practice, so we trade it for a raw `UnsafeCell` deref
136 /// confined to two call sites. This mirrors how euv-core
137 /// implements its global registries
138 /// (`core/src/renderer/registry/struct.rs:62`).
139 pub(crate) pending_error: Rc<PendingErrorCell>,
140 /// The currently-open `GpuCommandEncoder`, if any.
141 ///
142 /// WebGPU expects the application to encode all work for a
143 /// frame (clear, render passes, compute passes, copy ops) into
144 /// a single command encoder, then call `encoder.finish()` to
145 /// produce a `GpuCommandBuffer` and submit it to the queue.
146 /// The encoder is `None` after `submit()` finishes and must
147 /// be re-acquired via `device.createCommandEncoder()` before
148 /// the next frame.
149 #[get(type(clone))]
150 pub(crate) command_encoder: Option<JsValue>,
151 /// OPT 34: cached render-pass descriptor, allocated lazily on the
152 /// first call to [`WebGpuRenderer::begin_render_pass_full`].
153 ///
154 /// The pre-WebGPU-audit path allocated a fresh `Object` +
155 /// `Array` + 8-15 `Reflect::set` calls every frame. We keep the
156 /// descriptor Object alive for the renderer's lifetime, refreshing
157 /// the per-frame fields (`view`, `resolveTarget`, `clearValue`) in
158 /// place on every call. The cache invalidates itself automatically
159 /// when `load_op` / `store_op`, the depth-stencil shape, or the
160 /// resolve-target shape changes.
161 ///
162 /// `None` until the first `begin_render_pass_full` call; `Some(_)`
163 /// afterwards and persists for the lifetime of the renderer.
164 #[get(type(clone))]
165 pub(crate) render_pass_descriptor_cache: Option<RenderPassDescriptorCache>,
166}
167
168/// OPT 34: persistent render-pass descriptor and its inner
169/// attachments, reused across `begin_render_pass_full` calls.
170///
171/// # Why
172///
173/// `begin_render_pass_full` historically allocated a fresh descriptor
174/// `Object`, a `colorAttachments` `Array`, and one inner
175/// `color_attachment` `Object` (plus an optional `clearValue` `Object`)
176/// on every frame, then ran 8-15 `Reflect::set` calls to populate
177/// them. WebGPU re-validates the descriptor each call, but the JS-side
178/// `Object` / `Array` allocations and the per-property `Reflect::set`
179/// crossings are pure overhead — only the `clearValue` and (rarely)
180/// `view` / `loadOp` / `storeOp` fields change between frames.
181///
182/// # What we cache
183///
184/// - The top-level descriptor `Object` (the one passed to
185/// `beginRenderPass`).
186/// - The `colorAttachments` `Array` (always exactly one element —
187/// we keep the same `Array` reference and mutate its slot 0 in
188/// place).
189/// - The inner color attachment `Object` (slot 0 of
190/// `colorAttachments`).
191/// - The `clearValue` `Object` (the `{r, g, b, a}` dictionary that
192/// is the actual per-frame mutating field).
193/// - Last-applied `loadOp` / `storeOp` string slices, to detect when
194/// the caller switched ops and the cached descriptor must be
195/// rebuilt (rare; WebGPU does not hot-swap ops every frame).
196/// - Last-applied depth-stencil shape (present / absent), to detect
197/// when the depth-stencil shape changes.
198///
199/// # Invalidation
200///
201/// The cache is invalidated (rebuilt from scratch) when any of:
202/// - `load_op` changes between calls,
203/// - `store_op` changes between calls,
204/// - the depth-stencil shape changes (None → Some / Some → None),
205/// - the resolve-target shape changes (MSAA on/off).
206///
207/// `view` / `resolveTarget` / `clearValue` are refreshed on every call
208/// (the swap-chain view expires after each presented frame, so caching
209/// it across frames silently invalidates every subsequent render pass).
210///
211/// These are all `&'static str` (they come from `WEBGPU_*_OP_*`
212/// constants), so invalidation is a pointer-compare.
213///
214/// `Clone` is derived so the parent `WebGpuRenderer`'s `Data` derive
215/// (which adds a `Clone` bound on every field) keeps compiling;
216/// `js_sys::Object` and `js_sys::Array` both derive `Clone`, so the
217/// derived `Clone` impl just clones the inner JS-side references
218/// (cheap, no JS allocation).
219#[derive(Clone, Debug)]
220pub struct RenderPassDescriptorCache {
221 /// The cached top-level `GpuRenderPassDescriptor` Object.
222 /// Pass directly to `encoder.beginRenderPass(descriptor)`.
223 pub(crate) descriptor: Object,
224 /// The cached inner color attachment Object.
225 /// `descriptor.colorAttachments[0]` in JS terms.
226 pub(crate) attachment: Object,
227 /// The cached `clearValue` Object (the `{r, g, b, a}` dictionary
228 /// under `attachment.clearValue`). The hot-path field — only
229 /// this is mutated on most frames.
230 pub(crate) clear_value: Object,
231 /// Last applied `loadOp` (as a `&'static str`). Used to detect
232 /// op changes that invalidate the descriptor.
233 pub(crate) last_load_op: Option<&'static str>,
234 /// Last applied `storeOp` (as a `&'static str`). Used to detect
235 /// op changes that invalidate the descriptor.
236 pub(crate) last_store_op: Option<&'static str>,
237 /// Whether the last applied descriptor had a depth-stencil
238 /// attachment (`true`) or not (`false`). Used to detect shape
239 /// changes that invalidate the descriptor.
240 pub(crate) last_has_depth: bool,
241 /// Whether the last applied descriptor had a `resolveTarget`
242 /// (`true`, MSAA path) or not (`false`). Used to detect shape
243 /// changes that invalidate the descriptor, so a stale
244 /// `resolveTarget` never survives an MSAA -> non-MSAA switch.
245 pub(crate) last_has_resolve: bool,
246}
247
248/// Interior-mutable slot for the renderer's pending error-scope value.
249///
250/// This is the `euv-engine` analog of euv-core's `HandlerRegistryCell`
251/// (`core/src/renderer/registry/struct.rs:62`): a single-element
252/// `Sync` wrapper that holds an `Option<JsValue>` behind an
253/// `UnsafeCell`.
254///
255/// # Why this type exists
256///
257/// `WebGpuRenderer::pending_error` needs interior mutability
258/// because:
259///
260/// 1. `pop_error_sync` takes `&self` (the WebGPU hot path cannot
261/// be `async`), but the spawned `wasm_bindgen_futures::spawn_local`
262/// future must mutate the slot to store the resolved
263/// `Promise<GPUError?>` value.
264/// 2. `take_last_error` also takes `&self` and drains the slot
265/// on the next render tick.
266///
267/// The first implementation used `Rc<RefCell<Option<JsValue>>>`,
268/// which works but pays for:
269///
270/// - a `RefCell::borrow_mut` runtime borrow check on every
271/// write (the panic path is unreachable in practice — only
272/// the spawn_local future and `take_last_error` ever touch
273/// the slot, and they never overlap because the future is
274/// a microtask drained before the next render tick).
275/// - a heap allocation for the `RefCell`'s borrow state.
276///
277/// The newtype keeps the interior-mutability primitive (`Rc`),
278/// because the spawn_local future needs its own owning handle,
279/// but swaps the inner cell from `RefCell` to `UnsafeCell`:
280///
281/// - zero runtime borrow check (the WASM single-threaded
282/// scheduler makes the borrow impossible to violate).
283/// - zero allocation (the cell is just a `*mut Option<JsValue>`
284/// sitting inside the `Rc`-managed box).
285///
286/// # Sync safety
287///
288/// `PendingErrorCell` is **not** `Sync` by default (`UnsafeCell`
289/// explicitly opts out). We hand-implement `Sync` for it because
290/// the renderer is only ever used in the WASM single-threaded
291/// runtime; the `Rc` ensures the same instance is never shared
292/// across threads (it is not `Send`/`Sync` either), and the
293/// WASM main thread is the only place that ever touches the
294/// slot. This matches euv-core's pattern
295/// (`unsafe impl Sync for HandlerRegistryCell {}`).
296///
297/// If the engine is ever compiled for a multi-threaded target
298/// (native, `wasm-bindgen-rayon`), this `unsafe impl Sync` is
299/// unsound and must be removed.
300pub(crate) struct PendingErrorCell(
301 /// Interior-mutable storage for the optional `JsValue`.
302 ///
303 /// Marked `pub(crate)` (not just `pub`) because the field is
304 /// only meant to be touched from inside the renderer module —
305 /// specifically from the `impl PendingErrorCell` block in
306 /// `impl.rs`. The struct itself stays `pub` so external code
307 /// can name the type, but the raw `UnsafeCell` is an
308 /// implementation detail.
309 pub(crate) UnsafeCell<Option<JsValue>>,
310);