#![forbid(unsafe_code)]
mod cpu;
#[cfg(feature = "hud")]
mod cpu_egui;
mod gpu;
use std::sync::Arc;
use roxlap_core::opticast::OpticastSettings;
use roxlap_core::sky::Sky;
use roxlap_core::sprite::SpriteLighting;
use roxlap_core::Camera;
use roxlap_scene::Scene;
pub use roxlap_formats::kfa::KfaSprite;
pub use roxlap_formats::sprite::Sprite;
pub use roxlap_gpu::{GpuInitError, GpuRendererSettings};
pub use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
#[cfg(feature = "hud")]
pub use egui;
use crate::cpu::CpuBackend;
use crate::gpu::GpuBackend;
pub(crate) type DynDisplay = dyn HasDisplayHandle + Send + Sync + 'static;
pub(crate) type DynWindow = dyn HasWindowHandle + Send + Sync + 'static;
pub struct SpriteInstanceDesc {
pub model: usize,
pub pos: [f32; 3],
}
pub struct SpriteSet {
pub models: Vec<Sprite>,
pub instances: Vec<SpriteInstanceDesc>,
pub carve_model: Option<usize>,
}
pub struct FrameParams<'a> {
pub settings: &'a OpticastSettings,
pub sky_color: u32,
pub sky: Option<&'a Sky>,
pub fog_color: u32,
pub fog_max_scan_dist: i32,
pub treat_z_max_as_air: bool,
pub gpu_mip_scan_dist: f32,
pub gpu_max_outer_steps: u32,
pub gpu_fov_y_rad: f32,
pub sprite_lighting: Option<&'a SpriteLighting<'a>>,
pub side_shades: [i8; 6],
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct PickHit {
pub world: [f32; 3],
pub grid: roxlap_scene::GridId,
pub voxel: glam::IVec3,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Ray {
pub origin: glam::DVec3,
pub dir: glam::DVec3,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Backend {
Cpu,
Gpu,
}
pub struct RenderOptions {
pub want_gpu: bool,
pub gpu: GpuRendererSettings,
pub clear_sky: u32,
pub cpu_max_grid_vsid: u32,
pub cpu_render_threads: usize,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
want_gpu: false,
gpu: GpuRendererSettings::default(),
clear_sky: 0x0099_b3d9,
cpu_max_grid_vsid: 32 * roxlap_scene::CHUNK_SIZE_XY,
cpu_render_threads: 4,
}
}
}
enum BackendImpl {
Cpu(Box<CpuBackend>),
Gpu(Box<GpuBackend>),
}
pub struct SceneRenderer {
inner: BackendImpl,
}
impl SceneRenderer {
#[must_use]
pub fn new<W>(window: Arc<W>, size: (u32, u32), opts: &RenderOptions) -> Self
where
W: HasWindowHandle + HasDisplayHandle + Send + Sync + 'static,
{
if opts.want_gpu {
match GpuBackend::new(window.clone(), size, opts) {
Ok(g) => {
return Self {
inner: BackendImpl::Gpu(Box::new(g)),
};
}
Err(e) => {
eprintln!(
"roxlap-render: GPU init failed ({e}); falling back to the CPU renderer",
);
}
}
}
Self {
inner: BackendImpl::Cpu(Box::new(CpuBackend::new(window, size, opts))),
}
}
#[must_use]
pub fn backend(&self) -> Backend {
match self.inner {
BackendImpl::Cpu(_) => Backend::Cpu,
BackendImpl::Gpu(_) => Backend::Gpu,
}
}
#[must_use]
pub fn adapter_info(&self) -> Option<&str> {
match &self.inner {
BackendImpl::Gpu(g) => Some(g.adapter_info()),
BackendImpl::Cpu(_) => None,
}
}
pub fn set_sky_panorama(&mut self, rgba: &[u8], w: u32, h: u32) {
if let BackendImpl::Gpu(g) = &mut self.inner {
g.set_sky_panorama(rgba, w, h);
}
}
pub fn resize(&mut self, width: u32, height: u32) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.resize(width, height),
BackendImpl::Gpu(g) => g.resize(width, height),
}
}
pub fn render(&mut self, scene: &mut Scene, camera: &Camera, frame: &FrameParams) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.render(scene, camera, frame),
BackendImpl::Gpu(g) => g.render(scene, camera, frame),
}
}
pub fn present(&mut self) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.present(),
BackendImpl::Gpu(g) => g.present(),
}
}
#[cfg(feature = "hud")]
pub fn paint_egui(
&mut self,
jobs: &[egui::ClippedPrimitive],
textures: &egui::TexturesDelta,
pixels_per_point: f32,
) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.paint_egui(jobs, textures, pixels_per_point),
BackendImpl::Gpu(g) => g.paint_egui(jobs, textures, pixels_per_point),
}
}
pub fn set_sprites(&mut self, set: &SpriteSet) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.set_sprites(set),
BackendImpl::Gpu(g) => g.set_sprites(set),
}
}
pub fn set_kfa_sprites(&mut self, kfas: &mut [KfaSprite]) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.set_kfa_sprites(kfas),
BackendImpl::Gpu(g) => g.set_kfa_sprites(kfas),
}
}
pub fn update_kfa_poses(&mut self, kfas: &mut [KfaSprite]) {
match &mut self.inner {
BackendImpl::Cpu(c) => c.update_kfa_poses(kfas),
BackendImpl::Gpu(g) => g.update_kfa_poses(kfas),
}
}
pub fn carve_active_sprite(&mut self) -> u32 {
match &mut self.inner {
BackendImpl::Cpu(_) => 0,
BackendImpl::Gpu(g) => g.carve_active_sprite(),
}
}
pub fn request_capture(&mut self) {
if let BackendImpl::Cpu(c) = &mut self.inner {
c.request_capture();
}
}
pub fn take_capture(&mut self) -> Option<(Vec<u32>, u32, u32)> {
match &mut self.inner {
BackendImpl::Cpu(c) => c.take_capture(),
BackendImpl::Gpu(_) => None,
}
}
#[must_use]
pub fn pick_depth(&self, x: u32, y: u32) -> Option<f32> {
match &self.inner {
BackendImpl::Cpu(c) => c.pick_depth(x, y),
BackendImpl::Gpu(g) => g.pick_depth(x, y),
}
}
#[must_use]
pub fn pixel_ray(&self, camera: &Camera, x: f64, y: f64) -> Option<[f64; 3]> {
match &self.inner {
BackendImpl::Cpu(c) => c.pixel_ray(camera, x, y),
BackendImpl::Gpu(g) => g.pixel_ray(camera, x, y),
}
}
#[must_use]
pub fn view_ray(&self, camera: &Camera, x: f64, y: f64) -> Option<Ray> {
let d = self.pixel_ray(camera, x, y)?;
let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
if len < 1e-12 {
return None;
}
Some(Ray {
origin: glam::DVec3::from_array([camera.pos[0], camera.pos[1], camera.pos[2]]),
dir: glam::DVec3::new(d[0] / len, d[1] / len, d[2] / len),
})
}
#[must_use]
pub fn pick(&self, scene: &Scene, camera: &Camera, x: u32, y: u32) -> Option<PickHit> {
let dir = self.pixel_ray(camera, f64::from(x), f64::from(y))?;
let t = f64::from(self.pick_depth(x, y)?);
let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
if len < 1e-9 {
return None;
}
let s = t / len; let world = glam::DVec3::new(
camera.pos[0] + dir[0] * s,
camera.pos[1] + dir[1] * s,
camera.pos[2] + dir[2] * s,
);
let (grid, voxel) = scene.resolve_voxel(world, glam::DVec3::from_array(dir))?;
#[allow(clippy::cast_possible_truncation)]
let world_f32 = [world.x as f32, world.y as f32, world.z as f32];
Some(PickHit {
world: world_f32,
grid,
voxel,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn options_default_is_cpu_intent() {
let o = RenderOptions::default();
assert!(!o.want_gpu);
assert_eq!(o.clear_sky & 0xFF00_0000, 0, "clear_sky is 0x00RRGGBB");
}
}