nightshade-renderer 0.57.0

GPU-driven wgpu renderer with a built-in frame graph.
//! A thin windowing and frame-loop harness for the nightshade-renderer
//! examples. It owns only the window, a fly camera, and the render loop; each
//! example fills a [`SingleViewHost`] of renderer data using the raw renderer
//! and nightshade_ecs API (`world.spawn`, `RenderMaterials::add`, `mesh_cache_insert`,
//! `world.set`) and the loop draws it with `render_frame`. Everything here is a
//! free function or a plain data struct.
//!
//! Controls: W/A/S/D to move, Q/E down/up, arrow keys to look.

use std::collections::HashSet;
use std::sync::Arc;

use glm::{Mat4, Vec3};
use nalgebra_glm as glm;

use nightshade_ecs::Entity;
use nightshade_ecs::dynamic::DynWorld;
use nightshade_renderer::config::{
    EffectiveShading, RenderLight, RenderLightData, RenderSettings, RendererState,
};
use nightshade_renderer::mesh_cache::MeshCache;
use nightshade_renderer::render_world::{Transform, register_render_components};
use nightshade_renderer::wgpu::WgpuRenderer;
use nightshade_renderer::wgpu::frame::render_frame;
use nightshade_renderer::wgpu::presentation::install_presentation_passes;
#[cfg(feature = "screenshot")]
use nightshade_renderer::wgpu::presentation::{poll_screenshot_readback, request_screenshot_copy};
use nightshade_renderer::wgpu::render_configs::{
    CameraFrameInputs, CameraProjectionParams, SingleViewHost, ViewFrame, compose_single_view,
    restore_single_view,
};
use nightshade_renderer::wgpu::texture_cache::TextureCache;

use winit::application::ApplicationHandler;
use winit::event::{ElementState, KeyEvent, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowId};

const INITIAL_WIDTH: u32 = 1280;
const INITIAL_HEIGHT: u32 = 720;

// The renderer keys its per-camera state by this handle. The scene never spawns
// the camera, so any stable id works.
const CAMERA_ENTITY: Entity = Entity {
    id: 0,
    generation: 0,
};

/// A first-person fly camera as plain data. Yaw rotates about world +Y, pitch
/// about the camera's right axis; zero yaw looks down -Z.
pub struct Camera {
    pub position: Vec3,
    pub yaw: f32,
    pub pitch: f32,
    pub y_fov_rad: f32,
    pub z_near: f32,
    pub z_far: f32,
    pub move_speed: f32,
    pub look_speed: f32,
}

impl Default for Camera {
    fn default() -> Self {
        Self {
            position: Vec3::new(0.0, 1.5, 6.0),
            yaw: 0.0,
            pitch: -0.15,
            y_fov_rad: std::f32::consts::FRAC_PI_4,
            z_near: 0.1,
            z_far: 1000.0,
            move_speed: 6.0,
            look_speed: 1.5,
        }
    }
}

fn camera_forward(camera: &Camera) -> Vec3 {
    Vec3::new(
        camera.pitch.cos() * camera.yaw.sin(),
        camera.pitch.sin(),
        -camera.pitch.cos() * camera.yaw.cos(),
    )
}

fn camera_view(camera: &Camera) -> Mat4 {
    let target = camera.position + camera_forward(camera);
    glm::look_at(&camera.position, &target, &Vec3::new(0.0, 1.0, 0.0))
}

fn camera_advance(camera: &mut Camera, delta_time: f32, keys: &HashSet<KeyCode>) {
    let mut look = 0.0;
    let mut pitch = 0.0;
    if keys.contains(&KeyCode::ArrowLeft) {
        look += 1.0;
    }
    if keys.contains(&KeyCode::ArrowRight) {
        look -= 1.0;
    }
    if keys.contains(&KeyCode::ArrowUp) {
        pitch += 1.0;
    }
    if keys.contains(&KeyCode::ArrowDown) {
        pitch -= 1.0;
    }
    camera.yaw += look * camera.look_speed * delta_time;
    camera.pitch = (camera.pitch + pitch * camera.look_speed * delta_time).clamp(
        -std::f32::consts::FRAC_PI_2 + 0.01,
        std::f32::consts::FRAC_PI_2 - 0.01,
    );

    let forward = camera_forward(camera);
    let right = forward.cross(&Vec3::new(0.0, 1.0, 0.0)).normalize();
    let mut motion = Vec3::zeros();
    if keys.contains(&KeyCode::KeyW) {
        motion += forward;
    }
    if keys.contains(&KeyCode::KeyS) {
        motion -= forward;
    }
    if keys.contains(&KeyCode::KeyD) {
        motion += right;
    }
    if keys.contains(&KeyCode::KeyA) {
        motion -= right;
    }
    if keys.contains(&KeyCode::KeyE) {
        motion += Vec3::new(0.0, 1.0, 0.0);
    }
    if keys.contains(&KeyCode::KeyQ) {
        motion -= Vec3::new(0.0, 1.0, 0.0);
    }
    if motion.norm() > 0.0 {
        camera.position += motion.normalize() * camera.move_speed * delta_time;
    }
}

