use crate::engine::{BlankTileStrategy, EngineConfig, EngineError, EngineResult};
use crate::observe::{EngineEvent, EngineObserver, MemoryTracker};
use crate::pixel::PixelFormat;
use crate::planner::{Layout, PyramidPlan, TileCoord};
use crate::raster::{Raster, RasterError};
use crate::resize;
use crate::sink::{Tile, TileSink};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BudgetPolicy {
#[default]
Error,
AutoAdjustDpi {
min_dpi: u32,
},
}
#[derive(Debug, Clone)]
pub struct StreamingConfig {
pub memory_budget_bytes: u64,
pub engine: EngineConfig,
pub budget_policy: BudgetPolicy,
}
impl Default for StreamingConfig {
fn default() -> Self {
Self {
memory_budget_bytes: u64::MAX,
engine: EngineConfig::default(),
budget_policy: BudgetPolicy::Error,
}
}
}
pub trait StripSource: Send + Sync {
fn render_strip(&self, y_offset: u32, height: u32) -> Result<Raster, EngineError>;
fn width(&self) -> u32;
fn height(&self) -> u32;
fn format(&self) -> PixelFormat;
}
pub struct RasterStripSource<'a> {
raster: &'a Raster,
}
impl<'a> RasterStripSource<'a> {
pub fn new(raster: &'a Raster) -> Self {
Self { raster }
}
}
impl<'a> StripSource for RasterStripSource<'a> {
fn render_strip(&self, y_offset: u32, height: u32) -> Result<Raster, EngineError> {
let h = height.min(self.raster.height() - y_offset);
self.raster
.extract(0, y_offset, self.raster.width(), h)
.map_err(EngineError::from)
}
fn width(&self) -> u32 {
self.raster.width()
}
fn height(&self) -> u32 {
self.raster.height()
}
fn format(&self) -> PixelFormat {
self.raster.format()
}
}
#[cfg(feature = "pdfium")]
pub struct PdfiumStripSource {
width: u32,
height: u32,
dpi: u32,
full_raster: Raster,
}
#[cfg(feature = "pdfium")]
impl std::fmt::Debug for PdfiumStripSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PdfiumStripSource")
.field("width", &self.width)
.field("height", &self.height)
.field("dpi", &self.dpi)
.finish()
}
}
#[cfg(feature = "pdfium")]
impl PdfiumStripSource {
pub fn new(
path: impl Into<std::path::PathBuf>,
page: usize,
dpi: u32,
) -> Result<Self, crate::pdf::PdfError> {
let path = path.into();
let full = crate::pdf::render_page_pdfium(&path, page, dpi)?;
let width = full.width();
let height = full.height();
Ok(Self {
width,
height,
dpi,
full_raster: full,
})
}
pub fn new_with_budget(
path: impl Into<std::path::PathBuf>,
page: usize,
dpi_hint: u32,
min_strip_height: u32,
budget_bytes: u64,
policy: BudgetPolicy,
) -> Result<Self, crate::pdf::PdfError> {
let path = path.into();
let pdfium = crate::pdf::init_pdfium()?;
let resolved_dpi = match policy {
BudgetPolicy::Error => {
let (width, _, _, _) = probe_page_dims(pdfium, &path, page, dpi_hint)?;
let strip_bytes = width as u64 * min_strip_height as u64 * 4;
if strip_bytes > budget_bytes {
return Err(crate::pdf::PdfError::BudgetExceeded {
strip_bytes,
budget_bytes,
dpi: dpi_hint,
});
}
dpi_hint
}
BudgetPolicy::AutoAdjustDpi { min_dpi } => resolve_dpi_under_budget(
pdfium,
&path,
page,
dpi_hint,
min_dpi,
min_strip_height,
budget_bytes,
)?,
};
let full = crate::pdf::render_page_pdfium(&path, page, resolved_dpi)?;
let width = full.width();
let height = full.height();
Ok(Self {
width,
height,
dpi: resolved_dpi,
full_raster: full,
})
}
pub fn dpi(&self) -> u32 {
self.dpi
}
fn render_strip_inner(
&self,
y_offset: u32,
height: u32,
) -> Result<Raster, crate::pdf::PdfError> {
let strip_h = height.min(self.height.saturating_sub(y_offset));
if strip_h == 0 {
let data = vec![0u8; self.width as usize * height as usize * 4];
return Raster::new(self.width, height, PixelFormat::Rgba8, data)
.map_err(crate::pdf::PdfError::from);
}
self.full_raster
.extract(0, y_offset, self.width, strip_h)
.map_err(crate::pdf::PdfError::from)
}
}
#[cfg(feature = "pdfium")]
fn probe_page_dims(
pdfium: &pdfium_render::prelude::Pdfium,
path: &std::path::Path,
page: usize,
dpi: u32,
) -> Result<(u32, u32, f32, f32), crate::pdf::PdfError> {
let document = pdfium
.load_pdf_from_file(path, None)
.map_err(|e| crate::pdf::PdfError::Pdfium(e.to_string()))?;
let pages = document.pages();
let total = pages.len();
if page == 0 || page > total as usize {
return Err(crate::pdf::PdfError::PageOutOfRange {
page,
total: total as usize,
});
}
let pdf_page = pages
.get(page as u16 - 1)
.map_err(|e| crate::pdf::PdfError::Pdfium(e.to_string()))?;
let scale = dpi as f32 / 72.0;
let w_pts = pdf_page.width().value;
let h_pts = pdf_page.height().value;
let width = (w_pts * scale) as u32;
let height = (h_pts * scale) as u32;
Ok((width, height, w_pts, h_pts))
}
#[cfg(feature = "pdfium")]
fn resolve_dpi_under_budget(
pdfium: &pdfium_render::prelude::Pdfium,
path: &std::path::Path,
page: usize,
dpi_hint: u32,
min_dpi: u32,
min_strip_height: u32,
budget_bytes: u64,
) -> Result<u32, crate::pdf::PdfError> {
if min_dpi == 0 {
return Err(crate::pdf::PdfError::Pdfium(
"min_dpi must be at least 1".into(),
));
}
let (anchor_w, _, _, _) = probe_page_dims(pdfium, path, page, dpi_hint)?;
if anchor_w == 0 {
return Err(crate::pdf::PdfError::Pdfium(
"page width is 0; cannot resolve DPI".into(),
));
}
let strip_at_hint = anchor_w as u64 * min_strip_height as u64 * 4;
if strip_at_hint <= budget_bytes {
return Ok(dpi_hint);
}
let ratio = budget_bytes as f64 / strip_at_hint as f64;
let resolved = (dpi_hint as f64 * ratio).floor() as u32;
let resolved = resolved.max(1);
if resolved < min_dpi {
return Err(crate::pdf::PdfError::BudgetExceeded {
strip_bytes: strip_at_hint,
budget_bytes,
dpi: dpi_hint,
});
}
let (verified_w, _, _, _) = probe_page_dims(pdfium, path, page, resolved)?;
let verified_strip = verified_w as u64 * min_strip_height as u64 * 4;
if verified_strip > budget_bytes {
let stepped = resolved.saturating_sub(1).max(1);
if stepped < min_dpi {
return Err(crate::pdf::PdfError::BudgetExceeded {
strip_bytes: verified_strip,
budget_bytes,
dpi: resolved,
});
}
let (vw2, _, _, _) = probe_page_dims(pdfium, path, page, stepped)?;
let strip2 = vw2 as u64 * min_strip_height as u64 * 4;
if strip2 > budget_bytes {
return Err(crate::pdf::PdfError::BudgetExceeded {
strip_bytes: strip2,
budget_bytes,
dpi: stepped,
});
}
Ok(stepped)
} else {
Ok(resolved)
}
}
#[cfg(feature = "pdfium")]
impl StripSource for PdfiumStripSource {
fn render_strip(&self, y_offset: u32, height: u32) -> Result<Raster, EngineError> {
self.render_strip_inner(y_offset, height)
.map_err(|e| EngineError::Sink(crate::sink::SinkError::Other(e.to_string())))
}
fn width(&self) -> u32 {
self.width
}
fn height(&self) -> u32 {
self.height
}
fn format(&self) -> PixelFormat {
PixelFormat::Rgba8
}
}
pub fn compute_strip_height(plan: &PyramidPlan, format: PixelFormat, budget: u64) -> Option<u32> {
let ch = plan.canvas_height as u64;
let ts = plan.tile_size;
let unit = 2 * ts;
if plan.canvas_width == 0 || unit == 0 {
return None;
}
let cost_per_unit = estimate_streaming_memory(plan, format, unit);
if cost_per_unit == 0 {
return None;
}
let max_units = budget / cost_per_unit;
if max_units == 0 {
return None;
}
let strip_height = (max_units * u64::from(unit)).min(ch) as u32;
let strip_height = (strip_height / unit) * unit;
if strip_height == 0 {
return None;
}
Some(strip_height)
}
pub fn estimate_streaming_memory(
plan: &PyramidPlan,
format: PixelFormat,
strip_height: u32,
) -> u64 {
plan.estimate_streaming_peak_memory(format, strip_height)
}
pub(crate) fn generate_pyramid_streaming(
source: &dyn StripSource,
plan: &PyramidPlan,
sink: &dyn TileSink,
config: &StreamingConfig,
observer: &dyn EngineObserver,
) -> Result<EngineResult, EngineError> {
let format = source.format();
let bpp = format.bytes_per_pixel();
let min_strip_height = 2 * plan.tile_size;
let worst_case_strip_bytes = plan.canvas_width as u64 * min_strip_height as u64 * bpp as u64;
if worst_case_strip_bytes > config.memory_budget_bytes {
return Err(EngineError::BudgetExceeded {
strip_bytes: worst_case_strip_bytes,
budget_bytes: config.memory_budget_bytes,
});
}
let strip_height =
compute_strip_height(plan, format, config.memory_budget_bytes).unwrap_or(min_strip_height);
let ch = plan.canvas_height;
let top_level = plan.levels.len() - 1;
let tracker = MemoryTracker::new();
let mut tiles_produced: u64 = 0;
let mut tiles_skipped: u64 = 0;
let mut accumulators: Vec<Option<Raster>> = vec![None; plan.levels.len()];
let monolithic_threshold = find_monolithic_threshold(plan, format, strip_height);
let mut mono_accumulators: Vec<Vec<u8>> = plan.levels.iter().map(|_| Vec::new()).collect();
for level_idx in (0..plan.levels.len()).rev() {
let level = &plan.levels[level_idx];
observer.on_event(EngineEvent::LevelStarted {
level: level.level,
width: level.width,
height: level.height,
tile_count: level.tile_count(),
});
}
let total_strips = ch.div_ceil(strip_height);
let mut strip_index: u32 = 0;
let mut y: u32 = 0;
while y < ch {
let sh = strip_height.min(ch - y);
let strip = obtain_canvas_strip(source, plan, y, sh, &config.engine)?;
observer.on_event(EngineEvent::StripRendered {
strip_index,
total_strips,
});
strip_index += 1;
let strip_bytes = strip.data().len() as u64;
tracker.alloc(strip_bytes);
let (tp, ts_skip) = emit_strip_tiles(
&strip,
plan,
top_level as u32,
y,
sink,
&config.engine,
observer,
)?;
tiles_produced += tp;
tiles_skipped += ts_skip;
let half = resize::downscale_half(&strip)?;
tracker.dealloc(strip_bytes);
let half_bytes = half.data().len() as u64;
tracker.alloc(half_bytes);
propagate_down(
half,
top_level - 1,
y / 2,
&mut accumulators,
&mut mono_accumulators,
monolithic_threshold,
plan,
sink,
&config.engine,
observer,
&tracker,
&mut tiles_produced,
&mut tiles_skipped,
)?;
y += sh;
}
for level_idx in (monolithic_threshold + 1..plan.levels.len()).rev() {
if let Some(leftover) = accumulators[level_idx].take() {
let (_, lh) = if plan.layout == Layout::Google {
plan.canvas_size_at_level(plan.levels[level_idx].level)
} else {
(plan.levels[level_idx].width, plan.levels[level_idx].height)
};
let leftover_y = lh.saturating_sub(leftover.height());
let (tp, ts_skip) = emit_strip_tiles(
&leftover,
plan,
level_idx as u32,
leftover_y,
sink,
&config.engine,
observer,
)?;
tiles_produced += tp;
tiles_skipped += ts_skip;
if level_idx > 0 {
let further_half = resize::downscale_half(&leftover)?;
propagate_down(
further_half,
level_idx - 1,
leftover_y / 2,
&mut accumulators,
&mut mono_accumulators,
monolithic_threshold,
plan,
sink,
&config.engine,
observer,
&tracker,
&mut tiles_produced,
&mut tiles_skipped,
)?;
}
}
}
{
let top_mono = monolithic_threshold.min(plan.levels.len() - 1);
let mut prev_raster: Option<Raster> = None;
for level_idx in (0..=top_mono).rev() {
let level = &plan.levels[level_idx];
let (lw, lh) = if plan.layout == Layout::Google {
plan.canvas_size_at_level(level.level)
} else {
(level.width, level.height)
};
if lw == 0 || lh == 0 {
continue;
}
let raster = if let Some(prev) = prev_raster.take() {
resize::downscale_half(&prev)?
} else {
let mut acc_data = std::mem::take(&mut mono_accumulators[level_idx]);
if acc_data.is_empty() {
continue;
}
let expected = lw as usize * lh as usize * bpp;
if acc_data.len() > expected {
acc_data.truncate(expected);
}
if acc_data.len() < expected {
let filled_rows = acc_data.len() / (lw as usize * bpp);
acc_data.resize(expected, 0);
fill_background_rows(
&mut acc_data,
filled_rows,
lw,
lh,
bpp,
config.engine.background_rgb,
);
}
Raster::new(lw, lh, format, acc_data)?
};
let (tp, ts_skip) = emit_full_level_tiles(
&raster,
plan,
level_idx as u32,
sink,
&config.engine,
observer,
)?;
tiles_produced += tp;
tiles_skipped += ts_skip;
prev_raster = Some(raster);
}
}
for level in &plan.levels {
observer.on_event(EngineEvent::LevelCompleted {
level: level.level,
tiles_produced: level.tile_count(),
});
}
sink.finish()?;
observer.on_event(EngineEvent::Finished {
total_tiles: tiles_produced,
levels: plan.levels.len() as u32,
});
Ok(EngineResult {
tiles_produced,
tiles_skipped,
levels_processed: plan.levels.len() as u32,
peak_memory_bytes: tracker.peak_bytes(),
bytes_read: 0,
bytes_written: 0,
retry_count: 0,
queue_pressure_peak: 0,
duration: std::time::Duration::ZERO,
stage_durations: crate::engine::StageDurations::default(),
skipped_due_to_failure: 0,
})
}
pub(crate) fn find_monolithic_threshold(
plan: &PyramidPlan,
format: PixelFormat,
strip_height: u32,
) -> usize {
let bpp = format.bytes_per_pixel() as u64;
let strip_budget = plan.canvas_width as u64 * strip_height as u64 * bpp;
for level_idx in (0..plan.levels.len()).rev() {
let (lw, lh) = if plan.layout == Layout::Google {
plan.canvas_size_at_level(plan.levels[level_idx].level)
} else {
let lp = &plan.levels[level_idx];
(lp.width, lp.height)
};
let level_bytes = lw as u64 * lh as u64 * bpp;
if level_bytes <= strip_budget {
return level_idx;
}
}
0
}
pub(crate) fn obtain_canvas_strip(
source: &dyn StripSource,
plan: &PyramidPlan,
y: u32,
height: u32,
config: &EngineConfig,
) -> Result<Raster, EngineError> {
let cw = plan.canvas_width;
let format = source.format();
let bpp = format.bytes_per_pixel();
if plan.centre && (plan.centre_offset_x > 0 || plan.centre_offset_y > 0) {
embed_strip_in_canvas(source, plan, y, height, config.background_rgb)
} else if plan.layout == Layout::Google {
let src_h = source.height();
if y >= src_h {
let data = make_background_buffer(cw, height, bpp, config.background_rgb);
Raster::new(cw, height, format, data).map_err(EngineError::from)
} else {
let avail_h = (src_h - y).min(height);
let src_strip = source.render_strip(y, avail_h)?;
if src_strip.width() == cw && avail_h == height {
return Ok(src_strip);
}
let mut data = make_background_buffer(cw, height, bpp, config.background_rgb);
let src_row_bytes = src_strip.width() as usize * bpp;
let dst_stride = cw as usize * bpp;
for row in 0..avail_h as usize {
let src_start = row * src_row_bytes;
let dst_start = row * dst_stride;
data[dst_start..dst_start + src_row_bytes]
.copy_from_slice(&src_strip.data()[src_start..src_start + src_row_bytes]);
}
Raster::new(cw, height, format, data).map_err(EngineError::from)
}
} else {
let src_h = source.height();
let avail_h = (src_h - y).min(height);
source.render_strip(y, avail_h)
}
}
fn embed_strip_in_canvas(
source: &dyn StripSource,
plan: &PyramidPlan,
canvas_y: u32,
strip_h: u32,
background_rgb: [u8; 3],
) -> Result<Raster, EngineError> {
let cw = plan.canvas_width;
let format = source.format();
let bpp = format.bytes_per_pixel();
let ox = plan.centre_offset_x;
let oy = plan.centre_offset_y;
let src_w = source.width();
let src_h = source.height();
let mut data = make_background_buffer(cw, strip_h, bpp, background_rgb);
let strip_top = canvas_y;
let strip_bottom = canvas_y + strip_h;
let img_top = oy;
let img_bottom = oy + src_h;
let inter_top = strip_top.max(img_top);
let inter_bottom = strip_bottom.min(img_bottom);
if inter_top < inter_bottom {
let src_y = inter_top - oy;
let src_rows = inter_bottom - inter_top;
let src_strip = source.render_strip(src_y, src_rows)?;
let dst_stride = cw as usize * bpp;
let src_row_bytes = src_w.min(src_strip.width()) as usize * bpp;
let local_y = (inter_top - canvas_y) as usize;
for row in 0..src_rows as usize {
let src_start = row * src_strip.stride();
let dst_start = (local_y + row) * dst_stride + ox as usize * bpp;
let copy_len = src_row_bytes.min(data.len() - dst_start);
data[dst_start..dst_start + copy_len]
.copy_from_slice(&src_strip.data()[src_start..src_start + copy_len]);
}
}
Raster::new(cw, strip_h, format, data).map_err(EngineError::from)
}
pub(crate) fn make_background_buffer(
w: u32,
h: u32,
bpp: usize,
background_rgb: [u8; 3],
) -> Vec<u8> {
let size = w as usize * h as usize * bpp;
let mut buf = vec![0u8; size];
let bg_pixel: Vec<u8> = match bpp {
1 => vec![background_rgb[0]],
3 => background_rgb.to_vec(),
4 => vec![background_rgb[0], background_rgb[1], background_rgb[2], 255],
_ => vec![background_rgb[0]; bpp],
};
for pixel in buf.chunks_exact_mut(bpp) {
pixel.copy_from_slice(&bg_pixel);
}
buf
}
pub(crate) fn fill_background_rows(
buf: &mut [u8],
filled_rows: usize,
w: u32,
h: u32,
bpp: usize,
background_rgb: [u8; 3],
) {
let bg_pixel: Vec<u8> = match bpp {
1 => vec![background_rgb[0]],
3 => background_rgb.to_vec(),
4 => vec![background_rgb[0], background_rgb[1], background_rgb[2], 255],
_ => vec![background_rgb[0]; bpp],
};
let stride = w as usize * bpp;
for row in filled_rows..h as usize {
let start = row * stride;
let end = start + stride;
if end > buf.len() {
break;
}
for pixel in buf[start..end].chunks_exact_mut(bpp) {
pixel.copy_from_slice(&bg_pixel);
}
}
}
pub(crate) fn emit_strip_tiles(
strip: &Raster,
plan: &PyramidPlan,
level: u32,
strip_canvas_y: u32,
sink: &dyn TileSink,
config: &EngineConfig,
observer: &dyn EngineObserver,
) -> Result<(u64, u64), EngineError> {
let level_plan = &plan.levels[level as usize];
let ts = plan.tile_size;
let blank_strategy = config.blank_tile_strategy;
let first_row = strip_canvas_y / ts;
let last_row = (strip_canvas_y + strip.height()).div_ceil(ts);
let last_row = last_row.min(level_plan.rows);
let mut count = 0u64;
let mut skipped = 0u64;
for row in first_row..last_row {
for col in 0..level_plan.cols {
let coord = TileCoord::new(level, col, row);
let tile_raster =
extract_tile_from_strip(strip, plan, coord, strip_canvas_y, config.background_rgb)?;
let blank = matches!(
blank_strategy,
BlankTileStrategy::Placeholder | BlankTileStrategy::PlaceholderWithTolerance { .. }
) && match blank_strategy {
BlankTileStrategy::PlaceholderWithTolerance { max_channel_delta } => {
crate::engine::is_blank_tile_with_tolerance(&tile_raster, max_channel_delta)
}
_ => crate::engine::is_blank_tile(&tile_raster),
};
if blank {
skipped += 1;
}
sink.write_tile(&Tile {
coord,
raster: tile_raster,
blank,
})?;
observer.on_event(EngineEvent::TileCompleted { coord });
count += 1;
}
}
Ok((count, skipped))
}
pub(crate) fn extract_tile_from_strip(
strip: &Raster,
plan: &PyramidPlan,
coord: TileCoord,
strip_canvas_y: u32,
background_rgb: [u8; 3],
) -> Result<Raster, RasterError> {
let rect = plan
.tile_rect(coord)
.expect("tile_rect returned None for valid coord");
let ts = plan.tile_size;
let bpp = strip.format().bytes_per_pixel();
let local_y = rect.y.saturating_sub(strip_canvas_y);
let strip_h = strip.height();
let strip_w = strip.width();
let avail_y_start = strip_canvas_y.saturating_sub(rect.y);
let avail_h = if local_y + rect.height > strip_h {
strip_h.saturating_sub(local_y)
} else {
rect.height
};
let tile_w = if plan.layout == Layout::Google {
ts
} else {
rect.width
};
let tile_h = if plan.layout == Layout::Google {
ts
} else {
rect.height
};
let avail_x = rect.x.min(strip_w);
let avail_w = (rect.x + rect.width).min(strip_w).saturating_sub(avail_x);
let needs_edge_pad = |dim: u32| -> u32 {
if plan.layout == Layout::Google || (plan.overlap == 0 && dim < ts) {
ts
} else {
dim
}
};
if avail_w == 0 || avail_h == 0 || avail_x >= strip_w || local_y >= strip_h {
let out_w = needs_edge_pad(tile_w);
let out_h = needs_edge_pad(tile_h);
let padded = make_background_buffer(out_w, out_h, bpp, background_rgb);
return Raster::new(out_w, out_h, strip.format(), padded);
}
if avail_x == rect.x
&& avail_w == tile_w
&& local_y == 0
&& avail_h == tile_h
&& avail_y_start == 0
{
let content = strip.extract(avail_x, local_y, tile_w, tile_h)?;
if plan.overlap == 0
&& plan.layout != Layout::Google
&& (content.width() < ts || content.height() < ts)
{
let mut padded = make_background_buffer(ts, ts, bpp, background_rgb);
let src_stride = content.width() as usize * bpp;
let dst_stride = ts as usize * bpp;
for row in 0..content.height() as usize {
let src_start = row * src_stride;
let dst_start = row * dst_stride;
padded[dst_start..dst_start + src_stride]
.copy_from_slice(&content.data()[src_start..src_start + src_stride]);
}
return Raster::new(ts, ts, strip.format(), padded);
}
return Ok(content);
}
let content = strip.extract(avail_x, local_y, avail_w, avail_h)?;
let out_w = needs_edge_pad(tile_w);
let out_h = needs_edge_pad(tile_h);
let mut padded = make_background_buffer(out_w, out_h, bpp, background_rgb);
let src_stride = avail_w as usize * bpp;
let dst_stride = out_w as usize * bpp;
let dx = (avail_x - rect.x.min(avail_x)) as usize * bpp;
let dy = avail_y_start as usize;
for row in 0..avail_h as usize {
let src_start = row * src_stride;
let dst_start = (row + dy) * dst_stride + dx;
if dst_start + src_stride <= padded.len() {
padded[dst_start..dst_start + src_stride]
.copy_from_slice(&content.data()[src_start..src_start + src_stride]);
}
}
Raster::new(out_w, out_h, strip.format(), padded)
}
pub(crate) fn emit_full_level_tiles(
raster: &Raster,
plan: &PyramidPlan,
level: u32,
sink: &dyn TileSink,
config: &EngineConfig,
observer: &dyn EngineObserver,
) -> Result<(u64, u64), EngineError> {
emit_strip_tiles(raster, plan, level, 0, sink, config, observer)
}
#[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)]
pub(crate) fn propagate_down(
half_strip: Raster,
level_idx: usize,
strip_y_at_level: u32,
accumulators: &mut Vec<Option<Raster>>,
mono_accumulators: &mut Vec<Vec<u8>>,
monolithic_threshold: usize,
plan: &PyramidPlan,
sink: &dyn TileSink,
config: &EngineConfig,
observer: &dyn EngineObserver,
tracker: &MemoryTracker,
tiles_produced: &mut u64,
tiles_skipped: &mut u64,
) -> Result<(), EngineError> {
if level_idx <= monolithic_threshold {
let acc = &mut mono_accumulators[level_idx];
acc.extend_from_slice(half_strip.data());
return Ok(());
}
match accumulators[level_idx].take() {
None => {
accumulators[level_idx] = Some(half_strip);
}
Some(prev) => {
let combined = concat_vertical(&prev, &half_strip)?;
let combined_y = strip_y_at_level.saturating_sub(prev.height());
let (tp, ts_skip) = emit_strip_tiles(
&combined,
plan,
level_idx as u32,
combined_y,
sink,
config,
observer,
)?;
*tiles_produced += tp;
*tiles_skipped += ts_skip;
if level_idx > 0 {
let further_half = resize::downscale_half(&combined)?;
propagate_down(
further_half,
level_idx - 1,
combined_y / 2,
accumulators,
mono_accumulators,
monolithic_threshold,
plan,
sink,
config,
observer,
tracker,
tiles_produced,
tiles_skipped,
)?;
}
}
}
Ok(())
}
pub(crate) fn concat_vertical(top: &Raster, bottom: &Raster) -> Result<Raster, RasterError> {
debug_assert_eq!(top.width(), bottom.width());
debug_assert_eq!(top.format(), bottom.format());
let w = top.width();
let h = top.height() + bottom.height();
let mut data = Vec::with_capacity(top.data().len() + bottom.data().len());
data.extend_from_slice(top.data());
data.extend_from_slice(bottom.data());
Raster::new(w, h, top.format(), data)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::observe::NoopObserver;
use crate::planner::{Layout, PyramidPlanner};
use crate::sink::MemorySink;
fn gradient_raster(w: u32, h: u32) -> Raster {
let bpp = PixelFormat::Rgb8.bytes_per_pixel();
let mut data = vec![0u8; w as usize * h as usize * bpp];
for y in 0..h {
for x in 0..w {
let off = (y as usize * w as usize + x as usize) * bpp;
data[off] = (x % 256) as u8;
data[off + 1] = (y % 256) as u8;
data[off + 2] = ((x + y) % 256) as u8;
}
}
Raster::new(w, h, PixelFormat::Rgb8, data).unwrap()
}
fn solid_raster(w: u32, h: u32, val: u8) -> Raster {
let data = vec![val; w as usize * h as usize * 3];
Raster::new(w, h, PixelFormat::Rgb8, data).unwrap()
}
#[test]
fn compute_strip_height_basic() {
let planner = PyramidPlanner::new(512, 512, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sh = compute_strip_height(&plan, PixelFormat::Rgb8, u64::MAX);
assert!(sh.is_some());
let sh = sh.unwrap();
assert!(sh >= plan.tile_size * 2);
assert!(sh <= plan.canvas_height);
}
#[test]
fn compute_strip_height_tight_budget() {
let planner = PyramidPlanner::new(1024, 1024, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sh = compute_strip_height(&plan, PixelFormat::Rgb8, 1);
assert!(sh.is_none());
}
#[test]
fn compute_strip_height_is_multiple_of_2ts() {
let planner = PyramidPlanner::new(2048, 2048, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let budget = 50_000_000u64; if let Some(sh) = compute_strip_height(&plan, PixelFormat::Rgb8, budget) {
assert_eq!(sh % (2 * plan.tile_size), 0);
}
}
#[test]
fn estimate_streaming_memory_monotonic() {
let planner = PyramidPlanner::new(1024, 1024, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let m1 = estimate_streaming_memory(&plan, PixelFormat::Rgb8, 512);
let m2 = estimate_streaming_memory(&plan, PixelFormat::Rgb8, 1024);
assert!(m2 >= m1, "Larger strip should use more memory");
}
#[test]
fn estimate_peak_memory_for_format_scales_with_bpp() {
let planner = PyramidPlanner::new(1024, 1024, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let rgb = plan.estimate_peak_memory_for_format(PixelFormat::Rgb8);
let rgba = plan.estimate_peak_memory_for_format(PixelFormat::Rgba8);
assert!(rgba > rgb);
}
#[test]
fn streaming_parity_deepzoom_small() {
let src = gradient_raster(256, 256);
let planner = PyramidPlanner::new(256, 256, 128, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let ref_sink = MemorySink::new();
crate::engine::generate_pyramid_observed(
&src,
&plan,
&ref_sink,
&EngineConfig::default(),
&NoopObserver,
)
.unwrap();
let mut ref_tiles = ref_sink.tiles();
ref_tiles.sort_by_key(|t| (t.coord.level, t.coord.row, t.coord.col));
let sink = MemorySink::new();
let config = StreamingConfig {
memory_budget_bytes: u64::MAX,
engine: EngineConfig::default(),
budget_policy: BudgetPolicy::Error,
};
generate_pyramid_streaming(
&RasterStripSource::new(&src),
&plan,
&sink,
&config,
&NoopObserver,
)
.unwrap();
let mut tiles = sink.tiles();
tiles.sort_by_key(|t| (t.coord.level, t.coord.row, t.coord.col));
assert_eq!(ref_tiles.len(), tiles.len(), "tile count mismatch");
for (r, t) in ref_tiles.iter().zip(tiles.iter()) {
assert_eq!(r.coord, t.coord);
assert_eq!(r.data, t.data, "Tile data mismatch at {:?}", t.coord);
}
}
#[test]
fn streaming_produces_all_tiles_deepzoom() {
let src = gradient_raster(500, 300);
let planner = PyramidPlanner::new(500, 300, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let config = StreamingConfig {
memory_budget_bytes: 1_000_000, engine: EngineConfig::default(),
budget_policy: BudgetPolicy::Error,
};
let strip_src = RasterStripSource::new(&src);
let result =
generate_pyramid_streaming(&strip_src, &plan, &sink, &config, &NoopObserver).unwrap();
assert_eq!(
result.tiles_produced,
plan.total_tile_count(),
"Not all tiles produced"
);
}
#[test]
fn streaming_produces_all_tiles_google_centre() {
let src = gradient_raster(500, 300);
let planner = PyramidPlanner::new(500, 300, 256, 0, Layout::Google)
.unwrap()
.with_centre(true);
let plan = planner.plan();
let sink = MemorySink::new();
let config = StreamingConfig {
memory_budget_bytes: 2_000_000,
engine: EngineConfig::default(),
budget_policy: BudgetPolicy::Error,
};
let strip_src = RasterStripSource::new(&src);
let result =
generate_pyramid_streaming(&strip_src, &plan, &sink, &config, &NoopObserver).unwrap();
assert_eq!(
result.tiles_produced,
plan.total_tile_count(),
"Not all tiles produced for Google+centre"
);
}
#[test]
fn auto_selects_monolithic_for_large_budget() {
let src = gradient_raster(128, 128);
let planner = PyramidPlanner::new(128, 128, 64, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let ref_sink = MemorySink::new();
crate::engine::generate_pyramid_observed(
&src,
&plan,
&ref_sink,
&EngineConfig::default(),
&NoopObserver,
)
.unwrap();
let mut ref_tiles = ref_sink.tiles();
ref_tiles.sort_by_key(|t| (t.coord.level, t.coord.row, t.coord.col));
let sink = MemorySink::new();
let config = StreamingConfig {
memory_budget_bytes: u64::MAX,
engine: EngineConfig::default(),
budget_policy: BudgetPolicy::Error,
};
generate_pyramid_streaming(
&RasterStripSource::new(&src),
&plan,
&sink,
&config,
&NoopObserver,
)
.unwrap();
let mut tiles = sink.tiles();
tiles.sort_by_key(|t| (t.coord.level, t.coord.row, t.coord.col));
assert_eq!(ref_tiles.len(), tiles.len());
for (r, t) in ref_tiles.iter().zip(tiles.iter()) {
assert_eq!(r.coord, t.coord);
assert_eq!(r.data, t.data, "Tile data mismatch at {:?}", t.coord);
}
}
#[test]
fn auto_selects_streaming_for_small_budget() {
let src = gradient_raster(512, 512);
let planner = PyramidPlanner::new(512, 512, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let config = StreamingConfig {
memory_budget_bytes: 800_000,
engine: EngineConfig::default(),
budget_policy: BudgetPolicy::Error,
};
let result = generate_pyramid_streaming(
&RasterStripSource::new(&src),
&plan,
&sink,
&config,
&NoopObserver,
)
.unwrap();
assert_eq!(result.tiles_produced, plan.total_tile_count());
}
#[test]
fn concat_vertical_works() {
let top = solid_raster(4, 2, 100);
let bottom = solid_raster(4, 3, 200);
let combined = concat_vertical(&top, &bottom).unwrap();
assert_eq!(combined.width(), 4);
assert_eq!(combined.height(), 5);
let bpp = 3;
let stride = 4 * bpp;
for row in 0..2 {
for byte in &combined.data()[row * stride..(row + 1) * stride] {
assert_eq!(*byte, 100);
}
}
for row in 2..5 {
for byte in &combined.data()[row * stride..(row + 1) * stride] {
assert_eq!(*byte, 200);
}
}
}
#[test]
fn raster_strip_source_extracts_correctly() {
let src = gradient_raster(100, 200);
let strip_src = RasterStripSource::new(&src);
assert_eq!(strip_src.width(), 100);
assert_eq!(strip_src.height(), 200);
let strip = strip_src.render_strip(50, 30).unwrap();
assert_eq!(strip.width(), 100);
assert_eq!(strip.height(), 30);
assert_eq!(strip.data()[0], 0); assert_eq!(strip.data()[1], 50); assert_eq!(strip.data()[2], 50); }
#[test]
fn streaming_emits_strip_rendered_events() {
use crate::observe::CollectingObserver;
let src = gradient_raster(512, 512);
let planner = PyramidPlanner::new(512, 512, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let config = StreamingConfig {
memory_budget_bytes: 800_000,
engine: EngineConfig::default(),
budget_policy: BudgetPolicy::Error,
};
let observer = CollectingObserver::new();
let strip_src = RasterStripSource::new(&src);
generate_pyramid_streaming(&strip_src, &plan, &sink, &config, &observer).unwrap();
let strip_events: Vec<_> = observer
.events()
.into_iter()
.filter(|e| matches!(e, EngineEvent::StripRendered { .. }))
.collect();
assert!(!strip_events.is_empty(), "expected StripRendered events");
for (i, e) in strip_events.iter().enumerate() {
if let EngineEvent::StripRendered {
strip_index,
total_strips,
} = e
{
assert_eq!(*strip_index, i as u32);
assert_eq!(*total_strips, strip_events.len() as u32);
}
}
}
#[test]
fn streaming_odd_dimensions() {
let src = gradient_raster(500, 300);
let planner = PyramidPlanner::new(500, 300, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let config = StreamingConfig {
memory_budget_bytes: 800_000,
engine: EngineConfig::default(),
budget_policy: BudgetPolicy::Error,
};
let strip_src = RasterStripSource::new(&src);
let result =
generate_pyramid_streaming(&strip_src, &plan, &sink, &config, &NoopObserver).unwrap();
assert_eq!(result.tiles_produced, plan.total_tile_count());
}
}