Add robustness to GPU shaders (#537)

* Add robustness to GPU shaders

Make each stage quit early if a previous stage has failed.

The CPU shaders are minimally changed to be layout compatible. For the most part, they'll panic on a bounds check if sizes are exceeded. That's arguably useful for debugging, but a case can be made they should have the same behavior as the GPU shaders.

Work towards #366

* Address review feedback

Clarify some nits, and also make a distinction between reporting failure in path_count and coarse.
diff --git a/crates/encoding/src/config.rs b/crates/encoding/src/config.rs
index a6a40db..78c9d7f 100644
--- a/crates/encoding/src/config.rs
+++ b/crates/encoding/src/config.rs
@@ -137,10 +137,14 @@
     pub base_color: u32,
     /// Layout of packed scene data.
     pub layout: Layout,
+    /// Size of line soup buffer allocation (in [`LineSoup`]s)
+    pub lines_size: u32,
     /// Size of binning buffer allocation (in `u32`s).
     pub binning_size: u32,
     /// Size of tile buffer allocation (in [`Tile`]s).
     pub tiles_size: u32,
+    /// Size of segment count buffer allocation (in [`SegmentCount`]s).
+    pub seg_counts_size: u32,
     /// Size of segment buffer allocation (in [`PathSegment`]s).
     pub segments_size: u32,
     /// Size of per-tile command list buffer allocation (in `u32`s).
@@ -175,8 +179,10 @@
                 target_width: width,
                 target_height: height,
                 base_color: base_color.to_premul_u32(),
+                lines_size: buffer_sizes.lines.len(),
                 binning_size: buffer_sizes.bin_data.len() - layout.bin_data_start,
                 tiles_size: buffer_sizes.tiles.len(),
+                seg_counts_size: buffer_sizes.seg_counts.len(),
                 segments_size: buffer_sizes.segments.len(),
                 ptcl_size: buffer_sizes.ptcl.len(),
                 layout: *layout,
diff --git a/shader/binning.wgsl b/shader/binning.wgsl
index daeb6f0..55d80c3 100644
--- a/shader/binning.wgsl
+++ b/shader/binning.wgsl
@@ -53,6 +53,7 @@
 // store count values packed two u16's to a u32
 var<workgroup> sh_count: array<array<u32, N_TILE>, N_SUBSLICE>;
 var<workgroup> sh_chunk_offset: array<u32, N_TILE>;
+var<workgroup> sh_previous_failed: u32;
 
 @compute @workgroup_size(256)
 fn main(
@@ -63,7 +64,18 @@
     for (var i = 0u; i < N_SLICE; i += 1u) {
         atomicStore(&sh_bitmaps[i][local_id.x], 0u);
     }
-    workgroupBarrier();
+    if local_id.x == 0u {
+        let failed = bump.lines > config.lines_size;
+        sh_previous_failed = u32(failed);
+    }
+    // also functions as barrier to protect zeroing of bitmaps
+    let failed = workgroupUniformLoad(&sh_previous_failed);
+    if failed != 0u {
+        if global_id.x == 0u {
+            bump.failed |= STAGE_FLATTEN;
+        }
+        return;
+    }
 
     // Read inputs and determine coverage of bins
     let element_ix = global_id.x;
diff --git a/shader/coarse.wgsl b/shader/coarse.wgsl
index 758244c..c28f8d2 100644
--- a/shader/coarse.wgsl
+++ b/shader/coarse.wgsl
@@ -73,6 +73,9 @@
         let ptcl_dyn_start = config.width_in_tiles * config.height_in_tiles * PTCL_INITIAL_ALLOC;
         var new_cmd = ptcl_dyn_start + atomicAdd(&bump.ptcl, PTCL_INCREMENT);
         if new_cmd + PTCL_INCREMENT > config.ptcl_size {
+            // This sets us up for technical UB, as lots of threads will be writing
+            // to the same locations. But I think it's fine, and predicating the
+            // writes would probably slow things down.
             new_cmd = 0u;
             atomicOr(&bump.failed, STAGE_COARSE);
         }
@@ -152,11 +155,19 @@
     // We need to check only prior stages, as if this stage has failed in another workgroup, 
     // we still want to know this workgroup's memory requirement.   
     if local_id.x == 0u {
+        var failed = atomicLoad(&bump.failed) & (STAGE_BINNING | STAGE_TILE_ALLOC | STAGE_FLATTEN);
+        if atomicLoad(&bump.seg_counts) > config.seg_counts_size {
+            failed |= STAGE_PATH_COUNT;
+        }
         // Reuse sh_part_count to hold failed flag, shmem is tight
-        sh_part_count[0] = atomicLoad(&bump.failed);
+        sh_part_count[0] = u32(failed);
     }
     let failed = workgroupUniformLoad(&sh_part_count[0]);
-    if (failed & (STAGE_BINNING | STAGE_TILE_ALLOC | STAGE_PATH_COARSE)) != 0u {
+    if failed != 0u {
+        if wg_id.x == 0u && local_id.x == 0u {
+            // propagate PATH_COUNT failure to path_tiling_setup so it doesn't need to bind config
+            atomicOr(&bump.failed, failed);
+        }
         return;
     }
     let width_in_bins = (config.width_in_tiles + N_TILE_X - 1u) / N_TILE_X;
@@ -431,9 +442,11 @@
     }
     if bin_tile_x + tile_x < config.width_in_tiles && bin_tile_y + tile_y < config.height_in_tiles {
         ptcl[cmd_offset] = CMD_END;
+        var blend_ix = 0u;
         if max_blend_depth > BLEND_STACK_SPLIT {
             let scratch_size = max_blend_depth * TILE_WIDTH * TILE_HEIGHT;
-            ptcl[blend_offset] = atomicAdd(&bump.blend, scratch_size);
+            blend_ix = atomicAdd(&bump.blend, scratch_size);
         }
+        ptcl[blend_offset] = blend_ix;
     }
 }
diff --git a/shader/fine.wgsl b/shader/fine.wgsl
index 539ccbe..64621cb 100644
--- a/shader/fine.wgsl
+++ b/shader/fine.wgsl
@@ -867,6 +867,11 @@
     @builtin(local_invocation_id) local_id: vec3<u32>,
     @builtin(workgroup_id) wg_id: vec3<u32>,
 ) {
+    if ptcl[0] == ~0u {
+        // An earlier stage has failed, don't try to render.
+        // We use ptcl[0] for this so we don't use up a binding for bump.
+        return;
+    }
     let tile_ix = wg_id.y * config.width_in_tiles + wg_id.x;
     let xy = vec2(f32(global_id.x * PIXELS_PER_THREAD), f32(global_id.y));
     let local_xy = vec2(f32(local_id.x * PIXELS_PER_THREAD), f32(local_id.y));
diff --git a/shader/flatten.wgsl b/shader/flatten.wgsl
index 3f1a855..80da188 100644
--- a/shader/flatten.wgsl
+++ b/shader/flatten.wgsl
@@ -746,7 +746,9 @@
 // Writes a line into a the `lines` buffer at a pre-allocated location designated by `line_ix`.
 fn write_line(line_ix: u32, path_ix: u32, p0: vec2f, p1: vec2f) {
     bbox = vec4(min(bbox.xy, min(p0, p1)), max(bbox.zw, max(p0, p1)));
-    lines[line_ix] = LineSoup(path_ix, p0, p1);
+    if line_ix < config.lines_size {
+        lines[line_ix] = LineSoup(path_ix, p0, p1);
+    }
 }
 
 fn write_line_with_transform(line_ix: u32, path_ix: u32, p0: vec2f, p1: vec2f, t: Transform) {
diff --git a/shader/path_count.wgsl b/shader/path_count.wgsl
index 34913eb..7de8927 100644
--- a/shader/path_count.wgsl
+++ b/shader/path_count.wgsl
@@ -15,18 +15,21 @@
 }
 
 @group(0) @binding(0)
-var<storage, read_write> bump: BumpAllocators;
+var<uniform> config: Config;
 
 @group(0) @binding(1)
-var<storage> lines: array<LineSoup>;
+var<storage, read_write> bump: BumpAllocators;
 
 @group(0) @binding(2)
-var<storage> paths: array<Path>;
+var<storage> lines: array<LineSoup>;
 
 @group(0) @binding(3)
-var<storage, read_write> tile: array<AtomicTile>;
+var<storage> paths: array<Path>;
 
 @group(0) @binding(4)
+var<storage, read_write> tile: array<AtomicTile>;
+
+@group(0) @binding(5)
 var<storage, read_write> seg_counts: array<SegmentCount>;
 
 // number of integer cells spanned by interval defined by a, b
@@ -187,7 +190,10 @@
             // Pack two count values into a single u32
             let counts = (seg_within_slice << 16u) | subix;
             let seg_count = SegmentCount(line_ix, counts);
-            seg_counts[seg_base + i - imin] = seg_count;
+            let seg_ix = seg_base + i - imin;
+            if seg_ix < config.seg_counts_size {
+                seg_counts[seg_ix] = seg_count;
+            }
             // Note: since we're iterating, we have a reliable value for
             // last_z.
             last_z = z;
diff --git a/shader/path_count_setup.wgsl b/shader/path_count_setup.wgsl
index 9279359..a9c4a91 100644
--- a/shader/path_count_setup.wgsl
+++ b/shader/path_count_setup.wgsl
@@ -16,8 +16,12 @@
 
 @compute @workgroup_size(1)
 fn main() {
-    let lines = atomicLoad(&bump.lines);
-    indirect.count_x = (lines + (WG_SIZE - 1u)) / WG_SIZE;
+    if atomicLoad(&bump.failed) != 0u {
+        indirect.count_x = 0u;
+    } else {
+        let lines = atomicLoad(&bump.lines);
+        indirect.count_x = (lines + (WG_SIZE - 1u)) / WG_SIZE;
+    }
     indirect.count_y = 1u;
     indirect.count_z = 1u;
 }
diff --git a/shader/path_tiling_setup.wgsl b/shader/path_tiling_setup.wgsl
index 6fc70c3..4d5bf2e 100644
--- a/shader/path_tiling_setup.wgsl
+++ b/shader/path_tiling_setup.wgsl
@@ -11,13 +11,22 @@
 @group(0) @binding(1)
 var<storage, read_write> indirect: IndirectCount;
 
+@group(0) @binding(2)
+var<storage, read_write> ptcl: array<u32>;
+
 // Partition size for path tiling stage
 let WG_SIZE = 256u;
 
 @compute @workgroup_size(1)
 fn main() {
-    let segments = atomicLoad(&bump.seg_counts);
-    indirect.count_x = (segments + (WG_SIZE - 1u)) / WG_SIZE;
+    if atomicLoad(&bump.failed) != 0u {
+        indirect.count_x = 0u;
+        // signal fine rasterizer that failure happened (it doesn't bind bump)
+        ptcl[0] = ~0u;
+    } else {
+        let segments = atomicLoad(&bump.seg_counts);
+        indirect.count_x = (segments + (WG_SIZE - 1u)) / WG_SIZE;
+    }
     indirect.count_y = 1u;
     indirect.count_z = 1u;
 }
diff --git a/shader/shared/bump.wgsl b/shader/shared/bump.wgsl
index 48338f5..9270fc2 100644
--- a/shader/shared/bump.wgsl
+++ b/shader/shared/bump.wgsl
@@ -4,8 +4,9 @@
 // Bitflags for each stage that can fail allocation.
 let STAGE_BINNING: u32 = 0x1u;
 let STAGE_TILE_ALLOC: u32 = 0x2u;
-let STAGE_PATH_COARSE: u32 = 0x4u;
-let STAGE_COARSE: u32 = 0x8u;
+let STAGE_FLATTEN: u32 = 0x4u;
+let STAGE_PATH_COUNT: u32 = 0x8u;
+let STAGE_COARSE: u32 = 0x10u;
 
 // This must be kept in sync with the struct in config.rs in the encoding crate.
 struct BumpAllocators {
diff --git a/shader/shared/config.wgsl b/shader/shared/config.wgsl
index fe8580f..ef7b928 100644
--- a/shader/shared/config.wgsl
+++ b/shader/shared/config.wgsl
@@ -33,8 +33,10 @@
     style_base: u32,
 
     // Sizes of bump allocated buffers (in element size units)
+    lines_size: u32,
     binning_size: u32,
     tiles_size: u32,
+    seg_counts_size: u32,
     segments_size: u32,
     ptcl_size: u32,
 }
diff --git a/shader/tile_alloc.wgsl b/shader/tile_alloc.wgsl
index 7b05bdc..c6073d1 100644
--- a/shader/tile_alloc.wgsl
+++ b/shader/tile_alloc.wgsl
@@ -30,7 +30,7 @@
 
 var<workgroup> sh_tile_count: array<u32, WG_SIZE>;
 var<workgroup> sh_tile_offset: u32;
-var<workgroup> sh_atomic_failed: u32;
+var<workgroup> sh_previous_failed: u32;
 
 @compute @workgroup_size(256)
 fn main(
@@ -41,10 +41,11 @@
     // We need to check only prior stages, as if this stage has failed in another workgroup, 
     // we still want to know this workgroup's memory requirement.
     if local_id.x == 0u {
-        sh_atomic_failed = atomicLoad(&bump.failed);
+        let failed = (atomicLoad(&bump.failed) & (STAGE_BINNING | STAGE_FLATTEN)) != 0u;
+        sh_previous_failed = u32(failed);
     }
-    let failed = workgroupUniformLoad(&sh_atomic_failed);
-    if (failed & STAGE_BINNING) != 0u {
+    let failed = workgroupUniformLoad(&sh_previous_failed);
+    if failed != 0u {
         return;
     }    
     // scale factors useful for converting coordinates to tiles
diff --git a/src/cpu_shader/path_count.rs b/src/cpu_shader/path_count.rs
index ed04d7a..80eafc9 100644
--- a/src/cpu_shader/path_count.rs
+++ b/src/cpu_shader/path_count.rs
@@ -153,10 +153,11 @@
 }
 
 pub fn path_count(_n_wg: u32, resources: &[CpuBinding]) {
-    let mut bump = resources[0].as_typed_mut();
-    let lines = resources[1].as_slice();
-    let paths = resources[2].as_slice();
-    let mut tile = resources[3].as_slice_mut();
-    let mut seg_counts = resources[4].as_slice_mut();
+    // config is binding 0
+    let mut bump = resources[1].as_typed_mut();
+    let lines = resources[2].as_slice();
+    let paths = resources[3].as_slice();
+    let mut tile = resources[4].as_slice_mut();
+    let mut seg_counts = resources[5].as_slice_mut();
     path_count_main(&mut bump, &lines, &paths, &mut tile, &mut seg_counts);
 }
diff --git a/src/cpu_shader/path_tiling_setup.rs b/src/cpu_shader/path_tiling_setup.rs
index 33ca6bb..2b90145 100644
--- a/src/cpu_shader/path_tiling_setup.rs
+++ b/src/cpu_shader/path_tiling_setup.rs
@@ -17,5 +17,6 @@
 pub fn path_tiling_setup(_n_wg: u32, resources: &[CpuBinding]) {
     let bump = resources[0].as_typed();
     let mut indirect = resources[1].as_typed_mut();
+    // binding 2 is ptcl, which we would need if we propagate failure
     path_tiling_setup_main(&bump, &mut indirect);
 }
diff --git a/src/render.rs b/src/render.rs
index 264b241..279f145 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -345,7 +345,14 @@
             shaders.path_count,
             indirect_count_buf,
             0,
-            [bump_buf, lines_buf, path_buf, tile_buf, seg_counts_buf],
+            [
+                config_buf,
+                bump_buf,
+                lines_buf,
+                path_buf,
+                tile_buf,
+                seg_counts_buf,
+            ],
         );
         recording.dispatch(
             shaders.backdrop,
@@ -370,7 +377,7 @@
         recording.dispatch(
             shaders.path_tiling_setup,
             wg_counts.path_tiling_setup,
-            [bump_buf, indirect_count_buf.into()],
+            [bump_buf, indirect_count_buf.into(), ptcl_buf],
         );
         recording.dispatch_indirect(
             shaders.path_tiling,
diff --git a/src/shaders.rs b/src/shaders.rs
index fab6b7f..e00c1a9 100644
--- a/src/shaders.rs
+++ b/src/shaders.rs
@@ -222,7 +222,7 @@
     let path_count_setup = add_shader!(path_count_setup, [Buffer, Buffer], &empty);
     let path_count = add_shader!(
         path_count,
-        [Buffer, BufReadOnly, BufReadOnly, Buffer, Buffer]
+        [Uniform, Buffer, BufReadOnly, BufReadOnly, Buffer, Buffer]
     );
     let backdrop = add_shader!(
         backdrop_dyn,
@@ -245,7 +245,7 @@
         ],
         &empty
     );
-    let path_tiling_setup = add_shader!(path_tiling_setup, [Buffer, Buffer], &empty);
+    let path_tiling_setup = add_shader!(path_tiling_setup, [Buffer, Buffer, Buffer], &empty);
     let path_tiling = add_shader!(
         path_tiling,
         [