Merge PR #6001: Text: admit the rasterizer's peak working bytes (#4979)

This commit is contained in:
aiongg
2026-10-22 02:23:35 +00:00
2 changed files with 97 additions and 18 deletions
+19 -11
View File
@@ -76,8 +76,13 @@ pub struct Coverage {
}
impl Coverage {
pub fn new(width: usize, height: usize) -> Self {
Self { width, height, acc: vec![0.0; width * height + 2] }
/// A zeroed accumulator, or `None` when its bytes cannot be reserved.
pub fn try_new(width: usize, height: usize) -> Option<Self> {
let n = width.checked_mul(height)?.checked_add(2)?;
let mut acc = Vec::new();
acc.try_reserve_exact(n).ok()?;
acc.resize(n, 0.0);
Some(Self { width, height, acc })
}
/// Adds a line segment (pixel coordinates, y down).
@@ -148,15 +153,18 @@ impl Coverage {
}
/// Resolves the accumulation into per-pixel coverage in 0..=1.
pub fn finish(self) -> Vec<f32> {
/// The accumulated coverage, or `None` when the output cannot be reserved. The input
/// accumulator is still live while this builds, so both count toward a caller's budget.
pub fn finish(self) -> Option<Vec<f32>> {
let n = self.width * self.height;
let mut out = Vec::with_capacity(n);
let mut out = Vec::new();
out.try_reserve_exact(n).ok()?;
let mut sum = 0.0f32;
for v in &self.acc[..n] {
sum += v;
out.push(sum.abs().min(1.0));
}
out
Some(out)
}
}
@@ -240,9 +248,9 @@ mod tests {
#[test]
fn square_area_exact() {
let mut c = Coverage::new(10, 10);
let mut c = Coverage::try_new(10, 10).unwrap();
rect_to(&mut c, &Xform::IDENTITY, 2.25, 3.5, 6.75, 8.0);
let v = c.finish();
let v = c.finish().unwrap();
assert!((sum(&v) - 4.5 * 4.5).abs() < 1e-3, "{}", sum(&v));
assert_eq!(v[5 * 10 + 4], 1.0);
assert_eq!(v[0], 0.0);
@@ -250,13 +258,13 @@ mod tests {
#[test]
fn triangle_area_and_clipping() {
let mut c = Coverage::new(8, 8);
let mut c = Coverage::try_new(8, 8).unwrap();
// Right triangle partly outside on the left and bottom.
let pts = [(-4.0, 2.0), (6.0, 2.0), (-4.0, 12.0)];
for i in 0..3 {
c.line(pts[i], pts[(i + 1) % 3]);
}
let v = c.finish();
let v = c.finish().unwrap();
// Visible part: region x in [0,6], y in [2,8] under the hypotenuse x + y <= 8.
// Area = ∫_{y=2}^{8} (8 - y) dy = 18.
assert!((sum(&v) - 18.0).abs() < 1e-3, "{}", sum(&v));
@@ -266,7 +274,7 @@ mod tests {
#[test]
fn circle_area_via_pen() {
use skrifa::outline::OutlinePen;
let mut c = Coverage::new(40, 40);
let mut c = Coverage::try_new(40, 40).unwrap();
{
let mut p = Pen::new(&mut c, Xform([1.0, 0.0, 0.0, -1.0, 20.0, 20.0]));
// Cubic circle approximation, r = 15.
@@ -278,7 +286,7 @@ mod tests {
p.curve_to(k, -r, r, -k, r, 0.0);
p.close();
}
let a = sum(&c.finish());
let a = sum(&c.finish().unwrap());
let exact = std::f32::consts::PI * 225.0;
assert!((a - exact).abs() / exact < 0.005, "{a} vs {exact}");
}
+78 -7
View File
@@ -21,6 +21,40 @@ pub const FAUX_ITALIC_DEG: f32 = 12.0;
pub const FAUX_BOLD_RADIUS: f32 = 0.036;
/// Largest raster we produce (pixels), as a guard against absurd sizes.
const MAX_PIXELS: u64 = 256 * 1024 * 1024;
/// Peak working bytes one rasterization may keep live. [`MAX_PIXELS`] does not bound them: the
/// accumulator and the output surface are both `stride` deep, and a faux-bold pass keeps one more
/// coverage buffer live, so a CMYK bold render reserves several times what a plain grey one does
/// for the same pixels.
const MAX_RASTER_BYTES: u64 = if cfg!(target_pointer_width = "64") { 4 << 30 } else { 1 << 30 };
/// Whether a `w` x `h` render's peak working bytes fit `max_bytes`: the f32 accumulator and the
/// output surface (both `stride` deep), plus the coverage buffers live at the peak. A plain pass
/// peaks inside `Coverage::finish`, which holds its accumulator and the vector it builds beside
/// it; a faux-bold pass peaks one buffer higher, since the resolved plain coverage is kept while
/// the bold pass resolves into it. `false` when the arithmetic overflows, so an absurd size is
/// refused rather than wrapping.
fn fits_raster_budget(w: usize, h: usize, stride: usize, sample: SampleType, bold: bool, max_bytes: u64) -> bool {
let f32s = size_of::<f32>() as u64;
let peak = (w as u64).checked_mul(h as u64).and_then(|px| {
let deep = px.checked_mul(stride as u64)?.checked_mul(f32s + sample.bytes() as u64)?;
let coverage = px.checked_mul(if bold { 3 } else { 2 })?.checked_mul(f32s)?;
deep.checked_add(coverage)
});
peak.is_some_and(|b| b <= max_bytes)
}
/// A zeroed buffer, or `None` when its bytes cannot be reserved.
fn zeroed(len: usize) -> Option<Vec<f32>> {
let mut v = Vec::new();
v.try_reserve_exact(len).ok()?;
v.resize(len, 0.0);
Some(v)
}
/// What a refused or unrenderable layout returns.
fn nothing(format: PixelFormat) -> Rendered {
Rendered { surface: Surface::new(format), rect: Rect::new(0, 0, 0, 0) }
}
/// Maps raw area-coverage to the opacity Photoshop's anti-alias modes imply. Exponents above 1 thin
/// the fringe (sharper); below 1 widen it (stronger smoothing).
@@ -277,8 +311,14 @@ pub fn rasterize_warped(layout: &TextLayout, transform: &Affine, format: PixelFo
}
let n = format.mode.color_channels();
let stride = n + 1;
let bold = layout.glyphs.iter().any(|g| is_bold(layout, g));
if !fits_raster_budget(w, h, stride, format.sample, bold, MAX_RASTER_BYTES) {
return nothing(format);
}
// Premultiplied accumulation.
let mut acc = vec![0.0f32; w * h * stride];
let Some(mut acc) = zeroed(w * h * stride) else {
return nothing(format);
};
let xf = Xform([1.0, 0.0, 0.0, 1.0, -rect.x0 as f64, -rect.y0 as f64]).mul(&Xform(transform.m));
let mut colors: Vec<Color> = Vec::new();
for st in &layout.styles {
@@ -287,14 +327,23 @@ pub fn rasterize_warped(layout: &TextLayout, transform: &Affine, format: PixelFo
}
}
for c in &colors {
let mut cov = Coverage::new(w, h);
let Some(mut cov) = Coverage::try_new(w, h) else {
return nothing(format);
};
draw_pass(layout, &xf, &mut cov, Some(c), warp, Pass::Plain);
let mut cov = cov.finish();
if layout.glyphs.iter().any(|g| is_bold(layout, g)) {
let Some(mut cov) = cov.finish() else {
return nothing(format);
};
if bold {
for k in 0..BOLD_OFFSETS.len() {
let mut bc = Coverage::new(w, h);
let Some(mut bc) = Coverage::try_new(w, h) else {
return nothing(format);
};
draw_pass(layout, &xf, &mut bc, Some(c), warp, Pass::Bold(k));
for (a, b) in cov.iter_mut().zip(bc.finish()) {
let Some(bold_cov) = bc.finish() else {
return nothing(format);
};
for (a, b) in cov.iter_mut().zip(bold_cov) {
*a = a.max(b);
}
}
@@ -445,7 +494,8 @@ pub fn outlines(layout: &TextLayout, transform: &Affine, warp: Option<&Warp>) ->
#[cfg(test)]
mod tests {
use super::{MAX_PIXELS, rect_from_bounds};
use super::{MAX_PIXELS, fits_raster_budget, rect_from_bounds, zeroed};
use photocraft_color::SampleType;
use photocraft_geom::Rect;
#[test]
@@ -466,4 +516,25 @@ mod tests {
assert_eq!(rect_from_bounds(bounds), first, "bounds: {bounds:?}");
}
}
/// #4979: the pixel cap says nothing about bytes, so the peak is admitted from the channel
/// depth of the accumulator and surface, the sample size and the faux-bold coverage buffers.
/// A 100 x 100 grey+alpha render at 8-bit keeps 2 x (4 + 1) + 2 x 4 = 18 bytes a pixel live.
#[test]
fn the_byte_budget_follows_channels_depth_and_faux_bold() {
let px = 100 * 100;
assert!(fits_raster_budget(100, 100, 2, SampleType::U8, false, px * 18));
assert!(!fits_raster_budget(100, 100, 2, SampleType::U8, false, px * 18 - 1), "the allowance is exact");
// The same pixels cost more with more channels, a wider sample, or a bold pass.
assert!(!fits_raster_budget(100, 100, 5, SampleType::U8, false, px * 18), "CMYK is deeper");
assert!(!fits_raster_budget(100, 100, 2, SampleType::F32, false, px * 18), "f32 output is wider");
assert!(!fits_raster_budget(100, 100, 2, SampleType::U8, true, px * 18), "bold holds one more buffer");
assert!(fits_raster_budget(100, 100, 2, SampleType::U8, true, px * 22), "2 x 5 + 3 x 4 bytes a pixel");
// A size whose peak overflows is refused rather than wrapping to a small number.
assert!(!fits_raster_budget(usize::MAX, usize::MAX, 5, SampleType::F32, true, u64::MAX));
// Nothing is reserved for a buffer that cannot be served.
assert!(zeroed(usize::MAX).is_none());
}
}