use windows::Win32::Graphics::Direct3D12::*;
use windows::Win32::Graphics::Dxgi::Common::*;
use windows::core::Interface;
use super::allocator::{DeviceAllocator, PooledBuffer};
use crate::gfx::render_types::{DrawObject, InstancedCluster, RtGeomEntry, SkinnedDrawObject};
use crate::gfx::rt_geom::{cluster_geom_entry, geom_entry, models_dirty, skinned_geom_entry};
use crate::gfx::rt_refit::{BlasUpdate, SkinnedRefit, SkinnedShape};
use crate::gfx::rt_topology::{GeomSig, plan_topology_refresh};
pub(super) use crate::gfx::rt_geom::RtDynamicMode;
use super::com;
use super::context::FRAMES;
use super::texture::{create_buffer, create_uav_buffer, transition_barrier};
use crate::directx::slang_builtins::SlangCompile;
const VERTEX_STRIDE: u64 = 56;
pub(in crate::directx) 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
}
pub(super) fn pack_instance_transform(model: [[f32; 4]; 4]) -> [f32; 12] {
[
model[0][0],
model[1][0],
model[2][0],
model[3][0],
model[0][1],
model[1][1],
model[2][1],
model[3][1],
model[0][2],
model[1][2],
model[2][2],
model[3][2],
]
}
fn instance_desc(
model: [[f32; 4]; 4],
instance_id: u32,
blas_gva: u64,
) -> D3D12_RAYTRACING_INSTANCE_DESC {
D3D12_RAYTRACING_INSTANCE_DESC {
Transform: pack_instance_transform(model),
_bitfield1: (instance_id & 0x00FF_FFFF) | (0xFFu32 << 24),
_bitfield2: 0,
AccelerationStructure: blas_gva,
}
}
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,
},
},
},
}
}
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) -> Result<ID3D12Resource, String> {
create_uav_buffer(
device,
size.max(256),
D3D12_RESOURCE_STATE_RAYTRACING_ACCELERATION_STRUCTURE,
)
}
fn create_scratch(device: &ID3D12Device, size: u64) -> Result<ID3D12Resource, String> {
create_uav_buffer(device, size.max(256), D3D12_RESOURCE_STATE_COMMON)
}
fn upload_slice<T: Copy>(
alloc: &DeviceAllocator,
data: &[T],
label: &str,
) -> Result<PooledBuffer, String> {
let bytes = std::mem::size_of_val(data).max(16) as u64;
let buf = create_buffer(
alloc,
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| format!("map {label}: {e}"))?;
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,
}
const SKIN_PARAMS_DWORDS: u32 = 4;
fn create_skin_root_signature(device: &ID3D12Device) -> Result<ID3D12RootSignature, String> {
let params = [
D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS,
Anonymous: D3D12_ROOT_PARAMETER_0 {
Constants: D3D12_ROOT_CONSTANTS {
ShaderRegister: 0,
RegisterSpace: 0,
Num32BitValues: SKIN_PARAMS_DWORDS,
},
},
ShaderVisibility: D3D12_SHADER_VISIBILITY_ALL,
},
D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_SRV,
Anonymous: D3D12_ROOT_PARAMETER_0 {
Descriptor: D3D12_ROOT_DESCRIPTOR {
ShaderRegister: 0,
RegisterSpace: 0,
},
},
ShaderVisibility: D3D12_SHADER_VISIBILITY_ALL,
},
D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_SRV,
Anonymous: D3D12_ROOT_PARAMETER_0 {
Descriptor: D3D12_ROOT_DESCRIPTOR {
ShaderRegister: 1,
RegisterSpace: 0,
},
},
ShaderVisibility: D3D12_SHADER_VISIBILITY_ALL,
},
D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_UAV,
Anonymous: D3D12_ROOT_PARAMETER_0 {
Descriptor: D3D12_ROOT_DESCRIPTOR {
ShaderRegister: 0,
RegisterSpace: 0,
},
},
ShaderVisibility: D3D12_SHADER_VISIBILITY_ALL,
},
D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_SRV,
Anonymous: D3D12_ROOT_PARAMETER_0 {
Descriptor: D3D12_ROOT_DESCRIPTOR {
ShaderRegister: 2,
RegisterSpace: 0,
},
},
ShaderVisibility: D3D12_SHADER_VISIBILITY_ALL,
},
D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_SRV,
Anonymous: D3D12_ROOT_PARAMETER_0 {
Descriptor: D3D12_ROOT_DESCRIPTOR {
ShaderRegister: 3,
RegisterSpace: 0,
},
},
ShaderVisibility: D3D12_SHADER_VISIBILITY_ALL,
},
];
let desc = D3D12_ROOT_SIGNATURE_DESC {
NumParameters: params.len() as u32,
pParameters: params.as_ptr(),
Flags: D3D12_ROOT_SIGNATURE_FLAG_NONE,
..Default::default()
};
super::pipeline::serialize_desc_and_create(device, &desc, "rt skin root sig")
}
fn build_skin_pipeline(device: &ID3D12Device, hot_reload: bool) -> Result<SkinPipeline, String> {
let cs = super::slang_builtins::RT_SKIN.compile(hot_reload)?;
let root_sig = create_skin_root_signature(device)?;
let desc = D3D12_COMPUTE_PIPELINE_STATE_DESC {
pRootSignature: com::borrowed(&root_sig),
CS: D3D12_SHADER_BYTECODE {
pShaderBytecode: cs.as_ptr() as _,
BytecodeLength: cs.len(),
},
..Default::default()
};
let pso = unsafe { crate::directx::pso_library::create_compute(device, &desc) }
.map_err(|e| format!("create rt skin PSO: {e}"))?;
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],
}
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,
scratch: Option<ID3D12Resource>,
scratch_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 next_slot(cursor: usize, len: usize) -> usize {
(cursor + 1) % len.max(1)
}
fn collect_models(
object_indices: &[usize],
draw_objects: &[DrawObject],
out: &mut Vec<[[f32; 4]; 4]>,
) -> bool {
out.clear();
for &idx in object_indices {
match draw_objects.get(idx) {
Some(o) if o.resident && o.index_count >= 3 => out.push(o.model),
_ => return false,
}
}
true
}
#[derive(Default)]
struct RtUpdateScratch {
skinned: Vec<usize>,
models: Vec<[[f32; 4]; 4]>,
shapes: Vec<SkinnedShape>,
geo: Vec<D3D12_RAYTRACING_GEOMETRY_DESC>,
instances: Vec<D3D12_RAYTRACING_INSTANCE_DESC>,
geom: Vec<RtGeomEntry>,
}
struct RetiredBlas {
free_at: u64,
#[expect(
dead_code,
reason = "held so the orphaned BLAS and scratch stay alive until free_at passes"
)]
resources: Vec<ID3D12Resource>,
}
fn write_upload_ring<T: Copy>(
slot: &mut Option<PooledBuffer>,
cap: &mut u64,
alloc: &DeviceAllocator,
data: &[T],
label: &str,
) -> Result<(), String> {
let len_bytes = std::mem::size_of_val(data);
let needed = (len_bytes as u64).max(4);
if ring_slot_needs_grow(slot.is_some(), *cap, needed) {
*slot = Some(
create_buffer(
alloc,
needed,
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
)
.map_err(|e| format!("{label}: {e}"))?,
);
*cap = needed;
}
let buf = slot
.as_ref()
.expect("the upload buffer was just allocated or already met the capacity");
let mut ptr = std::ptr::null_mut::<std::ffi::c_void>();
unsafe {
buf.Map(0, None, Some(&mut ptr))
.map_err(|e| format!("{label} map: {e}"))?;
std::ptr::copy_nonoverlapping(data.as_ptr() as *const u8, ptr as *mut u8, len_bytes);
buf.Unmap(0, None);
}
Ok(())
}
pub(super) struct RtAccelData {
blas: Vec<ID3D12Resource>,
static_blas_count: usize,
tlas: ID3D12Resource,
geom_table: PooledBuffer,
instance_buffer: PooledBuffer,
scratch: ID3D12Resource,
tlas_size: u64,
object_indices: Vec<usize>,
draw_blas_sigs: Vec<GeomSig>,
cached_models: Vec<[[f32; 4]; 4]>,
cluster_instances: Vec<D3D12_RAYTRACING_INSTANCE_DESC>,
cluster_geom: Vec<RtGeomEntry>,
static_ring: Vec<StaticFrameRing>,
static_cursor: usize,
skinned_ring: Vec<SkinnedFrameRing>,
skin: Option<SkinPipeline>,
deformed_verts: ID3D12Resource,
skinned_indices: PooledBuffer,
has_skinned: bool,
albedo_count: u32,
total_vertices: u32,
vbuf_gva: u64,
ibuf_gva: u64,
retire: Vec<RetiredBlas>,
frame_counter: u64,
update_scratch: RtUpdateScratch,
}
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 set_skin_pipeline(&mut self, skin: SkinPipeline) {
self.skin = Some(skin);
}
}
pub(super) fn build_rt_skin_pipeline(
device: &ID3D12Device,
hot_reload: bool,
) -> Result<SkinPipeline, String> {
build_skin_pipeline(device, hot_reload)
}
fn participates_in_bvh(o: &DrawObject, exclude_seethrough: bool) -> bool {
o.resident && o.index_count >= 3 && !(exclude_seethrough && o.material.see_through != 0)
}
#[derive(Clone, Copy)]
pub(super) struct RtInitGeometry<'a> {
pub alloc: &'a DeviceAllocator,
pub vertex_buffer: &'a ID3D12Resource,
pub index_buffer: &'a ID3D12Resource,
pub draw_objects: &'a [DrawObject],
pub clusters: &'a [InstancedCluster],
pub total_vertices: usize,
pub albedo_count: u32,
pub exclude_seethrough: bool,
}
pub(super) struct RtDynamicInputs<'a> {
pub mode: RtDynamicMode,
pub skinned: Option<SkinnedRtInputs<'a>>,
pub frame_idx: usize,
pub topology_dirty: bool,
pub exclude_seethrough: bool,
}
pub(super) fn build_rt_accel(geometry: RtInitGeometry) -> Result<Option<RtAccelData>, String> {
let RtInitGeometry {
alloc,
vertex_buffer,
index_buffer,
draw_objects,
clusters,
total_vertices,
albedo_count,
exclude_seethrough,
} = geometry;
let device = alloc.device();
let queue = alloc.queue();
let device5: ID3D12Device5 = device
.cast()
.map_err(|e| format!("ID3D12Device5 cast (DXR unsupported?): {e}"))?;
let object_indices: Vec<usize> = draw_objects
.iter()
.enumerate()
.filter(|(_, o)| participates_in_bvh(o, exclude_seethrough))
.map(|(i, _)| i)
.collect();
let cluster_list: Vec<(usize, &InstancedCluster)> = clusters
.iter()
.enumerate()
.filter(|(_, c)| c.index_count >= 3 && !c.instances.is_empty())
.collect();
if object_indices.is_empty() && cluster_list.is_empty() {
return Ok(None);
}
let vbuf_gva = com::gpu_va(vertex_buffer);
let ibuf_gva = com::gpu_va(index_buffer);
let mut geo_descs: Vec<D3D12_RAYTRACING_GEOMETRY_DESC> =
Vec::with_capacity(object_indices.len() + cluster_list.len());
for &i in &object_indices {
let obj = &draw_objects[i];
let base_vertex = obj.base_vertex as u64;
let vcount = (total_vertices as u64).saturating_sub(base_vertex) as u32;
geo_descs.push(triangle_geometry(
vbuf_gva + base_vertex * VERTEX_STRIDE,
vcount,
ibuf_gva + obj.index_offset as u64 * 4,
obj.index_count as u32,
));
}
for (_, c) in &cluster_list {
geo_descs.push(triangle_geometry(
vbuf_gva,
total_vertices as u32,
ibuf_gva + c.index_offset as u64 * 4,
c.index_count as u32,
));
}
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 draw_blas_count = object_indices.len();
let mut instance_descs: Vec<D3D12_RAYTRACING_INSTANCE_DESC> =
Vec::with_capacity(object_indices.len());
let mut geom_entries: Vec<RtGeomEntry> = Vec::with_capacity(object_indices.len());
for (slot, &i) in object_indices.iter().enumerate() {
let obj = &draw_objects[i];
instance_descs.push(instance_desc(
obj.model,
slot as u32,
com::gpu_va(&blas[slot]),
));
geom_entries.push(geom_entry(obj, albedo_count));
}
let mut cluster_instances: Vec<D3D12_RAYTRACING_INSTANCE_DESC> = Vec::new();
let mut cluster_geom: Vec<RtGeomEntry> = Vec::new();
for (ci, (_cluster_idx, c)) in cluster_list.iter().enumerate() {
let blas_gva = com::gpu_va(&blas[draw_blas_count + ci]);
for model in &c.instances {
let id = (instance_descs.len() + cluster_instances.len()) as u32;
cluster_instances.push(instance_desc(*model, id, blas_gva));
cluster_geom.push(cluster_geom_entry(c, *model, albedo_count));
}
}
instance_descs.extend_from_slice(&cluster_instances);
geom_entries.extend_from_slice(&cluster_geom);
let instance_buffer = upload_slice(alloc, &instance_descs, "RT instance descriptors")?;
let geom_table = upload_slice(alloc, &geom_entries, "RT geometry table")?;
let tlas_pre = prebuild_info(&device5, &tlas_inputs(instance_descs.len() as u32, 0));
max_scratch = max_scratch.max(tlas_pre.ScratchDataSizeInBytes);
let tlas = create_as_buffer(device, tlas_pre.ResultDataMaxSizeInBytes)?;
let scratch = create_scratch(device, max_scratch)?;
let scratch_gva = com::gpu_va(&scratch);
record_builds(alloc, queue, |cmd4| unsafe {
for (slot, geo) in geo_descs.iter().enumerate() {
let desc = D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC {
DestAccelerationStructureData: com::gpu_va(&blas[slot]),
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_descs.len() as u32, com::gpu_va(&instance_buffer)),
SourceAccelerationStructureData: 0,
ScratchAccelerationStructureData: scratch_gva,
};
cmd4.BuildRaytracingAccelerationStructure(&tlas_desc, None);
})?;
let cached_models = object_indices
.iter()
.map(|&i| draw_objects[i].model)
.collect();
let draw_blas_sigs = object_indices
.iter()
.map(|&i| GeomSig::of(&draw_objects[i]))
.collect();
let deformed_verts = create_uav_buffer(device, VERTEX_STRIDE, D3D12_RESOURCE_STATE_COMMON)?;
let skinned_indices = create_buffer(
alloc,
4,
D3D12_HEAP_TYPE_DEFAULT,
D3D12_RESOURCE_STATE_COMMON,
)?;
let static_blas_count = blas.len();
let mut static_ring: Vec<StaticFrameRing> =
(0..FRAMES).map(|_| StaticFrameRing::default()).collect();
static_ring[0] = StaticFrameRing {
tlas: Some(tlas.clone()),
tlas_cap: tlas_pre.ResultDataMaxSizeInBytes.max(256),
instance: Some(instance_buffer.clone()),
instance_cap: (std::mem::size_of_val(instance_descs.as_slice()) as u64).max(16),
geom: Some(geom_table.clone()),
geom_cap: (std::mem::size_of_val(geom_entries.as_slice()) as u64).max(16),
};
Ok(Some(RtAccelData {
blas,
static_blas_count,
tlas,
geom_table,
instance_buffer,
scratch,
tlas_size: tlas_pre.ResultDataMaxSizeInBytes,
object_indices,
draw_blas_sigs,
cached_models,
cluster_instances,
cluster_geom,
retire: Vec::new(),
frame_counter: 0,
update_scratch: RtUpdateScratch::default(),
static_ring,
static_cursor: 0,
skinned_ring: (0..FRAMES).map(|_| SkinnedFrameRing::default()).collect(),
skin: None,
deformed_verts,
skinned_indices,
has_skinned: false,
albedo_count,
total_vertices: total_vertices as u32,
vbuf_gva,
ibuf_gva,
}))
}
fn record_builds<F>(
alloc: &DeviceAllocator,
queue: &ID3D12CommandQueue,
record: F,
) -> Result<(), String>
where
F: FnOnce(&ID3D12GraphicsCommandList4),
{
let device = alloc.device();
let alloc: ID3D12CommandAllocator =
unsafe { device.CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT) }
.map_err(|e| format!("RT build allocator: {e}"))?;
let cmd: ID3D12GraphicsCommandList =
unsafe { device.CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, &alloc, None) }
.map_err(|e| format!("RT build cmd list: {e}"))?;
let cmd4: ID3D12GraphicsCommandList4 = cmd
.cast()
.map_err(|e| format!("ID3D12GraphicsCommandList4 cast: {e}"))?;
record(&cmd4);
unsafe { cmd.Close() }.map_err(|e| format!("RT build close: {e}"))?;
let list: ID3D12CommandList = cmd.cast().map_err(|e| format!("RT build cast: {e}"))?;
unsafe { queue.ExecuteCommandLists(&[Some(list)]) };
let fence: ID3D12Fence = unsafe { device.CreateFence(0, D3D12_FENCE_FLAG_NONE) }
.map_err(|e| format!("RT build fence: {e}"))?;
let event =
unsafe { windows::Win32::System::Threading::CreateEventW(None, false, false, None) }
.map_err(|e| format!("RT build event: {e}"))?;
unsafe { queue.Signal(&fence, 1) }.map_err(|e| format!("RT build signal: {e}"))?;
if unsafe { fence.GetCompletedValue() } < 1 {
unsafe { fence.SetEventOnCompletion(1, event) }
.map_err(|e| format!("RT build set event: {e}"))?;
unsafe { windows::Win32::System::Threading::WaitForSingleObject(event, u32::MAX) };
}
unsafe { windows::Win32::Foundation::CloseHandle(event) }.ok();
Ok(())
}
impl RtAccelData {
pub(super) fn dynamic_update(
&mut self,
alloc: &DeviceAllocator,
cmd: &ID3D12GraphicsCommandList,
draw_objects: &[DrawObject],
inputs: RtDynamicInputs,
) {
let mut scratch = std::mem::take(&mut self.update_scratch);
self.dynamic_update_inner(alloc, cmd, draw_objects, inputs, &mut scratch);
self.update_scratch = scratch;
}
fn dynamic_update_inner(
&mut self,
alloc: &DeviceAllocator,
cmd: &ID3D12GraphicsCommandList,
draw_objects: &[DrawObject],
inputs: RtDynamicInputs,
scratch: &mut RtUpdateScratch,
) {
let RtDynamicInputs {
mode,
skinned,
frame_idx,
topology_dirty,
exclude_seethrough,
} = inputs;
self.frame_counter += 1;
let now = self.frame_counter;
let mut i = 0;
while i < self.retire.len() {
if self.retire[i].free_at <= now {
self.retire.swap_remove(i);
} else {
i += 1;
}
}
if !mode.is_dynamic() {
return;
}
scratch.skinned.clear();
if let (Some(_), Some(s)) = (&self.skin, &skinned) {
scratch.skinned.extend(
s.objects
.iter()
.enumerate()
.filter(|(_, o)| o.visible && o.index_count >= 3)
.map(|(i, _)| i),
);
}
if topology_dirty
&& let Err(e) = self.refresh_topology(alloc, cmd, draw_objects, exclude_seethrough, now)
{
tracing::warn!("RT topology refresh failed (keeping live BVH): {e}");
}
if !scratch.skinned.is_empty() {
let s = skinned.expect("scratch.skinned non-empty implies inputs present");
if !collect_models(&self.object_indices, draw_objects, &mut scratch.models) {
return;
}
if let Err(e) = self.rebuild_skinned(alloc, cmd, draw_objects, &s, frame_idx, scratch) {
tracing::warn!("RT skinned rebuild failed (keeping live BVH): {e}");
}
return;
}
if topology_dirty {
return;
}
if !collect_models(&self.object_indices, draw_objects, &mut scratch.models) {
return;
}
let needs_rebuild = match mode {
RtDynamicMode::Auto => {
self.has_skinned || models_dirty(&self.cached_models, &scratch.models)
}
RtDynamicMode::Rebuild | RtDynamicMode::Tlas => true,
RtDynamicMode::Off => false,
};
if !needs_rebuild {
return;
}
if let Err(e) = self.rebuild_tlas(alloc, cmd, draw_objects, scratch) {
tracing::warn!("RT dynamic TLAS rebuild failed (keeping live BVH): {e}");
}
}
fn refresh_topology(
&mut self,
alloc: &DeviceAllocator,
cmd: &ID3D12GraphicsCommandList,
draw_objects: &[DrawObject],
exclude_seethrough: bool,
now: u64,
) -> Result<(), String> {
let device = alloc.device();
let device5: ID3D12Device5 = device
.cast()
.map_err(|e| format!("ID3D12Device5 cast (topology refresh): {e}"))?;
let cmd4: ID3D12GraphicsCommandList4 = cmd
.cast()
.map_err(|e| format!("ID3D12GraphicsCommandList4 cast (topology refresh): {e}"))?;
let new_indices: Vec<usize> = draw_objects
.iter()
.enumerate()
.filter(|(_, o)| participates_in_bvh(o, exclude_seethrough))
.map(|(i, _)| i)
.collect();
let new_sigs: Vec<GeomSig> = new_indices
.iter()
.map(|&i| GeomSig::of(&draw_objects[i]))
.collect();
if new_indices.is_empty() && self.cluster_instances.is_empty() {
return Ok(());
}
let new_draw_count = new_indices.len();
let mut rebaked_clusters = self.cluster_instances.clone();
for (ci, inst) in rebaked_clusters.iter_mut().enumerate() {
let id = (new_draw_count + ci) as u32;
inst._bitfield1 = (id & 0x00FF_FFFF) | (0xFFu32 << 24);
}
let plan = plan_topology_refresh(
&self.object_indices,
&self.draw_blas_sigs,
&new_indices,
&new_sigs,
);
let old_draw_count = self.object_indices.len();
let cluster_count = self.static_blas_count - old_draw_count;
let mut new_draw_blas: Vec<ID3D12Resource> = Vec::with_capacity(new_indices.len());
let mut fresh_builds: Vec<(D3D12_RAYTRACING_GEOMETRY_DESC, ID3D12Resource)> = Vec::new();
let mut max_scratch: u64 = 0;
for (j, reuse) in plan.reuse.iter().enumerate() {
match reuse {
Some(k) => new_draw_blas.push(self.blas[*k].clone()),
None => {
let obj = &draw_objects[new_indices[j]];
let base_vertex = obj.base_vertex as u64;
let vcount = (self.total_vertices as u64).saturating_sub(base_vertex) as u32;
let geo = triangle_geometry(
self.vbuf_gva + base_vertex * VERTEX_STRIDE,
vcount,
self.ibuf_gva + obj.index_offset as u64 * 4,
obj.index_count as u32,
);
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()));
new_draw_blas.push(blas);
}
}
}
let mut orphans: Vec<ID3D12Resource> =
plan.retire.iter().map(|&k| self.blas[k].clone()).collect();
let cluster_blas: Vec<ID3D12Resource> =
self.blas[old_draw_count..self.static_blas_count].to_vec();
let mut new_blas = new_draw_blas;
new_blas.extend(cluster_blas);
let new_static_blas_count = new_indices.len() + cluster_count;
let mut instance_descs: Vec<D3D12_RAYTRACING_INSTANCE_DESC> =
Vec::with_capacity(new_indices.len() + rebaked_clusters.len());
let mut geom_entries: Vec<RtGeomEntry> = Vec::with_capacity(instance_descs.capacity());
for (slot, &idx) in new_indices.iter().enumerate() {
let obj = &draw_objects[idx];
instance_descs.push(instance_desc(
obj.model,
slot as u32,
com::gpu_va(&new_blas[slot]),
));
geom_entries.push(geom_entry(obj, self.albedo_count));
}
instance_descs.extend_from_slice(&rebaked_clusters);
geom_entries.extend_from_slice(&self.cluster_geom);
let tlas_pre = prebuild_info(&device5, &tlas_inputs(instance_descs.len() as u32, 0));
max_scratch = max_scratch.max(tlas_pre.ScratchDataSizeInBytes);
let tlas_needed = tlas_pre.ResultDataMaxSizeInBytes;
let scratch = create_scratch(device, max_scratch.max(256))?;
let scratch_gva = com::gpu_va(&scratch);
self.static_cursor = next_slot(self.static_cursor, self.static_ring.len());
let mut slot = std::mem::take(&mut self.static_ring[self.static_cursor]);
write_upload_ring(
&mut slot.instance,
&mut slot.instance_cap,
alloc,
&instance_descs,
"RT instance descriptors",
)?;
write_upload_ring(
&mut slot.geom,
&mut slot.geom_cap,
alloc,
&geom_entries,
"RT geometry table",
)?;
if ring_slot_needs_grow(slot.tlas.is_some(), slot.tlas_cap, tlas_needed) {
slot.tlas = Some(create_as_buffer(device, tlas_needed)?);
slot.tlas_cap = tlas_needed;
}
let instance_buffer = slot
.instance
.clone()
.expect("RT instance buffer was sized by write_upload_ring above");
let geom_table = slot
.geom
.clone()
.expect("RT geometry table was sized by write_upload_ring above");
let tlas = slot.tlas.clone().expect("RT TLAS buffer was sized above");
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_descs.len() as u32, com::gpu_va(&instance_buffer)),
SourceAccelerationStructureData: 0,
ScratchAccelerationStructureData: scratch_gva,
};
cmd4.BuildRaytracingAccelerationStructure(&desc, None);
cmd.ResourceBarrier(&[uav_barrier()]);
}
self.blas = new_blas;
self.static_blas_count = new_static_blas_count;
self.draw_blas_sigs = new_sigs;
self.cluster_instances = rebaked_clusters;
self.has_skinned = false;
for ring in &mut self.skinned_ring {
ring.refit.reset();
}
self.tlas = tlas;
self.geom_table = geom_table;
self.instance_buffer = instance_buffer;
self.tlas_size = tlas_needed;
self.static_ring[self.static_cursor] = slot;
self.cached_models = new_indices.iter().map(|&i| draw_objects[i].model).collect();
self.object_indices = new_indices;
orphans.push(scratch);
self.retire.push(RetiredBlas {
free_at: now + FRAMES as u64,
resources: orphans,
});
Ok(())
}
fn rebuild_tlas(
&mut self,
alloc: &DeviceAllocator,
cmd: &ID3D12GraphicsCommandList,
draw_objects: &[DrawObject],
scratch: &mut RtUpdateScratch,
) -> Result<(), String> {
let device = alloc.device();
let RtUpdateScratch {
models,
instances: instance_descs,
geom: geom_entries,
..
} = scratch;
instance_descs.clear();
geom_entries.clear();
for (slot, &idx) in self.object_indices.iter().enumerate() {
let obj = &draw_objects[idx];
instance_descs.push(instance_desc(
obj.model,
slot as u32,
com::gpu_va(&self.blas[slot]),
));
geom_entries.push(geom_entry(obj, self.albedo_count));
}
instance_descs.extend_from_slice(&self.cluster_instances);
geom_entries.extend_from_slice(&self.cluster_geom);
self.static_cursor = next_slot(self.static_cursor, self.static_ring.len());
let mut slot = std::mem::take(&mut self.static_ring[self.static_cursor]);
write_upload_ring(
&mut slot.instance,
&mut slot.instance_cap,
alloc,
instance_descs,
"RT instance descriptors",
)?;
write_upload_ring(
&mut slot.geom,
&mut slot.geom_cap,
alloc,
geom_entries,
"RT geometry table",
)?;
if ring_slot_needs_grow(slot.tlas.is_some(), slot.tlas_cap, self.tlas_size) {
slot.tlas = Some(create_as_buffer(device, self.tlas_size)?);
slot.tlas_cap = self.tlas_size;
}
let instance_buffer = slot
.instance
.clone()
.expect("RT instance buffer was sized by write_upload_ring above");
let geom_table = slot
.geom
.clone()
.expect("RT geometry table was sized by write_upload_ring above");
let tlas = slot.tlas.clone().expect("RT TLAS buffer was sized above");
let cmd4: ID3D12GraphicsCommandList4 = cmd
.cast()
.map_err(|e| format!("ID3D12GraphicsCommandList4 cast (rebuild): {e}"))?;
let desc = D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC {
DestAccelerationStructureData: com::gpu_va(&tlas),
Inputs: tlas_inputs(instance_descs.len() as u32, com::gpu_va(&instance_buffer)),
SourceAccelerationStructureData: 0,
ScratchAccelerationStructureData: com::gpu_va(&self.scratch),
};
unsafe {
cmd4.BuildRaytracingAccelerationStructure(&desc, None);
cmd.ResourceBarrier(&[uav_barrier()]);
}
self.tlas = tlas;
self.geom_table = geom_table;
self.instance_buffer = instance_buffer;
if self.blas.len() > self.static_blas_count {
self.blas.truncate(self.static_blas_count);
self.has_skinned = false;
for ring in &mut self.skinned_ring {
ring.refit.reset();
}
}
self.static_ring[self.static_cursor] = slot;
self.cached_models.clear();
self.cached_models.extend_from_slice(models);
Ok(())
}
fn rebuild_skinned(
&mut self,
alloc: &DeviceAllocator,
cmd: &ID3D12GraphicsCommandList,
draw_objects: &[DrawObject],
skinned: &SkinnedRtInputs,
frame_idx: usize,
scratch: &mut RtUpdateScratch,
) -> Result<(), String> {
let RtUpdateScratch {
skinned: skinned_objects,
models,
shapes,
geo: skinned_geo,
instances: instance_descs,
geom: geom_entries,
} = scratch;
let device = alloc.device();
let device5: ID3D12Device5 = device
.cast()
.map_err(|e| format!("ID3D12Device5 cast (skinned rebuild): {e}"))?;
let cmd4: ID3D12GraphicsCommandList4 = cmd
.cast()
.map_err(|e| format!("ID3D12GraphicsCommandList4 cast (skinned rebuild): {e}"))?;
let mut ring = std::mem::take(&mut self.skinned_ring[frame_idx]);
let deformed_extent: u64 = skinned_objects
.iter()
.map(|&i| {
skinned.objects[i].vertex_base as u64 + skinned.objects[i].vertex_count as u64
})
.max()
.unwrap_or(0);
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()
.expect("deformed vertex buffer was sized above");
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 = self
.skin
.as_ref()
.ok_or("rebuild_skinned called without a skin pipeline")?;
unsafe {
cmd.SetComputeRootSignature(&skin.root_sig);
cmd.SetPipelineState(&skin.pso);
}
}
for &obj_idx in skinned_objects.iter() {
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.SetComputeRoot32BitConstants(
0,
SKIN_PARAMS_DWORDS,
¶ms as *const SkinParams as *const std::ffi::c_void,
0,
);
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 skinned_idx_gva = skinned.index_gva;
skinned_geo.clear();
shapes.clear();
for &i in skinned_objects.iter() {
let obj = &skinned.objects[i];
skinned_geo.push(skinned_triangle_geometry(
deformed_gva,
deformed_extent as u32,
skinned_idx_gva + obj.index_offset as u64 * 4,
obj.index_count as u32,
));
shapes.push(SkinnedShape {
index_offset: obj.index_offset,
index_count: obj.index_count,
vertex_extent: deformed_extent 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);
}
instance_descs.clear();
geom_entries.clear();
for (slot, &idx) in self.object_indices.iter().enumerate() {
let obj = &draw_objects[idx];
instance_descs.push(instance_desc(
obj.model,
slot as u32,
com::gpu_va(&self.blas[slot]),
));
geom_entries.push(geom_entry(obj, self.albedo_count));
}
instance_descs.extend_from_slice(&self.cluster_instances);
geom_entries.extend_from_slice(&self.cluster_geom);
for (si, &obj_idx) in skinned_objects.iter().enumerate() {
let obj = &skinned.objects[obj_idx];
let id = instance_descs.len() as u32;
let blas_gva = com::gpu_va(&ring.blas[si].0);
instance_descs.push(instance_desc(obj.model, id, blas_gva));
geom_entries.push(skinned_geom_entry(obj, self.albedo_count));
}
write_upload_ring(
&mut ring.instance,
&mut ring.instance_cap,
alloc,
instance_descs,
"RT instance descriptors",
)?;
write_upload_ring(
&mut ring.geom,
&mut ring.geom_cap,
alloc,
geom_entries,
"RT geometry table",
)?;
let instance_buffer = ring
.instance
.clone()
.expect("RT instance buffer was sized by write_upload_ring above");
let geom_table = ring
.geom
.clone()
.expect("RT geometry table was sized by write_upload_ring above");
let tlas_pre = prebuild_info(&device5, &tlas_inputs(instance_descs.len() as u32, 0));
max_scratch = max_scratch.max(tlas_pre.ScratchDataSizeInBytes);
let tlas_needed = tlas_pre.ResultDataMaxSizeInBytes;
if ring_slot_needs_grow(ring.tlas.is_some(), ring.tlas_cap, tlas_needed) {
ring.tlas = Some(create_as_buffer(device, tlas_needed)?);
ring.tlas_cap = tlas_needed;
}
let scratch_needed = max_scratch.max(256);
if ring_slot_needs_grow(ring.scratch.is_some(), ring.scratch_cap, scratch_needed) {
ring.scratch = Some(create_scratch(device, scratch_needed)?);
ring.scratch_cap = scratch_needed;
}
let tlas = ring.tlas.clone().expect("RT TLAS buffer was sized above");
let scratch_buffer = ring
.scratch
.clone()
.expect("RT scratch buffer was sized above");
let scratch_gva = com::gpu_va(&scratch_buffer);
let update = ring.refit.plan(shapes, storage_changed);
unsafe {
for (si, geo) in skinned_geo.iter().enumerate() {
let dest = com::gpu_va(&ring.blas[si].0);
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_descs.len() as u32, com::gpu_va(&instance_buffer)),
SourceAccelerationStructureData: 0,
ScratchAccelerationStructureData: scratch_gva,
};
cmd4.BuildRaytracingAccelerationStructure(&tlas_desc, None);
cmd.ResourceBarrier(&[uav_barrier()]);
}
self.blas.truncate(self.static_blas_count);
for (blas, _) in &ring.blas[..skinned_geo.len()] {
self.blas.push(blas.clone());
}
self.tlas = tlas;
self.geom_table = geom_table;
self.instance_buffer = instance_buffer;
self.scratch = scratch_buffer;
self.deformed_verts = deformed_verts;
self.skinned_ring[frame_idx] = ring;
self.has_skinned = true;
self.cached_models.clear();
self.cached_models.extend_from_slice(models);
Ok(())
}
}
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.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.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.SetComputeRoot32BitConstants(
0,
SKIN_PARAMS_DWORDS,
¶ms as *const SkinParams as *const std::ffi::c_void,
0,
);
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 topology_dirty = std::mem::take(&mut self.rt_topology_dirty);
if self.rt_accel.is_none() {
if topology_dirty && self.rt_reflections.is_some() && self.rt_dynamic_mode.is_dynamic()
{
self.seed_rt_accel();
}
return;
}
let skinned_inputs = match (
self.skinned.vertex_buffer.as_ref(),
self.skinned.index_buffer.as_ref(),
) {
(Some(vb), Some(ib))
if self.rt_skinned_geometry && !self.skinned.draw_objects.is_empty() =>
{
let vertex_gva = com::gpu_va(vb);
let index_gva = com::gpu_va(ib);
Some((vertex_gva, index_gva))
}
_ => None,
};
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 Some(accel) = self.rt_accel.as_mut() else {
return;
};
let skinned = skinned_inputs.map(|(v, i)| SkinnedRtInputs {
objects: &self.skinned.draw_objects,
vertex_gva: v,
index_gva: i,
joint_buffers,
});
accel.dynamic_update(
&self.alloc,
cmd,
&self.draw.objects,
RtDynamicInputs {
mode: self.rt_dynamic_mode,
skinned,
frame_idx,
topology_dirty,
exclude_seethrough,
},
);
}
fn seed_rt_accel(&mut self) {
if let Some(accel) = self.build_scene_accel() {
self.rt_accel = Some(accel);
}
}
pub(super) fn rebuild_rt_accel(&mut self) {
self.rt_accel = self.build_scene_accel();
}
fn build_scene_accel(&self) -> Option<RtAccelData> {
let hot_reload = self.hot_reload.enabled;
let mut accel = match build_rt_accel(RtInitGeometry {
alloc: &self.alloc,
vertex_buffer: &self.geometry.vertex_buffer,
index_buffer: &self.geometry.index_buffer,
draw_objects: &self.draw.objects,
clusters: &self.instanced.clusters,
total_vertices: self.rt_static_vertex_count,
albedo_count: self.descriptors.textures.len() as u32,
exclude_seethrough: self.seethrough_meshes_enabled(),
}) {
Ok(Some(accel)) => accel,
Ok(None) => return None,
Err(e) => {
tracing::warn!("RT acceleration-structure build failed: {e}");
return None;
}
};
match build_rt_skin_pipeline(&self.device, hot_reload) {
Ok(skin) => accel.set_skin_pipeline(skin),
Err(e) => {
tracing::warn!("RT skin pipeline build failed (skinned meshes absent): {e}")
}
}
Some(accel)
}
}
#[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 next_slot_wraps_around_the_ring() {
assert_eq!(next_slot(0, 3), 1);
assert_eq!(next_slot(1, 3), 2);
assert_eq!(next_slot(2, 3), 0);
assert_eq!(next_slot(0, 1), 0);
}
#[test]
fn pack_instance_transform_transposes_column_major_to_3x4_row_major() {
let model = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[10.0, 20.0, 30.0, 1.0],
];
let t = pack_instance_transform(model);
assert_eq!(
t,
[
1.0, 0.0, 0.0, 10.0, 0.0, 1.0, 0.0, 20.0, 0.0, 0.0, 1.0, 30.0
]
);
}
#[test]
fn pack_instance_transform_preserves_a_rotation_shear() {
let model = [
[1.0, 2.0, 3.0, 0.0],
[4.0, 5.0, 6.0, 0.0],
[7.0, 8.0, 9.0, 0.0],
[10.0, 11.0, 12.0, 1.0],
];
let t = pack_instance_transform(model);
assert_eq!(
t,
[
1.0, 4.0, 7.0, 10.0, 2.0, 5.0, 8.0, 11.0, 3.0, 6.0, 9.0, 12.0
]
);
}
#[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);
}
#[test]
fn skin_params_dwords_matches_size() {
assert_eq!(
SKIN_PARAMS_DWORDS as usize,
std::mem::size_of::<super::SkinParams>() / 4
);
}
}