// -*- coding: utf-8 -*- // // Copyright 2021-2023 Michael Büsch // // Licensed under the Apache License version 2.0 // or the MIT license, at your option. // SPDX-License-Identifier: Apache-2.0 OR MIT // use super::map::Map; use crate::util::{degrees, radians}; use graphics::{Landscape3D, Point3D, Render3D, View3D}; use parking_lot::RwLock; use range_lock::{RepVecRangeLock, RepVecRangeLockGuard}; use std::cell::Cell; use std::cmp::{max, min}; use std::sync::{atomic, Arc, Barrier}; use std::thread; type RenderType = f32; const RENDER_MAX_NR_THREADS: usize = 32; fn rotate_2d( x: RenderType, y: RenderType, pivot_x: RenderType, pivot_y: RenderType, angle_sin: RenderType, angle_cos: RenderType, ) -> (RenderType, RenderType) { let rel_x = x - pivot_x; let rel_y = y - pivot_y; let rot_rel_x = (rel_x * angle_cos) - (rel_y * angle_sin); let rot_rel_y = (rel_x * angle_sin) + (rel_y * angle_cos); (pivot_x + rot_rel_x, pivot_y + rot_rel_y) } /// Structure for passing work information to the worker threads. struct RenderWorkData { image_width: i32, image_height: i32, image: Option>>, map: Option>, pos: Point3D, view_dist: i32, horizon: i32, yaw_angle_sin: RenderType, yaw_angle_cos: RenderType, fov_angle_half_tan: RenderType, } // Image data layout: // // p -> pixel RGBA ralue, u32. // image: Vec // // |--- image_width --- | // v v // // vec![ p, p, p, p, p, p, p, p, <-- <- lock cycle 0 // p, p, p, p, p, p, p, p, image <- lock cycle 1 // p, p, p, p, p, p, p, p, height <- lock cycle 2 // p, p, p, p, p, p, p, p, | <- lock cycle 3 // p, p, p, p, p, p, p, p, ] <-- <- lock cycle 4 // // ^ ^ |^ ^ // |--------| ||--------| // thread 0 | thread 1 // lock- | lock- // offset 0 | offset 1 // | // ^ ^ ^ ^ |^ ^ ^ ^ // 0 1 2 3 |0 1 2 3 // lock slice |lock slice // element | element // #[allow(clippy::too_many_arguments)] #[inline] fn draw_vertical_line( image_guard: &mut RepVecRangeLockGuard, image_width: i32, image_height: i32, x_slice_len: usize, x: i32, y0: i32, mut y1: i32, argb: u32, ) { if x < image_width { if y1 >= image_height { y1 = image_height - 1; } let sliceidx = x as usize % x_slice_len; for y in y0..=y1 { let cycleidx = y as usize; image_guard[cycleidx][sliceidx] = argb; } } } /// Per thread renderer. fn render(data: &RenderWorkData, y_limits: &mut Vec, thread_index: usize, nr_threads: usize) { debug_assert!(data.image_width >= 0 && data.image_width as usize % nr_threads == 0); debug_assert!(data.image_height >= 0); let map = data.map.as_ref().expect("render(): Map is not present."); let image = data .image .as_ref() .expect("render(): Image is not present."); let image_width = data.image_width as RenderType; let image_height = data.image_height as RenderType; let horizon = data.horizon as RenderType; let pos_x = data.pos.x() as RenderType; let pos_y = data.pos.y() as RenderType; let pos_z = data.pos.z() as RenderType; let y_limits_size = data.image_width as usize / nr_threads; if y_limits.len() != y_limits_size { y_limits.resize(y_limits_size, u16::MAX); } y_limits.fill(u16::MAX); let x_slice_len = data.image_width as usize / nr_threads; let x_begin = thread_index as i32 * x_slice_len as i32; let x_end = x_begin + x_slice_len as i32; let mut image_guard = image .try_lock(thread_index) .expect("render(): Failed to lock image slice."); for z in 1..data.view_dist { let z = z as RenderType; // Calculate the field-of-view width, for the current Z distance. let fov_half = z * data.fov_angle_half_tan; let fov = fov_half * 2.0; // Calculate the leftmost X position in the current fov width. let left = pos_x - fov_half; // Walk the width (X), from left to right. for x in x_begin..x_end { // Calculate the current X offset, referenced to the left hand edge. let x_offset = (fov * x as RenderType) / image_width; // Calculate the non-rotated map position of this voxel. let map_x = left + x_offset; let map_y = pos_z + z; // Rotate the map position around the Y axis of the current position. let (map_x, map_y) = rotate_2d( map_x, map_y, pos_x, pos_z, data.yaw_angle_sin, data.yaw_angle_cos, ); // Get the map color and height values. let map_value = map.get( map_x.round() as isize as usize, map_y.round() as isize as usize, ); // Calculate the height over ground. let base_height = pos_y - map_value.height() as RenderType; // Calculate the Y position of the top of this voxel. let y_raw = ((base_height * 150.0) / z) + horizon; let y = y_raw.clamp(0.0, u16::MAX as RenderType); // Scale the Y positions from map-based to screen view height based. let div = (Map::MAX_HEIGHT as i32 + 1) as RenderType; let y0_scaled = ((y * image_height) / div).round() as i32; let y_limit = y_limits[x as usize - x_begin as usize]; let y1_scaled = ((y_limit as RenderType * image_height) / div).round() as i32; // Draw the vertical line from y0 to y1. if y0_scaled < data.image_height && y0_scaled <= y1_scaled && y1_scaled > 0 { draw_vertical_line( &mut image_guard, data.image_width, data.image_height, x_slice_len, x, y0_scaled, y1_scaled, map_value.argb(), ); } // If the y0 value is a new uppermost value, store it as new limit. let y_u16 = y.round() as u16; if y_u16 < y_limit { y_limits[x as usize - x_begin as usize] = y_u16; } } } } /// Renderer thread. fn thread_render( cpu: usize, nr_threads: usize, stop: Arc, work_data: Arc>, run_barrier: Arc, done_barrier: Arc, ) { assert!(nr_threads > 0 && nr_threads <= RENDER_MAX_NR_THREADS); assert!(cpu < nr_threads); let mut y_limits = vec![]; while !stop.load(atomic::Ordering::Acquire) { { // Wait for work to be done. run_barrier.wait(); let work_data = work_data.read(); // Run the render work. render(&work_data, &mut y_limits, cpu, nr_threads); } // Notify completion. done_barrier.wait(); } } pub struct Render { map: Arc, view_dist: i32, fov_angle: f32, yaw_angle: f32, roll_angle: f32, pitch_angle: f32, horizon: i32, pos: Point3D, sky_color: u32, terr_height: Cell, terr_height_pt: Cell<(i32, i32, i32)>, work_data: Arc>, run_barrier: Arc, done_barrier: Arc, nr_threads: usize, joinh: Vec>, threads_stop: Arc, } impl Render { pub fn new(map: Map) -> Render { let nr_threads = min(num_cpus::get(), RENDER_MAX_NR_THREADS); let mut render = Render { map: Arc::new(map), view_dist: 0, fov_angle: radians(0.0), yaw_angle: radians(0.0), roll_angle: radians(0.0), pitch_angle: radians(0.0), horizon: 0, pos: Default::default(), sky_color: 0, terr_height: Cell::new(0), terr_height_pt: Cell::new((i32::MAX, i32::MAX, i32::MAX)), work_data: Arc::new(RwLock::new(RenderWorkData { image_width: 0, image_height: 0, image: None, map: None, pos: Default::default(), view_dist: 0, horizon: 0, yaw_angle_sin: 0.0, yaw_angle_cos: 0.0, fov_angle_half_tan: 0.0, })), run_barrier: Arc::new(Barrier::new(nr_threads + 1)), done_barrier: Arc::new(Barrier::new(nr_threads + 1)), nr_threads, joinh: vec![], threads_stop: Arc::new(atomic::AtomicBool::new(false)), }; render.get_terrain_height_at(Point3D::new(1.0, 1.0, 1.0)); render.set_sky_color(0xFF007CB0); render.set_position(&Point3D::new(0.0, 0x5FFF as f32, 0.0)); render.set_view_distance(1000); render.set_fov_angle(90.0); render.set_yaw_angle(0.0); render.set_pitch_angle(0.0); render.calc_horizon(); // Start the worker threads. for cpu in 0..nr_threads { let stop = Arc::clone(&render.threads_stop); let done_barrier = Arc::clone(&render.done_barrier); let run_barrier = Arc::clone(&render.run_barrier); let work_data = Arc::clone(&render.work_data); let joinh = thread::spawn(move || { thread_render(cpu, nr_threads, stop, work_data, run_barrier, done_barrier); }); render.joinh.push(joinh); } render } pub fn set_sky_color(&mut self, color: u32) { self.sky_color = color; } fn calc_horizon(&mut self) { let screen_center = (Map::MAX_HEIGHT as i32 + 1) / 2; let height_mul = 1 << self.map.get_height_shift(); let pitch_offset = (self.pitch_angle.sin() * self.view_dist as f32) * height_mul as f32; self.horizon = screen_center + pitch_offset.round() as i32; } } impl View3D for Render { fn set_position(&mut self, pos: &Point3D) { if self.map.get_x_size() == 0 || self.map.get_y_size() == 0 { self.pos = Default::default(); } else { let y = pos.y(); self.pos = Point3D::new( pos.x() % self.map.get_x_size() as f32, if y > 0.0 { y } else { 0.0 }, pos.z() % self.map.get_y_size() as f32, ); } } fn get_position(&self) -> Point3D { self.pos } fn set_view_distance(&mut self, view_dist: i32) { self.view_dist = min(max(view_dist, 1), 8192); self.calc_horizon(); } fn get_view_distance(&self) -> i32 { self.view_dist } fn set_fov_angle(&mut self, mut fov_angle: f32) { if fov_angle < 15.0 { fov_angle = 15.0; } if fov_angle > 165.0 { fov_angle = 165.0; } self.fov_angle = radians(fov_angle); } fn get_fov_angle(&self) -> f32 { degrees(self.fov_angle) } fn set_yaw_angle(&mut self, yaw_angle: f32) { self.yaw_angle = radians(yaw_angle % 360.0); } fn get_yaw_angle(&self) -> f32 { degrees(self.yaw_angle) } fn set_roll_angle(&mut self, roll_angle: f32) { self.roll_angle = radians(roll_angle % 360.0); } fn get_roll_angle(&self) -> f32 { degrees(self.roll_angle) } fn set_pitch_angle(&mut self, mut pitch_angle: f32) { pitch_angle %= 360.0; if pitch_angle < -45.0 { pitch_angle = -45.0; } if pitch_angle > 45.0 { pitch_angle = 45.0; } self.pitch_angle = radians(pitch_angle); self.calc_horizon(); } fn get_pitch_angle(&self) -> f32 { degrees(self.pitch_angle) } } impl Landscape3D for Render { fn get_terrain_height_at(&self, point: Point3D) -> f32 { let (x, y, z) = ( point.x().round() as i32, point.y().round() as i32, point.z().round() as i32, ); let terr_height_pt = self.terr_height_pt.get(); let (hpt_x, hpt_z) = (terr_height_pt.0, terr_height_pt.2); if (x, z) != (hpt_x, hpt_z) { let map_value = self.map.get(x as usize, z as usize); self.terr_height_pt.set((x, y, z)); self.terr_height.set(map_value.height() as i32); } self.terr_height.get() as f32 } } impl Render3D for Render { fn calc_scene_dimensions(&self, desired_width: usize, desired_height: usize) -> (usize, usize) { ( desired_width - (desired_width % self.nr_threads), desired_height, ) } fn render_scene(&mut self, render_buf: &mut [u32], image_width: usize, image_height: usize) { assert!(image_width % self.nr_threads == 0); // Create a fake-Vec from the slice. // Be careful! This "Vec" must not be re-allocated or dropped. let mut image = unsafe { Vec::from_raw_parts(render_buf.as_mut_ptr(), render_buf.len(), render_buf.len()) }; image.fill(self.sky_color); let (yaw_angle_sin, yaw_angle_cos) = (self.yaw_angle as RenderType).sin_cos(); let fov_angle_half_tan = (self.fov_angle as RenderType / 2.0).tan(); { let image_slice_len = image_width / self.nr_threads; let image_cycle_len = self.nr_threads; let image_lock = RepVecRangeLock::new(image, image_slice_len, image_cycle_len); // Lock and prepare the work data. let mut work_data = self.work_data.write(); work_data.image_width = image_width as i32; work_data.image_height = image_height as i32; work_data.image = Some(Arc::new(image_lock)); work_data.map = Some(Arc::clone(&self.map)); work_data.pos = self.pos; work_data.view_dist = self.view_dist; work_data.horizon = self.horizon; work_data.yaw_angle_sin = yaw_angle_sin; work_data.yaw_angle_cos = yaw_angle_cos; work_data.fov_angle_half_tan = fov_angle_half_tan; } // Start all workers. self.run_barrier.wait(); // Wait for all workers. self.done_barrier.wait(); let mut work_data = self.work_data.write(); let image = Arc::try_unwrap(work_data.image.take().expect("Image data is None.")) .expect("Image: Failed to unwrap Arc.") .into_inner(); // Leak the fake-Vec. We must not de-allocate the buffer. image.leak(); } } impl Drop for Render { fn drop(&mut self) { self.threads_stop.store(true, atomic::Ordering::Release); for joinh in self.joinh.drain(..) { joinh.join().unwrap(); } } } // vim: ts=4 sw=4 expandtab