vello_gpu: direct GPU strip draws inside layers, exact buffer sizing Paths inside layers are now drawn from GPU-built instances too: the instance pass clips each segment to the draw target's tile bounds and applies the layer's geometry shift (as `visit_strip_fill_segments` and `RectU16::shift` do), with unused instance slots cleared per frame. Buffers and clears are sized to the exact strip and block totals from the path summaries, and the strip-record pass is skipped when nothing is read back. Readback now happens only for paths under a clip path and for non-solid paints. Written primarily by Claude Fable 5.1
diff --git a/vello_gpu/examples/sparse_strips_compare.rs b/vello_gpu/examples/sparse_strips_compare.rs index fe292aa..cb88c9b 100644 --- a/vello_gpu/examples/sparse_strips_compare.rs +++ b/vello_gpu/examples/sparse_strips_compare.rs
@@ -214,7 +214,7 @@ for _ in 0..timed_iterations { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - pipelines.encode_timed(&mut encoder, &buffers, Some(&query_set)); + pipelines.encode_timed(&mut encoder, &buffers, Some(&query_set), true); encoder.resolve_query_set(&query_set, 0..n_queries, &query_buf, 0); encoder.copy_buffer_to_buffer(&query_buf, 0, &query_read, 0, u64::from(n_queries) * 8); queue.submit([encoder.finish()]);
diff --git a/vello_gpu/src/draw.rs b/vello_gpu/src/draw.rs index 2801600..b87f1bd 100644 --- a/vello_gpu/src/draw.rs +++ b/vello_gpu/src/draw.rs
@@ -201,12 +201,20 @@ if self.gpu_draw_info.len() <= ix { self.gpu_draw_info.resize(ix + 1, GpuDrawInfo::default()); } + let bounds = self.state.target_bbox.to_tile_bounds(); + let shift = self.state.target.geometry_shift(); self.gpu_draw_info[ix] = GpuDrawInfo { paint: paint.paint, + // Only solid paints are direct, so the payload does not depend on + // the strip position. payload: paint.payload_at(0, 0), depth_index, flags: crate::sparse_strips::DRAW_DIRECT, instance_base: *instance_base, + bounds_x: u32::from(bounds.x0) | (u32::from(bounds.x1) << 16), + bounds_y: u32::from(bounds.y0) | (u32::from(bounds.y1) << 16), + shift_x: shift.0, + shift_y: shift.1, _pad: [0; 3], }; self.draw.direct_draws.push(DirectDraw {
diff --git a/vello_gpu/src/scene.rs b/vello_gpu/src/scene.rs index cc90c78..c8965c3 100644 --- a/vello_gpu/src/scene.rs +++ b/vello_gpu/src/scene.rs
@@ -291,9 +291,6 @@ /// State of GPU strip generation, when enabled. #[cfg(feature = "wgpu")] pub(crate) gpu_strips: Option<RefCell<GpuStrips>>, - /// Number of currently pushed layers. - #[cfg(feature = "wgpu")] - layer_depth: usize, } /// Paths whose strips are generated on the GPU, and the result once resolved. @@ -423,8 +420,6 @@ level, #[cfg(feature = "wgpu")] gpu_strips: None, - #[cfg(feature = "wgpu")] - layer_depth: 0, } } @@ -470,8 +465,8 @@ let clip_key = self.viewport_state.clip_key(); let (width, height) = (self.width, self.height); let strip_storage = &self.strip_storage; - // Only root-level, unclipped, solid-paint paths are drawn straight from GPU buffers. - let direct_paint = matches!(paint, Paint::Solid(_)) && self.layer_depth == 0; + // Only unclipped, solid-paint paths are drawn straight from GPU buffers. + let direct_paint = matches!(paint, Paint::Solid(_)); let outcome = self .viewport_state .with_generator_and_clip(|generator, clip| { @@ -593,6 +588,11 @@ gpu.instance_spans.push(None); } } + // The summaries give exact totals, so the buffers are sized to them. + let (total_strips, total_blocks) = gpu.summaries.iter().fold((0, 0), |(s, b), summary| { + (s + summary.strips, b + summary.blocks) + }); + gpu.input.exact_counts = Some((total_strips, total_blocks)); let buffers = pipelines.prepare_buffers( device, queue, @@ -611,7 +611,7 @@ let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("sparse strips"), }); - pipelines.encode(&mut encoder, &buffers); + pipelines.encode_timed(&mut encoder, &buffers, None, gpu.needs_readback()); if !gpu.needs_readback() { // Everything is drawn from GPU buffers: submit the passes now so that the GPU // produces the strips while the CPU schedules the frame, and never wait for them. @@ -1141,10 +1141,6 @@ if let Some(filter_plan) = &filter_data { self.viewport_state.push_root_viewport(filter_plan); } - #[cfg(feature = "wgpu")] - { - self.layer_depth += 1; - } let clip_path = clip_path.map(|path| { let mut strip_storage = self.strip_storage.borrow_mut(); @@ -1219,10 +1215,6 @@ self.viewport_state.pop_root_viewport(); } self.root_transforms.pop_root(); - #[cfg(feature = "wgpu")] - { - self.layer_depth = self.layer_depth.saturating_sub(1); - } } /// Set the blend mode for subsequent rendering operations. @@ -1361,11 +1353,8 @@ self.recorder.reset(self.width, self.height); self.filter = None; #[cfg(feature = "wgpu")] - { - if let Some(gpu) = &self.gpu_strips { - gpu.borrow_mut().reset(self.width, self.height); - } - self.layer_depth = 0; + if let Some(gpu) = &self.gpu_strips { + gpu.borrow_mut().reset(self.width, self.height); } }
diff --git a/vello_gpu/src/sparse_strips/gpu.rs b/vello_gpu/src/sparse_strips/gpu.rs index fbb5944..5793f95 100644 --- a/vello_gpu/src/sparse_strips/gpu.rs +++ b/vello_gpu/src/sparse_strips/gpu.rs
@@ -53,6 +53,8 @@ pub max_blocks: u32, /// Number of alpha texture rows `alphas` covers (its size is this many rows). pub alpha_rows: u32, + /// Instance slots in use this frame. + pub n_instances: u32, n_paths: u32, n_entries: u32, n_wg: u32, @@ -126,7 +128,9 @@ input: &StripInput, alpha_base_blocks: u32, alpha_row_bytes: u32, + n_instances: u32, ) { + self.n_instances = n_instances; let mut config = input.config(); config.alpha_base_blocks = alpha_base_blocks; let write = |buffer: &wgpu::Buffer, data: &[u8]| { @@ -371,7 +375,10 @@ let instances = make( "sparse strips instances", caps.instances.max(1) * size_of::<crate::GpuStrip>() as u64, - storage | wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_SRC, + storage + | wgpu::BufferUsages::VERTEX + | wgpu::BufferUsages::COPY_SRC + | wgpu::BufferUsages::COPY_DST, ); let draw_info = make( "sparse strips draw info", @@ -434,6 +441,7 @@ max_strips: 0, max_blocks: 0, alpha_rows: 0, + n_instances: 0, n_paths: 0, n_entries: 0, n_wg: 0, @@ -450,7 +458,13 @@ instances_group, counters, }; - buffers.upload(queue, input, alpha_base_blocks, alpha_row_bytes); + buffers.upload( + queue, + input, + alpha_base_blocks, + alpha_row_bytes, + n_instances, + ); buffers } @@ -470,7 +484,13 @@ let needed = Capacities::for_input(&config, input, alpha_row_bytes, n_instances); match existing { Some(mut buffers) if buffers.caps.fits(&needed) => { - buffers.upload(queue, input, alpha_base_blocks, alpha_row_bytes); + buffers.upload( + queue, + input, + alpha_base_blocks, + alpha_row_bytes, + n_instances, + ); buffers } _ => self.create_buffers( @@ -491,7 +511,17 @@ encoder: &mut wgpu::CommandEncoder, buffers: &SparseStripBuffers, ) { - encoder.clear_buffer(&buffers.counters, 0, None); + encoder.clear_buffer( + &buffers.counters, + 0, + Some(u64::from(buffers.n_paths.max(1)) * 4), + ); + // Unused slots must draw nothing. + encoder.clear_buffer( + &buffers.instances, + 0, + Some(u64::from(buffers.n_instances.max(1)) * size_of::<crate::GpuStrip>() as u64), + ); let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: Some("sparse strips instances"), timestamp_writes: None, @@ -506,19 +536,25 @@ /// Record the passes. `buffers` must come from [`Self::create_buffers`] with a non-empty /// input. pub fn encode(&self, encoder: &mut wgpu::CommandEncoder, buffers: &SparseStripBuffers) { - self.encode_timed(encoder, buffers, None); + self.encode_timed(encoder, buffers, None, true); } /// Record the passes, each in its own compute pass, writing a start and end timestamp per /// pass into `timestamps` (query `2 * i` and `2 * i + 1` for pass `i` in [`PASS_NAMES`] - /// order) when given. + /// order) when given. `strip_records` may be false when the strip records are not needed + /// (nothing is read back), which skips that pass. pub fn encode_timed( &self, encoder: &mut wgpu::CommandEncoder, buffers: &SparseStripBuffers, timestamps: Option<&wgpu::QuerySet>, + strip_records: bool, ) { - encoder.clear_buffer(&buffers.acc, 0, None); + encoder.clear_buffer( + &buffers.acc, + 0, + Some(u64::from(buffers.max_blocks.max(1)) * u64::from(ACC_WORDS) * 4), + ); // Direct workgroup counts, or the byte offset of indirect arguments. let passes: [( &str, @@ -554,6 +590,9 @@ ), ]; for (i, (label, pipeline, group, workgroups)) in passes.into_iter().enumerate() { + if label == "resolve_strips" && !strip_records { + continue; + } let timestamp_writes = timestamps.map(|query_set| wgpu::ComputePassTimestampWrites { query_set, beginning_of_pass_write_index: Some(2 * i as u32),
diff --git a/vello_gpu/src/sparse_strips/mod.rs b/vello_gpu/src/sparse_strips/mod.rs index 25d5cf0..5b889a8 100644 --- a/vello_gpu/src/sparse_strips/mod.rs +++ b/vello_gpu/src/sparse_strips/mod.rs
@@ -185,7 +185,15 @@ pub flags: u32, /// First instance slot of this path in the instance buffer. pub instance_base: u32, - /// Padding to 32 bytes. + /// Visible tile bounds of the draw's target: `x0 | x1 << 16`. + pub bounds_x: u32, + /// Visible tile bounds of the draw's target: `y0 | y1 << 16`. + pub bounds_y: u32, + /// Pixel shift applied to the geometry for the target (layers render at their region). + pub shift_x: i32, + /// See `shift_x`. + pub shift_y: i32, + /// Padding to 48 bytes. pub _pad: [u32; 3], } @@ -479,6 +487,10 @@ pub paths: Vec<GpuPathInfo>, /// Row tables and filled-row lists of culled paths. pub aux: Vec<u32>, + /// Exact totals of strips and alpha blocks, when the caller knows them (see + /// [`PathSummary`]); otherwise the bounds [`Self::max_strips`] and [`Self::max_blocks`] + /// size the buffers. + pub exact_counts: Option<(u32, u32)>, } impl StripInput { @@ -511,6 +523,7 @@ self.entries.clear(); self.paths.clear(); self.aux.clear(); + self.exact_counts = None; } /// Add a path from its sorted tiles and lines. @@ -639,8 +652,8 @@ rows: self.rows(), viewport_width: self.viewport_width(), n_wg: (self.entries.len() as u32).div_ceil(SCAN_WG), - max_strips: self.max_strips(), - max_blocks: self.max_blocks(), + max_strips: self.exact_counts.map_or_else(|| self.max_strips(), |c| c.0), + max_blocks: self.exact_counts.map_or_else(|| self.max_blocks(), |c| c.1), alpha_base_blocks: 0, } }
diff --git a/vello_gpu_shaders/shaders/helpers/sparse_strips.wesl b/vello_gpu_shaders/shaders/helpers/sparse_strips.wesl index f9af3f0..67193e5 100644 --- a/vello_gpu_shaders/shaders/helpers/sparse_strips.wesl +++ b/vello_gpu_shaders/shaders/helpers/sparse_strips.wesl
@@ -66,6 +66,12 @@ depth_index: u32, flags: u32, instance_base: u32, + // Visible tile bounds of the target, `x0 | x1 << 16` and `y0 | y1 << 16`. + bounds_x: u32, + bounds_y: u32, + // Pixel shift applied to geometry for the target. + shift_x: i32, + shift_y: i32, _p0: u32, _p1: u32, _p2: u32,
diff --git a/vello_gpu_shaders/shaders/sparse_strips_instances.wesl b/vello_gpu_shaders/shaders/sparse_strips_instances.wesl index c1ebc24..30dd518 100644 --- a/vello_gpu_shaders/shaders/sparse_strips_instances.wesl +++ b/vello_gpu_shaders/shaders/sparse_strips_instances.wesl
@@ -3,9 +3,10 @@ //! The instance pass of GPU sparse strips: one thread per strip descriptor emits the strip //! instances `vello_common::strip::visit_strip_fill_segments` would emit for it (an alpha -//! segment, and a fill segment for the gap to the next strip), for paths the scheduler marked -//! as drawn directly from GPU buffers. Instances land in the path's slot range, in no -//! particular order (strips of one path never overlap). +//! segment, and a fill segment for the gap to the next strip), clipped to the draw target's +//! bounds and shifted into the target, for paths the scheduler marked as drawn directly from +//! GPU buffers. Instances land in the path's slot range, in no particular order (strips of one +//! path never overlap); unused slots stay zero (the buffer is cleared) and draw nothing. import package::helpers::sparse_strips::{ StripDesc, DrawInfo, Instance, config, paths, DRAW_DIRECT, TILE_W, TILE_H, TILE_PIXELS, @@ -20,11 +21,21 @@ // Per-path instance counters, zero at dispatch. @group(1) @binding(3) var<storage, read_write> counters: array<atomic<u32>>; -fn emit(info: DrawInfo, path: u32, x0: u32, y: u32, width: u32, col_idx: u32, dense: bool) { +fn shift_coord(v: u32, shift: i32) -> u32 { + return u32(clamp(i32(v) + shift, 0, 0xffff)); +} + +// Emit the pixel rectangle `[x0, x1) x [y, y + TILE_H)` of the target, shifted like +// `RectU16::shift` (each coordinate clamped to u16). +fn emit(info: DrawInfo, path: u32, x0: u32, x1: u32, y: u32, col_idx: u32, dense: bool) { + let sx0 = shift_coord(x0, info.shift_x); + let sx1 = shift_coord(x1, info.shift_x); + let sy = shift_coord(y, info.shift_y); + let width = sx1 - sx0; let slot = info.instance_base + atomicAdd(&counters[path], 1u); let dense_width = select(0u, width, dense); instances[slot] = Instance( - x0 | (y << 16u), + sx0 | (sy << 16u), width | (dense_width << 16u), select(0u, col_idx, dense), info.payload, @@ -48,23 +59,32 @@ if (info.flags & DRAW_DIRECT) == 0u { return; } - // Every path ends with a sentinel, so the next descriptor belongs to the same path. - let next = strip_descs[s + 1u]; let x = d.xy & 0xffffu; let y = d.xy >> 16u; - let bounds_x1 = config.viewport_width / TILE_W; + let bx0 = info.bounds_x & 0xffffu; + let bx1 = info.bounds_x >> 16u; + let by0 = info.bounds_y & 0xffffu; + let by1 = info.bounds_y >> 16u; + let tile_y = y / TILE_H; + if tile_y < by0 || tile_y >= by1 { + return; + } + // Every path ends with a sentinel, so the next descriptor belongs to the same path. + let next = strip_descs[s + 1u]; let tile_x0 = x / TILE_W; // One alpha block per tile column. let tile_x1 = tile_x0 + (next.block_start - d.block_start); - let cx1 = min(tile_x1, bounds_x1); - if tile_x0 < cx1 { - let alpha_idx = (d.block_start + config.alpha_base_blocks) * TILE_PIXELS; - emit(info, d.path, x, y, (cx1 - tile_x0) * TILE_W, alpha_idx / TILE_H, true); + let cx0 = max(tile_x0, bx0); + let cx1 = min(tile_x1, bx1); + if cx0 < cx1 { + let alpha_idx = (d.block_start + config.alpha_base_blocks + (cx0 - tile_x0)) * TILE_PIXELS; + emit(info, d.path, cx0 * TILE_W, cx1 * TILE_W, y, alpha_idx / TILE_H, true); } if (next.info & STRIP_FILL_GAP) != 0u && (next.xy >> 16u) == y { - let fx1 = min((next.xy & 0xffffu) / TILE_W, bounds_x1); - if tile_x1 < fx1 { - emit(info, d.path, tile_x1 * TILE_W, y, (fx1 - tile_x1) * TILE_W, 0u, false); + let fx0 = max(tile_x1, bx0); + let fx1 = min((next.xy & 0xffffu) / TILE_W, bx1); + if fx0 < fx1 { + emit(info, d.path, fx0 * TILE_W, fx1 * TILE_W, y, 0u, false); } } }