use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender};
use super::mesh::DecodedMesh;
use crate::geometry::{
ChunkBlockType, ChunkGenerator, build_chunk_impostor_mesh, build_chunk_mesh,
};
use crate::gfx::chunk_coord::ChunkCoord;
use crate::gfx::chunk_window::{ChunkDetail, ChunkWindow};
use crate::gfx::mesh_payload::Vertex;
pub(crate) trait ChunkSource: Send + Sync {
fn generate(&self, coord: ChunkCoord, detail: ChunkDetail) -> Result<DecodedMesh, String>;
}
pub(crate) struct ProceduralChunkSource {
generator: ChunkGenerator,
palette: Vec<ChunkBlockType>,
chunk_blocks: [u32; 3],
block_size: f32,
impostor_step: u32,
surface_block: ChunkBlockType,
}
impl ProceduralChunkSource {
pub(crate) fn new(
seed: u64,
chunk_blocks: [u32; 3],
block_size: f32,
palette: Vec<ChunkBlockType>,
impostor_step: u32,
) -> Self {
let generator = ChunkGenerator::new(seed, chunk_blocks, palette.len() as u32);
let surface_idx = generator.surface_palette_index() as usize;
let surface_block = palette
.get(surface_idx)
.or_else(|| palette.first())
.copied()
.unwrap_or(ChunkBlockType {
solid: true,
uv_top: [0.0, 0.0, 1.0, 1.0],
uv_bottom: [0.0, 0.0, 1.0, 1.0],
uv_side: [0.0, 0.0, 1.0, 1.0],
});
Self {
generator,
palette,
chunk_blocks,
block_size,
impostor_step: impostor_step.max(1),
surface_block,
}
}
fn impostor_heights(&self, coord: ChunkCoord) -> Vec<i32> {
let step = self.impostor_step;
let [dx, _dy, dz] = self.chunk_blocks;
let nx = dx.div_ceil(step);
let nz = dz.div_ceil(step);
let base_x = coord.x * dx as i32;
let base_z = coord.z * dz as i32;
let mut heights = Vec::with_capacity(((nx + 1) * (nz + 1)) as usize);
for gz in 0..=nz {
let lz = (gz * step).min(dz) as i32;
for gx in 0..=nx {
let lx = (gx * step).min(dx) as i32;
heights.push(
self.generator
.surface_height_world(base_x + lx, base_z + lz),
);
}
}
heights
}
}
impl ChunkSource for ProceduralChunkSource {
fn generate(&self, coord: ChunkCoord, detail: ChunkDetail) -> Result<DecodedMesh, String> {
let (vertices, indices) = match detail {
ChunkDetail::Near => {
let blocks = self.generator.generate(coord);
build_chunk_mesh(self.chunk_blocks, self.block_size, &blocks, &self.palette)?
}
ChunkDetail::Far => {
let heights = self.impostor_heights(coord);
build_chunk_impostor_mesh(
self.chunk_blocks,
self.block_size,
self.impostor_step,
&heights,
self.surface_block.uv_top,
self.surface_block.uv_side,
)?
}
};
Ok(DecodedMesh { vertices, indices })
}
}
struct LoadResult {
coord: ChunkCoord,
decoded: Result<DecodedMesh, String>,
}
pub(crate) struct ChunkStreamer {
window: ChunkWindow,
chunk_w: f32,
chunk_d: f32,
worker: super::worker::Worker<(ChunkCoord, ChunkDetail)>,
result_rx: Receiver<LoadResult>,
}
impl ChunkStreamer {
pub(crate) fn new(
source: Arc<dyn ChunkSource>,
near_radius: i32,
far_radius: i32,
load_budget: usize,
chunk_w: f32,
chunk_d: f32,
) -> Self {
let window = ChunkWindow::new(near_radius, far_radius, load_budget);
let (request_tx, request_rx) = std::sync::mpsc::channel::<(ChunkCoord, ChunkDetail)>();
let (result_tx, result_rx) = std::sync::mpsc::channel::<LoadResult>();
let worker =
super::worker::Worker::spawn("cn-chunk-stream", request_rx, request_tx, move |rx| {
worker_loop(source, rx, result_tx)
});
Self {
window,
chunk_w,
chunk_d,
result_rx,
worker,
}
}
pub(crate) fn camera_chunk(&self, camera: [f32; 3]) -> ChunkCoord {
ChunkCoord::from_world(camera[0], camera[2], self.chunk_w, self.chunk_d)
}
pub(crate) fn plan_and_dispatch(&mut self, camera: ChunkCoord) -> Vec<ChunkCoord> {
let plan = self.window.plan(camera);
for &(coord, detail) in &plan.to_load {
let sent = self.worker.send((coord, detail));
if !sent {
self.window.forget(coord);
}
}
plan.to_evict
}
pub(crate) fn drain_completed(
&mut self,
mut upload: impl FnMut(ChunkCoord, Vec<Vertex>, Vec<u16>),
) -> usize {
let mut applied = 0;
while let Ok(result) = self.result_rx.try_recv() {
if !self.window.is_tracked(result.coord) {
continue; }
match result.decoded {
Ok(mesh) => {
let bytes = mesh.vertices.len() * std::mem::size_of::<Vertex>()
+ mesh.indices.len() * std::mem::size_of::<u16>();
upload(result.coord, mesh.vertices, mesh.indices);
self.window.mark_resident(result.coord, bytes as u64);
applied += 1;
}
Err(e) => {
tracing::warn!(
"chunk stream: generation of chunk ({},{}) failed: {}",
result.coord.x,
result.coord.z,
e
);
self.window.mark_resident(result.coord, 0);
}
}
}
applied
}
pub(crate) fn stats(&self) -> (usize, usize) {
self.window.counts()
}
pub(crate) fn detail_counts(&self) -> (usize, usize) {
self.window.counts_by_detail()
}
pub(crate) fn set_byte_budget(&mut self, budget: Option<u64>) {
self.window.set_byte_budget(budget);
}
pub(crate) fn resident_bytes(&self) -> u64 {
self.window.resident_bytes()
}
pub(crate) fn byte_budget(&self) -> Option<u64> {
self.window.byte_budget()
}
}
fn worker_loop(
source: Arc<dyn ChunkSource>,
requests: Receiver<(ChunkCoord, ChunkDetail)>,
results: Sender<LoadResult>,
) {
while let Ok((coord, detail)) = requests.recv() {
let decoded = source.generate(coord, detail);
if results.send(LoadResult { coord, decoded }).is_err() {
break;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cc(x: i32, z: i32) -> ChunkCoord {
ChunkCoord::new(x, z)
}
fn mk_vertex(x: f32) -> Vertex {
Vertex {
pos: [x, 0.0, 0.0],
normal: [0.0, 1.0, 0.0],
tangent: [1.0, 0.0, 0.0],
color: [1.0, 1.0, 1.0],
uv: [0.0, 0.0],
}
}
struct ConstSource;
impl ChunkSource for ConstSource {
fn generate(
&self,
_coord: ChunkCoord,
_detail: ChunkDetail,
) -> Result<DecodedMesh, String> {
Ok(DecodedMesh {
vertices: vec![mk_vertex(0.0), mk_vertex(1.0), mk_vertex(2.0)],
indices: vec![0, 1, 2],
})
}
}
fn drain_until(streamer: &mut ChunkStreamer, want: usize) -> Vec<ChunkCoord> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
let mut uploaded = Vec::new();
while std::time::Instant::now() < deadline {
streamer.drain_completed(|coord, _, _| uploaded.push(coord));
if streamer.stats().0 >= want {
break;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
uploaded
}
#[test]
fn procedural_source_generates_a_non_empty_chunk() {
let palette = vec![
ChunkBlockType {
solid: false,
uv_top: [0.0; 4],
uv_bottom: [0.0; 4],
uv_side: [0.0; 4],
},
ChunkBlockType {
solid: true,
uv_top: [0.0, 0.0, 1.0, 1.0],
uv_bottom: [0.0, 0.0, 1.0, 1.0],
uv_side: [0.0, 0.0, 1.0, 1.0],
},
];
let source = ProceduralChunkSource::new(42, [8, 16, 8], 1.0, palette, 4);
let mesh = source
.generate(cc(0, 0), ChunkDetail::Near)
.expect("generate ok");
assert!(!mesh.vertices.is_empty());
assert!(!mesh.indices.is_empty());
let full = source
.generate(cc(0, 0), ChunkDetail::Near)
.expect("full ok");
let impostor = source
.generate(cc(0, 0), ChunkDetail::Far)
.expect("impostor ok");
assert!(!impostor.vertices.is_empty());
assert!(
impostor.vertices.len() < full.vertices.len(),
"impostor ({}) should be cheaper than full ({})",
impostor.vertices.len(),
full.vertices.len()
);
}
#[test]
fn camera_chunk_maps_world_position_to_a_chunk() {
let streamer = ChunkStreamer::new(Arc::new(ConstSource), 2, 2, 4, 16.0, 16.0);
assert_eq!(streamer.camera_chunk([0.0, 5.0, 0.0]), cc(0, 0));
assert_eq!(streamer.camera_chunk([20.0, 5.0, -1.0]), cc(1, -1));
}
#[test]
fn plan_dispatches_chunks_and_drain_uploads_them() {
let mut streamer = ChunkStreamer::new(Arc::new(ConstSource), 1, 1, 100, 16.0, 16.0);
let evict = streamer.plan_and_dispatch(cc(0, 0));
assert!(evict.is_empty());
let uploaded = drain_until(&mut streamer, 9);
assert_eq!(uploaded.len(), 9);
assert!(uploaded.contains(&cc(0, 0)));
assert_eq!(streamer.stats(), (9, 0));
}
#[test]
fn detail_counts_split_the_near_and_far_bands() {
let mut streamer = ChunkStreamer::new(Arc::new(ConstSource), 0, 1, 100, 16.0, 16.0);
streamer.plan_and_dispatch(cc(0, 0));
drain_until(&mut streamer, 9);
assert_eq!(streamer.detail_counts(), (1, 8));
}
struct FailingSource;
impl ChunkSource for FailingSource {
fn generate(
&self,
_coord: ChunkCoord,
_detail: ChunkDetail,
) -> Result<DecodedMesh, String> {
Err("no terrain".to_string())
}
}
#[test]
fn a_failed_generation_is_not_retried() {
let mut streamer = ChunkStreamer::new(Arc::new(FailingSource), 0, 0, 4, 16.0, 16.0);
streamer.plan_and_dispatch(cc(0, 0));
let uploaded = drain_until(&mut streamer, 1);
assert!(uploaded.is_empty(), "a failed generation uploads nothing");
assert_eq!(streamer.stats(), (1, 0));
assert_eq!(streamer.resident_bytes(), 0);
}
#[test]
fn moving_far_evicts_the_old_window() {
let mut streamer = ChunkStreamer::new(Arc::new(ConstSource), 1, 1, 100, 16.0, 16.0);
streamer.plan_and_dispatch(cc(0, 0));
drain_until(&mut streamer, 9);
let evict = streamer.plan_and_dispatch(cc(50, 0));
assert!(evict.contains(&cc(0, 0)));
assert_eq!(evict.len(), 9);
}
}