use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::sync::Arc;
use std::time::Duration;
use locum_capture::d3d12 as model;
use locum_capture::d3d12::indirect::Kind;
use locum_capture::db;
use serde_json::{Value, json};
use windows::Win32::Graphics::Direct3D12::{ID3D12CommandQueue, ID3D12Resource};
use windows::core::Interface;
use super::readback::{Results, Snapshot};
enum Frame<'a> {
Closed(&'a model::Prepared),
Submitting(&'a model::Projection),
}
impl Frame<'_> {
fn description(&self, id: u64) -> Option<&Value> {
match self {
Frame::Closed(p) => p.description(id),
Frame::Submitting(p) => p.description(id),
}
}
}
#[derive(Default)]
struct Programs {
declared: RefCell<HashMap<String, Result<Arc<model::programs::Declared>, String>>>,
decoded: RefCell<HashMap<Vec<u8>, Result<Value, String>>>,
}
impl Programs {
fn declared(&self, key: &str) -> Result<Arc<model::programs::Declared>, String> {
if let Some(found) = self.declared.borrow().get(key) {
return found.clone();
}
locum_capture::writer::flush();
let found = crate::settings::settings()
.blobs
.get(key)
.map_err(|e| format!("a DXIL library's blob {key}: {e}"))
.and_then(|bytes| model::programs::Declared::read(&bytes))
.map(Arc::new);
self.declared.borrow_mut().insert(key.to_string(), found.clone());
found
}
fn named(
&self,
frame: Frame,
rays: &model::Indirect,
tables: &[model::tables::Table],
bytes: &[u8],
gaps: &mut BTreeMap<String, u64>,
) -> (Vec<u64>, Vec<u64>) {
let mut decode = |local: &model::programs::Local| match local {
model::programs::Local::Object(id) => {
let d = frame
.description(*id)
.ok_or_else(|| format!("local root signature #{id} is not described"))?;
match &d["decoded"] {
v if v["parameters"].is_array() => Ok(v.clone()),
v => Err(format!(
"local root signature #{id} was not decoded: {}",
v["diagnostic"]
)),
}
}
model::programs::Local::Declared(serialized) => self
.decoded
.borrow_mut()
.entry(serialized.clone())
.or_insert_with(|| model::programs::root_layout(serialized))
.clone(),
};
let buffers = |address: u64| {
super::slots::buffers_at(address)
.into_iter()
.map(|(resource, _)| resource)
.collect()
};
let declared = |key: &str| self.declared(key);
let named = match frame {
Frame::Closed(p) => p.named_by_tables(rays, tables, bytes, &declared, &mut decode, &buffers),
Frame::Submitting(p) => p.named_by_tables(rays, tables, bytes, &declared, &mut decode, &buffers),
};
for (why, count) in named.unplaced {
*gaps.entry(why).or_default() += count;
}
(named.reads, named.writes)
}
}
pub unsafe fn named_at_submission(
projection: &model::Projection,
events: &[db::Event],
from: usize,
queue: &ID3D12CommandQueue,
) -> Vec<u64> {
let programs = Programs::default();
let written = |r: u64| events.iter().any(|e| e.writes.contains(&r));
let mut named = BTreeSet::new();
for rays in projection.indirect().get(from..).unwrap_or_default() {
if rays.arguments.pointers > 0 {
named.extend(unsafe { pointed_at_submission(rays, &written, queue) });
continue;
}
let tables = &rays.arguments.tables;
if tables.is_empty() {
continue;
}
let mut ranges = Vec::new();
let readable = tables
.iter()
.all(|t| match super::slots::buffers_at(t.address)[..] {
[(resource, offset)] if !written(resource) => super::real::<ID3D12Resource>(resource)
.map(|real| ranges.push((real, offset, t.bytes)))
.is_some(),
_ => false,
});
if !readable {
continue;
}
let Ok(bytes) = (unsafe { super::passes::read_now(queue, ranges, PATIENCE) }) else {
continue;
};
let (reads, writes) = programs.named(
Frame::Submitting(projection),
rays,
tables,
&bytes,
&mut BTreeMap::new(),
);
named.extend(reads.into_iter().chain(writes));
}
named.into_iter().collect()
}
unsafe fn pointed_at_submission(
build: &model::Indirect,
written: &dyn Fn(u64) -> bool,
queue: &ID3D12CommandQueue,
) -> Vec<u64> {
let ranges = match super::slots::buffers_at(build.arguments.pointers_at)[..] {
[(resource, offset)] if !written(resource) => super::real::<ID3D12Resource>(resource)
.map(|real| vec![(real, offset, build.arguments.bytes)])
.unwrap_or_default(),
_ => return Vec::new(),
};
let Ok(bytes) = (unsafe { super::passes::read_now(queue, ranges, PATIENCE) }) else {
return Vec::new();
};
pointed(&bytes).0
}
fn pointed(bytes: &[u8]) -> (Vec<u64>, bool) {
let mut found = BTreeSet::new();
let mut unplaced = false;
for p in model::pointers(bytes).filter(|&p| p != 0) {
let at = super::slots::buffers_at(p);
unplaced |= at.is_empty();
found.extend(at.into_iter().map(|(resource, _)| resource));
}
(found.into_iter().collect(), unplaced)
}
const PATIENCE: Duration = Duration::from_secs(10);
pub unsafe fn after_frame(
prepared: &mut model::Prepared,
capture: &mut db::Capture,
used: &HashSet<u64>,
results: &Results,
) {
let copies = super::passes::argument_records(PATIENCE);
let indirect = prepared.indirect().to_vec();
let mut gaps = BTreeMap::<String, u64>::new();
let mut table_gaps = BTreeMap::<String, u64>::new();
let mut pointer_gaps = BTreeMap::<String, u64>::new();
let programs = Programs::default();
let mut indirect_rays = Vec::new();
for i in &indirect {
let bytes = match copies.get(&i.arguments.id) {
Some(Ok(bytes)) => bytes,
Some(Err(why)) => {
let into = if i.arguments.pointers > 0 {
&mut pointer_gaps
} else if i.arguments.tables.is_empty() {
&mut gaps
} else {
&mut table_gaps
};
*into.entry(why.clone()).or_default() += 1;
continue;
}
None => {
*gaps.entry("its copy was not kept".into()).or_default() += 1;
continue;
}
};
if i.arguments.pointers > 0 {
let (reads, unplaced) = pointed(bytes);
if unplaced {
*pointer_gaps
.entry("an instance pointer is in no live buffer: replay leaves the instance out".into())
.or_default() += 1;
}
prepared.named_by_arguments(capture, i, &reads, &[]);
continue;
}
if !i.arguments.tables.is_empty() {
let (reads, writes) = programs.named(
Frame::Closed(prepared),
i,
&i.arguments.tables,
bytes,
&mut table_gaps,
);
prepared.named_by_arguments(capture, i, &reads, &writes);
continue;
}
let records = i.arguments.records(bytes);
let (mut reads, mut writes) = (BTreeSet::new(), BTreeSet::new());
let mut unplaced = false;
model::indirect::addresses(bytes, &i.arguments.layout, records, |address, kind| {
let found = super::slots::buffers_at(address);
unplaced |= found.is_empty();
let into = if kind == Kind::UnorderedAccess {
&mut writes
} else {
&mut reads
};
into.extend(found.into_iter().map(|(resource, _)| resource));
});
if unplaced {
*gaps
.entry("an address in its records is in no live buffer".into())
.or_default() += 1;
}
let reads: Vec<u64> = reads.difference(&writes).copied().collect();
let writes: Vec<u64> = writes.into_iter().collect();
prepared.named_by_arguments(capture, i, &reads, &writes);
let tables: Vec<_> = model::indirect::shader_tables(bytes, &i.arguments.layout, records)
.into_iter()
.flatten()
.collect();
if !tables.is_empty() {
indirect_rays.push((i.clone(), tables));
}
}
for (i, tables) in indirect_rays {
let written = |r: u64| capture.events.iter().any(|e| e.writes.contains(&r));
let mut ranges = Vec::new();
let read = tables.iter().try_for_each(|t| {
let (resource, offset) = match super::slots::buffers_at(t.address)[..] {
[one] => one,
[] => return Err("a shader table's address is in no live buffer".to_string()),
_ => return Err("a shader table's address is in more than one live buffer".to_string()),
};
if written(resource) {
return Err(
"the frame writes a shader table its records name, so what it held then is not known"
.into(),
);
}
let real =
super::real::<ID3D12Resource>(resource).ok_or("a shader table's buffer was destroyed")?;
ranges.push((real, offset, t.bytes));
Ok(())
});
let bytes = read.and_then(|()| {
let queue = super::real::<ID3D12CommandQueue>(i.queue).ok_or("its queue was destroyed")?;
unsafe { super::passes::read_now(&queue, ranges, PATIENCE) }
});
match bytes {
Ok(bytes) => {
let (reads, writes) =
programs.named(Frame::Closed(prepared), &i, &tables, &bytes, &mut table_gaps);
prepared.named_by_arguments(capture, &i, &reads, &writes);
}
Err(why) => *table_gaps.entry(why).or_default() += 1,
}
}
for (why, count) in gaps {
capture.gap("d3d12-indirect-arguments", why, count);
}
for (why, count) in table_gaps {
capture.gap("d3d12-shader-tables", why, count);
}
for (why, count) in pointer_gaps {
capture.gap("d3d12-acceleration-structures", why, count);
}
let mut by_queue = BTreeMap::<(u64, u64), Vec<Snapshot>>::new();
for copy in prepared.after_frame(capture, |r| used.contains(&r)) {
by_queue
.entry((copy.submission, copy.queue))
.or_default()
.push(Snapshot {
resource: copy.resource,
states: Vec::new(),
subresources: 1,
layout: copy.layout,
joined: None,
});
}
for ((submission, queue), snapshots) in by_queue {
let Some(real) = super::real::<ID3D12CommandQueue>(queue) else {
let failed = snapshots
.iter()
.map(|s| {
(
s.resource,
model::FirstUse {
submission,
states: json!([]),
content: Err("the queue to copy it on after the frame was destroyed".into()),
},
)
})
.collect();
results
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(super::readback::Batch {
submission,
contents: failed,
..Default::default()
});
continue;
};
unsafe { super::readback::submit(queue, real.as_raw(), submission, Vec::new(), snapshots, results) };
}
}