use concinnity_core::gfx::transform::mat4_inverse;
use windows::Win32::Foundation::RECT;
use windows::Win32::Graphics::Direct3D12::*;
use windows::Win32::Graphics::Dxgi::Common::*;
use super::allocator::{DeviceAllocator, PooledBuffer};
use super::com;
use crate::directx::context::{DxContext, FRAMES, align256, dump_on_err};
use crate::directx::pipeline::serialize_desc_and_create;
use crate::directx::slang_builtins;
use crate::directx::slang_builtins::SlangCompile;
use crate::directx::texture::{HDR_FORMAT, create_buffer, upload_buffer, write_texture_srv};
use crate::gfx::decal::DecalRecord;
pub(in crate::directx) fn compile_decal_shaders(
msaa_samples: u32,
hot_reload: bool,
) -> Result<(Vec<u8>, Vec<u8>), String> {
let frag = if msaa_samples > 1 {
&slang_builtins::DECAL_FRAG_MSAA
} else {
&slang_builtins::DECAL_FRAG
};
let vs = slang_builtins::DECAL_VERT.compile(hot_reload)?;
let ps = frag.compile(hot_reload)?;
Ok((vs, ps))
}
pub(in crate::directx) fn rebuild_decal_pso(
device: &ID3D12Device,
root_sig: &ID3D12RootSignature,
msaa_samples: u32,
hot_reload: bool,
info_queue: Option<&ID3D12InfoQueue>,
) -> Result<ID3D12PipelineState, String> {
let (vs, ps) = compile_decal_shaders(msaa_samples, hot_reload)?;
dump_on_err(info_queue, create_decal_pso(device, root_sig, &vs, &ps))
}
pub(in crate::directx) const MAX_DECALS: usize = 256;
const CUBE_VERTS: [f32; 24] = [
-0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5,
0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5,
];
const CUBE_INDICES: [u16; 36] = [
0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 3, 6, 2, 3, 7, 6, 0, 4, 7, 0, 7, 3, 1, 2, 6, 1, 6, 5,
];
pub(in crate::directx) use concinnity_render::uniforms::DecalParams;
pub(in crate::directx) use concinnity_render::uniforms::DecalView;
fn create_decal_root_signature(device: &ID3D12Device) -> Result<ID3D12RootSignature, String> {
let depth_range = D3D12_DESCRIPTOR_RANGE {
RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
NumDescriptors: 1,
BaseShaderRegister: 0, RegisterSpace: 0,
OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,
};
let albedo_range = D3D12_DESCRIPTOR_RANGE {
RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
NumDescriptors: 1,
BaseShaderRegister: 1, RegisterSpace: 0,
OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,
};
let params = [
D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_CBV,
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_CBV,
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_DESCRIPTOR_TABLE,
Anonymous: D3D12_ROOT_PARAMETER_0 {
DescriptorTable: D3D12_ROOT_DESCRIPTOR_TABLE {
NumDescriptorRanges: 1,
pDescriptorRanges: &depth_range,
},
},
ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL,
},
D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE,
Anonymous: D3D12_ROOT_PARAMETER_0 {
DescriptorTable: D3D12_ROOT_DESCRIPTOR_TABLE {
NumDescriptorRanges: 1,
pDescriptorRanges: &albedo_range,
},
},
ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL,
},
];
let samp = D3D12_STATIC_SAMPLER_DESC {
Filter: D3D12_FILTER_MIN_MAG_MIP_LINEAR,
AddressU: D3D12_TEXTURE_ADDRESS_MODE_CLAMP,
AddressV: D3D12_TEXTURE_ADDRESS_MODE_CLAMP,
AddressW: D3D12_TEXTURE_ADDRESS_MODE_CLAMP,
ComparisonFunc: D3D12_COMPARISON_FUNC_ALWAYS,
BorderColor: D3D12_STATIC_BORDER_COLOR_OPAQUE_BLACK,
MinLOD: 0.0,
MaxLOD: f32::MAX,
ShaderRegister: 0,
RegisterSpace: 0,
ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL,
..Default::default()
};
let desc = D3D12_ROOT_SIGNATURE_DESC {
NumParameters: params.len() as u32,
pParameters: params.as_ptr(),
NumStaticSamplers: 1,
pStaticSamplers: &samp,
Flags: D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT,
};
serialize_desc_and_create(device, &desc, "decal root sig")
}
fn decal_input_layout() -> [D3D12_INPUT_ELEMENT_DESC; 1] {
[D3D12_INPUT_ELEMENT_DESC {
SemanticName: windows::core::s!("POSITION"),
SemanticIndex: 0,
Format: DXGI_FORMAT_R32G32B32_FLOAT,
InputSlot: 0,
AlignedByteOffset: 0,
InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,
InstanceDataStepRate: 0,
}]
}
fn create_decal_pso(
device: &ID3D12Device,
root_sig: &ID3D12RootSignature,
vs: &[u8],
ps: &[u8],
) -> Result<ID3D12PipelineState, String> {
let layout = decal_input_layout();
let pso_desc = D3D12_GRAPHICS_PIPELINE_STATE_DESC {
pRootSignature: com::borrowed(root_sig),
VS: D3D12_SHADER_BYTECODE {
pShaderBytecode: vs.as_ptr() as _,
BytecodeLength: vs.len(),
},
PS: D3D12_SHADER_BYTECODE {
pShaderBytecode: ps.as_ptr() as _,
BytecodeLength: ps.len(),
},
InputLayout: D3D12_INPUT_LAYOUT_DESC {
pInputElementDescs: layout.as_ptr(),
NumElements: layout.len() as u32,
},
PrimitiveTopologyType: D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE,
NumRenderTargets: 1,
RTVFormats: {
let mut a = [DXGI_FORMAT_UNKNOWN; 8];
a[0] = HDR_FORMAT;
a
},
DSVFormat: DXGI_FORMAT_UNKNOWN,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
SampleMask: u32::MAX,
RasterizerState: D3D12_RASTERIZER_DESC {
FillMode: D3D12_FILL_MODE_SOLID,
CullMode: D3D12_CULL_MODE_FRONT,
FrontCounterClockwise: true.into(),
DepthClipEnable: false.into(),
..Default::default()
},
DepthStencilState: D3D12_DEPTH_STENCIL_DESC {
DepthEnable: false.into(),
DepthWriteMask: D3D12_DEPTH_WRITE_MASK_ZERO,
StencilEnable: false.into(),
..Default::default()
},
BlendState: D3D12_BLEND_DESC {
RenderTarget: {
let mut arr = [D3D12_RENDER_TARGET_BLEND_DESC::default(); 8];
arr[0] = D3D12_RENDER_TARGET_BLEND_DESC {
BlendEnable: true.into(),
SrcBlend: D3D12_BLEND_SRC_ALPHA,
DestBlend: D3D12_BLEND_INV_SRC_ALPHA,
BlendOp: D3D12_BLEND_OP_ADD,
SrcBlendAlpha: D3D12_BLEND_SRC_ALPHA,
DestBlendAlpha: D3D12_BLEND_INV_SRC_ALPHA,
BlendOpAlpha: D3D12_BLEND_OP_ADD,
RenderTargetWriteMask: D3D12_COLOR_WRITE_ENABLE_ALL.0 as u8,
..Default::default()
};
arr
},
..Default::default()
},
..Default::default()
};
unsafe { crate::directx::pso_library::create_graphics(device, &pso_desc) }
.map_err(|e| format!("create decal PSO: {e}"))
}
pub(in crate::directx) struct DecalResources {
pub(in crate::directx) root_sig: ID3D12RootSignature,
pub(in crate::directx) pso: ID3D12PipelineState,
#[expect(
dead_code,
reason = "held to keep the GPU memory alive; the encoder binds through vertex_buffer_view"
)]
pub(in crate::directx) vertex_buffer: PooledBuffer,
pub(in crate::directx) vertex_buffer_view: D3D12_VERTEX_BUFFER_VIEW,
#[expect(
dead_code,
reason = "held to keep the GPU memory alive; the encoder binds through index_buffer_view"
)]
pub(in crate::directx) index_buffer: PooledBuffer,
pub(in crate::directx) index_buffer_view: D3D12_INDEX_BUFFER_VIEW,
pub(in crate::directx) view_ubo_resources: Vec<PooledBuffer>,
pub(in crate::directx) view_ubo_ptrs: Vec<*mut u8>,
pub(in crate::directx) params_ubo_resources: Vec<PooledBuffer>,
pub(in crate::directx) params_ubo_ptrs: Vec<*mut u8>,
pub(in crate::directx) params_stride: u64,
pub(in crate::directx) decal_srv_base_slot: usize,
pub(in crate::directx) depth_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
}
impl DecalResources {
pub(in crate::directx) fn new(
alloc: &DeviceAllocator,
msaa_samples: u32,
decal_srv_base_slot: usize,
depth_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
info_queue: Option<&ID3D12InfoQueue>,
hot_reload: bool,
) -> Result<Self, String> {
let device = alloc.device();
let (vs, ps) = compile_decal_shaders(msaa_samples, hot_reload)?;
let root_sig = dump_on_err(info_queue, create_decal_root_signature(device))?;
let pso = dump_on_err(info_queue, create_decal_pso(device, &root_sig, &vs, &ps))?;
let vbytes = bytemuck::cast_slice(CUBE_VERTS.as_slice());
let ibytes = bytemuck::cast_slice(CUBE_INDICES.as_slice());
let vertex_buffer = upload_buffer(
alloc,
vbytes,
D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER,
)?;
let index_buffer = upload_buffer(alloc, ibytes, D3D12_RESOURCE_STATE_INDEX_BUFFER)?;
let vertex_buffer_view = D3D12_VERTEX_BUFFER_VIEW {
BufferLocation: com::gpu_va(&vertex_buffer),
SizeInBytes: vbytes.len() as u32,
StrideInBytes: 12,
};
let index_buffer_view = D3D12_INDEX_BUFFER_VIEW {
BufferLocation: com::gpu_va(&index_buffer),
SizeInBytes: ibytes.len() as u32,
Format: DXGI_FORMAT_R16_UINT,
};
let view_size = align256(std::mem::size_of::<DecalView>() as u64);
let mut view_ubo_resources: Vec<PooledBuffer> = Vec::with_capacity(FRAMES);
let mut view_ubo_ptrs: Vec<*mut u8> = Vec::with_capacity(FRAMES);
for _ in 0..FRAMES {
let buf = create_buffer(
alloc,
view_size,
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 decal view ubo: {e}"))?;
view_ubo_ptrs.push(ptr as *mut u8);
view_ubo_resources.push(buf);
}
let params_stride = align256(std::mem::size_of::<DecalParams>() as u64);
let params_total = params_stride * MAX_DECALS as u64;
let mut params_ubo_resources: Vec<PooledBuffer> = Vec::with_capacity(FRAMES);
let mut params_ubo_ptrs: Vec<*mut u8> = Vec::with_capacity(FRAMES);
for _ in 0..FRAMES {
let buf = create_buffer(
alloc,
params_total,
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 decal params ubo: {e}"))?;
params_ubo_ptrs.push(ptr as *mut u8);
params_ubo_resources.push(buf);
}
Ok(Self {
root_sig,
pso,
vertex_buffer,
vertex_buffer_view,
index_buffer,
index_buffer_view,
view_ubo_resources,
view_ubo_ptrs,
params_ubo_resources,
params_ubo_ptrs,
params_stride,
decal_srv_base_slot,
depth_srv_gpu,
})
}
}
pub(in crate::directx) fn write_main_depth_srv(
device: &ID3D12Device,
depth: &ID3D12Resource,
srv_cpu: D3D12_CPU_DESCRIPTOR_HANDLE,
sample_count: u32,
) {
let srv_desc = if sample_count > 1 {
D3D12_SHADER_RESOURCE_VIEW_DESC {
Format: DXGI_FORMAT_R32_FLOAT,
ViewDimension: D3D12_SRV_DIMENSION_TEXTURE2DMS,
Shader4ComponentMapping: D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,
Anonymous: D3D12_SHADER_RESOURCE_VIEW_DESC_0 {
Texture2DMS: D3D12_TEX2DMS_SRV {
UnusedField_NothingToDefine: 0,
},
},
}
} else {
D3D12_SHADER_RESOURCE_VIEW_DESC {
Format: DXGI_FORMAT_R32_FLOAT,
ViewDimension: D3D12_SRV_DIMENSION_TEXTURE2D,
Shader4ComponentMapping: D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,
Anonymous: D3D12_SHADER_RESOURCE_VIEW_DESC_0 {
Texture2D: D3D12_TEX2D_SRV {
MipLevels: 1,
..Default::default()
},
},
}
};
unsafe { device.CreateShaderResourceView(depth, Some(&srv_desc), srv_cpu) };
}
impl DxContext {
pub(in crate::directx) fn encode_decals(
&self,
cmd: &ID3D12GraphicsCommandList,
frame_idx: usize,
vp: [[f32; 4]; 4],
frustum: &crate::gfx::frustum::Frustum,
) {
let decals = match &self.decal.state {
Some(s) => s,
None => return,
};
if self.decal.records.iter().all(|slot| slot.is_none()) {
return;
}
let visible_count = self
.decal
.records
.iter()
.filter(|slot| {
slot.as_ref()
.map(|d| {
let (mn, mx) = d.aabb();
frustum.intersects_aabb(mn, mx)
})
.unwrap_or(false)
})
.count();
if visible_count == 0 {
return;
}
let inv_vp = mat4_inverse(vp);
let viewport = [
self.extent.render_width as f32,
self.extent.render_height as f32,
];
let view_uni = DecalView {
vp,
inv_vp,
viewport,
_pad: [0.0; 2],
};
unsafe {
std::ptr::copy_nonoverlapping(
&view_uni as *const DecalView as *const u8,
decals.view_ubo_ptrs[frame_idx],
std::mem::size_of::<DecalView>(),
);
}
let view_gva = com::gpu_va(&decals.view_ubo_resources[frame_idx]);
let params_base_gva = com::gpu_va(&decals.params_ubo_resources[frame_idx]);
let scene_rtv = self.hdr_scene_rtv();
let w = self.extent.render_width;
let h = self.extent.render_height;
unsafe {
cmd.OMSetRenderTargets(1, Some(&scene_rtv), false, None);
let vp = D3D12_VIEWPORT {
TopLeftX: 0.0,
TopLeftY: 0.0,
Width: w as f32,
Height: h as f32,
MinDepth: 0.0,
MaxDepth: 1.0,
};
cmd.RSSetViewports(&[vp]);
let scissor = RECT {
left: 0,
top: 0,
right: w as i32,
bottom: h as i32,
};
cmd.RSSetScissorRects(&[scissor]);
cmd.IASetPrimitiveTopology(
windows::Win32::Graphics::Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST,
);
cmd.IASetVertexBuffers(0, Some(&[decals.vertex_buffer_view]));
cmd.IASetIndexBuffer(Some(&decals.index_buffer_view));
cmd.SetPipelineState(&decals.pso);
cmd.SetGraphicsRootSignature(&decals.root_sig);
cmd.SetDescriptorHeaps(&[Some(self.descriptors.srv_heap.clone())]);
cmd.SetGraphicsRootConstantBufferView(0, view_gva);
cmd.SetGraphicsRootDescriptorTable(2, decals.depth_srv_gpu);
}
let last_tex = self.descriptors.textures.len().saturating_sub(1);
for (i, slot) in self.decal.records.iter().enumerate() {
let d = match slot {
Some(d) => d,
None => continue,
};
let (mn, mx) = d.aabb();
if !frustum.intersects_aabb(mn, mx) {
continue;
}
let params = DecalParams {
model: d.model,
inv_model: d.inv_model,
tint: d.tint,
fade_pow: 2.0,
_pad0: 0.0,
_pad1: 0.0,
_pad2: 0.0,
};
let dst = unsafe {
decals.params_ubo_ptrs[frame_idx].add((i as u64 * decals.params_stride) as usize)
};
unsafe {
std::ptr::copy_nonoverlapping(
¶ms as *const DecalParams as *const u8,
dst,
std::mem::size_of::<DecalParams>(),
);
}
let params_gva = params_base_gva + i as u64 * decals.params_stride;
let tex_slot = d.texture_slot.min(last_tex);
let albedo_srv_gpu = self.decal_albedo_srv_gpu(i);
let _ = tex_slot;
unsafe {
cmd.SetGraphicsRootConstantBufferView(1, params_gva);
cmd.SetGraphicsRootDescriptorTable(3, albedo_srv_gpu);
cmd.DrawIndexedInstanced(36, 1, 0, 0, 0);
}
self.inc_draw_calls(1);
}
}
pub(in crate::directx) fn decal_albedo_srv_gpu(&self, i: usize) -> D3D12_GPU_DESCRIPTOR_HANDLE {
let base = self
.decal
.state
.as_ref()
.map(|s| s.decal_srv_base_slot)
.unwrap_or(0);
let srv_gpu_base = unsafe {
self.descriptors
.srv_heap
.GetGPUDescriptorHandleForHeapStart()
};
D3D12_GPU_DESCRIPTOR_HANDLE {
ptr: srv_gpu_base.ptr + ((base + i) * self.descriptors.srv_descriptor_size) as u64,
}
}
}
impl DxContext {
pub(crate) fn add_decal(&mut self, record: DecalRecord) -> Result<usize, String> {
let state = self
.decal
.state
.as_ref()
.ok_or_else(|| "add_decal: decal pipeline unavailable".to_string())?;
let base_slot = state.decal_srv_base_slot;
let last_tex = self.descriptors.textures.len().saturating_sub(1);
let tex_idx = record.texture_slot.min(last_tex);
let id = if let Some(slot) = self.decal.free_slots.pop() {
self.decal.records[slot] = Some(record);
slot
} else {
if self.decal.records.len() >= MAX_DECALS {
return Err(format!("add_decal: MAX_DECALS ({MAX_DECALS}) exceeded"));
}
self.decal.records.push(Some(record));
self.decal.records.len() - 1
};
let srv_cpu = D3D12_CPU_DESCRIPTOR_HANDLE {
ptr: unsafe {
self.descriptors
.srv_heap
.GetCPUDescriptorHandleForHeapStart()
}
.ptr + (base_slot + id) * self.descriptors.srv_descriptor_size,
};
write_texture_srv(&self.device, &self.descriptors.textures[tex_idx], srv_cpu);
Ok(id)
}
pub(crate) fn remove_decal(&mut self, decal_id: usize) -> Result<(), String> {
let slot = self
.decal
.records
.get_mut(decal_id)
.ok_or_else(|| format!("remove_decal: id {decal_id} out of range"))?;
if slot.is_none() {
return Err(format!("remove_decal: id {decal_id} already removed"));
}
*slot = None;
self.decal.free_slots.push(decal_id);
Ok(())
}
}