use core::ffi::c_void;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::PathBuf;
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use std::time::Instant;
use super::cost::Counter;
use locum_capture::{d3d12 as model, db, environment, writer};
use serde_json::{Value, json};
use windows::core::HRESULT;
use crate::log::{folder, log};
pub static RECORDING_COST: Counter = Counter::new(0);
pub static SUBMISSION_COST: Counter = Counter::new(1);
pub static ADDRESS_COST: Counter = Counter::new(2);
pub static DESCRIPTOR_COST: Counter = Counter::new(3);
pub static DESTROY_COST: Counter = Counter::new(4);
pub static UPLOAD_COST: Counter = Counter::new(5);
pub static RESET_COST: Counter = Counter::new(6);
pub fn enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| std::env::var("LOCUM_D3D12_JOURNAL").as_deref() != Ok("off"))
}
pub struct Data {
pub id: u64,
pub recording: Mutex<Option<model::Recording>>,
pub queue_seq: Mutex<u64>,
pub pass: Mutex<Option<super::passes::Pass>>,
}
impl Data {
pub fn new(id: u64) -> Self {
Self {
id,
recording: Mutex::new(None),
queue_seq: Mutex::new(0),
pass: Mutex::new(None),
}
}
pub fn record(&self, real: *mut c_void, call: &'static str, build: impl FnOnce() -> Value) {
if !enabled() {
return;
}
let start = Instant::now();
let args = build();
let mut recording = self.recording.lock().unwrap_or_else(|e| e.into_inner());
if let Some(recording) = recording.as_mut() {
self.checkpoints(recording, |pass, at| unsafe {
pass.before(real, call, &args, at)
});
recording.record(model::Command {
call,
args,
bundle: None,
diagnostic: None,
});
}
RECORDING_COST.add(start, 0);
}
pub fn bundle(&self, real: *mut c_void, pointer: *mut c_void) {
if !enabled() {
return;
}
let bundle = super::data(pointer).and_then(|d| d.snapshot());
let mut recording = self.recording.lock().unwrap_or_else(|e| e.into_inner());
if let Some(recording) = recording.as_mut() {
self.checkpoints(recording, |pass, at| unsafe {
pass.before(real, "ExecuteBundle", &Value::Null, at)
});
let diagnostic = bundle
.is_none()
.then(|| "bundle has no recorded generation".into());
recording.record(model::Command {
call: "ExecuteBundle",
args: json!({"list":super::id(pointer)}),
bundle,
diagnostic,
});
}
}
pub fn snapshot(&self) -> Option<Arc<model::List>> {
self.recording
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
.map(|r| r.snapshot())
}
fn checkpoints(
&self,
recording: &mut model::Recording,
make: impl FnOnce(&mut super::passes::Pass, usize) -> Vec<model::Checkpoint>,
) {
let mut pass = self.pass.lock().unwrap_or_else(|e| e.into_inner());
let Some(pass) = pass.as_mut() else {
return;
};
let checkpoints = make(pass, recording.len());
self.keep(recording, checkpoints, pass.outputs(), pass.arguments());
}
fn keep(
&self,
recording: &mut model::Recording,
checkpoints: Vec<model::Checkpoint>,
outputs: Vec<model::Output>,
arguments: Vec<model::Arguments>,
) {
super::passes::belong(
self.id,
checkpoints
.iter()
.map(|c| c.id)
.chain(outputs.iter().map(|o| o.id))
.chain(arguments.iter().map(|a| a.id)),
);
for c in checkpoints {
recording.checkpoint(c);
}
for o in outputs {
recording.output(o);
}
for a in arguments {
recording.arguments(a);
}
}
pub fn closing(&self, real: *mut c_void) {
if !enabled() {
return;
}
let mut recording = self.recording.lock().unwrap_or_else(|e| e.into_inner());
let pass = self.pass.lock().unwrap_or_else(|e| e.into_inner()).take();
if let (Some(recording), Some(mut pass)) = (recording.as_mut(), pass) {
let checkpoints = unsafe { pass.closing(real, recording.len()) };
self.keep(recording, checkpoints, pass.outputs(), pass.arguments());
let (continues, written) = pass.touches();
recording.touches(continues, written);
}
}
pub fn close(&self, hr: HRESULT) {
if hr.is_ok()
&& let Some(r) = self.recording.lock().unwrap_or_else(|e| e.into_inner()).as_mut()
{
r.close();
}
}
pub fn reset(&self, real: *mut c_void, hr: HRESULT, allocator: *mut c_void, pso: *mut c_void) {
let start = Instant::now();
let (allocator, pso) = (super::id(allocator), super::id(pso));
super::passes::retire(self.id);
if hr.is_ok()
&& let Some(r) = self.recording.lock().unwrap_or_else(|e| e.into_inner()).as_mut()
{
r.reset(allocator, pso);
self.instrument(r, real, pso);
}
RESET_COST.add(start, 0);
}
fn instrument(&self, recording: &mut model::Recording, real: *mut c_void, pso: u64) {
let pass = if super::passes::ARMED.load(std::sync::atomic::Ordering::Acquire) {
unsafe { super::passes::Pass::new(real, pso) }
} else {
None
};
if pass.is_some() {
recording.instrument();
}
*self.pass.lock().unwrap_or_else(|e| e.into_inner()) = pass;
}
}
#[derive(Default)]
struct State {
objects: BTreeMap<u64, model::Object>,
uploads: BTreeMap<u64, Upload>,
cpu_seq: u64,
next_submission: u64,
capture: Option<Live>,
descriptors: super::slots::Known,
constant_buffers: BTreeMap<(u64, u32), (u64, u64, u64)>,
states: super::states::Tracker,
series: Option<Series>,
waits: HashMap<u64, Vec<(u64, u64)>>,
signals: HashMap<u64, u64>,
}
#[derive(Clone, Debug)]
struct Series {
label: Option<String>,
index: u32,
frames: u32,
}
const MAX_FRAMES: u32 = 64;
struct Live {
series: Option<Series>,
path: PathBuf,
frame: model::Frame,
projection: model::Projection,
capture: db::Capture,
results: super::readback::Results,
used: HashSet<u64>,
executed: HashSet<(u64, u64)>,
paged: HashSet<u64>,
}
#[derive(Default)]
struct Upload {
pointer: usize,
maps: u32,
bytes: usize,
}
fn state() -> MutexGuard<'static, State> {
static STATE: OnceLock<Mutex<State>> = OnceLock::new();
STATE
.get_or_init(|| Mutex::new(State::default()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}
pub fn register(id: u64, kind: &'static str) {
if !enabled() {
return;
}
state().objects.entry(id).or_insert_with(|| model::Object {
id,
kind,
device: 0,
description: json!({}),
});
}
pub fn describe(id: u64, device: u64, description: Value) {
let mut s = state();
if let Some(object) = s.objects.get_mut(&id) {
object.device = device;
object.description = description;
let object = object.clone();
if let Some(live) = s.capture.as_mut() {
live.projection.describe(id, &object.description);
live.frame.objects.retain(|o| o.id != id);
live.frame.objects.push(object);
}
}
}
pub fn track(id: u64, description: &Value, initial: super::states::Access) {
if !enabled() {
return;
}
let format = description["format"].as_u64().unwrap_or(0) as u32;
let planes = if locum_capture::format::is_depth_stencil(format) {
2
} else {
1
};
state().states.create(id, description, initial, planes);
}
pub fn owner_device(id: u64) -> u64 {
state().objects.get(&id).map_or(0, |o| o.device)
}
pub fn destroy(id: u64) {
let start = Instant::now();
super::passes::destroyed(id);
super::outputs::destroyed(id);
{
let mut s = state();
s.objects.remove(&id);
s.uploads.remove(&id);
s.states.destroy(id);
}
super::tiles::destroyed(id);
super::slots::destroy(id);
DESTROY_COST.add(start, 0);
}
pub fn resource_address(id: u64, address: u64, bytes: u64) {
super::slots::register_address(id, address, bytes);
}
pub fn descriptor_addresses(id: u64, cpu: u64, gpu: u64, count: u32, stride: u32) {
super::slots::register_heap(id, cpu, gpu, count, stride);
}
pub fn address(value: u64) -> Value {
if value == 0 {
return Value::Null;
}
let start = Instant::now();
let result = super::slots::address(value);
ADDRESS_COST.add(start, 0);
result
}
pub fn constant_buffer(address: u64, bytes: u32) -> super::descriptors::Description {
let start = Instant::now();
let result = if let Some((resource, offset)) = super::slots::unique_address(address) {
super::descriptors::Description::Cb {
resource,
offset,
bytes,
}
} else {
let buffer = if address == 0 {
Value::Null
} else {
super::slots::address(address)
};
super::descriptors::Description::Json(json!({"buffer":buffer,"bytes":bytes}))
};
ADDRESS_COST.add(start, 0);
result
}
pub fn descriptor(value: u64, gpu: bool) -> Value {
if value == 0 {
return Value::Null;
}
let start = Instant::now();
let result = super::slots::descriptor(value, gpu);
DESCRIPTOR_COST.add(start, 0);
result
}
pub fn set_descriptor(
handle: u64,
kind: &'static str,
resource: u64,
counter: u64,
description: super::descriptors::Description,
) {
if !enabled() {
return;
}
let start = Instant::now();
super::slots::set(handle, kind, resource, counter, description);
DESCRIPTOR_COST.add(start, 0);
}
pub fn copy_descriptors(destinations: &[u64], sources: &[u64]) {
if !enabled() {
return;
}
let start = Instant::now();
super::slots::copy(destinations, sources);
DESCRIPTOR_COST.add(start, 0);
}
pub fn copy_descriptors_simple(count: u32, destination: u64, source: u64) {
if !enabled() {
return;
}
let start = Instant::now();
super::slots::copy_simple(count, destination, source);
DESCRIPTOR_COST.add(start, 0);
}
pub fn descriptor_stride(handle: u64) -> Option<u32> {
super::slots::stride(handle)
}
pub fn render_targets(
pointer: *const windows::Win32::Graphics::Direct3D12::D3D12_CPU_DESCRIPTOR_HANDLE,
count: u32,
contiguous: bool,
) -> Value {
if count == 0 {
return json!([]);
}
if contiguous {
let Some(first) = (unsafe { pointer.as_ref() }) else {
return json!([]);
};
let Some(stride) = descriptor_stride(first.ptr as u64) else {
return json!({"diagnostic":"contiguous RTV heap is not registered","count":count});
};
json!(
(0..count)
.map(|i| descriptor(first.ptr as u64 + u64::from(i) * u64::from(stride), false))
.collect::<Vec<_>>()
)
} else {
json!(
unsafe { slice(pointer, count as usize) }
.iter()
.map(|h| descriptor(h.ptr as u64, false))
.collect::<Vec<_>>()
)
}
}
pub unsafe fn mapped(resource: u64, sub: u32, out: *mut *mut c_void) {
if !enabled() || sub != 0 {
return;
}
let mut s = state();
let Some(o) = s.objects.get(&resource) else {
return;
};
if o.description["dimension"] != 1 || o.description["heap_properties"]["type"] != 2 {
return;
}
let bytes = o.description["width"].as_u64().unwrap_or(0) as usize;
let u = s.uploads.entry(resource).or_insert_with(|| Upload {
bytes,
..Upload::default()
});
u.maps += 1;
if !out.is_null() {
u.pointer = unsafe { *out } as usize;
}
}
pub fn unmap(resource: u64, sub: u32) {
if !enabled() || sub != 0 {
return;
}
let mut s = state();
if let Some(u) = s.uploads.get_mut(&resource) {
u.maps = u.maps.saturating_sub(1);
if u.maps == 0 {
u.pointer = 0;
}
}
}
pub unsafe fn created_list(
device: u64,
out: *mut *mut c_void,
kind: i32,
allocator: *mut c_void,
pso: *mut c_void,
closed: bool,
) {
if !enabled() || out.is_null() {
return;
}
if let Some(data) = super::data(unsafe { *out }) {
let (allocator, pso) = (super::id(allocator), super::id(pso));
let mut recording = model::Recording::new(data.id, kind, allocator, pso, closed);
if !closed && let Some(real) = super::real(data.id) {
data.instrument(&mut recording, windows::core::Interface::as_raw(&real), pso);
}
*data.recording.lock().unwrap_or_else(|e| e.into_inner()) = Some(recording);
describe(
data.id,
device,
json!({"type":kind,"allocator":allocator,"initial_pso":pso}),
);
}
}
pub unsafe fn submit(
queue: &Data,
real: *mut c_void,
sequence: u64,
count: u32,
pointers: *const *mut c_void,
) -> super::passes::Submitted {
if !enabled() {
return Default::default();
}
let start = Instant::now();
let mut s = state();
s.cpu_seq += 1;
s.next_submission += 1;
let (cpu_seq, id) = (s.cpu_seq, s.next_submission);
let armed = s.capture.is_some();
let descriptors = if armed {
let changes = super::slots::changes(&mut s.descriptors);
for d in &changes {
let range = (
d.kind,
d.description["buffer"]["offset"].as_u64(),
d.description["bytes"].as_u64(),
);
match range {
("cb", Some(offset), Some(bytes)) => s.constant_buffers.insert(
(d.heap, d.slot),
(d.resource, offset, offset.saturating_add(bytes)),
),
_ => s.constant_buffers.remove(&(d.heap, d.slot)),
};
}
changes
} else {
Vec::new()
};
let mut lists = Vec::new();
let mut ordinals = Vec::new();
let mut diagnostics = Vec::new();
for (ordinal, pointer) in unsafe { slice(pointers, count as usize) }.iter().enumerate() {
if let Some(data) = super::data(*pointer) {
ordinals.push(ordinal);
lists.push(data.snapshot().unwrap_or_else(|| {
Arc::new(model::List {
id: data.id,
..model::List::default()
})
}));
} else if armed {
diagnostics.push(format!(
"submission {id} queue {}: list {ordinal} of {count} has no registered identity",
queue.id
));
}
}
let queue_type = s
.objects
.get(&queue.id)
.and_then(|o| o.description["type"].as_i64())
.unwrap_or(0) as i32;
let State {
capture,
objects,
uploads,
constant_buffers,
states,
waits,
signals,
..
} = &mut *s;
if let Some(live) = capture.as_mut() {
let start = Instant::now();
let first = live.frame.submissions.is_empty();
live.frame.diagnostics.extend(diagnostics);
live.frame.submissions.push(model::Submission {
id,
queue: queue.id,
queue_seq: sequence,
cpu_seq,
lists: lists.clone(),
descriptors,
uploads: Vec::new(),
});
let submission = live.frame.submissions.last().expect("pushed");
let touched = live.projection.submit(&mut live.capture, submission);
let unknown = unsafe { unknown_wait(waits.entry(queue.id).or_default(), signals) };
let snapshots = first_uses(live, objects, states, touched, unknown.as_deref(), cpu_seq);
let copies = upload_copies(uploads, first, &lists, constant_buffers.values());
if !copies.is_empty() || !snapshots.is_empty() {
unsafe { super::readback::submit(real, id, copies, snapshots, &live.results) };
}
UPLOAD_COST.add(start, 0);
}
states.execute(&lists, queue_type);
let mut submitted = super::passes::Submitted::new(id);
let mut executed = Vec::new();
for (at, list) in ordinals.into_iter().zip(&lists).filter(|(_, l)| l.instrumented) {
let copied = list.checkpoints.iter().filter(|c| c.gap.is_none()).map(|c| c.id);
let copied = copied.chain(list.outputs.iter().filter(|o| o.gap.is_none()).map(|o| o.id));
submitted.all.extend(copied.clone());
if capture.is_some() {
let arguments = list.arguments.iter().filter(|a| a.gap.is_none()).map(|a| a.id);
submitted.all.extend(arguments.clone());
submitted.arguments.extend(arguments);
}
let first = capture
.as_mut()
.is_some_and(|live| live.executed.insert((list.id, list.generation)));
if first {
submitted.read.extend(copied);
}
executed.push(super::passes::Executed { at, list, first });
}
let continued = unsafe { super::passes::ends(&mut submitted, &executed) };
if let Some(live) = capture.as_ref() {
if submitted.reads() {
submitted.results = Some(live.results.clone());
}
if !continued.is_empty() {
live.results
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(super::readback::Batch {
submission: id,
continued,
..Default::default()
});
}
}
SUBMISSION_COST.add(start, 0);
submitted
}
pub unsafe fn submitted(queue: &Data, real: *mut c_void, submission: super::passes::Submitted) {
unsafe { super::passes::submitted(queue.id, real, submission) };
}
unsafe fn unknown_wait(waits: &mut Vec<(u64, u64)>, signals: &HashMap<u64, u64>) -> Option<String> {
unsafe { prune(waits, signals) };
waits.first().map(|(fence, value)| {
format!(
"the queue waits for fence {fence} to reach {value}, which nothing submitted yet signals: the GPU runs later work first"
)
})
}
unsafe fn prune(waits: &mut Vec<(u64, u64)>, signals: &HashMap<u64, u64>) {
use windows::Win32::Graphics::Direct3D12::ID3D12Fence;
use windows::core::Interface;
waits.retain(|&(fence, value)| {
let signalled = signals.get(&fence).is_some_and(|&v| v >= value);
let completed = super::real(fence)
.and_then(|f| f.cast::<ID3D12Fence>().ok())
.is_none_or(|f| unsafe { f.GetCompletedValue() } >= value);
!(signalled || completed)
});
}
fn first_uses(
live: &mut Live,
objects: &BTreeMap<u64, model::Object>,
states: &super::states::Tracker,
touched: Vec<u64>,
unknown: Option<&str>,
cpu_seq: u64,
) -> Vec<super::readback::Snapshot> {
let submission = live.frame.submissions.last().map_or(0, |s| s.id);
let mut snapshots = Vec::new();
let mut failed = Vec::new();
for resource in touched {
if !live.used.insert(resource) {
continue;
}
let d = match objects.get(&resource).map(|o| &o.description) {
Some(d) if d["dimension"].is_u64() => d,
found => {
let why = if found.is_none() {
"destroyed before the submission that names it (a stale descriptor)"
} else {
"no description: locum did not see its creation"
};
failed.push((
resource,
model::FirstUse {
submission,
states: json!([]),
content: Err(why.to_string()),
},
));
continue;
}
};
let n = |key: &str| d[key].as_u64().unwrap_or(0) as u32;
if matches!(d["heap_properties"]["type"].as_u64(), Some(2 | 3)) {
continue;
}
let texture = n("dimension") > 1;
let simultaneous = n("flags") & 0x20 != 0;
let format = n("format");
let layout = model::layout(d);
let reserved = d["heap_properties"].is_null();
if reserved {
page(live, resource, cpu_seq);
}
let why = if let Some(why) = unknown {
Some(why.to_string())
} else if let Some(why) = reserved.then(|| super::tiles::unreadable(resource)).flatten() {
Some(why)
} else if n("sample_count") > 1 {
Some("multisampled: copying samples out is not supported yet".to_string())
} else if layout.is_none() {
Some(format!("format {format} has no canonical layout"))
} else if texture && !simultaneous && !states.tracked(resource) {
Some("its state is unknown: no creation locum saw".to_string())
} else {
None
};
if let Some(why) = why {
failed.push((
resource,
model::FirstUse {
submission,
states: json!([]),
content: Err(why),
},
));
continue;
}
let states = states.states(resource).map(<[_]>::to_vec).unwrap_or_default();
let planes = if locum_capture::format::is_depth_stencil(format) {
2
} else {
1
};
let layout = layout.expect("checked");
snapshots.push(super::readback::Snapshot {
resource,
subresources: if texture { layout.len() as u32 * planes } else { 1 },
joined: (planes == 2).then(|| locum_capture::format::layout(format).map_or(4, |f| f.block_bytes)),
states,
layout,
});
}
if !failed.is_empty() {
live.results
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(super::readback::Batch {
submission,
contents: failed,
..Default::default()
});
}
snapshots
}
const CONSTANT_BUFFER_BYTES: u64 = 65536;
fn upload_copies<'a>(
uploads: &BTreeMap<u64, Upload>,
first: bool,
lists: &[Arc<model::List>],
constant_buffers: impl Iterator<Item = &'a (u64, u64, u64)>,
) -> Vec<super::readback::Copy> {
let mut out = Vec::new();
if first {
for (&resource, u) in uploads {
out.push(super::readback::Copy {
resource,
offset: 0,
len: u.bytes as u64,
whole: true,
});
}
}
let mut ranges = Vec::new();
for list in lists {
read_ranges(list, uploads, &mut ranges);
}
ranges.extend(constant_buffers.filter(|(resource, _, _)| uploads.contains_key(resource)));
for (resource, begin, end) in runs(ranges, CONSTANT_BUFFER_BYTES) {
let end = end.min(uploads[&resource].bytes as u64);
if begin < end {
out.push(super::readback::Copy {
resource,
offset: begin,
len: end - begin,
whole: false,
});
}
}
out
}
fn read_ranges(list: &model::List, uploads: &BTreeMap<u64, Upload>, ranges: &mut Vec<(u64, u64, u64)>) {
for c in &list.commands {
if let Some(bundle) = &c.bundle {
read_ranges(bundle, uploads, ranges);
}
let upload = |v: &Value| v.as_u64().filter(|r| uploads.contains_key(r));
match c.call {
"CopyBufferRegion" => {
if let (Some(resource), Some(offset), Some(bytes)) =
(upload(&c.args[2]), c.args[3].as_u64(), c.args[4].as_u64())
{
ranges.push((resource, offset, offset.saturating_add(bytes)));
}
}
"CopyResource" => {
if let Some(resource) = upload(&c.args[1]) {
ranges.push((resource, 0, uploads[&resource].bytes as u64));
}
}
_ => {}
}
if matches!(
c.call,
"SetGraphicsRootConstantBufferView" | "SetComputeRootConstantBufferView"
) {
let at = &c.args[1];
if let (Some(resource), Some(offset)) = (at["resource"].as_u64(), at["offset"].as_u64())
&& uploads.contains_key(&resource)
{
ranges.push((resource, offset, offset.saturating_add(CONSTANT_BUFFER_BYTES)));
}
}
}
}
fn runs(mut ranges: Vec<(u64, u64, u64)>, gap: u64) -> Vec<(u64, u64, u64)> {
ranges.sort_unstable();
let mut merged: Vec<(u64, u64, u64)> = Vec::new();
for (resource, begin, end) in ranges {
match merged.last_mut() {
Some(last) if last.0 == resource && begin <= last.2.saturating_add(gap) => {
last.2 = last.2.max(end)
}
_ => merged.push((resource, begin, end)),
}
}
merged
}
fn page(live: &mut Live, resource: u64, cpu_seq: u64) {
if resource != 0 && live.paged.insert(resource) {
live.frame.tile_pages.push(model::TilePages {
resource,
cpu_seq,
pages: super::tiles::pages(resource),
});
}
}
pub fn tiles_changing(resources: &[u64]) {
let mut s = state();
let cpu_seq = s.cpu_seq;
if let Some(live) = s.capture.as_mut() {
for &resource in resources {
page(live, resource, cpu_seq);
}
}
}
pub fn tile_mapping(
queue: u64,
queue_seq: u64,
call: &'static str,
resource: u64,
source: Option<u64>,
args: Value,
after: Result<Value, String>,
) {
let mut s = state();
s.cpu_seq += 1;
let cpu_seq = s.cpu_seq;
if let Some(live) = s.capture.as_mut() {
live.frame.tile_mappings.push(model::TileMapping {
queue,
queue_seq,
cpu_seq,
call,
resource,
source,
args,
after,
});
}
}
pub fn sync(queue: &Data, sequence: u64, kind: &'static str, fence: *mut c_void, value: u64, hr: HRESULT) {
if !enabled() || hr.is_err() {
return;
}
let fence = super::id(fence);
let mut s = state();
s.cpu_seq += 1;
let cpu_seq = s.cpu_seq;
match kind {
"Signal" => {
let v = s.signals.entry(fence).or_default();
*v = (*v).max(value);
for waits in s.waits.values_mut() {
waits.retain(|&(f, v)| f != fence || v > value);
}
}
"Wait" => {
let State { waits, signals, .. } = &mut *s;
let waits = waits.entry(queue.id).or_default();
waits.push((fence, value));
if waits.len() > 64 {
unsafe { prune(waits, signals) };
}
}
_ => {}
}
if let Some(live) = s.capture.as_mut() {
live.frame.sync.push(model::Sync {
queue: queue.id,
queue_seq: sequence,
cpu_seq,
kind,
fence,
value,
});
}
}
pub unsafe fn slice<'a, T>(pointer: *const T, len: usize) -> &'a [T] {
if pointer.is_null() || len == 0 {
&[]
} else {
unsafe { std::slice::from_raw_parts(pointer, len) }
}
}
fn adapter(presented: Option<u64>) -> Result<environment::Adapter, String> {
use windows::Win32::Graphics::Direct3D12::ID3D12Device;
use windows::Win32::Graphics::Dxgi::{IDXGIAdapter, IDXGIFactory4};
use windows::core::{GUID, Interface};
let device = presented
.map(owner_device)
.filter(|&d| d != 0)
.or_else(|| {
let s = state();
let mut devices: Vec<u64> = s
.objects
.values()
.filter(|o| o.kind.starts_with("ID3D12Device"))
.map(|o| o.id)
.collect();
devices.sort_unstable();
devices.first().copied()
})
.ok_or("the process has no D3D12 device")?;
let device: ID3D12Device = super::real(device)
.ok_or_else(|| format!("device #{device} has no real device"))?
.cast()
.map_err(|e| format!("device #{device} as an ID3D12Device: {e}"))?;
let luid = unsafe { device.GetAdapterLuid() };
type Factory = unsafe extern "system" fn(u32, *const GUID, *mut *mut c_void) -> HRESULT;
let create =
unsafe { crate::system::export::<Factory>(crate::system::system().dxgi, c"CreateDXGIFactory2") }
.ok_or("the system's dxgi.dll has no CreateDXGIFactory2")?;
let mut raw = std::ptr::null_mut();
unsafe { create(0, &IDXGIFactory4::IID, &mut raw) }
.ok()
.map_err(|e| format!("CreateDXGIFactory2: {e}"))?;
let factory = unsafe { IDXGIFactory4::from_raw(raw) };
let adapter: IDXGIAdapter = unsafe { factory.EnumAdapterByLuid(luid) }.map_err(|e| {
format!(
"EnumAdapterByLuid({:08x}:{:08x}): {e}",
luid.HighPart, luid.LowPart
)
})?;
environment::adapter(&adapter, environment::Api::D3d12)
}
pub fn present(frame_number: u64, presented: Option<u64>) {
static REPORT: OnceLock<Mutex<Instant>> = OnceLock::new();
let mut last = REPORT
.get_or_init(|| Mutex::new(Instant::now()))
.lock()
.unwrap_or_else(|e| e.into_inner());
if last.elapsed().as_secs() >= 10 {
*last = Instant::now();
super::log_counts();
let [
recording,
submission,
address,
descriptor,
destruction,
upload,
reset,
] = super::cost::take();
log!(
"D3D12 journal CPU: recording {}, submission {}",
recording,
submission
);
log!(
"D3D12 journal detail: VA {}, descriptor {}, destruction {}, upload {}, reset {}; frame {frame_number}",
address,
descriptor,
destruction,
upload,
reset
);
let s = state();
let (ranges, heaps, slots) = super::slots::counts();
log!(
"D3D12 journal live: objects {}, buffer ranges {}, descriptor heaps {}, descriptor slots {}, upload buffers {}",
s.objects.len(),
ranges,
heaps,
slots,
s.uploads.len()
);
log!(
"D3D12 states: {} textures tracked, {} transitions, {} not from the tracked state",
s.states.count(),
s.states.transitions,
s.states.mismatches
);
}
drop(last);
if !enabled() {
return;
}
let Some(root) = folder() else {
return;
};
super::passes::sweep();
let (finished, series) = {
let mut s = state();
(s.capture.take(), s.series.take())
};
if finished.is_some() {
super::passes::ARMED.store(false, std::sync::atomic::Ordering::Release);
log!(
"D3D12 capture closing: {} copies recorded so far",
super::passes::recorded()
);
}
if let Some(Live {
path,
frame,
projection,
mut capture,
results,
series: of,
used,
..
}) = finished
{
super::log_counts();
capture.meta("api", "d3d12");
if let Some(of) = &of {
capture.meta(
"series",
serde_json::json!({"label": of.label, "index": of.index, "frames": of.frames}).to_string(),
);
}
capture.meta("name", path.file_name().unwrap_or_default().to_string_lossy());
capture.meta("locum", env!("CARGO_PKG_VERSION"));
capture.meta("frame", frame_number);
capture.meta("pid", std::process::id());
if let Some(id) = presented {
capture.meta("presented", id);
}
capture.meta(
"exe",
std::env::current_exe()
.map(|p| p.display().to_string())
.unwrap_or_default(),
);
capture.meta("blobs", crate::settings::settings().blobs.root().display());
let adapter = adapter(presented);
capture.meta("capture_status", "incomplete");
capture.meta(
"verification_scope",
"command history, first-use contents, pass-end targets and what draws, dispatches and clears wrote through UAVs",
);
let mut prepared = frame.finish(&mut capture, projection);
let after_frame = super::readback::Results::default();
unsafe { super::indirect::after_frame(&mut prepared, &mut capture, &used, &after_frame) };
let t = std::time::Instant::now();
super::readback::then(move || {
writer::run(move || {
capture.environment(&environment::Environment::probe(
adapter,
environment::Api::D3d12,
true,
));
for batch in std::mem::take(&mut *results.lock().unwrap_or_else(|e| e.into_inner())) {
prepared.add_uploads(batch.submission, batch.uploads);
for (resource, first) in batch.contents {
prepared.set_first_use(resource, first);
}
for (id, content) in batch.checkpoints {
prepared.set_checkpoint(id, content);
}
for id in batch.continued {
prepared.set_continued(id);
}
}
for batch in std::mem::take(&mut *after_frame.lock().unwrap_or_else(|e| e.into_inner())) {
for (resource, copy) in batch.contents {
prepared.set_after_frame(resource, copy);
}
}
prepared.contents(&mut capture, &crate::settings::settings().blobs);
capture.d3d12 = Some(prepared);
let file = path.join("capture.db");
let result = std::fs::create_dir_all(&path)
.map_err(|e| e.to_string())
.and_then(|()| db::write(&file, &capture).map_err(|e| e.to_string()));
match result {
Ok(()) => log!(
"capture written: {} ({} events, {} draws, {} gaps; {:.1} s after the frame)",
file.display(),
capture.events.len(),
capture.draws.len(),
capture.gaps.len(),
t.elapsed().as_secs_f64()
),
Err(e) => log!("capture NOT written to {}: {e}", file.display()),
}
})
});
}
if let Some(series) = series.filter(|s| s.index < s.frames) {
arm(
&root,
frame_number,
Series {
index: series.index + 1,
..series
},
);
return;
}
let request = root.join("capture");
if let Ok(text) = std::fs::read_to_string(&request) {
if let Err(e) = std::fs::remove_file(&request) {
log!("D3D12 capture request: {e}");
return;
}
let label = text.lines().find_map(|l| l.strip_prefix("label ")).unwrap_or("");
let label: String = label
.chars()
.filter(|c| c.is_ascii_alphanumeric() || "-_".contains(*c))
.collect();
let frames = match text.lines().find_map(|l| l.strip_prefix("frames ")) {
None => 1,
Some(n) => match n.trim().parse::<u32>() {
Ok(n @ 1..=MAX_FRAMES) => n,
_ => {
log!("D3D12 capture request: frames {n:?} is not 1 to {MAX_FRAMES}; one frame");
1
}
},
};
arm(
&root,
frame_number,
Series {
label: (!label.is_empty()).then_some(label),
index: 1,
frames,
},
);
}
}
fn arm(root: &std::path::Path, frame_number: u64, series: Series) {
let name = match &series.label {
Some(label) if series.frames > 1 => format!("{label}-{}", series.index),
Some(label) => label.clone(),
None => format!("frame-{}", frame_number + 1),
};
let path = root.join("captures").join(name);
if path.exists() {
log!("D3D12 capture destination already exists: {}", path.display());
return;
}
let mut s = state();
s.descriptors.clear();
s.constant_buffers.clear();
let objects: Vec<_> = s.objects.values().cloned().collect();
s.capture = Some(Live {
series: (series.frames > 1).then(|| series.clone()),
path,
projection: model::Projection::new(&objects),
frame: model::Frame {
objects,
..model::Frame::default()
},
capture: db::Capture::default(),
results: Default::default(),
used: HashSet::new(),
executed: HashSet::new(),
paged: HashSet::new(),
});
s.series = Some(series.clone());
super::passes::ARMED.store(true, std::sync::atomic::Ordering::Release);
log!(
"D3D12 capture armed at Present {frame_number} (frame {} of {}); cached list history retained; {} copies recorded so far",
series.index,
series.frames,
super::passes::recorded()
);
}
#[cfg(test)]
mod tests {
use super::runs;
#[test]
fn ranges_merge_across_gaps_smaller_than_the_threshold_and_never_across_resources() {
let ranges = vec![
(7, 1000, 1256),
(7, 0, 256),
(7, 100, 300),
(7, 5000, 5100),
(8, 300, 400),
];
assert_eq!(
runs(ranges.clone(), 0),
vec![(7, 0, 300), (7, 1000, 1256), (7, 5000, 5100), (8, 300, 400)]
);
assert_eq!(
runs(ranges.clone(), 700),
vec![(7, 0, 1256), (7, 5000, 5100), (8, 300, 400)]
);
assert_eq!(runs(ranges, 1 << 20), vec![(7, 0, 5100), (8, 300, 400)]);
}
}