/// The directional-light components (a sun) an example spawns into the scene
/// world with `world.spawn(directional_light(..))`. A reusable helper because
/// `RenderLightData` carries far more fields than a sun sets.
pub fn directional_light(
    direction: Vec3,
    color: Vec3,
    intensity: f32,
) -> (RenderLightData, Transform) {
    let light = RenderLight::directional(CAMERA_ENTITY, direction, color, intensity);
    (light.light, Transform(light.transform))
}

fn new_host() -> SingleViewHost {
    SingleViewHost {
        scene_world: DynWorld::from_registry(register_render_components()),
        state: RendererState {
            occlusion_culling_enabled: false,
            ..RendererState::default()
        },
        mesh_cache: MeshCache::default(),
        texture_cache: TextureCache::default(),
        settings: RenderSettings::default(),
        debug_draw: Default::default(),
    }
}

/// What an example does: build the scene once in [`Example::setup`], then
/// optionally advance it each frame in [`Example::update`], which receives the
/// seconds elapsed since the first frame. A static example implements only
/// `setup` and inherits the no-op `update`.
pub trait Example {
    type State;
    fn setup(host: &mut SingleViewHost, camera: &mut Camera) -> Self::State;
    fn update(_: &mut SingleViewHost, _: &mut Camera, _: &mut Self::State, _: f32) {}
}

struct Harness<E: Example> {
    title: String,
    window: Option<Arc<Window>>,
    renderer: Option<WgpuRenderer>,
    host: SingleViewHost,
    camera: Camera,
    state: Option<E::State>,
    keys: HashSet<KeyCode>,
    last_frame: std::time::Instant,
    time: f32,
    shot: Option<std::path::PathBuf>,
    shot_frames: u32,
}

impl<E: Example> Harness<E> {
    fn frame(&mut self, width: u32, height: u32, delta_time: f32) {
        self.time += delta_time;
        camera_advance(&mut self.camera, delta_time, &self.keys);
        if let Some(state) = self.state.as_mut() {
            E::update(&mut self.host, &mut self.camera, state, self.time);
        }
        let aspect = width as f32 / height.max(1) as f32;
        let camera = CameraFrameInputs::perspective(
            CAMERA_ENTITY,
            camera_view(&self.camera),
            self.camera.position,
            CameraProjectionParams {
                z_near: self.camera.z_near,
                z_far: self.camera.z_far,
                y_fov_rad: self.camera.y_fov_rad,
                aspect,
                orthographic: None,
            },
            EffectiveShading::from_settings(&self.host.settings, &Default::default()),
        );
        let view = ViewFrame {
            camera,
            viewport: (width.max(1), height.max(1)),
            uptime_milliseconds: (self.time * 1000.0) as u64,
            delta_time,
        };
        let mut inputs = compose_single_view(&mut self.host, view);
        let mut outputs = Default::default();
        if let Some(renderer) = self.renderer.as_mut()
            && let Err(error) = render_frame(renderer, &mut inputs, &mut outputs)
        {
            eprintln!("render_frame failed: {error}");
        }
        restore_single_view(&mut self.host, inputs);
    }

    fn render(&mut self) {
        let Some(window) = self.window.as_ref() else {
            return;
        };
        let size = window.inner_size();
        if size.width == 0 || size.height == 0 {
            return;
        }
        let now = std::time::Instant::now();
        let delta_time = (now - self.last_frame).as_secs_f32().min(0.1);
        self.last_frame = now;
        self.frame(size.width, size.height, delta_time);
    }

