mod cpu; mod gpu; mod hybrid; pub use cpu::CpuRenderBackend; pub use gpu::GpuRenderBackend; pub use hybrid::HybridRenderBackend; use image::{ImageEncoder, RgbImage}; use std::io::Cursor; use std::sync::OnceLock; use crate::textures::TexturePalette; /// Set once at startup (see main.rs) if `ACCEPT_MINECRAFT_EULA` is set and a texture palette was /// built/loaded successfully — read by `base_colors` below so every `RenderBackend` (cpu/gpu/ /// hybrid all share `base_colors`) automatically picks up texture-averaged colors without the /// `RenderBackend` trait itself needing a new parameter. Left unset in tests and when the operator /// hasn't opted into the EULA, which keeps `base_colors`/hand-picked-color test assertions valid — /// `palette::color_for_textured` falls back to `palette::color_for` whenever this is `None`. static TEXTURE_PALETTE: OnceLock = OnceLock::new(); /// Called at most once, before the first render (see main.rs). A second call is a no-op (`OnceLock` /// semantics) — main.rs only ever calls this once anyway, since the palette is resolved once at /// startup, not per-request. pub fn set_texture_palette(palette: TexturePalette) { let _ = TEXTURE_PALETTE.set(palette); } /// Read by `mesh.rs` so 3D section meshing's vertex-color fallback picks up the same /// texture-averaged colors the 2D tile path already uses via `base_colors` — see this module's /// doc comment above. pub(crate) fn texture_palette() -> Option<&'static TexturePalette> { TEXTURE_PALETTE.get() } /// Same `OnceLock`-once-at-startup pattern as `TEXTURE_PALETTE`, for the Phase 12 texture atlas /// (see `atlas.rs`). `None` until `main.rs` successfully builds one (`ACCEPT_MINECRAFT_EULA` not /// set, or the build failed) — `mesh.rs` falls back to a flat vertex color per quad whenever this /// is `None` or the block's texture name has no atlas entry. static TEXTURE_ATLAS: OnceLock = OnceLock::new(); pub fn set_texture_atlas(atlas: crate::atlas::TextureAtlas) { let _ = TEXTURE_ATLAS.set(atlas); } pub(crate) fn texture_atlas() -> Option<&'static crate::atlas::TextureAtlas> { TEXTURE_ATLAS.get() } /// Same `OnceLock`-once-at-startup pattern as `TEXTURE_PALETTE`/`TEXTURE_ATLAS`, for the Phase 13 /// vanilla model registry (see `models.rs`). Worker-wide, not per-server — the same simplification /// tier already accepted for the texture palette/atlas. `None` until `main.rs` successfully builds /// one; `mesh.rs` simply skips non-cube resolution entirely when this is `None`. static VANILLA_MODEL_REGISTRY: OnceLock = OnceLock::new(); pub fn set_vanilla_model_registry(registry: crate::models::ModelRegistry) { let _ = VANILLA_MODEL_REGISTRY.set(registry); } pub(crate) fn vanilla_model_registry() -> Option<&'static crate::models::ModelRegistry> { VANILLA_MODEL_REGISTRY.get() } /// Bundles everything `mesh.rs` needs to resolve one voxel's real (possibly non-cube) shape: /// the worker-wide vanilla registry plus one job's per-server modded registry/id-map, both /// optional (a server with no modded model data yet, or a worker that never built the vanilla /// registry, simply resolves nothing — mesh_section falls back to its pre-Phase-13 behavior). /// Built fresh per chunk-job in `main.rs` (the modded half is genuinely per-server; the vanilla /// half is just a borrow of the static above) rather than stored globally, since it's cheap and /// keeps `mesh_section` ignorant of *where* the data came from. #[derive(Default)] pub struct ModelContext<'a> { pub vanilla: Option<&'a crate::models::ModelRegistry>, pub modded: Option<&'a crate::models::ModelRegistry>, pub modded_id_to_name: std::collections::HashMap, } impl<'a> ModelContext<'a> { pub fn resolve(&self, block_id: u16, meta: u8) -> Option { crate::models::resolve_for_block(self.vanilla, self.modded, &self.modded_id_to_name, block_id, meta) } } /// Builds a `ModelContext` for one chunk-job, combining the worker-wide vanilla registry (read /// from the `OnceLock` above — kept private to this module, `main.rs` never reads it directly) with /// the caller-supplied per-server modded registry/id-map (see `main.rs`'s `fetch_chunk_data`, /// which queries `block_registry`/`block_models` for the job's `server_id`). pub fn model_context( modded: Option<&crate::models::ModelRegistry>, modded_id_to_name: std::collections::HashMap, ) -> ModelContext<'_> { ModelContext { vanilla: vanilla_model_registry(), modded, modded_id_to_name } } /// One rendered column within a chunk, in chunk-local coordinates (0..16). pub struct ColumnPixel { pub local_x: u8, pub local_z: u8, pub block_id: u16, pub block_meta: u8, } /// Native tile resolution (one pixel per block within a chunk) before upscaling for display. pub const CHUNK_SIZE: u32 = 16; /// Upscale factor applied so tiles are a reasonable size for a Leaflet `tileSize: 256` layer — /// Phase 1 has one native zoom level (see api's tile route), so this is purely cosmetic, not a /// multi-resolution pyramid (that's Phase 2's job). pub const UPSCALE: u32 = 16; /// Per-voxel face-visibility extraction for one 16x16x16 section: for each of the 6 /// axis+direction combinations (index = `axis*2 + (dir==1 as usize)`, axis 0=x/1=y/2=z, matching /// `mesh.rs`'s own iteration order), a full section-sized grid (same x/y/z-major flat layout as /// the `blocks` input) where each entry is the voxel's own blockId if a face should be drawn in /// that direction (the voxel is solid and its neighbor along that axis/direction is air or out of /// bounds) or 0 otherwise. This is the embarrassingly-parallel per-voxel step Phase 8 targets for /// GPU offload; the caller (`mesh::mesh_section`) still does greedy-mesh merge/compaction on CPU /// regardless of which backend produced these masks, since that step is sequential/branchy — see /// the plan's "partial GPU rendering" note. pub type FaceMasks = [[u16; 4096]; 6]; /// Swappable rendering strategy (CPU via rayon / GPU via wgpu compute / hybrid) selected at /// startup via the `RENDER_BACKEND` env var, with automatic fallback to CPU (logged) if `gpu`/ /// `hybrid` is requested but no compatible GPU adapter is found — see `main.rs`. pub trait RenderBackend: Send + Sync { fn name(&self) -> &'static str; /// Rasterize one chunk's worth of columns (up to 256, sparse if the chunk isn't fully /// synced yet) into a single-resolution top-down PNG tile, returned as encoded bytes. fn rasterize_tile(&self, columns: &[ColumnPixel]) -> anyhow::Result>; fn compute_face_masks(&self, blocks: &[u16; 4096]) -> FaceMasks; } /// Resolves a batch of columns into the native-resolution (16x16) base color grid, flat in /// row-major (z-major, matching `ColumnPixel`'s local_x/local_z) order. Shared by every backend /// — palette lookup is cheap (256 entries at most) and keeping it in one place avoids maintaining /// two copies of `palette::color_for`'s logic (one in Rust, one duplicated into WGSL). pub(crate) fn base_colors(columns: &[ColumnPixel]) -> [[u8; 3]; 256] { let textures = TEXTURE_PALETTE.get(); let mut base = [crate::palette::color_for(0, 0); 256]; // air everywhere until overwritten for col in columns { if col.local_x as u32 >= CHUNK_SIZE || col.local_z as u32 >= CHUNK_SIZE { continue; } let color = crate::palette::color_for_textured(col.block_id, col.block_meta, textures); base[(col.local_z as usize) * 16 + col.local_x as usize] = color; } base } /// Nearest-neighbor-upscales a 16x16 base color grid into a `CHUNK_SIZE*UPSCALE` square, on the /// CPU. The GPU backend performs the equivalent expansion via a compute dispatch instead (see /// `gpu.rs`) and feeds its readback through `encode_png` below — both paths are expected to /// produce byte-identical output for the same input, only *how* the loop runs differs. pub(crate) fn cpu_upscale(base: &[[u8; 3]; 256]) -> RgbImage { let size = CHUNK_SIZE * UPSCALE; let mut upscaled = RgbImage::new(size, size); for y in 0..size { for x in 0..size { let bx = (x / UPSCALE) as usize; let by = (y / UPSCALE) as usize; let [r, g, b] = base[by * 16 + bx]; upscaled.put_pixel(x, y, image::Rgb([r, g, b])); } } upscaled } pub(crate) fn upscale_and_encode(base: &[[u8; 3]; 256]) -> anyhow::Result> { let upscaled = cpu_upscale(base); encode_png(upscaled.as_raw(), upscaled.width(), upscaled.height()) } /// PNG-encodes a flat RGB8 pixel buffer. Shared by every backend so tile output is /// byte-identical regardless of which one produced the raw pixels. pub(crate) fn encode_png(rgb: &[u8], width: u32, height: u32) -> anyhow::Result> { let mut bytes = Vec::new(); image::codecs::png::PngEncoder::new(&mut Cursor::new(&mut bytes)).write_image( rgb, width, height, image::ExtendedColorType::Rgb8, )?; Ok(bytes) }