use concinnity_core::gfx::render_types::{DrawObject, InstancedCluster, SkinnedDrawObject};
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::rt_accel::{
AccelBook, EmptyHead, FrameRing, HeadRefresh, InstanceBlas, RefreshMode, RtStep, RtUpdate,
ScratchRing, SeedSet, StaticRing, empty_head, seed_wanted,
};
use concinnity_core::render::rt_geom::{RtDynamicMode, instance_id_and_mask, pack_row_major_3x4};
use concinnity_core::render::rt_refit::{BlasUpdate, SkinnedRefit};
use concinnity_core::render::rt_topology::blas_vertex_count;
use windows::Win32::Graphics::Direct3D12::*;
use windows::Win32::Graphics::Dxgi::Common::*;
use windows::core::Interface;
use super::allocator::{DeviceAllocator, PooledBuffer};
use super::com;
use super::context::{DxGeometry, FRAMES};
use super::error::map_hresult;
use super::texture::{create_uav_buffer, transition_barrier};
use crate::directx::builtin_shaders::CompileProgram;
use crate::directx::pso::compute_pso;
use crate::directx::root_constants::RootConstants;
use crate::directx::root_sig::{RootSig, Visibility};
const VERTEX_STRIDE: u64 = 56;
use concinnity_core::render::uniforms::SkinParams;
pub(super) fn raytracing_supported(device: &ID3D12Device) -> bool {
let mut opts5 = D3D12_FEATURE_DATA_D3D12_OPTIONS5::default();
let ok = unsafe {
device.CheckFeatureSupport(
D3D12_FEATURE_D3D12_OPTIONS5,
&mut opts5 as *mut _ as *mut std::ffi::c_void,
std::mem::size_of::<D3D12_FEATURE_DATA_D3D12_OPTIONS5>() as u32,
)
};
ok.is_ok() && opts5.RaytracingTier.0 >= D3D12_RAYTRACING_TIER_1_1.0
}
fn instance_desc(
model: [[f32; 4]; 4],
instance_id: u32,
blas_gva: u64,
) -> D3D12_RAYTRACING_INSTANCE_DESC {
D3D12_RAYTRACING_INSTANCE_DESC {
Transform: pack_row_major_3x4(model),
_bitfield1: instance_id_and_mask(instance_id),
_bitfield2: 0,
AccelerationStructure: blas_gva,
}
}
fn instance_blas_gva(
blas: InstanceBlas<'_, ID3D12Resource>,
fresh: &[u64],
skinned: &[u64],
) -> u64 {
match blas {
InstanceBlas::Head { blas, .. } => com::gpu_va(blas),
InstanceBlas::Fresh { index } => fresh.get(index).copied().unwrap_or(0),
InstanceBlas::Skinned { n } => skinned.get(n).copied().unwrap_or(0),
}
}
fn triangle_geometry(
vertex_start: u64,
vertex_count: u32,
index_start: u64,
index_count: u32,
) -> D3D12_RAYTRACING_GEOMETRY_DESC {
D3D12_RAYTRACING_GEOMETRY_DESC {
Type: D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES,
Flags: D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE,
Anonymous: D3D12_RAYTRACING_GEOMETRY_DESC_0 {
Triangles: D3D12_RAYTRACING_GEOMETRY_TRIANGLES_DESC {
Transform3x4: 0,
IndexFormat: DXGI_FORMAT_R32_UINT,
VertexFormat: DXGI_FORMAT_R32G32B32_FLOAT,
IndexCount: index_count,
VertexCount: vertex_count,
IndexBuffer: index_start,
VertexBuffer: D3D12_GPU_VIRTUAL_ADDRESS_AND_STRIDE {
StartAddress: vertex_start,
StrideInBytes: VERTEX_STRIDE,
},
},
},
}
}
#[derive(Clone, Copy)]
pub(super) struct SharedGeometry {
vertex_gva: u64,
index_gva: u64,
vertex_count: u64,
}
impl SharedGeometry {
pub(super) fn of(geometry: &DxGeometry) -> Self {
Self {
vertex_gva: com::gpu_va(&geometry.vertex_buffer),
index_gva: com::gpu_va(&geometry.index_buffer),
vertex_count: u64::from(geometry.vertex_buffer_view.SizeInBytes) / VERTEX_STRIDE,
}
}
fn draw_geometry(&self, obj: &DrawObject) -> D3D12_RAYTRACING_GEOMETRY_DESC {
let base_vertex = u64::try_from(obj.base_vertex).unwrap_or(0);
triangle_geometry(
self.vertex_gva + base_vertex * VERTEX_STRIDE,
blas_vertex_count(obj.base_vertex, self.vertex_count),
self.index_gva + obj.index_offset as u64 * 4,
obj.index_count as u32,
)
}
fn cluster_geometry(&self, cluster: &InstancedCluster) -> D3D12_RAYTRACING_GEOMETRY_DESC {
triangle_geometry(
self.vertex_gva,
blas_vertex_count(0, self.vertex_count),
self.index_gva + cluster.index_offset as u64 * 4,
cluster.index_count as u32,
)
}
}
fn skinned_triangle_geometry(
vertex_start: u64,
vertex_count: u32,
index_start: u64,
index_count: u32,
) -> D3D12_RAYTRACING_GEOMETRY_DESC {
D3D12_RAYTRACING_GEOMETRY_DESC {
Type: D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES,
Flags: D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE,
Anonymous: D3D12_RAYTRACING_GEOMETRY_DESC_0 {
Triangles: D3D12_RAYTRACING_GEOMETRY_TRIANGLES_DESC {
Transform3x4: 0,
IndexFormat: DXGI_FORMAT_R32_UINT,
VertexFormat: DXGI_FORMAT_R32G32B32_FLOAT,
IndexCount: index_count,
VertexCount: vertex_count,
IndexBuffer: index_start,
VertexBuffer: D3D12_GPU_VIRTUAL_ADDRESS_AND_STRIDE {
StartAddress: vertex_start,
StrideInBytes: VERTEX_STRIDE,
},
},
},
}
}
fn create_as_buffer(device: &ID3D12Device, size: u64) -> RenderResult<ID3D12Resource> {
create_uav_buffer(
device,
size.max(256),
D3D12_RESOURCE_STATE_RAYTRACING_ACCELERATION_STRUCTURE,
)
}
fn create_scratch(device: &ID3D12Device, size: u64) -> RenderResult<ID3D12Resource> {
create_uav_buffer(device, size.max(256), D3D12_RESOURCE_STATE_COMMON)
}
fn scratch_capacity(needed: u64) -> u64 {
needed.max(256)
}
fn ensure_scratch(
ring: &mut ScratchRing<ID3D12Resource>,
device: &ID3D12Device,
frame_idx: usize,
needed: u64,
) -> RenderResult<u64> {
ring.ensure(frame_idx, scratch_capacity(needed), |capacity| {
create_scratch(device, capacity)
})
.map(com::gpu_va)
}
fn upload_slice<T: Copy>(
alloc: &DeviceAllocator,
data: &[T],
label: &str,
) -> RenderResult<PooledBuffer> {
let bytes = std::mem::size_of_val(data).max(16) as u64;
let buf = alloc.alloc_buffer(
bytes,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
)?;
let mut ptr = std::ptr::null_mut::<std::ffi::c_void>();
unsafe { buf.Map(0, None, Some(&mut ptr)) }
.map_err(|e| map_hresult(e.code(), &format!("map {label}")))?;
unsafe {
std::ptr::copy_nonoverlapping(
data.as_ptr() as *const u8,
ptr as *mut u8,
std::mem::size_of_val(data),
);
buf.Unmap(0, None);
}
Ok(buf)
}
fn uav_barrier() -> D3D12_RESOURCE_BARRIER {
D3D12_RESOURCE_BARRIER {
Type: D3D12_RESOURCE_BARRIER_TYPE_UAV,
Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE,
Anonymous: D3D12_RESOURCE_BARRIER_0 {
UAV: std::mem::ManuallyDrop::new(D3D12_RESOURCE_UAV_BARRIER {
pResource: std::mem::ManuallyDrop::new(None),
}),
},
}
}
fn prebuild_info(
device: &ID3D12Device5,
inputs: &D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS,
) -> D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO {
let mut info = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO::default();
unsafe { device.GetRaytracingAccelerationStructurePrebuildInfo(inputs, &mut info) };
info
}
fn blas_inputs(
geo: &D3D12_RAYTRACING_GEOMETRY_DESC,
) -> D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS {
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS {
Type: D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL,
Flags: D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE,
NumDescs: 1,
DescsLayout: D3D12_ELEMENTS_LAYOUT_ARRAY,
Anonymous: D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS_0 {
pGeometryDescs: geo,
},
}
}
fn skinned_blas_inputs(
geo: &D3D12_RAYTRACING_GEOMETRY_DESC,
update: BlasUpdate,
) -> D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS {
let mut flags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE
| D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_ALLOW_UPDATE;
if update == BlasUpdate::Refit {
flags |= D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PERFORM_UPDATE;
}
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS {
Type: D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL,
Flags: flags,
NumDescs: 1,
DescsLayout: D3D12_ELEMENTS_LAYOUT_ARRAY,
Anonymous: D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS_0 {
pGeometryDescs: geo,
},
}
}
fn tlas_inputs(
instance_count: u32,
instance_descs_gva: u64,
) -> D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS {
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS {
Type: D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL,
Flags: D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE,
NumDescs: instance_count,
DescsLayout: D3D12_ELEMENTS_LAYOUT_ARRAY,
Anonymous: D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS_0 {
InstanceDescs: instance_descs_gva,
},
}
}
pub(super) struct SkinPipeline {
pub(super) root_sig: ID3D12RootSignature,
pub(super) pso: ID3D12PipelineState,
}
fn create_skin_root_signature(device: &ID3D12Device) -> RenderResult<ID3D12RootSignature> {
use Visibility::All;
RootSig::new()
.constants::<SkinParams>(0, All) .srv(0, All) .srv(1, All) .uav(0, All) .srv(2, All) .srv(3, All) .build(device, "rt skin root sig")
}
fn build_skin_pipeline(device: &ID3D12Device, hot_reload: bool) -> RenderResult<SkinPipeline> {
let cs = super::builtin_shaders::RT_SKIN.compile(hot_reload)?;
let root_sig = create_skin_root_signature(device)?;
let pso = compute_pso(device, &root_sig, &cs, "rt skin")?;
Ok(SkinPipeline { root_sig, pso })
}
pub(super) struct SkinnedRtInputs<'a> {
pub objects: &'a [SkinnedDrawObject],
pub vertex_gva: u64,
pub index_gva: u64,
pub joint_buffers: &'a [PooledBuffer],
pub skin: &'a SkinPipeline,
}
fn ring_slot_needs_grow(present: bool, capacity: u64, needed: u64) -> bool {
!present || capacity < needed
}
#[derive(Default)]
struct SkinnedFrameRing {
deformed: Option<ID3D12Resource>,
deformed_cap: u64,
blas: Vec<(ID3D12Resource, u64)>,
refit: SkinnedRefit,
tlas: Option<ID3D12Resource>,
tlas_cap: u64,
instance: Option<PooledBuffer>,
instance_cap: u64,
geom: Option<PooledBuffer>,
geom_cap: u64,
}
#[derive(Default)]
struct StaticFrameRing {
tlas: Option<ID3D12Resource>,
tlas_cap: u64,
instance: Option<PooledBuffer>,
instance_cap: u64,
geom: Option<PooledBuffer>,
geom_cap: u64,
}
fn write_upload_ring<T: Copy>(
slot: &mut Option<PooledBuffer>,
cap: &mut u64,
alloc: &DeviceAllocator,
data: &[T],
label: &str,
) -> RenderResult<PooledBuffer> {
let len_bytes = std::mem::size_of_val(data);
let needed = upload_ring_size(data);
if ring_slot_needs_grow(slot.is_some(), *cap, needed) {
*slot = Some(
alloc
.alloc_buffer(
needed,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
)
.map_err(|e| e.context(label))?,
);
*cap = needed;
}
let buf = slot.clone().ok_or_else(missing_slot_buffer)?;
let mut ptr = std::ptr::null_mut::<std::ffi::c_void>();
unsafe {
buf.Map(0, None, Some(&mut ptr))
.map_err(|e| map_hresult(e.code(), &format!("{label} map")))?;
std::ptr::copy_nonoverlapping(data.as_ptr() as *const u8, ptr as *mut u8, len_bytes);
buf.Unmap(0, None);
}
Ok(buf)
}
fn upload_ring_size<T>(data: &[T]) -> u64 {
(std::mem::size_of_val(data).max(std::mem::size_of::<T>()) as u64).max(4)
}
fn ensure_as_buffer(
slot: &mut Option<ID3D12Resource>,
cap: &mut u64,
device: &ID3D12Device,
needed: u64,
) -> RenderResult<ID3D12Resource> {
if ring_slot_needs_grow(slot.is_some(), *cap, needed) {
*slot = Some(create_as_buffer(device, needed)?);
*cap = needed;
}
slot.clone().ok_or_else(missing_slot_buffer)
}
fn missing_slot_buffer() -> RenderError {
RenderError::Other("RT ring slot is missing a buffer it was just sized for".into())
}
pub(super) struct RtAccelData {
book: AccelBook<ID3D12Resource, D3D12_RAYTRACING_INSTANCE_DESC>,
tlas: ID3D12Resource,
geom_table: PooledBuffer,
instance_buffer: PooledBuffer,
scratch: ScratchRing<ID3D12Resource>,
tlas_size: u64,
static_ring: StaticRing<StaticFrameRing>,
skinned_ring: FrameRing<SkinnedFrameRing>,
deformed_verts: ID3D12Resource,
skinned_indices: PooledBuffer,
skinned_scratch: SkinnedScratch,
}
#[derive(Default)]
struct SkinnedScratch {
geo: Vec<D3D12_RAYTRACING_GEOMETRY_DESC>,
blas_gvas: Vec<u64>,
}
impl RtAccelData {
pub(super) fn tlas_gva(&self) -> u64 {
com::gpu_va(&self.tlas)
}
pub(super) fn geom_table_gva(&self) -> u64 {
com::gpu_va(&self.geom_table)
}
pub(super) fn deformed_verts_gva(&self) -> u64 {
com::gpu_va(&self.deformed_verts)
}
pub(super) fn skinned_index_gva(&self) -> u64 {
com::gpu_va(&self.skinned_indices)
}
}
pub(super) fn build_rt_skin_pipeline(
device: &ID3D12Device,
hot_reload: bool,
) -> RenderResult<SkinPipeline> {
build_skin_pipeline(device, hot_reload)
}
pub(super) fn build_rt_skin(device: &ID3D12Device, hot_reload: bool) -> Option<SkinPipeline> {
build_skin_pipeline(device, hot_reload)
.inspect_err(|e| {
tracing::warn!(
"RT skin pipeline build failed (skinned meshes absent from reflections): {e}"
)
})
.ok()
}
#[derive(Clone, Copy)]
pub(super) struct RtInitGeometry<'a> {
pub alloc: &'a DeviceAllocator,
pub shared: SharedGeometry,
pub draw_objects: &'a [DrawObject],
pub clusters: &'a [InstancedCluster],
pub albedo_count: u32,
pub exclude_seethrough: bool,
pub skinned_present: bool,
}
pub(super) struct RtDynamicInputs<'a> {
pub mode: RtDynamicMode,
pub skinned: Option<SkinnedRtInputs<'a>>,
pub frame_idx: usize,
pub shared: SharedGeometry,
pub topology_dirty: bool,
pub exclude_seethrough: bool,
}
pub(super) fn build_rt_accel(geometry: RtInitGeometry) -> RenderResult<Option<RtAccelData>> {
let RtInitGeometry {
alloc,
shared,
draw_objects,
clusters,
albedo_count,
exclude_seethrough,
skinned_present,
} = geometry;
let device = alloc.device();
let queue = alloc.queue();
let device5: ID3D12Device5 = device
.cast()
.map_err(|e| map_hresult(e.code(), "ID3D12Device5 cast (DXR unsupported?)"))?;
let seed = SeedSet::new(draw_objects, clusters, exclude_seethrough);
if seed.builds_nothing(skinned_present) {
return Ok(None);
}
let geo_descs: Vec<D3D12_RAYTRACING_GEOMETRY_DESC> = seed
.objects
.iter()
.map(|&i| shared.draw_geometry(&draw_objects[i]))
.chain(seed.clusters.iter().map(|c| shared.cluster_geometry(c)))
.collect();
let mut blas: Vec<ID3D12Resource> = Vec::with_capacity(geo_descs.len());
let mut max_scratch: u64 = 0;
for geo in &geo_descs {
let inputs = blas_inputs(geo);
let info = prebuild_info(&device5, &inputs);
blas.push(create_as_buffer(device, info.ResultDataMaxSizeInBytes)?);
max_scratch = max_scratch.max(info.ScratchDataSizeInBytes);
}
let mut book = AccelBook::new(&seed, blas, draw_objects, albedo_count)?;
book.fill_instances(draw_objects, None, |model, id, blas| {
instance_desc(model, id, instance_blas_gva(blas, &[], &[]))
});
let instance_count = book.instances().len() as u32;
let instance_buffer = upload_slice(alloc, book.instances(), "RT instance descriptors")?;
let geom_table = upload_slice(alloc, book.geom_table(), "RT geometry table")?;
let tlas_pre = prebuild_info(&device5, &tlas_inputs(instance_count, 0));
max_scratch = max_scratch.max(tlas_pre.ScratchDataSizeInBytes);
let tlas = create_as_buffer(device, tlas_pre.ResultDataMaxSizeInBytes)?;
let scratch = ScratchRing::filled(FRAMES, scratch_capacity(max_scratch), |capacity| {
create_scratch(device, capacity)
})?;
let scratch_gva = scratch.get(0).map_or(0, com::gpu_va);
record_builds(alloc, queue, |cmd4| unsafe {
for (dest, geo) in book.head().iter().zip(&geo_descs) {
let desc = D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC {
DestAccelerationStructureData: com::gpu_va(dest),
Inputs: blas_inputs(geo),
SourceAccelerationStructureData: 0,
ScratchAccelerationStructureData: scratch_gva,
};
cmd4.BuildRaytracingAccelerationStructure(&desc, None);
cmd4.ResourceBarrier(&[uav_barrier()]);
}
let tlas_desc = D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC {
DestAccelerationStructureData: com::gpu_va(&tlas),
Inputs: tlas_inputs(instance_count, com::gpu_va(&instance_buffer)),
SourceAccelerationStructureData: 0,
ScratchAccelerationStructureData: scratch_gva,
};
cmd4.BuildRaytracingAccelerationStructure(&tlas_desc, None);
})?;
let deformed_verts = create_uav_buffer(device, VERTEX_STRIDE, D3D12_RESOURCE_STATE_COMMON)?;
let skinned_indices =
alloc.alloc_buffer(4, D3D12_HEAP_TYPE_DEFAULT, D3D12_RESOURCE_STATE_COMMON)?;
let static_ring = StaticRing::new(
FRAMES,
StaticFrameRing {
tlas: Some(tlas.clone()),
tlas_cap: tlas_pre.ResultDataMaxSizeInBytes.max(256),
instance: Some(instance_buffer.clone()),
instance_cap: (std::mem::size_of_val(book.instances()) as u64).max(16),
geom: Some(geom_table.clone()),
geom_cap: (std::mem::size_of_val(book.geom_table()) as u64).max(16),
},
);
Ok(Some(RtAccelData {
book,
tlas,
geom_table,
instance_buffer,
scratch,
tlas_size: tlas_pre.ResultDataMaxSizeInBytes,
static_ring,
skinned_ring: FrameRing::new(FRAMES),
deformed_verts,
skinned_indices,
skinned_scratch: SkinnedScratch::default(),
}))
}
fn record_builds<F>(
alloc: &DeviceAllocator,
queue: &ID3D12CommandQueue,
record: F,
) -> RenderResult<()>
where
F: FnOnce(&ID3D12GraphicsCommandList4),
{
let device = alloc.device();
let alloc: ID3D12CommandAllocator =
unsafe { device.CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT) }
.map_err(|e| map_hresult(e.code(), "RT build allocator"))?;
let cmd: ID3D12GraphicsCommandList =
unsafe { device.CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, &alloc, None) }
.map_err(|e| map_hresult(e.code(), "RT build cmd list"))?;
let cmd4: ID3D12GraphicsCommandList4 = cmd
.cast()
.map_err(|e| map_hresult(e.code(), "ID3D12GraphicsCommandList4 cast"))?;
record(&cmd4);
unsafe { cmd.Close() }.map_err(|e| map_hresult(e.code(), "RT build close"))?;
let list: ID3D12CommandList = cmd
.cast()
.map_err(|e| map_hresult(e.code(), "RT build cast"))?;
unsafe { queue.ExecuteCommandLists(&[Some(list)]) };
let fence: ID3D12Fence = unsafe { device.CreateFence(0, D3D12_FENCE_FLAG_NONE) }
.map_err(|e| map_hresult(e.code(), "RT build fence"))?;
let event =
unsafe { windows::Win32::System::Threading::CreateEventW(None, false, false, None) }
.map_err(|e| map_hresult(e.code(), "RT build event"))?;
unsafe { queue.Signal(&fence, 1) }.map_err(|e| map_hresult(e.code(), "RT build signal"))?;
if unsafe { fence.GetCompletedValue() } < 1 {
unsafe { fence.SetEventOnCompletion(1, event) }
.map_err(|e| map_hresult(e.code(), "RT build set event"))?;
unsafe { windows::Win32::System::Threading::WaitForSingleObject(event, u32::MAX) };
}
unsafe { windows::Win32::Foundation::CloseHandle(event) }.ok();
Ok(())
}
#[derive(Clone, Copy)]
struct TopologyRefresh {
shared: SharedGeometry,
exclude_seethrough: bool,
mode: RefreshMode,
}
impl RtAccelData {
pub(super) fn dynamic_update(
&mut self,
alloc: &DeviceAllocator,
cmd: &ID3D12GraphicsCommandList,
draw_objects: &[DrawObject],
inputs: RtDynamicInputs,
) -> RenderResult<RtUpdate> {
let RtDynamicInputs {
mode,
skinned,
frame_idx,
shared,
topology_dirty,
exclude_seethrough,
} = inputs;
self.book.tick();
let Some(plan) = self
.book
.plan(mode, topology_dirty, skinned.as_ref().map(|s| s.objects))
else {
return Ok(RtUpdate::Done);
};
let mut refreshed = Ok(());
if let Some(mode) = plan.refresh {
let req = TopologyRefresh {
shared,
exclude_seethrough,
mode,
};
let if_empty = empty_head(plan.skinned, skinned.is_some());
refreshed = self.refresh_topology(alloc, cmd, draw_objects, req, if_empty);
if refreshed.is_err() {
self.book.owe_refresh();
}
}
let stepped = match self.book.next_step(mode, &plan, draw_objects) {
RtStep::Keep => RtUpdate::Done,
RtStep::Tlas if self.book.is_empty() && skinned.is_none() => {
self.book.release_skinned();
RtUpdate::Done
}
RtStep::Tlas => {
self.rebuild_tlas(alloc, cmd, draw_objects, frame_idx)?;
RtUpdate::Done
}
RtStep::Skinned => match skinned {
Some(s) => self.rebuild_skinned(SkinnedRebuild {
alloc,
cmd,
draw_objects,
skinned: &s,
frame_idx,
full_build: plan.full_skinned_build,
})?,
None => RtUpdate::Done,
},
};
refreshed.map(|()| stepped)
}
pub(super) fn is_spent(&self, skinned_present: bool) -> bool {
self.book.is_spent(skinned_present)
}
fn refresh_topology(
&mut self,
alloc: &DeviceAllocator,
cmd: &ID3D12GraphicsCommandList,
draw_objects: &[DrawObject],
req: TopologyRefresh,
if_empty: EmptyHead,
) -> RenderResult<()> {
let refresh = self
.book
.plan_refresh(draw_objects, req.exclude_seethrough, req.mode);
if if_empty != EmptyHead::Build && self.book.refresh_leaves_nothing(&refresh) {
let orphans = self.book.commit_refresh(refresh, Vec::new(), draw_objects);
self.book.park(orphans);
if if_empty == EmptyHead::Drop {
self.book.release_skinned();
}
return Ok(());
}
let (next, mut slot) = self.static_ring.take_next();
let result = self.refresh_topology_into(alloc, cmd, draw_objects, refresh, req, &mut slot);
if result.is_ok() {
self.static_ring.publish(next, slot);
} else {
self.static_ring.put(next, slot);
}
result
}
fn refresh_topology_into(
&mut self,
alloc: &DeviceAllocator,
cmd: &ID3D12GraphicsCommandList,
draw_objects: &[DrawObject],
refresh: HeadRefresh,
req: TopologyRefresh,
slot: &mut StaticFrameRing,
) -> RenderResult<()> {
let device = alloc.device();
let device5: ID3D12Device5 = device
.cast()
.map_err(|e| map_hresult(e.code(), "ID3D12Device5 cast (topology refresh)"))?;
let cmd4: ID3D12GraphicsCommandList4 = cmd.cast().map_err(|e| {
map_hresult(
e.code(),
"ID3D12GraphicsCommandList4 cast (topology refresh)",
)
})?;
let mut fresh: Vec<Option<ID3D12Resource>> =
(0..refresh.indices().len()).map(|_| None).collect();
let mut fresh_builds: Vec<(D3D12_RAYTRACING_GEOMETRY_DESC, ID3D12Resource)> = Vec::new();
let mut max_scratch: u64 = 0;
for (j, idx) in refresh.fresh_slots() {
let geo = req.shared.draw_geometry(&draw_objects[idx]);
let info = prebuild_info(&device5, &blas_inputs(&geo));
let blas = create_as_buffer(device, info.ResultDataMaxSizeInBytes)?;
max_scratch = max_scratch.max(info.ScratchDataSizeInBytes);
fresh_builds.push((geo, blas.clone()));
fresh[j] = Some(blas);
}
let fresh_gvas: Vec<u64> = fresh
.iter()
.map(|b| b.as_ref().map_or(0, com::gpu_va))
.collect();
self.book
.fill_refresh_instances(&refresh, draw_objects, |model, id, blas| {
instance_desc(model, id, instance_blas_gva(blas, &fresh_gvas, &[]))
});
let instance_count = self.book.instances().len() as u32;
let tlas_pre = prebuild_info(&device5, &tlas_inputs(instance_count, 0));
max_scratch = max_scratch.max(tlas_pre.ScratchDataSizeInBytes);
let tlas_needed = tlas_pre.ResultDataMaxSizeInBytes;
let scratch = create_scratch(device, scratch_capacity(max_scratch))?;
let scratch_gva = com::gpu_va(&scratch);
let instance_buffer = write_upload_ring(
&mut slot.instance,
&mut slot.instance_cap,
alloc,
self.book.instances(),
"RT instance descriptors",
)?;
let geom_table = write_upload_ring(
&mut slot.geom,
&mut slot.geom_cap,
alloc,
self.book.geom_table(),
"RT geometry table",
)?;
let tlas = ensure_as_buffer(&mut slot.tlas, &mut slot.tlas_cap, device, tlas_needed)?;
self.book.check_refresh(&refresh, &fresh)?;
unsafe {
for (geo, dest) in &fresh_builds {
let desc = D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC {
DestAccelerationStructureData: com::gpu_va(dest),
Inputs: blas_inputs(geo),
SourceAccelerationStructureData: 0,
ScratchAccelerationStructureData: scratch_gva,
};
cmd4.BuildRaytracingAccelerationStructure(&desc, None);
cmd.ResourceBarrier(&[uav_barrier()]);
}
let desc = D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC {
DestAccelerationStructureData: com::gpu_va(&tlas),
Inputs: tlas_inputs(instance_count, com::gpu_va(&instance_buffer)),
SourceAccelerationStructureData: 0,
ScratchAccelerationStructureData: scratch_gva,
};
cmd4.BuildRaytracingAccelerationStructure(&desc, None);
cmd.ResourceBarrier(&[uav_barrier()]);
}
alloc.retire(scratch);
let orphans = self.book.commit_refresh(refresh, fresh, draw_objects);
for orphan in orphans.into_iter().chain(self.book.take_parked()) {
alloc.retire(orphan);
}
self.book.release_skinned();
self.skinned_ring.unpublish(self.book.clock());
for ring in self.skinned_ring.slots_mut() {
ring.refit.reset();
}
self.tlas = tlas;
self.geom_table = geom_table;
self.instance_buffer = instance_buffer;
self.tlas_size = tlas_needed;
Ok(())
}
fn rebuild_tlas(
&mut self,
alloc: &DeviceAllocator,
cmd: &ID3D12GraphicsCommandList,
draw_objects: &[DrawObject],
frame_idx: usize,
) -> RenderResult<()> {
let (next, mut slot) = self.static_ring.take_next();
let result = self.rebuild_tlas_into(alloc, cmd, draw_objects, frame_idx, &mut slot);
if result.is_ok() {
self.static_ring.publish(next, slot);
for orphan in self.book.take_parked() {
alloc.retire(orphan);
}
} else {
self.static_ring.put(next, slot);
}
result
}
fn rebuild_tlas_into(
&mut self,
alloc: &DeviceAllocator,
cmd: &ID3D12GraphicsCommandList,
draw_objects: &[DrawObject],
frame_idx: usize,
slot: &mut StaticFrameRing,
) -> RenderResult<()> {
let device = alloc.device();
let device5: ID3D12Device5 = device
.cast()
.map_err(|e| map_hresult(e.code(), "ID3D12Device5 cast (rebuild)"))?;
self.book
.fill_instances(draw_objects, None, |model, id, blas| {
instance_desc(model, id, instance_blas_gva(blas, &[], &[]))
});
let instance_count = self.book.instances().len() as u32;
let instance_buffer = write_upload_ring(
&mut slot.instance,
&mut slot.instance_cap,
alloc,
self.book.instances(),
"RT instance descriptors",
)?;
let geom_table = write_upload_ring(
&mut slot.geom,
&mut slot.geom_cap,
alloc,
self.book.geom_table(),
"RT geometry table",
)?;
let tlas = ensure_as_buffer(&mut slot.tlas, &mut slot.tlas_cap, device, self.tlas_size)?;
let scratch_needed =
prebuild_info(&device5, &tlas_inputs(instance_count, 0)).ScratchDataSizeInBytes;
let scratch_gva = ensure_scratch(&mut self.scratch, device, frame_idx, scratch_needed)?;
let cmd4: ID3D12GraphicsCommandList4 = cmd
.cast()
.map_err(|e| map_hresult(e.code(), "ID3D12GraphicsCommandList4 cast (rebuild)"))?;
let desc = D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC {
DestAccelerationStructureData: com::gpu_va(&tlas),
Inputs: tlas_inputs(instance_count, com::gpu_va(&instance_buffer)),
SourceAccelerationStructureData: 0,
ScratchAccelerationStructureData: scratch_gva,
};
unsafe {
cmd4.BuildRaytracingAccelerationStructure(&desc, None);
cmd.ResourceBarrier(&[uav_barrier()]);
}
self.tlas = tlas;
self.geom_table = geom_table;
self.instance_buffer = instance_buffer;
if self.book.commit_static().is_some() {
self.skinned_ring.unpublish(self.book.clock());
for ring in self.skinned_ring.slots_mut() {
ring.refit.reset();
}
}
Ok(())
}
fn rebuild_skinned(&mut self, req: SkinnedRebuild) -> RenderResult<RtUpdate> {
let frame_idx = req.frame_idx;
let alloc = req.alloc;
let now = self.book.clock();
let Some(mut ring) = self.skinned_ring.take(frame_idx, now)? else {
return Ok(RtUpdate::Skipped);
};
let result = self.rebuild_skinned_into(req, &mut ring);
if result.is_ok() {
self.skinned_ring.publish(frame_idx, ring, now);
for orphan in self.book.take_parked() {
alloc.retire(orphan);
}
} else {
ring.refit.reset();
self.skinned_ring.put(frame_idx, ring);
}
result.map(|()| RtUpdate::Done)
}
fn rebuild_skinned_into(
&mut self,
req: SkinnedRebuild,
ring: &mut SkinnedFrameRing,
) -> RenderResult<()> {
let SkinnedRebuild {
alloc,
cmd,
draw_objects,
skinned,
frame_idx,
full_build,
} = req;
let device = alloc.device();
let device5: ID3D12Device5 = device
.cast()
.map_err(|e| map_hresult(e.code(), "ID3D12Device5 cast (skinned rebuild)"))?;
let cmd4: ID3D12GraphicsCommandList4 = cmd.cast().map_err(|e| {
map_hresult(
e.code(),
"ID3D12GraphicsCommandList4 cast (skinned rebuild)",
)
})?;
let deformed_extent = self.book.skinned_vertex_extent(skinned.objects);
self.book
.fill_skinned_shapes(skinned.objects, deformed_extent as u32);
let deformed_bytes = (deformed_extent * VERTEX_STRIDE).max(VERTEX_STRIDE);
let read_state = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE
| D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
let deformed_realloc =
ring_slot_needs_grow(ring.deformed.is_some(), ring.deformed_cap, deformed_bytes);
if deformed_realloc {
ring.deformed = Some(create_uav_buffer(
device,
deformed_bytes,
D3D12_RESOURCE_STATE_COMMON,
)?);
ring.deformed_cap = deformed_bytes;
}
let deformed_verts = ring.deformed.clone().ok_or_else(missing_slot_buffer)?;
let deformed_gva = com::gpu_va(&deformed_verts);
let deformed_before = if deformed_realloc {
D3D12_RESOURCE_STATE_COMMON
} else {
read_state
};
unsafe {
cmd.ResourceBarrier(&[transition_barrier(
&deformed_verts,
deformed_before,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
)]);
}
let skin = skinned.skin;
unsafe {
cmd.SetComputeRootSignature(&skin.root_sig);
cmd.SetPipelineState(&skin.pso);
}
let visible = self.book.visible_skinned();
for &obj_idx in visible {
let obj = &skinned.objects[obj_idx];
let Some(joint) = skinned.joint_buffers.get(obj_idx) else {
continue;
};
let joint_gva = com::gpu_va(joint);
if joint_gva == 0 {
continue;
}
let params = SkinParams {
vertex_base: obj.vertex_base,
vertex_count: obj.vertex_count as u32,
joint_count: obj.joint_count.max(1) as u32,
target_count: 0,
};
unsafe {
cmd.set_compute_root_constants(0, ¶ms);
cmd.SetComputeRootShaderResourceView(1, skinned.vertex_gva);
cmd.SetComputeRootShaderResourceView(2, joint_gva);
cmd.SetComputeRootUnorderedAccessView(3, deformed_gva);
cmd.SetComputeRootShaderResourceView(4, skinned.vertex_gva);
cmd.SetComputeRootShaderResourceView(5, skinned.vertex_gva);
cmd.Dispatch((obj.vertex_count as u32).div_ceil(64), 1, 1);
}
}
unsafe {
cmd.ResourceBarrier(&[uav_barrier()]);
cmd.ResourceBarrier(&[transition_barrier(
&deformed_verts,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
read_state,
)]);
}
let SkinnedScratch {
geo: skinned_geo,
blas_gvas: skinned_gvas,
} = &mut self.skinned_scratch;
skinned_geo.clear();
skinned_geo.extend(visible.iter().map(|&i| {
let obj = &skinned.objects[i];
skinned_triangle_geometry(
deformed_gva,
deformed_extent as u32,
skinned.index_gva + obj.index_offset as u64 * 4,
obj.index_count as u32,
)
}));
let mut max_scratch: u64 = 0;
let mut storage_changed = deformed_realloc;
for (si, geo) in skinned_geo.iter().enumerate() {
let info = prebuild_info(&device5, &skinned_blas_inputs(geo, BlasUpdate::Build));
let needed = info.ResultDataMaxSizeInBytes;
if si >= ring.blas.len() {
ring.blas.push((create_as_buffer(device, needed)?, needed));
storage_changed = true;
} else if ring_slot_needs_grow(true, ring.blas[si].1, needed) {
ring.blas[si] = (create_as_buffer(device, needed)?, needed);
storage_changed = true;
}
max_scratch = max_scratch
.max(info.ScratchDataSizeInBytes)
.max(info.UpdateScratchDataSizeInBytes);
}
skinned_gvas.clear();
skinned_gvas.extend(
ring.blas
.iter()
.take(skinned_geo.len())
.map(|(b, _)| com::gpu_va(b)),
);
self.book
.fill_instances(draw_objects, Some(skinned.objects), |model, id, blas| {
instance_desc(model, id, instance_blas_gva(blas, &[], skinned_gvas))
});
let instance_count = self.book.instances().len() as u32;
let instance_buffer = write_upload_ring(
&mut ring.instance,
&mut ring.instance_cap,
alloc,
self.book.instances(),
"RT instance descriptors",
)?;
let geom_table = write_upload_ring(
&mut ring.geom,
&mut ring.geom_cap,
alloc,
self.book.geom_table(),
"RT geometry table",
)?;
let tlas_pre = prebuild_info(&device5, &tlas_inputs(instance_count, 0));
max_scratch = max_scratch.max(tlas_pre.ScratchDataSizeInBytes);
let tlas = ensure_as_buffer(
&mut ring.tlas,
&mut ring.tlas_cap,
device,
tlas_pre.ResultDataMaxSizeInBytes,
)?;
let scratch_gva = ensure_scratch(&mut self.scratch, device, frame_idx, max_scratch)?;
let update = ring
.refit
.plan(self.book.skinned_shapes(), storage_changed || full_build);
unsafe {
let SkinnedScratch { geo, blas_gvas } = &self.skinned_scratch;
for (geo, &dest) in geo.iter().zip(blas_gvas) {
let desc = D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC {
DestAccelerationStructureData: dest,
Inputs: skinned_blas_inputs(geo, update),
SourceAccelerationStructureData: match update {
BlasUpdate::Build => 0,
BlasUpdate::Refit => dest,
},
ScratchAccelerationStructureData: scratch_gva,
};
cmd4.BuildRaytracingAccelerationStructure(&desc, None);
cmd.ResourceBarrier(&[uav_barrier()]);
}
let tlas_desc = D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC {
DestAccelerationStructureData: com::gpu_va(&tlas),
Inputs: tlas_inputs(instance_count, com::gpu_va(&instance_buffer)),
SourceAccelerationStructureData: 0,
ScratchAccelerationStructureData: scratch_gva,
};
cmd4.BuildRaytracingAccelerationStructure(&tlas_desc, None);
cmd.ResourceBarrier(&[uav_barrier()]);
}
let count = self.skinned_scratch.geo.len();
self.book
.replace_tail(ring.blas.iter().take(count).map(|(b, _)| b.clone()));
self.tlas = tlas;
self.geom_table = geom_table;
self.instance_buffer = instance_buffer;
self.deformed_verts = deformed_verts;
self.book.commit_skinned();
Ok(())
}
}
struct SkinnedRebuild<'a> {
alloc: &'a DeviceAllocator,
cmd: &'a ID3D12GraphicsCommandList,
draw_objects: &'a [DrawObject],
skinned: &'a SkinnedRtInputs<'a>,
frame_idx: usize,
full_build: bool,
}
impl super::context::DxContext {
pub(in crate::directx) fn encode_skin(
&self,
cmd: &ID3D12GraphicsCommandList,
frame_idx: usize,
) {
let (Some(skin), Some(deformed), Some(vb)) = (
self.skinned.skin_pipeline.as_ref(),
self.skinned.deformed_buffers.get(frame_idx),
self.skinned.vertex_buffer.as_ref(),
) else {
return;
};
if self.state.skinned.draw_objects.is_empty() {
return;
}
let src_gva = com::gpu_va(vb);
let dst_gva = com::gpu_va(deformed);
unsafe {
cmd.ResourceBarrier(&[transition_barrier(
deformed,
D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
)]);
cmd.SetComputeRootSignature(&skin.root_sig);
cmd.SetPipelineState(&skin.pso);
}
for (i, obj) in self.state.skinned.draw_objects.iter().enumerate() {
let joint_gva = self.skinned_joint_gva(frame_idx, i);
let target_count = self
.skinned
.morph_target_counts
.get(i)
.copied()
.unwrap_or(0);
let params = SkinParams {
vertex_base: obj.vertex_base,
vertex_count: obj.vertex_count as u32,
joint_count: obj.joint_count.max(1) as u32,
target_count,
};
let delta_gva = self
.skinned
.morph_delta_buffers
.get(i)
.and_then(|b| b.as_ref())
.map(|b| com::gpu_va(b))
.unwrap_or(src_gva);
let weight_gva = self.morph_weight_gva(frame_idx, i).unwrap_or(src_gva);
unsafe {
cmd.set_compute_root_constants(0, ¶ms);
cmd.SetComputeRootShaderResourceView(1, src_gva);
cmd.SetComputeRootShaderResourceView(2, joint_gva);
cmd.SetComputeRootUnorderedAccessView(3, dst_gva);
cmd.SetComputeRootShaderResourceView(4, delta_gva);
cmd.SetComputeRootShaderResourceView(5, weight_gva);
cmd.Dispatch((obj.vertex_count as u32).div_ceil(64), 1, 1);
}
}
unsafe {
cmd.ResourceBarrier(&[transition_barrier(
deformed,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER,
)]);
}
}
pub(super) fn rt_dynamic_update(&mut self, cmd: &ID3D12GraphicsCommandList, frame_idx: usize) {
let mut topology_dirty = std::mem::take(&mut self.state.gpu_dirty.rt_topology);
self.rt.retire_tick += 1;
self.rt
.retired
.collect(self.rt.retire_tick, FRAMES as u64 + 1);
let skinned_gvas = self.rt_skinned_gvas();
let skinned_present = skinned_gvas.is_some();
if self.rt.accel.is_none() {
if self.rt_reflections.is_none()
|| !seed_wanted(self.rt.dynamic_mode, topology_dirty, skinned_present)
{
return;
}
match self.build_scene_accel(skinned_present) {
Ok(Some(accel)) => self.rt.accel = Some(accel),
Ok(None) => return,
Err(e) => {
crate::rt_report::report_rt_update(&mut self.rt.update_streak, Err(e));
return;
}
}
topology_dirty = false;
}
let joint_buffers: &[PooledBuffer] = self
.skinned
.joint_buffers
.get(frame_idx)
.map(|b| b.as_slice())
.unwrap_or(&[]);
let exclude_seethrough = self.seethrough_meshes_enabled();
let shared = SharedGeometry::of(&self.scene.geometry);
let Some(accel) = self.rt.accel.as_mut() else {
return;
};
let skinned = skinned_gvas
.zip(self.rt.skin.as_ref())
.map(|((v, i), skin)| SkinnedRtInputs {
objects: &self.state.skinned.draw_objects,
vertex_gva: v,
index_gva: i,
joint_buffers,
skin,
});
let updated = accel.dynamic_update(
&self.hw.alloc,
cmd,
&self.state.draw.objects,
RtDynamicInputs {
mode: self.rt.dynamic_mode,
skinned,
frame_idx,
shared,
topology_dirty,
exclude_seethrough,
},
);
crate::rt_report::report_rt_update(&mut self.rt.update_streak, updated);
if self
.rt
.accel
.as_ref()
.is_some_and(|a| a.is_spent(skinned_present))
&& let Some(accel) = self.rt.accel.take()
{
self.rt.retired.push(self.rt.retire_tick, accel);
}
}
fn rt_skinned_gvas(&self) -> Option<(u64, u64)> {
if !self.rt.skinned_geometry
|| self.rt.skin.is_none()
|| self.state.skinned.draw_objects.is_empty()
{
return None;
}
let vb = self.skinned.vertex_buffer.as_ref()?;
let ib = self.skinned.index_buffer.as_ref()?;
Some((com::gpu_va(vb), com::gpu_va(ib)))
}
pub(super) fn rt_skinned_present(&self) -> bool {
self.rt_skinned_gvas().is_some()
}
pub(super) fn rebuild_rt_accel(&mut self) {
let skinned_present = self.rt_skinned_present();
self.rt.accel = self.build_scene_accel(skinned_present).unwrap_or_else(|e| {
tracing::warn!("RT acceleration-structure build failed: {e}");
None
});
}
pub(super) fn build_scene_accel(
&self,
skinned_present: bool,
) -> RenderResult<Option<RtAccelData>> {
build_rt_accel(RtInitGeometry {
alloc: &self.hw.alloc,
shared: SharedGeometry::of(&self.scene.geometry),
draw_objects: &self.state.draw.objects,
clusters: &self.instanced.clusters,
albedo_count: self.scene.textures.len() as u32,
exclude_seethrough: self.seethrough_meshes_enabled(),
skinned_present,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ring_slot_grows_when_empty_or_undersized_only() {
assert!(ring_slot_needs_grow(false, 0, 0));
assert!(ring_slot_needs_grow(false, 0, 1024));
assert!(!ring_slot_needs_grow(true, 1024, 1024));
assert!(!ring_slot_needs_grow(true, 4096, 1024));
assert!(ring_slot_needs_grow(true, 512, 1024));
}
#[test]
fn an_empty_upload_slot_still_holds_one_element() {
assert_eq!(
upload_ring_size::<concinnity_core::gfx::render_types::RtGeomEntry>(&[]),
128
);
assert_eq!(upload_ring_size::<u8>(&[]), 4);
assert_eq!(upload_ring_size(&[0u32; 3]), 12);
}
#[test]
fn scratch_capacity_never_drops_below_the_buffer_minimum() {
assert_eq!(scratch_capacity(1024), 1024);
assert_eq!(scratch_capacity(257), 257);
assert_eq!(scratch_capacity(0), 256);
assert_eq!(scratch_capacity(255), 256);
}
#[test]
fn instance_desc_packs_id_and_full_mask() {
let d = instance_desc(
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
],
7,
0xDEAD_BEEF,
);
assert_eq!(d._bitfield1 & 0x00FF_FFFF, 7);
assert_eq!(d._bitfield1 >> 24, 0xFF);
assert_eq!(d._bitfield2, 0);
assert_eq!(d.AccelerationStructure, 0xDEAD_BEEF);
}
}