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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user