use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::render_graph::{
ClearValue, PixelFormat, TextureUsage, TransientSlot, TransientTexture,
};
use std::collections::HashMap;
use windows::Win32::Graphics::Direct3D12::*;
use windows::Win32::Graphics::Dxgi::Common::*;
use super::texture::{one_shot_submit, transition_barrier};
use crate::directx::error::map_hresult;
struct LabelEntry {
resource: usize,
alias_predecessor: Option<&'static str>,
}
pub(super) struct TransientResourcePool {
#[expect(
dead_code,
reason = "a placed resource does not keep its heap alive, so the pool retains the heaps"
)]
heaps: Vec<ID3D12Heap>,
resources: Vec<ID3D12Resource>,
by_label: HashMap<&'static str, LabelEntry>,
#[cfg(debug_assertions)]
slot_labels: Vec<Vec<&'static str>>,
allocated_bytes: u64,
}
impl TransientResourcePool {
pub(super) fn build(
device: &ID3D12Device,
queue: &ID3D12CommandQueue,
slots: &[TransientSlot],
) -> RenderResult<Self> {
let mut heaps = Vec::new();
let mut resources = Vec::new();
let mut allocated_bytes: u64 = 0;
let mut unaliased_bytes: u64 = 0;
let mut to_init: Vec<(ID3D12Resource, D3D12_RESOURCE_STATES)> = Vec::new();
for slot in slots {
let shared = slot.members.len() > 1;
let mut slot_size: u64 = 0;
let mut slot_align: u64 = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT as u64;
let descs: Vec<(&TransientTexture, D3D12_RESOURCE_DESC)> = slot
.members
.iter()
.map(|m| {
let desc = rt_desc(m);
let info = unsafe { device.GetResourceAllocationInfo(0, &[desc]) };
slot_size = slot_size.max(info.SizeInBytes);
slot_align = slot_align.max(info.Alignment);
unaliased_bytes += info.SizeInBytes;
(m, desc)
})
.collect();
let heap_desc = D3D12_HEAP_DESC {
SizeInBytes: slot_size,
Properties: D3D12_HEAP_PROPERTIES {
Type: D3D12_HEAP_TYPE_DEFAULT,
..Default::default()
},
Alignment: slot_align,
Flags: D3D12_HEAP_FLAG_ALLOW_ONLY_RT_DS_TEXTURES,
};
allocated_bytes += slot_size;
let mut heap: Option<ID3D12Heap> = None;
unsafe { device.CreateHeap(&heap_desc, &mut heap) }
.map_err(|e| map_hresult(e.code(), "transient pool heap"))?;
let heap =
heap.ok_or_else(|| RenderError::Other("transient pool heap returned None".into()))?;
for (m, desc) in &descs {
let clear = clear_value(m);
let mut res: Option<ID3D12Resource> = None;
unsafe {
device.CreatePlacedResource(
&heap,
0,
desc,
resting_state(m),
Some(&clear),
&mut res,
)
}
.map_err(|e| map_hresult(e.code(), &format!("transient pool place {}", m.label)))?;
let resource = res.ok_or_else(|| {
RenderError::Other("transient pool placed resource None".into())
})?;
if !shared {
to_init.push((resource.clone(), resting_state(m)));
}
resources.push(resource);
}
heaps.push(heap);
}
if !to_init.is_empty() {
one_shot_submit(device, queue, |cmd| {
for (res, resting) in &to_init {
unsafe {
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,
)]);
}
}
})?;
}
tracing::info!(
"transient heap pool: {} heap allocation(s), {} KiB ({} KiB saved by aliasing)",
heaps.len(),
allocated_bytes / 1024,
unaliased_bytes.saturating_sub(allocated_bytes) / 1024,
);
let by_label = index_labels(slots);
debug_assert_eq!(by_label.len(), resources.len(), "a label was placed twice");
Ok(Self {
heaps,
resources,
by_label,
#[cfg(debug_assertions)]
slot_labels: slots.iter().map(|s| s.labels()).collect(),
allocated_bytes,
})
}
pub(super) fn resource_for(&self, label: &str) -> Option<&ID3D12Resource> {
self.resources.get(self.by_label.get(label)?.resource)
}
pub(super) fn alias_predecessor(&self, label: &str) -> Option<&'static str> {
self.by_label.get(label)?.alias_predecessor
}
pub(super) fn allocated_bytes(&self) -> u64 {
self.allocated_bytes
}
pub(super) fn gbuffer_pooled(&self) -> Option<super::post::gbuffer::GbufferPooled> {
Some(super::post::gbuffer::GbufferPooled {
normal_depth: self.resource_for("gbuffer_normal_depth")?.clone(),
roughness: self.resource_for("gbuffer_roughness")?.clone(),
velocity: self.resource_for("gbuffer_velocity")?.clone(),
})
}
#[cfg(debug_assertions)]
pub(super) fn slot_labels(&self) -> &[Vec<&'static str>] {
&self.slot_labels
}
pub(super) fn rebuild(
&mut self,
device: &ID3D12Device,
queue: &ID3D12CommandQueue,
slots: &[TransientSlot],
) -> RenderResult<()> {
*self = Self::build(device, queue, slots)?;
Ok(())
}
}
fn index_labels(slots: &[TransientSlot]) -> HashMap<&'static str, LabelEntry> {
let mut by_label = HashMap::new();
let mut next = 0;
for slot in slots {
let n = slot.members.len();
for (pos, m) in slot.members.iter().enumerate() {
by_label.insert(
m.label,
LabelEntry {
resource: next,
alias_predecessor: (n > 1).then(|| slot.members[(pos + n - 1) % n].label),
},
);
next += 1;
}
}
by_label
}
fn clear_value(m: &TransientTexture) -> D3D12_CLEAR_VALUE {
let format = dxgi_format(m.format);
match m.clear {
ClearValue::Color(color) => D3D12_CLEAR_VALUE {
Format: format,
Anonymous: D3D12_CLEAR_VALUE_0 { Color: color },
},
ClearValue::Depth(depth) => D3D12_CLEAR_VALUE {
Format: format,
Anonymous: D3D12_CLEAR_VALUE_0 {
DepthStencil: D3D12_DEPTH_STENCIL_VALUE {
Depth: depth,
Stencil: 0,
},
},
},
}
}
fn rt_desc(m: &TransientTexture) -> D3D12_RESOURCE_DESC {
let (dimension, depth_or_array) = if m.depth.max(1) > 1 {
(D3D12_RESOURCE_DIMENSION_TEXTURE3D, m.depth.max(1))
} else {
(D3D12_RESOURCE_DIMENSION_TEXTURE2D, m.array_layers.max(1))
};
D3D12_RESOURCE_DESC {
Dimension: dimension,
Alignment: 0,
Width: m.width.max(1) as u64,
Height: m.height.max(1),
DepthOrArraySize: depth_or_array as u16,
MipLevels: m.mip_levels.max(1) as u16,
Format: dxgi_format(m.format),
SampleDesc: DXGI_SAMPLE_DESC {
Count: m.sample_count.max(1),
Quality: 0,
},
Layout: D3D12_TEXTURE_LAYOUT_UNKNOWN,
Flags: resource_flags(m.usage),
}
}
pub(in crate::directx) fn dxgi_format(format: PixelFormat) -> DXGI_FORMAT {
match format {
PixelFormat::Rgba16Float => DXGI_FORMAT_R16G16B16A16_FLOAT,
PixelFormat::Rgba8Unorm => DXGI_FORMAT_R8G8B8A8_UNORM,
PixelFormat::Rg16Float => DXGI_FORMAT_R16G16_FLOAT,
PixelFormat::R8Unorm => DXGI_FORMAT_R8_UNORM,
PixelFormat::R16Float => DXGI_FORMAT_R16_FLOAT,
PixelFormat::R32Float => DXGI_FORMAT_R32_FLOAT,
PixelFormat::Rg32Float => DXGI_FORMAT_R32G32_FLOAT,
PixelFormat::Depth32Float => DXGI_FORMAT_D32_FLOAT,
PixelFormat::BgraSwapchain => DXGI_FORMAT_B8G8R8A8_UNORM,
}
}
fn resource_flags(usage: TextureUsage) -> D3D12_RESOURCE_FLAGS {
let mut flags = D3D12_RESOURCE_FLAG_NONE;
if usage.contains(TextureUsage::RENDER_TARGET) {
flags |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
}
if usage.contains(TextureUsage::DEPTH_STENCIL) {
flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
}
if usage.contains(TextureUsage::STORAGE) {
flags |= D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
}
flags
}
fn resting_state(m: &TransientTexture) -> D3D12_RESOURCE_STATES {
if m.usage.contains(TextureUsage::DEPTH_STENCIL) {
D3D12_RESOURCE_STATE_DEPTH_WRITE
} else {
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE
}
}
#[cfg(test)]
mod tests {
use super::super::post::gbuffer::GBUFFER_ROUGHNESS_CLEAR;
use super::*;
use concinnity_core::render::depth::DEPTH_CLEAR;
use concinnity_core::render::post::device::PostExtent;
use concinnity_core::render::post::{bloom, ssao};
use concinnity_core::render::render_graph::{self, PoolGates, plan_pool_slots};
fn planned(render: (u32, u32), output: (u32, u32)) -> Vec<TransientSlot> {
let gates = PoolGates {
ssao: true,
gbuffer: true,
};
plan_pool_slots(gates, render, output).expect("plans")
}
#[test]
fn the_label_index_resolves_placement_order_and_the_cyclic_predecessor() {
let slots = planned((1024, 768), (1024, 768));
let index = index_labels(&slots);
let mut placed = 0;
for slot in &slots {
for m in &slot.members {
assert_eq!(index[m.label].resource, placed, "{}", m.label);
placed += 1;
let pred = index[m.label].alias_predecessor;
if slot.members.len() > 1 {
assert!(
slot.labels().contains(&pred.expect("shared member")),
"{} reclaims from outside its slot",
m.label
);
} else {
assert_eq!(pred, None, "{} sits alone", m.label);
}
}
}
assert_eq!(index.len(), placed, "a label was placed twice");
assert!(index["bloom_top"].alias_predecessor.is_some());
}
#[test]
fn the_roughness_clear_matches_the_feature_constant() {
let slots = planned((1024, 768), (1024, 768));
let roughness = slots
.iter()
.flat_map(|s| &s.members)
.find(|m| m.label == "gbuffer_roughness")
.expect("roughness pooled");
assert_eq!(
roughness.clear,
render_graph::ClearValue::Color(GBUFFER_ROUGHNESS_CLEAR)
);
}
#[test]
fn translated_descs_match_the_feature_formats() {
let slots = planned((1024, 768), (1920, 1080));
let member = |label: &str| {
slots
.iter()
.flat_map(|s| &s.members)
.find(|m| m.label == label)
.unwrap_or_else(|| panic!("{label} pooled"))
.clone()
};
let ao = member("ao_output");
let ao_desc = rt_desc(&ao);
assert_eq!((ao_desc.Width, ao_desc.Height), (1024, 768));
assert_eq!(ao_desc.Format, dxgi_format(ssao::OCCLUSION_FORMAT));
assert_eq!(ao_desc.MipLevels, 1);
assert_eq!(ao_desc.SampleDesc.Count, 1);
assert_eq!(ao_desc.Dimension, D3D12_RESOURCE_DIMENSION_TEXTURE2D);
assert_eq!(ao_desc.Flags, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET);
assert_eq!(
resting_state(&ao),
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE
);
let bloom = member("bloom_top");
let bloom_desc = rt_desc(&bloom);
let output = PostExtent {
width: 1920,
height: 1080,
};
let top = bloom::top_extent(output);
assert_eq!(
(bloom_desc.Width, bloom_desc.Height),
(u64::from(top.width), top.height)
);
assert_eq!(
bloom_desc.Format,
dxgi_format(bloom::chain_desc(output).format)
);
use super::super::post::gbuffer::{
GBUFFER_NORMAL_DEPTH_FORMAT, GBUFFER_ROUGHNESS_FORMAT, GBUFFER_VELOCITY_FORMAT,
};
for (label, format) in [
("gbuffer_normal_depth", GBUFFER_NORMAL_DEPTH_FORMAT),
("gbuffer_roughness", GBUFFER_ROUGHNESS_FORMAT),
("gbuffer_velocity", GBUFFER_VELOCITY_FORMAT),
] {
let desc = rt_desc(&member(label));
assert_eq!(desc.Format, format, "{label}");
assert_eq!((desc.Width, desc.Height), (1024, 768), "{label}");
assert_eq!(desc.SampleDesc.Count, 1, "{label} rasterizes once");
assert_eq!(
desc.Flags, D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET,
"{label}"
);
}
}
#[test]
fn depth_and_volume_shapes_translate() {
let depth = TransientTexture {
label: "probe_depth",
width: 8,
height: 8,
depth: 1,
format: PixelFormat::Depth32Float,
sample_count: 4,
array_layers: 1,
mip_levels: 1,
usage: TextureUsage::DEPTH_STENCIL.union(TextureUsage::SHADER_READ),
clear: ClearValue::DEPTH,
};
let desc = rt_desc(&depth);
assert_eq!(desc.Format, DXGI_FORMAT_D32_FLOAT);
assert_eq!(desc.SampleDesc.Count, 4);
assert_eq!(desc.Flags, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL);
assert_eq!(resting_state(&depth), D3D12_RESOURCE_STATE_DEPTH_WRITE);
assert_eq!(
unsafe { clear_value(&depth).Anonymous.DepthStencil.Depth },
DEPTH_CLEAR
);
let volume = TransientTexture {
label: "probe_volume",
depth: 64,
format: PixelFormat::Rgba16Float,
sample_count: 1,
usage: TextureUsage::STORAGE.union(TextureUsage::SHADER_READ),
clear: ClearValue::Color([0.0; 4]),
..depth
};
let desc = rt_desc(&volume);
assert_eq!(desc.Dimension, D3D12_RESOURCE_DIMENSION_TEXTURE3D);
assert_eq!(desc.DepthOrArraySize, 64);
assert_eq!(desc.Flags, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS);
}
}