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)]
pub struct StreamingConfig {
pub memory_budget_bytes: u64,
pub engine: EngineConfig,
}
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 {
raster: std::sync::Mutex<Option<Raster>>,
path: std::path::PathBuf,
page: usize,
dpi: u32,
full_width: u32,
full_height: u32,
}
#[cfg(feature = "pdfium")]
impl PdfiumStripSource {
pub fn new(
path: impl Into<std::path::PathBuf>,
page: usize,
dpi: u32,
full_width: u32,
full_height: u32,
) -> Self {
Self {
raster: std::sync::Mutex::new(None),
path: path.into(),
page,
dpi,
full_width,
full_height,
}
}
fn ensure_rendered(&self) -> Result<(), EngineError> {
let mut guard = self.raster.lock().unwrap();
if guard.is_some() {
return Ok(());
}
let raster = crate::pdf::render_page_pdfium(&self.path, self.page, self.dpi)
.map_err(|e| EngineError::Sink(crate::sink::SinkError::Other(e.to_string())))?;
*guard = Some(raster);
Ok(())
}
fn extract_strip(&self, y_offset: u32, height: u32) -> Result<Raster, EngineError> {
let guard = self.raster.lock().unwrap();
let raster = guard
.as_ref()
.expect("ensure_rendered must be called first");
let h = height.min(raster.height().saturating_sub(y_offset));
if h == 0 {
let bpp = raster.format().bytes_per_pixel();
let data = vec![255u8; self.full_width as usize * height as usize * bpp];
return Raster::new(self.full_width, height, raster.format(), data)
.map_err(EngineError::from);
}
raster
.extract(0, y_offset, raster.width(), h)
.map_err(EngineError::from)
}
}
#[cfg(feature = "pdfium")]
impl StripSource for PdfiumStripSource {
fn render_strip(&self, y_offset: u32, height: u32) -> Result<Raster, EngineError> {
self.ensure_rendered()?;
self.extract_strip(y_offset, height)
}
fn width(&self) -> u32 {
self.full_width
}
fn height(&self) -> u32 {
self.full_height
}
fn format(&self) -> PixelFormat {
let guard = self.raster.lock().unwrap();
guard
.as_ref()
.map(|r| r.format())
.unwrap_or(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 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 strip_height = match compute_strip_height(plan, format, config.memory_budget_bytes) {
Some(h) => h,
None => {
2 * plan.tile_size
}
};
let ch = plan.canvas_height;
let top_level = plan.levels.len() - 1;
let tracker = MemoryTracker::new();
let bpp = format.bytes_per_pixel();
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(),
})
}
pub fn generate_pyramid_auto(
source: &Raster,
plan: &PyramidPlan,
sink: &dyn TileSink,
config: &StreamingConfig,
observer: &dyn EngineObserver,
) -> Result<EngineResult, EngineError> {
let mono_peak = plan.estimate_peak_memory_for_format(source.format());
if mono_peak <= config.memory_budget_bytes {
crate::engine::generate_pyramid_observed(source, plan, sink, &config.engine, observer)
} else {
let strip_source = RasterStripSource::new(source);
generate_pyramid_streaming(&strip_source, plan, sink, config, observer)
}
}
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 use_placeholders = config.blank_tile_strategy == BlankTileStrategy::Placeholder;
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 = use_placeholders && 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(&src, &plan, &ref_sink, &EngineConfig::default()).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(),
};
generate_pyramid_auto(&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(),
};
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(),
};
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(&src, &plan, &ref_sink, &EngineConfig::default()).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(),
};
generate_pyramid_auto(&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: 1_000, engine: EngineConfig::default(),
};
let result = generate_pyramid_auto(&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: 1_000, engine: EngineConfig::default(),
};
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: 500_000,
engine: EngineConfig::default(),
};
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());
}
}