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);
         }
     }
 }