    // Renders a fixed number of frames offscreen and writes the last to a PNG,
    // used by NS_SHOT=<path> for headless capture. The GPU-driven passes need a
    // few frames of warmup before the first draws land, so this renders many.
    fn capture(&mut self, width: u32, height: u32, path: &std::path::Path) {
        for _ in 0..self.shot_frames.max(1) {
            self.frame(width, height, 1.0 / 60.0);
        }
        #[cfg(feature = "screenshot")]
        {
            if let Some(renderer) = self.renderer.as_mut() {
                if let Some(parent) = path.parent() {
                    let _ = std::fs::create_dir_all(parent);
                }
                request_screenshot_copy(renderer, Some(path.to_path_buf()), None);
                let mut guard = 0;
                while renderer.screenshot_pending() && guard < 600 {
                    let _ = renderer
                        .device
                        .poll(nightshade_renderer::wgpu::PollType::wait_indefinitely());
                    poll_screenshot_readback(renderer);
                    guard += 1;
                }
                let mut written = 0;
                let mut waited = 0;
                while written == 0 && waited < 5000 {
                    written = std::fs::metadata(path).map(|meta| meta.len()).unwrap_or(0);
                    if written == 0 {
                        std::thread::sleep(std::time::Duration::from_millis(2));
                        waited += 1;
                    }
                }
                if written > 0 {
                    println!("wrote {}", path.display());
                } else {
                    eprintln!("screenshot was not written");
                }
            }
        }
        #[cfg(not(feature = "screenshot"))]
        {
            let _ = path;
            eprintln!("NS_SHOT needs the screenshot feature: --features wgpu,hdr,screenshot");
        }
    }
}

impl<E: Example> ApplicationHandler for Harness<E> {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.window.is_some() {
            return;
        }
        let attributes = Window::default_attributes()
            .with_title(self.title.clone())
            .with_visible(self.shot.is_none())
            .with_inner_size(winit::dpi::PhysicalSize::new(INITIAL_WIDTH, INITIAL_HEIGHT));
        let window = Arc::new(
            event_loop
                .create_window(attributes)
                .expect("failed to create window"),
        );
        let size = window.inner_size();
        let mut renderer = pollster::block_on(WgpuRenderer::new_async(
            window.clone(),
            size.width.max(1),
            size.height.max(1),
        ))
        .expect("failed to create renderer");
        install_presentation_passes(&mut renderer).expect("failed to install presentation passes");

        self.renderer = Some(renderer);
        self.state = Some(E::setup(&mut self.host, &mut self.camera));

        if let Some(path) = self.shot.clone() {
            self.capture(size.width.max(1), size.height.max(1), &path);
            event_loop.exit();
            return;
        }
        self.window = Some(window);
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::KeyboardInput {
                event:
                    KeyEvent {
                        physical_key: PhysicalKey::Code(code),
                        state,
                        ..
                    },
                ..
            } => {
                if code == KeyCode::Escape {
                    event_loop.exit();
                } else if state == ElementState::Pressed {
                    self.keys.insert(code);
                } else {
                    self.keys.remove(&code);
                }
            }
            WindowEvent::Resized(size) if size.width > 0 && size.height > 0 => {
                if let Some(renderer) = self.renderer.as_mut()
                    && let Err(error) = renderer.resize_surface(size.width, size.height)
                {
                    eprintln!("resize_surface failed: {error}");
                }
            }
            WindowEvent::RedrawRequested => {
                self.render();
                if let Some(window) = self.window.as_ref() {
                    window.request_redraw();
                }
            }
            _ => {}
        }
    }
}

/// Opens a window, brings the renderer up, runs the example's `setup` once, then
/// its `update` every frame before composing and drawing. `NS_SHOT=<path>`
/// renders offscreen and writes a PNG instead of presenting.
pub fn run<E: Example>(title: &str)
where
    E::State: 'static,
{
    let shot = std::env::var("NS_SHOT").ok().map(std::path::PathBuf::from);
    let shot_frames = std::env::var("NS_SHOT_FRAMES")
        .ok()
        .and_then(|value| value.parse().ok())
        .unwrap_or(120);
    let event_loop = EventLoop::new().expect("failed to create event loop");
    event_loop.set_control_flow(ControlFlow::Poll);
    let mut harness: Harness<E> = Harness {
        title: title.to_string(),
        window: None,
        renderer: None,
        host: new_host(),
        camera: Camera::default(),
        state: None,
        keys: HashSet::new(),
        last_frame: std::time::Instant::now(),
        time: 0.0,
        shot,
        shot_frames,
    };
    event_loop.run_app(&mut harness).expect("event loop failed");
}