use concinnity_core::gfx::transform::mat4_inverse;
use std::sync::Mutex;
use windows::Win32::Graphics::Direct3D12::*;
use crate::gfx::render_graph::{
CompiledGraph, CompiledPass, GraphResourceClass, PassId, final_states,
};
use crate::gfx::render_types::{LineVertex, TextDrawCall};
use super::barrier_translate::{DxBarrier, d3d12_barrier, d3d12_restore};
use super::context::DxContext;
use super::parallel_encoder::{ParallelCtxRef, SendableCmdList, pool_index};
use super::texture::{aliasing_barrier, transition_barrier, uav_barrier};
struct DxBarrierTarget {
resources: Vec<ID3D12Resource>,
class: GraphResourceClass,
resting: D3D12_RESOURCE_STATES,
}
struct DxBarrierRegistry(Vec<Option<DxBarrierTarget>>);
unsafe impl Sync for DxBarrierRegistry {}
struct DxAliasBarriers(Vec<Vec<ID3D12Resource>>);
unsafe impl Sync for DxAliasBarriers {}
fn emit_alias_barriers(cmd: &ID3D12GraphicsCommandList, resources: &[ID3D12Resource]) {
const RESTING: D3D12_RESOURCE_STATES = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
for res in resources {
unsafe {
cmd.ResourceBarrier(&[aliasing_barrier(res)]);
cmd.ResourceBarrier(&[transition_barrier(
res,
RESTING,
D3D12_RESOURCE_STATE_RENDER_TARGET,
)]);
cmd.DiscardResource(res, None);
cmd.ResourceBarrier(&[transition_barrier(
res,
D3D12_RESOURCE_STATE_RENDER_TARGET,
RESTING,
)]);
}
}
}
fn emit_graph_barriers(
cmd: &ID3D12GraphicsCommandList,
registry: &DxBarrierRegistry,
pass: &CompiledPass,
) {
for op in &pass.barriers_before {
let Some(Some(target)) = registry.0.get(op.resource_index()) else {
continue;
};
let Some(barrier) = d3d12_barrier(
target.class,
target.resting,
op.source_state(),
op.to_state(),
op.read_stages(),
) else {
continue;
};
let native: Vec<D3D12_RESOURCE_BARRIER> = target
.resources
.iter()
.map(|r| match barrier {
DxBarrier::Transition(before, after) => transition_barrier(r, before, after),
DxBarrier::Uav => uav_barrier(r),
})
.collect();
unsafe {
cmd.ResourceBarrier(&native);
}
}
}
fn emit_pass_prologue(
cmd: &ID3D12GraphicsCommandList,
registry: &DxBarrierRegistry,
alias: &DxAliasBarriers,
idx: usize,
pass: &CompiledPass,
) {
emit_alias_barriers(cmd, &alias.0[idx]);
emit_graph_barriers(cmd, registry, pass);
}
fn emit_graph_restores(
cmd: &ID3D12GraphicsCommandList,
registry: &DxBarrierRegistry,
graph: &CompiledGraph,
) {
for (idx, (state, stages)) in final_states(graph).into_iter().enumerate() {
let Some(Some(target)) = registry.0.get(idx) else {
continue;
};
let Some((before, after)) = d3d12_restore(target.class, target.resting, state, stages)
else {
continue;
};
let native: Vec<D3D12_RESOURCE_BARRIER> = target
.resources
.iter()
.map(|r| transition_barrier(r, before, after))
.collect();
unsafe {
cmd.ResourceBarrier(&native);
}
}
}
#[cfg(debug_assertions)]
fn debug_assert_graph_drives(graph: &CompiledGraph, registry: &DxBarrierRegistry) {
use super::barrier_translate::d3d12_state;
use crate::gfx::render_graph::{ResourceState, barrier_coverage_gaps_for_driven};
let driven: Vec<bool> = registry.0.iter().map(|t| t.is_some()).collect();
let gaps = barrier_coverage_gaps_for_driven(graph, &driven);
assert!(
gaps.is_empty(),
"render graph (directx): uncovered accesses on graph-driven resources: {}",
gaps.iter()
.map(|g| g.to_string())
.collect::<Vec<_>>()
.join(", ")
);
for (idx, (state, stages)) in final_states(graph).into_iter().enumerate() {
let Some(Some(target)) = registry.0.get(idx) else {
continue;
};
if state == ResourceState::Undefined {
continue;
}
let restored = match d3d12_restore(target.class, target.resting, state, stages) {
Some((_, after)) => after,
None => d3d12_state(target.class, state, stages),
};
assert_eq!(
restored.0, target.resting.0,
"render graph (directx): {} rests in {:?} but the frame leaves it in {:?}",
graph.resources[idx].label, target.resting, restored,
);
}
}
#[derive(Clone, Copy)]
pub(in crate::directx) struct CompositeRenderTarget<'a> {
pub back_buffer: &'a ID3D12Resource,
pub back_buffer_rtv: D3D12_CPU_DESCRIPTOR_HANDLE,
}
#[derive(Clone, Copy)]
pub(in crate::directx) struct CompositeResolution {
pub width: u32,
pub height: u32,
}
#[derive(Clone, Copy)]
pub(in crate::directx) struct MainPassExtent {
pub width: u32,
pub height: u32,
}
#[derive(Clone, Copy)]
pub(in crate::directx) struct FrameGpuBuffers {
pub view_gva: u64,
pub light_gva: u64,
pub local_lights_gva: u64,
pub shadow_ubo_gva: u64,
}
pub(in crate::directx) struct GraphFrameParams<'a> {
pub cmd: &'a ID3D12GraphicsCommandList,
pub frame_idx: usize,
pub back_buffer: &'a ID3D12Resource,
pub back_buffer_rtv: D3D12_CPU_DESCRIPTOR_HANDLE,
pub text_calls: &'a [TextDrawCall],
pub lines: &'a [LineVertex],
pub world_hidden: bool,
pub scene_srv: D3D12_GPU_DESCRIPTOR_HANDLE,
pub width: u32,
pub height: u32,
pub output_width: u32,
pub output_height: u32,
pub cam_pos: [f32; 3],
pub shadow_ubo_gva: u64,
pub view_gva: u64,
pub light_gva: u64,
pub local_lights_gva: u64,
pub vp_mat: [[f32; 4]; 4],
pub cur_vp: [[f32; 4]; 4],
pub frustum: &'a crate::gfx::frustum::Frustum,
pub fov_y_radians: f32,
pub aspect: f32,
pub elapsed: f32,
pub near: f32,
pub far: f32,
}
impl DxContext {
pub(in crate::directx) fn execute_graph(
&self,
graph: &CompiledGraph,
params: &GraphFrameParams<'_>,
) -> Result<Vec<ID3D12GraphicsCommandList>, String> {
let composite_idx = graph.passes.iter().position(|p| p.id == PassId::Composite);
let worker_slots: Mutex<Vec<Option<SendableCmdList>>> =
Mutex::new((0..graph.passes.len()).map(|_| None).collect());
let first_error: Mutex<Option<String>> = Mutex::new(None);
let ctx_ref = ParallelCtxRef::new(self);
let registry = self.build_barrier_registry(graph, params.frame_idx);
#[cfg(debug_assertions)]
debug_assert_graph_drives(graph, ®istry);
#[cfg(debug_assertions)]
crate::gfx::render_graph::assert_slot_aliasing_sound(
graph,
self.transient_pool.slot_labels(),
"directx",
);
let registry_ref = ®istry;
let alias_barriers = self.build_alias_barriers(graph);
let alias_barriers_ref = &alias_barriers;
let frame_idx = params.frame_idx;
crate::jobs::pool().install(|| {
rayon::scope(|scope| {
for (idx, pass) in graph.passes.iter().enumerate() {
if Some(idx) == composite_idx {
continue;
}
let pass_id = pass.id;
let first_error_ref = &first_error;
let worker_slots_ref = &worker_slots;
scope.spawn(move |_| {
let ctx = ctx_ref.as_ctx();
let pool_idx = pool_index(frame_idx, pass_id);
let alloc = &ctx.commands.pass_allocators[pool_idx];
let cmd = &ctx.commands.pass_cmd_lists[pool_idx];
if let Err(e) = unsafe { alloc.Reset() } {
let mut lock = first_error_ref.lock().unwrap();
if lock.is_none() {
*lock = Some(format!(
"per-pass allocator reset ({}): {e}",
pass_id.name()
));
}
return;
}
if let Err(e) = unsafe { cmd.Reset(alloc, None) } {
let mut lock = first_error_ref.lock().unwrap();
if lock.is_none() {
*lock = Some(format!(
"per-pass cmd list reset ({}): {e}",
pass_id.name()
));
}
return;
}
if let Some(heap) = ctx.timestamps.query_heap.as_ref() {
let (start_slot, _) = super::pass_timing::pass_pair(frame_idx, pass_id);
unsafe {
cmd.EndQuery(heap, D3D12_QUERY_TYPE_TIMESTAMP, start_slot);
}
}
emit_pass_prologue(cmd, registry_ref, alias_barriers_ref, idx, pass);
let encode_result = ctx.encode_pass_into(pass_id, cmd, params);
if let Some(heap) = ctx.timestamps.query_heap.as_ref() {
let (_, end_slot) = super::pass_timing::pass_pair(frame_idx, pass_id);
unsafe {
cmd.EndQuery(heap, D3D12_QUERY_TYPE_TIMESTAMP, end_slot);
}
}
if let Err(e) = unsafe { cmd.Close() } {
let mut lock = first_error_ref.lock().unwrap();
if lock.is_none() {
*lock = Some(format!(
"per-pass cmd list close ({}): {e}",
pass_id.name()
));
}
return;
}
match encode_result {
Ok(()) => {
let mut lock = worker_slots_ref.lock().unwrap();
lock[idx] = Some(SendableCmdList(cmd.clone()));
}
Err(e) => {
let mut lock = first_error_ref.lock().unwrap();
if lock.is_none() {
*lock = Some(e);
}
}
}
});
}
});
});
if let Some(err) = first_error.into_inner().unwrap_or(None) {
return Err(err);
}
if let Some(idx) = composite_idx {
if let Some(heap) = self.timestamps.query_heap.as_ref() {
let (start_slot, _) = super::pass_timing::pass_pair(frame_idx, PassId::Composite);
unsafe {
params
.cmd
.EndQuery(heap, D3D12_QUERY_TYPE_TIMESTAMP, start_slot);
}
}
emit_pass_prologue(
params.cmd,
®istry,
&alias_barriers,
idx,
&graph.passes[idx],
);
self.encode_composite_and_text(
params.cmd,
params.frame_idx,
CompositeRenderTarget {
back_buffer: params.back_buffer,
back_buffer_rtv: params.back_buffer_rtv,
},
params.text_calls,
params.scene_srv,
CompositeResolution {
width: params.output_width,
height: params.output_height,
},
)?;
if let Some(heap) = self.timestamps.query_heap.as_ref() {
let (_, end_slot) = super::pass_timing::pass_pair(frame_idx, PassId::Composite);
unsafe {
params
.cmd
.EndQuery(heap, D3D12_QUERY_TYPE_TIMESTAMP, end_slot);
}
}
}
emit_graph_restores(params.cmd, ®istry, graph);
let slots = worker_slots
.into_inner()
.map_err(|_| "graph executor (directx): worker slot mutex poisoned".to_string())?;
let mut ordered = Vec::with_capacity(graph.passes.len());
for cb in slots.into_iter().flatten() {
ordered.push(cb.0);
}
Ok(ordered)
}
fn build_barrier_registry(&self, graph: &CompiledGraph, frame_idx: usize) -> DxBarrierRegistry {
DxBarrierRegistry(
graph
.resources
.iter()
.map(|res| {
let class = res.class()?;
let (resources, resting) =
self.barrier_objects_for_label(res.label, frame_idx)?;
Some(DxBarrierTarget {
resources,
class,
resting,
})
})
.collect(),
)
}
fn build_alias_barriers(&self, graph: &CompiledGraph) -> DxAliasBarriers {
let mut table: Vec<Vec<ID3D12Resource>> = vec![Vec::new(); graph.passes.len()];
for res in &graph.resources {
if self.transient_pool.alias_predecessor(res.label).is_none() {
continue;
}
if let Some(r) = self.transient_pool.resource_for(res.label) {
let first = res.lifetime.first;
if first < table.len() {
table[first].push(r.clone());
}
}
}
DxAliasBarriers(table)
}
fn barrier_objects_for_label(
&self,
label: &str,
frame_idx: usize,
) -> Option<(Vec<ID3D12Resource>, D3D12_RESOURCE_STATES)> {
let buffer = |slots: &[ID3D12Resource], resting| {
slots.get(frame_idx).map(|r| (vec![r.clone()], resting))
};
let one = |r: &ID3D12Resource, resting| (vec![r.clone()], resting);
const SAMPLED: D3D12_RESOURCE_STATES = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
match label {
"draw_args" => buffer(
&self.cull.indirect_cmd_buffers,
D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT,
),
"draw_args2" => buffer(
&self.cull.indirect_cmd_buffers_2,
D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT,
),
"cull_status" => buffer(
&self.cull.cull_status_buffers,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
),
"cluster_light_list" => Some(one(&self.light_cull.cluster_buffer, SAMPLED)),
"ao_output" => self
.transient_pool
.resource_for("ao_output")
.map(|r| one(r, SAMPLED)),
"bloom_top" => self
.transient_pool
.resource_for("bloom_top")
.map(|r| one(r, SAMPLED)),
"shadow_map" => self
.shadow
.resource
.as_ref()
.filter(|_| !self.shadow.dsvs.is_empty())
.map(|s| one(&s.resource, SAMPLED)),
"spot_shadow_map" => self
.spot_shadow
.resource
.as_ref()
.filter(|_| !self.spot_shadow.dsvs.is_empty())
.map(|s| one(&s.resource, SAMPLED)),
"fog_froxel_volume" => self
.fog
.resources
.as_ref()
.map(|f| one(&f.volume_resource, SAMPLED)),
"hdr_depth" => Some(one(&self.depth.resource, D3D12_RESOURCE_STATE_DEPTH_WRITE)),
"hdr_color" => self
.hdr
.resolve
.is_some()
.then(|| one(&self.hdr.color, D3D12_RESOURCE_STATE_RENDER_TARGET)),
"hdr_resolve" => Some(match &self.hdr.resolve {
Some(resolve) => one(resolve, SAMPLED),
None => one(&self.hdr.color, D3D12_RESOURCE_STATE_RENDER_TARGET),
}),
"scene_pre_taa" => self
.reflection_composite
.as_ref()
.map(|rc| one(&rc.output, SAMPLED)),
"scene_color" => self
.taa
.as_ref()
.filter(|_| self.upscale.backend.is_none())
.map(|taa| one(&taa.history[taa.output_index()], SAMPLED)),
"gbuffer_normal_depth" => self
.gbuffer
.as_ref()
.map(|gb| one(&gb.normal_depth, SAMPLED)),
"gbuffer_roughness" => self.gbuffer.as_ref().map(|gb| one(&gb.roughness, SAMPLED)),
"gbuffer_velocity" => self.gbuffer.as_ref().map(|gb| one(&gb.velocity, SAMPLED)),
"hiz_pyramid" => self
.cull
.hiz
.as_ref()
.map(|h| one(&h.texture, h.rest_state)),
_ => None,
}
}
fn build_raymarch_view(&self, params: &GraphFrameParams<'_>) -> super::raymarch::RaymarchView {
let inv_vp = mat4_inverse(params.cur_vp);
super::raymarch::RaymarchView {
vp: params.cur_vp,
inv_vp,
cam_pos: [params.cam_pos[0], params.cam_pos[1], params.cam_pos[2], 0.0],
viewport: [params.width as f32, params.height as f32],
time: params.elapsed,
prefilter_mip_count: self.env_map.prefilter_mip_count as f32,
sky_rot: self.view.sky_rot,
}
}
fn build_transparent_view(
&self,
params: &GraphFrameParams<'_>,
) -> super::transparent::TransparentView {
let inv_vp = mat4_inverse(params.vp_mat);
let (sun_dir, sun_color) = crate::gfx::lights::glint_sun(&self.uniforms.light_uniforms);
super::transparent::TransparentView {
vp: params.vp_mat,
inv_vp,
camera_pos: [params.cam_pos[0], params.cam_pos[1], params.cam_pos[2], 0.0],
viewport: [params.width as f32, params.height as f32],
time: params.elapsed,
prefilter_mip_count: self.env_map.prefilter_mip_count as f32,
sky_rot: self.view.sky_rot,
sun_dir,
sun_color,
}
}
fn encode_pass_into(
&self,
pass_id: PassId,
cmd: &ID3D12GraphicsCommandList,
params: &GraphFrameParams<'_>,
) -> Result<(), String> {
match pass_id {
PassId::Cull => {
self.encode_cull(cmd, params.frame_idx, params.frustum, params.cam_pos);
self.encode_skin(cmd, params.frame_idx);
}
PassId::SsaoBlur => {
self.encode_ssao(cmd, params.fov_y_radians, params.aspect);
}
PassId::SsaoPrepass | PassId::SsaoKernel => {
return Err(format!(
"graph executor (directx): pass {} is bundled inside SsaoBlur \
(encode_ssao encodes all three SSAO sub-passes); it \
should not appear as its own graph node",
pass_id.name()
));
}
PassId::ReflectionComposite => {
return Err(format!(
"graph executor (directx): pass {} is a Metal-only inline \
reflection composite and should not appear as a graph node",
pass_id.name()
));
}
PassId::LightCull => {
self.encode_light_cull(cmd, params.frame_idx)?;
}
PassId::SsrPrepass => {
return Err(format!(
"graph executor (directx): pass {} is merged into GBufferPrepass \
and should not appear in the frame graph",
pass_id.name()
));
}
PassId::Shadow => {
let raymarch_view = self
.raymarch
.as_ref()
.filter(|rm| rm.any_shadow_casters())
.map(|_| self.build_raymarch_view(params));
self.encode_shadow_pass(
cmd,
params.frame_idx,
params.shadow_ubo_gva,
params.cam_pos,
raymarch_view.as_ref(),
);
}
PassId::SpotShadow => {
self.encode_spot_shadow_pass(cmd, params.frame_idx, params.cam_pos);
}
PassId::AutoExposure => {
self.encode_auto_exposure(cmd, params.frame_idx);
}
PassId::Main => {
self.encode_main_pass(
cmd,
params.frame_idx,
MainPassExtent {
width: params.width,
height: params.height,
},
FrameGpuBuffers {
view_gva: params.view_gva,
light_gva: params.light_gva,
local_lights_gva: params.local_lights_gva,
shadow_ubo_gva: params.shadow_ubo_gva,
},
params.world_hidden,
);
}
PassId::Decals => {
self.encode_decals(cmd, params.frame_idx, params.vp_mat, params.frustum);
}
PassId::Lines => {
self.encode_lines(cmd, params.frame_idx, params.vp_mat, params.lines)?;
}
PassId::Fog => {
self.encode_fog(cmd, params.frame_idx, params.vp_mat, params.cam_pos);
}
PassId::ParticlesDraw => {
self.encode_particles(
cmd,
params.frame_idx,
params.elapsed,
params.vp_mat,
params.frustum,
);
}
PassId::ParticlesSim => {
return Err(format!(
"graph executor (directx): pass {} is bundled inside ParticlesDraw \
(encode_particles runs both compute sim and render); it \
should not appear as its own graph node",
pass_id.name()
));
}
PassId::SsrResolve => {
self.encode_ssr_resolve(
cmd,
params.frame_idx,
params.fov_y_radians,
params.aspect,
params.cam_pos,
);
}
PassId::Velocity => {
return Err(format!(
"graph executor (directx): pass {} is merged into GBufferPrepass \
and should not appear in the frame graph",
pass_id.name()
));
}
PassId::TaaResolve => {
self.encode_taa(cmd);
}
PassId::Bloom => {
self.encode_bloom(cmd, params.scene_srv);
}
PassId::Composite => {
return Err(
"graph executor (directx): Composite must run on the outer cmd \
list: encode_pass_into is not the right entry point"
.into(),
);
}
PassId::Raymarch => {
let view = self.build_raymarch_view(params);
self.encode_raymarch(cmd, params.frame_idx, &view)?;
}
PassId::FogFroxel => {
self.encode_fog_froxel(
cmd,
params.frame_idx,
params.near,
params.vp_mat,
params.cam_pos,
params.shadow_ubo_gva,
);
}
PassId::Upscale => {
self.encode_upscale(cmd, params)?;
}
PassId::Transparent => {
if self.planar_pass_needed() {
self.encode_planar_reflections(cmd, params)?;
}
let view = self.build_transparent_view(params);
self.encode_transparent(
cmd,
params.frame_idx,
&view,
params.fov_y_radians,
params.aspect,
)?;
}
PassId::HizBuild | PassId::HizFinal => {
self.encode_hiz_build(cmd);
}
PassId::Cull2 => {
self.encode_cull_phase2(cmd, params.frame_idx, params.frustum, params.cur_vp);
}
PassId::Main2 => {
self.encode_main_pass_phase2(
cmd,
params.frame_idx,
params.width,
params.height,
FrameGpuBuffers {
view_gva: params.view_gva,
light_gva: params.light_gva,
local_lights_gva: params.local_lights_gva,
shadow_ubo_gva: params.shadow_ubo_gva,
},
);
}
PassId::Ssgi => {
self.encode_ssgi(cmd, params.frame_idx, params.fov_y_radians, params.aspect);
}
PassId::RtReflections => {
self.encode_rt_reflections(
cmd,
params.frame_idx,
params.fov_y_radians,
params.aspect,
params.cam_pos,
);
}
PassId::GBufferPrepass => {
self.encode_gbuffer_prepass(
cmd,
params.frame_idx,
crate::directx::post::gbuffer::GbufferPrepassView {
jittered_vp: params.vp_mat,
cur_vp: params.cur_vp,
},
self.taa.is_some(),
);
}
}
Ok(())
}
}