diff --git a/crates/geom/src/warp.rs b/crates/geom/src/warp.rs
--- a/crates/geom/src/warp.rs
+++ b/crates/geom/src/warp.rs
@@ -645,8 +645,13 @@ impl BezierMesh {
     }
 
     /// Splits the patch column containing `s` at `s` (a vertical split line). Exact: the surface
-    /// is unchanged. Returns false when `s` is on or too close to an existing boundary.
+    /// is unchanged. Returns false when `s` is on or too close to an existing boundary, or the
+    /// mesh already has the maximum 64 patch columns.
     pub fn split_u(&mut self, s: f64) -> bool {
+        // A successful split must remain valid for the next edit and for serialization.
+        if self.us.len() >= 65 {
+            return false;
+        }
         let (pi, a) = Self::locate(&self.us, s);
         if !(0.01..=0.99).contains(&a) {
             return false;
@@ -666,7 +671,11 @@ impl BezierMesh {
     }
 
     /// Splits the patch row containing `t` at `t` (a horizontal split line). Exact.
+    /// Returns false at the maximum 64 patch rows or too close to an existing boundary.
     pub fn split_v(&mut self, t: f64) -> bool {
+        if self.vs.len() >= 65 {
+            return false;
+        }
         self.transpose();
         let ok = self.split_u(t);
         self.transpose();
diff --git a/crates/geom/src/warp/tests.rs b/crates/geom/src/warp/tests.rs
--- a/crates/geom/src/warp/tests.rs
+++ b/crates/geom/src/warp/tests.rs
@@ -2,6 +2,51 @@ use super::*;
 
 const B: [f64; 4] = [10.0, 20.0, 110.0, 70.0];
 
+#[test]
+fn splitting_at_the_grid_limit_preserves_a_valid_mesh() {
+    for (cols, rows) in [(64, 2), (2, 64), (64, 64)] {
+        let mut mesh = BezierMesh::identity(B, cols, rows);
+        assert!(mesh.is_valid());
+        if cols == 64 {
+            let before = mesh.clone();
+            assert!(!mesh.split_u(0.5 / 64.0));
+            assert_eq!(mesh, before, "a refused vertical split must not change the mesh");
+        }
+        if rows == 64 {
+            let before = mesh.clone();
+            assert!(!mesh.split_v(0.5 / 64.0));
+            assert_eq!(mesh, before, "a refused horizontal split must not change the mesh");
+        }
+        // Reaching the limit on one axis must not prevent splitting the other.
+        if cols < 64 {
+            assert!(mesh.split_u(0.25));
+        }
+        if rows < 64 {
+            assert!(mesh.split_v(0.25));
+        }
+        assert!(mesh.is_valid());
+    }
+}
+
+#[test]
+fn splitting_to_the_grid_limit_and_removing_a_split_still_work() {
+    let mut mesh = BezierMesh::identity(B, 63, 63);
+    assert!(mesh.split_u(0.5 / 63.0));
+    assert!(mesh.split_v(0.5 / 63.0));
+    assert_eq!((mesh.us.len(), mesh.vs.len()), (65, 65));
+    assert!(mesh.is_valid());
+    for (s, t) in [(0.0, 0.0), (0.37, 0.61), (1.0, 1.0)] {
+        let p = mesh.eval(s, t);
+        assert!((p[0] - (B[0] + s * (B[2] - B[0]))).abs() < 1e-9);
+        assert!((p[1] - (B[1] + t * (B[3] - B[1]))).abs() < 1e-9);
+    }
+    assert!(mesh.remove_split_u(1));
+    assert!(mesh.remove_split_v(1));
+    assert!(mesh.split_u(0.5 / 63.0));
+    assert!(mesh.split_v(0.5 / 63.0));
+    assert!(mesh.is_valid());
+}
+
 /// The largest mismatch between the tangents on the two sides of any interior grid line.
 fn tangent_defect(m: &BezierMesh) -> f64 {
     let nx = m.nx();
