feat: add render graph driven renderer architecture

Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab

Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-07-27 02:53:44 +00:00
co-authored by heaust
parent 8a8369706b
commit d4e8634f67
290 changed files with 48804 additions and 1995 deletions
+2 -1
View File
@@ -42,11 +42,12 @@ bytemuck = { workspace = true }
cgmath = { workspace = true }
raw-window-handle = { workspace = true }
wgpu = { workspace = true }
reqwest = { workspace = true }
thiserror = { workspace = true }
ultraviolet = { workspace = true }
futures = { workspace = true }
gltf = { workspace = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
+31 -16
View File
@@ -3,11 +3,7 @@ use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
#[cfg(target_arch = "wasm32")]
use web_sys::AddEventListenerOptions;
#[cfg(target_arch = "wasm32")]
use wgpu::Error;
use crate::command_ring::CommandRing;
use crate::message::WindowEvent;
#[cfg(target_arch = "wasm32")]
use crate::platform::web;
@@ -15,6 +11,8 @@ use crate::platform::web;
use crate::platform::web::worker::MainWorker;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_futures::spawn_local;
#[cfg(target_arch = "wasm32")]
use web_sys::AddEventListenerOptions;
/// Helper struct to store event listener closures
#[cfg(target_arch = "wasm32")]
@@ -41,16 +39,20 @@ impl EventListeners {
/// Setup default window event listeners that forward events to the worker thread
#[cfg(target_arch = "wasm32")]
pub fn setup_event_listeners(worker_chan: &Sender<WindowEvent>) -> Result<EventListeners, JsValue> {
pub fn setup_event_listeners(
worker_chan: &Sender<WindowEvent>,
canvas: &web_sys::HtmlCanvasElement,
) -> Result<EventListeners, JsValue> {
let window = web_sys::window().unwrap();
let resize_worker_chan = worker_chan.clone();
let resize_canvas = canvas.clone();
let resize_listener: Closure<dyn FnMut()> = Closure::new(move || {
use crate::message::ResizeMessage;
let window = web_sys::window().unwrap();
let width = window.inner_width().ok().unwrap().as_f64().unwrap();
let height = window.inner_height().ok().unwrap().as_f64().unwrap();
let width = f64::from(resize_canvas.client_width().max(1));
let height = f64::from(resize_canvas.client_height().max(1));
resize_worker_chan
.send(WindowEvent::Resize(ResizeMessage {
@@ -155,29 +157,41 @@ pub struct WebAppRuntime {
worker: MainWorker,
worker_chan: Sender<WindowEvent>,
_event_listeners: EventListeners,
ring: Box<CommandRing>,
}
#[cfg(target_arch = "wasm32")]
impl WebAppRuntime {
/// Initialize the web worker, canvas ownership, and event listeners.
pub fn new<T: crate::renderer::scene::Scene + 'static>(worker_name: &str, canvas_selector: &str) -> Result<Self, JsValue> {
pub fn new<T: crate::renderer::scene::Scene + 'static>(
worker_name: &str,
canvas_selector: &str,
) -> Result<Self, JsValue> {
let (sender, receiver) = mpsc::channel::<WindowEvent>();
let canvas = web::get_canvas_element(canvas_selector);
let worker = MainWorker::spawn(worker_name, 1, move || {
let window = web_sys::window().unwrap();
let dpr = window.device_pixel_ratio();
canvas.set_width((canvas.client_width() as f64 * dpr).round() as u32);
canvas.set_height((canvas.client_height() as f64 * dpr).round() as u32);
let ring = CommandRing::new();
let ring_ptr = ring.ptr();
let worker = MainWorker::spawn(worker_name, 1, ring_ptr, move || {
spawn_local(async move {
MainWorker::run_render_loop::<T>(receiver).await;
let ring = unsafe { &*(ring_ptr as *const CommandRing) };
MainWorker::run_render_loop::<T>(receiver, ring).await;
});
})?;
worker.transfer_ownership(&canvas);
let event_listeners = setup_event_listeners(&sender)?;
let event_listeners = setup_event_listeners(&sender, &canvas)?;
Ok(Self {
worker,
worker_chan: sender,
_event_listeners: event_listeners,
ring,
})
}
@@ -190,6 +204,9 @@ impl WebAppRuntime {
pub fn worker(&self) -> &MainWorker {
&self.worker
}
pub fn ring_ptr(&self) -> u32 {
self.ring.ptr()
}
}
/// Trait for applications that rely on the renderer's default WASM setup.
@@ -212,10 +229,8 @@ pub trait WebApp {
/// Perform the default WASM initialization routine.
fn setup_runtime() -> Result<WebAppRuntime, JsValue> {
let mut runtime = WebAppRuntime::new::<Self::Scene>(
Self::worker_name(),
Self::canvas_selector(),
)?;
let mut runtime =
WebAppRuntime::new::<Self::Scene>(Self::worker_name(), Self::canvas_selector())?;
Self::on_runtime_initialized(&mut runtime);
Ok(runtime)
}
+153
View File
@@ -0,0 +1,153 @@
//! Versioned, fixed-slot SPSC command transport in shared WebAssembly memory.
use std::sync::atomic::{AtomicU32, Ordering};
pub const MAGIC: u32 = u32::from_le_bytes(*b"YAWN");
pub const VERSION: u32 = 1;
pub const CAPACITY: usize = 1024;
pub const SLOT_WORDS: usize = 24;
pub const SLOT_BYTES: usize = 96;
pub const HEADER_BYTES: usize = 64;
pub const SLOT_VERSION: u32 = 1;
const STATE_OPEN: u32 = 0;
const STATE_CORRUPT: u32 = 1;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RingError {
Closed,
Backlog,
SlotVersion,
ZeroRequest,
}
#[repr(C, align(64))]
pub struct CommandRing {
header: [AtomicU32; 16],
slots: [[AtomicU32; SLOT_WORDS]; CAPACITY],
}
impl CommandRing {
pub fn new() -> Box<Self> {
let ring = Box::new(Self {
header: std::array::from_fn(|_| AtomicU32::new(0)),
slots: std::array::from_fn(|_| std::array::from_fn(|_| AtomicU32::new(0))),
});
ring.header[0].store(MAGIC, Ordering::Relaxed);
ring.header[1].store(VERSION, Ordering::Relaxed);
ring.header[2].store(CAPACITY as u32, Ordering::Relaxed);
ring.header[3].store(SLOT_WORDS as u32, Ordering::Relaxed);
ring
}
pub fn ptr(&self) -> u32 {
self as *const Self as usize as u32
}
/// Consumer-only. The producer publishes word zero (slot version) last, then write_index.
pub fn drain(&self, mut visit: impl FnMut([u32; SLOT_WORDS])) -> Result<(), RingError> {
if self.header[6].load(Ordering::Acquire) != STATE_OPEN {
return Err(RingError::Closed);
}
let mut read = self.header[4].load(Ordering::Relaxed);
let write = self.header[5].load(Ordering::Acquire);
if write.wrapping_sub(read) > CAPACITY as u32 {
self.header[6].store(STATE_CORRUPT, Ordering::Release);
return Err(RingError::Backlog);
}
while read != write {
let slot = &self.slots[read as usize % CAPACITY];
let mut words = [0; SLOT_WORDS];
for (out, word) in words.iter_mut().zip(slot) {
*out = word.load(Ordering::Relaxed);
}
let error = if words[0] != SLOT_VERSION {
Some(RingError::SlotVersion)
} else if words[2] == 0 {
Some(RingError::ZeroRequest)
} else {
None
};
if let Some(error) = error {
self.header[6].store(STATE_CORRUPT, Ordering::Release);
return Err(error);
}
visit(words);
read = read.wrapping_add(1);
self.header[4].store(read, Ordering::Release);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_layout() {
assert_eq!(std::mem::size_of::<[AtomicU32; 16]>(), HEADER_BYTES);
assert_eq!(std::mem::size_of::<[AtomicU32; SLOT_WORDS]>(), SLOT_BYTES);
assert_eq!(
std::mem::size_of::<CommandRing>(),
HEADER_BYTES + CAPACITY * SLOT_BYTES
);
assert_eq!(std::mem::align_of::<CommandRing>(), 64);
}
#[test]
fn tagged_header_and_fifo_drain() {
let ring = CommandRing::new();
assert_eq!(ring.header[0].load(Ordering::Relaxed), MAGIC);
assert_eq!(ring.header[1].load(Ordering::Relaxed), VERSION);
ring.slots[0][0].store(SLOT_VERSION, Ordering::Relaxed);
ring.slots[0][1].store(7, Ordering::Relaxed);
ring.slots[0][2].store(99, Ordering::Relaxed);
ring.header[5].store(1, Ordering::Release);
let mut seen = vec![];
ring.drain(|w| seen.push((w[1], w[2]))).unwrap();
assert_eq!(seen, [(7, 99)]);
assert_eq!(ring.header[4].load(Ordering::Acquire), 1);
}
#[test]
fn wraps_slots() {
let ring = CommandRing::new();
ring.header[4].store(CAPACITY as u32, Ordering::Relaxed);
ring.slots[0][0].store(SLOT_VERSION, Ordering::Relaxed);
ring.slots[0][1].store(3, Ordering::Relaxed);
ring.slots[0][2].store(1, Ordering::Relaxed);
ring.header[5].store(CAPACITY as u32 + 1, Ordering::Release);
let mut opcode = 0;
ring.drain(|w| opcode = w[1]).unwrap();
assert_eq!(opcode, 3);
}
#[test]
fn malformed_slot_fails_closed() {
for (version, request, expected) in [
(2, 1, RingError::SlotVersion),
(SLOT_VERSION, 0, RingError::ZeroRequest),
] {
let ring = CommandRing::new();
ring.slots[0][0].store(version, Ordering::Relaxed);
ring.slots[0][2].store(request, Ordering::Relaxed);
ring.header[5].store(1, Ordering::Release);
assert_eq!(ring.drain(|_| {}), Err(expected));
assert_eq!(ring.drain(|_| {}), Err(RingError::Closed));
}
}
#[test]
fn full_is_valid_but_overfull_is_corrupt() {
let full = CommandRing::new();
for slot in &full.slots {
slot[0].store(SLOT_VERSION, Ordering::Relaxed);
slot[2].store(1, Ordering::Relaxed);
}
full.header[5].store(CAPACITY as u32, Ordering::Release);
let mut count = 0;
full.drain(|_| count += 1).unwrap();
assert_eq!(count, CAPACITY);
let overfull = CommandRing::new();
overfull.header[5].store(CAPACITY as u32 + 1, Ordering::Release);
assert_eq!(overfull.drain(|_| {}), Err(RingError::Backlog));
assert_eq!(overfull.drain(|_| {}), Err(RingError::Closed));
}
}
+220 -184
View File
@@ -1,216 +1,252 @@
use std::collections::HashMap;
use gltf::Gltf;
use ultraviolet::{Mat4, Vec3};
use wgpu::TextureFormat;
use crate::renderer::scene::{mesh_vertex_layout, MeshBuilder};
use crate::render_data::{
InstanceHandle, MeshCreateInfo, MeshHandle, ModelTransform, PipelineKey, RenderData,
RenderDataError, RenderFlags,
};
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
pub struct InstalledScene {
pub meshes: Vec<MeshHandle>,
pub instances: Vec<InstanceHandle>,
pub bounds: Option<ModelBounds>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ModelBounds {
pub min: [f32; 3],
pub max: [f32; 3],
}
impl ModelBounds {
fn new(min: [f32; 3], max: [f32; 3]) -> Self {
Self { min, max }
}
fn include_point(&mut self, point: [f32; 3]) {
fn include(&mut self, p: [f32; 3]) {
for i in 0..3 {
self.min[i] = self.min[i].min(point[i]);
self.max[i] = self.max[i].max(point[i]);
self.min[i] = self.min[i].min(p[i]);
self.max[i] = self.max[i].max(p[i]);
}
}
}
fn focus_bounds(points: &[[f32; 3]]) -> Option<ModelBounds> {
let first = *points.first()?;
if points.len() < 200 {
let mut bounds = ModelBounds {
min: first,
max: first,
};
for point in &points[1..] {
bounds.include(*point);
}
return Some(bounds);
}
let trim = points.len() / 100;
let mut min = [0.0; 3];
let mut max = [0.0; 3];
for axis in 0..3 {
let mut values: Vec<_> = points.iter().map(|point| point[axis]).collect();
values.sort_by(f32::total_cmp);
min[axis] = values[trim];
max[axis] = values[values.len() - trim - 1];
}
Some(ModelBounds { min, max })
}
#[derive(Debug, thiserror::Error)]
pub enum ImportError {
#[error("failed to fetch the model")]
Http(#[from] reqwest::Error),
#[error("failed to decode bytes")]
GltfParse(#[from] gltf::Error),
#[error("failed to load model")]
LoadError,
#[error("{0}")]
Other(String),
#[error("unsupported or malformed primitive: {0}")]
InvalidPrimitive(String),
#[error("failed to install imported scene")]
Install(#[from] RenderDataError),
}
fn convert_tex_coords(tex_coords: gltf::mesh::util::ReadTexCoords<'_>) -> Vec<[f32; 2]> {
use gltf::mesh::util::ReadTexCoords;
match tex_coords {
ReadTexCoords::F32(iter) => iter.collect(),
ReadTexCoords::U16(iter) => iter
.map(|[u, v]| [u as f32 / u16::MAX as f32, v as f32 / u16::MAX as f32])
.collect(),
ReadTexCoords::U8(iter) => iter
.map(|[u, v]| [u as f32 / u8::MAX as f32, v as f32 / u8::MAX as f32])
.collect(),
}
#[derive(Clone, Debug)]
pub struct ImportedGeometry {
pub key: (usize, usize),
pub double_sided: bool,
pub positions: Vec<[f32; 3]>,
pub normals: Vec<[f32; 3]>,
pub uvs: Vec<[f32; 2]>,
pub indices: Vec<u32>,
}
#[derive(Clone, Debug)]
pub struct ImportedOccurrence {
pub key: (usize, usize),
pub transform: ModelTransform,
}
#[derive(Clone, Debug, Default)]
pub struct ImportedScene {
pub geometries: Vec<ImportedGeometry>,
pub occurrences: Vec<ImportedOccurrence>,
}
fn convert_indices(indices: gltf::mesh::util::ReadIndices<'_>) -> Vec<u32> {
use gltf::mesh::util::ReadIndices;
match indices {
ReadIndices::U8(iter) => iter.map(|i| i as u32).collect(),
ReadIndices::U16(iter) => iter.map(|i| i as u32).collect(),
ReadIndices::U32(iter) => iter.collect(),
}
}
fn visit_node<'a>(
node: gltf::Node<'a>,
parent_transform: Mat4,
device: &wgpu::Device,
resources: &mut crate::renderer::GpuResources,
meshes: &mut Vec<crate::renderer::scene::Mesh>,
data_blob: &[u8],
pipeline_index: usize,
model_bounds: &mut Option<ModelBounds>,
) {
let local_transform = Mat4::from(node.transform().matrix());
let world_transform = parent_transform * local_transform;
let normal_matrix = world_transform.inversed().transposed();
if let Some(mesh) = node.mesh() {
for primitive in mesh.primitives() {
let reader = primitive.reader(|buffer| match buffer.source() {
gltf::buffer::Source::Bin => Some(&data_blob[..]),
_ => None,
});
let positions: Vec<[f32; 3]> = match reader.read_positions() {
Some(iter) => iter.collect(),
None => Vec::new(),
};
if positions.is_empty() {
continue;
}
let vertex_count = positions.len();
let default_normal_vec = normal_matrix.transform_vec3(Vec3::unit_y()).normalized();
let default_normal = [
default_normal_vec.x,
default_normal_vec.y,
default_normal_vec.z,
];
let mut normals: Vec<[f32; 3]> = reader
.read_normals()
.map(|iter| {
iter.map(|normal| {
let vec = Vec3::new(normal[0], normal[1], normal[2]);
let transformed = normal_matrix.transform_vec3(vec).normalized();
[transformed.x, transformed.y, transformed.z]
})
.collect()
})
.unwrap_or_else(|| vec![default_normal; vertex_count]);
if normals.len() != vertex_count {
normals.resize(vertex_count, default_normal);
}
let mut uvs: Vec<[f32; 2]> = reader
.read_tex_coords(0)
.map(convert_tex_coords)
.unwrap_or_else(|| vec![[0.0, 0.0]; vertex_count]);
if uvs.len() != vertex_count {
uvs.resize(vertex_count, [0.0, 0.0]);
}
for position in &positions {
let vec = Vec3::new(position[0], position[1], position[2]);
let transformed = world_transform.transform_point3(vec);
let world_point = [transformed.x, transformed.y, transformed.z];
if let Some(bounds) = model_bounds.as_mut() {
bounds.include_point(world_point);
} else {
*model_bounds = Some(ModelBounds::new(world_point, world_point));
pub fn decode_gltf(bytes: &[u8]) -> Result<ImportedScene, ImportError> {
let model = Gltf::from_slice(bytes)?;
let buffers = gltf::import_buffers(&model.document, None, model.blob.clone())?;
let mut result = ImportedScene::default();
let mut seen = HashMap::new();
fn visit(
node: gltf::Node<'_>,
parent: Mat4,
buffers: &[gltf::buffer::Data],
result: &mut ImportedScene,
seen: &mut HashMap<(usize, usize), ()>,
) -> Result<(), ImportError> {
let world = parent * Mat4::from(node.transform().matrix());
if let Some(mesh) = node.mesh() {
for primitive in mesh.primitives() {
if primitive.mode() != gltf::mesh::Mode::Triangles {
return Err(ImportError::InvalidPrimitive(
"only triangle primitives are supported".into(),
));
}
let key = (mesh.index(), primitive.index());
if seen.insert(key, ()).is_none() {
let reader = primitive
.reader(|buffer| buffers.get(buffer.index()).map(|data| data.0.as_slice()));
let Some(read_positions) = reader.read_positions() else {
continue;
};
let positions: Vec<_> = read_positions.collect();
let count = positions.len();
if count == 0 {
continue;
}
let mut normals: Vec<_> = reader
.read_normals()
.map(|x| x.collect())
.unwrap_or_default();
normals.resize(count, [0., 1., 0.]);
normals.truncate(count);
let mut uvs: Vec<_> = reader
.read_tex_coords(0)
.map(|x| x.into_f32().collect())
.unwrap_or_default();
uvs.resize(count, [0., 0.]);
uvs.truncate(count);
let indices: Vec<u32> = if let Some(indices) = reader.read_indices() {
indices.into_u32().collect()
} else {
let count = u32::try_from(count).map_err(|_| {
ImportError::InvalidPrimitive("vertex count exceeds u32".into())
})?;
(0..count).collect()
};
if indices.is_empty() {
continue;
}
result.geometries.push(ImportedGeometry {
key,
double_sided: primitive.material().double_sided(),
positions,
normals,
uvs,
indices,
});
} else if !result.geometries.iter().any(|geometry| geometry.key == key) {
continue;
}
result.occurrences.push(ImportedOccurrence {
key,
transform: world.into(),
});
}
let indices: Vec<u32> = reader
.read_indices()
.map(convert_indices)
.unwrap_or_else(|| (0..vertex_count as u32).collect());
if indices.is_empty() {
continue;
}
let mesh = MeshBuilder::default()
.with_vertices(device, resources, &positions, &normals, &uvs)
.with_indices(device, resources, &indices)
.with_pipeline(pipeline_index)
.with_model_matrix(device, resources, world_transform)
.build();
meshes.push(mesh);
}
for child in node.children() {
visit(child, world, buffers, result, seen)?
}
Ok(())
}
for child in node.children() {
visit_node(
child,
world_transform,
device,
resources,
meshes,
data_blob,
pipeline_index,
model_bounds,
);
}
}
pub async fn load_gltf_model(
device: &wgpu::Device,
resources: &mut crate::renderer::GpuResources,
meshes: &mut Vec<crate::renderer::scene::Mesh>,
surface_format: TextureFormat,
) -> Result<Option<ModelBounds>, ImportError> {
let glb_data = reqwest::get("http://localhost:8080/themanor.glb")
.await?
.bytes()
.await?;
let model = Gltf::from_slice(&glb_data)?;
let data_blob = model.blob.as_ref().ok_or(ImportError::LoadError)?;
let vertex_layout = mesh_vertex_layout();
let pipeline_index = resources.get_or_create_pipeline(
device,
"gltf_standard",
&vertex_layout,
include_str!("./gltf.wgsl"),
surface_format,
);
let mut model_bounds: Option<ModelBounds> = None;
for scene in model.scenes() {
for node in scene.nodes() {
visit_node(
node,
Mat4::identity(),
device,
resources,
meshes,
data_blob,
pipeline_index,
&mut model_bounds,
);
visit(node, Mat4::identity(), &buffers, &mut result, &mut seen)?
}
}
Ok(result)
}
Ok(model_bounds)
pub fn install_imported(
target: &mut RenderData,
imported: &ImportedScene,
pipelines: [PipelineKey; 2],
) -> Result<InstalledScene, ImportError> {
let mut stage = target.replacement_stage()?;
let mut handles = HashMap::new();
let mut mesh_handles = Vec::with_capacity(imported.geometries.len());
let mut instance_handles = Vec::new();
let mut first = HashMap::new();
for occurrence in &imported.occurrences {
first.entry(occurrence.key).or_insert(occurrence.transform);
}
for geometry in &imported.geometries {
let transform = *first
.get(&geometry.key)
.ok_or_else(|| ImportError::InvalidPrimitive("geometry has no occurrence".into()))?;
let created = stage.create_mesh(MeshCreateInfo {
positions: &geometry.positions,
normals: &geometry.normals,
uvs: &geometry.uvs,
indices: &geometry.indices,
pipeline: pipelines[usize::from(geometry.double_sided)],
flags: RenderFlags::VISIBLE,
default_instance_flags: RenderFlags::VISIBLE,
default_transform: transform,
})?;
handles.insert(geometry.key, created.mesh);
mesh_handles.push(created.mesh);
instance_handles.push(created.default_instance);
}
let mut consumed = HashMap::new();
let mut bounds: Option<ModelBounds> = None;
let geometries: HashMap<_, _> = imported
.geometries
.iter()
.map(|geometry| (geometry.key, geometry))
.collect();
let mut focus_points = Vec::new();
for occurrence in &imported.occurrences {
let mesh = *handles
.get(&occurrence.key)
.ok_or_else(|| ImportError::InvalidPrimitive("occurrence has no geometry".into()))?;
if consumed.insert(occurrence.key, ()).is_some() {
instance_handles.push(stage.create_instance(
mesh,
occurrence.transform,
RenderFlags::VISIBLE,
)?);
}
let geometry = geometries
.get(&occurrence.key)
.expect("installed occurrence must have geometry");
let transform = Mat4::from(occurrence.transform);
focus_points.extend(geometry.positions.iter().map(|position| {
let point = transform.transform_point3(Vec3::from(*position));
[point.x, point.y, point.z]
}));
let local = stage.mesh(mesh).unwrap().aabb;
for x in [local.min[0], local.max[0]] {
for y in [local.min[1], local.max[1]] {
for z in [local.min[2], local.max[2]] {
let p = Mat4::from(occurrence.transform).transform_point3(Vec3::new(x, y, z));
let p = [p.x, p.y, p.z];
if let Some(b) = bounds.as_mut() {
b.include(p)
} else {
bounds = Some(ModelBounds { min: p, max: p })
}
}
}
}
}
bounds = focus_bounds(&focus_points).or(bounds);
target.replace_with(stage)?;
Ok(InstalledScene {
meshes: mesh_handles,
instances: instance_handles,
bounds,
})
}
+7 -3
View File
@@ -18,6 +18,9 @@ struct VertexInput {
@location(4) model_col1: vec4<f32>,
@location(5) model_col2: vec4<f32>,
@location(6) model_col3: vec4<f32>,
@location(7) normal_col0: vec4<f32>,
@location(8) normal_col1: vec4<f32>,
@location(9) normal_col2: vec4<f32>,
}
struct VertexOutput {
@@ -39,7 +42,8 @@ fn vs_main(in: VertexInput) -> VertexOutput {
let world_position = model * vec4<f32>(in.pos, 1.0);
out.clip_position = view_proj * world_position;
out.world_pos = world_position.xyz;
out.normal = normalize(in.normal);
let normal_matrix = mat3x3<f32>(in.normal_col0.xyz, in.normal_col1.xyz, in.normal_col2.xyz);
out.normal = normalize(normal_matrix * in.normal);
return out;
}
@@ -47,13 +51,13 @@ fn vs_main(in: VertexInput) -> VertexOutput {
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let light_direction = normalize(vec3<f32>(0.35, 1.0, 0.45));
let light_color = vec3<f32>(1.0, 0.95, 0.85);
let base_color = vec3<f32>(0.2, 0.2, 0.2);
let base_color = vec3<f32>(0.55, 0.58, 0.62);
let normal = normalize(in.normal);
let view_dir = normalize(uni.camera_position.xyz - in.world_pos);
let diffuse_strength = max(dot(normal, light_direction), 0.0);
let ambient = 0.15;
let ambient = 0.45;
var specular = 0.0;
if diffuse_strength > 0.0 {
+39
View File
@@ -1,9 +1,48 @@
pub mod app_setup;
pub mod camera;
pub mod command_ring;
pub mod gltf;
pub mod message;
pub mod platform;
pub mod render_data;
pub mod render_graph;
pub mod renderer;
pub mod shared_snapshot;
#[cfg(target_arch = "wasm32")]
thread_local! { static PAYLOADS: std::cell::RefCell<std::collections::HashMap<u32, Vec<u8>>> = Default::default(); }
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn stage_payload(id: u32, bytes: js_sys::Uint8Array) {
PAYLOADS.with(|payloads| {
payloads.borrow_mut().insert(id, bytes.to_vec());
});
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn discard_payload(id: u32) {
PAYLOADS.with(|payloads| {
payloads.borrow_mut().remove(&id);
});
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn clear_payloads() {
PAYLOADS.with(|payloads| payloads.borrow_mut().clear());
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn take_payload(id: u32) -> Option<Vec<u8>> {
PAYLOADS.with(|payloads| payloads.borrow_mut().remove(&id))
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn take_payload(_id: u32) -> Option<Vec<u8>> {
None
}
/// Worker entrypoint helper - executes the closure it is spawned with
/// Applications should export this with #[wasm_bindgen]
+1 -1
View File
@@ -1,6 +1,6 @@
use core::fmt;
use std::sync::mpsc::TryRecvError;
use std::cell::BorrowMutError;
use std::sync::mpsc::TryRecvError;
#[derive(Debug)]
pub enum WindowEvent {
+49 -15
View File
@@ -1,21 +1,32 @@
// Generic worker that imports the app's WASM module relative to the generated pkg folder.
// Works for any application because the relative depth from this file to pkg is stable.
import initWasm, { worker_entrypoint } from "/level-editor/pkg/level_editor.js";
import initWasm, { clear_payloads, discard_payload, stage_payload, worker_entrypoint } from "/level-editor/pkg/level_editor.js";
export function attachMain() {}
let isReady = false;
export function listenerReady() {
if (state !== "waiting-listener") return;
state = "replaying";
for (const queued of pending.splice(0)) route(queued);
state = "ready";
}
onmessage = async (event) => {
console.log("worker received message", event);
if (isReady) return;
let api;
let state = "uninitialized";
const pending = [];
isReady = true;
const wasmModule = event.data[0]; // WebAssembly.Module from wasm_bindgen::module()
const workerId = event.data[1]; // worker ID
const memory = event.data[2]; // shared memory
const entryPtr = event.data[3]; // worker entrypoint function pointer
// This listener is never replaced: canvas and payload transfers that race WASM
// initialization remain ordered and are replayed after init.
addEventListener("message", async (event) => {
const message = event.data;
if (message?.type !== "init") {
if (state !== "ready") pending.push(message);
else route(message);
return;
}
if (state !== "uninitialized") return;
state = "initializing";
const { wasmModule, workerId, memory, entryPtr } = message;
console.log(
"worker: initializing with WASM module",
@@ -25,8 +36,31 @@ onmessage = async (event) => {
);
// Initialize WASM with the shared module and memory forwarded from the main thread.
await initWasm({ module_or_path: wasmModule, memory });
try {
api = await initWasm({ module_or_path: wasmModule, memory });
state = "waiting-listener";
worker_entrypoint(entryPtr);
} catch (error) {
fatal("WORKER_INIT_FAILED", String(error));
}
});
// Call the app-provided worker entrypoint once initialization completes.
worker_entrypoint(entryPtr);
};
function route(message) {
if (message?.type === "canvas") {
dispatchEvent(new MessageEvent("renderer-canvas", { data: message.canvas }));
} else if (message?.type === "payload") {
stage_payload(message.id, new Uint8Array(message.buffer));
postMessage({ type: "payload-ready", id: message.id });
} else if (message?.type === "payload-release") {
discard_payload(message.id);
}
}
function fatal(code, message) {
state = "failed";
pending.length = 0;
try { clear_payloads?.(); } catch { /* best effort during a fatal failure */ }
postMessage({type:"fatal",code,message});
}
addEventListener("error", event => fatal("WORKER_RUNTIME_ERROR", event.error?.stack || `${event.message} (${event.filename}:${event.lineno}:${event.colno})`));
addEventListener("unhandledrejection", event => fatal("WORKER_UNHANDLED_REJECTION",String(event.reason)));
+28 -13
View File
@@ -1,3 +1,4 @@
use crate::command_ring::CommandRing;
use crate::message::WindowEvent;
use log::info;
use std::sync::mpsc::Receiver;
@@ -22,6 +23,9 @@ extern "C" {
/// Nothing to do.
#[wasm_bindgen]
fn attachMain();
#[wasm_bindgen(js_name = "listenerReady")]
fn listener_ready();
}
pub struct MainWorker {
@@ -52,6 +56,7 @@ impl MainWorker {
pub fn spawn(
name: &str,
id: usize,
ring_ptr: u32,
f: impl FnOnce() + Send + 'static,
) -> Result<Self, JsValue> {
// Creates a new worker.
@@ -62,19 +67,20 @@ impl MainWorker {
let ptr = Box::into_raw(Box::new(Box::new(f) as Box<dyn FnOnce()>));
// Sets default callback.
let callback = Closure::new(|_ev| {
info!("got a message..canvas?");
});
let callback = Closure::new(|_ev| {});
handle.set_onmessage(Some(callback.as_ref().unchecked_ref()));
let msg: js_sys::Array = [
&wasm_bindgen::module(),
&id.into(),
&wasm_bindgen::memory(),
&JsValue::from(ptr as u32),
]
.into_iter()
.collect();
let msg = js_sys::Object::new();
for (key, value) in [
("type", JsValue::from("init")),
("wasmModule", wasm_bindgen::module()),
("workerId", id.into()),
("memory", wasm_bindgen::memory()),
("entryPtr", (ptr as u32).into()),
("ringPtr", ring_ptr.into()),
] {
js_sys::Reflect::set(&msg, &key.into(), &value)?;
}
info!("posting message");
handle.post_message(&msg)?;
@@ -90,21 +96,26 @@ impl MainWorker {
let offscreen_canvas = canvas.transfer_control_to_offscreen().unwrap();
let transfer_list = js_sys::Array::new();
transfer_list.push(&offscreen_canvas);
let msg = js_sys::Object::new();
js_sys::Reflect::set(&msg, &"type".into(), &"canvas".into()).unwrap();
js_sys::Reflect::set(&msg, &"canvas".into(), &offscreen_canvas).unwrap();
info!("posting canvas (is_undefined: {})", canvas.is_undefined());
self.handle
.post_message_with_transfer(&offscreen_canvas, &transfer_list)
.post_message_with_transfer(&msg, &transfer_list)
.unwrap();
}
pub async fn run_render_loop<T: crate::renderer::scene::Scene + 'static>(
events_chan: Receiver<WindowEvent>,
ring: &'static CommandRing,
) {
use crate::renderer::Renderer;
let canvas = wait_for_canvas_transfer().await;
let renderer = Rc::new(RefCell::new(Renderer::<T>::new(canvas, events_chan).await));
renderer.borrow_mut().command_ring = Some(ring);
Renderer::run_render_loop(renderer);
}
}
@@ -135,8 +146,12 @@ pub async fn wait_for_canvas_transfer() -> web_sys::OffscreenCanvas {
}
});
global.set_onmessage(Some(handler.as_ref().unchecked_ref()));
global
.add_event_listener_with_callback("renderer-canvas", handler.as_ref().unchecked_ref())
.unwrap();
handler.forget();
listener_ready();
});
let canvas: web_sys::OffscreenCanvas = JsFuture::from(promise)
+240
View File
@@ -0,0 +1,240 @@
use bytemuck::{Pod, Zeroable};
macro_rules! handle {
($name:ident) => {
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Pod, Zeroable)]
pub struct $name {
slot: u32,
generation: u32,
}
impl $name {
pub const fn from_parts(slot: u32, generation: u32) -> Self {
Self { slot, generation }
}
pub const fn slot(self) -> u32 {
self.slot
}
pub const fn generation(self) -> u32 {
self.generation
}
}
};
}
handle!(MeshHandle);
handle!(InstanceHandle);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum SlotState {
Occupied,
Vacant { next: Option<u32> },
Retired,
}
#[derive(Clone, Copy, Debug)]
pub(super) struct PreparedSlot {
pub slot: u32,
pub generation: u32,
reused_next: Option<u32>,
append: bool,
}
pub(super) struct SlotTable {
pub(super) generations: Vec<u32>,
pub(super) states: Vec<SlotState>,
free_head: Option<u32>,
live_count: u32,
logical_capacity: u32,
pub(super) max_capacity: Option<u32>,
}
impl SlotTable {
pub fn new(
initial: u32,
max: Option<u32>,
resource: &'static str,
) -> Result<Self, crate::render_data::RenderDataError> {
let mut table = Self {
generations: Vec::new(),
states: Vec::new(),
free_head: None,
live_count: 0,
logical_capacity: 0,
max_capacity: max,
};
table.reserve_for_len(initial, resource)?;
Ok(table)
}
pub fn live_count(&self) -> u32 {
self.live_count
}
pub fn logical_capacity(&self) -> u32 {
self.logical_capacity
}
pub fn max_capacity(&self) -> Option<u32> {
self.max_capacity
}
pub fn required_len_for_prepare(&self) -> Result<u32, crate::render_data::RenderDataError> {
if self.free_head.is_some() {
u32::try_from(self.generations.len()).map_err(|_| {
crate::render_data::RenderDataError::CapacityOverflow { resource: "slots" }
})
} else {
let len = u32::try_from(self.generations.len()).map_err(|_| {
crate::render_data::RenderDataError::CapacityOverflow { resource: "slots" }
})?;
len.checked_add(1)
.ok_or(crate::render_data::RenderDataError::CapacityOverflow { resource: "slots" })
}
}
pub fn reserve_for_len(
&mut self,
required: u32,
resource: &'static str,
) -> Result<(), crate::render_data::RenderDataError> {
let target = crate::render_data::next_capacity(
self.logical_capacity,
required,
self.max_capacity,
resource,
)?;
crate::render_data::reserve_vec(&mut self.generations, target, resource)?;
crate::render_data::reserve_vec(&mut self.states, target, resource)?;
self.logical_capacity = target;
Ok(())
}
pub fn prepare(&self) -> Result<PreparedSlot, crate::render_data::RenderDataError> {
if let Some(slot) = self.free_head {
let index = slot as usize;
let SlotState::Vacant { next } = self.states[index] else {
unreachable!("free list points to a non-vacant slot")
};
Ok(PreparedSlot {
slot,
generation: self.generations[index],
reused_next: next,
append: false,
})
} else {
let slot = u32::try_from(self.generations.len()).map_err(|_| {
crate::render_data::RenderDataError::CapacityOverflow { resource: "slots" }
})?;
Ok(PreparedSlot {
slot,
generation: 1,
reused_next: None,
append: true,
})
}
}
pub fn commit(&mut self, prepared: PreparedSlot) {
if prepared.append {
self.generations.push(prepared.generation);
self.states.push(SlotState::Occupied);
} else {
self.free_head = prepared.reused_next;
self.states[prepared.slot as usize] = SlotState::Occupied;
}
self.live_count += 1;
}
pub fn contains(&self, slot: u32, generation: u32) -> bool {
let index = slot as usize;
self.generations.get(index) == Some(&generation)
&& matches!(self.states.get(index), Some(SlotState::Occupied))
}
pub fn remove(&mut self, slot: u32, generation: u32) -> bool {
if !self.contains(slot, generation) {
return false;
}
let index = slot as usize;
self.live_count -= 1;
if generation == u32::MAX {
self.states[index] = SlotState::Retired;
} else {
self.generations[index] = generation + 1;
self.states[index] = SlotState::Vacant {
next: self.free_head,
};
self.free_head = Some(slot);
}
true
}
pub fn clear(&mut self) {
self.free_head = None;
self.live_count = 0;
for index in (0..self.states.len()).rev() {
match self.states[index] {
SlotState::Occupied if self.generations[index] == u32::MAX => {
self.states[index] = SlotState::Retired;
}
SlotState::Occupied => {
self.generations[index] += 1;
self.states[index] = SlotState::Vacant {
next: self.free_head,
};
self.free_head = Some(
u32::try_from(index).expect("slot table length was checked before append"),
);
}
SlotState::Vacant { .. } => {
self.states[index] = SlotState::Vacant {
next: self.free_head,
};
self.free_head = Some(
u32::try_from(index).expect("slot table length was checked before append"),
);
}
SlotState::Retired => {}
}
}
}
pub fn seed_successor(&mut self, predecessor: &Self) {
self.generations.clear();
self.states.clear();
self.free_head = None;
self.live_count = 0;
for generation in predecessor.generations.iter().copied() {
let generation = generation.saturating_add(1);
self.generations.push(generation);
if generation == u32::MAX {
self.states.push(SlotState::Retired);
} else {
self.states.push(SlotState::Vacant {
next: self.free_head,
});
self.free_head = Some((self.states.len() - 1) as u32);
}
}
}
pub fn occupied(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
self.states.iter().enumerate().filter_map(|(index, state)| {
matches!(state, SlotState::Occupied).then(|| {
(
u32::try_from(index).expect("slot table length was checked before append"),
self.generations[index],
)
})
})
}
#[cfg(test)]
pub fn force_generation(&mut self, slot: u32, generation: u32) {
self.generations[slot as usize] = generation;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,85 @@
use std::ops::Range;
use super::RenderDataError;
#[derive(Default, Debug)]
pub(super) struct RangeAllocator {
free: Vec<Range<u32>>,
pub(super) high_water: u32,
}
impl RangeAllocator {
pub fn allocate(&mut self, count: u32) -> Result<Range<u32>, RenderDataError> {
if count == 0 {
return Err(RenderDataError::EmptyRange);
}
if let Some(index) = self
.free
.iter()
.position(|range| range.end - range.start >= count)
{
let start = self.free[index].start;
let end = start
.checked_add(count)
.ok_or(RenderDataError::RangeOverflow)?;
self.free[index].start = end;
if self.free[index].is_empty() {
self.free.remove(index);
}
return Ok(start..end);
}
let end = self
.high_water
.checked_add(count)
.ok_or(RenderDataError::RangeOverflow)?;
let range = self.high_water..end;
self.high_water = end;
Ok(range)
}
pub fn free(&mut self, range: Range<u32>) -> Result<u32, RenderDataError> {
if range.start >= range.end {
return Err(RenderDataError::EmptyRange);
}
if range.end > self.high_water {
return Err(RenderDataError::RangeOutOfBounds);
}
let index = self
.free
.partition_point(|candidate| candidate.start < range.start);
if index > 0 && self.free[index - 1].end > range.start
|| index < self.free.len() && self.free[index].start < range.end
{
return Err(RenderDataError::RangeOverlap);
}
let joins_left = index > 0 && self.free[index - 1].end == range.start;
let joins_right = index < self.free.len() && self.free[index].start == range.end;
match (joins_left, joins_right) {
(true, true) => {
let right_end = self.free.remove(index).end;
self.free[index - 1].end = right_end;
}
(true, false) => self.free[index - 1].end = range.end,
(false, true) => self.free[index].start = range.start,
(false, false) => self.free.insert(index, range),
}
while self
.free
.last()
.is_some_and(|range| range.end == self.high_water)
{
self.high_water = self.free.pop().unwrap().start;
}
Ok(self.high_water)
}
pub fn high_water(&self) -> u32 {
self.high_water
}
pub fn clear(&mut self) {
self.free.clear();
self.high_water = 0;
}
}
+522
View File
@@ -0,0 +1,522 @@
use super::*;
use crate::render_data::handle::SlotState;
const POSITIONS: [[f32; 3]; 3] = [[-1.0, 2.0, 3.0], [4.0, -2.0, 1.0], [0.0, 1.0, -3.0]];
const NORMALS: [[f32; 3]; 3] = [[0.0, 1.0, 0.0]; 3];
const UVS: [[f32; 2]; 3] = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
const INDICES: [u32; 3] = [0, 1, 2];
fn info() -> MeshCreateInfo<'static> {
MeshCreateInfo {
positions: &POSITIONS,
normals: &NORMALS,
uvs: &UVS,
indices: &INDICES,
pipeline: PipelineKey::new(7),
flags: RenderFlags::from_bits_retain(2),
default_instance_flags: RenderFlags::VISIBLE,
default_transform: IDENTITY_MODEL_TRANSFORM,
}
}
fn data() -> RenderData {
RenderData::new(RenderDataConfig {
initial_vertices: 0,
initial_indices: 0,
initial_meshes: 0,
initial_instances: 0,
..RenderDataConfig::default()
})
.unwrap()
}
#[test]
fn affine_world_bounds_cover_translation_scale_shear_and_planes() {
let local = Aabb {
min: [-1.0, -2.0, 0.0],
max: [1.0, 2.0, 0.0],
};
assert_eq!(
affine_world_aabb(local, IDENTITY_MODEL_TRANSFORM),
Ok(local)
);
let model = [
[-2.0, 0.0, 0.0, 0.0],
[0.5, 3.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[10.0, -4.0, 2.0, 1.0],
];
assert_eq!(
affine_world_aabb(local, model),
Ok(Aabb {
min: [7.0, -10.0, 2.0],
max: [13.0, 2.0, 2.0],
})
);
}
#[test]
fn world_bounds_reject_projective_and_overflowing_transforms() {
let local = Aabb {
min: [-1.0; 3],
max: [1.0; 3],
};
let mut projective = IDENTITY_MODEL_TRANSFORM;
projective[0][3] = 0.5;
assert_eq!(
affine_world_aabb(local, projective),
Err(RenderDataError::InvalidTransform)
);
let mut overflowing = IDENTITY_MODEL_TRANSFORM;
overflowing[0][0] = f32::MAX;
overflowing[1][0] = f32::MAX;
assert_eq!(
affine_world_aabb(local, overflowing),
Err(RenderDataError::InvalidTransform)
);
}
#[test]
fn default_instance_is_protected_and_flags_are_separate() {
let mut data = data();
let created = data.create_mesh(info()).unwrap();
assert!(data.instance(created.default_instance).unwrap().is_default);
assert_eq!(data.mesh(created.mesh).unwrap().flags.bits(), 2);
assert_eq!(
data.instance(created.default_instance).unwrap().flags,
RenderFlags::VISIBLE
);
assert_eq!(
data.destroy_instance(created.default_instance),
Err(RenderDataError::CannotDestroyDefaultInstance)
);
data.set_mesh_flags(created.mesh, RenderFlags::NONE)
.unwrap();
assert_eq!(data.mesh(created.mesh).unwrap().flags, RenderFlags::NONE);
assert_eq!(
data.instance(created.default_instance).unwrap().flags,
RenderFlags::VISIBLE
);
}
#[test]
fn stale_mesh_and_instance_handles_are_rejected_after_reuse() {
let mut data = data();
let first = data.create_mesh(info()).unwrap();
let old_instance = data
.create_instance(first.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
.unwrap();
data.destroy_instance(old_instance).unwrap();
let replacement = data
.create_instance(first.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
.unwrap();
assert_eq!(old_instance.slot(), replacement.slot());
assert_ne!(old_instance.generation(), replacement.generation());
assert!(data.instance(old_instance).is_none());
data.destroy_mesh(first.mesh).unwrap();
let second = data.create_mesh(info()).unwrap();
assert_eq!(first.mesh.slot(), second.mesh.slot());
assert_ne!(first.mesh.generation(), second.mesh.generation());
assert!(data.mesh(first.mesh).is_none());
}
#[test]
fn clear_handles_all_slot_states_retains_capacity_and_never_reuses_retired() {
let mut data = data();
let mesh = data.create_mesh(info()).unwrap();
let vacant = data
.create_instance(mesh.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
.unwrap();
data.destroy_instance(vacant).unwrap();
data.instances
.slots
.force_generation(mesh.default_instance.slot(), u32::MAX);
let old_capacity = data.capacities();
data.clear().unwrap();
assert_eq!(data.capacities(), old_capacity);
assert_eq!(data.mesh_count(), 0);
assert_eq!(data.instance_count(), 0);
assert!(data.mesh(mesh.mesh).is_none());
assert!(matches!(
data.instances.slots.states[mesh.default_instance.slot() as usize],
SlotState::Retired
));
let new_mesh = data.create_mesh(info()).unwrap();
assert_ne!(
new_mesh.default_instance.slot(),
mesh.default_instance.slot()
);
}
#[test]
fn capacity_math_has_exact_bounded_and_unbounded_overflow_behavior() {
assert_eq!(next_capacity(0, 1, None, "x"), Ok(1));
assert_eq!(next_capacity(1, 2, None, "x"), Ok(2));
assert_eq!(next_capacity(2, 3, Some(3), "x"), Ok(3));
assert_eq!(
next_capacity(u32::MAX - 1, u32::MAX, Some(u32::MAX), "x"),
Ok(u32::MAX)
);
assert!(matches!(
next_capacity(u32::MAX - 1, u32::MAX, None, "x"),
Err(RenderDataError::CapacityOverflow { .. })
));
assert!(matches!(
next_capacity(2, 4, Some(3), "x"),
Err(RenderDataError::CapacityExceeded { .. })
));
}
#[test]
fn all_storage_classes_grow_and_retired_slots_force_max_checked_append() {
let mut data = data();
let mesh = data.create_mesh(info()).unwrap();
assert_eq!(
data.capacities(),
RenderDataCapacities {
vertices: 3,
indices: 3,
meshes: 1,
instances: 1,
}
);
data.create_instance(mesh.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
.unwrap();
assert_eq!(data.capacities().instances, 2);
let mut slots = SlotTable::new(0, Some(1), "test").unwrap();
slots.reserve_for_len(1, "test").unwrap();
let prepared = slots.prepare().unwrap();
slots.commit(prepared);
slots.force_generation(0, u32::MAX);
slots.remove(0, u32::MAX);
assert!(matches!(
slots.reserve_for_len(slots.required_len_for_prepare().unwrap(), "test"),
Err(RenderDataError::CapacityExceeded { .. })
));
}
#[test]
fn allocator_checks_errors_splits_first_fit_coalesces_and_trims_tail() {
let mut allocator = RangeAllocator::default();
assert_eq!(allocator.allocate(0), Err(RenderDataError::EmptyRange));
let left = allocator.allocate(2).unwrap();
let middle = allocator.allocate(4).unwrap();
let right = allocator.allocate(2).unwrap();
assert_eq!(allocator.free(middle.clone()), Ok(8));
assert_eq!(allocator.allocate(2).unwrap(), 2..4);
assert_eq!(allocator.free(2..4), Ok(8));
assert_eq!(allocator.free(2..4), Err(RenderDataError::RangeOverlap));
assert_eq!(allocator.free(8..9), Err(RenderDataError::RangeOutOfBounds));
assert_eq!(allocator.free(3..3), Err(RenderDataError::EmptyRange));
assert_eq!(allocator.free(left), Ok(8));
assert_eq!(allocator.free(right), Ok(0));
let mut bridge = RangeAllocator::default();
bridge.allocate(6).unwrap();
bridge.free(0..2).unwrap();
bridge.free(4..6).unwrap();
bridge.free(2..4).unwrap();
assert_eq!(bridge.high_water(), 0);
let mut overflow = RangeAllocator::default();
overflow.high_water = u32::MAX;
assert_eq!(overflow.allocate(1), Err(RenderDataError::RangeOverflow));
}
#[test]
fn streams_remain_coordinated_across_interior_delete_tail_delete_and_reuse() {
let mut data = data();
let first = data.create_mesh(info()).unwrap();
let second = data.create_mesh(info()).unwrap();
assert_eq!(data.streams().positions.len(), 6);
assert_eq!(data.indices().len(), 6);
data.destroy_mesh(first.mesh).unwrap();
assert_eq!(data.streams().positions.len(), 6);
let reused = data.create_mesh(info()).unwrap();
assert_eq!(data.mesh(reused.mesh).unwrap().geometry.vertex_start, 0);
data.destroy_mesh(second.mesh).unwrap();
assert_eq!(data.streams().positions.len(), 3);
assert_eq!(data.streams().normals.len(), 3);
assert_eq!(data.streams().uvs.len(), 3);
assert_eq!(data.indices().len(), 3);
}
#[test]
fn failed_default_instance_preparation_rolls_back_empty_and_existing_geometry() {
let mut data = RenderData::new(RenderDataConfig {
initial_vertices: 0,
initial_indices: 0,
initial_meshes: 0,
initial_instances: 0,
max_instances: Some(0),
..RenderDataConfig::default()
})
.unwrap();
for _ in 0..2 {
let generations = data.meshes.slots.generations.clone();
assert!(matches!(
data.create_mesh(info()),
Err(RenderDataError::CapacityExceeded {
resource: "instances",
..
})
));
assert_eq!(data.vertices.allocator.high_water(), 0);
assert_eq!(data.indices.allocator.high_water(), 0);
assert!(data.streams().positions.is_empty());
assert!(data.indices().is_empty());
assert_eq!(data.meshes.slots.generations, generations);
}
data.instances.slots.max_capacity = Some(1);
let existing = data.create_mesh(info()).unwrap();
data.instances.slots.max_capacity = Some(0);
assert!(data.create_mesh(info()).is_err());
assert_eq!(data.vertices.allocator.high_water(), 3);
assert_eq!(data.mesh_count(), 1);
assert!(data.mesh(existing.mesh).is_some());
}
#[test]
fn aabb_supports_one_point_and_multiple_points() {
let point = [[2.0, -3.0, 4.0]];
let normal = [[0.0, 1.0, 0.0]];
let uv = [[0.0, 0.0]];
let index = [0];
let mut one = info();
one.positions = &point;
one.normals = &normal;
one.uvs = &uv;
one.indices = &index;
let mut data = data();
let mesh = data.create_mesh(one).unwrap();
assert_eq!(
data.mesh(mesh.mesh).unwrap().aabb,
Aabb {
min: point[0],
max: point[0]
}
);
let mesh = data.create_mesh(info()).unwrap();
assert_eq!(
data.mesh(mesh.mesh).unwrap().aabb,
Aabb {
min: [-1.0, -2.0, -3.0],
max: [4.0, 2.0, 3.0],
}
);
}
#[test]
fn malformed_geometry_matrix_is_rejected_without_consumption() {
let mut data = data();
let mut candidate = info();
candidate.positions = &[];
assert_eq!(
data.create_mesh(candidate).unwrap_err(),
RenderDataError::EmptyVertices
);
let short_normals = &NORMALS[..2];
let mut candidate = info();
candidate.normals = short_normals;
assert_eq!(
data.create_mesh(candidate).unwrap_err(),
RenderDataError::MismatchedVertexStreams
);
let short_uvs = &UVS[..2];
let mut candidate = info();
candidate.uvs = short_uvs;
assert_eq!(
data.create_mesh(candidate).unwrap_err(),
RenderDataError::MismatchedVertexStreams
);
let mut candidate = info();
candidate.indices = &[];
assert_eq!(
data.create_mesh(candidate).unwrap_err(),
RenderDataError::EmptyIndices
);
for stream in 0..3 {
for bad in [f32::NAN, f32::INFINITY] {
let mut positions = POSITIONS;
let mut normals = NORMALS;
let mut uvs = UVS;
match stream {
0 => positions[0][0] = bad,
1 => normals[0][0] = bad,
_ => uvs[0][0] = bad,
}
let mut candidate = info();
candidate.positions = &positions;
candidate.normals = &normals;
candidate.uvs = &uvs;
assert_eq!(
data.create_mesh(candidate).unwrap_err(),
RenderDataError::NonFiniteGeometry
);
}
}
let invalid = [3];
let mut candidate = info();
candidate.indices = &invalid;
assert_eq!(
data.create_mesh(candidate).unwrap_err(),
RenderDataError::IndexOutOfBounds
);
let valid_last = [2];
let mut candidate = info();
candidate.indices = &valid_last;
assert!(data.create_mesh(candidate).is_ok());
}
#[test]
fn normal_matrices_and_failed_transform_operations_are_transactional() {
let mut data = data();
let mesh = data.create_mesh(info()).unwrap();
assert_eq!(
data.instance(mesh.default_instance).unwrap().normal,
IDENTITY_NORMAL_MATRIX
);
let translation = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[4.0, 5.0, 6.0, 1.0],
];
data.set_instance_transform(mesh.default_instance, translation)
.unwrap();
assert_eq!(
data.instance(mesh.default_instance).unwrap().normal,
IDENTITY_NORMAL_MATRIX
);
let scale = [
[2.0, 0.0, 0.0, 0.0],
[0.0, 4.0, 0.0, 0.0],
[0.0, 0.0, 0.5, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
data.set_instance_transform(mesh.default_instance, scale)
.unwrap();
assert_eq!(
data.instance(mesh.default_instance).unwrap().normal,
[[0.5, 0.0, 0.0], [0.0, 0.25, 0.0], [0.0, 0.0, 2.0]]
);
let rotation = [
[0.0, 1.0, 0.0, 0.0],
[-1.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
data.set_instance_transform(mesh.default_instance, rotation)
.unwrap();
let old = data.instance(mesh.default_instance).unwrap();
for invalid in [[[0.0; 4]; 4], {
let mut value = IDENTITY_MODEL_TRANSFORM;
value[0][0] = f32::INFINITY;
value
}] {
assert_eq!(
data.set_instance_transform(mesh.default_instance, invalid),
Err(RenderDataError::InvalidTransform)
);
assert_eq!(data.instance(mesh.default_instance).unwrap(), old);
let count = data.instance_count();
assert_eq!(
data.create_instance(mesh.mesh, invalid, RenderFlags::NONE),
Err(RenderDataError::InvalidTransform)
);
assert_eq!(data.instance_count(), count);
let mut candidate = info();
candidate.default_transform = invalid;
assert_eq!(
data.create_mesh(candidate).unwrap_err(),
RenderDataError::InvalidTransform
);
}
}
#[test]
fn destroying_mesh_invalidates_exact_owner_instances_with_reused_generations() {
let mut data = data();
let first = data.create_mesh(info()).unwrap();
let second = data.create_mesh(info()).unwrap();
let first_extra = data
.create_instance(first.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
.unwrap();
let second_extra = data
.create_instance(second.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
.unwrap();
data.destroy_instance(first_extra).unwrap();
let reused = data
.create_instance(second.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::NONE)
.unwrap();
assert_eq!(first_extra.slot(), reused.slot());
data.destroy_mesh(first.mesh).unwrap();
assert!(data.instance(first.default_instance).is_none());
assert!(data.instance(second.default_instance).is_some());
assert!(data.instance(second_extra).is_some());
assert!(data.instance(reused).is_some());
assert_eq!(data.instances().count(), 3);
}
#[test]
fn revision_changes_only_after_success_and_replacement_rejects_old_handles() {
let mut data = data();
assert_eq!(data.revision(), 0);
assert!(data.destroy_mesh(MeshHandle::from_parts(9, 9)).is_err());
assert_eq!(data.revision(), 0);
let old = data.create_mesh(info()).unwrap();
assert_eq!(data.revision(), 1);
let mut stage = data.replacement_stage().unwrap();
let new = stage.create_mesh(info()).unwrap();
assert_ne!(old.mesh, new.mesh);
data.replace_with(stage).unwrap();
assert_eq!(data.revision(), 2);
assert!(data.mesh(old.mesh).is_none());
assert!(data.mesh(new.mesh).is_some());
}
#[test]
fn replacement_stage_is_rejected_after_source_mutation() {
let mut data = data();
let original = data.create_mesh(info()).unwrap();
let mut stage = data.replacement_stage().unwrap();
stage.create_mesh(info()).unwrap();
data.destroy_mesh(original.mesh).unwrap();
let current = data.create_mesh(info()).unwrap();
assert_eq!(current.mesh.slot(), original.mesh.slot());
assert_eq!(
data.replace_with(stage),
Err(RenderDataError::StaleReplacementStage)
);
assert!(data.mesh(current.mesh).is_some());
}
#[test]
fn replacement_stage_is_rejected_by_a_different_render_data() {
let source = data();
let stage = source.replacement_stage().unwrap();
let mut other = data();
assert_eq!(
other.replace_with(stage),
Err(RenderDataError::StaleReplacementStage)
);
}
#[test]
fn revision_overflow_rejects_mutation_without_committing() {
let mut data = data();
data.revision = u64::MAX;
assert_eq!(
data.create_mesh(info()),
Err(RenderDataError::RevisionOverflow)
);
assert_eq!(data.mesh_count(), 0);
assert_eq!(data.revision(), u64::MAX);
}
File diff suppressed because it is too large Load Diff
+58
View File
@@ -0,0 +1,58 @@
//! Device-free V1 render graph compiler and compiled graph registry.
mod compiler;
mod registry;
mod runtime;
mod schema;
pub use compiler::{
compile, compile_with, parse_and_compile, AllocationClass, CompiledGraph, CompiledOutput,
CompiledPass, CompiledRead, CompiledResource, CompiledWrite, ExecutorContract,
ExecutorRegistry, ExecutorResolution, Lifetime, NormalizedParameters, SceneForwardExecutors,
TextureAllocationKey, TextureUsage, TransientAllocation,
};
pub use registry::{CompiledGraphId, Registry};
pub use runtime::{
class_offsets, resolve_extent, runtime_texture_key, validate_activatable, ResolvedExtent,
RuntimeTextureKey,
};
pub use schema::*;
pub const MAX_JSON_BYTES: usize = 1024 * 1024;
pub const MAX_RESOURCES: usize = 1024;
pub const MAX_PASSES: usize = 1024;
pub const MAX_USES: usize = 8192;
pub const MAX_OUTPUTS: usize = 64;
#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
pub struct GraphError {
pub code: &'static str,
pub message: String,
pub details: serde_json::Value,
}
impl GraphError {
pub(crate) fn new(code: &'static str, message: impl Into<String>) -> Self {
let message = message.into();
Self {
code,
details: serde_json::json!({"message": message}),
message,
}
}
pub(crate) fn at(
code: &'static str,
message: impl Into<String>,
path: impl Into<String>,
) -> Self {
let message = message.into();
Self {
code,
details: serde_json::json!({"message": message, "path": path.into()}),
message,
}
}
}
#[cfg(test)]
mod tests;
+114
View File
@@ -0,0 +1,114 @@
use super::{parse_and_compile, CompiledGraph, GraphError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CompiledGraphId {
pub slot: u32,
pub generation: u32,
}
impl From<CompiledGraphId> for [u32; 2] {
fn from(x: CompiledGraphId) -> Self {
[x.slot, x.generation]
}
}
#[derive(Debug)]
struct Slot {
generation: u32,
value: Option<CompiledGraph>,
retired: bool,
}
#[derive(Debug)]
pub struct Registry {
slots: Vec<Slot>,
capacity: u32,
}
impl Default for Registry {
fn default() -> Self {
Self::new(16)
}
}
impl Registry {
pub fn new(capacity: u32) -> Self {
Self {
slots: vec![],
capacity,
}
}
pub fn compile(
&mut self,
bytes: &[u8],
) -> Result<(CompiledGraphId, serde_json::Value), GraphError> {
let graph = parse_and_compile(bytes)?;
if let Some((i, s)) = self.slots.iter_mut().enumerate().find(|(_, s)| {
s.value
.as_ref()
.is_some_and(|g| g.graph_id == graph.graph_id)
}) {
if graph.revision <= s.value.as_ref().unwrap().revision {
return Err(GraphError::new(
"GRAPH_REVISION_CONFLICT",
"revision must increase",
));
}
let id = CompiledGraphId {
slot: u32::try_from(i).map_err(|_| {
GraphError::new("GRAPH_LIMIT_EXCEEDED", "registry slot overflow")
})?,
generation: s.generation,
};
let summary = graph.summary(id.into());
s.value = Some(graph);
return Ok((id, summary));
}
let i = if let Some(i) = self
.slots
.iter()
.position(|s| s.value.is_none() && !s.retired)
{
i
} else {
if u32::try_from(self.slots.len()).map_or(true, |len| len >= self.capacity) {
return Err(GraphError::new(
"GRAPH_REGISTRY_FULL",
"compiled graph registry is full",
));
}
self.slots.push(Slot {
generation: 1,
value: None,
retired: false,
});
self.slots.len() - 1
};
let id = CompiledGraphId {
slot: u32::try_from(i)
.map_err(|_| GraphError::new("GRAPH_LIMIT_EXCEEDED", "registry slot overflow"))?,
generation: self.slots[i].generation,
};
let summary = graph.summary(id.into());
self.slots[i].value = Some(graph);
Ok((id, summary))
}
pub fn get(&self, id: CompiledGraphId) -> Result<&CompiledGraph, GraphError> {
self.slots
.get(id.slot as usize)
.filter(|s| s.generation == id.generation)
.and_then(|s| s.value.as_ref())
.ok_or_else(|| GraphError::new("STALE_GRAPH_ID", "stale compiled graph id"))
}
pub fn contains(&self, id: CompiledGraphId) -> bool {
self.get(id).is_ok()
}
pub fn drop_graph(&mut self, id: CompiledGraphId) -> Result<(), GraphError> {
let s = self
.slots
.get_mut(id.slot as usize)
.filter(|s| s.generation == id.generation && s.value.is_some())
.ok_or_else(|| GraphError::new("STALE_GRAPH_ID", "stale compiled graph id"))?;
s.value = None;
if s.generation == u32::MAX {
s.retired = true
} else {
s.generation += 1
}
Ok(())
}
}
+215
View File
@@ -0,0 +1,215 @@
use std::collections::BTreeMap;
use super::{
CompiledGraph, Dimension, Extent, ExternalSource, Format, GraphError, Residency,
TextureAllocationKey, TextureUsage,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ResolvedExtent {
pub width: u32,
pub height: u32,
pub depth_or_array_layers: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RuntimeTextureKey {
pub dimension: Dimension,
pub format: Format,
pub extent: ResolvedExtent,
pub mip_level_count: u32,
pub sample_count: u32,
pub usage: Vec<TextureUsage>,
pub view_formats: Vec<Format>,
}
fn scaled(value: u32, numerator: u32, denominator: u32) -> Result<u32, GraphError> {
if denominator == 0 {
return Err(GraphError::new(
"GRAPH_EXECUTION_UNSUPPORTED",
"zero extent denominator",
));
}
let product = u64::from(value)
.checked_mul(u64::from(numerator))
.ok_or_else(|| GraphError::new("GRAPH_EXECUTION_UNSUPPORTED", "extent overflow"))?;
let result = product
.checked_add(u64::from(denominator) - 1)
.ok_or_else(|| GraphError::new("GRAPH_EXECUTION_UNSUPPORTED", "extent overflow"))?
/ u64::from(denominator);
u32::try_from(result.max(1))
.map_err(|_| GraphError::new("GRAPH_EXECUTION_UNSUPPORTED", "extent overflow"))
}
pub fn resolve_extent(extent: &Extent, surface: [u32; 2]) -> Result<ResolvedExtent, GraphError> {
let (width, height, depth_or_array_layers) = match extent {
Extent::Absolute {
width,
height,
depth_or_array_layers,
} => (*width, *height, *depth_or_array_layers),
Extent::SurfaceRelative {
width,
height,
depth_or_array_layers,
} => (
scaled(surface[0], width.numerator, width.denominator)?,
scaled(surface[1], height.numerator, height.denominator)?,
*depth_or_array_layers,
),
};
if width == 0 || height == 0 || depth_or_array_layers == 0 {
return Err(GraphError::new(
"GRAPH_EXECUTION_UNSUPPORTED",
"texture extent must be nonzero",
));
}
Ok(ResolvedExtent {
width,
height,
depth_or_array_layers,
})
}
pub fn runtime_texture_key(
key: &TextureAllocationKey,
surface: [u32; 2],
) -> Result<RuntimeTextureKey, GraphError> {
Ok(RuntimeTextureKey {
dimension: key.descriptor.dimension,
format: key.descriptor.format,
extent: resolve_extent(&key.descriptor.extent, surface)?,
mip_level_count: key.descriptor.mip_level_count,
sample_count: key.descriptor.sample_count,
usage: key.usage.clone(),
view_formats: key.view_formats.clone(),
})
}
/// Assigns disjoint physical ranges after merging symbolic allocation classes that
/// resolve to the same concrete descriptor key.
pub fn class_offsets(
classes: &[(TextureAllocationKey, u32)],
surface: [u32; 2],
) -> Result<Vec<u32>, GraphError> {
let mut next = BTreeMap::new();
let mut offsets = Vec::with_capacity(classes.len());
for (key, count) in classes {
let concrete = runtime_texture_key(key, surface)?;
let offset = next.entry(concrete).or_insert(0u32);
offsets.push(*offset);
*offset = offset.checked_add(*count).ok_or_else(|| {
GraphError::new("GRAPH_EXECUTION_UNSUPPORTED", "transient slot overflow")
})?;
}
Ok(offsets)
}
pub fn validate_activatable(graph: &CompiledGraph) -> Result<(), GraphError> {
let unsupported = || {
GraphError::new(
"GRAPH_EXECUTION_UNSUPPORTED",
"graph is outside the activatable Phase 6 subset",
)
};
if graph.passes.is_empty() || graph.outputs.is_empty() {
return Err(unsupported());
}
let surface_outputs = graph
.outputs
.iter()
.filter(|o| {
matches!(
graph.resources[o.resource as usize].residency,
Residency::External {
source: ExternalSource::SurfaceColor
}
)
})
.count();
if surface_outputs == 0 {
return Err(unsupported());
}
for pass in &graph.passes {
if pass.executor.key != "scene_forward"
|| pass.executor.version != 1
|| !pass.reads.is_empty()
{
return Err(unsupported());
}
let color = pass
.writes
.iter()
.find(|w| w.binding == "color")
.ok_or_else(&unsupported)?;
let depth = pass
.writes
.iter()
.find(|w| w.binding == "depth")
.ok_or_else(&unsupported)?;
let c = &graph.resources[color.resource as usize];
let d = &graph.resources[depth.resource as usize];
if !matches!(
c.residency,
Residency::External {
source: ExternalSource::SurfaceColor
}
) || !matches!(d.residency, Residency::Transient)
|| d.descriptor.format != Format::Depth32Float
|| d.descriptor.dimension != Dimension::D2
|| d.descriptor.mip_level_count != 1
|| d.descriptor.sample_count != 1
|| d.descriptor.extent != c.descriptor.extent
{
return Err(unsupported());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render_graph::{Dimension, Ratio, TextureDescriptor, TextureUsage};
fn key(n: u32, d: u32) -> TextureAllocationKey {
TextureAllocationKey {
descriptor: TextureDescriptor {
dimension: Dimension::D2,
format: Format::Depth32Float,
extent: Extent::SurfaceRelative {
width: Ratio {
numerator: n,
denominator: d,
},
height: Ratio {
numerator: n,
denominator: d,
},
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
},
usage: vec![TextureUsage::DepthAttachment],
view_formats: vec![],
}
}
#[test]
fn extent_uses_checked_ceil_and_minimum_one() {
assert_eq!(
resolve_extent(&key(1, 2).descriptor.extent, [3, 1]).unwrap(),
ResolvedExtent {
width: 2,
height: 1,
depth_or_array_layers: 1
}
);
}
#[test]
fn equivalent_symbolic_classes_are_disjoint() {
assert_eq!(
class_offsets(&[(key(1, 2), 2), (key(2, 4), 3)], [100, 100]).unwrap(),
vec![0, 2]
);
}
}
+188
View File
@@ -0,0 +1,188 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct GraphV1 {
pub schema_version: u32,
pub graph_id: String,
pub revision: u32,
pub resources: Vec<Resource>,
pub passes: Vec<Pass>,
pub outputs: Vec<Output>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ResourceRef {
pub id: String,
pub version: u32,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Resource {
pub id: String,
pub version: u32,
pub residency: Residency,
pub texture: TextureDescriptor,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum Residency {
External { source: ExternalSource },
Transient,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ExternalSource {
SurfaceColor,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct TextureDescriptor {
pub dimension: Dimension,
pub format: Format,
pub extent: Extent,
pub mip_level_count: u32,
pub sample_count: u32,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum Dimension {
D1,
D2,
D3,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum Format {
Surface,
Rgba8Unorm,
Rgba8UnormSrgb,
Bgra8Unorm,
Bgra8UnormSrgb,
Rgba16Float,
R32Float,
Depth32Float,
}
impl Format {
pub(crate) fn depth(self) -> bool {
self == Self::Depth32Float
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum Extent {
Absolute {
width: u32,
height: u32,
#[serde(rename = "depthOrArrayLayers")]
depth_or_array_layers: u32,
},
SurfaceRelative {
width: Ratio,
height: Ratio,
#[serde(rename = "depthOrArrayLayers")]
depth_or_array_layers: u32,
},
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(deny_unknown_fields)]
pub struct Ratio {
pub numerator: u32,
pub denominator: u32,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Pass {
pub id: String,
pub state: PassState,
pub executor: ExecutorRef,
pub parameters: serde_json::Value,
pub reads: Vec<ReadBinding>,
pub writes: Vec<WriteBinding>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PassState {
Enabled,
Muted,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ExecutorRef {
pub key: String,
pub version: u32,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ReadBinding {
pub binding: String,
pub resource: ResourceRef,
pub access: ReadAccess,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum ReadAccess {
Sampled,
Storage,
CopySrc,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct WriteBinding {
pub binding: String,
pub resource: ResourceRef,
pub access: WriteAccess,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum WriteAccess {
Storage,
CopyDst,
ColorAttachment {
location: u32,
load: ColorLoad,
store: StoreOp,
},
DepthAttachment {
load: DepthLoad,
store: StoreOp,
},
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
pub enum ColorLoad {
Clear { value: [f64; 4] },
Load,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
pub enum DepthLoad {
Clear { value: f32 },
Load,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StoreOp {
Store,
Discard,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Output {
pub name: String,
pub resource: ResourceRef,
}
pub(crate) fn identifier(s: &str) -> bool {
s.as_bytes()
.first()
.is_some_and(|c| c.is_ascii_alphabetic() || *c == b'_')
&& s.bytes()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, b'.' | b'_' | b'/' | b'-'))
}
+702
View File
@@ -0,0 +1,702 @@
use super::*;
struct TestExecutors;
struct TestExecutor {
observable: bool,
}
static TEST_EXECUTOR: TestExecutor = TestExecutor { observable: false };
static OBSERVABLE_EXECUTOR: TestExecutor = TestExecutor { observable: true };
impl ExecutorRegistry for TestExecutors {
fn resolve(&self, executor: &ExecutorRef) -> ExecutorResolution<'_> {
if executor.version != 1 {
return ExecutorResolution::UnsupportedVersion;
}
match executor.key.as_str() {
"test" => ExecutorResolution::Found(&TEST_EXECUTOR),
"observable" => ExecutorResolution::Found(&OBSERVABLE_EXECUTOR),
_ => ExecutorResolution::UnknownKey,
}
}
}
impl ExecutorContract for TestExecutor {
fn inherently_observable(&self) -> bool {
self.observable
}
fn normalize_parameters(
&self,
parameters: &serde_json::Value,
) -> Result<NormalizedParameters, String> {
if parameters == &serde_json::json!({}) {
Ok(NormalizedParameters::SceneForward)
} else {
Err("test parameters must be empty".into())
}
}
fn validate_bindings(
&self,
_pass: &Pass,
_resources: &std::collections::HashMap<ResourceRef, &Resource>,
) -> Result<(), String> {
Ok(())
}
}
fn compile_json(value: serde_json::Value) -> Result<CompiledGraph, GraphError> {
compile_with(&serde_json::to_vec(&value).unwrap(), &TestExecutors)
}
fn texture(format: &str) -> serde_json::Value {
serde_json::json!({
"dimension": "d2",
"format": format,
"extent": {"kind":"absolute", "width":16, "height":16, "depthOrArrayLayers":1},
"mipLevelCount": 1,
"sampleCount": 1
})
}
fn transient(id: &str, format: &str) -> serde_json::Value {
serde_json::json!({
"id": id,
"version": 0,
"residency": {"kind":"transient"},
"texture": texture(format)
})
}
fn pass(
id: &str,
executor: &str,
reads: serde_json::Value,
writes: serde_json::Value,
) -> serde_json::Value {
serde_json::json!({
"id": id,
"state": "enabled",
"executor": {"key":executor, "version":1},
"parameters": {},
"reads": reads,
"writes": writes
})
}
fn resource_ref(id: &str) -> serde_json::Value {
serde_json::json!({"id":id, "version":0})
}
fn sampled(binding: &str, id: &str) -> serde_json::Value {
serde_json::json!({"binding":binding, "resource":resource_ref(id), "access":"sampled"})
}
fn copy_write(binding: &str, id: &str) -> serde_json::Value {
serde_json::json!({"binding":binding, "resource":resource_ref(id), "access":{"kind":"copy_dst"}})
}
fn color_write(binding: &str, id: &str, location: u32) -> serde_json::Value {
serde_json::json!({
"binding":binding,
"resource":resource_ref(id),
"access":{
"kind":"color_attachment",
"location":location,
"load":{"op":"clear", "value":[0.0, 0.0, 0.0, 1.0]},
"store":"store"
}
})
}
fn color_load(binding: &str, id: &str, location: u32) -> serde_json::Value {
serde_json::json!({
"binding":binding,
"resource":resource_ref(id),
"access":{
"kind":"color_attachment",
"location":location,
"load":{"op":"load"},
"store":"store"
}
})
}
fn graph(
resources: serde_json::Value,
passes: serde_json::Value,
outputs: serde_json::Value,
) -> serde_json::Value {
serde_json::json!({
"schemaVersion":1,
"graphId":"test_graph",
"revision":1,
"resources":resources,
"passes":passes,
"outputs":outputs
})
}
fn empty(id: &str, revision: u32) -> Vec<u8> {
format!(r#"{{"schemaVersion":1,"graphId":"{id}","revision":{revision},"resources":[],"passes":[],"outputs":[]}}"#).into_bytes()
}
fn error(bytes: &[u8]) -> &'static str {
parse_and_compile(bytes).unwrap_err().code
}
#[test]
fn size_precedes_encoding() {
assert_eq!(
error(&vec![0xff; MAX_JSON_BYTES + 1]),
"GRAPH_PAYLOAD_TOO_LARGE"
);
}
#[test]
fn encoding_precedes_schema() {
assert_eq!(error(&[0xff]), "GRAPH_ENCODING_INVALID");
}
#[test]
fn malformed_json() {
assert_eq!(error(b"{"), "GRAPH_JSON_INVALID");
}
#[test]
fn missing_schema_probe() {
assert_eq!(error(b"{}"), "GRAPH_SCHEMA_UNSUPPORTED");
}
#[test]
fn unsupported_schema_probe() {
assert_eq!(error(br#"{"schemaVersion":2}"#), "GRAPH_SCHEMA_UNSUPPORTED");
}
#[test]
fn strict_unknown_field() {
assert_eq!(error(br#"{"schemaVersion":1,"graphId":"g","revision":1,"resources":[],"passes":[],"outputs":[],"extra":0}"#), "GRAPH_JSON_INVALID");
}
#[test]
fn identifier_first_character() {
assert_eq!(error(&empty("1bad", 1)), "GRAPH_INVALID_ID");
}
#[test]
fn underscore_identifier() {
assert_eq!(parse_and_compile(&empty("_ok", 1)).unwrap().graph_id, "_ok");
}
#[test]
fn revision_required() {
assert_eq!(error(&empty("g", 0)), "GRAPH_INVALID_ID");
}
#[test]
fn resource_version_zero_and_wire_names() {
let json = br#"{"schemaVersion":1,"graphId":"g","revision":1,"resources":[{"id":"r","version":0,"residency":{"kind":"transient"},"texture":{"dimension":"d2","format":"rgba8_unorm","extent":{"kind":"absolute","width":1,"height":1,"depthOrArrayLayers":1},"mipLevelCount":1,"sampleCount":1}}],"passes":[],"outputs":[]}"#;
assert_eq!(parse_and_compile(json).unwrap().culled_resource_count, 1);
}
#[test]
fn old_mip_wire_rejected() {
let mut s = String::from_utf8(empty("g", 1)).unwrap();
s=s.replace("\"resources\":[]", "\"resources\":[{\"id\":\"r\",\"version\":0,\"residency\":{\"kind\":\"transient\"},\"texture\":{\"dimension\":\"d2\",\"format\":\"rgba8_unorm\",\"extent\":{\"kind\":\"absolute\",\"width\":1,\"height\":1,\"depthOrArrayLayers\":1},\"mipLevels\":1,\"sampleCount\":1}}]");
assert_eq!(error(s.as_bytes()), "GRAPH_JSON_INVALID");
}
#[test]
fn registry_transaction_on_parse_failure() {
let mut r = Registry::new(1);
assert!(r.compile(b"{").is_err());
assert!(r.compile(&empty("g", 1)).is_ok());
}
#[test]
fn registry_capacity() {
let mut r = Registry::new(1);
r.compile(&empty("a", 1)).unwrap();
assert_eq!(
r.compile(&empty("b", 1)).unwrap_err().code,
"GRAPH_REGISTRY_FULL"
);
}
#[test]
fn registry_revision_replaces_in_place() {
let mut r = Registry::new(1);
let (a, _) = r.compile(&empty("g", 1)).unwrap();
let (b, _) = r.compile(&empty("g", 2)).unwrap();
assert_eq!(a, b);
assert_eq!(r.get(a).unwrap().revision, 2);
}
#[test]
fn registry_revision_conflict() {
let mut r = Registry::new(1);
r.compile(&empty("g", 2)).unwrap();
assert_eq!(
r.compile(&empty("g", 2)).unwrap_err().code,
"GRAPH_REVISION_CONFLICT"
);
}
#[test]
fn registry_drop_and_stale() {
let mut r = Registry::new(1);
let (id, _) = r.compile(&empty("g", 1)).unwrap();
r.drop_graph(id).unwrap();
assert_eq!(r.get(id).unwrap_err().code, "STALE_GRAPH_ID");
assert_eq!(r.drop_graph(id).unwrap_err().code, "STALE_GRAPH_ID");
}
#[test]
fn registry_reuse_increments_generation() {
let mut r = Registry::new(1);
let (a, _) = r.compile(&empty("a", 1)).unwrap();
r.drop_graph(a).unwrap();
let (b, _) = r.compile(&empty("b", 1)).unwrap();
assert_eq!(a.slot, b.slot);
assert_eq!(a.generation + 1, b.generation);
}
#[test]
fn graph_error_details_always_have_message() {
let e = parse_and_compile(b"{}").unwrap_err();
assert!(e.details["message"].is_string());
}
#[test]
fn zero_surface_ratio_is_rejected_without_panicking() {
for field in ["width", "height"] {
let mut resource = transient("r", "rgba8_unorm");
resource["texture"]["extent"] = serde_json::json!({
"kind":"surface_relative",
"width":{"numerator":1,"denominator":1},
"height":{"numerator":1,"denominator":1},
"depthOrArrayLayers":1
});
resource["texture"]["extent"][field] = serde_json::json!({"numerator":0,"denominator":0});
let error = compile_json(graph(
serde_json::json!([resource]),
serde_json::json!([]),
serde_json::json!([]),
))
.unwrap_err();
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
}
}
#[test]
fn depth_texture_accepts_copy_destination_access() {
let compiled = compile_json(graph(
serde_json::json!([transient("depth", "depth32_float")]),
serde_json::json!([pass(
"write_depth",
"observable",
serde_json::json!([]),
serde_json::json!([copy_write("destination", "depth")])
)]),
serde_json::json!([]),
))
.unwrap();
assert_eq!(compiled.passes.len(), 1);
}
#[test]
fn unknown_resources_precede_executor_and_parameter_errors() {
let mut invalid = pass(
"bad",
"missing_executor",
serde_json::json!([sampled("input", "missing_resource")]),
serde_json::json!([]),
);
invalid["parameters"] = serde_json::json!({"also":"invalid"});
let error = compile_json(graph(
serde_json::json!([]),
serde_json::json!([invalid]),
serde_json::json!([]),
))
.unwrap_err();
assert_eq!(error.code, "GRAPH_UNKNOWN_RESOURCE");
}
#[test]
fn executor_contract_normalizes_parameters_and_reports_invalid_parameters() {
let valid = compile_json(graph(
serde_json::json!([transient("r", "rgba8_unorm")]),
serde_json::json!([pass(
"p",
"observable",
serde_json::json!([]),
serde_json::json!([copy_write("out", "r")])
)]),
serde_json::json!([]),
))
.unwrap();
assert_eq!(
valid.passes[0].parameters,
NormalizedParameters::SceneForward
);
let mut invalid = pass(
"p",
"observable",
serde_json::json!([]),
serde_json::json!([copy_write("out", "r")]),
);
invalid["parameters"] = serde_json::json!({"unexpected":true});
let error = compile_json(graph(
serde_json::json!([transient("r", "rgba8_unorm")]),
serde_json::json!([invalid]),
serde_json::json!([]),
))
.unwrap_err();
assert_eq!(error.code, "GRAPH_PARAMETERS_INVALID");
}
#[test]
fn culls_dead_branches_and_orders_live_dependencies_deterministically() {
let compiled = compile_json(graph(
serde_json::json!([
transient("middle", "rgba8_unorm"),
transient("output", "rgba8_unorm"),
transient("dead", "rgba8_unorm")
]),
serde_json::json!([
pass(
"consumer",
"test",
serde_json::json!([sampled("input", "middle")]),
serde_json::json!([copy_write("out", "output")])
),
pass(
"producer",
"test",
serde_json::json!([]),
serde_json::json!([copy_write("out", "middle")])
),
pass(
"dead",
"test",
serde_json::json!([]),
serde_json::json!([copy_write("out", "dead")])
)
]),
serde_json::json!([{"name":"present", "resource":resource_ref("output")}]),
))
.unwrap();
assert_eq!(
compiled
.passes
.iter()
.map(|p| p.id.as_str())
.collect::<Vec<_>>(),
["producer", "consumer"]
);
assert_eq!(compiled.culled_pass_count, 1);
assert_eq!(compiled.culled_resource_count, 1);
}
#[test]
fn output_extends_inclusive_lifetime_to_graph_boundary() {
let compiled = compile_json(graph(
serde_json::json!([transient("out", "rgba8_unorm")]),
serde_json::json!([pass(
"write",
"test",
serde_json::json!([]),
serde_json::json!([copy_write("out", "out")])
)]),
serde_json::json!([{"name":"present", "resource":resource_ref("out")}]),
))
.unwrap();
assert_eq!(compiled.resources[0].lifetime.first_use, 0);
assert_eq!(compiled.resources[0].lifetime.last_use, 1);
}
#[test]
fn transient_slots_reuse_only_for_non_overlapping_compatible_lifetimes() {
let compiled = compile_json(graph(
serde_json::json!([
transient("first", "rgba8_unorm"),
transient("second", "rgba8_unorm"),
transient("incompatible", "rgba16_float")
]),
serde_json::json!([
pass(
"a",
"observable",
serde_json::json!([]),
serde_json::json!([copy_write("out", "first")])
),
pass(
"b",
"observable",
serde_json::json!([]),
serde_json::json!([copy_write("out", "second")])
),
pass(
"c",
"observable",
serde_json::json!([]),
serde_json::json!([copy_write("out", "incompatible")])
)
]),
serde_json::json!([]),
))
.unwrap();
let allocations: Vec<_> = compiled
.resources
.iter()
.map(|resource| resource.allocation.unwrap())
.collect();
assert_eq!(allocations[0], allocations[1]);
assert_ne!(allocations[0].class, allocations[2].class);
assert_eq!(compiled.allocation_classes.len(), 2);
}
#[test]
fn cycle_details_survive_a_non_cycle_dfs_branch() {
let result = compile_json(graph(
serde_json::json!([
transient("ab", "rgba8_unorm"),
transient("bc", "rgba8_unorm"),
transient("ca", "rgba8_unorm"),
transient("branch", "rgba8_unorm")
]),
serde_json::json!([
pass(
"a",
"observable",
serde_json::json!([sampled("ca", "ca")]),
serde_json::json!([copy_write("ab", "ab"), copy_write("branch", "branch")])
),
pass(
"branch",
"observable",
serde_json::json!([sampled("input", "branch")]),
serde_json::json!([])
),
pass(
"b",
"observable",
serde_json::json!([sampled("ab", "ab")]),
serde_json::json!([copy_write("bc", "bc")])
),
pass(
"c",
"observable",
serde_json::json!([sampled("bc", "bc")]),
serde_json::json!([copy_write("ca", "ca")])
)
]),
serde_json::json!([]),
));
let error = result.unwrap_err();
assert_eq!(error.code, "GRAPH_CYCLE");
assert_eq!(error.details["kind"], "cycle");
let edges = error.details["edges"].as_array().unwrap();
assert_eq!(edges.len(), 3);
assert!(edges.iter().all(|edge| edge["from"] != "branch"));
}
#[test]
fn duplicate_external_source_is_an_identity_error_before_descriptor_validation() {
let external = |id: &str, texture: serde_json::Value| {
serde_json::json!({
"id":id,
"version":0,
"residency":{"kind":"external", "source":"surface_color"},
"texture":texture
})
};
let surface = serde_json::json!({
"dimension":"d2",
"format":"surface",
"extent":{
"kind":"surface_relative",
"width":{"numerator":1,"denominator":1},
"height":{"numerator":1,"denominator":1},
"depthOrArrayLayers":1
},
"mipLevelCount":1,
"sampleCount":1
});
let error = compile_json(graph(
serde_json::json!([
external("first", surface),
external("second", texture("rgba8_unorm"))
]),
serde_json::json!([]),
serde_json::json!([]),
))
.unwrap_err();
assert_eq!(error.code, "GRAPH_DUPLICATE_ID");
}
#[test]
fn duplicate_writer_precedes_illegal_access_on_the_second_writer() {
let illegal_depth_color = color_write("bad", "depth", 0);
let error = compile_json(graph(
serde_json::json!([transient("depth", "depth32_float")]),
serde_json::json!([
pass(
"first",
"observable",
serde_json::json!([]),
serde_json::json!([copy_write("out", "depth")])
),
pass(
"second",
"observable",
serde_json::json!([]),
serde_json::json!([illegal_depth_color])
)
]),
serde_json::json!([]),
))
.unwrap_err();
assert_eq!(error.code, "GRAPH_DUPLICATE_WRITER");
}
#[test]
fn rejects_device_invalid_dimensions_and_mismatched_attachments() {
let mut d1 = transient("d1", "rgba8_unorm");
d1["texture"]["dimension"] = serde_json::json!("d1");
assert_eq!(
compile_json(graph(
serde_json::json!([d1]),
serde_json::json!([]),
serde_json::json!([])
))
.unwrap_err()
.code,
"GRAPH_ILLEGAL_ACCESS"
);
let mut depth_d3 = transient("depth", "depth32_float");
depth_d3["texture"]["dimension"] = serde_json::json!("d3");
assert_eq!(
compile_json(graph(
serde_json::json!([depth_d3]),
serde_json::json!([]),
serde_json::json!([])
))
.unwrap_err()
.code,
"GRAPH_ILLEGAL_ACCESS"
);
let first = transient("first", "rgba8_unorm");
let mut second = transient("second", "rgba8_unorm");
second["texture"]["extent"]["width"] = serde_json::json!(32);
let error = compile_json(graph(
serde_json::json!([first, second]),
serde_json::json!([pass(
"attachments",
"observable",
serde_json::json!([]),
serde_json::json!([
color_write("first", "first", 0),
color_write("second", "second", 1)
])
)]),
serde_json::json!([]),
))
.unwrap_err();
assert_eq!(error.code, "GRAPH_ILLEGAL_ACCESS");
}
#[test]
fn cycle_tie_breaks_parallel_edges_by_original_resource_index() {
let error = compile_json(graph(
serde_json::json!([
transient("z_declared_first", "rgba8_unorm"),
transient("a_declared_second", "rgba8_unorm"),
transient("back", "rgba8_unorm")
]),
serde_json::json!([
pass(
"a",
"observable",
serde_json::json!([sampled("back", "back")]),
serde_json::json!([
copy_write("first", "z_declared_first"),
copy_write("second", "a_declared_second")
])
),
pass(
"b",
"observable",
serde_json::json!([
sampled("first", "z_declared_first"),
sampled("second", "a_declared_second")
]),
serde_json::json!([copy_write("back", "back")])
)
]),
serde_json::json!([]),
))
.unwrap_err();
assert_eq!(error.code, "GRAPH_CYCLE");
assert_eq!(
error.details["edges"][0]["resource"]["id"],
"z_declared_first"
);
}
#[test]
fn identifier_byte_limit_precedes_reference_and_executor_resolution() {
let overlong = "a".repeat(65);
let invalid = pass(
"p",
"unknown_executor",
serde_json::json!([sampled(&overlong, &overlong)]),
serde_json::json!([]),
);
let error = compile_json(graph(
serde_json::json!([]),
serde_json::json!([invalid]),
serde_json::json!([]),
))
.unwrap_err();
assert_eq!(error.code, "GRAPH_LIMIT_EXCEEDED");
}
#[test]
fn uninitialized_resource_precedes_transient_attachment_load_legality() {
let error = compile_json(graph(
serde_json::json!([
transient("loaded", "rgba8_unorm"),
transient("uninitialized", "rgba8_unorm")
]),
serde_json::json!([pass(
"conflicting_errors",
"observable",
serde_json::json!([sampled("missing_writer", "uninitialized")]),
serde_json::json!([color_load("loaded", "loaded", 0)])
)]),
serde_json::json!([]),
))
.unwrap_err();
assert_eq!(error.code, "GRAPH_UNINITIALIZED_RESOURCE");
}
#[test]
fn malformed_resource_reference_ids_precede_resolution() {
for bindings in ["reads", "writes"] {
let mut invalid = pass(
"p",
"unknown_executor",
serde_json::json!([]),
serde_json::json!([]),
);
invalid[bindings] = if bindings == "reads" {
serde_json::json!([sampled("input", "1bad")])
} else {
serde_json::json!([copy_write("output", "1bad")])
};
let error = compile_json(graph(
serde_json::json!([]),
serde_json::json!([invalid]),
serde_json::json!([]),
))
.unwrap_err();
assert_eq!(error.code, "GRAPH_INVALID_ID");
}
let error = compile_json(graph(
serde_json::json!([]),
serde_json::json!([]),
serde_json::json!([{"name":"present", "resource":resource_ref("1bad")}]),
))
.unwrap_err();
assert_eq!(error.code, "GRAPH_INVALID_ID");
}
+394
View File
@@ -0,0 +1,394 @@
use std::mem::size_of;
use crate::render_data::{MeshHandle, PipelineKey, RenderData, RenderFlags};
use bytemuck::{Pod, Zeroable};
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable, PartialEq)]
pub struct GpuInstance {
pub model: [[f32; 4]; 4],
pub normal_0: [f32; 4],
pub normal_1: [f32; 4],
pub normal_2: [f32; 4],
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DrawItem {
pub pipeline: PipelineKey,
pub mesh: MeshHandle,
pub indices: std::ops::Range<u32>,
pub base_vertex: i32,
pub instances: std::ops::Range<u32>,
}
#[derive(Default)]
pub struct GpuScenePlan {
pub positions: Vec<[f32; 3]>,
pub normals: Vec<[f32; 3]>,
pub uvs: Vec<[f32; 2]>,
pub indices: Vec<u32>,
pub instances: Vec<GpuInstance>,
pub draws: Vec<DrawItem>,
}
impl GpuScenePlan {
pub fn build(data: &RenderData) -> Result<Self, &'static str> {
let mut plan = Self::default();
let mut meshes: Vec<_> = data
.meshes()
.filter(|(_, mesh)| mesh.flags.contains(RenderFlags::VISIBLE))
.collect();
meshes.sort_by_key(|(handle, mesh)| {
(mesh.pipeline.get(), handle.slot(), handle.generation())
});
let streams = data.streams();
for (handle, mesh) in meshes {
let mut occurrences: Vec<_> = data
.instances()
.filter(|(_, instance)| {
instance.mesh == handle && instance.flags.contains(RenderFlags::VISIBLE)
})
.collect();
occurrences.sort_by_key(|(handle, _)| (handle.slot(), handle.generation()));
if occurrences.is_empty() {
continue;
}
let vertex_start = plan.positions.len();
let source_start = mesh.geometry.vertex_start as usize;
let source_end = source_start
.checked_add(mesh.geometry.vertex_count as usize)
.ok_or("vertex range overflow")?;
plan.positions.extend_from_slice(
streams
.positions
.get(source_start..source_end)
.ok_or("invalid vertex range")?,
);
plan.normals.extend_from_slice(
streams
.normals
.get(source_start..source_end)
.ok_or("invalid normal range")?,
);
plan.uvs.extend_from_slice(
streams
.uvs
.get(source_start..source_end)
.ok_or("invalid uv range")?,
);
let index_start =
u32::try_from(plan.indices.len()).map_err(|_| "index start exceeds u32")?;
let source_index = mesh.geometry.index_start as usize;
let source_index_end = source_index
.checked_add(mesh.geometry.index_count as usize)
.ok_or("index range overflow")?;
plan.indices.extend_from_slice(
data.indices()
.get(source_index..source_index_end)
.ok_or("invalid index range")?,
);
let instance_start =
u32::try_from(plan.instances.len()).map_err(|_| "instance start exceeds u32")?;
for (_, instance) in occurrences {
plan.instances.push(GpuInstance {
model: instance.model,
normal_0: [
instance.normal[0][0],
instance.normal[0][1],
instance.normal[0][2],
0.0,
],
normal_1: [
instance.normal[1][0],
instance.normal[1][1],
instance.normal[1][2],
0.0,
],
normal_2: [
instance.normal[2][0],
instance.normal[2][1],
instance.normal[2][2],
0.0,
],
});
}
plan.draws.push(DrawItem {
pipeline: mesh.pipeline,
mesh: handle,
indices: index_start
..index_start
.checked_add(mesh.geometry.index_count)
.ok_or("draw index range overflow")?,
base_vertex: i32::try_from(vertex_start).map_err(|_| "base vertex exceeds i32")?,
instances: instance_start
..u32::try_from(plan.instances.len())
.map_err(|_| "instance end exceeds u32")?,
});
}
Ok(plan)
}
}
pub fn required_buffer_capacity(
current: u64,
required: u64,
maximum: u64,
) -> Result<u64, &'static str> {
if required > maximum {
return Err("buffer exceeds device max_buffer_size");
}
if required == 0 || current >= required {
return Ok(current);
}
let grown = current
.checked_mul(2)
.ok_or("buffer capacity overflow")?
.max(1)
.max(required);
Ok(grown.min(maximum))
}
pub fn vertex_layouts() -> [wgpu::VertexBufferLayout<'static>; 4] {
const INSTANCE_ATTRIBUTES: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![3 => Float32x4, 4 => Float32x4, 5 => Float32x4, 6 => Float32x4, 7 => Float32x4, 8 => Float32x4, 9 => Float32x4];
[
wgpu::VertexBufferLayout {
array_stride: 12,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![0 => Float32x3],
},
wgpu::VertexBufferLayout {
array_stride: 12,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![1 => Float32x3],
},
wgpu::VertexBufferLayout {
array_stride: 8,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![2 => Float32x2],
},
wgpu::VertexBufferLayout {
array_stride: size_of::<GpuInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &INSTANCE_ATTRIBUTES,
},
]
}
#[derive(Default)]
pub struct BufferSlot {
pub buffer: Option<wgpu::Buffer>,
capacity: u64,
}
#[derive(Default)]
pub struct GpuSceneCache {
revision: Option<u64>,
pub positions: BufferSlot,
pub normals: BufferSlot,
pub uvs: BufferSlot,
pub indices: BufferSlot,
pub instances: BufferSlot,
pub draws: Vec<DrawItem>,
}
impl GpuSceneCache {
pub fn upload(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
data: &RenderData,
) -> Result<(), String> {
if self.revision == Some(data.revision()) {
return Ok(());
}
let plan = GpuScenePlan::build(data).map_err(str::to_owned)?;
if plan.draws.is_empty() {
self.draws.clear();
self.revision = Some(data.revision());
return Ok(());
}
let maximum = device.limits().max_buffer_size;
fn bytes<T>(values: &[T]) -> Result<u64, String> {
u64::try_from(values.len())
.map_err(|_| "buffer length overflow".to_owned())?
.checked_mul(size_of::<T>() as u64)
.ok_or_else(|| "buffer byte size overflow".to_owned())
}
let required = [
bytes(&plan.positions)?,
bytes(&plan.normals)?,
bytes(&plan.uvs)?,
bytes(&plan.indices)?,
bytes(&plan.instances)?,
];
let old = [
self.positions.capacity,
self.normals.capacity,
self.uvs.capacity,
self.indices.capacity,
self.instances.capacity,
];
let mut capacities = [0; 5];
for i in 0..5 {
capacities[i] =
required_buffer_capacity(old[i], required[i], maximum).map_err(str::to_owned)?;
}
let usages = [
wgpu::BufferUsages::VERTEX,
wgpu::BufferUsages::VERTEX,
wgpu::BufferUsages::VERTEX,
wgpu::BufferUsages::INDEX,
wgpu::BufferUsages::VERTEX,
];
let labels = [
"scene positions",
"scene normals",
"scene uvs",
"scene indices",
"scene instances",
];
let mut replacements: [Option<wgpu::Buffer>; 5] = Default::default();
for i in 0..5 {
if capacities[i] != old[i] {
replacements[i] = Some(device.create_buffer(&wgpu::BufferDescriptor {
label: Some(labels[i]),
size: capacities[i],
usage: usages[i] | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}));
}
}
let slots = [
&mut self.positions,
&mut self.normals,
&mut self.uvs,
&mut self.indices,
&mut self.instances,
];
for (i, slot) in slots.into_iter().enumerate() {
if let Some(buffer) = replacements[i].take() {
slot.buffer = Some(buffer);
slot.capacity = capacities[i];
}
}
let contents = [
bytemuck::cast_slice(&plan.positions),
bytemuck::cast_slice(&plan.normals),
bytemuck::cast_slice(&plan.uvs),
bytemuck::cast_slice(&plan.indices),
bytemuck::cast_slice(&plan.instances),
];
let slots = [
&self.positions,
&self.normals,
&self.uvs,
&self.indices,
&self.instances,
];
for (slot, contents) in slots.into_iter().zip(contents) {
if !contents.is_empty() {
queue.write_buffer(
slot.buffer.as_ref().expect("nonempty slot allocated"),
0,
contents,
);
}
}
self.draws = plan.draws;
self.revision = Some(data.revision());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render_data::{MeshCreateInfo, RenderDataConfig, IDENTITY_MODEL_TRANSFORM};
#[test]
fn instance_is_112_bytes_and_padding_is_zero() {
assert_eq!(size_of::<GpuInstance>(), 112);
let value = GpuInstance {
model: [[1.0; 4]; 4],
normal_0: [1., 2., 3., 0.],
normal_1: [4., 5., 6., 0.],
normal_2: [7., 8., 9., 0.],
};
assert_eq!(value.normal_2[3], 0.0);
}
#[test]
fn capacity_grows_and_checks_limit() {
assert_eq!(required_buffer_capacity(8, 9, 32), Ok(16));
assert!(required_buffer_capacity(0, 33, 32).is_err());
}
#[test]
fn capacity_reuses_and_layout_matches_shader_contract() {
assert_eq!(required_buffer_capacity(16, 12, 32), Ok(16));
assert_eq!(required_buffer_capacity(0, 1, 32), Ok(1));
let layouts = vertex_layouts();
assert_eq!(
layouts
.iter()
.map(|layout| layout.array_stride)
.collect::<Vec<_>>(),
[12, 12, 8, 112]
);
assert_eq!(
layouts[3]
.attributes
.iter()
.map(|attribute| attribute.shader_location)
.collect::<Vec<_>>(),
vec![3, 4, 5, 6, 7, 8, 9]
);
}
#[test]
fn plan_orders_pipelines_skips_hidden_and_uses_local_indices() {
let mut data = RenderData::new(RenderDataConfig {
initial_vertices: 0,
initial_indices: 0,
initial_meshes: 0,
initial_instances: 0,
..Default::default()
})
.unwrap();
let p = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]];
let n = [[0., 0., 1.]; 3];
let u = [[0., 0.]; 3];
let i = [0, 1, 2];
let mut add = |pipeline, visible| {
data.create_mesh(MeshCreateInfo {
positions: &p,
normals: &n,
uvs: &u,
indices: &i,
pipeline: PipelineKey::new(pipeline),
flags: RenderFlags::VISIBLE,
default_instance_flags: if visible {
RenderFlags::VISIBLE
} else {
RenderFlags::NONE
},
default_transform: IDENTITY_MODEL_TRANSFORM,
})
.unwrap()
};
let high = add(9, true);
let _hidden = add(0, false);
let low = add(2, true);
data.create_instance(low.mesh, IDENTITY_MODEL_TRANSFORM, RenderFlags::VISIBLE)
.unwrap();
let plan = GpuScenePlan::build(&data).unwrap();
assert_eq!(
plan.draws
.iter()
.map(|d| d.pipeline.get())
.collect::<Vec<_>>(),
vec![2, 9]
);
assert_eq!(plan.draws[0].base_vertex, 0);
assert_eq!(plan.draws[1].base_vertex, 3);
assert_eq!(plan.draws[0].instances, 0..2);
assert_eq!(plan.indices, [0, 1, 2, 0, 1, 2]);
assert_eq!(high.mesh, plan.draws[1].mesh);
}
}
File diff suppressed because it is too large Load Diff
+47 -308
View File
@@ -1,9 +1,9 @@
use ultraviolet::Mat4;
use wgpu::util::DeviceExt;
use crate::{
camera::Camera,
renderer::{self, BufferIndex, GpuResources, Index, ModelMatrix, Normal, Position, UV},
render_data::RenderData,
renderer::{self, GpuResources},
};
pub struct UniformResource {
@@ -12,7 +12,6 @@ pub struct UniformResource {
pub bind_group_layout: wgpu::BindGroupLayout,
}
/// Simple uniform data.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug, Default)]
pub struct FrameMetadata {
@@ -23,36 +22,30 @@ pub struct FrameMetadata {
_padding0: f32,
pub camera_position: [f32; 4],
}
impl FrameMetadata {
pub fn new(dimension: ultraviolet::Vec2) -> Self {
FrameMetadata {
Self {
resolution: dimension.into(),
mouse_move: [std::f32::MIN, std::f32::MIN],
mouse_click: [std::f32::MIN, std::f32::MIN],
_padding0: 0.0,
camera_position: [0.0, 0.0, 0.0, 1.0],
mouse_move: [f32::MIN; 2],
mouse_click: [f32::MIN; 2],
camera_position: [0., 0., 0., 1.],
..Default::default()
}
}
pub fn set_camera_position(&mut self, position: ultraviolet::Vec3) {
self.camera_position = [position.x, position.y, position.z, 1.0];
pub fn set_camera_position(&mut self, p: ultraviolet::Vec3) {
self.camera_position = [p.x, p.y, p.z, 1.];
}
pub fn update_dimension(&mut self, dimension: ultraviolet::Vec2) {
self.resolution = dimension.into();
pub fn update_dimension(&mut self, d: ultraviolet::Vec2) {
self.resolution = d.into();
}
pub fn create_uniform_resource(self, device: &wgpu::Device) -> UniformResource {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("frame metadata uniform buffer"),
contents: bytemuck::cast_slice(&[self][..]),
label: Some("frame metadata"),
contents: bytemuck::bytes_of(&self),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Uniform bind group layout"),
label: Some("frame layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
@@ -64,325 +57,71 @@ impl FrameMetadata {
count: None,
}],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Uniform bind group"),
label: Some("frame group"),
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
});
UniformResource {
buffer,
bind_group_layout,
bind_group,
}
}
}
pub struct Mesh {
pub pipeline_index: usize,
pub position_buffer_index: BufferIndex<Position>,
pub normal_buffer_index: BufferIndex<Normal>,
pub uv_buffer_index: BufferIndex<UV>,
pub model_buffer_index: BufferIndex<ModelMatrix>,
pub index_buffer_index: BufferIndex<Index>,
pub index_format: wgpu::IndexFormat,
pub index_count: u32,
pub instance_count: u32,
}
type VertexBufferSet = (BufferIndex<Position>, BufferIndex<Normal>, BufferIndex<UV>);
type IndexBufferInfo = (BufferIndex<Index>, u32, wgpu::IndexFormat);
pub fn mesh_vertex_layout() -> [wgpu::VertexBufferLayout<'static>; 4] {
[
wgpu::VertexBufferLayout {
array_stride: 12,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: 12,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: 8,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Float32x2,
}],
},
wgpu::VertexBufferLayout {
array_stride: 64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 3,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: 16,
shader_location: 4,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: 32,
shader_location: 5,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: 48,
shader_location: 6,
format: wgpu::VertexFormat::Float32x4,
},
],
},
]
}
pub struct MeshBuilder<I, V, P, M> {
indices: I,
vertices: V,
pipeline: P,
model_matrix: M,
instance_count: u32,
}
impl Default for MeshBuilder<(), (), (), ()> {
fn default() -> Self {
Self {
indices: (),
vertices: (),
pipeline: (),
model_matrix: (),
instance_count: 1,
}
}
}
impl<P, M> MeshBuilder<(), (), P, M> {
pub fn with_vertices(
self,
device: &wgpu::Device,
resources: &mut GpuResources,
positions: &[[f32; 3]],
normals: &[[f32; 3]],
uvs: &[[f32; 2]],
) -> MeshBuilder<(), VertexBufferSet, P, M> {
let position_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Positions"),
contents: bytemuck::cast_slice(positions),
usage: wgpu::BufferUsages::VERTEX,
});
let normal_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Normals"),
contents: bytemuck::cast_slice(normals),
usage: wgpu::BufferUsages::VERTEX,
});
let uv_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh UVs"),
contents: bytemuck::cast_slice(uvs),
usage: wgpu::BufferUsages::VERTEX,
});
let position_buffer_index = resources.add_position_buffer(position_buffer);
let normal_buffer_index = resources.add_normal_buffer(normal_buffer);
let uv_buffer_index = resources.add_uv_buffer(uv_buffer);
MeshBuilder {
vertices: (position_buffer_index, normal_buffer_index, uv_buffer_index),
indices: self.indices,
pipeline: self.pipeline,
model_matrix: self.model_matrix,
instance_count: self.instance_count,
}
}
}
impl<V, P, M> MeshBuilder<(), V, P, M> {
pub fn with_indices(
self,
device: &wgpu::Device,
resources: &mut GpuResources,
indices: &[u32],
) -> MeshBuilder<IndexBufferInfo, V, P, M> {
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Indices"),
contents: bytemuck::cast_slice(indices),
usage: wgpu::BufferUsages::INDEX,
});
let index_buffer_index = resources.add_index_buffer(index_buffer);
MeshBuilder {
indices: (
index_buffer_index,
indices.len() as u32,
wgpu::IndexFormat::Uint32,
),
vertices: self.vertices,
pipeline: self.pipeline,
model_matrix: self.model_matrix,
instance_count: self.instance_count,
}
}
}
impl<I, V, M> MeshBuilder<I, V, (), M> {
pub fn with_pipeline(self, pipeline_index: usize) -> MeshBuilder<I, V, usize, M> {
MeshBuilder {
pipeline: pipeline_index,
indices: self.indices,
vertices: self.vertices,
model_matrix: self.model_matrix,
instance_count: self.instance_count,
}
}
}
impl<I, V, P> MeshBuilder<I, V, P, ()> {
pub fn with_model_matrix(
self,
device: &wgpu::Device,
resources: &mut GpuResources,
matrix_columns: Mat4,
) -> MeshBuilder<I, V, P, BufferIndex<ModelMatrix>> {
let model_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Model Matrix"),
contents: bytemuck::cast_slice(matrix_columns.as_slice()),
usage: wgpu::BufferUsages::VERTEX,
});
let model_buffer_index = resources.add_model_matrix_buffer(model_buffer);
MeshBuilder {
indices: self.indices,
vertices: self.vertices,
pipeline: self.pipeline,
model_matrix: model_buffer_index,
instance_count: self.instance_count,
}
}
}
impl MeshBuilder<IndexBufferInfo, VertexBufferSet, usize, BufferIndex<ModelMatrix>> {
pub fn build(self) -> Mesh {
Mesh {
pipeline_index: self.pipeline,
position_buffer_index: (self.vertices).0,
normal_buffer_index: (self.vertices).1,
uv_buffer_index: (self.vertices).2,
model_buffer_index: self.model_matrix,
index_buffer_index: (self.indices).0,
index_count: (self.indices).1,
index_format: (self.indices).2,
instance_count: self.instance_count,
bind_group_layout,
}
}
}
pub trait Scene: Sized {
fn setup(renderer_context: &renderer::RendererContext, resources: &mut GpuResources) -> Self;
fn setup(
context: &renderer::RendererContext,
resources: &mut GpuResources,
data: &mut RenderData,
) -> Self;
fn bind_groups(&self) -> &[wgpu::BindGroup];
fn meshes(&self) -> &[Mesh];
fn handle_mouse_click(&mut self, x: f32, y: f32);
fn handle_zoom(&mut self, delta_y: f32);
fn handle_orbit(&mut self, delta_x: f32, delta_y: f32);
fn clear(&mut self);
fn add_mesh(&mut self, mesh: Mesh);
fn handle_orbit(&mut self, dx: f32, dy: f32);
fn set_camera_depth_range(&mut self, near: f32, far: f32);
fn set_camera_look_at(&mut self, eye: ultraviolet::Vec3, center: ultraviolet::Vec3);
fn frame_metadata_mut(&mut self) -> Option<&mut FrameMetadata> {
None
}
fn camera_mut(&mut self) -> Option<&mut Camera> {
None
}
fn uniform_buffers(&self) -> Option<&[wgpu::Buffer]> {
fn uniform_buffers(&self) -> Option<[&wgpu::Buffer; 2]> {
None
}
fn resize(&mut self, width: f64, height: f64, _scale_factor: f64, queue: &wgpu::Queue) {
let fm_copy = if let Some(fm) = self.frame_metadata_mut() {
let dimension = ultraviolet::Vec2::new(width as f32, height as f32);
fm.update_dimension(dimension);
*fm
} else {
return;
};
let view_proj_copy = if let Some(cam) = self.camera_mut() {
cam.update_aspect_ratio(width as f32 / height as f32);
cam.view_proj
} else {
return;
};
if let Some(buffers) = self.uniform_buffers() {
if buffers.len() >= 2 {
queue.write_buffer(&buffers[0], 0, bytemuck::cast_slice(&[fm_copy]));
queue.write_buffer(&buffers[1], 0, bytemuck::cast_slice(&[view_proj_copy]));
}
fn resize(&mut self, width: f64, height: f64, _: f64, queue: &wgpu::Queue) {
if let Some(f) = self.frame_metadata_mut() {
f.update_dimension(ultraviolet::Vec2::new(width as f32, height as f32));
}
if let Some(c) = self.camera_mut() {
c.update_aspect_ratio(width as f32 / height as f32)
}
self.write_uniforms(queue);
}
fn update(
&mut self,
renderer_context: &renderer::RendererContext,
_resources: &mut GpuResources,
) {
let camera_position = if let Some(cam) = self.camera_mut() {
cam.position()
} else {
return;
fn update(&mut self, context: &renderer::RendererContext) {
let position = match self.camera_mut() {
Some(c) => c.position(),
None => return,
};
let fm_copy = if let Some(fm) = self.frame_metadata_mut() {
let time = (js_sys::Date::now() as f32) * 0.001;
fm.time = time;
fm.set_camera_position(camera_position);
*fm
} else {
return;
};
let view_proj_copy = if let Some(cam) = self.camera_mut() {
cam.view_proj
} else {
return;
};
if let Some(buffers) = self.uniform_buffers() {
if buffers.len() >= 2 {
renderer_context.queue.write_buffer(
&buffers[0],
0,
bytemuck::cast_slice(&[fm_copy]),
);
renderer_context.queue.write_buffer(
&buffers[1],
0,
bytemuck::cast_slice(&[view_proj_copy]),
);
}
if let Some(f) = self.frame_metadata_mut() {
f.time = js_sys::Date::now() as f32 * 0.001;
f.set_camera_position(position)
}
self.write_uniforms(&context.queue);
}
fn write_uniforms(&mut self, queue: &wgpu::Queue) {
let frame = self.frame_metadata_mut().copied();
let view = self.camera_mut().map(|c| c.view_proj);
if let (Some(f), Some(v), Some([frame_buffer, camera_buffer])) =
(frame, view, self.uniform_buffers())
{
queue.write_buffer(frame_buffer, 0, bytemuck::bytes_of(&f));
queue.write_buffer(camera_buffer, 0, bytemuck::bytes_of(&v));
}
}
}
+420
View File
@@ -0,0 +1,420 @@
//! Triple-buffered, immutable packed scene snapshot shared with JavaScript.
use std::sync::atomic::{AtomicU32, Ordering};
use crate::render_data::{affine_world_aabb, RenderData, RenderFlags};
pub const MAGIC: u32 = u32::from_le_bytes(*b"YSNP");
pub const BLOB_MAGIC: u32 = u32::from_le_bytes(*b"RDS1");
pub const CONTROL_VERSION: u32 = 1;
pub const SCHEMA: u32 = 1;
pub const SLOT_COUNT: usize = 3;
pub const INIT: u32 = 0;
pub const OPEN: u32 = 1;
pub const FAILED: u32 = 2;
pub const CLOSED: u32 = 3;
pub const FREE: u32 = 0;
pub const WRITING: u32 = 1;
pub const READY: u32 = 2;
pub const READING: u32 = 3;
pub const ERROR_NO_SLOT: u32 = 1;
pub const ERROR_OVERFLOW: u32 = 2;
pub const ERROR_INVARIANT: u32 = 3;
pub const ERROR_PUBLICATION: u32 = 4;
const CONTROL_BYTES: u32 = 256;
const SLOT_BYTES: u32 = 64;
const SNAPSHOT_HEADER_BYTES: usize = 64;
const DATA_OFFSET: usize = 512;
const DESCRIPTOR_BYTES: usize = 32;
const STREAMS: usize = 14;
const SCHEMA_FLAGS: u32 = 3; // dense arrays | affine transforms
#[repr(C, align(64))]
pub struct SnapshotDescriptor(pub [AtomicU32; 16]);
#[repr(C, align(64))]
pub struct SnapshotControl {
pub header: [AtomicU32; 16],
pub slots: [SnapshotDescriptor; SLOT_COUNT],
}
#[repr(C, align(16))]
#[derive(Clone, Copy)]
pub struct SnapshotBlock(pub [u8; 16]);
pub struct SharedSnapshot {
pub control: Box<SnapshotControl>,
blocks: [Vec<SnapshotBlock>; SLOT_COUNT],
last_revision: Option<u64>,
next_epoch: u32,
layout_epoch: u32,
}
impl SharedSnapshot {
pub fn new() -> Self {
let control = Box::new(SnapshotControl {
header: std::array::from_fn(|_| AtomicU32::new(0)),
slots: std::array::from_fn(|_| {
SnapshotDescriptor(std::array::from_fn(|_| AtomicU32::new(0)))
}),
});
let this = Self {
control,
blocks: Default::default(),
last_revision: None,
next_epoch: 1,
layout_epoch: 0,
};
for (i, value) in [
MAGIC,
CONTROL_VERSION,
CONTROL_BYTES,
SLOT_COUNT as u32,
SLOT_BYTES,
SCHEMA,
INIT,
]
.into_iter()
.enumerate()
{
this.control.header[i].store(value, Ordering::Relaxed);
}
// No publication exists yet; zero is a valid slot number.
this.control.header[9].store(u32::MAX, Ordering::Relaxed);
this
}
pub fn control_ptr(&self) -> u32 {
self.control.as_ref() as *const _ as usize as u32
}
/// Permanently fails snapshot publication without affecting rendering or mutations.
pub fn fail(&self, error: u32) {
self.control.header[14].store(error, Ordering::Relaxed);
self.control.header[6].store(FAILED, Ordering::Release);
}
/// Packs and publishes if `data` changed. Returns the newly published data epoch.
pub fn publish(&mut self, data: &RenderData) -> Result<Option<u32>, u32> {
if self.control.header[6].load(Ordering::Acquire) == FAILED {
return Err(self.control.header[14].load(Ordering::Relaxed));
}
if self.last_revision == Some(data.revision()) {
return Ok(None);
}
let slot = match self.claim_slot() {
Some(slot) => slot,
None => {
self.fail(ERROR_NO_SLOT);
return Err(ERROR_NO_SLOT);
}
};
let result = self.publish_claimed(slot, data);
if let Err(error) = result {
self.control.slots[slot].0[0].store(FREE, Ordering::Release);
self.fail(error);
}
result
}
fn publish_claimed(&mut self, slot: usize, data: &RenderData) -> Result<Option<u32>, u32> {
let epoch = self.next_epoch;
let next_epoch = epoch.checked_add(1).ok_or(ERROR_OVERFLOW)?;
let bytes = pack(data, epoch)?;
let blocks = bytes.len().checked_add(15).ok_or(ERROR_OVERFLOW)? / 16;
if self.blocks[slot].capacity() < blocks {
self.layout_epoch = self.layout_epoch.checked_add(1).ok_or(ERROR_OVERFLOW)?;
}
self.blocks[slot].resize(blocks, SnapshotBlock([0; 16]));
let allocation_bytes = blocks.checked_mul(16).ok_or(ERROR_OVERFLOW)?;
let target = unsafe {
std::slice::from_raw_parts_mut(
self.blocks[slot].as_mut_ptr().cast::<u8>(),
allocation_bytes,
)
};
target[..bytes.len()].copy_from_slice(&bytes);
let ptr = self.blocks[slot].as_ptr() as usize;
let ptr32 = u32::try_from(ptr).map_err(|_| ERROR_OVERFLOW)?;
let length = u32::try_from(bytes.len()).map_err(|_| ERROR_OVERFLOW)?;
let revision = data.revision();
let d = &self.control.slots[slot].0;
let values = [
epoch,
self.layout_epoch,
ptr32,
length,
revision as u32,
(revision >> 32) as u32,
data.mesh_count(),
data.instance_count(),
SCHEMA,
SNAPSHOT_HEADER_BYTES as u32,
0,
0,
0,
0,
0,
];
for (i, value) in values.into_iter().enumerate() {
d[i + 1].store(value, Ordering::Relaxed);
}
let end = ptr.checked_add(allocation_bytes).ok_or(ERROR_OVERFLOW)?;
let pages = wasm_pages(end)?;
let seq = self.open_sequence()?;
d[0].store(READY, Ordering::Release);
for (i, value) in [
epoch,
slot as u32,
revision as u32,
(revision >> 32) as u32,
pages,
self.layout_epoch,
]
.into_iter()
.enumerate()
{
self.control.header[8 + i].store(value, Ordering::Relaxed);
}
self.control.header[14].store(0, Ordering::Relaxed);
self.control.header[15].store(0, Ordering::Relaxed);
self.control.header[6].store(OPEN, Ordering::Relaxed);
self.control.header[7].fetch_add(1, Ordering::Release);
debug_assert_eq!(seq & 1, 0);
self.next_epoch = next_epoch;
self.last_revision = Some(revision);
Ok(Some(epoch))
}
fn open_sequence(&self) -> Result<u32, u32> {
loop {
let seq = self.control.header[7].load(Ordering::Acquire);
if seq & 1 != 0 {
std::hint::spin_loop();
continue;
}
match self.control.header[7].compare_exchange_weak(
seq,
seq.checked_add(1).ok_or(ERROR_OVERFLOW)?,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return Ok(seq),
Err(_) => continue,
}
}
}
fn claim_slot(&self) -> Option<usize> {
loop {
for slot in 0..SLOT_COUNT {
if self.control.slots[slot].0[0]
.compare_exchange(FREE, WRITING, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return Some(slot);
}
}
let oldest = (0..SLOT_COUNT)
.filter(|&slot| self.control.slots[slot].0[0].load(Ordering::Acquire) == READY)
.min_by_key(|&slot| self.control.slots[slot].0[1].load(Ordering::Relaxed));
let slot = oldest?;
if self.control.slots[slot].0[0]
.compare_exchange(READY, WRITING, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return Some(slot);
}
// A reader or another claimant won. Recompute rather than touching READING.
}
}
}
fn align16(value: usize) -> Result<usize, u32> {
Ok(value.checked_add(15).ok_or(ERROR_OVERFLOW)? & !15)
}
#[cfg(target_arch = "wasm32")]
fn wasm_pages(_minimum_end: usize) -> Result<u32, u32> {
u32::try_from(core::arch::wasm32::memory_size(0)).map_err(|_| ERROR_OVERFLOW)
}
#[cfg(not(target_arch = "wasm32"))]
fn wasm_pages(minimum_end: usize) -> Result<u32, u32> {
u32::try_from(minimum_end.checked_add(65535).ok_or(ERROR_OVERFLOW)? / 65536)
.map_err(|_| ERROR_OVERFLOW)
}
fn pack(data: &RenderData, epoch: u32) -> Result<Vec<u8>, u32> {
let meshes: Vec<_> = data.meshes().collect();
let instances: Vec<_> = data.instances().collect();
let strides = [4usize, 4, 4, 12, 12, 4, 4, 4, 4, 4, 64, 12, 12, 4];
let components = [1u32, 1, 1, 3, 3, 1, 1, 1, 1, 1, 16, 3, 3, 1];
let scalar = [1u32, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 1];
let counts = [meshes.len(); 5]
.into_iter()
.chain([instances.len(); 9])
.collect::<Vec<_>>();
let mut offsets = [0usize; STREAMS];
let mut cursor = DATA_OFFSET;
for i in 0..STREAMS {
offsets[i] = cursor;
let bytes = strides[i].checked_mul(counts[i]).ok_or(ERROR_OVERFLOW)?;
cursor = align16(cursor.checked_add(bytes).ok_or(ERROR_OVERFLOW)?)?;
}
let total = u32::try_from(cursor).map_err(|_| ERROR_OVERFLOW)?;
let mesh_count = u32::try_from(meshes.len()).map_err(|_| ERROR_OVERFLOW)?;
let instance_count = u32::try_from(instances.len()).map_err(|_| ERROR_OVERFLOW)?;
let mut out = vec![0u8; cursor];
let put32 = |out: &mut [u8], at: usize, value: u32| {
out[at..at + 4].copy_from_slice(&value.to_le_bytes())
};
let revision = data.revision();
for (i, value) in [
BLOB_MAGIC,
SCHEMA,
SNAPSHOT_HEADER_BYTES as u32,
total,
epoch,
revision as u32,
(revision >> 32) as u32,
STREAMS as u32,
SNAPSHOT_HEADER_BYTES as u32,
DESCRIPTOR_BYTES as u32,
mesh_count,
instance_count,
0x0102_0304,
SCHEMA_FLAGS,
0,
0,
]
.into_iter()
.enumerate()
{
put32(&mut out, i * 4, value);
}
for i in 0..STREAMS {
let at = SNAPSHOT_HEADER_BYTES + i * DESCRIPTOR_BYTES;
for (j, value) in [
i as u32 + 1,
scalar[i],
offsets[i] as u32,
counts[i] as u32,
components[i],
strides[i] as u32,
4,
0,
]
.into_iter()
.enumerate()
{
put32(&mut out, at + j * 4, value);
}
}
for (dense, (handle, mesh)) in meshes.iter().enumerate() {
for (i, value) in [handle.slot(), handle.generation(), mesh.flags.bits()]
.into_iter()
.enumerate()
{
put32(&mut out, offsets[i] + dense * 4, value);
}
for i in 0..3 {
put32(
&mut out,
offsets[3] + dense * 12 + i * 4,
mesh.aabb.min[i].to_bits(),
);
put32(
&mut out,
offsets[4] + dense * 12 + i * 4,
mesh.aabb.max[i].to_bits(),
);
}
}
for (dense, (handle, instance)) in instances.iter().enumerate() {
let mesh = data.mesh(instance.mesh).ok_or(ERROR_INVARIANT)?;
let world = affine_world_aabb(mesh.aabb, instance.model).map_err(|_| ERROR_INVARIANT)?;
for (i, value) in [
handle.slot(),
handle.generation(),
instance.mesh.slot(),
instance.mesh.generation(),
instance.flags.bits(),
]
.into_iter()
.enumerate()
{
put32(&mut out, offsets[5 + i] + dense * 4, value);
}
for i in 0..16 {
put32(
&mut out,
offsets[10] + dense * 64 + i * 4,
instance.model[i / 4][i % 4].to_bits(),
);
}
for i in 0..3 {
put32(
&mut out,
offsets[11] + dense * 12 + i * 4,
world.min[i].to_bits(),
);
put32(
&mut out,
offsets[12] + dense * 12 + i * 4,
world.max[i].to_bits(),
);
}
put32(
&mut out,
offsets[13] + dense * 4,
(mesh.flags.contains(RenderFlags::VISIBLE)
&& instance.flags.contains(RenderFlags::VISIBLE)) as u32,
);
}
Ok(out)
}
impl Drop for SharedSnapshot {
fn drop(&mut self) {
if self.control.header[6].load(Ordering::Acquire) != FAILED {
self.control.header[6].store(CLOSED, Ordering::Release);
}
}
}
const _: [(); 256] = [(); std::mem::size_of::<SnapshotControl>()];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_control_layout_and_initial_values() {
assert_eq!(std::mem::size_of::<SnapshotControl>(), 256);
assert_eq!(std::mem::size_of::<SnapshotDescriptor>(), 64);
let snapshot = SharedSnapshot::new();
let values: Vec<_> = snapshot
.control
.header
.iter()
.map(|v| v.load(Ordering::Relaxed))
.collect();
assert_eq!(&values[..7], &[MAGIC, 1, 256, 3, 64, 1, INIT]);
assert_eq!(values[9], u32::MAX);
}
#[test]
fn claiming_never_overwrites_reading_and_prefers_free() {
let snapshot = SharedSnapshot::new();
snapshot.control.slots[0].0[0].store(READING, Ordering::Relaxed);
assert_eq!(snapshot.claim_slot(), Some(1));
assert_eq!(
snapshot.control.slots[0].0[0].load(Ordering::Relaxed),
READING
);
assert_eq!(
snapshot.control.slots[1].0[0].load(Ordering::Relaxed),
WRITING
);
}
}