Fix #3592: a gradient mesh from a file is budgeted at about a million quads

A mesh read from a file can have any number of patches, and the view any zoom: a 150 x 150 mesh from a 3 MB file exported at 20x asked for about 23 million quads (2 GB), and the command's own cap of 50 rows and columns does not apply to a loaded one. One tessellation now makes at most 1,048,576 quads, the subdivision per patch giving way before that, and a mesh with more patches than that is malformed, like a point count that does not fit.
This commit is contained in:
Taha-Mahmoodi
2026-10-31 19:43:16 +07:00
committed by bflatastic
parent b13b3260cd
commit a1a3a99104
4 changed files with 48 additions and 5 deletions
+23
View File
@@ -181,6 +181,29 @@ mod tests {
assert!((p.x - 37.0).abs() < 1e-6 && (p.y - 81.0).abs() < 1e-6, "{p:?}");
}
/// #3592: a mesh read from a file can have any number of patches, and the view any zoom, so the
/// quads one tessellation makes are budgeted: the subdivision per patch gives way before memory
/// does, and each patch stays at least one quad.
#[test]
fn a_large_mesh_tessellates_within_the_quad_budget() {
let mesh = |n: u32| GradientMesh::from_surface(n, n, &|u, v| Point::new(u * 300.0, v * 300.0), &|u, v| Color::rgb(u as f32, v as f32, 0.5));
// 3,600 patches at 32 x 32 would be 3.7 million quads.
let m = mesh(60);
let quads = m.quads(32);
assert!(quads.len() <= GradientMesh::MAX_QUADS, "{} quads", quads.len());
assert!(quads.len() >= 3600 * 4, "still finer than a quad per patch: {}", quads.len());
// A small mesh keeps the subdivision asked for.
assert_eq!(mesh(3).quads(8).len(), 9 * 64);
// The finest the view asks for, on a mesh of 22,500 patches, is a few quads per patch.
let big = mesh(150).quads(64);
assert!(big.len() <= GradientMesh::MAX_QUADS && big.len() >= 22_500, "{} quads", big.len());
// More patches than the budget has quads is malformed, as a point count that doesn't fit is.
let point = MeshPoint { p: Point::ZERO, color: Color::rgb(0.0, 0.0, 0.0), opacity: 1.0, handles: [Vec2::ZERO; 4] };
let huge = GradientMesh { rows: 2000, cols: 2000, points: vec![point; 2001 * 2001] };
assert!(!huge.is_valid());
assert!(huge.quads(1).is_empty());
}
/// A file's rows and columns can be anything: a point count too large to compute is no match
/// for the points (u32::MAX × u32::MAX wrapped to 0 and passed for a grid without points).
#[test]
+15 -3
View File
@@ -170,11 +170,22 @@ pub(super) fn grid_len(rows: u32, cols: u32) -> Option<usize> {
}
impl GradientMesh {
/// The most quads one tessellation ([`Self::quads`]) makes, and so the most patches a mesh can
/// have: about 170 bytes a quad, so about 170 MB. A file's mesh can have any number of patches
/// and the view any zoom; the subdivision per patch gives way before memory does, and a mesh
/// with more patches than that is malformed, like a point count that doesn't fit (#3592).
pub const MAX_QUADS: usize = 1 << 20;
pub fn idx(&self, r: usize, c: usize) -> usize {
r * (self.cols as usize + 1) + c
}
pub fn is_valid(&self) -> bool {
self.rows >= 1 && self.cols >= 1 && grid_len(self.rows, self.cols) == Some(self.points.len())
self.rows >= 1 && self.cols >= 1 && grid_len(self.rows, self.cols) == Some(self.points.len()) && self.patches() <= Self::MAX_QUADS
}
/// The number of patches (rows times columns), saturating.
fn patches(&self) -> usize {
(self.rows as usize).saturating_mul(self.cols as usize)
}
/// A 1×1 mesh from its four corners (bottom-left, top-left, top-right, bottom-right in u/v
@@ -352,8 +363,9 @@ impl GradientMesh {
if !self.is_valid() {
return vec![];
}
let n = n.clamp(1, 64);
let mut out = Vec::with_capacity(self.rows as usize * self.cols as usize * n * n);
// Finer than the budget allows gives way: a quad per patch is the least.
let n = n.clamp(1, 64).min(((Self::MAX_QUADS / self.patches()) as f64).sqrt() as usize).max(1);
let mut out = Vec::with_capacity(self.patches() * n * n);
let mut grid = vec![Point::ZERO; (n + 1) * (n + 1)];
let mut colors = vec![(Color::default(), 0.0f32); (n + 1) * (n + 1)];
for r in 0..self.rows as usize {
+8 -1
View File
@@ -1,6 +1,6 @@
# Gaps
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (G13: gradient presets done; G1: Average & Join with a corner or smooth choice, Cmd+Alt+Shift+J, done; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (G8: a gradient mesh from a file is budgeted at about a million patches and quads, #3592; estimate unchanged; previously: G13: gradient presets done; G1: Average & Join with a corner or smooth choice, Cmd+Alt+Shift+J, done; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
## Feature submission evidence
@@ -224,6 +224,12 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
taken whole or not at all, and the walk that counts an art tile's anchors charges the whole budget for a tree it
cannot finish, so nesting buys no room (#3594). The budget is per brushed stroke, not per document: many strokes
that each reach it still add up, and a brush inside a symbol is bounded per stroke rather than per instantiation.
- **Done (2026-10-31):** a gradient mesh is budgeted (#3592). One tessellation makes at most 1,048,576 quads, and a
mesh with more patches than that (rows times columns) is malformed, like a point count that doesn't fit, so it draws
nothing instead of exhausting memory: a 150 x 150 mesh from a 3 MB file exported at 20x asked for about 23 million
quads (2 GB). Below the budget the subdivision per patch gives way a step at a time (a 60 x 60 mesh draws at 16 x 16
quads a patch instead of 32 x 32); a small mesh is drawn as finely as before. The command's own cap of 50 rows and
columns is unchanged.
- **Done (2026-10-29):** a blend makes at most 100,000 steps in all, counting the steps of the blends inside its keys
and steps. Each step between keys that are blends is a copy of one, which makes its own steps when drawn, so step
counts capped at 1,000 a pair multiplied by nesting: five levels of 15 steps were 1.3 million steps from a 23 KB
@@ -660,6 +666,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv
| Date | Change | Summary |
|---|---|---|
| 2026-10-31 | minor | G8: a gradient mesh read from a file is budgeted: at most about a million quads per tessellation, and a mesh with more patches than that is malformed (#3592); estimate unchanged. |
| 2026-10-30 | minor | G9: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. |
| 2026-10-30 | minor | G1: Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J) moves two open path ends to their midpoint and joins them there, the joined anchor a corner or a smooth point as its dialog's Points choice asks, in one undo step; the "Average and Join in one step" entry is done. Estimate unchanged |
| 2026-10-30 | minor | G12: Preferences audit (#394): 5 options are stored but not acted on (Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard), each waiting on a subsystem; Highlight Alternate Glyphs, Real-time Drawing and the Devices options act. Estimate unchanged. |
+2 -1
View File
@@ -1,6 +1,6 @@
# Parity with Adobe Illustrator
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Path Precision Tools add-on: Shared Edge Merge Tool; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (G8: a gradient mesh from a file is budgeted, #3592; scores unchanged; previously: Path Precision Tools add-on: Shared Edge Merge Tool; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
## Feature submission evidence
@@ -19,6 +19,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
| Date | Change | Summary |
|---|---|---|
| 2026-10-31 | minor | Hardening: a gradient mesh read from a file is budgeted at about a million quads, so a huge one no longer exhausts memory when drawn or exported (#3592); scores unchanged. |
| 2026-10-30 | minor | File formats: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. |
| 2026-10-30 | minor | File formats: `.ai`/EPS editing-data fidelity from an audit of a user's files: All Caps and Small Caps (#4708); placed files at their box's corner, kept where they draw closer to the page (#4664, #4723, #4742); pages cropped to the art, with `UserUnit` (#4658, #4727); gradient mesh corners with handle weights of 0 and the state written after a mesh (#4738, #4744, #4748); type on a path spaced as the file lays it out (#4743); synthetic regressions in `crates/eps`; evidence added; scores unchanged. |
| 2026-10-30 | minor | Effects: bounded length inputs clamp on commit and reject invalid text visibly (#4638); scores unchanged. |