GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/web/renderer.rs (42.5K)
1 //! `WebRenderer`: the Vulkan renderer's 2D path, on WebGPU.
2 //!
3 //! It draws a [`Frame2D`] the way `vk::VkRenderer::draw_frame_2d` does, batch
4 //! for batch: the display-list geometry under each batch's scissor with its
5 //! parameter block (a dynamic-offset uniform here, push constants there),
6 //! user images interleaved by `z_before`, a blur-behind batch preceded by a
7 //! snapshot of the frame so far (the pass ends, the target is copied, the
8 //! pass resumes), then the text, then the overlay geometry. What differs is
9 //! only what WebGPU has instead:
10 //!
11 //! - **The canvas.** A WebGPU canvas takes no sRGB format, so it is
12 //! configured with its preferred (unorm) format and an sRGB *view* format,
13 //! and drawn through the sRGB view: the hardware encodes on write and blends
14 //! in linear, as the Vulkan swapchain's `*_SRGB` format does.
15 //! - **The parameter block** is a uniform at `@group(1)`, one 256-byte slot
16 //! per batch (`draw::shaders::WEBGPU_BLOCK_STRIDE`), plus a zero slot for
17 //! the overlay — laid out by the same `draw::batch_push_constants`.
18 //! - **The backdrop.** With no 3D scene behind the UI, the Vulkan renderer's
19 //! backdrop is a cleared image; here it is a 1x1 transparent texture (the
20 //! shader samples it clamped, so every texel reads the same zero). The
21 //! blur snapshot is a full-size texture of the canvas's sRGB view format.
22 //! - **No damage.** Every frame is drawn whole (`Frame2D::damage` is a
23 //! promise the renderer may use, never a requirement).
24 //! - **Images have one mip level.** A mipmapped upload is uploaded plain, as
25 //! the Vulkan renderer does on a device that cannot blit mips.
26
27 use std::collections::HashMap;
28
29 use super::rt::WebRt;
30 use super::scene::WebScene;
31 use crate::draw::rt::{PreparedRtScene, RtCamera, RtEnvironment};
32 use crate::draw::scene::{MeshId, SceneDraw, SceneImage, Stage3D, Vertex3D};
33
34 use wasm_bindgen::{JsCast, JsValue};
35 use web_sys::{
36 gpu_buffer_usage as buffer_usage, gpu_map_mode as map_mode, gpu_shader_stage as shader_stage,
37 gpu_texture_usage as texture_usage, GpuAddressMode, GpuBindGroup, GpuBindGroupDescriptor,
38 GpuBindGroupEntry, GpuBindGroupLayout, GpuBindGroupLayoutDescriptor, GpuBindGroupLayoutEntry,
39 GpuBlendComponent, GpuBlendFactor, GpuBlendOperation, GpuBlendState, GpuBuffer, GpuBufferBinding,
40 GpuBufferBindingLayout, GpuBufferBindingType, GpuBufferDescriptor, GpuCanvasAlphaMode,
41 GpuCanvasConfiguration, GpuCanvasContext, GpuColorTargetState, GpuCommandEncoder, GpuDevice,
42 GpuExtent3dDict, GpuFilterMode, GpuFragmentState, GpuLoadOp, GpuMipmapFilterMode,
43 GpuPipelineLayoutDescriptor, GpuPrimitiveState, GpuPrimitiveTopology, GpuQueue,
44 GpuRenderPassColorAttachment, GpuRenderPassDescriptor, GpuRenderPassEncoder, GpuRenderPipeline,
45 GpuRenderPipelineDescriptor, GpuSampler, GpuSamplerBindingLayout, GpuSamplerBindingType,
46 GpuSamplerDescriptor, GpuShaderModuleDescriptor, GpuStoreOp, GpuTexelCopyBufferInfo,
47 GpuOrigin3dDict, GpuTexelCopyBufferLayout, GpuTexelCopyTextureInfo, GpuTexture, GpuTextureBindingLayout,
48 GpuTextureDescriptor, GpuTextureFormat, GpuTextureSampleType, GpuTextureView,
49 GpuTextureViewDescriptor, GpuTextureViewDimension, GpuVertexAttribute, GpuVertexBufferLayout,
50 GpuVertexFormat, GpuVertexState, HtmlCanvasElement,
51 };
52
53 use crate::backend::tessellate::Vertex;
54 use crate::draw::glyphs::{image_quad_vertices, GlyphAtlas, GlyphVertex, ATLAS_SIZE};
55 use crate::draw::images::{image_table_built, retire_buffer, take_pending, Pending, PixelFormat};
56 use crate::draw::shaders::{shader2d_for_webgpu, GLYPH, WEBGPU_BLOCK_STRIDE};
57 use crate::draw::{
58 batch_push_constants, relief_px_at, window_info_data, Batch2D, Frame2D, TextSpan, MAX_PLATE_FEATURES,
59 PLATE_FEATURE_BYTES, PUSH_CONSTANT_FLOATS, WINDOW_INFO_BYTES,
60 };
61
62 /// The shader's `PlateFeatures` array is 128 entries; the binding must cover
63 /// all of it even though a frame uses at most [`MAX_PLATE_FEATURES`].
64 const PLATE_FEATURES_BINDING_BYTES: usize = 128 * PLATE_FEATURE_BYTES;
65
66 /// One uploaded user image.
67 struct WebImage {
68 texture: GpuTexture,
69 group: GpuBindGroup,
70 width: u32,
71 height: u32,
72 format: PixelFormat,
73 }
74
75 /// A grow-only GPU buffer: replaced by a larger one when a frame needs more.
76 pub(super) struct Growable {
77 pub(super) buffer: GpuBuffer,
78 size: u32,
79 usage: u32,
80 label: &'static str,
81 }
82
83 impl Growable {
84 pub(super) fn new(device: &GpuDevice, size: u32, usage: u32, label: &'static str) -> Result<Self, JsValue> {
85 let size = size.max(256).next_power_of_two();
86 let desc = GpuBufferDescriptor::new(size, usage | buffer_usage::COPY_DST);
87 desc.set_label(label);
88 Ok(Self { buffer: device.create_buffer(&desc)?, size, usage, label })
89 }
90
91 /// Make room for `needed` bytes; true when the buffer was replaced (any
92 /// bind group naming it must be rebuilt).
93 pub(super) fn ensure(&mut self, device: &GpuDevice, needed: u32) -> Result<bool, JsValue> {
94 if needed <= self.size {
95 return Ok(false);
96 }
97 self.buffer.destroy();
98 *self = Self::new(device, needed, self.usage, self.label)?;
99 Ok(true)
100 }
101
102 pub(super) fn write(&self, queue: &GpuQueue, bytes: &[u8]) -> Result<(), JsValue> {
103 if !bytes.is_empty() {
104 queue.write_buffer_with_u32_and_u8_slice(&self.buffer, 0, bytes)?;
105 }
106 Ok(())
107 }
108 }
109
110 /// A frame read back from the canvas: `width` x `height` RGBA8, sRGB-encoded
111 /// — the bytes the Vulkan swapchain image would hold.
112 pub struct Capture {
113 pub width: u32,
114 pub height: u32,
115 pub rgba: Vec<u8>,
116 }
117
118 /// A captured frame still on the GPU: [`read`](Self::read) waits for it.
119 pub struct PendingCapture {
120 buffer: GpuBuffer,
121 width: u32,
122 height: u32,
123 /// Bytes per row in the buffer: a copy's rows are 256-byte aligned.
124 row: u32,
125 /// The canvas is BGRA: swap to RGBA on the way out.
126 bgra: bool,
127 }
128
129 impl PendingCapture {
130 pub async fn read(self) -> Result<Capture, JsValue> {
131 let Self { buffer, width, height, row, bgra } = self;
132 buffer.map_async(map_mode::READ).await?;
133 let mapped = js_sys::Uint8Array::new(&JsValue::from(buffer.get_mapped_range()?));
134 let padded = mapped.to_vec();
135 buffer.unmap();
136 buffer.destroy();
137 let mut rgba = Vec::with_capacity((width * height * 4) as usize);
138 for y in 0..height as usize {
139 rgba.extend_from_slice(&padded[y * row as usize..y * row as usize + width as usize * 4]);
140 }
141 if bgra {
142 for px in rgba.chunks_exact_mut(4) {
143 px.swap(0, 2);
144 }
145 }
146 Ok(Capture { width, height, rgba })
147 }
148 }
149
150 pub struct WebRenderer {
151 /// Held for the device's lifetime: the device is the adapter's, and a
152 /// browser may tear the instance under it down once nothing holds it.
153 _gpu: web_sys::Gpu,
154 _adapter: web_sys::GpuAdapter,
155 device: GpuDevice,
156 queue: GpuQueue,
157 canvas: HtmlCanvasElement,
158 context: GpuCanvasContext,
159 /// What the canvas is configured as, and the sRGB view it is drawn through.
160 canvas_format: GpuTextureFormat,
161 view_format: GpuTextureFormat,
162
163 pipeline_2d: GpuRenderPipeline,
164 layout_0: GpuBindGroupLayout,
165 layout_1: GpuBindGroupLayout,
166 window_info: GpuBuffer,
167 plate_features: GpuBuffer,
168 backdrop_sampler: GpuSampler,
169 /// `@group(0)` over the empty backdrop, and over the blur snapshot.
170 group_0: GpuBindGroup,
171 snapshot: Option<(GpuTexture, GpuBindGroup, u32, u32)>,
172 /// The 3D scene pass, and `@group(0)` over its backdrop (rebuilt with
173 /// the backdrop) — what the 2D pass binds while a scene is shown.
174 scene: WebScene,
175 scene_group_0: Option<GpuBindGroup>,
176 /// The path tracer, made by the first `set_rt_scene`; the background and
177 /// environment it is handed at each `stage_rt`, as on Vulkan.
178 rt: Option<WebRt>,
179 rt_background: Option<[f32; 3]>,
180 rt_environment: RtEnvironment,
181 blocks: Growable,
182 group_1: GpuBindGroup,
183 vertices: Growable,
184
185 glyph_pipeline: GpuRenderPipeline,
186 glyph_layout: GpuBindGroupLayout,
187 atlas: GlyphAtlas,
188 atlas_texture: GpuTexture,
189 atlas_group: GpuBindGroup,
190 atlas_uploaded: u64,
191 glyph_vertices: Growable,
192
193 image_sampler: GpuSampler,
194 images: HashMap<u32, WebImage>,
195 image_vertices: Growable,
196
197 /// Copy the next frame into this, for [`take_capture`](Self::take_capture).
198 capture: Option<(GpuBuffer, u32, u32, u32)>,
199 capture_requested: bool,
200 }
201
202 pub(super) fn shader_module(device: &GpuDevice, code: &str, label: &str) -> web_sys::GpuShaderModule {
203 let desc = GpuShaderModuleDescriptor::new(code);
204 desc.set_label(label);
205 device.create_shader_module(&desc)
206 }
207
208 /// `wgpu::BlendState::ALPHA_BLENDING`, as both Vulkan pipelines blend.
209 pub(super) fn alpha_blending() -> GpuBlendState {
210 let color = GpuBlendComponent::new();
211 color.set_src_factor(GpuBlendFactor::SrcAlpha);
212 color.set_dst_factor(GpuBlendFactor::OneMinusSrcAlpha);
213 color.set_operation(GpuBlendOperation::Add);
214 let alpha = GpuBlendComponent::new();
215 alpha.set_src_factor(GpuBlendFactor::One);
216 alpha.set_dst_factor(GpuBlendFactor::OneMinusSrcAlpha);
217 alpha.set_operation(GpuBlendOperation::Add);
218 GpuBlendState::new(&alpha, &color)
219 }
220
221 fn uniform_entry(binding: u32, min_size: u32, dynamic: bool) -> GpuBindGroupLayoutEntry {
222 let entry = GpuBindGroupLayoutEntry::new(binding, shader_stage::FRAGMENT);
223 let layout = GpuBufferBindingLayout::new();
224 layout.set_type(GpuBufferBindingType::Uniform);
225 layout.set_min_binding_size(min_size);
226 layout.set_has_dynamic_offset(dynamic);
227 entry.set_buffer(&layout);
228 entry
229 }
230
231 fn texture_entry(binding: u32) -> GpuBindGroupLayoutEntry {
232 let entry = GpuBindGroupLayoutEntry::new(binding, shader_stage::FRAGMENT);
233 let layout = GpuTextureBindingLayout::new();
234 layout.set_sample_type(GpuTextureSampleType::Float);
235 layout.set_view_dimension(GpuTextureViewDimension::N2d);
236 entry.set_texture(&layout);
237 entry
238 }
239
240 fn sampler_entry(binding: u32) -> GpuBindGroupLayoutEntry {
241 let entry = GpuBindGroupLayoutEntry::new(binding, shader_stage::FRAGMENT);
242 let layout = GpuSamplerBindingLayout::new();
243 layout.set_type(GpuSamplerBindingType::Filtering);
244 entry.set_sampler(&layout);
245 entry
246 }
247
248 fn sampler(device: &GpuDevice, filter: GpuFilterMode, mipmap: GpuMipmapFilterMode) -> GpuSampler {
249 let desc = GpuSamplerDescriptor::new();
250 desc.set_address_mode_u(GpuAddressMode::ClampToEdge);
251 desc.set_address_mode_v(GpuAddressMode::ClampToEdge);
252 desc.set_address_mode_w(GpuAddressMode::ClampToEdge);
253 desc.set_mag_filter(filter);
254 desc.set_min_filter(filter);
255 desc.set_mipmap_filter(mipmap);
256 device.create_sampler_with_descriptor(&desc)
257 }
258
259 pub(super) fn texture(device: &GpuDevice, format: GpuTextureFormat, w: u32, h: u32, usage: u32, label: &str) -> Result<GpuTexture, JsValue> {
260 let size = [js_sys::Number::from(w.max(1)), js_sys::Number::from(h.max(1))];
261 let desc = GpuTextureDescriptor::new(format, &size, usage);
262 desc.set_label(label);
263 device.create_texture(&desc)
264 }
265
266 pub(super) fn whole_view(texture: &GpuTexture) -> Result<GpuTextureView, JsValue> {
267 texture.create_view()
268 }
269
270 fn extent(w: u32, h: u32) -> GpuExtent3dDict {
271 let e = GpuExtent3dDict::new(w);
272 e.set_height(h);
273 e
274 }
275
276 /// Write `pixels` (`w` x `h`, 4 bytes a texel, rows packed) into `texture`.
277 fn write_texture(queue: &GpuQueue, texture: &GpuTexture, pixels: &[u8], w: u32, h: u32) -> Result<(), JsValue> {
278 let layout = GpuTexelCopyBufferLayout::new();
279 layout.set_bytes_per_row(w * 4);
280 layout.set_rows_per_image(h);
281 queue.write_texture_with_u8_slice_and_gpu_extent_3d_dict(
282 &GpuTexelCopyTextureInfo::new(texture),
283 pixels,
284 &layout,
285 &extent(w, h),
286 )
287 }
288
289 /// [`write_texture`] for one rectangle of `texture`, at `(x, y)`: `pixels`
290 /// holds just that rectangle, tightly packed.
291 fn write_texture_region(
292 queue: &GpuQueue,
293 texture: &GpuTexture,
294 pixels: &[u8],
295 (x, y, w, h): (u32, u32, u32, u32),
296 ) -> Result<(), JsValue> {
297 let layout = GpuTexelCopyBufferLayout::new();
298 layout.set_bytes_per_row(w * 4);
299 layout.set_rows_per_image(h);
300 let origin = GpuOrigin3dDict::new();
301 origin.set_x(x);
302 origin.set_y(y);
303 let target = GpuTexelCopyTextureInfo::new(texture);
304 target.set_origin_gpu_origin_3d_dict(&origin);
305 queue.write_texture_with_u8_slice_and_gpu_extent_3d_dict(&target, pixels, &layout, &extent(w, h))
306 }
307
308 /// A two-entry (texture, sampler) bind group for the glyph shader.
309 fn texture_group(device: &GpuDevice, layout: &GpuBindGroupLayout, view: &GpuTextureView, sampler: &GpuSampler) -> GpuBindGroup {
310 let entries = [GpuBindGroupEntry::new_with_gpu_texture_view(0, view), GpuBindGroupEntry::new(1, sampler)];
311 device.create_bind_group(&GpuBindGroupDescriptor::new(&entries, layout))
312 }
313
314 fn uniform_binding(buffer: &GpuBuffer, size: u32) -> GpuBufferBinding {
315 let binding = GpuBufferBinding::new(buffer);
316 binding.set_size(size);
317 binding
318 }
319
320 impl WebRenderer {
321 /// Ask the browser for a WebGPU device and set `canvas` up to draw into.
322 pub async fn new(canvas: HtmlCanvasElement) -> Result<Self, JsValue> {
323 let (gpu, adapter, device) = super::request_device(&[]).await?;
324 let queue = device.queue();
325 let context: GpuCanvasContext = canvas
326 .get_context("webgpu")?
327 .ok_or("the canvas has no webgpu context")?
328 .dyn_into()?;
329
330 let canvas_format = gpu.get_preferred_canvas_format();
331 let view_format = match canvas_format {
332 GpuTextureFormat::Bgra8unorm => GpuTextureFormat::Bgra8unormSrgb,
333 GpuTextureFormat::Rgba8unorm => GpuTextureFormat::Rgba8unormSrgb,
334 other => other,
335 };
336 let config = GpuCanvasConfiguration::new(&device, canvas_format);
337 config.set_usage(texture_usage::RENDER_ATTACHMENT | texture_usage::COPY_SRC | texture_usage::COPY_DST);
338 config.set_view_formats(&[js_sys::JsString::from(JsValue::from(view_format))]);
339 config.set_alpha_mode(GpuCanvasAlphaMode::Premultiplied);
340 context.configure(&config)?;
341
342 // The 2D pipeline: shader2d with its parameter block as a uniform.
343 let layout_0 = device.create_bind_group_layout(&GpuBindGroupLayoutDescriptor::new(&[
344 texture_entry(0),
345 sampler_entry(1),
346 uniform_entry(2, WINDOW_INFO_BYTES as u32, false),
347 uniform_entry(3, PLATE_FEATURES_BINDING_BYTES as u32, false),
348 ]))?;
349 let layout_1 = device.create_bind_group_layout(&GpuBindGroupLayoutDescriptor::new(&[uniform_entry(
350 0,
351 (PUSH_CONSTANT_FLOATS * 4) as u32,
352 true,
353 )]))?;
354 let layout_2d = device.create_pipeline_layout(&GpuPipelineLayoutDescriptor::new(&[
355 js_sys::JsOption::wrap(layout_0.clone()),
356 js_sys::JsOption::wrap(layout_1.clone()),
357 ]));
358 let module_2d = shader_module(&device, &shader2d_for_webgpu(), "shader2d");
359 let attrs_2d = [
360 GpuVertexAttribute::new(GpuVertexFormat::Float32x2, 0, 0),
361 GpuVertexAttribute::new(GpuVertexFormat::Float32x4, 8, 1),
362 GpuVertexAttribute::new(GpuVertexFormat::Float32x3, 24, 2),
363 ];
364 let vertex_2d = GpuVertexState::new(&module_2d);
365 vertex_2d.set_entry_point("vs_main");
366 vertex_2d.set_buffers(&[js_sys::JsOption::wrap(GpuVertexBufferLayout::new(
367 std::mem::size_of::<Vertex>() as u32,
368 &attrs_2d,
369 ))]);
370 let target = GpuColorTargetState::new(view_format);
371 target.set_blend(&alpha_blending());
372 let targets = [js_sys::JsOption::wrap(target)];
373 let fragment_2d = GpuFragmentState::new(&module_2d, &targets);
374 fragment_2d.set_entry_point("fs_main");
375 let primitive = GpuPrimitiveState::new();
376 primitive.set_topology(GpuPrimitiveTopology::TriangleList);
377 let desc_2d = GpuRenderPipelineDescriptor::new(&layout_2d, &vertex_2d);
378 desc_2d.set_fragment(&fragment_2d);
379 desc_2d.set_primitive(&primitive);
380 desc_2d.set_label("2d");
381 let pipeline_2d = device.create_render_pipeline(&desc_2d)?;
382
383 // Text and images: glyph.wgsl, one (texture, sampler) group each.
384 let glyph_layout = device.create_bind_group_layout(&GpuBindGroupLayoutDescriptor::new(&[
385 texture_entry(0),
386 sampler_entry(1),
387 ]))?;
388 let layout_glyph =
389 device.create_pipeline_layout(&GpuPipelineLayoutDescriptor::new(&[js_sys::JsOption::wrap(glyph_layout.clone())]));
390 let module_glyph = shader_module(&device, GLYPH, "glyph");
391 let attrs_glyph = [
392 GpuVertexAttribute::new(GpuVertexFormat::Float32x2, 0, 0),
393 GpuVertexAttribute::new(GpuVertexFormat::Float32x2, 8, 1),
394 GpuVertexAttribute::new(GpuVertexFormat::Float32x4, 16, 2),
395 GpuVertexAttribute::new(GpuVertexFormat::Float32x3, 32, 3),
396 GpuVertexAttribute::new(GpuVertexFormat::Float32x2, 44, 4),
397 ];
398 let vertex_glyph = GpuVertexState::new(&module_glyph);
399 vertex_glyph.set_entry_point("vs_main");
400 vertex_glyph.set_buffers(&[js_sys::JsOption::wrap(GpuVertexBufferLayout::new(
401 std::mem::size_of::<GlyphVertex>() as u32,
402 &attrs_glyph,
403 ))]);
404 let target = GpuColorTargetState::new(view_format);
405 target.set_blend(&alpha_blending());
406 let targets = [js_sys::JsOption::wrap(target)];
407 let fragment_glyph = GpuFragmentState::new(&module_glyph, &targets);
408 fragment_glyph.set_entry_point("fs_main");
409 let desc_glyph = GpuRenderPipelineDescriptor::new(&layout_glyph, &vertex_glyph);
410 desc_glyph.set_fragment(&fragment_glyph);
411 desc_glyph.set_primitive(&primitive);
412 desc_glyph.set_label("glyph");
413 let glyph_pipeline = device.create_render_pipeline(&desc_glyph)?;
414
415 let uniform_desc = |size: usize, label: &str| {
416 let d = GpuBufferDescriptor::new(size as u32, buffer_usage::UNIFORM | buffer_usage::COPY_DST);
417 d.set_label(label);
418 d
419 };
420 let window_info = device.create_buffer(&uniform_desc(WINDOW_INFO_BYTES, "window-info"))?;
421 let plate_features = device.create_buffer(&uniform_desc(PLATE_FEATURES_BINDING_BYTES, "plate-features"))?;
422
423 // Linear, clamp-to-edge, as the Vulkan backdrop sampler.
424 let backdrop_sampler = sampler(&device, GpuFilterMode::Linear, GpuMipmapFilterMode::Nearest);
425 let empty = texture(&device, view_format, 1, 1, texture_usage::TEXTURE_BINDING | texture_usage::COPY_DST, "backdrop")?;
426 write_texture(&queue, &empty, &[0, 0, 0, 0], 1, 1)?;
427 let group_0 = Self::group_0(&device, &layout_0, &whole_view(&empty)?, &backdrop_sampler, &window_info, &plate_features);
428
429 let blocks = Growable::new(&device, (2 * WEBGPU_BLOCK_STRIDE) as u32, buffer_usage::UNIFORM, "batch-blocks")?;
430 let group_1 = Self::group_1(&device, &layout_1, &blocks.buffer);
431 let vertices = Growable::new(&device, 64 * 1024, buffer_usage::VERTEX, "vertices")?;
432
433 // The atlas: RGBA8 unorm, sampled nearest (as the Vulkan atlas).
434 let atlas_texture = texture(
435 &device,
436 GpuTextureFormat::Rgba8unorm,
437 ATLAS_SIZE,
438 ATLAS_SIZE,
439 texture_usage::TEXTURE_BINDING | texture_usage::COPY_DST,
440 "glyph-atlas",
441 )?;
442 let atlas_sampler = sampler(&device, GpuFilterMode::Nearest, GpuMipmapFilterMode::Nearest);
443 let atlas_group = texture_group(&device, &glyph_layout, &whole_view(&atlas_texture)?, &atlas_sampler);
444 let scene = WebScene::new(&device, view_format, &glyph_layout)?;
445 let glyph_vertices = Growable::new(&device, 64 * 1024, buffer_usage::VERTEX, "glyph-vertices")?;
446
447 // Images: sRGB, linear (as the Vulkan image stage).
448 let image_sampler = sampler(&device, GpuFilterMode::Linear, GpuMipmapFilterMode::Linear);
449 let image_vertices = Growable::new(&device, 4 * 1024, buffer_usage::VERTEX, "image-quads")?;
450 // This renderer's image table: the ids queued before it existed are
451 // drained into it, as a Vulkan renderer's are.
452 image_table_built();
453
454 Ok(Self {
455 _gpu: gpu,
456 _adapter: adapter,
457 device,
458 queue,
459 canvas,
460 context,
461 canvas_format,
462 view_format,
463 pipeline_2d,
464 layout_0,
465 layout_1,
466 window_info,
467 plate_features,
468 backdrop_sampler,
469 group_0,
470 snapshot: None,
471 scene,
472 scene_group_0: None,
473 rt: None,
474 rt_background: None,
475 rt_environment: RtEnvironment::default(),
476 blocks,
477 group_1,
478 vertices,
479 glyph_pipeline,
480 glyph_layout,
481 atlas: GlyphAtlas::new(),
482 atlas_texture,
483 atlas_group,
484 atlas_uploaded: 0,
485 glyph_vertices,
486 image_sampler,
487 images: HashMap::new(),
488 image_vertices,
489 capture: None,
490 capture_requested: false,
491 })
492 }
493
494 fn group_0(
495 device: &GpuDevice,
496 layout: &GpuBindGroupLayout,
497 backdrop: &GpuTextureView,
498 sampler: &GpuSampler,
499 window_info: &GpuBuffer,
500 plate_features: &GpuBuffer,
501 ) -> GpuBindGroup {
502 let entries = [
503 GpuBindGroupEntry::new_with_gpu_texture_view(0, backdrop),
504 GpuBindGroupEntry::new(1, sampler),
505 GpuBindGroupEntry::new_with_gpu_buffer_binding(2, &uniform_binding(window_info, WINDOW_INFO_BYTES as u32)),
506 GpuBindGroupEntry::new_with_gpu_buffer_binding(
507 3,
508 &uniform_binding(plate_features, PLATE_FEATURES_BINDING_BYTES as u32),
509 ),
510 ];
511 device.create_bind_group(&GpuBindGroupDescriptor::new(&entries, layout))
512 }
513
514 fn group_1(device: &GpuDevice, layout: &GpuBindGroupLayout, blocks: &GpuBuffer) -> GpuBindGroup {
515 let entries = [GpuBindGroupEntry::new_with_gpu_buffer_binding(
516 0,
517 &uniform_binding(blocks, (PUSH_CONSTANT_FLOATS * 4) as u32),
518 )];
519 device.create_bind_group(&GpuBindGroupDescriptor::new(&entries, layout))
520 }
521
522 /// The drawing buffer's size in physical px.
523 pub fn size(&self) -> (u32, u32) {
524 (self.canvas.width(), self.canvas.height())
525 }
526
527 /// Size the drawing buffer (physical px). The page sizes the element.
528 pub fn resize(&mut self, width: u32, height: u32) {
529 self.canvas.set_width(width.max(1));
530 self.canvas.set_height(height.max(1));
531 }
532
533 /// Shape this frame's text into glyph quads (see [`GlyphAtlas::prepare`]),
534 /// against the drawing buffer's current size.
535 pub fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem, swash: &mut cosmic_text::SwashCache, spans: &[TextSpan<'_>]) {
536 let (w, h) = self.size();
537 self.atlas.prepare(fs, swash, spans, w, h);
538 }
539
540 /// Copy the next frame drawn into a buffer [`take_capture`](Self::take_capture) reads.
541 pub fn capture_next_frame(&mut self) {
542 self.capture_requested = true;
543 }
544
545 /// The captured frame, once the GPU has finished it.
546 pub async fn take_capture(&mut self) -> Result<Option<Capture>, JsValue> {
547 match self.take_pending_capture() {
548 Some(pending) => pending.read().await.map(Some),
549 None => Ok(None),
550 }
551 }
552
553 /// The captured frame's buffer, to be read without holding the renderer
554 /// across the wait (the browser shell reads it after the turn that drew it).
555 pub fn take_pending_capture(&mut self) -> Option<PendingCapture> {
556 let (buffer, width, height, row) = self.capture.take()?;
557 Some(PendingCapture { buffer, width, height, row, bgra: matches!(self.canvas_format, GpuTextureFormat::Bgra8unorm) })
558 }
559
560 /// Apply the image queue: uploads, in-place updates, frees.
561 fn process_images(&mut self) -> Result<(), JsValue> {
562 for pending in take_pending() {
563 match pending {
564 Pending::Upload { id, pixels, width, height, format, mips: _ }
565 | Pending::Update { id, pixels, width, height, format } => {
566 let same = self
567 .images
568 .get(&id)
569 .is_some_and(|img| img.width == width && img.height == height && img.format == format);
570 if !same {
571 if let Some(old) = self.images.remove(&id) {
572 old.texture.destroy();
573 }
574 let tex_format = match format {
575 PixelFormat::Rgba => GpuTextureFormat::Rgba8unormSrgb,
576 PixelFormat::Bgra => GpuTextureFormat::Bgra8unormSrgb,
577 };
578 let texture = texture(
579 &self.device,
580 tex_format,
581 width,
582 height,
583 texture_usage::TEXTURE_BINDING | texture_usage::COPY_DST,
584 "image",
585 )?;
586 let group = texture_group(&self.device, &self.glyph_layout, &whole_view(&texture)?, &self.image_sampler);
587 self.images.insert(id, WebImage { texture, group, width, height, format });
588 }
589 let img = &self.images[&id];
590 write_texture(&self.queue, &img.texture, &pixels, width, height)?;
591 retire_buffer(pixels);
592 }
593 Pending::UpdateRegions { id, pixels, width, height, format, regions } => {
594 // Only into the picture the regions were cut from; see
595 // `update_pixel_regions` for why a mismatch writes nothing.
596 if let Some(img) = self
597 .images
598 .get(&id)
599 .filter(|img| img.width == width && img.height == height && img.format == format)
600 {
601 let mut offset = 0usize;
602 for ®ion in ®ions {
603 let len = (region.2 * region.3 * 4) as usize;
604 write_texture_region(&self.queue, &img.texture, &pixels[offset..offset + len], region)?;
605 offset += len;
606 }
607 }
608 retire_buffer(pixels);
609 }
610 Pending::Free { id } => {
611 if let Some(old) = self.images.remove(&id) {
612 old.texture.destroy();
613 }
614 }
615 }
616 }
617 Ok(())
618 }
619
620 /// The blur snapshot texture and its `@group(0)`, sized to the target.
621 fn snapshot_group(&mut self, w: u32, h: u32) -> Result<(GpuTexture, GpuBindGroup), JsValue> {
622 if let Some((tex, group, sw, sh)) = &self.snapshot {
623 if *sw == w && *sh == h {
624 return Ok((tex.clone(), group.clone()));
625 }
626 tex.destroy();
627 }
628 let tex = texture(&self.device, self.view_format, w, h, texture_usage::TEXTURE_BINDING | texture_usage::COPY_DST, "blur-snapshot")?;
629 let group = Self::group_0(&self.device, &self.layout_0, &whole_view(&tex)?, &self.backdrop_sampler, &self.window_info, &self.plate_features);
630 self.snapshot = Some((tex.clone(), group.clone(), w, h));
631 Ok((tex, group))
632 }
633
634 fn begin_pass(encoder: &GpuCommandEncoder, view: &GpuTextureView, clear: Option<[f32; 4]>) -> Result<GpuRenderPassEncoder, JsValue> {
635 let attachment = match clear {
636 Some(c) => {
637 let a = GpuRenderPassColorAttachment::new_with_gpu_texture_view(GpuLoadOp::Clear, GpuStoreOp::Store, view);
638 a.set_clear_value(&[
639 js_sys::Number::from(c[0] as f64),
640 js_sys::Number::from(c[1] as f64),
641 js_sys::Number::from(c[2] as f64),
642 js_sys::Number::from(c[3] as f64),
643 ]);
644 a
645 }
646 None => GpuRenderPassColorAttachment::new_with_gpu_texture_view(GpuLoadOp::Load, GpuStoreOp::Store, view),
647 };
648 encoder.begin_render_pass(&GpuRenderPassDescriptor::new(&[js_sys::JsOption::wrap(attachment)]))
649 }
650
651 /// Draw one frame into the canvas. The browser presents it when the task
652 /// that called this returns.
653 pub fn draw_frame_2d(&mut self, frame: Frame2D<'_>) -> Result<(), JsValue> {
654 self.process_images()?;
655 let target = self.context.get_current_texture()?;
656 let (w, h) = (target.width(), target.height());
657 // The scene's backdrop follows the canvas; a resized one holds no
658 // scene until the next is drawn into it, as on Vulkan.
659 let rt_staged = self.rt.as_ref().is_some_and(|rt| rt.staged());
660 if self.scene.has_staged() || rt_staged || self.scene.target.is_some() {
661 let had = self.scene.target.as_ref().map(|t| (t.width, t.height));
662 self.scene.fit(&self.device, w, h)?;
663 if had != Some((w, h)) {
664 let t = self.scene.target.as_ref().expect("fit made one");
665 self.scene_group_0 = Some(Self::group_0(
666 &self.device,
667 &self.layout_0,
668 &t.view,
669 &self.backdrop_sampler,
670 &self.window_info,
671 &self.plate_features,
672 ));
673 }
674 }
675 let view_desc = GpuTextureViewDescriptor::new();
676 view_desc.set_format(self.view_format);
677 let view = target.create_view_with_descriptor(&view_desc)?;
678
679 // The frame's uniforms and vertices.
680 let info = window_info_data(w, h, 0.0, relief_px_at(crate::scale::scale_factor()));
681 self.queue.write_buffer_with_u32_and_u8_slice(&self.window_info, 0, bytemuck::cast_slice(&info))?;
682 if !frame.plate_features.is_empty() {
683 let n = frame.plate_features.len().min(MAX_PLATE_FEATURES);
684 self.queue.write_buffer_with_u32_and_u8_slice(
685 &self.plate_features,
686 0,
687 bytemuck::cast_slice(&frame.plate_features[..n]),
688 )?;
689 }
690 let vert_bytes: &[u8] = bytemuck::cast_slice(frame.verts);
691 let overlay_bytes: &[u8] = bytemuck::cast_slice(frame.overlay_verts);
692 let mut all = Vec::with_capacity(vert_bytes.len() + overlay_bytes.len());
693 all.extend_from_slice(vert_bytes);
694 all.extend_from_slice(overlay_bytes);
695 self.vertices.ensure(&self.device, all.len() as u32)?;
696 self.vertices.write(&self.queue, &all)?;
697 let vertex_count = frame.verts.len() as u32;
698 let overlay_count = frame.overlay_verts.len() as u32;
699
700 let default_batch = [Batch2D { scissor: None, clip_rrect: None, start: 0, end: vertex_count, plate: None, blur_behind: false }];
701 let batches: &[Batch2D] = if frame.batches.is_empty() { &default_batch } else { frame.batches };
702 // Corner-shape exponent for the rounded-rect clip SDF (see the Vulkan renderer).
703 let clip_shape = crate::layout::corner_shape();
704 // One block per batch, then the overlay's zero block.
705 let block_floats = WEBGPU_BLOCK_STRIDE / 4;
706 let mut blocks = vec![0.0f32; (batches.len() + 1) * block_floats];
707 for (i, batch) in batches.iter().enumerate() {
708 blocks[i * block_floats..i * block_floats + PUSH_CONSTANT_FLOATS]
709 .copy_from_slice(&batch_push_constants(batch, clip_shape, 0));
710 }
711 let overlay_block = batches.len() as u32;
712 if self.blocks.ensure(&self.device, (blocks.len() * 4) as u32)? {
713 self.group_1 = Self::group_1(&self.device, &self.layout_1, &self.blocks.buffer);
714 }
715 self.blocks.write(&self.queue, bytemuck::cast_slice(&blocks))?;
716
717 if self.atlas_uploaded != self.atlas.generation() {
718 write_texture(&self.queue, &self.atlas_texture, self.atlas.pixels(), ATLAS_SIZE, ATLAS_SIZE)?;
719 self.atlas_uploaded = self.atlas.generation();
720 }
721 let glyph_bytes: &[u8] = bytemuck::cast_slice(self.atlas.vertices());
722 self.glyph_vertices.ensure(&self.device, glyph_bytes.len() as u32)?;
723 self.glyph_vertices.write(&self.queue, glyph_bytes)?;
724 let image_verts = image_quad_vertices(frame.images, w, h);
725 let image_bytes: &[u8] = bytemuck::cast_slice(&image_verts);
726 self.image_vertices.ensure(&self.device, image_bytes.len() as u32)?;
727 self.image_vertices.write(&self.queue, image_bytes)?;
728
729 // The blur snapshot, made (or resized) before recording when any
730 // batch needs one.
731 let snapshot = if batches.iter().any(|b| b.blur_behind) { Some(self.snapshot_group(w, h)?) } else { None };
732
733 // Record. The 3D pass first, into the backdrop; with a scene shown,
734 // the backdrop is copied into the canvas and the UI pass loads it
735 // (rather than clearing) and samples it for its blur plates.
736 let encoder = self.device.create_command_encoder();
737 let images_for_scene = &self.images;
738 self.scene.record(&self.device, &self.queue, &encoder, &|id| images_for_scene.get(&id).map(|i| i.group.clone()))?;
739 // The traced pane, into the same backdrop after the raster scene.
740 if let (Some(rt), Some(t)) = (self.rt.as_mut(), self.scene.target.as_ref()) {
741 let image_view = |id: u32| {
742 let img = images_for_scene.get(&id)?;
743 Some((img.texture.create_view().ok()?, img.width, img.height))
744 };
745 if rt.record(&self.device, &self.queue, &encoder, &t.view, (t.width, t.height), &image_view)? {
746 self.scene.backdrop_valid = true;
747 }
748 }
749 let base_group_0 = match (&self.scene_group_0, &self.scene.target) {
750 (Some(group), Some(t)) if self.scene.backdrop_valid => {
751 encoder.copy_texture_to_texture_with_gpu_extent_3d_dict(
752 &GpuTexelCopyTextureInfo::new(&t.backdrop),
753 &GpuTexelCopyTextureInfo::new(&target),
754 &extent(w, h),
755 )?;
756 Some(group.clone())
757 }
758 _ => None,
759 };
760 let mut pass = Self::begin_pass(&encoder, &view, if base_group_0.is_some() { None } else { Some(frame.clear_color) })?;
761 let base_group_0 = base_group_0.unwrap_or_else(|| self.group_0.clone());
762 let clamp_scissor = |pass: &GpuRenderPassEncoder, (x, y, sw, sh): (u32, u32, u32, u32)| {
763 let x = x.min(w);
764 let y = y.min(h);
765 pass.set_scissor_rect(x, y, sw.min(w - x), sh.min(h - y));
766 };
767 let images = frame.images;
768 let mut order: Vec<usize> = (0..images.len()).collect();
769 order.sort_by_key(|&k| images[k].z_before);
770 let mut img_i = 0usize;
771 let draw_image = |pass: &GpuRenderPassEncoder, k: usize| {
772 let q = &images[k];
773 let Some(img) = self.images.get(&q.image) else { return }; // not landed / freed: skipped, as on Vulkan
774 clamp_scissor(pass, q.clip.unwrap_or((0, 0, w, h)));
775 pass.set_pipeline(&self.glyph_pipeline);
776 pass.set_bind_group(0, Some(&img.group));
777 pass.set_vertex_buffer_with_u32(0, Some(&self.image_vertices.buffer), 0);
778 pass.draw_with_instance_count_and_first_vertex(6, 1, (k * 6) as u32);
779 };
780
781 // The `@group(0)` vertex draws bind: the empty backdrop until the
782 // first blur snapshot, the snapshot after. Consecutive blur plates
783 // share one snapshot; only a non-blur draw invalidates it.
784 let mut active_group_0 = base_group_0.clone();
785 let mut snapshot_fresh = false;
786
787 for (bi, batch) in batches.iter().enumerate() {
788 // Images due at this batch's boundary draw first (beneath its
789 // geometry, and inside a snapshot taken for it).
790 while let Some(&k) = order.get(img_i) {
791 if images[k].z_before > batch.start {
792 break;
793 }
794 img_i += 1;
795 draw_image(&pass, k);
796 snapshot_fresh = false;
797 }
798 if batch.blur_behind {
799 if !snapshot_fresh {
800 let (snap, snap_group) = snapshot.as_ref().expect("made above for a blur batch");
801 pass.end();
802 encoder.copy_texture_to_texture_with_gpu_extent_3d_dict(
803 &GpuTexelCopyTextureInfo::new(&target),
804 &GpuTexelCopyTextureInfo::new(snap),
805 &extent(w, h),
806 )?;
807 pass = Self::begin_pass(&encoder, &view, None)?;
808 active_group_0 = snap_group.clone();
809 snapshot_fresh = true;
810 }
811 } else if batch.start < batch.end {
812 snapshot_fresh = false;
813 }
814 // A degenerate scissor skips the geometry (images keep their own clips).
815 let scissor = match batch.scissor {
816 Some((bx, by, bw, bh)) => {
817 if bx >= w || by >= h || bw.min(w - bx) == 0 || bh.min(h - by) == 0 {
818 None
819 } else {
820 Some((bx, by, bw.min(w - bx), bh.min(h - by)))
821 }
822 }
823 None => Some((0, 0, w, h)),
824 };
825 let mut cursor = batch.start;
826 while cursor < batch.end {
827 let next_z = order.get(img_i).map(|&k| images[k].z_before).unwrap_or(u32::MAX);
828 if next_z <= cursor {
829 let k = order[img_i];
830 img_i += 1;
831 draw_image(&pass, k);
832 continue;
833 }
834 let upto = next_z.min(batch.end);
835 if let Some(s) = scissor {
836 pass.set_pipeline(&self.pipeline_2d);
837 pass.set_bind_group(0, Some(&active_group_0));
838 pass.set_bind_group_with_u32_slice_and_u32_and_dynamic_offsets_data_length(
839 1,
840 Some(&self.group_1),
841 &[(bi * WEBGPU_BLOCK_STRIDE) as u32],
842 0,
843 1,
844 )?;
845 pass.set_vertex_buffer_with_u32(0, Some(&self.vertices.buffer), 0);
846 clamp_scissor(&pass, s);
847 pass.draw_with_instance_count_and_first_vertex(upto - cursor, 1, cursor);
848 }
849 cursor = upto;
850 }
851 }
852 // Images sorting after all geometry.
853 while let Some(&k) = order.get(img_i) {
854 img_i += 1;
855 draw_image(&pass, k);
856 }
857 pass.set_scissor_rect(0, 0, w, h);
858
859 // Text on top of the geometry.
860 let glyph_count = self.atlas.vertices().len() as u32;
861 if glyph_count > 0 {
862 pass.set_pipeline(&self.glyph_pipeline);
863 pass.set_bind_group(0, Some(&self.atlas_group));
864 pass.set_vertex_buffer_with_u32(0, Some(&self.glyph_vertices.buffer), 0);
865 pass.draw_with_instance_count_and_first_vertex(glyph_count, 1, 0);
866 }
867 // Overlays last, with the frame's backdrop and a zero parameter block.
868 if overlay_count > 0 {
869 pass.set_pipeline(&self.pipeline_2d);
870 pass.set_bind_group(0, Some(&base_group_0));
871 pass.set_bind_group_with_u32_slice_and_u32_and_dynamic_offsets_data_length(
872 1,
873 Some(&self.group_1),
874 &[overlay_block * WEBGPU_BLOCK_STRIDE as u32],
875 0,
876 1,
877 )?;
878 pass.set_vertex_buffer_with_u32(0, Some(&self.vertices.buffer), 0);
879 pass.draw_with_instance_count_and_first_vertex(overlay_count, 1, vertex_count);
880 }
881 pass.end();
882
883 if std::mem::take(&mut self.capture_requested) {
884 let row = (w * 4).div_ceil(256) * 256;
885 let desc = GpuBufferDescriptor::new(row * h, buffer_usage::COPY_DST | buffer_usage::MAP_READ);
886 desc.set_label("capture");
887 let buffer = self.device.create_buffer(&desc)?;
888 let dst = GpuTexelCopyBufferInfo::new(&buffer);
889 dst.set_bytes_per_row(row);
890 dst.set_rows_per_image(h);
891 encoder.copy_texture_to_buffer_with_gpu_extent_3d_dict(&GpuTexelCopyTextureInfo::new(&target), &dst, &extent(w, h))?;
892 self.capture = Some((buffer, w, h, row));
893 }
894
895 self.queue.submit(&[encoder.finish()]);
896 Ok(())
897 }
898 }
899
900 /// The 3D half of the renderer (see `draw::scene::Stage3D`): the scene pass
901 /// in `web::scene`.
902 impl Stage3D for WebRenderer {
903 fn create_mesh(&mut self, verts: &[Vertex3D]) -> MeshId {
904 self.scene.create_mesh(&self.device, &self.queue, verts)
905 }
906 fn update_mesh(&mut self, id: MeshId, verts: &[Vertex3D]) {
907 self.scene.update_mesh(&self.device, &self.queue, id, verts)
908 }
909 fn stage_scene(&mut self, scissor: (u32, u32, u32, u32), draws: Vec<SceneDraw>) {
910 self.scene.stage(scissor, draws)
911 }
912 fn stage_scene_images(&mut self, images: Vec<SceneImage>) {
913 self.scene.stage_images(images)
914 }
915 fn set_scene_light(&mut self, toward: [f32; 3]) {
916 let v = glam::Vec3::from_array(toward);
917 if v.length_squared() > 1e-12 {
918 self.scene.light = v.normalize().to_array();
919 }
920 }
921 fn set_rt_scene_prepared(&mut self, scene: &PreparedRtScene) {
922 if self.rt.is_none() {
923 match WebRt::new(&self.device, self.view_format) {
924 Ok(rt) => self.rt = Some(rt),
925 Err(e) => {
926 web_sys::console::error_2(&"cce-ui: no path tracer:".into(), &e);
927 return;
928 }
929 }
930 }
931 let rt = self.rt.as_mut().unwrap();
932 if let Err(e) = rt.set_scene(&self.device, &self.queue, scene) {
933 web_sys::console::error_2(&"cce-ui: the traced scene was not uploaded:".into(), &e);
934 }
935 }
936 fn set_rt_environment(&mut self, environment: RtEnvironment) {
937 self.rt_environment = environment;
938 }
939 fn set_rt_background(&mut self, color: Option<[f32; 3]>) {
940 self.rt_background = color;
941 }
942 fn stage_rt(&mut self, pane: (u32, u32, u32, u32), camera: RtCamera) {
943 if let Some(rt) = self.rt.as_mut() {
944 rt.set_background(self.rt_background);
945 rt.set_environment(self.rt_environment);
946 if let Err(e) = rt.stage(&self.device, pane, camera) {
947 web_sys::console::error_2(&"cce-ui: the traced pane was not staged:".into(), &e);
948 }
949 }
950 }
951 fn rt_accumulating(&self) -> bool {
952 self.rt.as_ref().is_some_and(|rt| rt.accumulating())
953 }
954 }