use crate::components::{
File, FileKind, InstancedProp, InstancedPropGeometry, ProceduralMesh, Room, SubMeshRef,
VoxelChunk,
};
use crate::ecs::PipelineContext;
use crate::ecs::asset_id::AssetId;
use crate::ecs::{MaterialHandle, MeshHandle, TextureHandle};
use crate::gfx::material_entry::{MaterialEntry, resolve_material_slots};
use crate::gfx::mesh_payload::Vertex;
use crate::gfx::render_types::{
DrawObject, InstancedCluster, LodSlice, MaterialUniforms, NO_NORMAL_MAP_SLOT,
};
pub(crate) use crate::gfx::transform::IDENTITY as IDENTITY4;
pub(crate) type RoomGeometry = (Room, Vec<Vertex>, Vec<u16>, Vec<(f32, Vec<u16>)>);
pub(crate) type MeshGeometryMaps = (
Vec<LoadedMesh>,
std::collections::HashMap<usize, MeshSourceMeta>,
std::collections::HashSet<usize>,
std::collections::HashMap<AssetId, usize>,
std::collections::HashMap<usize, DeferredMeshSeed>,
);
pub(crate) struct DeferredMeshSeed {
pub locator: crate::ecs::PayloadLocator,
pub bytes: Option<Vec<u8>>,
}
#[derive(Default)]
pub(crate) struct DeferredMeshSources {
pub(crate) by_handle: std::collections::HashSet<u32>,
pub(crate) by_def: std::collections::HashSet<AssetId>,
pub bounds: std::collections::HashMap<u32, ([f32; 3], [f32; 3])>,
pub counts: std::collections::HashMap<u32, (u32, u32)>,
}
impl DeferredMeshSources {
fn resource_bounds(&self, handle: usize) -> Option<([f32; 3], [f32; 3])> {
if !self.by_handle.contains(&(handle as u32)) {
return None;
}
self.bounds.get(&(handle as u32)).copied()
}
fn def_bounds(&self, id: AssetId, handle: usize) -> Option<([f32; 3], [f32; 3])> {
if !self.by_def.contains(&id) {
return None;
}
self.bounds.get(&(handle as u32)).copied()
}
}
pub(crate) struct DrawListData {
pub vertices: Vec<Vertex>,
pub indices: Vec<u32>,
pub(crate) draw_objects: Vec<DrawObject>,
pub(crate) instanced_clusters: Vec<InstancedCluster>,
pub(crate) prop_draw_indices: Vec<Vec<usize>>,
pub(crate) mesh_handle_to_draws: std::collections::HashMap<usize, Vec<usize>>,
pub(crate) prop_local_bounds: Vec<([f32; 3], [f32; 3])>,
}
type AppendedMesh = (
usize,
usize,
usize,
usize,
Vec<LodSlice>,
[f32; 3],
[f32; 3],
);
const UNCULLED_BB: ([f32; 3], [f32; 3]) = (
[f32::NAN, f32::NAN, f32::NAN],
[f32::NAN, f32::NAN, f32::NAN],
);
fn local_bounds(verts: &[Vertex]) -> ([f32; 3], [f32; 3]) {
if verts.is_empty() {
return UNCULLED_BB;
}
let mut mn = [f32::INFINITY; 3];
let mut mx = [f32::NEG_INFINITY; 3];
for v in verts {
for i in 0..3 {
mn[i] = mn[i].min(v.pos[i]);
mx[i] = mx[i].max(v.pos[i]);
}
}
(mn, mx)
}
#[derive(Debug, PartialEq)]
pub(crate) struct RenderableItem {
pub asset_id: AssetId,
pub model: Option<AssetId>,
pub mesh: Option<MeshHandle>,
pub material: Option<MaterialHandle>,
pub texture: Option<TextureHandle>,
pub cull_distance: f32,
pub(crate) is_dynamic: bool,
}
pub(crate) fn decomposed_renderable_item(
ctx: &crate::ecs::PipelineContext,
entity: crate::ecs::Entity,
asset_id: AssetId,
) -> RenderableItem {
use crate::components::{Collider, Interactable, MeshRenderer, ModelRenderer, Parent, Pickup};
let (model, mesh, material, texture, cull_distance) =
if let Some(m) = ctx.get::<ModelRenderer>(entity) {
(Some(m.model), None, None, None, m.cull_distance)
} else if let Some(m) = ctx.get::<MeshRenderer>(entity) {
(None, m.mesh, m.material, m.texture, m.cull_distance)
} else {
(None, None, None, None, 0.0)
};
let is_dynamic = ctx.get::<Pickup>(entity).is_some()
|| ctx.get::<Interactable>(entity).is_some()
|| ctx.get::<Parent>(entity).is_some()
|| ctx.get::<Collider>(entity).is_some();
RenderableItem {
asset_id,
model,
mesh,
material,
texture,
cull_distance,
is_dynamic,
}
}
pub(crate) fn append_lod_slices<T>(
indices: &mut Vec<T>,
alternates: &[(f32, Vec<u16>)],
base: T,
) -> Vec<LodSlice>
where
T: Copy + From<u16> + std::ops::Add<Output = T>,
{
let mut slices = Vec::with_capacity(alternates.len());
for (switch_distance, alt) in alternates {
let index_offset = indices.len();
indices.extend(alt.iter().map(|&i| T::from(i) + base));
slices.push(LodSlice {
index_offset,
index_count: alt.len(),
switch_distance: *switch_distance,
});
}
slices
}
pub(crate) struct LoadedMesh {
pub vertices: Vec<Vertex>,
pub indices: Vec<u16>,
pub lod_alternates: Vec<(f32, Vec<u16>)>,
pub bounds: Option<([f32; 3], [f32; 3])>,
pub counts: Option<(u32, u32)>,
}
pub(crate) struct MeshSourceMeta {
pub source: String,
pub primitive_index: u32,
pub lod_levels: u32,
pub lod_distances: Vec<f32>,
}
pub(crate) fn load_mesh_geometry(
ctx: &mut PipelineContext,
deferred: &DeferredMeshSources,
blob_disk_backed: bool,
) -> Option<MeshGeometryMaps> {
let mut deferred_payloads: std::collections::HashMap<usize, DeferredMeshSeed> =
std::collections::HashMap::new();
let mesh_table = ctx
.resource::<crate::resource::MeshTable>()
.cloned()
.unwrap_or_default();
let capture_sources = crate::app::dev_flags::enabled();
let mut mesh_sources: std::collections::HashMap<usize, MeshSourceMeta> =
std::collections::HashMap::new();
if capture_sources && let Some(sources) = ctx.resource::<crate::resource::MeshSources>() {
for (handle, info) in sources.0.iter().enumerate() {
if !info.source.is_empty() {
mesh_sources.insert(
handle,
MeshSourceMeta {
source: info.source.clone(),
primitive_index: info.primitive_index,
lod_levels: info.lod_levels,
lod_distances: info.lod_distances.clone(),
},
);
}
}
}
let (proc_meshes, baked_meshes): (Vec<ProceduralMesh>, Vec<ProceduralMesh>) = ctx
.query::<ProceduralMesh>()
.cloned()
.partition(|m| m.locator.is_some());
let baked_payloads = ctx
.resource::<concinnity_core::resource::RuntimeMeshPayloads>()
.cloned()
.unwrap_or_default();
for mesh in &baked_meshes {
if baked_payloads.get(mesh.asset_id).is_none() {
tracing::error!(
"GraphicsSystem: ProceduralMesh {} was baked at start but left no payload",
mesh.asset_id
);
return None;
}
}
let voxel_chunks = ctx.drain::<VoxelChunk>();
let file_assets = ctx.drain::<File>();
let file_meshes: Vec<&File> = file_assets
.iter()
.filter(|f| f.kind.as_ref().map(FileKind::is_mesh).unwrap_or(false))
.collect();
if mesh_table.is_empty()
&& proc_meshes.is_empty()
&& baked_payloads.is_empty()
&& voxel_chunks.is_empty()
&& file_meshes.is_empty()
{
tracing::info!(
"GraphicsSystem: no Mesh, ProceduralMesh, VoxelChunk, or mesh-kind File sources found"
);
}
let mut geometry: Vec<LoadedMesh> = Vec::new();
let mut component_mesh_handles: std::collections::HashMap<AssetId, usize> =
std::collections::HashMap::new();
for (handle, entry) in mesh_table.0.iter().enumerate() {
let locator = match &entry.payload {
Some(l) => l,
None => {
tracing::error!(
"GraphicsSystem: Mesh handle {} has no compiled payload -- did the build succeed?",
handle
);
return None;
}
};
if let Some(bounds) = deferred.resource_bounds(handle) {
let bytes = if blob_disk_backed {
None
} else {
match ctx.read_payload(locator) {
Ok(b) => Some(b.to_vec()),
Err(e) => {
tracing::error!("GraphicsSystem: failed to read Mesh payload: {:?}", e);
return None;
}
}
};
deferred_payloads.insert(
handle,
DeferredMeshSeed {
locator: locator.clone(),
bytes,
},
);
geometry.push(LoadedMesh {
vertices: Vec::new(),
indices: Vec::new(),
lod_alternates: Vec::new(),
bounds: Some(bounds),
counts: deferred.counts.get(&(handle as u32)).copied(),
});
continue;
}
let bytes = match ctx.read_payload(locator) {
Ok(b) => b.to_vec(),
Err(e) => {
tracing::error!("GraphicsSystem: failed to read Mesh payload: {:?}", e);
return None;
}
};
match crate::gfx::mesh_payload::deserialise_with_lods(&bytes) {
Ok((verts, idxs, alternates)) => geometry.push(LoadedMesh {
vertices: verts,
indices: idxs,
lod_alternates: alternates,
bounds: None,
counts: None,
}),
Err(e) => {
tracing::error!("GraphicsSystem: malformed Mesh payload: {}", e);
return None;
}
}
}
macro_rules! load_meshes {
($label:expr_2021, $items:expr_2021) => {
for (i, mesh) in $items.iter().enumerate() {
let locator = match &mesh.locator {
Some(l) => l,
None => {
tracing::error!(
"GraphicsSystem: {}[{}] {} has no compiled payload",
$label,
i,
mesh.asset_id
);
return None;
}
};
if let Some(bounds) = deferred.def_bounds(mesh.asset_id, geometry.len()) {
let bytes = if blob_disk_backed {
None
} else {
match ctx.read_payload(locator) {
Ok(b) => Some(b.to_vec()),
Err(e) => {
tracing::error!(
"GraphicsSystem: failed to read {} payload: {:?}",
$label,
e
);
return None;
}
}
};
deferred_payloads.insert(
geometry.len(),
DeferredMeshSeed {
locator: locator.clone(),
bytes,
},
);
component_mesh_handles.insert(mesh.asset_id, geometry.len());
let counts = deferred.counts.get(&(geometry.len() as u32)).copied();
geometry.push(LoadedMesh {
vertices: Vec::new(),
indices: Vec::new(),
lod_alternates: Vec::new(),
bounds: Some(bounds),
counts,
});
continue;
}
let bytes = match ctx.read_payload(locator) {
Ok(b) => b.to_vec(),
Err(e) => {
tracing::error!(
"GraphicsSystem: failed to read {} payload: {:?}",
$label,
e
);
return None;
}
};
match crate::gfx::mesh_payload::deserialise_with_lods(&bytes) {
Ok((verts, idxs, alternates)) => {
component_mesh_handles.insert(mesh.asset_id, geometry.len());
geometry.push(LoadedMesh {
vertices: verts,
indices: idxs,
lod_alternates: alternates,
bounds: None,
counts: None,
});
}
Err(e) => {
tracing::error!("GraphicsSystem: malformed {} payload: {}", $label, e);
return None;
}
}
}
};
}
load_meshes!("ProceduralMesh", proc_meshes);
load_meshes!("VoxelChunk", voxel_chunks);
load_meshes!("File", file_meshes);
for (id, bytes) in baked_payloads.iter() {
match crate::gfx::mesh_payload::deserialise_with_lods(bytes) {
Ok((verts, idxs, alternates)) => {
component_mesh_handles.insert(id, geometry.len());
geometry.push(LoadedMesh {
vertices: verts,
indices: idxs,
lod_alternates: alternates,
bounds: None,
counts: None,
});
}
Err(e) => {
tracing::error!("GraphicsSystem: malformed baked mesh payload {}: {}", id, e);
return None;
}
}
}
let always_resident_meshes: std::collections::HashSet<usize> = proc_meshes
.iter()
.chain(&baked_meshes)
.filter(|pm| pm.generator == "skybox")
.filter_map(|pm| component_mesh_handles.get(&pm.asset_id).copied())
.collect();
Some((
geometry,
mesh_sources,
always_resident_meshes,
component_mesh_handles,
deferred_payloads,
))
}
pub(crate) fn load_room_geometry(
ctx: &mut PipelineContext,
) -> Option<(Vec<RoomGeometry>, Vec<u32>)> {
let rooms = ctx.drain::<Room>();
let mut room_geometry: Vec<RoomGeometry> = Vec::new();
let mut blob_indices: Vec<u32> = Vec::new();
for (i, room) in rooms.into_iter().enumerate() {
let locator = match &room.locator {
Some(l) => l.clone(),
None => {
tracing::error!(
"GraphicsSystem: Room[{}] {} has no compiled payload -- did the build succeed?",
i,
room.asset_id
);
return None;
}
};
blob_indices.push(locator.blob_index);
let bytes = match ctx.read_payload(&locator) {
Ok(b) => b.to_vec(),
Err(e) => {
tracing::error!(
"GraphicsSystem: failed to read Room {} payload: {:?}",
room.asset_id,
e
);
return None;
}
};
match crate::gfx::mesh_payload::deserialise_with_lods(&bytes) {
Ok((verts, idxs, alternates)) => room_geometry.push((room, verts, idxs, alternates)),
Err(e) => {
tracing::error!("GraphicsSystem: malformed Room payload: {}", e);
return None;
}
}
}
Some((room_geometry, blob_indices))
}
pub(crate) struct DrawListInputs<'a> {
pub items: &'a [RenderableItem],
pub instanced_props: &'a [InstancedProp],
pub world_mats: &'a [[[f32; 4]; 4]],
pub model_map: &'a std::collections::HashMap<AssetId, Vec<SubMeshRef>>,
pub mesh_geometry: &'a [LoadedMesh],
pub room_geometry: &'a [RoomGeometry],
pub texture_count: usize,
pub material_map: &'a std::collections::HashMap<MaterialHandle, MaterialEntry>,
pub always_resident_meshes: &'a std::collections::HashSet<usize>,
}
pub(crate) fn build_draw_list(inputs: DrawListInputs) -> Option<DrawListData> {
let DrawListInputs {
items,
instanced_props,
world_mats,
model_map,
mesh_geometry,
room_geometry,
texture_count,
material_map,
always_resident_meshes,
} = inputs;
let mut all_vertices: Vec<Vertex> = Vec::new();
let mut all_indices: Vec<u32> = Vec::new();
let mut draw_objects: Vec<DrawObject> = Vec::new();
let mut instanced_clusters: Vec<InstancedCluster> = Vec::new();
let mut prop_draw_indices: Vec<Vec<usize>> = Vec::new();
let mut prop_local_bounds: Vec<([f32; 3], [f32; 3])> = Vec::new();
let mut mesh_handle_to_draws: std::collections::HashMap<usize, Vec<usize>> =
std::collections::HashMap::new();
let mut referenced: std::collections::HashSet<usize> = std::collections::HashSet::new();
for item in items {
if let Some(mesh) = item.mesh {
referenced.insert(mesh.index());
}
if let Some(model_id) = item.model
&& let Some(submeshes) = model_map.get(&model_id)
{
for sub in submeshes {
if let Some(sub_mesh) = sub.mesh {
referenced.insert(sub_mesh.index());
}
}
}
}
for inst in instanced_props {
if let Some(mesh) = inst.mesh {
referenced.insert(mesh.index());
}
}
let mut append_mesh = |handle: usize| -> Option<AppendedMesh> {
let loaded = mesh_geometry.get(handle)?;
let vertex_byte_offset = all_vertices.len() * std::mem::size_of::<Vertex>();
let index_elem_offset = all_indices.len();
let base = all_vertices.len() as u32;
let (bb_min, bb_max) = loaded
.bounds
.unwrap_or_else(|| local_bounds(&loaded.vertices));
all_vertices.extend_from_slice(&loaded.vertices);
all_indices.extend(loaded.indices.iter().map(|i| u32::from(*i) + base));
let lod_slices = append_lod_slices(&mut all_indices, &loaded.lod_alternates, base);
let (vertex_count, index_count) = loaded
.counts
.map(|(v, i)| (v as usize, i as usize))
.unwrap_or((loaded.vertices.len(), loaded.indices.len()));
Some((
vertex_byte_offset,
vertex_count,
index_elem_offset,
index_count,
lod_slices,
bb_min,
bb_max,
))
};
for (item_idx, item) in items.iter().enumerate() {
let model_mat = world_mats[item_idx];
let mut prop_idxs: Vec<usize> = Vec::new();
let mut prop_min = [f32::INFINITY; 3];
let mut prop_max = [f32::NEG_INFINITY; 3];
let mut union_local = |mn: [f32; 3], mx: [f32; 3]| {
for i in 0..3 {
prop_min[i] = prop_min[i].min(mn[i]);
prop_max[i] = prop_max[i].max(mx[i]);
}
};
if let Some(model_id) = item.model {
let submeshes = match model_map.get(&model_id) {
Some(s) => s,
None => {
tracing::error!(
"GraphicsSystem: Prop {} references unknown model {} -- add a Model asset with that id",
item.asset_id,
model_id
);
return None;
}
};
for sub in submeshes {
let sub_mesh = match sub.mesh {
Some(m) => m.index(),
None => {
tracing::error!(
"GraphicsSystem: Model {} has a sub-mesh with no mesh",
model_id
);
return None;
}
};
let (
vertex_offset,
vertex_count,
index_offset,
index_count,
lod_alternates,
local_min,
local_max,
) = match append_mesh(sub_mesh) {
Some(t) => t,
None => {
tracing::error!(
"GraphicsSystem: Model {} sub-mesh handle {} out of range -- add a Mesh or ProceduralMesh asset with that name",
model_id,
sub_mesh
);
return None;
}
};
let mat_entry =
match resolve_material_slots(sub.material, None, material_map, texture_count) {
Ok(entry) => entry,
Err(mat_id) => {
tracing::error!(
"GraphicsSystem: Model {} sub-mesh material {} not found",
model_id,
mat_id.index()
);
return None;
}
};
let (bb_min, bb_max) =
if item.is_dynamic || always_resident_meshes.contains(&sub_mesh) {
UNCULLED_BB
} else {
crate::gfx::frustum::transform_aabb(local_min, local_max, model_mat)
};
union_local(local_min, local_max);
prop_idxs.push(draw_objects.len());
mesh_handle_to_draws
.entry(sub_mesh)
.or_default()
.push(draw_objects.len());
draw_objects.push(DrawObject {
vertex_offset,
vertex_count,
index_offset,
index_count,
base_vertex: 0,
geometry_generation: 0,
model: model_mat,
texture_slot: mat_entry.albedo_slot,
normal_map_slot: mat_entry.normal_map_slot,
material: mat_entry.uniforms,
shader_bucket: mat_entry.shader_bucket,
visible: true,
resident: true,
bb_min,
bb_max,
cull_distance: item.cull_distance,
lod_alternates,
});
}
} else {
let mesh_handle = match item.mesh {
Some(m) => m.index(),
None => {
tracing::error!(
"GraphicsSystem: Prop {} has neither a model nor a mesh",
item.asset_id
);
return None;
}
};
let (
vertex_offset,
vertex_count,
index_offset,
index_count,
lod_alternates,
local_min,
local_max,
) = match append_mesh(mesh_handle) {
Some(t) => t,
None => {
tracing::error!(
"GraphicsSystem: Prop {} references out-of-range mesh handle {} -- add a Mesh or ProceduralMesh asset with that name",
item.asset_id,
mesh_handle
);
return None;
}
};
let mat_entry = match resolve_material_slots(
item.material,
item.texture,
material_map,
texture_count,
) {
Ok(entry) => entry,
Err(mat_id) => {
tracing::error!(
"GraphicsSystem: Prop {} references unknown material {} -- add a Material asset with that id",
item.asset_id,
mat_id.index()
);
return None;
}
};
let (bb_min, bb_max) =
if item.is_dynamic || always_resident_meshes.contains(&mesh_handle) {
UNCULLED_BB
} else {
crate::gfx::frustum::transform_aabb(local_min, local_max, model_mat)
};
union_local(local_min, local_max);
prop_idxs.push(draw_objects.len());
mesh_handle_to_draws
.entry(mesh_handle)
.or_default()
.push(draw_objects.len());
draw_objects.push(DrawObject {
vertex_offset,
vertex_count,
index_offset,
index_count,
base_vertex: 0,
geometry_generation: 0,
model: model_mat,
texture_slot: mat_entry.albedo_slot,
normal_map_slot: mat_entry.normal_map_slot,
material: mat_entry.uniforms,
shader_bucket: mat_entry.shader_bucket,
visible: true,
resident: true,
bb_min,
bb_max,
cull_distance: item.cull_distance,
lod_alternates,
});
}
prop_draw_indices.push(prop_idxs);
let finite = prop_min
.iter()
.chain(prop_max.iter())
.all(|v| v.is_finite());
prop_local_bounds.push(if finite {
(prop_min, prop_max)
} else {
UNCULLED_BB
});
}
for inst in instanced_props {
let mesh_handle = match inst.mesh {
Some(m) if !inst.instances.is_empty() => m.index(),
_ => continue,
};
let (
vertex_offset,
vertex_count,
index_offset,
index_count,
lod_alternates,
local_min,
local_max,
) = match append_mesh(mesh_handle) {
Some(t) => t,
None => {
tracing::error!(
"GraphicsSystem: InstancedProp {} references out-of-range mesh handle {}",
inst.asset_id,
mesh_handle
);
return None;
}
};
let mat_entry = match resolve_material_slots(
inst.material,
inst.texture,
material_map,
texture_count,
) {
Ok(entry) => entry,
Err(mat_id) => {
tracing::error!(
"GraphicsSystem: InstancedProp {} references unknown material {}",
inst.asset_id,
mat_id.index()
);
return None;
}
};
let (texture_slot, normal_map_slot, material) = (
mat_entry.albedo_slot,
mat_entry.normal_map_slot,
mat_entry.uniforms,
);
let mut instance_mats: Vec<[[f32; 4]; 4]> = Vec::with_capacity(inst.instances.len());
let mut cluster_min = [f32::INFINITY; 3];
let mut cluster_max = [f32::NEG_INFINITY; 3];
for i in 0..inst.instances.len() {
let Some(model_mat) = inst.instance_model_matrix(i) else {
continue;
};
let (bb_min, bb_max) =
crate::gfx::frustum::transform_aabb(local_min, local_max, model_mat);
for k in 0..3 {
cluster_min[k] = cluster_min[k].min(bb_min[k]);
cluster_max[k] = cluster_max[k].max(bb_max[k]);
}
instance_mats.push(model_mat);
}
if instance_mats.is_empty() {
continue;
}
instanced_clusters.push(InstancedCluster {
vertex_offset,
vertex_count,
index_offset,
index_count,
texture_slot,
normal_map_slot,
material,
cluster_bb_min: cluster_min,
cluster_bb_max: cluster_max,
local_bb_min: local_min,
local_bb_max: local_max,
cull_distance: inst.cull_distance,
instances: instance_mats,
lod_alternates,
});
}
for mesh_handle in 0..mesh_geometry.len() {
if referenced.contains(&mesh_handle) {
continue;
}
if let Some((
vertex_offset,
vertex_count,
index_offset,
index_count,
lod_alternates,
_,
_,
)) = append_mesh(mesh_handle)
{
let _ = lod_alternates;
mesh_handle_to_draws
.entry(mesh_handle)
.or_default()
.push(draw_objects.len());
draw_objects.push(DrawObject {
vertex_offset,
vertex_count,
index_offset,
index_count,
base_vertex: 0,
geometry_generation: 0,
model: IDENTITY4,
texture_slot: 0,
normal_map_slot: NO_NORMAL_MAP_SLOT,
material: MaterialUniforms::DEFAULT,
shader_bucket: 0,
visible: true,
resident: true,
bb_min: UNCULLED_BB.0,
bb_max: UNCULLED_BB.1,
cull_distance: 0.0,
lod_alternates: Vec::new(),
});
}
}
for (room, verts, idxs, room_lods) in room_geometry {
let vertex_byte_offset = all_vertices.len() * std::mem::size_of::<Vertex>();
let index_elem_offset = all_indices.len();
let base = all_vertices.len() as u32;
all_vertices.extend_from_slice(verts);
all_indices.extend(idxs.iter().map(|i| u32::from(*i) + base));
let lod_slices = append_lod_slices(&mut all_indices, room_lods, base);
let texture_slot = match room.effective_texture() {
None => 0,
Some(handle) => {
let slot = handle.index();
if slot < texture_count { slot } else { 0 }
}
};
draw_objects.push(DrawObject {
vertex_offset: vertex_byte_offset,
vertex_count: verts.len(),
index_offset: index_elem_offset,
index_count: idxs.len(),
base_vertex: 0,
geometry_generation: 0,
model: IDENTITY4,
texture_slot,
normal_map_slot: NO_NORMAL_MAP_SLOT,
material: MaterialUniforms::DEFAULT,
shader_bucket: 0,
visible: true,
resident: true,
bb_min: UNCULLED_BB.0,
bb_max: UNCULLED_BB.1,
cull_distance: 0.0,
lod_alternates: lod_slices,
});
}
Some(DrawListData {
vertices: all_vertices,
indices: all_indices,
draw_objects,
instanced_clusters,
prop_draw_indices,
mesh_handle_to_draws,
prop_local_bounds,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::Prop;
use crate::ecs::TextureHandle;
use crate::gfx::render_types::NO_ALBEDO_SLOT;
fn make_prop(position: [f32; 3]) -> Prop {
Prop {
asset_id: AssetId::default(),
model: None,
mesh: None,
material: None,
texture: None,
position,
rotation_deg: [0.0, 0.0, 0.0],
scale: [1.0, 1.0, 1.0],
collider: None,
interactable: false,
pickup: false,
parent: None,
scene: None,
prefab: String::new(),
cull_distance: 0.0,
is_held: false,
}
}
fn unit_quad_mesh() -> LoadedMesh {
let mk = |x, y, z| Vertex {
pos: [x, y, z],
normal: [0.0, 1.0, 0.0],
tangent: [1.0, 0.0, 0.0],
color: [1.0, 1.0, 1.0],
uv: [0.0, 0.0],
};
let v = vec![
mk(-0.5, -0.5, -0.5),
mk(0.5, -0.5, -0.5),
mk(0.5, 0.5, -0.5),
mk(-0.5, 0.5, -0.5),
];
let i = vec![0u16, 1, 2, 0, 2, 3];
LoadedMesh {
vertices: v,
indices: i,
lod_alternates: Vec::new(),
bounds: None,
counts: None,
}
}
#[test]
fn build_draw_list_emits_one_cluster_for_instanced_prop() {
let mesh_geometry = vec![unit_quad_mesh()];
let inst = crate::components::InstancedProp {
asset_id: AssetId::default(),
mesh: Some(MeshHandle(0)),
material: None,
texture: None,
cull_distance: 0.0,
instances: vec![
crate::components::InstanceTransform {
position: [0.0, 0.0, 0.0],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
},
crate::components::InstanceTransform {
position: [5.0, 0.0, 0.0],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
},
crate::components::InstanceTransform {
position: [-3.0, 0.0, 2.0],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
},
],
};
let data = build_draw_list(DrawListInputs {
items: &[],
instanced_props: &[inst],
world_mats: &[],
model_map: &std::collections::HashMap::new(),
mesh_geometry: &mesh_geometry,
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
})
.expect("build_draw_list");
let DrawListData {
vertices: verts,
indices: idxs,
draw_objects,
instanced_clusters: clusters,
mesh_handle_to_draws,
..
} = data;
assert_eq!(verts.len(), 4);
assert_eq!(idxs.len(), 6);
assert!(mesh_handle_to_draws.is_empty());
assert!(draw_objects.is_empty());
assert_eq!(clusters.len(), 1);
let c = &clusters[0];
assert_eq!(c.instances.len(), 3);
assert_eq!(c.index_count, 6);
assert!((c.cluster_bb_min[0] - (-3.5)).abs() < 1e-5);
assert!((c.cluster_bb_max[0] - 5.5).abs() < 1e-5);
assert!((c.cluster_bb_min[1] - (-0.5)).abs() < 1e-5);
assert!((c.cluster_bb_max[1] - 0.5).abs() < 1e-5);
assert!((c.cluster_bb_min[2] - (-0.5)).abs() < 1e-5);
assert!((c.cluster_bb_max[2] - 1.5).abs() < 1e-5);
}
#[test]
fn build_draw_list_skips_empty_instanced_prop() {
let mesh_geometry = vec![unit_quad_mesh()];
let inst = crate::components::InstancedProp {
asset_id: AssetId::default(),
mesh: Some(MeshHandle(0)),
material: None,
texture: None,
cull_distance: 0.0,
instances: Vec::new(),
};
let data = build_draw_list(DrawListInputs {
items: &[],
instanced_props: &[inst],
world_mats: &[],
model_map: &std::collections::HashMap::new(),
mesh_geometry: &mesh_geometry,
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
})
.expect("build_draw_list");
let DrawListData {
draw_objects,
instanced_clusters: clusters,
..
} = data;
assert!(draw_objects.is_empty());
assert!(clusters.is_empty());
}
#[test]
fn always_resident_mesh_forces_uncullable_bb_on_static_prop() {
let mesh_geometry = vec![unit_quad_mesh()];
let items = vec![RenderableItem {
asset_id: AssetId(0),
model: None,
mesh: Some(MeshHandle(0)),
material: None,
texture: None,
cull_distance: 0.0,
is_dynamic: false,
}];
let world_mats = vec![IDENTITY4];
let mut always_resident = std::collections::HashSet::new();
always_resident.insert(0usize);
let data = build_draw_list(DrawListInputs {
items: &items,
instanced_props: &[],
world_mats: &world_mats,
model_map: &std::collections::HashMap::new(),
mesh_geometry: &mesh_geometry,
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &always_resident,
})
.expect("build_draw_list");
let DrawListData { draw_objects, .. } = data;
assert_eq!(draw_objects.len(), 1);
assert!(draw_objects[0].bb_min[0].is_nan());
assert!(draw_objects[0].bb_max[0].is_nan());
assert!(!draw_objects[0].cullable());
}
#[test]
fn decomposed_renderable_item_matches_a_mesh_prop() {
use crate::blob::BlobData;
use crate::components::{Collider, MeshRenderer, Pickup, PropCollider};
use crate::ecs::{ComponentStorage, PipelineContext, Resources};
use crate::gfx::profile::FrameProfile;
let mut prop = make_prop([0.0; 3]);
prop.asset_id = AssetId(7);
prop.mesh = Some(MeshHandle(10));
prop.material = Some(MaterialHandle(20));
prop.cull_distance = 50.0;
prop.pickup = true;
prop.collider = Some(PropCollider::default());
let mut components = ComponentStorage::default();
let mut blob = BlobData::empty();
let mut profile = FrameProfile::default();
let mut resources = Resources::new();
let scratch = crate::ecs::Arena::with_capacity(64 * 1024);
let mut ctx = PipelineContext {
components: &mut components,
blob: &mut blob,
profile: &mut profile,
resources: &mut resources,
frame: crate::ecs::FrameContext::new(&scratch),
};
let e = ctx.components.spawn();
ctx.insert(
e,
MeshRenderer {
mesh: prop.mesh,
material: prop.material,
texture: prop.texture,
cull_distance: prop.cull_distance,
},
);
ctx.insert(e, Pickup);
ctx.insert(e, Collider(prop.collider.clone().unwrap()));
let item = decomposed_renderable_item(&ctx, e, prop.asset_id);
assert_eq!(
item,
RenderableItem {
asset_id: AssetId(7),
model: None,
mesh: Some(MeshHandle(10)),
material: Some(MaterialHandle(20)),
texture: None,
cull_distance: 50.0,
is_dynamic: true,
}
);
}
fn mesh_item(mesh: AssetId) -> RenderableItem {
RenderableItem {
asset_id: mesh,
model: None,
mesh: Some(MeshHandle(mesh.0)),
material: None,
texture: None,
cull_distance: 0.0,
is_dynamic: false,
}
}
fn model_item(model: AssetId) -> RenderableItem {
RenderableItem {
asset_id: model,
model: Some(model),
mesh: None,
material: None,
texture: None,
cull_distance: 0.0,
is_dynamic: false,
}
}
#[test]
fn build_draw_list_model_emits_one_draw_per_submesh() {
let mesh_geometry = vec![unit_quad_mesh(), unit_quad_mesh()];
let mut model_map = std::collections::HashMap::new();
model_map.insert(
AssetId(1),
vec![
SubMeshRef {
mesh: Some(MeshHandle(0)),
material: Some(MaterialHandle(20)),
},
SubMeshRef {
mesh: Some(MeshHandle(1)),
material: None,
},
],
);
let mut material_map = std::collections::HashMap::new();
material_map.insert(
MaterialHandle(20),
MaterialEntry {
albedo_slot: 3,
normal_map_slot: 4,
uniforms: MaterialUniforms::DEFAULT,
shader_bucket: 0,
},
);
let data = build_draw_list(DrawListInputs {
items: &[model_item(AssetId(1))],
instanced_props: &[],
world_mats: &[IDENTITY4],
model_map: &model_map,
mesh_geometry: &mesh_geometry,
room_geometry: &[],
texture_count: 0,
material_map: &material_map,
always_resident_meshes: &std::collections::HashSet::new(),
})
.expect("build_draw_list");
let DrawListData {
vertices: verts,
indices: idxs,
draw_objects,
instanced_clusters: clusters,
prop_draw_indices: prop_idxs,
mesh_handle_to_draws,
..
} = data;
assert!(clusters.is_empty());
assert_eq!(draw_objects.len(), 2);
assert_eq!(prop_idxs, vec![vec![0, 1]]);
assert_eq!(verts.len(), 8, "each quad's 4 verts appended once");
assert_eq!(idxs.len(), 12);
assert_eq!(draw_objects[0].texture_slot, 3);
assert_eq!(draw_objects[0].normal_map_slot, 4);
assert_eq!(draw_objects[1].texture_slot, NO_ALBEDO_SLOT);
assert_eq!(draw_objects[1].normal_map_slot, NO_NORMAL_MAP_SLOT);
assert_eq!(mesh_handle_to_draws.get(&0), Some(&vec![0]));
assert_eq!(mesh_handle_to_draws.get(&1), Some(&vec![1]));
}
#[test]
fn build_draw_list_auto_renders_unreferenced_mesh() {
let mesh_geometry = vec![unit_quad_mesh()];
let data = build_draw_list(DrawListInputs {
items: &[],
instanced_props: &[],
world_mats: &[],
model_map: &std::collections::HashMap::new(),
mesh_geometry: &mesh_geometry,
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
})
.expect("build_draw_list");
let DrawListData {
draw_objects,
prop_draw_indices: prop_idxs,
mesh_handle_to_draws,
..
} = data;
assert!(prop_idxs.is_empty(), "no props drove this draw");
assert_eq!(draw_objects.len(), 1);
let d = &draw_objects[0];
assert_eq!(d.model, IDENTITY4);
assert_eq!(d.texture_slot, 0);
assert!(!d.cullable(), "unreferenced mesh draws unconditionally");
assert!(d.lod_alternates.is_empty());
assert_eq!(mesh_handle_to_draws.get(&0), Some(&vec![0]));
}
#[test]
fn build_draw_list_places_room_at_origin_with_texture_and_lods() {
let room = Room {
asset_id: AssetId(50),
half_width: 8.0,
half_depth: 10.0,
ceiling_height: 3.5,
texture: Some(TextureHandle(6)),
wall_texture: None,
floor_texture: None,
ceiling_texture: None,
locator: None,
};
let verts = unit_quad_mesh().vertices;
let idxs = vec![0u16, 1, 2, 0, 2, 3];
let room_lods = vec![(12.0_f32, vec![0u16, 1, 2])];
let room_geometry = vec![(room, verts, idxs, room_lods)];
let data = build_draw_list(DrawListInputs {
items: &[],
instanced_props: &[],
world_mats: &[],
model_map: &std::collections::HashMap::new(),
mesh_geometry: &[],
room_geometry: &room_geometry,
texture_count: 7,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
})
.expect("build_draw_list");
let DrawListData {
vertices: rv,
indices: ri,
draw_objects,
..
} = data;
assert_eq!(draw_objects.len(), 1);
let d = &draw_objects[0];
assert_eq!(d.model, IDENTITY4);
assert_eq!(d.texture_slot, 6, "room texture resolved to its slot");
assert!(!d.cullable(), "rooms enclose the camera and skip culling");
assert_eq!(d.lod_alternates.len(), 1);
assert_eq!(d.lod_alternates[0].switch_distance, 12.0);
assert_eq!(rv.len(), 4);
assert_eq!(ri.len(), 9);
}
#[test]
fn build_draw_list_single_mesh_resolves_texture_slot() {
let mesh_geometry = vec![unit_quad_mesh()];
let item = RenderableItem {
asset_id: AssetId(0),
model: None,
mesh: Some(MeshHandle(0)),
material: None,
texture: Some(TextureHandle(2)),
cull_distance: 0.0,
is_dynamic: false,
};
let data = build_draw_list(DrawListInputs {
items: &[item],
instanced_props: &[],
world_mats: &[IDENTITY4],
model_map: &std::collections::HashMap::new(),
mesh_geometry: &mesh_geometry,
room_geometry: &[],
texture_count: 3,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
})
.expect("build_draw_list");
let DrawListData { draw_objects, .. } = data;
assert_eq!(draw_objects.len(), 1);
assert_eq!(draw_objects[0].texture_slot, 2);
assert_eq!(draw_objects[0].normal_map_slot, NO_NORMAL_MAP_SLOT);
}
#[test]
fn build_draw_list_returns_none_on_missing_references() {
let mesh = || vec![unit_quad_mesh()];
let none = |inputs: DrawListInputs| build_draw_list(inputs).is_none();
assert!(none(DrawListInputs {
items: &[model_item(AssetId(1))],
instanced_props: &[],
world_mats: &[IDENTITY4],
model_map: &std::collections::HashMap::new(),
mesh_geometry: &mesh(),
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
}));
let mut model_no_mesh = std::collections::HashMap::new();
model_no_mesh.insert(
AssetId(1),
vec![SubMeshRef {
mesh: None,
material: None,
}],
);
assert!(none(DrawListInputs {
items: &[model_item(AssetId(1))],
instanced_props: &[],
world_mats: &[IDENTITY4],
model_map: &model_no_mesh,
mesh_geometry: &mesh(),
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
}));
let mut model_bad_geo = std::collections::HashMap::new();
model_bad_geo.insert(
AssetId(1),
vec![SubMeshRef {
mesh: Some(MeshHandle(999)),
material: None,
}],
);
assert!(none(DrawListInputs {
items: &[model_item(AssetId(1))],
instanced_props: &[],
world_mats: &[IDENTITY4],
model_map: &model_bad_geo,
mesh_geometry: &mesh(),
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
}));
let mut model_bad_mat = std::collections::HashMap::new();
model_bad_mat.insert(
AssetId(1),
vec![SubMeshRef {
mesh: Some(MeshHandle(0)),
material: Some(MaterialHandle(404)),
}],
);
assert!(none(DrawListInputs {
items: &[model_item(AssetId(1))],
instanced_props: &[],
world_mats: &[IDENTITY4],
model_map: &model_bad_mat,
mesh_geometry: &mesh(),
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
}));
assert!(none(DrawListInputs {
items: &[mesh_item(AssetId(999))],
instanced_props: &[],
world_mats: &[IDENTITY4],
model_map: &std::collections::HashMap::new(),
mesh_geometry: &mesh(),
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
}));
let mut item_bad_mat = mesh_item(AssetId(0));
item_bad_mat.material = Some(MaterialHandle(404));
assert!(none(DrawListInputs {
items: &[item_bad_mat],
instanced_props: &[],
world_mats: &[IDENTITY4],
model_map: &std::collections::HashMap::new(),
mesh_geometry: &mesh(),
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
}));
assert!(none(DrawListInputs {
items: &[RenderableItem {
asset_id: AssetId(0),
model: None,
mesh: None,
material: None,
texture: None,
cull_distance: 0.0,
is_dynamic: false,
}],
instanced_props: &[],
world_mats: &[IDENTITY4],
model_map: &std::collections::HashMap::new(),
mesh_geometry: &mesh(),
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
}));
let inst_bad_mesh = InstancedProp {
asset_id: AssetId::default(),
mesh: Some(MeshHandle(999)),
material: None,
texture: None,
cull_distance: 0.0,
instances: vec![crate::components::InstanceTransform::default()],
};
assert!(none(DrawListInputs {
items: &[],
instanced_props: &[inst_bad_mesh],
world_mats: &[],
model_map: &std::collections::HashMap::new(),
mesh_geometry: &mesh(),
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
}));
let inst_bad_mat = InstancedProp {
asset_id: AssetId::default(),
mesh: Some(MeshHandle(0)),
material: Some(MaterialHandle(404)),
texture: None,
cull_distance: 0.0,
instances: vec![crate::components::InstanceTransform::default()],
};
assert!(none(DrawListInputs {
items: &[],
instanced_props: &[inst_bad_mat],
world_mats: &[],
model_map: &std::collections::HashMap::new(),
mesh_geometry: &mesh(),
room_geometry: &[],
texture_count: 0,
material_map: &std::collections::HashMap::new(),
always_resident_meshes: &std::collections::HashSet::new(),
}));
}
struct BlobWorld {
components: crate::ecs::ComponentStorage,
section: Vec<u8>,
}
struct SealedWorld {
components: crate::ecs::ComponentStorage,
blob: crate::blob::BlobData,
profile: crate::gfx::profile::FrameProfile,
resources: crate::ecs::Resources,
scratch: crate::ecs::Arena,
}
impl BlobWorld {
fn new() -> Self {
Self {
components: crate::ecs::ComponentStorage::default(),
section: Vec::new(),
}
}
fn payload(&mut self, bytes: &[u8]) -> crate::ecs::PayloadLocator {
let offset = self.section.len() as u64;
self.section.extend_from_slice(bytes);
crate::ecs::PayloadLocator {
blob_index: 0,
offset,
len: bytes.len() as u64,
}
}
fn push<C: crate::ecs::ComponentSlot>(&mut self, c: C) {
self.components.push_typed(c);
}
fn seal(self) -> SealedWorld {
SealedWorld {
components: self.components,
blob: crate::blob::BlobData::new(vec![Some(self.section)]),
profile: crate::gfx::profile::FrameProfile::default(),
resources: crate::ecs::Resources::new(),
scratch: crate::ecs::Arena::with_capacity(64 * 1024),
}
}
}
impl SealedWorld {
fn ctx(&mut self) -> crate::ecs::PipelineContext<'_> {
crate::ecs::PipelineContext {
components: &mut self.components,
blob: &mut self.blob,
profile: &mut self.profile,
resources: &mut self.resources,
frame: crate::ecs::FrameContext::new(&self.scratch),
}
}
fn with_mesh_table(
mut self,
locators: Vec<Option<crate::ecs::PayloadLocator>>,
) -> SealedWorld {
let entries = locators
.into_iter()
.map(|payload| crate::resource::ResourceEntry {
payload,
data_bytes: Vec::new(),
})
.collect();
self.resources.insert(crate::resource::MeshTable(entries));
self
}
}
fn tri_payload() -> Vec<u8> {
let v = |x: f32, z: f32| {
(
[x, 0.0, z],
[0.0, 1.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 1.0],
[0.0, 0.0],
)
};
crate::gfx::mesh_payload::serialise(&[v(0.0, 0.0), v(1.0, 0.0), v(0.0, 1.0)], &[0u16, 1, 2])
}
#[test]
fn load_mesh_geometry_decodes_in_memory_mesh() {
let mut b = BlobWorld::new();
let loc = b.payload(&tri_payload());
let mut world = b.seal().with_mesh_table(vec![Some(loc)]);
let mut ctx = world.ctx();
let (geometry, sources, resident, component_handles, _deferred) =
load_mesh_geometry(&mut ctx, &DeferredMeshSources::default(), false).expect("decoded");
assert_eq!(geometry.len(), 1);
let m = &geometry[0];
assert_eq!(m.vertices.len(), 3);
assert_eq!(m.indices, vec![0, 1, 2]);
assert!(m.lod_alternates.is_empty());
assert!(component_handles.is_empty());
assert!(sources.is_empty());
assert!(
resident.is_empty(),
"no skybox mesh -> nothing always-resident"
);
}
#[test]
fn load_mesh_geometry_defers_scene_owned_mesh_with_baked_bounds() {
let mut b = BlobWorld::new();
let loc = b.payload(&tri_payload());
let mut world = b.seal().with_mesh_table(vec![Some(loc)]);
let mut ctx = world.ctx();
let mut deferred = DeferredMeshSources::default();
deferred.by_handle.insert(0);
deferred
.bounds
.insert(0, ([-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]));
let (geometry, _sources, _resident, _handles, seeds) =
load_mesh_geometry(&mut ctx, &deferred, false).expect("ok");
assert_eq!(geometry.len(), 1);
assert!(geometry[0].vertices.is_empty(), "decode skipped");
assert_eq!(
geometry[0].bounds,
Some(([-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]))
);
let seed = seeds.get(&0).expect("payload seed captured");
assert!(
seed.bytes.as_deref().is_some_and(|b| !b.is_empty()),
"RAM-backed seed carries the payload bytes"
);
}
#[test]
fn load_mesh_geometry_decodes_eagerly_without_baked_bounds() {
let mut b = BlobWorld::new();
let loc = b.payload(&tri_payload());
let mut world = b.seal().with_mesh_table(vec![Some(loc)]);
let mut ctx = world.ctx();
let mut deferred = DeferredMeshSources::default();
deferred.by_handle.insert(0);
let (geometry, _sources, _resident, _handles, seeds) =
load_mesh_geometry(&mut ctx, &deferred, false).expect("ok");
assert_eq!(geometry[0].vertices.len(), 3, "no bounds record -> decode");
assert!(seeds.is_empty());
}
#[test]
fn load_mesh_geometry_marks_skybox_always_resident() {
let mut b = BlobWorld::new();
let loc = b.payload(&tri_payload());
b.push(ProceduralMesh {
asset_id: AssetId(2),
generator: "skybox".to_string(),
locator: Some(loc),
..Default::default()
});
let mut world = b.seal();
let mut ctx = world.ctx();
let (geometry, _sources, resident, component_handles, _deferred) =
load_mesh_geometry(&mut ctx, &DeferredMeshSources::default(), false).expect("decoded");
assert_eq!(component_handles.get(&AssetId(2)), Some(&0));
assert_eq!(geometry.len(), 1);
assert!(resident.contains(&0), "skybox generator stays resident");
}
#[test]
fn load_mesh_geometry_loads_the_baked_block_last() {
let mut b = BlobWorld::new();
let compiled = b.payload(&tri_payload());
b.push(ProceduralMesh {
asset_id: AssetId(2),
generator: "box".to_string(),
locator: Some(compiled.clone()),
..Default::default()
});
b.push(ProceduralMesh {
asset_id: AssetId(3),
generator: "skybox".to_string(),
..Default::default()
});
let mut world = b.seal().with_mesh_table(vec![Some(compiled)]);
let mut payloads = concinnity_core::resource::RuntimeMeshPayloads::default();
payloads.push(AssetId(3), tri_payload());
world.resources.insert(payloads);
let mut ctx = world.ctx();
let (geometry, _sources, resident, component_handles, _deferred) =
load_mesh_geometry(&mut ctx, &DeferredMeshSources::default(), false).expect("decoded");
assert_eq!(geometry.len(), 3);
assert_eq!(component_handles.get(&AssetId(2)), Some(&1));
assert_eq!(
component_handles.get(&AssetId(3)),
Some(&2),
"the baked mesh trails the compiled blocks"
);
assert_eq!(geometry[2].vertices.len(), 3, "the baked payload decoded");
assert!(resident.contains(&2), "a baked skybox stays resident too");
}
#[test]
fn load_mesh_geometry_baked_mesh_without_a_payload_returns_none() {
let mut b = BlobWorld::new();
b.push(ProceduralMesh {
asset_id: AssetId(3),
generator: "skybox".to_string(),
..Default::default()
});
let mut world = b.seal();
let mut ctx = world.ctx();
assert!(load_mesh_geometry(&mut ctx, &DeferredMeshSources::default(), false).is_none());
}
#[test]
fn load_mesh_geometry_missing_locator_returns_none() {
let mut world = BlobWorld::new().seal().with_mesh_table(vec![None]);
let mut ctx = world.ctx();
assert!(load_mesh_geometry(&mut ctx, &DeferredMeshSources::default(), false).is_none());
}
#[test]
fn load_mesh_geometry_malformed_payload_returns_none() {
let mut b = BlobWorld::new();
let loc = b.payload(&1u32.to_le_bytes());
let mut world = b.seal().with_mesh_table(vec![Some(loc)]);
let mut ctx = world.ctx();
assert!(load_mesh_geometry(&mut ctx, &DeferredMeshSources::default(), false).is_none());
}
#[test]
fn load_mesh_geometry_empty_world_is_ok_and_empty() {
let mut world = BlobWorld::new().seal();
let mut ctx = world.ctx();
let (geometry, sources, resident, component_handles, _deferred) =
load_mesh_geometry(&mut ctx, &DeferredMeshSources::default(), false).expect("ok");
assert!(geometry.is_empty() && sources.is_empty() && resident.is_empty());
assert!(component_handles.is_empty());
}
fn test_room(locator: Option<crate::ecs::PayloadLocator>) -> Room {
Room {
asset_id: AssetId(50),
half_width: 8.0,
half_depth: 10.0,
ceiling_height: 3.5,
texture: None,
wall_texture: None,
floor_texture: None,
ceiling_texture: None,
locator,
}
}
#[test]
fn load_room_geometry_decodes_in_memory_room() {
let mut b = BlobWorld::new();
let loc = b.payload(&tri_payload());
b.push(test_room(Some(loc)));
let mut world = b.seal();
let mut ctx = world.ctx();
let (room_geometry, blob_indices) = load_room_geometry(&mut ctx).expect("decoded");
assert_eq!(room_geometry.len(), 1);
let (_room, verts, idxs, _lods) = &room_geometry[0];
assert_eq!(verts.len(), 3);
assert_eq!(*idxs, vec![0, 1, 2]);
assert_eq!(blob_indices, vec![0]);
}
#[test]
fn load_room_geometry_missing_locator_returns_none() {
let mut b = BlobWorld::new();
b.push(test_room(None));
let mut world = b.seal();
let mut ctx = world.ctx();
assert!(load_room_geometry(&mut ctx).is_none());
}
#[test]
fn decomposed_renderable_item_matches_a_model_prop() {
use crate::blob::BlobData;
use crate::components::ModelRenderer;
use crate::ecs::{ComponentStorage, PipelineContext, Resources};
use crate::gfx::profile::FrameProfile;
let mut prop = make_prop([0.0; 3]);
prop.asset_id = AssetId(8);
prop.model = Some(AssetId(100));
prop.cull_distance = 30.0;
let mut components = ComponentStorage::default();
let mut blob = BlobData::empty();
let mut profile = FrameProfile::default();
let mut resources = Resources::new();
let scratch = crate::ecs::Arena::with_capacity(64 * 1024);
let mut ctx = PipelineContext {
components: &mut components,
blob: &mut blob,
profile: &mut profile,
resources: &mut resources,
frame: crate::ecs::FrameContext::new(&scratch),
};
let e = ctx.components.spawn();
ctx.insert(
e,
ModelRenderer {
model: prop.model.unwrap(),
cull_distance: prop.cull_distance,
},
);
let item = decomposed_renderable_item(&ctx, e, prop.asset_id);
assert_eq!(
item,
RenderableItem {
asset_id: AssetId(8),
model: Some(AssetId(100)),
mesh: None,
material: None,
texture: None,
cull_distance: 30.0,
is_dynamic: false,
}
);
}
}