use windows::Win32::Foundation::RECT;
use windows::Win32::Graphics::Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
use windows::Win32::Graphics::Direct3D12::*;
use windows::Win32::Graphics::Dxgi::Common::*;
use super::allocator::{DeviceAllocator, PooledBuffer};
use super::com;
use crate::components::{GlassPanel, WaterSurface};
use crate::directx::context::{DxContext, FRAMES, align256, dump_on_err};
use crate::directx::pipeline::{main_input_layout, serialize_desc_and_create};
use crate::directx::texture::{
HDR_FORMAT, create_buffer, create_hdr_resolve_target, transition_barrier, upload_buffer,
};
use crate::gfx::mesh_payload::Vertex;
use crate::gfx::render_types::RtParams;
use crate::gfx::rt_reflections::RtParamsInputs;
const RT_PARAMS_UBO_SIZE: u64 = 144;
use concinnity_render::uniforms::GlassMeshParams;
pub(in crate::directx) use concinnity_render::uniforms::TransparentView;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(in crate::directx) enum Producer {
Glass,
Water,
GlassMesh,
}
pub(in crate::directx) struct TransparentRecord {
#[expect(
dead_code,
reason = "held to keep the GPU memory alive; the encoder binds through vertex_buffer_view"
)]
vertex_buffer: PooledBuffer,
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"
)]
index_buffer: PooledBuffer,
index_buffer_view: D3D12_INDEX_BUFFER_VIEW,
index_count: u32,
#[expect(
dead_code,
reason = "held to keep the GPU memory alive; the encoder binds through params_cbuffer_gva"
)]
params_cbuffer: PooledBuffer,
params_cbuffer_gva: u64,
visible: bool,
centre: [f32; 3],
planar_slot: Option<usize>,
}
pub(in crate::directx) struct RecordUpload<'a> {
pub vertices: &'a [Vertex],
pub indices: &'a [u16],
pub params: &'a [u8],
pub visible: bool,
pub centre: [f32; 3],
pub planar_slot: Option<usize>,
}
impl TransparentRecord {
pub(in crate::directx) fn upload(
alloc: &DeviceAllocator,
upload: RecordUpload<'_>,
) -> Result<Self, String> {
let vbytes = bytemuck::cast_slice(upload.vertices);
let ibytes = bytemuck::cast_slice(upload.indices);
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: std::mem::size_of::<Vertex>() as u32,
};
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 params_cbuffer = create_buffer(
alloc,
align256(upload.params.len() as u64),
D3D12_HEAP_TYPE_UPLOAD,
D3D12_RESOURCE_STATE_GENERIC_READ,
)?;
let mut p = std::ptr::null_mut::<std::ffi::c_void>();
unsafe { params_cbuffer.Map(0, None, Some(&mut p)) }
.map_err(|e| format!("map transparent params cb: {e}"))?;
unsafe {
std::ptr::copy_nonoverlapping(upload.params.as_ptr(), p as *mut u8, upload.params.len())
};
let params_cbuffer_gva = com::gpu_va(¶ms_cbuffer);
Ok(Self {
vertex_buffer,
vertex_buffer_view,
index_buffer,
index_buffer_view,
index_count: upload.indices.len() as u32,
params_cbuffer,
params_cbuffer_gva,
visible: upload.visible,
centre: upload.centre,
planar_slot: upload.planar_slot,
})
}
}
pub(in crate::directx) struct TransparentProducer {
pub pso: ID3D12PipelineState,
pub flat_rt_pso: Option<ID3D12PipelineState>,
pub textured_rt_pso: Option<ID3D12PipelineState>,
pub records: Vec<TransparentRecord>,
}
impl TransparentProducer {
fn pipeline(&self, rt_live: bool, textured: bool) -> &ID3D12PipelineState {
match (rt_live, textured) {
(true, true) => self
.textured_rt_pso
.as_ref()
.expect("rt_textured_ready gated the frame on every producer's textured PSO"),
(true, false) => self
.flat_rt_pso
.as_ref()
.expect("rt_pipelines_ready gated the frame on every producer's flat RT PSO"),
_ => &self.pso,
}
}
}
pub(in crate::directx) struct GlassMeshProducer {
flat_rt_pso: ID3D12PipelineState,
textured_rt_pso: Option<ID3D12PipelineState>,
object_indices: Vec<usize>,
params_ring: Vec<PooledBuffer>,
params_ptrs: Vec<*mut u8>,
}
struct GlassMeshDraw {
index_offset: u32,
index_count: u32,
base_vertex: i32,
params_gva: u64,
centre: [f32; 3],
}
impl GlassMeshProducer {
pub(in crate::directx) fn new(
alloc: &DeviceAllocator,
flat_rt_pso: ID3D12PipelineState,
textured_rt_pso: Option<ID3D12PipelineState>,
object_indices: Vec<usize>,
) -> Result<Self, String> {
let block = align256(std::mem::size_of::<GlassMeshParams>() as u64);
let ring_size = block * object_indices.len().max(1) as u64;
let mut params_ring: Vec<PooledBuffer> = Vec::with_capacity(FRAMES);
let mut params_ptrs: Vec<*mut u8> = Vec::with_capacity(FRAMES);
for _ in 0..FRAMES {
let buf = create_buffer(
alloc,
ring_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 glass mesh params ring: {e}"))?;
params_ptrs.push(ptr as *mut u8);
params_ring.push(buf);
}
Ok(Self {
flat_rt_pso,
textured_rt_pso,
object_indices,
params_ring,
params_ptrs,
})
}
fn pipeline(&self, textured: bool) -> &ID3D12PipelineState {
match textured {
true => self
.textured_rt_pso
.as_ref()
.expect("rt_textured_ready gated the frame on every producer's textured PSO"),
false => &self.flat_rt_pso,
}
}
}
pub(in crate::directx) struct TransparentResources {
root_sig: ID3D12RootSignature,
glass: Option<TransparentProducer>,
water: Option<TransparentProducer>,
glass_mesh: Option<GlassMeshProducer>,
view_ubo_resources: Vec<PooledBuffer>,
view_ubo_ptrs: Vec<*mut u8>,
scene_copy: ID3D12Resource,
scene_copy_srv_cpu: D3D12_CPU_DESCRIPTOR_HANDLE,
scene_copy_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
depth_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
rt_root_sig: Option<ID3D12RootSignature>,
rt_params_ubo_resources: Vec<PooledBuffer>,
rt_params_ubo_ptrs: Vec<*mut u8>,
}
unsafe impl Send for TransparentResources {}
unsafe impl Sync for TransparentResources {}
fn sort_distance(centre: [f32; 3], cam: [f32; 3]) -> f32 {
let dx = centre[0] - cam[0];
let dy = centre[1] - cam[1];
let dz = centre[2] - cam[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
fn ordered_visible(
glass: &[([f32; 3], bool)],
water: &[([f32; 3], bool)],
meshes: &[[f32; 3]],
cam: [f32; 3],
) -> Vec<(Producer, usize)> {
let live_of = |records: &[([f32; 3], bool)], kind: Producer| -> Vec<(Producer, usize)> {
records
.iter()
.enumerate()
.filter(|(_, (_, vis))| *vis)
.map(|(i, _)| (kind, i))
.collect()
};
let live: Vec<(Producer, usize)> = live_of(glass, Producer::Glass)
.into_iter()
.chain(live_of(water, Producer::Water))
.chain((0..meshes.len()).map(|i| (Producer::GlassMesh, i)))
.collect();
let dists: Vec<f32> = live
.iter()
.map(|&(kind, i)| {
let centre = match kind {
Producer::Glass => glass[i].0,
Producer::Water => water[i].0,
Producer::GlassMesh => meshes[i],
};
sort_distance(centre, cam)
})
.collect();
crate::gfx::transparent::back_to_front_order(&dists)
.into_iter()
.map(|oi| live[oi])
.collect()
}
const PLANAR_ROOT_BASE: u32 = 7;
const PLANAR_ROOT_RT: u32 = 15;
fn create_transparent_root_signature(device: &ID3D12Device) -> Result<ID3D12RootSignature, String> {
let scene_range = D3D12_DESCRIPTOR_RANGE {
RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
NumDescriptors: 1,
BaseShaderRegister: 0, RegisterSpace: 0,
OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,
};
let depth_range = D3D12_DESCRIPTOR_RANGE {
RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
NumDescriptors: 1,
BaseShaderRegister: 1, RegisterSpace: 0,
OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,
};
let prefilter_range = D3D12_DESCRIPTOR_RANGE {
RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
NumDescriptors: 1,
BaseShaderRegister: 2, RegisterSpace: 0,
OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,
};
let probe_cube_range = D3D12_DESCRIPTOR_RANGE {
RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
NumDescriptors: concinnity_render::uniforms::MAX_PROBES as u32,
BaseShaderRegister: 7, RegisterSpace: 0,
OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,
};
let planar_range = D3D12_DESCRIPTOR_RANGE {
RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
NumDescriptors: 1,
BaseShaderRegister: 3, RegisterSpace: 0,
OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,
};
let cbv = |reg: u32, vis: D3D12_SHADER_VISIBILITY| D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_CBV,
Anonymous: D3D12_ROOT_PARAMETER_0 {
Descriptor: D3D12_ROOT_DESCRIPTOR {
ShaderRegister: reg,
RegisterSpace: 0,
},
},
ShaderVisibility: vis,
};
let table = |range: &D3D12_DESCRIPTOR_RANGE| D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE,
Anonymous: D3D12_ROOT_PARAMETER_0 {
DescriptorTable: D3D12_ROOT_DESCRIPTOR_TABLE {
NumDescriptorRanges: 1,
pDescriptorRanges: range,
},
},
ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL,
};
let params = [
cbv(0, D3D12_SHADER_VISIBILITY_ALL), cbv(1, D3D12_SHADER_VISIBILITY_ALL), table(&scene_range), table(&depth_range), table(&prefilter_range), table(&probe_cube_range), cbv(4, D3D12_SHADER_VISIBILITY_PIXEL), table(&planar_range), ];
debug_assert_eq!(params.len() as u32 - 1, PLANAR_ROOT_BASE);
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 cube_samp = D3D12_STATIC_SAMPLER_DESC {
ShaderRegister: 2, ..samp
};
let samplers = [samp, cube_samp];
let desc = D3D12_ROOT_SIGNATURE_DESC {
NumParameters: params.len() as u32,
pParameters: params.as_ptr(),
NumStaticSamplers: samplers.len() as u32,
pStaticSamplers: samplers.as_ptr(),
Flags: D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT,
};
serialize_desc_and_create(device, &desc, "transparent root sig")
}
pub(in crate::directx) fn create_transparent_pso(
device: &ID3D12Device,
root_sig: &ID3D12RootSignature,
vs: &[u8],
ps: &[u8],
) -> Result<ID3D12PipelineState, String> {
let layout = main_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_NONE,
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 transparent PSO: {e}"))
}
fn create_transparent_rt_root_signature(
device: &ID3D12Device,
) -> Result<ID3D12RootSignature, String> {
let table_range = |reg: u32, space: u32, count: u32| D3D12_DESCRIPTOR_RANGE {
RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
NumDescriptors: count,
BaseShaderRegister: reg,
RegisterSpace: space,
OffsetInDescriptorsFromTableStart: D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,
};
let scene_range = table_range(0, 0, 1); let depth_range = table_range(1, 0, 1); let prefilter_range = table_range(2, 0, 1); let probe_cube_range = table_range(20, 0, concinnity_render::uniforms::MAX_PROBES as u32);
let planar_range = table_range(3, 0, 1); let pool_range = D3D12_DESCRIPTOR_RANGE {
RangeType: D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
NumDescriptors: u32::MAX, BaseShaderRegister: 0, RegisterSpace: 1, OffsetInDescriptorsFromTableStart: 0,
};
let cbv = |reg: u32, vis: D3D12_SHADER_VISIBILITY| D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_CBV,
Anonymous: D3D12_ROOT_PARAMETER_0 {
Descriptor: D3D12_ROOT_DESCRIPTOR {
ShaderRegister: reg,
RegisterSpace: 0,
},
},
ShaderVisibility: vis,
};
let root_srv = |reg: u32| D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_SRV,
Anonymous: D3D12_ROOT_PARAMETER_0 {
Descriptor: D3D12_ROOT_DESCRIPTOR {
ShaderRegister: reg,
RegisterSpace: 0,
},
},
ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL,
};
let table = |range: &D3D12_DESCRIPTOR_RANGE| D3D12_ROOT_PARAMETER {
ParameterType: D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE,
Anonymous: D3D12_ROOT_PARAMETER_0 {
DescriptorTable: D3D12_ROOT_DESCRIPTOR_TABLE {
NumDescriptorRanges: 1,
pDescriptorRanges: range,
},
},
ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL,
};
let params = [
cbv(0, D3D12_SHADER_VISIBILITY_ALL), cbv(1, D3D12_SHADER_VISIBILITY_ALL), table(&scene_range), table(&depth_range), table(&prefilter_range), table(&probe_cube_range), cbv(4, D3D12_SHADER_VISIBILITY_PIXEL), cbv(5, D3D12_SHADER_VISIBILITY_PIXEL), root_srv(4), root_srv(5), root_srv(6), root_srv(10), root_srv(8), root_srv(9), table(&pool_range), table(&planar_range), ];
debug_assert_eq!(params.len() as u32 - 1, PLANAR_ROOT_RT);
let linear = |addr: D3D12_TEXTURE_ADDRESS_MODE, reg: u32| D3D12_STATIC_SAMPLER_DESC {
Filter: D3D12_FILTER_MIN_MAG_MIP_LINEAR,
AddressU: addr,
AddressV: addr,
AddressW: addr,
ComparisonFunc: D3D12_COMPARISON_FUNC_ALWAYS,
BorderColor: D3D12_STATIC_BORDER_COLOR_OPAQUE_BLACK,
MinLOD: 0.0,
MaxLOD: f32::MAX,
ShaderRegister: reg,
RegisterSpace: 0,
ShaderVisibility: D3D12_SHADER_VISIBILITY_PIXEL,
..Default::default()
};
let samplers = [
linear(D3D12_TEXTURE_ADDRESS_MODE_CLAMP, 0), linear(D3D12_TEXTURE_ADDRESS_MODE_WRAP, 1), linear(D3D12_TEXTURE_ADDRESS_MODE_CLAMP, 2), ];
let desc = D3D12_ROOT_SIGNATURE_DESC {
NumParameters: params.len() as u32,
pParameters: params.as_ptr(),
NumStaticSamplers: samplers.len() as u32,
pStaticSamplers: samplers.as_ptr(),
Flags: D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT,
};
serialize_desc_and_create(device, &desc, "transparent rt root sig")
}
type RtParamsRing = (Vec<PooledBuffer>, Vec<*mut u8>);
fn build_rt_params_ring(alloc: &DeviceAllocator) -> Result<RtParamsRing, String> {
let params_size = align256(RT_PARAMS_UBO_SIZE);
let mut resources: Vec<PooledBuffer> = Vec::with_capacity(FRAMES);
let mut ptrs: Vec<*mut u8> = Vec::with_capacity(FRAMES);
for _ in 0..FRAMES {
let buf = create_buffer(
alloc,
params_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 transparent rt params ubo: {e}"))?;
ptrs.push(ptr as *mut u8);
resources.push(buf);
}
Ok((resources, ptrs))
}
fn write_scene_copy_srv(
device: &ID3D12Device,
scene_copy: &ID3D12Resource,
srv_cpu: D3D12_CPU_DESCRIPTOR_HANDLE,
) {
let desc = D3D12_SHADER_RESOURCE_VIEW_DESC {
Format: HDR_FORMAT,
ViewDimension: D3D12_SRV_DIMENSION_TEXTURE2D,
Shader4ComponentMapping: D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,
Anonymous: D3D12_SHADER_RESOURCE_VIEW_DESC_0 {
Texture2D: D3D12_TEX2D_SRV {
MostDetailedMip: 0,
MipLevels: 1,
PlaneSlice: 0,
ResourceMinLODClamp: 0.0,
},
},
};
unsafe { device.CreateShaderResourceView(scene_copy, Some(&desc), srv_cpu) };
}
#[derive(Clone, Copy)]
pub(in crate::directx) struct TransparentDeviceCtx<'a> {
pub alloc: &'a DeviceAllocator,
}
#[derive(Clone, Copy)]
pub(in crate::directx) struct TransparentBuildConfig {
pub msaa_samples: u32,
pub width: u32,
pub height: u32,
pub hot_reload: bool,
}
#[derive(Clone, Copy)]
pub(in crate::directx) struct TransparentSceneTargets {
pub scene_copy_srv_cpu: D3D12_CPU_DESCRIPTOR_HANDLE,
pub scene_copy_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
pub depth_srv_gpu: D3D12_GPU_DESCRIPTOR_HANDLE,
}
#[derive(Clone, Copy)]
pub(in crate::directx) struct TransparentContent<'a> {
pub glass_panels: &'a [GlassPanel],
pub glass_planar_slots: &'a [Option<usize>],
pub water_surfaces: &'a [WaterSurface],
pub water_planar_slots: &'a [Option<usize>],
pub seethrough_mesh_indices: &'a [usize],
}
impl TransparentResources {
pub(in crate::directx) fn new(
device_ctx: TransparentDeviceCtx,
config: TransparentBuildConfig,
scene: TransparentSceneTargets,
content: TransparentContent,
info_queue: Option<&ID3D12InfoQueue>,
) -> Result<Self, String> {
let TransparentDeviceCtx { alloc } = device_ctx;
let device = alloc.device();
let TransparentBuildConfig {
msaa_samples,
width,
height,
hot_reload,
} = config;
let TransparentSceneTargets {
scene_copy_srv_cpu,
scene_copy_srv_gpu,
depth_srv_gpu,
} = scene;
let root_sig = dump_on_err(info_queue, create_transparent_root_signature(device))?;
let rt = if crate::directx::raytrace::raytracing_supported(device) {
match dump_on_err(info_queue, create_transparent_rt_root_signature(device))
.and_then(|sig| build_rt_params_ring(alloc).map(|ring| (sig, ring)))
{
Ok((sig, ring)) => Some((sig, ring)),
Err(e) => {
tracing::warn!(
"transparent RT reflection setup failed ({e}); \
using the probe/planar path"
);
None
}
}
} else {
None
};
let (rt_root_sig, rt_params_ubo_resources, rt_params_ubo_ptrs) = match rt {
Some((sig, (res, ptrs))) => (Some(sig), res, ptrs),
None => (None, Vec::new(), Vec::new()),
};
let glass = if content.glass_panels.is_empty() {
None
} else {
Some(super::glass::build_glass_producer(
super::glass::GlassBuild {
alloc,
root_sig: &root_sig,
rt_root_sig: rt_root_sig.as_ref(),
msaa_samples,
hot_reload,
info_queue,
},
content.glass_panels,
content.glass_planar_slots,
)?)
};
let water = if content.water_surfaces.is_empty() {
None
} else {
Some(super::water::build_water_producer(
super::water::WaterBuild {
alloc,
root_sig: &root_sig,
rt_root_sig: rt_root_sig.as_ref(),
msaa_samples,
hot_reload,
info_queue,
},
content.water_surfaces,
content.water_planar_slots,
)?)
};
let glass_mesh = match (content.seethrough_mesh_indices.is_empty(), &rt_root_sig) {
(false, Some(sig)) => {
match super::glass::build_glass_mesh_producer(
super::glass::GlassMeshBuild {
alloc,
rt_root_sig: sig,
msaa_samples,
hot_reload,
info_queue,
},
content.seethrough_mesh_indices,
) {
Ok(p) => Some(p),
Err(e) => {
tracing::warn!(
"see-through glass mesh pipeline build failed ({e}); those meshes render opaque"
);
None
}
}
}
_ => None,
};
let view_size = align256(std::mem::size_of::<TransparentView>() 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 transparent view ubo: {e}"))?;
view_ubo_ptrs.push(ptr as *mut u8);
view_ubo_resources.push(buf);
}
let scene_copy = create_hdr_resolve_target(device, width.max(1), height.max(1))?;
write_scene_copy_srv(device, &scene_copy, scene_copy_srv_cpu);
Ok(Self {
root_sig,
glass,
water,
glass_mesh,
view_ubo_resources,
view_ubo_ptrs,
scene_copy,
scene_copy_srv_cpu,
scene_copy_srv_gpu,
depth_srv_gpu,
rt_root_sig,
rt_params_ubo_resources,
rt_params_ubo_ptrs,
})
}
pub(in crate::directx) fn root_sig(&self) -> &ID3D12RootSignature {
&self.root_sig
}
pub(in crate::directx) fn swap_pipelines(
&mut self,
glass_pso: Option<ID3D12PipelineState>,
water_pso: Option<ID3D12PipelineState>,
) {
if let (Some(pso), Some(p)) = (glass_pso, self.glass.as_mut()) {
p.pso = pso;
}
if let (Some(pso), Some(p)) = (water_pso, self.water.as_mut()) {
p.pso = pso;
}
}
pub(in crate::directx) fn has_glass(&self) -> bool {
self.glass.is_some()
}
pub(in crate::directx) fn has_water(&self) -> bool {
self.water.is_some()
}
pub(in crate::directx) fn rt_pipelines_ready(&self) -> bool {
self.rt_root_sig.is_some()
&& self.glass.as_ref().is_none_or(|p| p.flat_rt_pso.is_some())
&& self.water.as_ref().is_none_or(|p| p.flat_rt_pso.is_some())
}
pub(in crate::directx) fn rt_textured_ready(&self) -> bool {
self.glass
.as_ref()
.is_none_or(|p| p.textured_rt_pso.is_some())
&& self
.water
.as_ref()
.is_none_or(|p| p.textured_rt_pso.is_some())
&& self
.glass_mesh
.as_ref()
.is_none_or(|p| p.textured_rt_pso.is_some())
}
pub(in crate::directx) fn resize_to(
&mut self,
device: &ID3D12Device,
width: u32,
height: u32,
) -> Result<(), String> {
self.scene_copy = create_hdr_resolve_target(device, width.max(1), height.max(1))?;
write_scene_copy_srv(device, &self.scene_copy, self.scene_copy_srv_cpu);
Ok(())
}
pub(in crate::directx) fn water_planar_slot_live(&self) -> bool {
self.water.as_ref().is_some_and(|p| {
p.records
.iter()
.any(|r| r.visible && r.planar_slot.is_some())
})
}
pub(in crate::directx) fn any_visible(&self) -> bool {
let live = |p: &Option<TransparentProducer>| {
p.as_ref()
.is_some_and(|p| p.records.iter().any(|r| r.visible))
};
live(&self.glass) || live(&self.water)
}
pub(in crate::directx) fn seethrough_mesh_indices(&self) -> &[usize] {
self.glass_mesh
.as_ref()
.map(|p| p.object_indices.as_slice())
.unwrap_or_default()
}
pub(in crate::directx) fn mesh_pipelines_ready(&self) -> bool {
self.glass_mesh.is_some()
}
fn draw_order(&self, meshes: &[[f32; 3]], cam: [f32; 3]) -> Vec<(Producer, usize)> {
let centres = |p: &Option<TransparentProducer>| -> Vec<([f32; 3], bool)> {
p.as_ref()
.map(|p| p.records.iter().map(|r| (r.centre, r.visible)).collect())
.unwrap_or_default()
};
ordered_visible(¢res(&self.glass), ¢res(&self.water), meshes, cam)
}
fn record(&self, kind: Producer, index: usize) -> &TransparentRecord {
let producer = match kind {
Producer::Glass => self.glass.as_ref(),
Producer::Water => self.water.as_ref(),
Producer::GlassMesh => {
unreachable!("mesh draws are per-frame and never resolve to a static record")
}
};
&producer
.expect("the draw order only names live producers")
.records[index]
}
}
const GLASS_MESH_REFRACTION: f32 = 0.02;
const GLASS_MESH_FRESNEL_POWER: f32 = 1.0;
impl DxContext {
fn collect_mesh_draws(
&self,
transparent: &TransparentResources,
frame_idx: usize,
cam: [f32; 3],
) -> Vec<GlassMeshDraw> {
let Some(producer) = transparent.glass_mesh.as_ref() else {
return Vec::new();
};
let block = align256(std::mem::size_of::<GlassMeshParams>() as u64);
let ring_base = com::gpu_va(&producer.params_ring[frame_idx]);
let ring_ptr = producer.params_ptrs[frame_idx];
let prefilter_mip_count = self.env_map.prefilter_mip_count as f32;
let mut draws = Vec::with_capacity(producer.object_indices.len());
for (slot, &idx) in producer.object_indices.iter().enumerate() {
let Some(obj) = self.draw.objects.get(idx) else {
continue;
};
if !obj.visible || !obj.resident || obj.material.see_through == 0 {
continue;
}
let centre = [
0.5 * (obj.bb_min[0] + obj.bb_max[0]),
0.5 * (obj.bb_min[1] + obj.bb_max[1]),
0.5 * (obj.bb_min[2] + obj.bb_max[2]),
];
let d = crate::gfx::lod::camera_distance(obj, cam);
let (index_offset, index_count) = obj.active_lod(d);
let t = obj.material.tint;
let params = GlassMeshParams {
model: obj.model,
tint: [t[0], t[1], t[2], 0.0],
opacity: obj.material.opacity,
refraction_strength: GLASS_MESH_REFRACTION,
fresnel_power: GLASS_MESH_FRESNEL_POWER,
prefilter_mip_count,
};
let offset = slot as u64 * block;
unsafe {
std::ptr::copy_nonoverlapping(
¶ms as *const GlassMeshParams as *const u8,
ring_ptr.add(offset as usize),
std::mem::size_of::<GlassMeshParams>(),
);
}
draws.push(GlassMeshDraw {
index_offset: index_offset as u32,
index_count: index_count as u32,
base_vertex: obj.base_vertex,
params_gva: ring_base + offset,
centre,
});
}
draws
}
pub(in crate::directx) fn encode_transparent(
&self,
cmd: &ID3D12GraphicsCommandList,
frame_idx: usize,
view: &TransparentView,
fov_y_radians: f32,
aspect: f32,
) -> Result<(), String> {
let transparent = match &self.transparent {
Some(t) => t,
None => return Ok(()),
};
let cam = [view.camera_pos[0], view.camera_pos[1], view.camera_pos[2]];
let rt_live = self.rt_transparent_active();
let textured =
rt_live && self.cull.main_bindless_pso.is_some() && transparent.rt_textured_ready();
let mesh_draws = if rt_live {
self.collect_mesh_draws(transparent, frame_idx, cam)
} else {
Vec::new()
};
let mesh_centres: Vec<[f32; 3]> = mesh_draws.iter().map(|d| d.centre).collect();
let order = transparent.draw_order(&mesh_centres, cam);
if order.is_empty() {
return Ok(());
}
unsafe {
std::ptr::copy_nonoverlapping(
view as *const TransparentView as *const u8,
transparent.view_ubo_ptrs[frame_idx],
std::mem::size_of::<TransparentView>(),
);
}
let view_gva = com::gpu_va(&transparent.view_ubo_resources[frame_idx]);
let rt_params_gva = if rt_live {
let rt = self.rt_reflections.as_ref().expect("rt_reflections_active");
let v = self.view.matrix;
let inv_view_rot = [
[v[0][0], v[1][0], v[2][0], 0.0],
[v[0][1], v[1][1], v[2][1], 0.0],
[v[0][2], v[1][2], v[2][2], 0.0],
[0.0, 0.0, 0.0, 1.0],
];
let params = rt.settings.params(RtParamsInputs {
fov_y_radians,
aspect,
inv_view_rot,
cam_pos: cam,
sun_dir: self.fog.sun_dir,
sun_color: self.fog.sun_color,
prefilter_mip_count: self.env_map.prefilter_mip_count as f32,
});
unsafe {
std::ptr::copy_nonoverlapping(
¶ms as *const RtParams as *const u8,
transparent.rt_params_ubo_ptrs[frame_idx],
std::mem::size_of::<RtParams>(),
);
}
Some(com::gpu_va(&transparent.rt_params_ubo_resources[frame_idx]))
} else {
None
};
let scene_res = self.post_scene_target();
let scene_rtv = self.post_scene_rtv();
let scene_to_copy = transition_barrier(
self.post_scene_target(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_COPY_SOURCE,
);
let copy_to_dst = transition_barrier(
&transparent.scene_copy,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
D3D12_RESOURCE_STATE_COPY_DEST,
);
unsafe { cmd.ResourceBarrier(&[scene_to_copy, copy_to_dst]) };
unsafe { cmd.CopyResource(&transparent.scene_copy, scene_res) };
let scene_to_rt = transition_barrier(
self.post_scene_target(),
D3D12_RESOURCE_STATE_COPY_SOURCE,
D3D12_RESOURCE_STATE_RENDER_TARGET,
);
let copy_to_psr = transition_barrier(
&transparent.scene_copy,
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
);
unsafe { cmd.ResourceBarrier(&[scene_to_rt, copy_to_psr]) };
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(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
cmd.SetDescriptorHeaps(&[Some(self.descriptors.srv_heap.clone())]);
}
let prefilter_srv = self.prefilter_cube_srv_gpu();
let probe_cube_srv = self.probe_cube_table_gpu();
let probe_set_gva = com::gpu_va(&self.probe.set_cbvs[frame_idx]);
let planar_root = if rt_live {
PLANAR_ROOT_RT
} else {
PLANAR_ROOT_BASE
};
let root_sig = if rt_live {
transparent
.rt_root_sig
.as_ref()
.expect("rt_live built the rt root sig")
} else {
&transparent.root_sig
};
unsafe {
cmd.SetGraphicsRootSignature(root_sig);
cmd.SetGraphicsRootConstantBufferView(0, view_gva);
cmd.SetGraphicsRootDescriptorTable(2, transparent.scene_copy_srv_gpu);
cmd.SetGraphicsRootDescriptorTable(3, transparent.depth_srv_gpu);
cmd.SetGraphicsRootDescriptorTable(4, prefilter_srv);
cmd.SetGraphicsRootDescriptorTable(5, probe_cube_srv);
cmd.SetGraphicsRootConstantBufferView(6, probe_set_gva);
}
if rt_live {
let rt_params_gva = rt_params_gva.expect("rt_live uploaded RtParams");
let accel = self.rt_accel.as_ref().expect("rt_reflections_active");
unsafe {
cmd.SetGraphicsRootConstantBufferView(7, rt_params_gva);
cmd.SetGraphicsRootShaderResourceView(8, accel.tlas_gva());
cmd.SetGraphicsRootShaderResourceView(9, com::gpu_va(&self.geometry.vertex_buffer));
cmd.SetGraphicsRootShaderResourceView(10, com::gpu_va(&self.geometry.index_buffer));
cmd.SetGraphicsRootShaderResourceView(11, accel.geom_table_gva());
cmd.SetGraphicsRootShaderResourceView(12, accel.deformed_verts_gva());
cmd.SetGraphicsRootShaderResourceView(13, accel.skinned_index_gva());
if textured {
cmd.SetGraphicsRootDescriptorTable(
14,
self.cull.bindless_pool_gpu[self.current_frame],
);
}
}
}
unsafe { cmd.SetGraphicsRootDescriptorTable(planar_root, transparent.scene_copy_srv_gpu) };
let mut bound: Option<Producer> = None;
for &(kind, i) in &order {
if bound != Some(kind) {
unsafe {
match kind {
Producer::GlassMesh => cmd.SetPipelineState(
transparent
.glass_mesh
.as_ref()
.expect("the draw order only names live producers")
.pipeline(textured),
),
Producer::Glass | Producer::Water => {
let producer = match kind {
Producer::Glass => transparent.glass.as_ref(),
_ => transparent.water.as_ref(),
}
.expect("the draw order only names live producers");
cmd.SetPipelineState(producer.pipeline(rt_live, textured));
}
}
}
bound = Some(kind);
}
if kind == Producer::GlassMesh {
let d = &mesh_draws[i];
unsafe {
cmd.IASetVertexBuffers(0, Some(&[self.geometry.vertex_buffer_view]));
cmd.IASetIndexBuffer(Some(&self.geometry.index_buffer_view));
cmd.SetGraphicsRootConstantBufferView(1, d.params_gva);
cmd.DrawIndexedInstanced(d.index_count, 1, d.index_offset, d.base_vertex, 0);
}
self.inc_draw_calls(1);
continue;
}
let r = transparent.record(kind, i);
unsafe {
cmd.IASetVertexBuffers(0, Some(&[r.vertex_buffer_view]));
cmd.IASetIndexBuffer(Some(&r.index_buffer_view));
cmd.SetGraphicsRootConstantBufferView(1, r.params_cbuffer_gva);
let planar_srv = r
.planar_slot
.and_then(|s| {
self.planar_reflection
.as_ref()
.map(|set| set.resolve_srv_gpu(s))
})
.unwrap_or(transparent.scene_copy_srv_gpu);
cmd.SetGraphicsRootDescriptorTable(planar_root, planar_srv);
cmd.DrawIndexedInstanced(r.index_count, 1, 0, 0, 0);
}
self.inc_draw_calls(1);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sort_distance_is_euclidean_and_monotone() {
let cam = [0.0, 0.0, 0.0];
let near = sort_distance([0.0, 0.0, 1.0], cam);
let far = sort_distance([0.0, 0.0, 5.0], cam);
assert!((near - 1.0).abs() < 1e-5);
assert!((far - 5.0).abs() < 1e-5);
assert!(far > near);
}
#[test]
fn ordered_visible_excludes_hidden_and_sorts_back_to_front() {
let glass = [
([0.0, 0.0, 5.0], true),
([0.0, 0.0, 9.0], false),
([0.0, 0.0, 3.0], true),
];
let order = ordered_visible(&glass, &[], &[], [0.0, 0.0, 0.0]);
assert_eq!(order, vec![(Producer::Glass, 0), (Producer::Glass, 2)]);
}
#[test]
fn ordered_visible_interleaves_the_two_producers() {
let glass = [([0.0, 0.0, 9.0], true), ([0.0, 0.0, 1.0], true)];
let water = [([0.0, 0.0, 5.0], true), ([0.0, 0.0, 7.0], false)];
let order = ordered_visible(&glass, &water, &[], [0.0, 0.0, 0.0]);
assert_eq!(
order,
vec![
(Producer::Glass, 0),
(Producer::Water, 0),
(Producer::Glass, 1),
]
);
}
#[test]
fn ordered_visible_interleaves_mesh_draws_with_the_static_producers() {
let glass = [([0.0, 0.0, 9.0], true)];
let water = [([0.0, 0.0, 3.0], true)];
let meshes = [[0.0, 0.0, 6.0], [0.0, 0.0, 1.0]];
let order = ordered_visible(&glass, &water, &meshes, [0.0, 0.0, 0.0]);
assert_eq!(
order,
vec![
(Producer::Glass, 0),
(Producer::GlassMesh, 0),
(Producer::Water, 0),
(Producer::GlassMesh, 1),
]
);
}
#[test]
fn ordered_visible_orders_meshes_alone_back_to_front() {
let meshes = [[0.0, 0.0, 2.0], [0.0, 0.0, 8.0]];
let order = ordered_visible(&[], &[], &meshes, [0.0, 0.0, 0.0]);
assert_eq!(
order,
vec![(Producer::GlassMesh, 1), (Producer::GlassMesh, 0)]
);
}
#[test]
fn ordered_visible_is_empty_with_no_visible_records() {
let glass = [([0.0, 0.0, 5.0], false)];
let water = [([0.0, 0.0, 3.0], false)];
assert!(ordered_visible(&glass, &water, &[], [0.0, 0.0, 0.0]).is_empty());
}
}