made exporting a pool of images for maximum parrellelism

This commit is contained in:
grimsace
2026-05-06 11:02:19 -05:00
parent f7a30eaa7b
commit 5bdd2e2ff7
+289 -172
View File
@@ -109,6 +109,38 @@ struct SceneData {
door_edges: HashSet<((usize, usize), (usize, usize))>, door_edges: HashSet<((usize, usize), (usize, usize))>,
} }
#[derive(Debug, Clone, Copy)]
enum MaskTask {
Floors,
Walls,
Doors,
Archways,
LockedDoors,
SecretDoors,
Windows,
StartMarkers,
EndMarkers,
TrapMarkers,
MonsterMarkers,
Stairs,
Grid,
}
#[derive(Debug, Clone, Copy)]
enum CompositeTask {
Mask(MaskTask),
Composite,
}
struct CompositeExportContext<'a> {
layout: &'a DungeonLayout,
folder: PathBuf,
geometry: ExportGeometry,
scene: SceneData,
wall_width: f32,
door_width: f32,
}
// Collect precomputed cell and edge sets needed for rendering. // Collect precomputed cell and edge sets needed for rendering.
fn collect_scene_data(layout: &DungeonLayout, settings: &UiSettings) -> SceneData { fn collect_scene_data(layout: &DungeonLayout, settings: &UiSettings) -> SceneData {
let mut scene = SceneData::default(); let mut scene = SceneData::default();
@@ -193,21 +225,10 @@ pub fn export_to_target(
match target { match target {
ExportTarget::Folder(folder) => { ExportTarget::Folder(folder) => {
for &idx in &export_indices { if settings.export_format == ExportFormat::Folder {
let layout = &layouts[idx]; export_composite_masks_for_levels(layouts, settings, &folder, &export_indices)?;
if settings.export_format == ExportFormat::Folder { } else {
let subfolder = if settings.export_level_index == 999 { export_level_images_to_folder(layouts, settings, &folder, &export_indices)?;
folder.join(format!("level {}", idx + 1))
} else {
folder.clone()
};
export_composite_masks(layout, settings, &subfolder)?;
} else {
let ext = settings.export_format.extension();
let filename = format!("level{}.{ext}", idx + 1);
let path = folder.join(filename);
save_layout_to_file(layout, settings, &path)?;
}
} }
Ok(folder) Ok(folder)
} }
@@ -219,6 +240,22 @@ pub fn export_to_target(
} }
} }
fn export_level_images_to_folder(
layouts: &[DungeonLayout],
settings: &UiSettings,
folder: &std::path::Path,
export_indices: &[usize],
) -> Result<(), String> {
std::fs::create_dir_all(folder).map_err(|err| format!("Failed creating folder: {err}"))?;
let ext = settings.export_format.extension();
export_indices.par_iter().try_for_each(|&idx| {
let filename = format!("level{}.{ext}", idx + 1);
let path = folder.join(filename);
save_layout_to_file(&layouts[idx], settings, &path)
})
}
fn save_layout_to_file( fn save_layout_to_file(
layout: &DungeonLayout, layout: &DungeonLayout,
settings: &UiSettings, settings: &UiSettings,
@@ -250,6 +287,15 @@ fn save_layout_to_file(
fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap, String> { fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap, String> {
let g = ExportGeometry::new(settings.cols, settings.rows); let g = ExportGeometry::new(settings.cols, settings.rows);
let scene = collect_scene_data(layout, settings); let scene = collect_scene_data(layout, settings);
render_pixmap_with_scene(layout, settings, &g, &scene)
}
fn render_pixmap_with_scene(
layout: &DungeonLayout,
settings: &UiSettings,
g: &ExportGeometry,
scene: &SceneData,
) -> Result<Pixmap, String> {
let mut pixmap = let mut pixmap =
Pixmap::new(g.width, g.height).ok_or_else(|| "Failed to allocate canvas".to_string())?; Pixmap::new(g.width, g.height).ok_or_else(|| "Failed to allocate canvas".to_string())?;
fill_bg(&mut pixmap, BG); fill_bg(&mut pixmap, BG);
@@ -258,11 +304,11 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap
let door_w = (g.cell / 5.0).max(1.0).round(); let door_w = (g.cell / 5.0).max(1.0).round();
if settings.export_show_grid { if settings.export_show_grid {
draw_grid(&mut pixmap, &g); draw_grid(&mut pixmap, g);
} }
for &(x, y) in &scene.corridor_cells { for &(x, y) in &scene.corridor_cells {
fill_rect_cell(&mut pixmap, &g, x, y, CORRIDOR); fill_rect_cell(&mut pixmap, g, x, y, CORRIDOR);
if settings.colorblind_mode { if settings.colorblind_mode {
let (cx, cy) = g.cell_center(x, y); let (cx, cy) = g.cell_center(x, y);
draw_dot(&mut pixmap, cx, cy, g.cell * 0.14, BLACK); draw_dot(&mut pixmap, cx, cy, g.cell * 0.14, BLACK);
@@ -270,15 +316,15 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap
} }
for room in &layout.rooms { for room in &layout.rooms {
fill_rect_room(&mut pixmap, &g, room, ROOM); fill_rect_room(&mut pixmap, g, room, ROOM);
if settings.colorblind_mode { if settings.colorblind_mode {
draw_room_crosshatch(&mut pixmap, &g, room, 1.5, BLACK); draw_room_crosshatch(&mut pixmap, g, room, 1.5, BLACK);
} }
} }
draw_cell_walls( draw_cell_walls(
&mut pixmap, &mut pixmap,
&g, g,
&scene.corridor_cells, &scene.corridor_cells,
Some(&scene.corridor_edges), Some(&scene.corridor_edges),
&scene.door_edges, &scene.door_edges,
@@ -287,7 +333,7 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap
); );
draw_cell_walls( draw_cell_walls(
&mut pixmap, &mut pixmap,
&g, g,
&scene.room_cells, &scene.room_cells,
Some(&scene.room_edges), Some(&scene.room_edges),
&scene.door_edges, &scene.door_edges,
@@ -316,7 +362,7 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap
} else { } else {
(OPEN_DOOR, DoorStyle::Solid) (OPEN_DOOR, DoorStyle::Solid)
}; };
draw_door(&mut pixmap, &g, door, door_w, color, style); draw_door(&mut pixmap, g, door, door_w, color, style);
} }
for window in &layout.windows { for window in &layout.windows {
@@ -325,12 +371,12 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap
} else { } else {
DoorStyle::Solid DoorStyle::Solid
}; };
draw_window(&mut pixmap, &g, window, door_w, WINDOW, style); draw_window(&mut pixmap, g, window, door_w, WINDOW, style);
} }
draw_area_marker_group( draw_area_marker_group(
&mut pixmap, &mut pixmap,
&g, g,
&layout.start_markers, &layout.start_markers,
"S", "S",
START_MARKER, START_MARKER,
@@ -338,7 +384,7 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap
); );
draw_area_marker_group( draw_area_marker_group(
&mut pixmap, &mut pixmap,
&g, g,
&layout.end_markers, &layout.end_markers,
"E", "E",
END_MARKER, END_MARKER,
@@ -346,7 +392,7 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap
); );
draw_area_marker_group( draw_area_marker_group(
&mut pixmap, &mut pixmap,
&g, g,
&layout.trap_markers, &layout.trap_markers,
"T", "T",
TRAP_MARKER, TRAP_MARKER,
@@ -354,7 +400,7 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap
); );
draw_area_marker_group( draw_area_marker_group(
&mut pixmap, &mut pixmap,
&g, g,
&layout.monster_markers, &layout.monster_markers,
"M", "M",
MONSTER_MARKER, MONSTER_MARKER,
@@ -363,7 +409,7 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap
draw_staircase_group( draw_staircase_group(
&mut pixmap, &mut pixmap,
&g, g,
&layout.stairs, &layout.stairs,
STAIRS, STAIRS,
settings.colorblind_mode, settings.colorblind_mode,
@@ -372,35 +418,36 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap
Ok(pixmap) Ok(pixmap)
} }
// Export composite image plus mask layers into a folder. // Export composite images and masks using one global task queue.
fn export_composite_masks( fn export_composite_masks_for_levels(
layout: &DungeonLayout, layouts: &[DungeonLayout],
settings: &UiSettings, settings: &UiSettings,
folder: &std::path::Path, folder: &std::path::Path,
export_indices: &[usize],
) -> Result<(), String> { ) -> Result<(), String> {
std::fs::create_dir_all(folder).map_err(|e| format!("Failed creating folder: {e}"))?; let contexts = export_indices
.iter()
.map(|&idx| {
let subfolder = if settings.export_level_index == 999 {
folder.join(format!("level {}", idx + 1))
} else {
folder.to_path_buf()
};
std::fs::create_dir_all(&subfolder)
.map_err(|err| format!("Failed creating folder: {err}"))?;
let g = ExportGeometry::new(settings.cols, settings.rows); let geometry = ExportGeometry::new(settings.cols, settings.rows);
let scene = collect_scene_data(layout, settings); let scene = collect_scene_data(&layouts[idx], settings);
let wall_w = (g.cell / 2.5).max(1.0); Ok(CompositeExportContext {
let door_w = (g.cell / 5.0).max(1.0); layout: &layouts[idx],
folder: subfolder,
// List of tasks to perform in parallel geometry,
enum MaskTask { scene,
Floors, wall_width: (geometry.cell / 2.5).max(1.0),
Walls, door_width: (geometry.cell / 5.0).max(1.0),
Doors, })
Archways, })
LockedDoors, .collect::<Result<Vec<_>, String>>()?;
SecretDoors,
Windows,
StartMarkers,
EndMarkers,
TrapMarkers,
MonsterMarkers,
Stairs,
Grid,
}
let mut tasks = vec![ let mut tasks = vec![
MaskTask::Floors, MaskTask::Floors,
@@ -421,129 +468,43 @@ fn export_composite_masks(
tasks.push(MaskTask::Grid); tasks.push(MaskTask::Grid);
} }
// Process masks in parallel let export_tasks = contexts
tasks.into_par_iter().try_for_each(|task| { .iter()
let (stem, pixmap) = match task { .enumerate()
MaskTask::Floors => { .flat_map(|(context_idx, _)| {
let mut pixmap = Pixmap::new(g.width, g.height).unwrap(); tasks
fill_bg(&mut pixmap, BLACK); .iter()
for &(x, y) in &scene.corridor_cells { .copied()
fill_rect_cell(&mut pixmap, &g, x, y, WHITE); .map(move |task| (context_idx, CompositeTask::Mask(task)))
} .chain(std::iter::once((context_idx, CompositeTask::Composite)))
for room in &layout.rooms { })
fill_rect_room(&mut pixmap, &g, room, WHITE); .collect::<Vec<_>>();
}
("floors_mask", pixmap)
}
MaskTask::Walls => {
let mut pixmap = Pixmap::new(g.width, g.height).unwrap();
fill_bg(&mut pixmap, BLACK);
draw_cell_walls(
&mut pixmap,
&g,
&scene.corridor_cells,
Some(&scene.corridor_edges),
&scene.door_edges,
wall_w,
WHITE,
);
draw_cell_walls(
&mut pixmap,
&g,
&scene.room_cells,
Some(&scene.room_edges),
&scene.door_edges,
wall_w,
WHITE,
);
("walls_mask", pixmap)
}
MaskTask::Doors => {
let mut pixmap = blank_mask(&g, "doors").unwrap();
for door in &layout.doors {
if !door.secret && !door.archway && !door.locked {
draw_door(&mut pixmap, &g, door, door_w, WHITE, DoorStyle::Solid);
}
}
("doors_mask", pixmap)
}
MaskTask::Archways => {
let mut pixmap = blank_mask(&g, "archways").unwrap();
for door in &layout.doors {
if door.archway {
draw_door(&mut pixmap, &g, door, door_w, WHITE, DoorStyle::Solid);
}
}
("archways_mask", pixmap)
}
MaskTask::LockedDoors => {
let mut pixmap = blank_mask(&g, "locked doors").unwrap();
for door in &layout.doors {
if door.locked {
draw_door(&mut pixmap, &g, door, door_w, WHITE, DoorStyle::Solid);
}
}
("locked_doors_mask", pixmap)
}
MaskTask::SecretDoors => {
let mut pixmap = blank_mask(&g, "secret doors").unwrap();
for door in &layout.doors {
if door.secret {
draw_door(&mut pixmap, &g, door, door_w, WHITE, DoorStyle::Solid);
}
}
("secret_doors_mask", pixmap)
}
MaskTask::Windows => {
let mut pixmap = blank_mask(&g, "windows").unwrap();
for window in &layout.windows {
draw_window(&mut pixmap, &g, window, door_w, WHITE, DoorStyle::Solid);
}
("windows_mask", pixmap)
}
MaskTask::StartMarkers => {
let mut pixmap = blank_mask(&g, "start markers").unwrap();
fill_marker_mask(&mut pixmap, &g, &layout.start_markers);
("start_markers_mask", pixmap)
}
MaskTask::EndMarkers => {
let mut pixmap = blank_mask(&g, "end markers").unwrap();
fill_marker_mask(&mut pixmap, &g, &layout.end_markers);
("end_markers_mask", pixmap)
}
MaskTask::TrapMarkers => {
let mut pixmap = blank_mask(&g, "trap markers").unwrap();
fill_marker_mask(&mut pixmap, &g, &layout.trap_markers);
("trap_markers_mask", pixmap)
}
MaskTask::MonsterMarkers => {
let mut pixmap = blank_mask(&g, "monster markers").unwrap();
fill_marker_mask(&mut pixmap, &g, &layout.monster_markers);
("monster_markers_mask", pixmap)
}
MaskTask::Stairs => {
let mut pixmap = blank_mask(&g, "stairs").unwrap();
for staircase in &layout.stairs {
fill_rect_staircase(&mut pixmap, &g, staircase, WHITE);
}
("stairs_mask", pixmap)
}
MaskTask::Grid => {
let mut pixmap = Pixmap::new(g.width, g.height).unwrap();
fill_bg(&mut pixmap, BLACK);
draw_grid_color(&mut pixmap, &g, WHITE);
("grid_mask", pixmap)
}
};
save_mask_image(folder, stem, &pixmap, settings) export_tasks
})?; .into_par_iter()
.try_for_each(|(context_idx, task)| {
let context = &contexts[context_idx];
match task {
CompositeTask::Mask(mask_task) => {
let (stem, pixmap) = render_mask_pixmap(context, mask_task)?;
save_mask_image(&context.folder, stem, &pixmap, settings)
}
CompositeTask::Composite => save_composite_image(context, settings),
}
})?;
// Also export the composite image Ok(())
let composite = render_pixmap(layout, settings)?; }
fn save_composite_image(
context: &CompositeExportContext<'_>,
settings: &UiSettings,
) -> Result<(), String> {
let composite =
render_pixmap_with_scene(context.layout, settings, &context.geometry, &context.scene)?;
let composite_img = raster_image_from_pixmap(&composite, settings)?; let composite_img = raster_image_from_pixmap(&composite, settings)?;
let composite_ext = settings.export_format.extension(); let composite_ext = settings.export_format.extension();
let composite_path = folder.join(format!("composite.{composite_ext}")); let composite_path = context.folder.join(format!("composite.{composite_ext}"));
composite_img composite_img
.save_with_format(&composite_path, export_image_format(settings.export_format)) .save_with_format(&composite_path, export_image_format(settings.export_format))
.map_err(|e| format!("Failed writing {}: {e}", composite_path.display()))?; .map_err(|e| format!("Failed writing {}: {e}", composite_path.display()))?;
@@ -551,6 +512,162 @@ fn export_composite_masks(
Ok(()) Ok(())
} }
fn render_mask_pixmap(
context: &CompositeExportContext<'_>,
task: MaskTask,
) -> Result<(&'static str, Pixmap), String> {
let g = &context.geometry;
let scene = &context.scene;
let layout = context.layout;
let result = match task {
MaskTask::Floors => {
let mut pixmap = blank_mask(g, "floors")?;
for &(x, y) in &scene.corridor_cells {
fill_rect_cell(&mut pixmap, g, x, y, WHITE);
}
for room in &layout.rooms {
fill_rect_room(&mut pixmap, g, room, WHITE);
}
("floors_mask", pixmap)
}
MaskTask::Walls => {
let mut pixmap = blank_mask(g, "walls")?;
draw_cell_walls(
&mut pixmap,
g,
&scene.corridor_cells,
Some(&scene.corridor_edges),
&scene.door_edges,
context.wall_width,
WHITE,
);
draw_cell_walls(
&mut pixmap,
g,
&scene.room_cells,
Some(&scene.room_edges),
&scene.door_edges,
context.wall_width,
WHITE,
);
("walls_mask", pixmap)
}
MaskTask::Doors => {
let mut pixmap = blank_mask(g, "doors")?;
for door in &layout.doors {
if !door.secret && !door.archway && !door.locked {
draw_door(
&mut pixmap,
g,
door,
context.door_width,
WHITE,
DoorStyle::Solid,
);
}
}
("doors_mask", pixmap)
}
MaskTask::Archways => {
let mut pixmap = blank_mask(g, "archways")?;
for door in &layout.doors {
if door.archway {
draw_door(
&mut pixmap,
g,
door,
context.door_width,
WHITE,
DoorStyle::Solid,
);
}
}
("archways_mask", pixmap)
}
MaskTask::LockedDoors => {
let mut pixmap = blank_mask(g, "locked doors")?;
for door in &layout.doors {
if door.locked {
draw_door(
&mut pixmap,
g,
door,
context.door_width,
WHITE,
DoorStyle::Solid,
);
}
}
("locked_doors_mask", pixmap)
}
MaskTask::SecretDoors => {
let mut pixmap = blank_mask(g, "secret doors")?;
for door in &layout.doors {
if door.secret {
draw_door(
&mut pixmap,
g,
door,
context.door_width,
WHITE,
DoorStyle::Solid,
);
}
}
("secret_doors_mask", pixmap)
}
MaskTask::Windows => {
let mut pixmap = blank_mask(g, "windows")?;
for window in &layout.windows {
draw_window(
&mut pixmap,
g,
window,
context.door_width,
WHITE,
DoorStyle::Solid,
);
}
("windows_mask", pixmap)
}
MaskTask::StartMarkers => {
let mut pixmap = blank_mask(g, "start markers")?;
fill_marker_mask(&mut pixmap, g, &layout.start_markers);
("start_markers_mask", pixmap)
}
MaskTask::EndMarkers => {
let mut pixmap = blank_mask(g, "end markers")?;
fill_marker_mask(&mut pixmap, g, &layout.end_markers);
("end_markers_mask", pixmap)
}
MaskTask::TrapMarkers => {
let mut pixmap = blank_mask(g, "trap markers")?;
fill_marker_mask(&mut pixmap, g, &layout.trap_markers);
("trap_markers_mask", pixmap)
}
MaskTask::MonsterMarkers => {
let mut pixmap = blank_mask(g, "monster markers")?;
fill_marker_mask(&mut pixmap, g, &layout.monster_markers);
("monster_markers_mask", pixmap)
}
MaskTask::Stairs => {
let mut pixmap = blank_mask(g, "stairs")?;
for staircase in &layout.stairs {
fill_rect_staircase(&mut pixmap, g, staircase, WHITE);
}
("stairs_mask", pixmap)
}
MaskTask::Grid => {
let mut pixmap = blank_mask(g, "grid")?;
draw_grid_color(&mut pixmap, g, WHITE);
("grid_mask", pixmap)
}
};
Ok(result)
}
fn blank_mask(g: &ExportGeometry, name: &str) -> Result<Pixmap, String> { fn blank_mask(g: &ExportGeometry, name: &str) -> Result<Pixmap, String> {
let mut pixmap = Pixmap::new(g.width, g.height) let mut pixmap = Pixmap::new(g.width, g.height)
.ok_or_else(|| format!("Failed to allocate {name} mask canvas"))?; .ok_or_else(|| format!("Failed to allocate {name} mask canvas"))?;