From 6ff1cc5f8d1f44cf5c68dd0b8f81c6f6bd313236 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Sun, 8 Nov 2026 00:01:43 +0100 Subject: [PATCH] High-res textures built on first use: no cost in the pixel-art style, the 128x128 atlas only when zoomed in Co-Authored-By: Claude Opus 5.5 --- src/gfx.rs | 100 ++++++++++++++++++++++++++++++++++++++++-------- src/render3d.rs | 11 ++++-- 2 files changed, 91 insertions(+), 20 deletions(-) diff --git a/src/gfx.rs b/src/gfx.rs index 30ff1e7..55f1be3 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -101,11 +101,11 @@ pub const T_EGG: u32 = 222; // 2 variants: a painted egg (Easter) pub struct Gfx { pub atlas: Texture2D, pub sprites: Texture2D, - pub hd_sprites: Texture2D, // detailed sprites, 64x64 level (see HD_SPRITES, hd_picture) - pub hd_sprites32: Texture2D, // the same, 32x32 level - pub gear: Texture2D, // their gear parts (see GEAR_HELMET etc.) - pub atlas_hd: Texture2D, // the tile atlas of the high-res style, 64x64 tiles (see HD_TILES) - pub atlas_hd2: Texture2D, // the same with 128x128 tiles (close up) + pub hd_sprites: LazyTex, // detailed sprites, 64x64 level (see HD_SPRITES, hd_picture) + pub hd_sprites32: LazyTex, // the same, 32x32 level + pub gear: LazyTex, // their gear parts (see GEAR_HELMET etc.) + pub atlas_hd: LazyTex, // the tile atlas of the high-res style, 64x64 tiles (see HD_TILES) + pub atlas_hd2: LazyTex, // the same with 128x128 tiles (close up) pub materials: [Texture2D; 9], // furniture materials, high-res (see crate::props::Mat) pub materials_px: [Texture2D; 9], // the same for the pixel-art style (16x16) pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*) @@ -347,6 +347,32 @@ pub fn high_res_ui() -> bool { HIGH_RES_UI.with(|h| h.get()) } +// a texture with nearest filtering +fn tex(img: &Image) -> Texture2D { + let t = Texture2D::from_image(img); + t.set_filter(FilterMode::Nearest); + t +} + +// a texture built on first use (it derefs to the texture) +pub struct LazyTex { + cell: std::cell::OnceCell, + make: Box Texture2D>, +} + +impl LazyTex { + fn new(make: impl Fn() -> Texture2D + 'static) -> LazyTex { + LazyTex { cell: std::cell::OnceCell::new(), make: Box::new(make) } + } +} + +impl std::ops::Deref for LazyTex { + type Target = Texture2D; + fn deref(&self) -> &Texture2D { + self.cell.get_or_init(|| (self.make)()) + } +} + // ---- furniture materials (crate::props::Mat order): grey detail the furniture's colours tint // (the page keeps its colours), 64x64 and 16x16; see art/generated/textures/materials/ macro_rules! material { @@ -2939,17 +2965,30 @@ impl Gfx { cursors.set_filter(FilterMode::Nearest); let badges = Texture2D::from_image(&crate::badges::gen_badges()); badges.set_filter(FilterMode::Nearest); - let atlas_hd = Texture2D::from_image(&gen_hd_atlas(HD_TILE)); - atlas_hd.set_filter(FilterMode::Nearest); - let atlas_hd2 = Texture2D::from_image(&gen_hd_atlas(HD_TILE2)); - atlas_hd2.set_filter(FilterMode::Nearest); - let (hd64, hd32) = gen_hd_sprites(); - let hd_sprites = Texture2D::from_image(&hd64); - hd_sprites.set_filter(FilterMode::Nearest); - let hd_sprites32 = Texture2D::from_image(&hd32); - hd_sprites32.set_filter(FilterMode::Nearest); - let gear = Texture2D::from_image(&gen_gear()); - gear.set_filter(FilterMode::Nearest); + // the high-res textures are built when first used (the pixel-art style never needs them) + let atlas_hd = LazyTex::new(|| tex(&gen_hd_atlas(HD_TILE))); + let atlas_hd2 = LazyTex::new(|| tex(&gen_hd_atlas(HD_TILE2))); + // both levels of the detailed sprites come from one pass (which also measures the gear anchors) + let both: std::rc::Rc> = Default::default(); + let both2 = both.clone(); + let make = move |b: &std::cell::OnceCell<(Texture2D, Texture2D)>| { + b.get_or_init(|| { + let (a, c) = gen_hd_sprites(); + (tex(&a), tex(&c)) + }) + .clone() + }; + let hd_sprites = LazyTex::new(move || make(&both).0); + let hd_sprites32 = LazyTex::new(move || { + both2 + .get_or_init(|| { + let (a, c) = gen_hd_sprites(); + (tex(&a), tex(&c)) + }) + .1 + .clone() + }); + let gear = LazyTex::new(|| tex(&gen_gear())); let mat = |b: &[u8]| { let t = Texture2D::from_file_with_format(b, Some(ImageFormat::Png)); t.set_filter(FilterMode::Nearest); @@ -3097,7 +3136,7 @@ impl Gfx { // an atlas cell in the interface (tool and room buttons); the high-res style shows the // high-res atlas, so the room buttons show the rooms' new floors pub fn draw_cell(&self, id: u32, x: f32, y: f32, w: f32, h: f32, color: Color) { - let (tex, k) = if HIGH_RES_UI.with(|h| h.get()) { (&self.atlas_hd, (HD_TILE / 16) as f32) } else { (&self.atlas, 1.0) }; + let (tex, k): (&Texture2D, f32) = if HIGH_RES_UI.with(|h| h.get()) { (&self.atlas_hd, (HD_TILE / 16) as f32) } else { (&self.atlas, 1.0) }; let r = Self::cell(id); draw_texture_ex( tex, @@ -3357,6 +3396,33 @@ mod hd_atlas_tests { } } +#[cfg(test)] +mod startup_timing { + use super::*; + + // `cargo test --release --bins startup_timing -- --ignored --nocapture`: how long building + // the textures takes at the start + #[test] + #[ignore] + fn startup_timing() { + let t = std::time::Instant::now(); + let _ = gen_atlas(); + println!("atlas 16: {:?}", t.elapsed()); + let t = std::time::Instant::now(); + let _ = gen_sprites(); + println!("sprites: {:?}", t.elapsed()); + let t = std::time::Instant::now(); + let _ = gen_hd_atlas(HD_TILE); + println!("hd atlas 64: {:?}", t.elapsed()); + let t = std::time::Instant::now(); + let _ = gen_hd_atlas(HD_TILE2); + println!("hd atlas 128: {:?}", t.elapsed()); + let t = std::time::Instant::now(); + let _ = gen_hd_sprites(); + println!("hd sprites: {:?}", t.elapsed()); + } +} + #[cfg(test)] mod atlas_dump { use super::*; diff --git a/src/render3d.rs b/src/render3d.rs index 06fa061..a084d3c 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -1406,7 +1406,7 @@ impl R3d { lava_fresh = true; } let ch = &mut self.chunks[(cz * self.ncx + cx) as usize]; - let atlas = if self.chunks_hd { &gfx.atlas_hd } else { &gfx.atlas }; + let atlas: &Texture2D = if self.chunks_hd { &gfx.atlas_hd } else { &gfx.atlas }; let [m, o, l, k] = build_chunk(&g.map, a, &self.lava, cx, cz, atlas, self.wall_sub, self.chunks_hd); ch.mesh = m; ch.overlay = o; @@ -1652,8 +1652,8 @@ impl R3d { }; let atlas = match tile { 16 => &gfx.atlas, - crate::gfx::HD_TILE => &gfx.atlas_hd, - _ => &gfx.atlas_hd2, + crate::gfx::HD_TILE => &*gfx.atlas_hd, + _ => &*gfx.atlas_hd2, }; for ch in self.chunks.iter_mut() { for mesh in [&mut ch.mesh, &mut ch.overlay].into_iter().flatten() { @@ -1808,6 +1808,11 @@ impl R3d { // high) gives every texel nearly a pixel; fewer pixels would only turn it into noise let hd = self.pixel_rows >= crate::settings::HIGH_RES_ROWS as u32; let px_per_tile = world_rows / cam.view_h().max(0.1); + if hd { + // the detailed sprites are built on first use; that also measures the gear anchors, + // which the gear below needs + let _: &Texture2D = &gfx.hd_sprites; + } for e in &g.ents { if !e.alive || e.held || unit_hidden(g, e) { continue;