use std::sync::Arc;
use thiserror::Error;
use crate::observe::{EngineEvent, EngineObserver, MemoryTracker, NoopObserver};
use crate::planner::{PyramidPlan, TileCoord};
use crate::raster::{Raster, RasterError};
use crate::resize;
use crate::sink::{SinkError, Tile, TileSink};
#[derive(Debug, Error)]
pub enum EngineError {
#[error("raster error: {0}")]
Raster(#[from] RasterError),
#[error("sink error: {0}")]
Sink(#[from] SinkError),
#[error("engine cancelled")]
Cancelled,
#[error("worker panicked")]
WorkerPanic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlankTileStrategy {
Emit,
Placeholder,
}
#[derive(Debug, Clone)]
pub struct EngineConfig {
pub concurrency: usize,
pub buffer_size: usize,
pub background_rgb: [u8; 3],
pub blank_tile_strategy: BlankTileStrategy,
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
concurrency: 0,
buffer_size: 64,
background_rgb: [255, 255, 255],
blank_tile_strategy: BlankTileStrategy::Emit,
}
}
}
impl EngineConfig {
pub fn with_concurrency(mut self, n: usize) -> Self {
self.concurrency = n;
self
}
pub fn with_buffer_size(mut self, n: usize) -> Self {
self.buffer_size = n;
self
}
pub fn with_blank_tile_strategy(mut self, strategy: BlankTileStrategy) -> Self {
self.blank_tile_strategy = strategy;
self
}
}
#[derive(Debug)]
pub struct EngineResult {
pub tiles_produced: u64,
pub tiles_skipped: u64,
pub levels_processed: u32,
pub peak_memory_bytes: u64,
}
pub fn generate_pyramid(
source: &Raster,
plan: &PyramidPlan,
sink: &dyn TileSink,
config: &EngineConfig,
) -> Result<EngineResult, EngineError> {
generate_pyramid_observed(source, plan, sink, config, &NoopObserver)
}
pub fn generate_pyramid_observed(
source: &Raster,
plan: &PyramidPlan,
sink: &dyn TileSink,
config: &EngineConfig,
observer: &dyn EngineObserver,
) -> Result<EngineResult, EngineError> {
let top_level = plan.levels.len() - 1;
let mut tiles_produced: u64 = 0;
let mut tiles_skipped: u64 = 0;
let tracker = MemoryTracker::new();
let mut current = if plan.centre && (plan.centre_offset_x > 0 || plan.centre_offset_y > 0) {
let canvas = embed_in_canvas(source, plan, config.background_rgb)?;
let canvas_bytes = canvas.data().len() as u64;
tracker.alloc(canvas_bytes);
canvas
} else {
let source_bytes = source.data().len() as u64;
tracker.alloc(source_bytes);
source.clone()
};
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(),
});
if level_idx < top_level {
let old_bytes = current.data().len() as u64;
current = resize::downscale_half(¤t)?;
let new_bytes = current.data().len() as u64;
tracker.dealloc(old_bytes);
tracker.alloc(new_bytes);
}
let (level_tiles, level_skipped) =
extract_and_emit_level(¤t, plan, level_idx as u32, sink, config, observer)?;
tiles_produced += level_tiles;
tiles_skipped += level_skipped;
observer.on_event(EngineEvent::LevelCompleted {
level: level.level,
tiles_produced: level_tiles,
});
}
tracker.dealloc(current.data().len() as u64);
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(),
})
}
fn embed_in_canvas(
source: &Raster,
plan: &PyramidPlan,
background_rgb: [u8; 3],
) -> Result<Raster, RasterError> {
let cw = plan.canvas_width;
let ch = plan.canvas_height;
let bpp = source.format().bytes_per_pixel();
let mut canvas = make_background_tile(cw, bpp, background_rgb);
let ox = plan.centre_offset_x as usize;
let oy = plan.centre_offset_y as usize;
let iw = source.width() as usize;
let src_stride = iw * bpp;
let dst_stride = cw as usize * bpp;
for row in 0..source.height() as usize {
let src_start = row * src_stride;
let dst_start = (row + oy) * dst_stride + ox * bpp;
canvas[dst_start..dst_start + src_stride]
.copy_from_slice(&source.data()[src_start..src_start + src_stride]);
}
Raster::new(cw, ch, source.format(), canvas)
}
fn extract_and_emit_level(
raster: &Raster,
plan: &PyramidPlan,
level: u32,
sink: &dyn TileSink,
config: &EngineConfig,
observer: &dyn EngineObserver,
) -> Result<(u64, u64), EngineError> {
let level_plan = &plan.levels[level as usize];
let use_placeholders = config.blank_tile_strategy == BlankTileStrategy::Placeholder;
if config.concurrency == 0 {
let mut count = 0u64;
let mut skipped = 0u64;
for row in 0..level_plan.rows {
for col in 0..level_plan.cols {
let coord = TileCoord::new(level, col, row);
let tile_raster = extract_tile(raster, plan, coord, config.background_rgb)?;
let blank = use_placeholders && 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))
} else {
extract_and_emit_parallel(raster, plan, level, sink, config, observer)
}
}
fn extract_and_emit_parallel(
raster: &Raster,
plan: &PyramidPlan,
level: u32,
sink: &dyn TileSink,
config: &EngineConfig,
observer: &dyn EngineObserver,
) -> Result<(u64, u64), EngineError> {
let level_plan = &plan.levels[level as usize];
let total_tiles = level_plan.tile_count();
if total_tiles == 0 {
return Ok((0, 0));
}
let use_placeholders = config.blank_tile_strategy == BlankTileStrategy::Placeholder;
let (tx, rx) = std::sync::mpsc::sync_channel::<Result<Tile, EngineError>>(config.buffer_size);
let raster = Arc::new(raster.clone());
let plan = Arc::new(plan.clone());
let coords: Vec<TileCoord> = (0..level_plan.rows)
.flat_map(|row| (0..level_plan.cols).map(move |col| TileCoord::new(level, col, row)))
.collect();
let concurrency = config.concurrency.min(coords.len());
let chunk_size = coords.len().div_ceil(concurrency);
std::thread::scope(|s| {
for chunk in coords.chunks(chunk_size) {
let tx = tx.clone();
let raster = Arc::clone(&raster);
let plan = Arc::clone(&plan);
let chunk = chunk.to_vec();
let bg = config.background_rgb;
s.spawn(move || {
for coord in chunk {
let result = extract_tile(&raster, &plan, coord, bg)
.map(|tile_raster| {
let blank = use_placeholders && is_blank_tile(&tile_raster);
Tile {
coord,
raster: tile_raster,
blank,
}
})
.map_err(EngineError::from);
if tx.send(result).is_err() {
break; }
}
});
}
drop(tx);
let mut count = 0u64;
let mut skipped = 0u64;
for result in rx {
let tile = result?;
let coord = tile.coord;
if tile.blank {
skipped += 1;
}
sink.write_tile(&tile)?;
observer.on_event(EngineEvent::TileCompleted { coord });
count += 1;
}
Ok((count, skipped))
})
}
fn make_background_tile(ts: u32, bpp: usize, background_rgb: [u8; 3]) -> Vec<u8> {
let mut padded = vec![0u8; ts as usize * ts as usize * bpp];
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 padded.chunks_exact_mut(bpp) {
pixel.copy_from_slice(&bg_pixel);
}
padded
}
fn extract_tile(
raster: &Raster,
plan: &PyramidPlan,
coord: TileCoord,
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 = raster.format().bytes_per_pixel();
if plan.layout == crate::planner::Layout::Google {
let rw = raster.width();
let rh = raster.height();
let inter_right = (rect.x + rect.width).min(rw);
let inter_bottom = (rect.y + rect.height).min(rh);
if rect.x >= rw || rect.y >= rh {
let padded = make_background_tile(ts, bpp, background_rgb);
return Raster::new(ts, ts, raster.format(), padded);
}
let inter_w = inter_right - rect.x;
let inter_h = inter_bottom - rect.y;
if inter_w == ts && inter_h == ts {
return raster.extract(rect.x, rect.y, ts, ts);
}
let content = raster.extract(rect.x, rect.y, inter_w, inter_h)?;
let mut padded = make_background_tile(ts, bpp, background_rgb);
let src_stride = inter_w as usize * bpp;
let dst_stride = ts as usize * bpp;
for row in 0..inter_h 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, raster.format(), padded);
}
let content = raster.extract(rect.x, rect.y, rect.width, rect.height)?;
if plan.overlap == 0 && (content.width() < ts || content.height() < ts) {
let mut padded = make_background_tile(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]);
}
Raster::new(ts, ts, content.format(), padded)
} else {
Ok(content)
}
}
pub fn is_blank_tile(raster: &Raster) -> bool {
let data = raster.data();
let bpp = raster.format().bytes_per_pixel();
if data.len() <= bpp {
return true;
}
let first_pixel = &data[..bpp];
data.chunks(bpp).all(|px| px == first_pixel)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::observe::CollectingObserver;
use crate::pixel::PixelFormat;
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 single_threaded_produces_all_tiles() {
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 = EngineConfig::default();
let result = generate_pyramid(&src, &plan, &sink, &config).unwrap();
assert_eq!(result.tiles_produced, plan.total_tile_count());
assert_eq!(sink.tile_count() as u64, plan.total_tile_count());
}
#[test]
fn parallel_produces_all_tiles() {
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 = EngineConfig::default().with_concurrency(4);
let result = generate_pyramid(&src, &plan, &sink, &config).unwrap();
assert_eq!(result.tiles_produced, plan.total_tile_count());
assert_eq!(sink.tile_count() as u64, plan.total_tile_count());
}
#[test]
fn all_tile_coords_present() {
let src = gradient_raster(600, 400);
let planner = PyramidPlanner::new(600, 400, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let config = EngineConfig::default().with_concurrency(2);
generate_pyramid(&src, &plan, &sink, &config).unwrap();
let tiles = sink.tiles();
let mut coords: Vec<_> = tiles.iter().map(|t| t.coord).collect();
coords.sort_by_key(|c| (c.level, c.row, c.col));
let mut expected: Vec<_> = plan.tile_coords().collect();
expected.sort_by_key(|c| (c.level, c.row, c.col));
assert_eq!(coords, expected);
}
#[test]
fn tile_dimensions_match_plan() {
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 = EngineConfig::default();
generate_pyramid(&src, &plan, &sink, &config).unwrap();
for tile in sink.tiles() {
let rect = plan.tile_rect(tile.coord).unwrap();
let expected_w = if rect.width < 256 { 256 } else { rect.width };
let expected_h = if rect.height < 256 { 256 } else { rect.height };
assert_eq!(tile.width, expected_w, "Width mismatch at {:?}", tile.coord);
assert_eq!(
tile.height, expected_h,
"Height mismatch at {:?}",
tile.coord
);
}
}
#[test]
fn deterministic_across_concurrency_levels() {
let src = gradient_raster(256, 256);
let planner = PyramidPlanner::new(256, 256, 64, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let ref_sink = MemorySink::new();
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));
for concurrency in [1, 2, 4, 8, 16] {
let sink = MemorySink::new();
let config = EngineConfig::default().with_concurrency(concurrency);
generate_pyramid(&src, &plan, &sink, &config).unwrap();
let mut tiles = sink.tiles();
tiles.sort_by_key(|t| (t.coord.level, t.coord.row, t.coord.col));
assert_eq!(
tiles.len(),
ref_tiles.len(),
"Tile count mismatch at concurrency={concurrency}"
);
for (ref_t, t) in ref_tiles.iter().zip(tiles.iter()) {
assert_eq!(ref_t.coord, t.coord);
assert_eq!(
ref_t.data, t.data,
"Tile data diverged at {:?} with concurrency={concurrency}",
t.coord
);
}
}
}
#[test]
fn levels_processed_matches_plan() {
let src = gradient_raster(64, 64);
let planner = PyramidPlanner::new(64, 64, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let result = generate_pyramid(&src, &plan, &sink, &EngineConfig::default()).unwrap();
assert_eq!(result.levels_processed, plan.level_count() as u32);
}
#[test]
fn small_image_single_tile() {
let src = gradient_raster(10, 10);
let planner = PyramidPlanner::new(10, 10, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let result = generate_pyramid(&src, &plan, &sink, &EngineConfig::default()).unwrap();
assert_eq!(result.tiles_produced, plan.level_count() as u64);
}
#[test]
fn backpressure_small_buffer() {
let src = gradient_raster(512, 512);
let planner = PyramidPlanner::new(512, 512, 128, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let config = EngineConfig::default()
.with_concurrency(4)
.with_buffer_size(1);
let result = generate_pyramid(&src, &plan, &sink, &config).unwrap();
assert_eq!(result.tiles_produced, plan.total_tile_count());
}
#[test]
fn is_blank_tile_solid() {
let r = solid_raster(8, 8, 128);
assert!(is_blank_tile(&r));
}
#[test]
fn is_blank_tile_not_blank() {
let mut data = vec![128u8; 8 * 8 * 3];
data[0] = 0;
let r = Raster::new(8, 8, PixelFormat::Rgb8, data).unwrap();
assert!(!is_blank_tile(&r));
}
#[test]
fn is_blank_tile_single_pixel() {
let r = Raster::new(1, 1, PixelFormat::Rgb8, vec![1, 2, 3]).unwrap();
assert!(is_blank_tile(&r));
}
#[test]
fn overlap_tiles_have_correct_size() {
let src = gradient_raster(600, 400);
let planner = PyramidPlanner::new(600, 400, 256, 2, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let config = EngineConfig::default();
generate_pyramid(&src, &plan, &sink, &config).unwrap();
for tile in sink.tiles() {
let rect = plan.tile_rect(tile.coord).unwrap();
assert_eq!(tile.width, rect.width);
assert_eq!(tile.height, rect.height);
}
}
#[test]
fn concurrent_with_slow_sink() {
use crate::sink::SlowSink;
use std::time::Duration;
let src = gradient_raster(128, 128);
let planner = PyramidPlanner::new(128, 128, 64, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = SlowSink::new(Duration::from_millis(1));
let config = EngineConfig::default()
.with_concurrency(4)
.with_buffer_size(2);
let result = generate_pyramid(&src, &plan, &sink, &config).unwrap();
assert_eq!(result.tiles_produced, plan.total_tile_count());
assert_eq!(sink.tile_count() as u64, plan.total_tile_count());
}
#[test]
fn observer_receives_all_tile_events() {
let src = gradient_raster(128, 128);
let planner = PyramidPlanner::new(128, 128, 64, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let obs = CollectingObserver::new();
generate_pyramid_observed(&src, &plan, &sink, &EngineConfig::default(), &obs).unwrap();
let tile_events = obs
.events()
.iter()
.filter(|e| matches!(e, EngineEvent::TileCompleted { .. }))
.count();
assert_eq!(tile_events as u64, plan.total_tile_count());
}
#[test]
fn observer_receives_level_events_in_order() {
let src = gradient_raster(64, 64);
let planner = PyramidPlanner::new(64, 64, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let obs = CollectingObserver::new();
generate_pyramid_observed(&src, &plan, &sink, &EngineConfig::default(), &obs).unwrap();
let events = obs.events();
let level_starts: Vec<u32> = events
.iter()
.filter_map(|e| match e {
EngineEvent::LevelStarted { level, .. } => Some(*level),
_ => None,
})
.collect();
let expected_levels: Vec<u32> = (0..plan.level_count() as u32).rev().collect();
assert_eq!(level_starts, expected_levels);
assert!(matches!(events.last(), Some(EngineEvent::Finished { .. })));
}
#[test]
fn observer_finished_event_has_correct_totals() {
let src = gradient_raster(256, 256);
let planner = PyramidPlanner::new(256, 256, 128, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let obs = CollectingObserver::new();
generate_pyramid_observed(&src, &plan, &sink, &EngineConfig::default(), &obs).unwrap();
let events = obs.events();
let finished = events.last().unwrap();
match finished {
EngineEvent::Finished {
total_tiles,
levels,
} => {
assert_eq!(*total_tiles, plan.total_tile_count());
assert_eq!(*levels, plan.level_count() as u32);
}
_ => panic!("Last event should be Finished"),
}
}
#[test]
fn observer_works_with_concurrency() {
let src = gradient_raster(256, 256);
let planner = PyramidPlanner::new(256, 256, 64, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let obs = CollectingObserver::new();
generate_pyramid_observed(
&src,
&plan,
&sink,
&EngineConfig::default().with_concurrency(4),
&obs,
)
.unwrap();
let tile_events = obs
.events()
.iter()
.filter(|e| matches!(e, EngineEvent::TileCompleted { .. }))
.count();
assert_eq!(tile_events as u64, plan.total_tile_count());
}
#[test]
fn peak_memory_is_reported() {
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 result = generate_pyramid(&src, &plan, &sink, &EngineConfig::default()).unwrap();
let source_bytes = 512 * 512 * 3;
assert!(
result.peak_memory_bytes >= source_bytes,
"Peak {} < source {source_bytes}",
result.peak_memory_bytes
);
}
#[test]
fn peak_memory_is_bounded() {
let src = gradient_raster(1024, 1024);
let planner = PyramidPlanner::new(1024, 1024, 256, 0, Layout::DeepZoom).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let result = generate_pyramid(&src, &plan, &sink, &EngineConfig::default()).unwrap();
let source_bytes = 1024u64 * 1024 * 3;
assert!(
result.peak_memory_bytes < source_bytes * 2,
"Peak {} >= 2x source {source_bytes}",
result.peak_memory_bytes
);
}
#[test]
fn google_centre_produces_all_tiles() {
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 = EngineConfig::default();
let result = generate_pyramid(&src, &plan, &sink, &config).unwrap();
assert_eq!(result.tiles_produced, plan.total_tile_count());
assert_eq!(sink.tile_count() as u64, plan.total_tile_count());
}
#[test]
fn google_centre_all_tiles_full_size() {
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();
generate_pyramid(&src, &plan, &sink, &EngineConfig::default()).unwrap();
for tile in sink.tiles() {
assert_eq!(tile.width, 256, "Width mismatch at {:?}", tile.coord);
assert_eq!(tile.height, 256, "Height mismatch at {:?}", tile.coord);
}
}
#[test]
fn google_centre_edge_tiles_have_background() {
let src = solid_raster(10, 10, 200);
let planner = PyramidPlanner::new(10, 10, 256, 0, Layout::Google)
.unwrap()
.with_centre(true);
let plan = planner.plan();
let sink = MemorySink::new();
generate_pyramid(&src, &plan, &sink, &EngineConfig::default()).unwrap();
let tiles = sink.tiles();
let level0: Vec<_> = tiles.iter().filter(|t| t.coord.level == 0).collect();
assert_eq!(level0.len(), 1);
let tile = &level0[0];
assert_eq!(tile.width, 256);
assert_eq!(tile.height, 256);
assert!(
!is_blank_tile(
&Raster::new(
tile.width,
tile.height,
PixelFormat::Rgb8,
tile.data.clone()
)
.unwrap()
) || tile.data.chunks(3).all(|px| px == [255, 255, 255])
);
}
#[test]
fn google_centre_deterministic_across_concurrency() {
let src = gradient_raster(400, 300);
let planner = PyramidPlanner::new(400, 300, 128, 0, Layout::Google)
.unwrap()
.with_centre(true);
let plan = planner.plan();
let ref_sink = MemorySink::new();
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));
for concurrency in [1, 2, 4] {
let sink = MemorySink::new();
let config = EngineConfig::default().with_concurrency(concurrency);
generate_pyramid(&src, &plan, &sink, &config).unwrap();
let mut tiles = sink.tiles();
tiles.sort_by_key(|t| (t.coord.level, t.coord.row, t.coord.col));
assert_eq!(tiles.len(), ref_tiles.len());
for (ref_t, t) in ref_tiles.iter().zip(tiles.iter()) {
assert_eq!(ref_t.coord, t.coord);
assert_eq!(
ref_t.data, t.data,
"Tile {:?} diverged at concurrency={concurrency}",
t.coord
);
}
}
}
#[test]
fn google_no_centre_produces_all_tiles() {
let src = gradient_raster(500, 300);
let planner = PyramidPlanner::new(500, 300, 256, 0, Layout::Google).unwrap();
let plan = planner.plan();
let sink = MemorySink::new();
let result = generate_pyramid(&src, &plan, &sink, &EngineConfig::default()).unwrap();
assert_eq!(result.tiles_produced, plan.total_tile_count());
}
}