#![allow(clippy::must_use_candidate, clippy::too_many_lines)]
pub mod camera;
pub mod decompress;
pub mod grid;
#[cfg(not(target_arch = "wasm32"))]
pub mod headless;
pub mod resident;
pub mod scene;
pub mod sprite_model;
pub use camera::Camera;
pub use decompress::{decompress_chunk, ChunkUpload, BEDROCK_RGB, CHUNK_Z};
pub use grid::{bounding_box_of, GpuGridResident, GridUpload};
#[cfg(not(target_arch = "wasm32"))]
pub use headless::HeadlessGpu;
pub use resident::GpuChunkResident;
pub use scene::{
GpuSceneResident, GridRuntimeTransform, GridStaticMeta, RefreshOutcome, SceneUpload,
MAX_SCENE_GRIDS,
};
pub use sprite_model::{
build_sprite_model, SpriteInstance, SpriteInstanceTransform, SpriteModel, SpriteModelRegistry,
SpriteRegistryResident,
};
use std::sync::Arc;
use bytemuck::{Pod, Zeroable};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
#[derive(Debug, Clone, Copy)]
pub struct GpuRendererSettings {
pub power_preference: PowerPreference,
pub clear_colour: [f64; 3],
pub uncapped_present: bool,
}
#[derive(Debug, Clone, Copy)]
pub enum PowerPreference {
Low,
High,
}
impl Default for GpuRendererSettings {
fn default() -> Self {
Self {
power_preference: PowerPreference::High,
clear_colour: [0.06, 0.08, 0.12],
uncapped_present: true,
}
}
}
#[derive(Debug)]
pub enum GpuInitError {
CreateSurface(wgpu::CreateSurfaceError),
NoAdapter,
RequestDevice(wgpu::RequestDeviceError),
}
impl std::fmt::Display for GpuInitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CreateSurface(e) => write!(f, "create_surface failed: {e}"),
Self::NoAdapter => write!(
f,
"no compatible adapter — does this system have a Vulkan/Metal/DX12 driver?"
),
Self::RequestDevice(e) => write!(f, "request_device failed: {e}"),
}
}
}
impl std::error::Error for GpuInitError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::CreateSurface(e) => Some(e),
Self::RequestDevice(e) => Some(e),
Self::NoAdapter => None,
}
}
}
impl From<wgpu::CreateSurfaceError> for GpuInitError {
fn from(value: wgpu::CreateSurfaceError) -> Self {
Self::CreateSurface(value)
}
}
impl From<wgpu::RequestDeviceError> for GpuInitError {
fn from(value: wgpu::RequestDeviceError) -> Self {
Self::RequestDevice(value)
}
}
#[derive(Clone, Copy, Debug)]
pub struct GpuLine {
pub a: [f32; 3],
pub b: [f32; 3],
pub color: [f32; 4],
pub width_px: f32,
pub depth_test: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct GpuLineCamera {
pub pos: [f32; 3],
pub right: [f32; 3],
pub down: [f32; 3],
pub forward: [f32; 3],
}
const LINE_NEAR_Z: f32 = 0.0625;
const LINE_DEPTH_BIAS: f32 = 0.5;
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct LineVertex {
pos: [f32; 2],
depth: f32,
depth_test: f32,
color: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct LineParams {
screen_w: u32,
screen_h: u32,
depth_bias: f32,
no_depth: u32,
}
struct LineResources {
pipeline: wgpu::RenderPipeline,
bgl: wgpu::BindGroupLayout,
uniform_buf: wgpu::Buffer,
dummy_depth: wgpu::Buffer,
}
fn build_line_vertices(
cam: &GpuLineCamera,
lines: &[GpuLine],
w: u32,
h: u32,
fov_y: f32,
) -> Vec<LineVertex> {
let aspect = w as f32 / h as f32;
let half_h = (fov_y * 0.5).tan();
let half_w = half_h * aspect;
let (wf, hf) = (w as f32, h as f32);
let cam_coords = |p: [f32; 3]| -> [f32; 3] {
let d = [p[0] - cam.pos[0], p[1] - cam.pos[1], p[2] - cam.pos[2]];
[
cam.right[0] * d[0] + cam.right[1] * d[1] + cam.right[2] * d[2],
cam.down[0] * d[0] + cam.down[1] * d[1] + cam.down[2] * d[2],
cam.forward[0] * d[0] + cam.forward[1] * d[1] + cam.forward[2] * d[2],
]
};
let project = |q: [f32; 3]| -> ([f32; 2], f32) {
let inv = 1.0 / q[2];
let nx = q[0] * inv / half_w;
let ny = -q[1] * inv / half_h;
let depth = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2]).sqrt();
([nx, ny], depth)
};
let mut out = Vec::with_capacity(lines.len() * 6);
for line in lines {
let ca = cam_coords(line.a);
let cb = cam_coords(line.b);
let (cfa, cfb) = (ca[2], cb[2]);
if cfa < LINE_NEAR_Z && cfb < LINE_NEAR_Z {
continue;
}
let (mut t0, mut t1) = (0.0f32, 1.0f32);
let dz = cfb - cfa;
if dz.abs() > f32::EPSILON {
let tn = (LINE_NEAR_Z - cfa) / dz;
if dz > 0.0 {
t0 = t0.max(tn);
} else {
t1 = t1.min(tn);
}
}
if t0 > t1 {
continue;
}
let lerp3 = |t: f32| {
[
ca[0] + (cb[0] - ca[0]) * t,
ca[1] + (cb[1] - ca[1]) * t,
ca[2] + (cb[2] - ca[2]) * t,
]
};
let (n0, d0) = project(lerp3(t0));
let (n1, d1) = project(lerp3(t1));
let to_px = |n: [f32; 2]| [(n[0] * 0.5 + 0.5) * wf, (0.5 - n[1] * 0.5) * hf];
let to_ndc = |p: [f32; 2]| [p[0] / wf * 2.0 - 1.0, 1.0 - p[1] / hf * 2.0];
let p0 = to_px(n0);
let p1 = to_px(n1);
let (dx, dy) = (p1[0] - p0[0], p1[1] - p0[1]);
let len = (dx * dx + dy * dy).sqrt().max(1e-6);
let half = line.width_px.max(1.0) * 0.5;
let (ex, ey) = (-dy / len * half, dx / len * half);
let c0a = to_ndc([p0[0] + ex, p0[1] + ey]);
let c0b = to_ndc([p0[0] - ex, p0[1] - ey]);
let c1a = to_ndc([p1[0] + ex, p1[1] + ey]);
let c1b = to_ndc([p1[0] - ex, p1[1] - ey]);
let dt = if line.depth_test { 1.0 } else { 0.0 };
let vert = |pos: [f32; 2], depth: f32| LineVertex {
pos,
depth,
depth_test: dt,
color: line.color,
};
out.push(vert(c0a, d0));
out.push(vert(c0b, d0));
out.push(vert(c1a, d1));
out.push(vert(c1a, d1));
out.push(vert(c0b, d0));
out.push(vert(c1b, d1));
}
out
}
pub struct GpuRenderer {
surface: wgpu::Surface<'static>,
surface_config: wgpu::SurfaceConfiguration,
device: wgpu::Device,
queue: wgpu::Queue,
adapter_info: String,
clear_colour: [f64; 3],
frame_count: u32,
chunk_dda: Option<ChunkDdaResources>,
grid_dda: Option<GridDdaResources>,
scene_dda: Option<SceneDdaResources>,
sky_texture: wgpu::Texture,
sky_view: wgpu::TextureView,
sky_sampler: wgpu::Sampler,
fog_color: [f32; 3],
fog_near: f32,
fog_far: f32,
sprite_registry: Option<sprite_model::SpriteRegistryResident>,
sprite_model_dda: Option<SpriteModelDdaResources>,
sprite_lod_px: f32,
scene_mip_scan_dist: f32,
scene_side_shades: [[i32; 4]; 2],
last_fov_y_rad: f32,
pending_frame: Option<(wgpu::SurfaceTexture, wgpu::TextureView)>,
line_resources: Option<LineResources>,
line_vbuf: Option<wgpu::Buffer>,
line_vbuf_cap: u64,
#[cfg(feature = "hud")]
egui_renderer: Option<egui_wgpu::Renderer>,
}
struct ChunkDdaResources {
storage_size: (u32, u32),
storage_view: wgpu::TextureView,
uniform_buf: wgpu::Buffer,
bgl_dda: wgpu::BindGroupLayout,
pipeline_dda: wgpu::ComputePipeline,
blit_bg: wgpu::BindGroup,
pipeline_blit: wgpu::RenderPipeline,
_sampler: wgpu::Sampler,
}
struct GridDdaResources {
storage_size: (u32, u32),
storage_view: wgpu::TextureView,
uniform_buf: wgpu::Buffer,
bgl_dda: wgpu::BindGroupLayout,
pipeline_dda: wgpu::ComputePipeline,
blit_bg: wgpu::BindGroup,
pipeline_blit: wgpu::RenderPipeline,
_sampler: wgpu::Sampler,
}
struct SceneDdaResources {
storage_size: (u32, u32),
framebuffer: wgpu::Buffer,
uniform_buf: wgpu::Buffer,
bgl_dda: wgpu::BindGroupLayout,
pipeline_dda: wgpu::ComputePipeline,
blit_bg: wgpu::BindGroup,
pipeline_blit: wgpu::RenderPipeline,
depth_buffer: wgpu::Buffer,
depth_readback: wgpu::Buffer,
}
struct SpriteModelDdaResources {
bgl: wgpu::BindGroupLayout,
pipeline: wgpu::ComputePipeline,
uniform_buf: wgpu::Buffer,
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct SpriteModelUniform {
cam_pos: [f32; 3],
_p0: f32,
cam_right: [f32; 3],
_p1: f32,
cam_down: [f32; 3],
_p2: f32,
cam_forward: [f32; 3],
_p3: f32,
fog_color: [f32; 4],
screen_size: [u32; 2],
instance_count: u32,
fog_far: f32,
fov_y_rad: f32,
tiles_x: u32,
tile_size: u32,
_p6: f32,
}
const SCENE_MAX_GRIDS: usize = MAX_SCENE_GRIDS as usize;
const SPRITE_TILE_SIZE: u32 = 16;
const _: () = assert!(scene::MAX_OCC_PAGES == 4);
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct SceneDdaPerGridCamera {
pos: [f32; 3],
_pad0: f32,
right: [f32; 3],
_pad1: f32,
down: [f32; 3],
_pad2: f32,
forward: [f32; 3],
_pad3: f32,
}
impl SceneDdaPerGridCamera {
fn from_camera(c: &Camera) -> Self {
Self {
pos: c.position,
_pad0: 0.0,
right: c.right,
_pad1: 0.0,
down: c.down,
_pad2: 0.0,
forward: c.forward,
_pad3: 0.0,
}
}
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct SceneDdaUniform {
fov_y_rad: f32,
grid_count: u32,
max_outer_steps: u32,
_pad0: u32,
screen_size: [u32; 2],
_pad1: [u32; 2],
cameras: [SceneDdaPerGridCamera; SCENE_MAX_GRIDS],
fog_color: [f32; 4],
fog_far: f32,
write_depth: u32,
occ_page_words: u32,
occ_num_pages: u32,
mip_scan_dist: f32,
_pad2: u32,
_pad3: u32,
_pad4: u32,
sky_cam: SceneDdaPerGridCamera,
side_shades0: [i32; 4],
side_shades1: [i32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct GridDdaUniform {
camera_pos: [f32; 3],
_pad0: f32,
camera_right: [f32; 3],
_pad1: f32,
camera_down: [f32; 3],
_pad2: f32,
camera_forward: [f32; 3],
fov_y_rad: f32,
screen_size: [u32; 2],
vsid: u32,
max_outer_steps: u32,
chunks_dims: [u32; 3],
_pad3: u32,
origin_chunk: [i32; 3],
_pad4: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct ChunkDdaUniform {
camera_pos: [f32; 3],
_pad0: f32,
camera_right: [f32; 3],
_pad1: f32,
camera_down: [f32; 3],
_pad2: f32,
camera_forward: [f32; 3],
fov_y_rad: f32,
screen_size: [u32; 2],
vsid: u32,
max_scan_dist: u32,
}
impl GpuRenderer {
pub async fn new<W>(
window: Arc<W>,
size: (u32, u32),
settings: GpuRendererSettings,
) -> Result<Self, GpuInitError>
where
W: HasWindowHandle + HasDisplayHandle + Send + Sync + 'static,
{
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let surface = instance.create_surface(window.clone())?;
let adapter = Self::request_adapter(&instance, Some(&surface), settings).await?;
let (device, queue) = Self::request_device(&adapter).await?;
Ok(Self::finish_init(
&adapter, device, queue, surface, size, settings,
))
}
#[cfg(target_arch = "wasm32")]
pub async fn new_from_canvas(
canvas: web_sys::HtmlCanvasElement,
size: (u32, u32),
settings: GpuRendererSettings,
) -> Result<Self, GpuInitError> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = Self::request_adapter(&instance, None, settings).await?;
let (device, queue) = Self::request_device(&adapter).await?;
let surface = instance.create_surface(wgpu::SurfaceTarget::Canvas(canvas))?;
Ok(Self::finish_init(
&adapter, device, queue, surface, size, settings,
))
}
async fn request_adapter(
instance: &wgpu::Instance,
compatible_surface: Option<&wgpu::Surface<'static>>,
settings: GpuRendererSettings,
) -> Result<wgpu::Adapter, GpuInitError> {
let power_preference = match settings.power_preference {
PowerPreference::Low => wgpu::PowerPreference::LowPower,
PowerPreference::High => wgpu::PowerPreference::HighPerformance,
};
instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference,
compatible_surface,
force_fallback_adapter: false,
})
.await
.map_err(|_| GpuInitError::NoAdapter)
}
async fn request_device(
adapter: &wgpu::Adapter,
) -> Result<(wgpu::Device, wgpu::Queue), GpuInitError> {
Ok(adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("roxlap-gpu device"),
required_features: wgpu::Features::empty(),
required_limits: pick_required_limits(&adapter.limits()),
experimental_features: wgpu::ExperimentalFeatures::disabled(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
})
.await?)
}
fn finish_init(
adapter: &wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
surface: wgpu::Surface<'static>,
size: (u32, u32),
settings: GpuRendererSettings,
) -> Self {
let info = adapter.get_info();
let adapter_info = format!(
"{name} ({backend:?}, {device_type:?})",
name = info.name,
backend = info.backend,
device_type = info.device_type,
);
let caps = surface.get_capabilities(adapter);
let surface_format = caps
.formats
.iter()
.copied()
.find(|f| {
matches!(
f,
wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Rgba8Unorm
)
})
.or_else(|| caps.formats.iter().copied().find(|f| !f.is_srgb()))
.unwrap_or(caps.formats[0]);
let present_mode = if settings.uncapped_present {
pick_present_mode(&caps.present_modes)
} else {
wgpu::PresentMode::Fifo
};
eprintln!(
"roxlap-gpu: present mode = {present_mode:?} (available: {:?})",
caps.present_modes,
);
let (init_w, init_h) = size;
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: init_w.max(1),
height: init_h.max(1),
present_mode,
alpha_mode: caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &surface_config);
let default_sky_pixel = [0x80u8, 0x80, 0x80, 0xff];
let (sky_texture, sky_view) = create_sky_texture(&device, 1, 1, &default_sky_pixel);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &sky_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&default_sky_pixel,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4),
rows_per_image: Some(1),
},
wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
);
let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("roxlap-gpu sky_sampler"),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
Self {
surface,
surface_config,
device,
queue,
adapter_info,
clear_colour: settings.clear_colour,
frame_count: 0,
chunk_dda: None,
grid_dda: None,
scene_dda: None,
sky_texture,
sky_view,
sky_sampler,
fog_color: [0.66, 0.74, 0.88],
fog_near: 0.0,
fog_far: 1.0e30,
sprite_registry: None,
sprite_model_dda: None,
sprite_lod_px: 4.0,
scene_mip_scan_dist: 64.0,
scene_side_shades: [[0; 4]; 2],
last_fov_y_rad: 0.0,
pending_frame: None,
line_resources: None,
line_vbuf: None,
line_vbuf_cap: 0,
#[cfg(feature = "hud")]
egui_renderer: None,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new_blocking<W>(
window: Arc<W>,
size: (u32, u32),
settings: GpuRendererSettings,
) -> Result<Self, GpuInitError>
where
W: HasWindowHandle + HasDisplayHandle + Send + Sync + 'static,
{
pollster::block_on(Self::new(window, size, settings))
}
pub fn adapter_info(&self) -> &str {
&self.adapter_info
}
pub fn device(&self) -> &wgpu::Device {
&self.device
}
pub fn queue(&self) -> &wgpu::Queue {
&self.queue
}
pub fn set_sky_panorama(&mut self, rgba: &[u8], width: u32, height: u32) {
assert_eq!(
rgba.len(),
(width as usize) * (height as usize) * 4,
"set_sky_panorama: expected w*h*4 bytes, got {}",
rgba.len(),
);
let (tex, view) = create_sky_texture(&self.device, width, height, rgba);
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
self.sky_texture = tex;
self.sky_view = view;
}
pub fn set_fog(&mut self, color: [f32; 3], near: f32, far: f32) {
self.fog_color = color;
self.fog_near = near;
self.fog_far = far.max(near + 1.0);
}
pub fn resize(&mut self, width: u32, height: u32) {
if width == 0 || height == 0 {
return;
}
self.surface_config.width = width;
self.surface_config.height = height;
self.surface.configure(&self.device, &self.surface_config);
self.chunk_dda = None;
self.grid_dda = None;
self.scene_dda = None;
}
fn acquire_frame(&self) -> Option<wgpu::SurfaceTexture> {
use wgpu::CurrentSurfaceTexture as C;
match self.surface.get_current_texture() {
C::Success(t) | C::Suboptimal(t) => Some(t),
C::Outdated | C::Lost => {
self.surface.configure(&self.device, &self.surface_config);
None
}
C::Timeout | C::Occluded | C::Validation => None,
}
}
pub fn render(&mut self) {
let Some(surf_tex) = self.acquire_frame() else {
return;
};
let view = surf_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let phase = f64::from(self.frame_count % 1257) * 0.005;
let [r, g, b] = self.clear_colour;
let drift = (phase.sin() * 0.04 + 0.04).clamp(0.0, 0.1);
let clear = wgpu::Color {
r: (r + drift).clamp(0.0, 1.0),
g: (g + drift * 0.5).clamp(0.0, 1.0),
b: (b + drift * 0.25).clamp(0.0, 1.0),
a: 1.0,
};
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("roxlap-gpu encoder"),
});
{
let _rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("roxlap-gpu clear"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(clear),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
}
self.queue.submit(std::iter::once(encoder.finish()));
surf_tex.present();
self.frame_count = self.frame_count.wrapping_add(1);
}
pub fn render_chunk(
&mut self,
resident: &GpuChunkResident,
camera: &Camera,
max_scan_dist: u32,
) {
let Some(surf_tex) = self.acquire_frame() else {
return;
};
let surf_view = surf_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let surface_w = self.surface_config.width;
let surface_h = self.surface_config.height;
let surface_format = self.surface_config.format;
let needs_build = match &self.chunk_dda {
Some(r) => r.storage_size != (surface_w, surface_h),
None => true,
};
if needs_build {
self.chunk_dda = Some(self.build_chunk_dda(surface_w, surface_h, surface_format));
}
let dda = self.chunk_dda.as_ref().expect("just built");
let uniform = ChunkDdaUniform {
camera_pos: camera.position,
_pad0: 0.0,
camera_right: camera.right,
_pad1: 0.0,
camera_down: camera.down,
_pad2: 0.0,
camera_forward: camera.forward,
fov_y_rad: camera.fov_y_rad,
screen_size: [surface_w, surface_h],
vsid: resident.vsid,
max_scan_dist,
};
self.queue
.write_buffer(&dda.uniform_buf, 0, bytemuck::bytes_of(&uniform));
let dda_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("roxlap-gpu chunk_dda.bg"),
layout: &dda.bgl_dda,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: dda.uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: resident.occupancy.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: resident.color_offsets.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: resident.colors.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(&dda.storage_view),
},
],
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("roxlap-gpu chunk encoder"),
});
{
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("roxlap-gpu chunk_dda compute"),
timestamp_writes: None,
});
cpass.set_pipeline(&dda.pipeline_dda);
cpass.set_bind_group(0, &dda_bg, &[]);
cpass.dispatch_workgroups(surface_w.div_ceil(8), surface_h.div_ceil(8), 1);
}
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("roxlap-gpu chunk_dda blit"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &surf_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rpass.set_pipeline(&dda.pipeline_blit);
rpass.set_bind_group(0, &dda.blit_bg, &[]);
rpass.draw(0..3, 0..1);
}
self.queue.submit(std::iter::once(encoder.finish()));
surf_tex.present();
self.frame_count = self.frame_count.wrapping_add(1);
}
fn build_chunk_dda(
&self,
width: u32,
height: u32,
surface_format: wgpu::TextureFormat,
) -> ChunkDdaResources {
let storage_tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("roxlap-gpu chunk_dda.storage"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let storage_view = storage_tex.create_view(&wgpu::TextureViewDescriptor::default());
let uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu chunk_dda.uniform"),
size: std::mem::size_of::<ChunkDdaUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let dda_shader = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("chunk_dda.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/chunk_dda.wgsl").into()),
});
let bgl_dda = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("roxlap-gpu chunk_dda.bgl"),
entries: &[
bgl_uniform_entry(0),
bgl_storage_entry(1, true),
bgl_storage_entry(2, true),
bgl_storage_entry(3, true),
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::StorageTexture {
access: wgpu::StorageTextureAccess::WriteOnly,
format: wgpu::TextureFormat::Rgba8Unorm,
view_dimension: wgpu::TextureViewDimension::D2,
},
count: None,
},
],
});
let dda_pl = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("roxlap-gpu chunk_dda.layout"),
bind_group_layouts: &[Some(&bgl_dda)],
immediate_size: 0,
});
let pipeline_dda = self
.device
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("roxlap-gpu chunk_dda.pipeline"),
layout: Some(&dda_pl),
module: &dda_shader,
entry_point: Some("render_chunk"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
});
let blit_shader = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("blit.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/blit.wgsl").into()),
});
let bgl_blit = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("roxlap-gpu chunk_dda.blit_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
});
let blit_pl = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("roxlap-gpu chunk_dda.blit_layout"),
bind_group_layouts: &[Some(&bgl_blit)],
immediate_size: 0,
});
let pipeline_blit = self
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("roxlap-gpu chunk_dda.blit_pipeline"),
layout: Some(&blit_pl),
vertex: wgpu::VertexState {
module: &blit_shader,
entry_point: Some("vs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &blit_shader,
entry_point: Some("fs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("roxlap-gpu chunk_dda.blit_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let blit_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("roxlap-gpu chunk_dda.blit_bg"),
layout: &bgl_blit,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&storage_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
ChunkDdaResources {
storage_size: (width, height),
storage_view,
uniform_buf,
bgl_dda,
pipeline_dda,
blit_bg,
pipeline_blit,
_sampler: sampler,
}
}
pub fn render_grid(&mut self, grid: &GpuGridResident, camera: &Camera, max_outer_steps: u32) {
let Some(surf_tex) = self.acquire_frame() else {
return;
};
let surf_view = surf_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let surface_w = self.surface_config.width;
let surface_h = self.surface_config.height;
let surface_format = self.surface_config.format;
let needs_build = match &self.grid_dda {
Some(r) => r.storage_size != (surface_w, surface_h),
None => true,
};
if needs_build {
self.grid_dda = Some(self.build_grid_dda(surface_w, surface_h, surface_format));
}
let dda = self.grid_dda.as_ref().expect("just built");
let uniform = GridDdaUniform {
camera_pos: camera.position,
_pad0: 0.0,
camera_right: camera.right,
_pad1: 0.0,
camera_down: camera.down,
_pad2: 0.0,
camera_forward: camera.forward,
fov_y_rad: camera.fov_y_rad,
screen_size: [surface_w, surface_h],
vsid: grid.vsid,
max_outer_steps,
chunks_dims: grid.chunks_dims,
_pad3: 0,
origin_chunk: grid.origin_chunk,
_pad4: 0,
};
self.queue
.write_buffer(&dda.uniform_buf, 0, bytemuck::bytes_of(&uniform));
let dda_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("roxlap-gpu grid_dda.bg"),
layout: &dda.bgl_dda,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: dda.uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: grid.occupancy.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: grid.color_offsets.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: grid.colors.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: grid.chunk_colors_base.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: grid.chunk_occupancy.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::TextureView(&dda.storage_view),
},
],
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("roxlap-gpu grid encoder"),
});
{
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("roxlap-gpu grid_dda compute"),
timestamp_writes: None,
});
cpass.set_pipeline(&dda.pipeline_dda);
cpass.set_bind_group(0, &dda_bg, &[]);
cpass.dispatch_workgroups(surface_w.div_ceil(8), surface_h.div_ceil(8), 1);
}
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("roxlap-gpu grid_dda blit"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &surf_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rpass.set_pipeline(&dda.pipeline_blit);
rpass.set_bind_group(0, &dda.blit_bg, &[]);
rpass.draw(0..3, 0..1);
}
self.queue.submit(std::iter::once(encoder.finish()));
surf_tex.present();
self.frame_count = self.frame_count.wrapping_add(1);
}
fn build_grid_dda(
&self,
width: u32,
height: u32,
surface_format: wgpu::TextureFormat,
) -> GridDdaResources {
let storage_tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("roxlap-gpu grid_dda.storage"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let storage_view = storage_tex.create_view(&wgpu::TextureViewDescriptor::default());
let uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu grid_dda.uniform"),
size: std::mem::size_of::<GridDdaUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let dda_shader = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("grid_dda.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/grid_dda.wgsl").into()),
});
let bgl_dda = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("roxlap-gpu grid_dda.bgl"),
entries: &[
bgl_uniform_entry(0),
bgl_storage_entry(1, true),
bgl_storage_entry(2, true),
bgl_storage_entry(3, true),
bgl_storage_entry(4, true),
bgl_storage_entry(5, true),
wgpu::BindGroupLayoutEntry {
binding: 6,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::StorageTexture {
access: wgpu::StorageTextureAccess::WriteOnly,
format: wgpu::TextureFormat::Rgba8Unorm,
view_dimension: wgpu::TextureViewDimension::D2,
},
count: None,
},
],
});
let dda_pl = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("roxlap-gpu grid_dda.layout"),
bind_group_layouts: &[Some(&bgl_dda)],
immediate_size: 0,
});
let pipeline_dda = self
.device
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("roxlap-gpu grid_dda.pipeline"),
layout: Some(&dda_pl),
module: &dda_shader,
entry_point: Some("render_grid"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
});
let blit_shader = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("blit.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/blit.wgsl").into()),
});
let bgl_blit = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("roxlap-gpu grid_dda.blit_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
});
let blit_pl = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("roxlap-gpu grid_dda.blit_layout"),
bind_group_layouts: &[Some(&bgl_blit)],
immediate_size: 0,
});
let pipeline_blit = self
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("roxlap-gpu grid_dda.blit_pipeline"),
layout: Some(&blit_pl),
vertex: wgpu::VertexState {
module: &blit_shader,
entry_point: Some("vs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &blit_shader,
entry_point: Some("fs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("roxlap-gpu grid_dda.blit_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let blit_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("roxlap-gpu grid_dda.blit_bg"),
layout: &bgl_blit,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&storage_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
GridDdaResources {
storage_size: (width, height),
storage_view,
uniform_buf,
bgl_dda,
pipeline_dda,
blit_bg,
pipeline_blit,
_sampler: sampler,
}
}
pub fn render_scene(
&mut self,
scene: &GpuSceneResident,
cameras: &[Camera],
sprite_camera: &Camera,
fov_y_rad: f32,
max_outer_steps: u32,
) {
assert_eq!(
cameras.len(),
scene.grid_count as usize,
"render_scene: {} cameras supplied, scene has {} grids",
cameras.len(),
scene.grid_count,
);
assert!(
scene.grid_count as usize <= SCENE_MAX_GRIDS,
"render_scene: scene has {} grids, shader supports {}",
scene.grid_count,
SCENE_MAX_GRIDS,
);
self.last_fov_y_rad = fov_y_rad;
self.pending_frame = None;
let Some(surf_tex) = self.acquire_frame() else {
return;
};
let surf_view = surf_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let surface_w = self.surface_config.width;
let surface_h = self.surface_config.height;
let surface_format = self.surface_config.format;
let needs_build = match &self.scene_dda {
Some(r) => r.storage_size != (surface_w, surface_h),
None => true,
};
if needs_build {
self.scene_dda = Some(self.build_scene_dda(surface_w, surface_h, surface_format));
}
if self.sprite_registry.is_some() && self.sprite_model_dda.is_none() {
self.sprite_model_dda = Some(self.build_sprite_model_dda());
}
let sprite_pass: Option<(u32, u32)> = if let Some(reg) = self.sprite_registry.as_mut() {
if reg.instance_capacity > 0 {
let cam = sprite_camera;
#[allow(clippy::cast_precision_loss)]
let aspect = surface_w as f32 / surface_h as f32;
let half_h = (fov_y_rad * 0.5).tan();
let frustum = sprite_model::ViewFrustum {
pos: cam.position,
right: cam.right,
down: cam.down,
forward: cam.forward,
half_w: half_h * aspect,
half_h,
far: 1.0e9,
};
let (visible, tiles_x, _tiles_y) = reg.cull_bin_upload(
&self.device,
&self.queue,
&frustum,
surface_w,
surface_h,
SPRITE_TILE_SIZE,
self.sprite_lod_px,
);
(visible > 0).then_some((visible, tiles_x))
} else {
None
}
} else {
None
};
let dda = self.scene_dda.as_ref().expect("just built");
let mut cam_array = [SceneDdaPerGridCamera::zeroed(); SCENE_MAX_GRIDS];
for (i, cam) in cameras.iter().enumerate() {
cam_array[i] = SceneDdaPerGridCamera {
pos: cam.position,
_pad0: 0.0,
right: cam.right,
_pad1: 0.0,
down: cam.down,
_pad2: 0.0,
forward: cam.forward,
_pad3: 0.0,
};
}
let uniform = SceneDdaUniform {
fov_y_rad,
grid_count: scene.grid_count,
max_outer_steps,
_pad0: 0,
screen_size: [surface_w, surface_h],
_pad1: [0; 2],
cameras: cam_array,
fog_color: [
self.fog_color[0],
self.fog_color[1],
self.fog_color[2],
self.fog_near,
],
fog_far: self.fog_far,
write_depth: 1,
occ_page_words: scene.occupancy_page_words,
occ_num_pages: scene.occupancy_num_pages,
mip_scan_dist: self.scene_mip_scan_dist,
_pad2: 0,
_pad3: 0,
_pad4: 0,
sky_cam: SceneDdaPerGridCamera::from_camera(sprite_camera),
side_shades0: self.scene_side_shades[0],
side_shades1: self.scene_side_shades[1],
};
self.queue
.write_buffer(&dda.uniform_buf, 0, bytemuck::bytes_of(&uniform));
let dda_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("roxlap-gpu scene_dda.bg"),
layout: &dda.bgl_dda,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: dda.uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: scene.occupancy_pages[0].as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: scene.all_color_offsets.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: scene.all_colors.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: scene.all_chunk_colors_base.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: scene.all_chunk_occupancy.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 6,
resource: scene.grid_static_meta.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 7,
resource: scene.all_slot_chunk_idx.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 8,
resource: dda.framebuffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 9,
resource: wgpu::BindingResource::TextureView(&self.sky_view),
},
wgpu::BindGroupEntry {
binding: 10,
resource: wgpu::BindingResource::Sampler(&self.sky_sampler),
},
wgpu::BindGroupEntry {
binding: 11,
resource: dda.depth_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 12,
resource: scene.occupancy_pages[1].as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 13,
resource: scene.occupancy_pages[2].as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 14,
resource: scene.occupancy_pages[3].as_entire_binding(),
},
],
});
let sprite_model_bg = match (&self.sprite_model_dda, &self.sprite_registry, sprite_pass) {
(Some(smd), Some(reg), Some((visible, tiles_x))) => {
let cam = sprite_camera;
let uni = SpriteModelUniform {
cam_pos: cam.position,
_p0: 0.0,
cam_right: cam.right,
_p1: 0.0,
cam_down: cam.down,
_p2: 0.0,
cam_forward: cam.forward,
_p3: 0.0,
fog_color: [
self.fog_color[0],
self.fog_color[1],
self.fog_color[2],
self.fog_near,
],
screen_size: [surface_w, surface_h],
instance_count: visible,
fog_far: self.fog_far,
fov_y_rad,
tiles_x,
tile_size: SPRITE_TILE_SIZE,
_p6: 0.0,
};
self.queue
.write_buffer(&smd.uniform_buf, 0, bytemuck::bytes_of(&uni));
Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("roxlap-gpu sprite_model_dda.bg"),
layout: &smd.bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: smd.uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: reg.occupancy.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: reg.colors.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: reg.color_offsets.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: reg.model_meta.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: reg.instances.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 6,
resource: dda.depth_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 7,
resource: dda.framebuffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 8,
resource: reg.tile_ranges.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 9,
resource: reg.tile_instances.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 10,
resource: reg.dirs.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 11,
resource: reg.colmul.as_entire_binding(),
},
],
}))
}
_ => None,
};
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("roxlap-gpu scene encoder"),
});
{
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("roxlap-gpu scene_dda compute"),
timestamp_writes: None,
});
cpass.set_pipeline(&dda.pipeline_dda);
cpass.set_bind_group(0, &dda_bg, &[]);
cpass.dispatch_workgroups(surface_w.div_ceil(8), surface_h.div_ceil(8), 1);
}
if let (Some(smd), Some(bg)) = (&self.sprite_model_dda, &sprite_model_bg) {
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("roxlap-gpu sprite_model_dda"),
timestamp_writes: None,
});
cpass.set_pipeline(&smd.pipeline);
cpass.set_bind_group(0, bg, &[]);
cpass.dispatch_workgroups(surface_w.div_ceil(8), surface_h.div_ceil(8), 1);
}
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("roxlap-gpu scene_dda blit"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &surf_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rpass.set_pipeline(&dda.pipeline_blit);
rpass.set_bind_group(0, &dda.blit_bg, &[]);
rpass.draw(0..3, 0..1);
}
self.queue.submit(std::iter::once(encoder.finish()));
self.pending_frame = Some((surf_tex, surf_view));
self.frame_count = self.frame_count.wrapping_add(1);
}
pub fn render_clear_deferred(&mut self) {
self.pending_frame = None;
let Some(surf_tex) = self.acquire_frame() else {
return;
};
let view = surf_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let [r, g, b] = self.clear_colour;
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("roxlap-gpu clear (deferred)"),
});
{
let _rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("roxlap-gpu clear (deferred)"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { r, g, b, a: 1.0 }),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
}
self.queue.submit(std::iter::once(encoder.finish()));
self.pending_frame = Some((surf_tex, view));
}
pub fn present(&mut self) {
if let Some((surf_tex, _view)) = self.pending_frame.take() {
surf_tex.present();
}
}
pub fn draw_lines_deferred(&mut self, cam: &GpuLineCamera, lines: &[GpuLine]) {
if self.pending_frame.is_none() || lines.is_empty() {
return;
}
let (w, h) = (self.surface_config.width, self.surface_config.height);
let fov = self.last_fov_y_rad;
if w == 0 || h == 0 || fov <= 0.0 {
return; }
let verts = build_line_vertices(cam, lines, w, h, fov);
if verts.is_empty() {
return;
}
self.ensure_line_resources();
let res = self.line_resources.as_ref().expect("just built");
let no_depth = u32::from(self.scene_dda.is_none());
let params = LineParams {
screen_w: w,
screen_h: h,
depth_bias: LINE_DEPTH_BIAS,
no_depth,
};
self.queue
.write_buffer(&res.uniform_buf, 0, bytemuck::bytes_of(¶ms));
let depth_resource = match &self.scene_dda {
Some(dda) => dda.depth_buffer.as_entire_binding(),
None => res.dummy_depth.as_entire_binding(),
};
let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("roxlap-gpu line.bg"),
layout: &res.bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: res.uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: depth_resource,
},
],
});
let needed = std::mem::size_of_val(verts.as_slice()) as u64;
if self.line_vbuf_cap < needed {
let cap = needed.next_power_of_two().max(4096);
self.line_vbuf = Some(self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu line.vbuf"),
size: cap,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}));
self.line_vbuf_cap = cap;
}
let vbuf = self.line_vbuf.as_ref().expect("ensured above");
self.queue
.write_buffer(vbuf, 0, bytemuck::cast_slice(&verts));
let view = &self.pending_frame.as_ref().expect("checked above").1;
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("roxlap-gpu lines"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("roxlap-gpu line paint"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&res.pipeline);
pass.set_bind_group(0, &bg, &[]);
pass.set_vertex_buffer(0, vbuf.slice(..));
pass.draw(0..verts.len() as u32, 0..1);
}
self.queue.submit(std::iter::once(encoder.finish()));
}
fn ensure_line_resources(&mut self) {
if self.line_resources.is_some() {
return;
}
let shader = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("line.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/line.wgsl").into()),
});
let bgl = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("roxlap-gpu line.bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let layout = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("roxlap-gpu line.layout"),
bind_group_layouts: &[Some(&bgl)],
immediate_size: 0,
});
let pipeline = self
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("roxlap-gpu line.pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<LineVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![
0 => Float32x2, 1 => Float32, 2 => Float32, 3 => Float32x4, ],
}],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: self.surface_config.format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
cull_mode: None,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu line.uniform"),
size: std::mem::size_of::<LineParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let dummy_depth = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu line.dummy_depth"),
size: 4,
usage: wgpu::BufferUsages::STORAGE,
mapped_at_creation: false,
});
self.line_resources = Some(LineResources {
pipeline,
bgl,
uniform_buf,
dummy_depth,
});
}
#[cfg(feature = "hud")]
pub fn paint_egui(
&mut self,
jobs: &[egui::ClippedPrimitive],
textures: &egui::TexturesDelta,
pixels_per_point: f32,
) {
let Some((surf_tex, surf_view)) = self.pending_frame.take() else {
return;
};
let format = self.surface_config.format;
let egui_rend = self.egui_renderer.get_or_insert_with(|| {
egui_wgpu::Renderer::new(
&self.device,
format,
egui_wgpu::RendererOptions {
msaa_samples: 1,
depth_stencil_format: None,
dithering: false,
..Default::default()
},
)
});
let screen = egui_wgpu::ScreenDescriptor {
size_in_pixels: [self.surface_config.width, self.surface_config.height],
pixels_per_point,
};
for (id, delta) in &textures.set {
egui_rend.update_texture(&self.device, &self.queue, *id, delta);
}
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("roxlap-gpu egui"),
});
let user_bufs =
egui_rend.update_buffers(&self.device, &self.queue, &mut encoder, jobs, &screen);
{
let mut pass = encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("roxlap-gpu egui paint"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &surf_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
})
.forget_lifetime();
egui_rend.render(&mut pass, jobs, &screen);
}
for id in &textures.free {
egui_rend.free_texture(id);
}
self.queue.submit(
user_bufs
.into_iter()
.chain(std::iter::once(encoder.finish())),
);
surf_tex.present();
}
fn build_scene_dda(
&self,
width: u32,
height: u32,
surface_format: wgpu::TextureFormat,
) -> SceneDdaResources {
let framebuffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu scene_dda.framebuffer"),
size: u64::from(width) * u64::from(height) * 4,
usage: wgpu::BufferUsages::STORAGE,
mapped_at_creation: false,
});
let blit_dims = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu scene_dda.blit_dims"),
size: 8,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue
.write_buffer(&blit_dims, 0, bytemuck::bytes_of(&[width, height]));
let uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu scene_dda.uniform"),
size: std::mem::size_of::<SceneDdaUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let depth_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu scene_dda.depth"),
size: u64::from(width) * u64::from(height) * 4,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let depth_readback = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu scene_dda.depth_readback"),
size: u64::from(width) * u64::from(height) * 4,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let dda_shader = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("scene_dda.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/scene_dda.wgsl").into()),
});
let bgl_dda = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("roxlap-gpu scene_dda.bgl"),
entries: &[
bgl_uniform_entry(0),
bgl_storage_entry(1, true),
bgl_storage_entry(2, true),
bgl_storage_entry(3, true),
bgl_storage_entry(4, true),
bgl_storage_entry(5, true),
bgl_storage_entry(6, true),
bgl_storage_entry(7, true),
bgl_storage_entry(8, false),
wgpu::BindGroupLayoutEntry {
binding: 9,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 10,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
bgl_storage_entry(11, false),
bgl_storage_entry(12, true),
bgl_storage_entry(13, true),
bgl_storage_entry(14, true),
],
});
let dda_pl = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("roxlap-gpu scene_dda.layout"),
bind_group_layouts: &[Some(&bgl_dda)],
immediate_size: 0,
});
let pipeline_dda = self
.device
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("roxlap-gpu scene_dda.pipeline"),
layout: Some(&dda_pl),
module: &dda_shader,
entry_point: Some("render_scene"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
});
let blit_shader = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("scene_blit.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/scene_blit.wgsl").into()),
});
let bgl_blit = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("roxlap-gpu scene_dda.blit_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let blit_pl = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("roxlap-gpu scene_dda.blit_layout"),
bind_group_layouts: &[Some(&bgl_blit)],
immediate_size: 0,
});
let pipeline_blit = self
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("roxlap-gpu scene_dda.blit_pipeline"),
layout: Some(&blit_pl),
vertex: wgpu::VertexState {
module: &blit_shader,
entry_point: Some("vs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &blit_shader,
entry_point: Some("fs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let blit_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("roxlap-gpu scene_dda.blit_bg"),
layout: &bgl_blit,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: framebuffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: blit_dims.as_entire_binding(),
},
],
});
SceneDdaResources {
storage_size: (width, height),
framebuffer,
uniform_buf,
bgl_dda,
pipeline_dda,
blit_bg,
pipeline_blit,
depth_buffer,
depth_readback,
}
}
#[must_use]
pub fn read_depth_pixel(&self, x: u32, y: u32) -> Option<f32> {
let dda = self.scene_dda.as_ref()?;
let (w, h) = dda.storage_size;
if x >= w || y >= h {
return None;
}
let mut enc = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("roxlap-gpu depth readback"),
});
let size = u64::from(w) * u64::from(h) * 4;
enc.copy_buffer_to_buffer(&dda.depth_buffer, 0, &dda.depth_readback, 0, size);
self.queue.submit(std::iter::once(enc.finish()));
let slice = dda.depth_readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
self.device.poll(wgpu::PollType::wait_indefinitely()).ok();
rx.recv().ok()?.ok()?;
let t = {
let data = slice.get_mapped_range();
let idx = ((y * w + x) * 4) as usize;
let bytes: [u8; 4] = data[idx..idx + 4].try_into().ok()?;
f32::from_le_bytes(bytes)
};
dda.depth_readback.unmap();
if !t.is_finite() || t >= 1.0e29 {
return None;
}
Some(t)
}
#[must_use]
pub fn pixel_ray(
&self,
right: [f64; 3],
down: [f64; 3],
forward: [f64; 3],
x: f64,
y: f64,
) -> Option<[f64; 3]> {
let dda = self.scene_dda.as_ref()?;
let (w, h) = dda.storage_size;
if w == 0 || h == 0 || self.last_fov_y_rad <= 0.0 {
return None;
}
Some(pinhole_pixel_ray(
right,
down,
forward,
x,
y,
f64::from(w),
f64::from(h),
f64::from(self.last_fov_y_rad),
))
}
pub fn set_sprite_instances(
&mut self,
registry: &sprite_model::SpriteModelRegistry,
instances: &[sprite_model::SpriteInstance],
) {
if instances.is_empty() {
self.sprite_registry = None;
return;
}
self.sprite_registry = Some(sprite_model::SpriteRegistryResident::upload(
&self.device,
registry,
instances,
));
}
pub fn update_sprite_instance_transforms(
&mut self,
instances: &[sprite_model::SpriteInstance],
) {
if let Some(reg) = self.sprite_registry.as_mut() {
reg.update_transforms(instances);
}
}
pub fn set_sprite_instance_colmul(&mut self, tables: &[[u64; 256]]) {
if let Some(reg) = self.sprite_registry.as_mut() {
reg.set_instance_colmul(tables);
}
}
pub fn set_sprite_lod_px(&mut self, px: f32) {
self.sprite_lod_px = px.max(0.25);
}
pub fn set_scene_mip_scan_dist(&mut self, dist: f32) {
self.scene_mip_scan_dist = dist.max(0.0);
}
pub fn set_scene_side_shades(&mut self, s: [i8; 6]) {
let v = |i: usize| i32::from(s[i] as u8);
self.scene_side_shades = [[v(0), v(1), v(2), v(3)], [v(4), v(5), 0, 0]];
}
fn build_sprite_model_dda(&self) -> SpriteModelDdaResources {
let shader = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("sprite_model_dda.wgsl"),
source: wgpu::ShaderSource::Wgsl(
include_str!("../shaders/sprite_model_dda.wgsl").into(),
),
});
let bgl = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("roxlap-gpu sprite_model_dda.bgl"),
entries: &[
bgl_uniform_entry(0),
bgl_storage_entry(1, true), bgl_storage_entry(2, true), bgl_storage_entry(3, true), bgl_storage_entry(4, true), bgl_storage_entry(5, true), bgl_storage_entry(6, true), bgl_storage_entry(7, false), bgl_storage_entry(8, true), bgl_storage_entry(9, true), bgl_storage_entry(10, true), bgl_storage_entry(11, true), ],
});
let pl = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("roxlap-gpu sprite_model_dda.layout"),
bind_group_layouts: &[Some(&bgl)],
immediate_size: 0,
});
let pipeline = self
.device
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("roxlap-gpu sprite_model_dda.pipeline"),
layout: Some(&pl),
module: &shader,
entry_point: Some("march"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
});
let uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu sprite_model_dda.uniform"),
size: std::mem::size_of::<SpriteModelUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
SpriteModelDdaResources {
bgl,
pipeline,
uniform_buf,
}
}
}
pub struct HeadlessSceneRenderer {
width: u32,
height: u32,
framebuffer: wgpu::Buffer,
depth_buffer: wgpu::Buffer,
uniform_buf: wgpu::Buffer,
_sky_texture: wgpu::Texture,
sky_view: wgpu::TextureView,
sky_sampler: wgpu::Sampler,
bgl: wgpu::BindGroupLayout,
pipeline: wgpu::ComputePipeline,
readback: wgpu::Buffer,
side_shades: [[i32; 4]; 2],
}
impl HeadlessSceneRenderer {
#[must_use]
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, width: u32, height: u32) -> Self {
let framebuffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu headless.framebuffer"),
size: u64::from(width) * u64::from(height) * 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu headless.uniform"),
size: std::mem::size_of::<SceneDdaUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let depth_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu headless.depth"),
size: u64::from(width) * u64::from(height) * 4,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let default_sky_pixel = [120u8, 150, 220, 255];
let (sky_texture, sky_view) = create_sky_texture(device, 1, 1, &default_sky_pixel);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &sky_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&default_sky_pixel,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4),
rows_per_image: Some(1),
},
wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
);
let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("roxlap-gpu headless.sky_sampler"),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("scene_dda.wgsl (headless)"),
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/scene_dda.wgsl").into()),
});
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("roxlap-gpu headless.bgl"),
entries: &[
bgl_uniform_entry(0),
bgl_storage_entry(1, true),
bgl_storage_entry(2, true),
bgl_storage_entry(3, true),
bgl_storage_entry(4, true),
bgl_storage_entry(5, true),
bgl_storage_entry(6, true),
bgl_storage_entry(7, true),
bgl_storage_entry(8, false),
wgpu::BindGroupLayoutEntry {
binding: 9,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 10,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
bgl_storage_entry(11, false),
bgl_storage_entry(12, true),
bgl_storage_entry(13, true),
bgl_storage_entry(14, true),
],
});
let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("roxlap-gpu headless.layout"),
bind_group_layouts: &[Some(&bgl)],
immediate_size: 0,
});
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("roxlap-gpu headless.pipeline"),
layout: Some(&pl),
module: &shader,
entry_point: Some("render_scene"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
});
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("roxlap-gpu headless.readback"),
size: u64::from(width) * u64::from(height) * 4,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
Self {
width,
height,
framebuffer,
depth_buffer,
uniform_buf,
_sky_texture: sky_texture,
sky_view,
sky_sampler,
bgl,
pipeline,
readback,
side_shades: [[0; 4]; 2],
}
}
pub fn set_side_shades(&mut self, s: [i8; 6]) {
let v = |i: usize| i32::from(s[i] as u8);
self.side_shades = [[v(0), v(1), v(2), v(3)], [v(4), v(5), 0, 0]];
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn render(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
scene: &GpuSceneResident,
cameras: &[Camera],
fov_y_rad: f32,
max_outer_steps: u32,
mip_scan_dist: f32,
) -> Vec<u32> {
assert_eq!(
cameras.len(),
scene.grid_count as usize,
"headless render: {} cameras for {} grids",
cameras.len(),
scene.grid_count,
);
let mut cam_array = [SceneDdaPerGridCamera::zeroed(); SCENE_MAX_GRIDS];
for (i, cam) in cameras.iter().enumerate() {
cam_array[i] = SceneDdaPerGridCamera {
pos: cam.position,
_pad0: 0.0,
right: cam.right,
_pad1: 0.0,
down: cam.down,
_pad2: 0.0,
forward: cam.forward,
_pad3: 0.0,
};
}
let uniform = SceneDdaUniform {
fov_y_rad,
grid_count: scene.grid_count,
max_outer_steps,
_pad0: 0,
screen_size: [self.width, self.height],
_pad1: [0; 2],
cameras: cam_array,
fog_color: [0.0, 0.0, 0.0, 1.0e29],
fog_far: 1.0e30,
write_depth: 0,
occ_page_words: scene.occupancy_page_words,
occ_num_pages: scene.occupancy_num_pages,
mip_scan_dist,
_pad2: 0,
_pad3: 0,
_pad4: 0,
sky_cam: SceneDdaPerGridCamera::from_camera(&cameras.first().copied().unwrap_or(
Camera {
position: [0.0; 3],
right: [1.0, 0.0, 0.0],
down: [0.0, 0.0, 1.0],
forward: [0.0, 1.0, 0.0],
fov_y_rad,
},
)),
side_shades0: self.side_shades[0],
side_shades1: self.side_shades[1],
};
queue.write_buffer(&self.uniform_buf, 0, bytemuck::bytes_of(&uniform));
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("roxlap-gpu headless.bg"),
layout: &self.bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.uniform_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: scene.occupancy_pages[0].as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: scene.all_color_offsets.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: scene.all_colors.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: scene.all_chunk_colors_base.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: scene.all_chunk_occupancy.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 6,
resource: scene.grid_static_meta.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 7,
resource: scene.all_slot_chunk_idx.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 8,
resource: self.framebuffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 9,
resource: wgpu::BindingResource::TextureView(&self.sky_view),
},
wgpu::BindGroupEntry {
binding: 10,
resource: wgpu::BindingResource::Sampler(&self.sky_sampler),
},
wgpu::BindGroupEntry {
binding: 11,
resource: self.depth_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 12,
resource: scene.occupancy_pages[1].as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 13,
resource: scene.occupancy_pages[2].as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 14,
resource: scene.occupancy_pages[3].as_entire_binding(),
},
],
});
let mut enc =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("roxlap-gpu headless.pass"),
timestamp_writes: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &bg, &[]);
pass.dispatch_workgroups(self.width.div_ceil(8), self.height.div_ceil(8), 1);
}
enc.copy_buffer_to_buffer(
&self.framebuffer,
0,
&self.readback,
0,
u64::from(self.width) * u64::from(self.height) * 4,
);
queue.submit(Some(enc.finish()));
let slice = self.readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
device.poll(wgpu::PollType::wait_indefinitely()).ok();
rx.recv().expect("map_async channel").expect("map_async");
let data = slice.get_mapped_range();
let out: Vec<u32> = data
.chunks_exact(4)
.map(|px| u32::from_le_bytes([px[0], px[1], px[2], px[3]]))
.collect();
drop(data);
self.readback.unmap();
out
}
}
fn bgl_uniform_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}
fn bgl_storage_entry(binding: u32, read_only: bool) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}
fn create_sky_texture(
device: &wgpu::Device,
width: u32,
height: u32,
_initial_pixels: &[u8],
) -> (wgpu::Texture, wgpu::TextureView) {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("roxlap-gpu sky_texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
(tex, view)
}
pub(crate) fn pick_required_limits(adapter_limits: &wgpu::Limits) -> wgpu::Limits {
wgpu::Limits {
max_storage_buffer_binding_size: adapter_limits.max_storage_buffer_binding_size,
max_buffer_size: adapter_limits.max_buffer_size,
max_storage_buffers_per_shader_stage: adapter_limits
.max_storage_buffers_per_shader_stage
.min(16),
..wgpu::Limits::default()
}
}
fn pick_present_mode(modes: &[wgpu::PresentMode]) -> wgpu::PresentMode {
for &m in &[wgpu::PresentMode::Mailbox, wgpu::PresentMode::Immediate] {
if modes.contains(&m) {
return m;
}
}
wgpu::PresentMode::Fifo
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn pinhole_pixel_ray(
right: [f64; 3],
down: [f64; 3],
forward: [f64; 3],
x: f64,
y: f64,
w: f64,
h: f64,
fov_y_rad: f64,
) -> [f64; 3] {
let half_h = (fov_y_rad * 0.5).tan();
let half_w = half_h * (w / h);
let ndc_x = (x + 0.5) / w * 2.0 - 1.0;
let ndc_y_top = 1.0 - (y + 0.5) / h * 2.0;
let (kx, ky) = (ndc_x * half_w, ndc_y_top * half_h);
[
forward[0] + kx * right[0] - ky * down[0],
forward[1] + kx * right[1] - ky * down[1],
forward[2] + kx * right[2] - ky * down[2],
]
}
#[cfg(test)]
mod pixel_ray_tests {
use super::pinhole_pixel_ray;
const RIGHT: [f64; 3] = [1.0, 0.0, 0.0];
const DOWN: [f64; 3] = [0.0, 1.0, 0.0];
const FWD: [f64; 3] = [0.0, 0.0, 1.0];
#[test]
fn centre_pixel_is_forward() {
let d = pinhole_pixel_ray(
RIGHT,
DOWN,
FWD,
639.5,
359.5,
1280.0,
720.0,
60_f64.to_radians(),
);
assert!(
d[0].abs() < 1e-9 && d[1].abs() < 1e-9,
"centre ≈ forward, got {d:?}"
);
assert!((d[2] - 1.0).abs() < 1e-9);
}
#[test]
fn right_edge_tilts_by_half_w() {
let fov = 60_f64.to_radians();
let d = pinhole_pixel_ray(RIGHT, DOWN, FWD, 1279.5, 359.5, 1280.0, 720.0, fov);
let half_w = (fov * 0.5).tan() * (1280.0 / 720.0);
assert!((d[0] - half_w).abs() < 1e-6, "x={}, half_w={half_w}", d[0]);
assert!(d[0] > 0.0, "right edge tilts +right");
}
#[test]
fn wgsl_shaders_validate() {
let shaders: &[(&str, &str)] = &[
(
"sprite_model_dda.wgsl",
include_str!("../shaders/sprite_model_dda.wgsl"),
),
("scene_dda.wgsl", include_str!("../shaders/scene_dda.wgsl")),
("blit.wgsl", include_str!("../shaders/blit.wgsl")),
("chunk_dda.wgsl", include_str!("../shaders/chunk_dda.wgsl")),
("grid_dda.wgsl", include_str!("../shaders/grid_dda.wgsl")),
(
"scene_blit.wgsl",
include_str!("../shaders/scene_blit.wgsl"),
),
("line.wgsl", include_str!("../shaders/line.wgsl")),
];
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
);
for (name, src) in shaders {
let module = naga::front::wgsl::parse_str(src).unwrap_or_else(|e| {
panic!("{name}: WGSL parse failed:\n{}", e.emit_to_string(src))
});
validator
.validate(&module)
.unwrap_or_else(|e| panic!("{name}: WGSL validation failed: {e:?}"));
}
}
}