Skip to main content

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