roxlap-render 0.24.0

Unified CPU/GPU renderer facade for the roxlap scene-graph engine — one SceneRenderer over the roxlap-core CPU DDA renderer (softbuffer) and roxlap-gpu (wgpu), with automatic CPU fallback.
Documentation
//! Minimal roxlap "hello voxel world": open a winit window, build a
//! tiny scene (a grass plain + a blue dome), and render it with the
//! unified [`SceneRenderer`] facade — a slow orbit camera so the scene
//! reads as 3D.
//!
//! ```sh
//! cargo run --release -p roxlap-render --example quickstart
//! ROXLAP_GPU=0 cargo run --release -p roxlap-render --example quickstart  # force CPU
//! ```
//!
//! The renderer asks for the GPU backend and **falls back to CPU
//! automatically** when WGPU init fails, so this runs anywhere.
//!
//! To depend on roxlap from your own game, add to `Cargo.toml`:
//!
//! ```toml
//! roxlap-render = "0.22"   # SceneRenderer facade (this crate)
//! roxlap-scene  = "0.22"   # Scene / Grid / edits / streaming
//! roxlap-core   = "0.22"   # Camera + OpticastSettings
//! ```
//!
//! The `// ANCHOR:` comments mark sections included verbatim by the
//! engine book (`docs/book/`) — keep them when editing, and expect a
//! red `mdbook build` CI job if one goes missing.

use std::sync::Arc;
use std::time::Instant;

use glam::{DVec3, IVec3};
use roxlap_core::opticast::OpticastSettings;
use roxlap_core::Camera;
use roxlap_render::{BackendPreference, FrameParams, RenderOptions, Rgb, SceneRenderer, VoxColor};
use roxlap_scene::{GridTransform, Scene};
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::{Window, WindowId};

// ANCHOR: colors
/// [`VoxColor`] packs a voxel colour: RGB + a brightness byte (NOT
/// alpha; `rgb()` uses the neutral 0x80 — lighting bakes rewrite it).
const GRASS: VoxColor = VoxColor::rgb(0x4d, 0x8a, 0x3a);
const DOME: VoxColor = VoxColor::rgb(0x40, 0x60, 0xc0);
/// Sky/fog/tints are plain [`Rgb`] — no packing surprises.
const SKY: Rgb = Rgb::new(0x8f, 0xbc, 0xd4);
// ANCHOR_END: colors

// ANCHOR: build_scene
/// A one-grid scene. Grid-local voxel coordinates; **+z is down**, so
/// the ground surface at `z = 210` fills downward toward the bedrock
/// placeholder at `z = 255`, and "up" is toward smaller z.
fn build_scene() -> Scene {
    let mut scene = Scene::new();
    let id = scene.add_grid(GridTransform::at(DVec3::ZERO));
    let grid = scene.grid_mut(id).expect("grid just added");
    grid.set_rect(
        IVec3::new(-128, -128, 210),
        IVec3::new(127, 127, 254),
        Some(GRASS),
    );
    grid.set_sphere(IVec3::new(0, 0, 205), 30, Some(DOME));
    scene
}
// ANCHOR_END: build_scene

// ANCHOR: render_frame
/// One frame: orbit the camera, render the scene, present.
fn render_frame(renderer: &mut SceneRenderer, scene: &mut Scene, window: &Window, elapsed: f64) {
    // `Camera::orbit` / `from_yaw_pitch` / `look_at` produce the
    // canonical right-handed basis the sprite frustum cull needs —
    // don't hand-roll one.
    let camera = Camera::orbit(elapsed * 0.3, 0.35, 220.0, [0.0, 0.0, 195.0]);

    let size = window.inner_size();
    let settings = OpticastSettings::for_oracle_framebuffer(size.width.max(1), size.height.max(1));
    // Defaults for everything but the sky; the GPU projection is
    // derived from `settings`, so both backends show the same field
    // of view.
    let mut frame = FrameParams::new(&settings);
    frame.sky_color = SKY;
    frame.fog_color = SKY;

    renderer.render(scene, &camera, &frame);
    renderer.present(); // render() composites; present() finishes
}
// ANCHOR_END: render_frame

/// `renderer` is declared **before** `window` so it drops first: the
/// render surface must release its raw window handles while the window
/// is still alive.
#[derive(Default)]
struct App {
    renderer: Option<SceneRenderer>,
    window: Option<Arc<Window>>,
    scene: Option<Scene>,
    started: Option<Instant>,
}

impl ApplicationHandler for App {
    // ANCHOR: init
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        let window = Arc::new(
            event_loop
                .create_window(Window::default_attributes().with_title("roxlap quickstart"))
                .expect("create window"),
        );
        let size = window.inner_size();
        // QE.7b - BackendPreference replaces want_gpu; the demos prefer
        // GPU with automatic CPU fallback.
        let backend = if std::env::var_os("ROXLAP_GPU").map_or(true, |v| v != "0") {
            BackendPreference::PreferGpu
        } else {
            BackendPreference::Cpu
        };
        let opts = RenderOptions {
            backend,
            clear_sky: SKY,
            ..RenderOptions::default()
        };
        self.renderer = Some(SceneRenderer::new(
            window.clone(),
            (size.width, size.height),
            &opts,
        ));
        self.window = Some(window);
        self.scene = Some(build_scene());
        self.started = Some(Instant::now());
    }
    // ANCHOR_END: init

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
        let (Some(renderer), Some(scene)) = (self.renderer.as_mut(), self.scene.as_mut()) else {
            return;
        };
        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::Resized(size) => renderer.resize(size.width.max(1), size.height.max(1)),
            WindowEvent::RedrawRequested => {
                let t = self.started.map_or(0.0, |s| s.elapsed().as_secs_f64());
                let window = self.window.as_ref().expect("window outlives renderer");
                render_frame(renderer, scene, window, t);
                window.request_redraw();
            }
            _ => {}
        }
    }

    // ANCHOR: teardown
    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
        // Drain in-flight GPU work before surface/window teardown so
        // quitting never yanks the swapchain mid-submission.
        if let Some(renderer) = self.renderer.as_mut() {
            renderer.wait_idle();
        }
    }
    // ANCHOR_END: teardown
}

fn main() {
    // roxlap-render reports diagnostics (e.g. why GPU init failed and
    // it fell back to CPU) through the `log` facade — install any
    // logger to see them. RUST_LOG=debug for more detail.
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
    let event_loop = EventLoop::new().expect("create event loop");
    event_loop
        .run_app(&mut App::default())
        .expect("run event loop");
}