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:
+2
-1
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'-'))
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+801
-244
File diff suppressed because it is too large
Load Diff
+47
-308
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user