git.lucas.co / cce-ui
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 &region in &regions {
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 }