git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/web/scene.rs (17.7K)

  1 //! The 3D scene pass on WebGPU: the Vulkan `SceneStage`'s port, drawing the
  2 //! same staged [`SceneDraw`]s and [`SceneImage`]s from the same shaders
  3 //! (`draw::shaders::SCENE3D` / `SCENE3D_IMAGE`) and uniform blocks
  4 //! (`draw::scene::scene_uniforms`) into a full-size backdrop texture with a
  5 //! depth buffer, which the renderer copies beneath the UI pass and its blur
  6 //! plates sample — the Vulkan renderer's arrangement, step for step.
  7 //!
  8 //! What WebGPU makes different:
  9 //!
 10 //! - **Depth bias is pipeline state**, where Vulkan sets it per draw. It
 11 //!   takes one value here — a WebGPU line is one pixel wide (no wideLines),
 12 //!   so a fill carrying wires is pushed back by `wire_base_bias(1.0)`, as a
 13 //!   Vulkan device without wideLines pushes it — so the biased fill is a
 14 //!   pipeline of its own: four fills (opaque or see-through, biased or not),
 15 //!   two line pipelines (see-through wires write depth), the image pipeline.
 16 //! - **No Y flip.** The shaders were written for WebGPU's conventions (the
 17 //!   Vulkan renderer flips its viewport to match), so the viewport is the
 18 //!   plain one and counter-clockwise is front, as there.
 19 //! - **The backdrop is the canvas's sRGB view format**, copied into the
 20 //!   canvas texture (formats that differ only in sRGB-ness copy).
 21 
 22 use wasm_bindgen::JsValue;
 23 use web_sys::{
 24     gpu_buffer_usage as buffer_usage, gpu_shader_stage as shader_stage, gpu_texture_usage as texture_usage, GpuBindGroup,
 25     GpuBindGroupDescriptor, GpuBindGroupEntry, GpuBindGroupLayout, GpuBindGroupLayoutDescriptor, GpuBindGroupLayoutEntry,
 26     GpuBufferBinding, GpuBufferBindingLayout, GpuBufferBindingType, GpuColorTargetState, GpuCommandEncoder, GpuCompareFunction,
 27     GpuCullMode, GpuDepthStencilState, GpuDevice, GpuFragmentState, GpuFrontFace, GpuLoadOp, GpuPipelineLayoutDescriptor,
 28     GpuPrimitiveState, GpuPrimitiveTopology, GpuQueue, GpuRenderPassColorAttachment, GpuRenderPassDepthStencilAttachment,
 29     GpuRenderPassDescriptor, GpuRenderPipeline, GpuRenderPipelineDescriptor, GpuStoreOp, GpuTexture, GpuTextureFormat,
 30     GpuTextureView, GpuVertexAttribute, GpuVertexBufferLayout, GpuVertexFormat, GpuVertexState, GpuVertexStepMode,
 31 };
 32 
 33 use super::renderer::{alpha_blending, shader_module, texture, whole_view, Growable};
 34 use crate::draw::scene::{
 35     image_quads_3d, scene_uniforms, wire_base_bias, ImageVertex3D, MeshId, SceneDraw, SceneImage, SceneUniforms, Vertex3D,
 36     DEFAULT_SCENE_LIGHT, UNIT_INSTANCE,
 37 };
 38 use crate::draw::shaders::{SCENE3D, SCENE3D_IMAGE};
 39 
 40 /// One draw's uniform block sits at a multiple of this (the alignment every
 41 /// WebGPU device takes for a dynamic offset).
 42 const STRIDE: u32 = 256;
 43 const UNIFORM_SIZE: u32 = std::mem::size_of::<SceneUniforms>() as u32;
 44 const DEPTH: GpuTextureFormat = GpuTextureFormat::Depth32float;
 45 
 46 struct Mesh {
 47     buffer: Growable,
 48     count: u32,
 49 }
 50 
 51 struct Staged {
 52     scissor: (u32, u32, u32, u32),
 53     draws: Vec<SceneDraw>,
 54     images: Vec<SceneImage>,
 55 }
 56 
 57 /// The backdrop and depth textures, at the canvas's size.
 58 pub(crate) struct Target {
 59     pub backdrop: GpuTexture,
 60     pub view: GpuTextureView,
 61     depth_view: GpuTextureView,
 62     pub width: u32,
 63     pub height: u32,
 64 }
 65 
 66 pub(crate) struct WebScene {
 67     format: GpuTextureFormat,
 68     uniform_layout: GpuBindGroupLayout,
 69     /// [opaque, opaque biased, see-through, see-through biased].
 70     fills: [GpuRenderPipeline; 4],
 71     /// [no depth writes, depth writes (the wires of a see-through fill)].
 72     lines: [GpuRenderPipeline; 2],
 73     image: GpuRenderPipeline,
 74     uniforms: Growable,
 75     uniform_group: GpuBindGroup,
 76     image_verts: Growable,
 77     /// The one instance a draw without instances is drawn with
 78     /// ([`UNIT_INSTANCE`]), in slot 1 in place of an instance mesh; written
 79     /// at every record, being 24 bytes and `new` having no queue.
 80     unit_instance: Growable,
 81     meshes: Vec<Mesh>,
 82     staged: Option<Staged>,
 83     pub(crate) light: [f32; 3],
 84     pub(crate) target: Option<Target>,
 85     /// The backdrop holds a rendered scene worth showing under the UI.
 86     pub(crate) backdrop_valid: bool,
 87 }
 88 
 89 fn uniform_group(device: &GpuDevice, layout: &GpuBindGroupLayout, uniforms: &Growable) -> GpuBindGroup {
 90     let binding = GpuBufferBinding::new(&uniforms.buffer);
 91     binding.set_size(UNIFORM_SIZE);
 92     let entries = [GpuBindGroupEntry::new_with_gpu_buffer_binding(0, &binding)];
 93     device.create_bind_group(&GpuBindGroupDescriptor::new(&entries, layout))
 94 }
 95 
 96 impl WebScene {
 97     /// The pass's pipelines, drawing into `format` (the canvas's sRGB view
 98     /// format); `image_layout` is the renderer's (texture, sampler) layout,
 99     /// so a scene image draws with the group the 2D pass draws it with.
100     pub(crate) fn new(device: &GpuDevice, format: GpuTextureFormat, image_layout: &GpuBindGroupLayout) -> Result<Self, JsValue> {
101         let entry = GpuBindGroupLayoutEntry::new(0, shader_stage::VERTEX | shader_stage::FRAGMENT);
102         let buffer = GpuBufferBindingLayout::new();
103         buffer.set_type(GpuBufferBindingType::Uniform);
104         buffer.set_has_dynamic_offset(true);
105         buffer.set_min_binding_size(UNIFORM_SIZE);
106         entry.set_buffer(&buffer);
107         let uniform_layout = device.create_bind_group_layout(&GpuBindGroupLayoutDescriptor::new(&[entry]))?;
108         let mesh_layout = device.create_pipeline_layout(&GpuPipelineLayoutDescriptor::new(&[js_sys::JsOption::wrap(uniform_layout.clone())]));
109         let image_pipeline_layout = device.create_pipeline_layout(&GpuPipelineLayoutDescriptor::new(&[
110             js_sys::JsOption::wrap(uniform_layout.clone()),
111             js_sys::JsOption::wrap(image_layout.clone()),
112         ]));
113 
114         let module = shader_module(device, SCENE3D, "scene3d");
115         let mesh_attrs = [
116             GpuVertexAttribute::new(GpuVertexFormat::Float32x3, 0, 0),
117             GpuVertexAttribute::new(GpuVertexFormat::Float32x3, 12, 1),
118         ];
119         // Slot 1: the instances a mesh is drawn for (`SceneDraw::instances`),
120         // `Vertex3D`s read as an offset and a colour multiplier.
121         let instance_attrs = [
122             GpuVertexAttribute::new(GpuVertexFormat::Float32x3, 0, 2),
123             GpuVertexAttribute::new(GpuVertexFormat::Float32x3, 12, 3),
124         ];
125         let instance_layout = GpuVertexBufferLayout::new(std::mem::size_of::<Vertex3D>() as u32, &instance_attrs);
126         instance_layout.set_step_mode(GpuVertexStepMode::Instance);
127         let mesh_buffers = [
128             js_sys::JsOption::wrap(GpuVertexBufferLayout::new(std::mem::size_of::<Vertex3D>() as u32, &mesh_attrs)),
129             js_sys::JsOption::wrap(instance_layout),
130         ];
131         let target = GpuColorTargetState::new(format);
132         target.set_blend(&alpha_blending());
133         let targets = [js_sys::JsOption::wrap(target)];
134 
135         // One pipeline of the pass: its topology, culling, depth writes and
136         // compare, and the slope-scaled bias (constant, slope) it carries.
137         let pipeline = |layout: &web_sys::GpuPipelineLayout,
138                         module: &web_sys::GpuShaderModule,
139                         buffers: &[js_sys::JsOption<GpuVertexBufferLayout>],
140                         topology: GpuPrimitiveTopology,
141                         cull: GpuCullMode,
142                         write: bool,
143                         compare: GpuCompareFunction,
144                         bias: (f32, f32),
145                         label: &str|
146          -> Result<GpuRenderPipeline, JsValue> {
147             let vertex = GpuVertexState::new(module);
148             vertex.set_entry_point("vs_main");
149             vertex.set_buffers(buffers);
150             let fragment = GpuFragmentState::new(module, &targets);
151             fragment.set_entry_point("fs_main");
152             let primitive = GpuPrimitiveState::new();
153             primitive.set_topology(topology);
154             primitive.set_cull_mode(cull);
155             primitive.set_front_face(GpuFrontFace::Ccw);
156             let depth = GpuDepthStencilState::new(DEPTH);
157             depth.set_depth_write_enabled(write);
158             depth.set_depth_compare(compare);
159             depth.set_depth_bias(bias.0 as i32);
160             depth.set_depth_bias_slope_scale(bias.1);
161             let desc = GpuRenderPipelineDescriptor::new(layout, &vertex);
162             desc.set_fragment(&fragment);
163             desc.set_primitive(&primitive);
164             desc.set_depth_stencil(&depth);
165             desc.set_label(label);
166             device.create_render_pipeline(&desc)
167         };
168         use GpuCompareFunction::{Less, LessEqual};
169         use GpuPrimitiveTopology::{LineList, TriangleList};
170         let none = (0.0, 0.0);
171         let biased = wire_base_bias(1.0);
172         let fills = [
173             pipeline(&mesh_layout, &module, &mesh_buffers, TriangleList, GpuCullMode::Back, true, Less, none, "scene-fill")?,
174             pipeline(&mesh_layout, &module, &mesh_buffers, TriangleList, GpuCullMode::Back, true, Less, biased, "scene-fill-biased")?,
175             pipeline(&mesh_layout, &module, &mesh_buffers, TriangleList, GpuCullMode::None, false, Less, none, "scene-see-through")?,
176             pipeline(&mesh_layout, &module, &mesh_buffers, TriangleList, GpuCullMode::None, false, Less, biased, "scene-see-through-biased")?,
177         ];
178         let lines = [
179             pipeline(&mesh_layout, &module, &mesh_buffers, LineList, GpuCullMode::None, false, LessEqual, none, "scene-wires")?,
180             pipeline(&mesh_layout, &module, &mesh_buffers, LineList, GpuCullMode::None, true, LessEqual, none, "scene-wires-see-through")?,
181         ];
182         let image_module = shader_module(device, SCENE3D_IMAGE, "scene3d-image");
183         let image_attrs = [
184             GpuVertexAttribute::new(GpuVertexFormat::Float32x3, 0, 0),
185             GpuVertexAttribute::new(GpuVertexFormat::Float32x2, 12, 1),
186         ];
187         let image_buffers =
188             [js_sys::JsOption::wrap(GpuVertexBufferLayout::new(std::mem::size_of::<ImageVertex3D>() as u32, &image_attrs))];
189         let image = pipeline(&image_pipeline_layout, &image_module, &image_buffers, TriangleList, GpuCullMode::None, true, Less, none, "scene-image")?;
190 
191         let uniforms = Growable::new(device, STRIDE * 16, buffer_usage::UNIFORM, "scene-uniforms")?;
192         let uniform_group = uniform_group(device, &uniform_layout, &uniforms);
193         let image_verts = Growable::new(device, 1024, buffer_usage::VERTEX, "scene-image-quads")?;
194         let unit_instance = Growable::new(device, 64, buffer_usage::VERTEX, "scene-unit-instance")?;
195         let l = glam::Vec3::from_array(DEFAULT_SCENE_LIGHT).normalize().to_array();
196         Ok(Self {
197             format,
198             uniform_layout,
199             fills,
200             lines,
201             image,
202             uniforms,
203             uniform_group,
204             image_verts,
205             unit_instance,
206             meshes: Vec::new(),
207             staged: None,
208             light: l,
209             target: None,
210             backdrop_valid: false,
211         })
212     }
213 
214     pub(crate) fn create_mesh(&mut self, device: &GpuDevice, queue: &GpuQueue, verts: &[Vertex3D]) -> MeshId {
215         let bytes: &[u8] = bytemuck::cast_slice(verts);
216         let buffer = Growable::new(device, (bytes.len() as u32).max(64), buffer_usage::VERTEX, "mesh")
217             .expect("a vertex buffer the device can make");
218         let _ = buffer.write(queue, bytes);
219         self.meshes.push(Mesh { buffer, count: verts.len() as u32 });
220         MeshId(self.meshes.len() - 1)
221     }
222 
223     pub(crate) fn update_mesh(&mut self, device: &GpuDevice, queue: &GpuQueue, id: MeshId, verts: &[Vertex3D]) {
224         let Some(mesh) = self.meshes.get_mut(id.0) else { return };
225         let bytes: &[u8] = bytemuck::cast_slice(verts);
226         if mesh.buffer.ensure(device, bytes.len() as u32).is_err() {
227             return;
228         }
229         let _ = mesh.buffer.write(queue, bytes);
230         mesh.count = verts.len() as u32;
231     }
232 
233     pub(crate) fn stage(&mut self, scissor: (u32, u32, u32, u32), draws: Vec<SceneDraw>) {
234         self.staged = Some(Staged { scissor, draws, images: Vec::new() });
235     }
236 
237     pub(crate) fn has_staged(&self) -> bool {
238         self.staged.is_some()
239     }
240 
241     pub(crate) fn stage_images(&mut self, images: Vec<SceneImage>) {
242         if let Some(staged) = &mut self.staged {
243             staged.images = images;
244         }
245     }
246 
247     /// Size the backdrop and depth to the canvas. A new backdrop holds no
248     /// scene: `backdrop_valid` falls until a scene is drawn into it.
249     pub(crate) fn fit(&mut self, device: &GpuDevice, w: u32, h: u32) -> Result<(), JsValue> {
250         if self.target.as_ref().is_some_and(|t| (t.width, t.height) == (w, h)) {
251             return Ok(());
252         }
253         let backdrop = texture(
254             device,
255             self.format,
256             w,
257             h,
258             texture_usage::RENDER_ATTACHMENT | texture_usage::TEXTURE_BINDING | texture_usage::COPY_SRC,
259             "scene-backdrop",
260         )?;
261         let depth = texture(device, DEPTH, w, h, texture_usage::RENDER_ATTACHMENT, "scene-depth")?;
262         if let Some(old) = self.target.take() {
263             old.backdrop.destroy();
264         }
265         self.target = Some(Target { view: whole_view(&backdrop)?, backdrop, depth_view: whole_view(&depth)?, width: w, height: h });
266         self.backdrop_valid = false;
267         Ok(())
268     }
269 
270     /// Record the staged scene into the backdrop (after [`fit`](Self::fit)),
271     /// consuming it. False when nothing was staged.
272     pub(crate) fn record(
273         &mut self,
274         device: &GpuDevice,
275         queue: &GpuQueue,
276         encoder: &GpuCommandEncoder,
277         image_group: &dyn Fn(u32) -> Option<GpuBindGroup>,
278     ) -> Result<bool, JsValue> {
279         let Some(staged) = self.staged.take() else { return Ok(false) };
280         let Some(target) = &self.target else { return Ok(false) };
281         let (w, h) = (target.width, target.height);
282 
283         let blocks = scene_uniforms(&staged.draws, &staged.images, [w as f32, h as f32], 0.0, self.light);
284         let mut bytes = vec![0u8; blocks.len().max(1) * STRIDE as usize];
285         for (i, b) in blocks.iter().enumerate() {
286             let at = i * STRIDE as usize;
287             bytes[at..at + UNIFORM_SIZE as usize].copy_from_slice(bytemuck::bytes_of(b));
288         }
289         if self.uniforms.ensure(device, bytes.len() as u32)? {
290             self.uniform_group = uniform_group(device, &self.uniform_layout, &self.uniforms);
291         }
292         self.uniforms.write(queue, &bytes)?;
293         let quads = image_quads_3d(&staged.images);
294         let quad_bytes: &[u8] = bytemuck::cast_slice(&quads);
295         self.image_verts.ensure(device, quad_bytes.len() as u32)?;
296         self.image_verts.write(queue, quad_bytes)?;
297         self.unit_instance.write(queue, bytemuck::bytes_of(&UNIT_INSTANCE))?;
298 
299         let color = GpuRenderPassColorAttachment::new_with_gpu_texture_view(GpuLoadOp::Clear, GpuStoreOp::Store, &target.view);
300         color.set_clear_value(&[0.0, 0.0, 0.0, 0.0].map(js_sys::Number::from));
301         let depth = GpuRenderPassDepthStencilAttachment::new_with_gpu_texture_view(&target.depth_view);
302         depth.set_depth_load_op(GpuLoadOp::Clear);
303         depth.set_depth_clear_value(1.0);
304         depth.set_depth_store_op(GpuStoreOp::Discard);
305         let desc = GpuRenderPassDescriptor::new(&[js_sys::JsOption::wrap(color)]);
306         desc.set_depth_stencil_attachment(&depth);
307         let pass = encoder.begin_render_pass(&desc)?;
308         let (sx, sy, sw, sh) = staged.scissor;
309         let (sx, sy) = (sx.min(w), sy.min(h));
310         pass.set_scissor_rect(sx, sy, sw.min(w - sx), sh.min(h - sy));
311 
312         let offset = |slot: usize| [(slot as u32) * STRIDE];
313         // The images due before mesh draw `at` (past the last draw, every
314         // one left), in staged order.
315         let draw_images = |at: usize| -> Result<(), JsValue> {
316             for (j, image) in staged.images.iter().enumerate() {
317                 if (image.before as usize).min(staged.draws.len()) != at {
318                     continue;
319                 }
320                 let Some(group) = image_group(image.image) else { continue };
321                 pass.set_pipeline(&self.image);
322                 pass.set_bind_group_with_u32_slice_and_u32_and_dynamic_offsets_data_length(
323                     0,
324                     Some(&self.uniform_group),
325                     &offset(staged.draws.len() + j),
326                     0,
327                     1,
328                 )?;
329                 pass.set_bind_group(1, Some(&group));
330                 pass.set_vertex_buffer_with_u32(0, Some(&self.image_verts.buffer), 0);
331                 pass.draw_with_instance_count_and_first_vertex(6, 1, (j * 6) as u32);
332             }
333             Ok(())
334         };
335         for (i, draw) in staged.draws.iter().enumerate() {
336             draw_images(i)?;
337             let Some(mesh) = self.meshes.get(draw.mesh.0) else { continue };
338             if mesh.count == 0 {
339                 continue;
340             }
341             // What it is drawn for: an instance mesh, or the one unit
342             // instance that leaves it as it is.
343             let (instances, instance_count) = match draw.instances {
344                 Some(id) => match self.meshes.get(id.0) {
345                     Some(m) => (&m.buffer.buffer, m.count),
346                     None => continue,
347                 },
348                 None => (&self.unit_instance.buffer, 1),
349             };
350             if instance_count == 0 {
351                 continue;
352             }
353             let pipeline = if draw.wireframe {
354                 &self.lines[draw.see_through as usize]
355             } else {
356                 &self.fills[2 * draw.see_through as usize + (draw.wire_base_width > 0.0) as usize]
357             };
358             pass.set_pipeline(pipeline);
359             pass.set_bind_group_with_u32_slice_and_u32_and_dynamic_offsets_data_length(0, Some(&self.uniform_group), &offset(i), 0, 1)?;
360             pass.set_vertex_buffer_with_u32(0, Some(&mesh.buffer.buffer), 0);
361             pass.set_vertex_buffer_with_u32(1, Some(instances), 0);
362             pass.draw_with_instance_count(mesh.count, instance_count);
363         }
364         draw_images(staged.draws.len())?;
365         pass.end();
366         self.backdrop_valid = true;
367         Ok(true)
368     }
369 }