use std::collections::HashMap;
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use serde_json::Value;
use windows::Win32::Graphics::Direct3D12::*;
use super::descriptors::Description;
type Registers = Arc<Vec<(u32, u32, u32)>>;
fn lock<T>(m: &'static OnceLock<Mutex<T>>) -> MutexGuard<'static, T>
where
T: Default,
{
m.get_or_init(Default::default)
.lock()
.unwrap_or_else(|e| e.into_inner())
}
fn programs() -> MutexGuard<'static, HashMap<String, Option<Registers>>> {
static PROGRAMS: OnceLock<Mutex<HashMap<String, Option<Registers>>>> = OnceLock::new();
lock(&PROGRAMS)
}
type Stages = Arc<Vec<(&'static str, Option<Registers>)>>;
fn pipelines() -> MutexGuard<'static, HashMap<u64, Stages>> {
static PIPELINES: OnceLock<Mutex<HashMap<u64, Stages>>> = OnceLock::new();
lock(&PIPELINES)
}
#[derive(Clone, Copy)]
struct Place {
parameter: u32,
visibility: u32,
space: u32,
first: u32,
last: u32,
offset: Option<u32>,
}
fn roots() -> MutexGuard<'static, HashMap<u64, Arc<Vec<Place>>>> {
static ROOTS: OnceLock<Mutex<HashMap<u64, Arc<Vec<Place>>>>> = OnceLock::new();
lock(&ROOTS)
}
pub fn program(key: &str, code: &[u8]) {
let mut p = programs();
if !p.contains_key(key) {
p.insert(
key.to_string(),
locum_capture::rdef::uav_registers(code).map(Arc::new),
);
}
}
pub fn pipeline(id: u64, description: &Value) {
let stages: Vec<(&'static str, Option<Registers>)> = {
let p = programs();
description["shaders"]
.as_array()
.into_iter()
.flatten()
.filter_map(|s| {
let stage = match s["stage"].as_str()? {
"vs" => "vs",
"hs" => "hs",
"ds" => "ds",
"gs" => "gs",
"ps" => "ps",
"cs" => "cs",
"as" => "as",
"ms" => "ms",
_ => return None,
};
Some((stage, p.get(s["blob"].as_str()?).cloned().flatten()))
})
.collect()
};
pipelines().insert(id, Arc::new(stages));
}
pub fn state_object(id: u64, description: &Value) {
let subobjects = description["subobjects"]
.as_array()
.map_or(&[][..], Vec::as_slice);
let mut stages: Vec<(&'static str, Option<Registers>)> = {
let p = programs();
subobjects
.iter()
.filter(|s| s["type"] == D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY.0)
.map(|s| {
let registers = s["value"]["library"]["blob"]
.as_str()
.and_then(|k| p.get(k).cloned().flatten());
("lib", registers)
})
.collect()
};
let mut pipelines = pipelines();
let taken = subobjects
.iter()
.filter(|s| s["type"] == D3D12_STATE_SUBOBJECT_TYPE_EXISTING_COLLECTION.0)
.map(|s| s["value"]["collection"].as_u64().unwrap_or(0))
.chain(description["parent"].as_u64().filter(|&p| p != 0));
for other in taken {
match pipelines.get(&other) {
Some(theirs) => stages.extend(theirs.iter().cloned()),
None => stages.push(("lib", None)),
}
}
pipelines.insert(id, Arc::new(stages));
}
pub fn root(id: u64, decoded: &Value) {
let n = |v: &Value, key: &str| v[key].as_u64().unwrap_or(0) as u32;
let mut places = Vec::new();
for (i, p) in decoded["parameters"].as_array().into_iter().flatten().enumerate() {
let visibility = n(p, "visibility");
match p["type"].as_u64() {
Some(4) => {
let r = n(&p["value"], "ShaderRegister");
places.push(Place {
parameter: i as u32,
visibility,
space: n(&p["value"], "RegisterSpace"),
first: r,
last: r,
offset: None,
});
}
Some(0) => {
let mut next = 0u32;
for r in p["value"].as_array().into_iter().flatten() {
let count = n(r, "NumDescriptors");
let at = match n(r, "OffsetInDescriptorsFromTableStart") {
u32::MAX => next,
o => o,
};
next = at.saturating_add(count);
if n(r, "RangeType") == 1 {
let first = n(r, "BaseShaderRegister");
places.push(Place {
parameter: i as u32,
visibility,
space: n(r, "RegisterSpace"),
first,
last: if count == u32::MAX {
u32::MAX
} else {
first.saturating_add(count.max(1) - 1)
},
offset: Some(at),
});
}
}
}
_ => {}
}
}
roots().insert(id, Arc::new(places));
}
fn signatures() -> MutexGuard<'static, HashMap<u64, Arc<Vec<u32>>>> {
static SIGNATURES: OnceLock<Mutex<HashMap<u64, Arc<Vec<u32>>>>> = OnceLock::new();
lock(&SIGNATURES)
}
pub fn signature(id: u64, description: &Value) {
let set = locum_capture::d3d12::indirect::root_parameters(description);
signatures().insert(id, Arc::new(set));
}
pub fn destroyed(id: u64) {
pipelines().remove(&id);
roots().remove(&id);
signatures().remove(&id);
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Written {
pub resource: u64,
pub sub: Option<u32>,
}
#[derive(Default)]
pub struct Bound {
pso: u64,
roots: [u64; 2],
args: [HashMap<u32, Value>; 2],
}
const MOST_REGISTERS: u32 = 64;
impl Bound {
pub fn apply(&mut self, call: &str, a: &Value) {
let id = |v: &Value| v.as_u64().unwrap_or(0);
match call {
"SetPipelineState" | "SetPipelineState1" => self.pso = id(&a[0]),
"SetGraphicsRootSignature" | "SetComputeRootSignature" => {
let k = usize::from(call == "SetComputeRootSignature");
self.roots[k] = id(&a[0]);
self.args[k].clear();
}
"ClearState" => {
*self = Bound {
pso: id(&a[0]),
..Bound::default()
}
}
"ExecuteBundle" => *self = Bound::default(),
_ => {
let (k, kind) = if let Some(k) = call.strip_prefix("SetGraphicsRoot") {
(0, k)
} else if let Some(k) = call.strip_prefix("SetComputeRoot") {
(1, k)
} else {
return;
};
if matches!(kind, "DescriptorTable" | "UnorderedAccessView") {
self.args[k].insert(id(&a[0]) as u32, a[1].clone());
}
}
}
}
pub fn reset(pso: u64) -> Bound {
Bound {
pso,
..Bound::default()
}
}
pub fn written(&self, call: &str, a: &Value) -> (Vec<Written>, Vec<String>) {
let mut out = Vec::new();
let mut gaps = Vec::new();
match call {
"ClearUnorderedAccessViewUint" | "ClearUnorderedAccessViewFloat" => {
let resource = a[2].as_u64().unwrap_or(0);
let view = match a[1]["definition"].get("description") {
Some(d) => described(d),
None => match (a[0]["heap"].as_u64(), a[0]["slot"].as_u64()) {
(Some(heap), Some(slot)) => super::slots::uav(heap, slot as usize)
.filter(|(r, _, _)| *r == resource)
.map(|(_, _, view)| Some(view))
.ok_or_else(|| "its view is not known".to_string()),
_ => Err("its view is not known".into()),
},
};
match covered(resource, view) {
Ok(subs) => out.extend(subs),
Err(why) => gaps.push(why),
}
}
"Dispatch"
| "DispatchRays"
| "DispatchMesh"
| "DrawInstanced"
| "DrawIndexedInstanced"
| "ExecuteIndirect" => {
let kinds: &[usize] = match call {
"Dispatch" | "DispatchRays" => &[1],
"ExecuteIndirect" => &[0, 1],
_ => &[0],
};
let Some(stages) = pipelines().get(&self.pso).cloned() else {
gaps.push("its pipeline's programs are not known".into());
return (out, gaps);
};
let from_records = match call {
"ExecuteIndirect" => signatures()
.get(&a[0].as_u64().unwrap_or(0))
.cloned()
.unwrap_or_default(),
_ => Arc::default(),
};
for &k in kinds {
let compute = k == 1;
let Some(places) = roots().get(&self.roots[k]).cloned() else {
continue;
};
for (stage, registers) in stages
.iter()
.filter(|(s, _)| matches!(*s, "cs" | "lib") == compute)
{
let Some(registers) = registers else {
gaps.push(format!("its {stage} program says nothing of its UAVs"));
continue;
};
for &(space, first, last) in registers.iter() {
if last == u32::MAX || last - first >= MOST_REGISTERS {
gaps.push(
"a UAV array of unbounded or large size: what it writes is not named"
.into(),
);
continue;
}
for register in first..=last {
match self.resolve(&places, k, stage, space, register, &from_records) {
Ok(Some(w)) => out.extend(w),
Ok(None) => {}
Err(why) => gaps.push(why),
}
}
}
}
}
}
_ => {}
}
let mut seen = std::collections::HashSet::new();
out.retain(|w| seen.insert(w.clone()));
(out, gaps)
}
fn resolve(
&self,
places: &[Place],
k: usize,
stage: &str,
space: u32,
register: u32,
from_records: &[u32],
) -> Result<Option<Vec<Written>>, String> {
let visible = match stage {
"vs" => 1,
"hs" => 2,
"ds" => 3,
"gs" => 4,
"ps" => 5,
"as" => 6,
"ms" => 7,
_ => 0,
};
let place = places.iter().find(|p| {
(p.visibility == 0 || p.visibility == visible)
&& p.space == space
&& (p.first..=p.last).contains(®ister)
});
let place = match place {
Some(place) => place,
None if stage == "lib" => return Ok(None),
None => return Err(format!("u{register} space {space} is in no root parameter")),
};
if from_records.contains(&place.parameter) {
return Ok(None);
}
let Some(arg) = self.args[k].get(&place.parameter) else {
return Err(format!(
"root parameter {} was not set in this list",
place.parameter
));
};
match place.offset {
None => {
if arg.get("candidates").is_some() {
return Err("a root UAV's address names more than one resource".into());
}
let resource = arg["resource"].as_u64().unwrap_or(0);
Ok((resource != 0).then(|| vec![Written { resource, sub: None }]))
}
Some(offset) => {
let (Some(heap), Some(slot)) = (arg["heap"].as_u64(), arg["slot"].as_u64()) else {
return Err("a table's descriptor heap is not known".into());
};
let slot = slot as usize + offset as usize + (register - place.first) as usize;
let Some((resource, counter, view)) = super::slots::uav(heap, slot) else {
return Ok(None);
};
let mut written = covered(resource, Ok(Some(view)))?;
if counter != 0 {
written.push(Written {
resource: counter,
sub: None,
});
}
Ok(Some(written))
}
}
}
}
fn described(v: &Value) -> Result<Option<Description>, String> {
if v["default"] == true {
return Ok(Some(Description::DefaultView));
}
let n = |key: &str| v["payload"][key].as_u64().unwrap_or(0) as u32;
let mut d = D3D12_UNORDERED_ACCESS_VIEW_DESC {
Format: windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT(v["format"].as_i64().unwrap_or(0) as i32),
ViewDimension: D3D12_UAV_DIMENSION(v["dimension"].as_i64().unwrap_or(0) as i32),
..Default::default()
};
match d.ViewDimension {
D3D12_UAV_DIMENSION_BUFFER => {}
D3D12_UAV_DIMENSION_TEXTURE1D => {
d.Anonymous.Texture1D = D3D12_TEX1D_UAV {
MipSlice: n("MipSlice"),
}
}
D3D12_UAV_DIMENSION_TEXTURE1DARRAY => {
d.Anonymous.Texture1DArray = D3D12_TEX1D_ARRAY_UAV {
MipSlice: n("MipSlice"),
FirstArraySlice: n("FirstArraySlice"),
ArraySize: n("ArraySize"),
}
}
D3D12_UAV_DIMENSION_TEXTURE2D => {
d.Anonymous.Texture2D = D3D12_TEX2D_UAV {
MipSlice: n("MipSlice"),
PlaneSlice: n("PlaneSlice"),
}
}
D3D12_UAV_DIMENSION_TEXTURE2DARRAY => {
d.Anonymous.Texture2DArray = D3D12_TEX2D_ARRAY_UAV {
MipSlice: n("MipSlice"),
FirstArraySlice: n("FirstArraySlice"),
ArraySize: n("ArraySize"),
PlaneSlice: n("PlaneSlice"),
}
}
D3D12_UAV_DIMENSION_TEXTURE3D => {
d.Anonymous.Texture3D = D3D12_TEX3D_UAV {
MipSlice: n("MipSlice"),
FirstWSlice: n("FirstWSlice"),
WSize: n("WSize"),
}
}
other => return Err(format!("a UAV of dimension {} is not copied", other.0)),
}
Ok(Some(Description::Uav(d)))
}
fn covered(resource: u64, view: Result<Option<Description>, String>) -> Result<Vec<Written>, String> {
if resource == 0 {
return Ok(Vec::new());
}
let real = super::real::<ID3D12Resource>(resource).ok_or("destroyed before it was written")?;
let desc = unsafe { real.GetDesc() };
if desc.Dimension == D3D12_RESOURCE_DIMENSION_BUFFER {
return Ok(vec![Written { resource, sub: None }]);
}
if desc.SampleDesc.Count > 1 {
return Err("a multisampled UAV is not copied".into());
}
let mips = u32::from(desc.MipLevels.max(1));
let slices = if desc.Dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D {
1
} else {
u32::from(desc.DepthOrArraySize.max(1))
};
let span = |first: u32, size: u32| {
first
..first
.saturating_add(if size == u32::MAX { slices } else { size })
.min(slices)
};
let (mip, range, plane) = match view? {
None | Some(Description::DefaultView) => (0, 0..slices, 0),
Some(Description::Uav(d)) => unsafe {
match d.ViewDimension {
D3D12_UAV_DIMENSION_TEXTURE1D => (d.Anonymous.Texture1D.MipSlice, 0..1, 0),
D3D12_UAV_DIMENSION_TEXTURE1DARRAY => {
let a = d.Anonymous.Texture1DArray;
(a.MipSlice, span(a.FirstArraySlice, a.ArraySize), 0)
}
D3D12_UAV_DIMENSION_TEXTURE2D => (
d.Anonymous.Texture2D.MipSlice,
0..1,
d.Anonymous.Texture2D.PlaneSlice,
),
D3D12_UAV_DIMENSION_TEXTURE2DARRAY => {
let a = d.Anonymous.Texture2DArray;
(a.MipSlice, span(a.FirstArraySlice, a.ArraySize), a.PlaneSlice)
}
D3D12_UAV_DIMENSION_TEXTURE3D => (d.Anonymous.Texture3D.MipSlice, 0..1, 0),
other => return Err(format!("a UAV of dimension {} is not copied", other.0)),
}
},
Some(_) => return Err("the slot holds no UAV".into()),
};
if mip >= mips {
return Err(format!("its view names mip {mip} of {mips}"));
}
Ok(range
.map(|slice| Written {
resource,
sub: Some(mip + slice * mips + plane * mips * slices),
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_dispatch_of_rays_writes_through_its_libraries_registers() {
program(
"outputs-test-library",
include_bytes!("../../../capture/src/testdata/dxil-library.cso"),
);
let library = |key: &str| json!({"type": 5, "value": {"library": {"blob": key}, "exports": []}});
state_object(
9001,
&json!({"kind": "state_object", "parent": 0, "subobjects": [library("outputs-test-library")]}),
);
state_object(
9002,
&json!({"kind": "state_object", "parent": 0,
"subobjects": [{"type": 6, "value": {"collection": 9001, "exports": []}}]}),
);
state_object(
9003,
&json!({"kind": "state_object", "parent": 9002, "subobjects": [library("outputs-test-unread")]}),
);
let uav = |register: u32| json!({"type": 4, "visibility": 0, "value": {"ShaderRegister": register, "RegisterSpace": 0}});
root(9010, &json!({"parameters": [uav(0)]}));
root(9011, &json!({"parameters": [uav(1)]}));
let mut bound = Bound::default();
bound.apply("SetComputeRootSignature", &json!([9010]));
bound.apply(
"SetComputeRootUnorderedAccessView",
&json!([0, {"resource": 7, "offset": 0}]),
);
bound.apply("SetPipelineState1", &json!([9002]));
let (written, gaps) = bound.written("DispatchRays", &json!([]));
assert_eq!(
written,
[Written {
resource: 7,
sub: None
}]
);
assert!(gaps.is_empty(), "{gaps:?}");
bound.apply("SetComputeRootSignature", &json!([9011]));
bound.apply(
"SetComputeRootUnorderedAccessView",
&json!([0, {"resource": 8, "offset": 0}]),
);
let (written, gaps) = bound.written("DispatchRays", &json!([]));
assert!(written.is_empty() && gaps.is_empty(), "{gaps:?}");
bound.apply("SetComputeRootSignature", &json!([9010]));
bound.apply(
"SetComputeRootUnorderedAccessView",
&json!([0, {"resource": 7, "offset": 0}]),
);
bound.apply("SetPipelineState1", &json!([9003]));
let (written, gaps) = bound.written("DispatchRays", &json!([]));
assert_eq!(
written,
[Written {
resource: 7,
sub: None
}]
);
assert_eq!(gaps, ["its lib program says nothing of its UAVs"]);
}
}