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 }