use windows::Win32::Graphics::Direct3D12::*;
use windows::Win32::Graphics::Dxgi::Common::*;
use super::texture::{one_shot_submit, transition_barrier};
use crate::gfx::render_graph::{
ClearValue, PixelFormat, PoolGates, TextureUsage, TransientSlot, TransientTexture,
plan_pool_slots,
};
struct PlacedResource {
label: &'static str,
resource: ID3D12Resource,
}
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<PlacedResource>,
alias_pred: Vec<(&'static str, &'static str)>,
#[cfg(debug_assertions)]
slot_labels: Vec<Vec<&'static str>>,
allocated_bytes: u64,
}
impl TransientResourcePool {
pub(super) fn build(
device: &ID3D12Device,
queue: &ID3D12CommandQueue,
slots: &[TransientSlot],
) -> Result<Self, String> {
let mut heaps = Vec::new();
let mut resources = Vec::new();
let mut alias_pred: Vec<(&'static str, &'static str)> = 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| format!("transient pool heap: {e}"))?;
let heap = heap.ok_or("transient pool heap returned None")?;
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| format!("transient pool place {}: {e}", m.label))?;
let resource = res.ok_or("transient pool placed resource None")?;
if !shared {
to_init.push((resource.clone(), resting_state(m)));
}
resources.push(PlacedResource {
label: m.label,
resource,
});
}
if shared {
let n = slot.members.len();
for i in 0..n {
alias_pred.push((slot.members[i].label, slot.members[(i + n - 1) % n].label));
}
}
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,
);
Ok(Self {
heaps,
resources,
alias_pred,
#[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
.iter()
.find(|r| r.label == label)
.map(|r| &r.resource)
}
pub(super) fn alias_predecessor(&self, label: &str) -> Option<&'static str> {
self.alias_pred
.iter()
.find(|(l, _)| *l == label)
.map(|(_, p)| *p)
}
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],
) -> Result<(), String> {
*self = Self::build(device, queue, slots)?;
Ok(())
}
}
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),
}
}
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::R32Float => DXGI_FORMAT_R32_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
}
}
pub(super) fn transient_slots(
ssao_enabled: bool,
gbuffer_enabled: bool,
render_extent: (u32, u32),
output_extent: (u32, u32),
) -> Result<Vec<TransientSlot>, String> {
plan_pool_slots(
PoolGates {
ssao: ssao_enabled,
bloom: true,
gbuffer: gbuffer_enabled,
},
render_extent,
output_extent,
)
}
#[cfg(test)]
mod tests {
use super::super::post::gbuffer::GBUFFER_ROUGHNESS_CLEAR;
use super::*;
#[test]
fn the_late_bloom_target_aliases_an_early_one() {
let slots = transient_slots(true, true, (1024, 768), (1024, 768)).expect("plans");
let shared: Vec<Vec<&str>> = slots
.iter()
.map(|s| s.labels())
.filter(|l| l.len() > 1)
.collect();
assert!(
shared.iter().any(|l| l.contains(&"bloom_top")),
"bloom_top should reuse an earlier member's heap region: {:?}",
slots.iter().map(|s| s.labels()).collect::<Vec<_>>()
);
let pair = shared
.iter()
.find(|l| l.contains(&"bloom_top"))
.expect("checked above");
assert_ne!(pair[0], "bloom_top", "{pair:?}");
}
#[test]
fn bloom_top_alone_is_unshared() {
let slots = transient_slots(false, false, (1024, 768), (1024, 768)).expect("plans");
assert_eq!(slots.len(), 1);
assert_eq!(slots[0].members.len(), 1);
assert_eq!(slots[0].members[0].label, "bloom_top");
}
#[test]
fn the_gbuffer_colour_targets_are_pooled_and_depth_is_not() {
let slots = transient_slots(true, true, (1024, 768), (1024, 768)).expect("plans");
let labels: Vec<&str> = slots.iter().flat_map(|s| s.labels()).collect();
for want in [
"gbuffer_normal_depth",
"gbuffer_roughness",
"gbuffer_velocity",
] {
assert!(labels.contains(&want), "{want} pooled: {labels:?}");
}
assert!(
!labels.contains(&"gbuffer_depth"),
"gbuffer_depth must stay feature-owned: {labels:?}"
);
}
#[test]
fn the_gbuffer_gate_is_what_places_them() {
let slots = transient_slots(true, false, (1024, 768), (1024, 768)).expect("plans");
let labels: Vec<&str> = slots.iter().flat_map(|s| s.labels()).collect();
assert!(!labels.contains(&"gbuffer_normal_depth"), "{labels:?}");
}
#[test]
fn the_roughness_clear_matches_the_feature_constant() {
let slots = transient_slots(true, true, (1024, 768), (1024, 768)).expect("plans");
let roughness = slots
.iter()
.flat_map(|s| &s.members)
.find(|m| m.label == "gbuffer_roughness")
.expect("roughness pooled");
assert_eq!(
roughness.clear,
crate::gfx::render_graph::ClearValue::Color(GBUFFER_ROUGHNESS_CLEAR)
);
}
#[test]
fn translated_descs_match_the_feature_formats() {
let slots = transient_slots(true, true, (1024, 768), (1920, 1080)).expect("plans");
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,
super::super::post::ssao::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);
assert_eq!((bloom_desc.Width, bloom_desc.Height), (960, 540));
assert_eq!(bloom_desc.Format, super::super::texture::HDR_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} rasterises 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(1.0),
};
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 },
1.0
);
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);
}
}