From d6c2a694ec3bfc15e9d5facb672078d7b983a113 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:15:55 +0900 Subject: [PATCH] M3: rendered pixels are moved out of the pixmap, not copied (#404) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `render_page` copied every raster out of vello_cpu's pixmap with `data_as_u8_slice().to_vec()`. That is a second buffer the size of the raster (up to 256 MB for a 64 MP page), alive alongside the first for as long as the copy runs. `Pixmap::take` hands over the pixel vector, and `bytemuck` reinterprets its four one-byte channels as bytes in place: the same bytes, in the same buffer. On a 6182×8000 px page, peak RSS goes from about 425 MB to 246 MB and render time from 102 ms to 81 ms. Pixels are identical: every page of #260's test file, at two scales and as a tile, hashes the same before and after. Co-authored-by: Claude Opus 5.5 --- Cargo.lock | 1 + crates/render/Cargo.toml | 2 ++ crates/render/src/raster.rs | 22 +++++++++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ca64fd79..d43c0e0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3745,6 +3745,7 @@ dependencies = [ name = "pdfcraft-render" version = "0.4.0" dependencies = [ + "bytemuck", "getrandom 0.4.3", "hayro", "hayro-syntax", diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index fa68a064..0a42520e 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -16,6 +16,8 @@ lopdf = { workspace = true } kurbo = { workspace = true } thiserror = { workspace = true } log = { workspace = true } +# Moves rendered pixels out of the renderer's pixmap without copying; already in the tree through hayro. +bytemuck = { version = "1", features = ["extern_crate_alloc"] } # lopdf (bootstrap) draws random bytes for encryption; on the web they come from the browser. [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index 716d1ddd..f9512df4 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -17,6 +17,7 @@ use hayro::hayro_interpret::InterpreterSettings; use hayro::hayro_interpret::font::{FontData, FontQuery}; use hayro::hayro_interpret::hayro_cmap::CidFamily; use hayro::hayro_syntax::Pdf; +use hayro::vello_cpu::Pixmap; use hayro::vello_cpu::color::palette::css::WHITE; use hayro::{RenderCache, RenderSettings, render}; @@ -222,7 +223,7 @@ fn render_page<'a>(pdf: &'a Pdf, cache: &RenderCache<'a>, settings: &Interpreter } }; let pixmap = render(page, cache, settings, &rs); - Ok((pixmap.width() as u32, pixmap.height() as u32, pixmap.data_as_u8_slice().to_vec(), None)) + Ok((pixmap.width() as u32, pixmap.height() as u32, into_bytes(pixmap), None)) })); match result { Ok(Ok(v)) => Ok(v), @@ -231,6 +232,13 @@ fn render_page<'a>(pdf: &'a Pdf, cache: &RenderCache<'a>, settings: &Interpreter } } +/// The pixmap's premultiplied RGBA bytes, moved out rather than copied (a page raster can be +/// hundreds of megabytes). Four one-byte channels always recast to bytes in place; the copy is +/// only there because the cast's signature allows failure. +fn into_bytes(pixmap: Pixmap) -> Vec { + bytemuck::allocation::try_cast_vec(pixmap.take()).unwrap_or_else(|(_, pixels)| pixels.iter().flat_map(|p| [p.r, p.g, p.b, p.a]).collect()) +} + fn finish(req: RenderRequest, start: Stopwatch, r: Result) -> RenderedPage { let millis = start.millis(); match r { @@ -732,6 +740,18 @@ trailer << /Root 1 0 R >> assert_eq!(px(80, 5), &[255, 255, 255, 255]); } + #[test] + fn rendered_pixels_are_moved_out_of_the_pixmap() { + let mut pixmap = Pixmap::new(3, 2); + for (i, b) in pixmap.data_as_u8_slice_mut().iter_mut().enumerate() { + *b = i as u8; + } + let (at, expected) = (pixmap.data_as_u8_slice().as_ptr(), pixmap.data_as_u8_slice().to_vec()); + let bytes = into_bytes(pixmap); + assert_eq!(bytes, expected); + assert_eq!(bytes.as_ptr(), at, "the pixmap's own buffer, not a copy"); + } + #[test] fn missing_page_and_garbage_input_fail_gracefully() { let mut r = PageRenderer::new(Arc::new(ONE_PAGE.to_vec()), RenderConfig::default());