Composite smart object sources in tile-aligned blocks

Row strips of a very wide source still allocated whole 256-row tiles
across the full width for every few rows (about 293 MiB per strip of a
300000 px RGBA8 source). Blocks are now whole tiles, at most 4096 px wide
and 1 Mpx in all, and their tiles move into the result without a copy.
This commit is contained in:
Khawar Zamman Wani
2026-10-29 11:48:58 +05:30
parent dafbff2b05
commit e2cd2a335a
3 changed files with 48 additions and 28 deletions
+41 -22
View File
@@ -1,5 +1,7 @@
use std::sync::PoisonError;
use photocraft_geom::TILE_SIZE;
use super::{
Arc, ColorMode, Document, Layer, LayerContent, LayerId, Metadata, Mutex, PixelFormat, Rect, Result, SampleType, Session, SmartObject, SmartSource, Surface,
base_name, decode_source, json, other,
@@ -199,7 +201,7 @@ pub(super) fn source_surface_from_buffer(src: &Document, buf: &photocraft_compos
convert_source(src, fmt, raster)
}
/// `src` composited straight into `fmt` in strips, without the full-size RGBA buffer
/// `src` composited straight into `fmt` in blocks, without the full-size RGBA buffer
/// [`photocraft_compose::flatten`] allocates (16 bytes a pixel on top of the surface).
pub(super) fn flattened_source(src: &Document, fmt: PixelFormat) -> Result<Surface> {
let b = src.bounds();
@@ -212,34 +214,49 @@ pub(super) fn flattened_source(src: &Document, fmt: PixelFormat) -> Result<Surfa
if native(src.mode) || native(fmt.mode) {
return source_surface_from_buffer(src, &photocraft_compose::flatten(src), fmt);
}
convert_source(src, fmt, |f| composite_in_strips(src, f))
convert_source(src, fmt, |f| composite_in_blocks(src, f))
}
/// Pixels per strip of [`composite_in_strips`]: the compositor's own bands are 8 Mpx, which for
/// Pixels per block of [`composite_in_blocks`]: the compositor's own bands are 8 Mpx, which for
/// most sources is the whole image again (a band and its f32 conversion are 32 bytes a pixel).
const STRIP_PIXELS: u64 = 1 << 20;
const BLOCK_PIXELS: u64 = 1 << 20;
/// [`photocraft_compose::flatten_to_surface`] of `src`, composited a strip of rows at a time
/// so the float composite and its conversion never cover more than [`STRIP_PIXELS`].
fn composite_in_strips(src: &Document, fmt: PixelFormat) -> Surface {
/// [`photocraft_compose::flatten_to_surface`] of `src`, composited a block at a time so the float
/// composite and its conversion never cover more than [`BLOCK_PIXELS`]. Blocks are whole tiles,
/// so each block's tiles move into the result as they are: nothing is allocated outside the
/// block, and nothing is copied.
fn composite_in_blocks(src: &Document, fmt: PixelFormat) -> Surface {
let b = src.bounds();
let rows = strip_rows(b.width());
let (bw, bh) = block_size(b.width());
let mut out = Surface::new(fmt);
let mut y = b.y0;
// On the tile grid even if the bounds aren't, so no tile is shared by two blocks.
let mut y = b.y0 - b.y0.rem_euclid(TILE_SIZE);
while y < b.y1 {
let strip = Rect::new(b.x0, y, b.x1, y.saturating_add(rows).min(b.y1));
out.write_region(strip, &photocraft_compose::composite_to_surface(src, strip, fmt, None).read_region(strip));
y = strip.y1;
let y1 = y.saturating_add(bh);
let mut x = b.x0 - b.x0.rem_euclid(TILE_SIZE);
while x < b.x1 {
let x1 = x.saturating_add(bw);
let block = Rect::new(x, y, x1, y1).intersect(&b);
if !block.is_empty() {
let mut part = photocraft_compose::composite_to_surface(src, block, fmt, None);
out.put_tiles(part.take_tiles(block));
}
x = x1;
}
y = y1;
}
out.prune();
out
}
/// Rows per strip of a `width`-pixel source: as many as fit in [`STRIP_PIXELS`], at least one.
/// Not rounded up to the compositor's tile height, which for a very wide source would be many
/// times the budget (a 300000 px row is already 0.3 Mpx).
fn strip_rows(width: u32) -> i32 {
i32::try_from((STRIP_PIXELS / u64::from(width.max(1))).max(1)).unwrap_or(i32::MAX)
/// Width and height of a block of [`composite_in_blocks`] for a `width`-pixel source: whole tiles,
/// the full width (up to [`BLOCK_PIXELS`] over one row of tiles) and as many rows of tiles as fit.
fn block_size(width: u32) -> (i32, i32) {
let tile = TILE_SIZE.unsigned_abs() as u64;
let across = u64::from(width.max(1)).div_ceil(tile) * tile;
let bw = across.min((BLOCK_PIXELS / tile / tile).max(1) * tile);
let bh = (BLOCK_PIXELS / bw / tile).max(1) * tile;
(i32::try_from(bw).unwrap_or(TILE_SIZE), i32::try_from(bh).unwrap_or(TILE_SIZE))
}
/// Runs the gray and 32-bit conversions a source needs to reach `fmt`; `raster` yields the
@@ -329,13 +346,15 @@ mod tests {
}
#[test]
fn strips_stay_within_their_pixel_budget_however_wide_the_source() {
fn blocks_are_whole_tiles_within_their_pixel_budget_however_wide_the_source() {
for width in [1u32, 255, 4096, 5000, 300_000, u32::MAX] {
let rows = strip_rows(width);
assert!(rows >= 1, "{width}: {rows}");
assert!(rows == 1 || u64::from(width) * rows as u64 <= STRIP_PIXELS, "{width} px × {rows} rows");
let (bw, bh) = block_size(width);
assert!(bw > 0 && bh > 0 && bw % TILE_SIZE == 0 && bh % TILE_SIZE == 0, "{width}: {bw}×{bh}");
assert!(u64::from(bw.unsigned_abs()) * u64::from(bh.unsigned_abs()) <= BLOCK_PIXELS, "{width}: {bw}×{bh}");
}
assert_eq!(strip_rows(4096), 256);
assert_eq!(block_size(4096), (4096, 256));
assert_eq!(block_size(1000), (1024, 1024));
assert_eq!(block_size(300_000), (4096, 256));
}
#[test]
@@ -108,14 +108,15 @@ fn an_rgb_source_in_a_cmyk_or_lab_document_matches() {
}
}
/// Wide enough that strips are 256 rows (on the compositor's tile grid) or 209 (off it): the
/// shadow, mask and blur cross the strip edges.
/// Blocks are 256 rows of the full width at 4096 px, and split at x = 4096 at 5000 px: the shadow
/// and mask cross the block edges both ways.
#[test]
fn effects_across_strip_edges_match() {
fn effects_across_block_edges_match() {
for (width, depth) in [(4096, SampleType::U8), (4096, SampleType::F32), (5000, SampleType::U8)] {
let mut doc = Document::new("wide", Size::new(width, 700), ColorMode::Rgb, depth);
let fmt = doc.pixel_format();
let mut base = Layer::new("base", LayerContent::Raster(block(&fmt, Rect::new(100, 200, 900, 560))));
let x0 = if width > 4096 { 3800 } else { 100 };
let mut base = Layer::new("base", LayerContent::Raster(block(&fmt, Rect::new(x0, 200, x0 + 800, 560))));
let mut glow = photocraft_doc::Effect::default_drop_shadow();
if let photocraft_doc::Effect::DropShadow(d) = &mut glow {
d.size = 40.0;
@@ -127,6 +128,6 @@ fn effects_across_strip_edges_match() {
mask.surface.fill_rect(Rect::new(0, 240, width as i32, 270), &[0.3]);
base.mask = Some(mask);
doc.layers = vec![base];
same(&format!("strips {width} {depth:?}"), &doc, fmt);
same(&format!("blocks {width} {depth:?}"), &doc, fmt);
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
//! #10030: decoding a smart object's source (what its first re-render does) composites it in
//! strips, so the peak memory growth stays a small multiple of the source's pixels, not the 32
//! tile-aligned blocks, so the peak memory growth stays a small multiple of the source's pixels, not the 32
//! bytes a pixel the full-size RGBA buffer and its f32 copy took on top of the decoded document.
//!
//! The one test here runs in its own process: it reads the process's peak RSS (`VmHWM`, Linux