pub mod contents;
mod counters;
pub mod coverage;
pub mod ids;
pub mod ops;
mod snapshot;
mod targets;
pub mod tiled;
mod write;
use core::ffi::c_void;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use locum_capture::cost::Cost;
use locum_capture::db;
use serde_json::Value;
use windows::Win32::Graphics::Direct3D11::*;
use crate::d3d11::registry::Dims;
use crate::log::{folder, log};
use ids::buffer_id;
pub use ids::input_layout_created;
static ARMED: AtomicBool = AtomicBool::new(false);
static EPOCH: AtomicU64 = AtomicU64::new(0);
fn epoch() -> u64 {
if armed() { EPOCH.load(Ordering::Relaxed) } else { 0 }
}
pub fn armed() -> bool {
ARMED.load(Ordering::Relaxed)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Phase {
Idle,
Before,
Recording,
}
#[derive(Clone, Debug)]
struct Request {
label: Option<String>,
targets: bool,
contents: bool,
memory: bool,
max_target_bytes: u64,
frames: u32,
}
const MAX_FRAMES: u32 = 64;
impl Request {
fn parse(text: &str) -> Request {
let mut r = Request {
label: None,
targets: true,
contents: true,
memory: true,
max_target_bytes: 4096 << 20,
frames: 1,
};
for line in text.lines() {
let mut words = line.split_whitespace();
match (words.next(), words.next()) {
(Some("label"), Some(name)) => {
let clean: String = name
.chars()
.filter(|c| c.is_ascii_alphanumeric() || "-_.".contains(*c))
.collect();
r.label = (!clean.is_empty()).then_some(clean);
}
(Some("skip"), Some("targets")) => r.targets = false,
(Some("skip"), Some("contents")) => r.contents = false,
(Some("skip"), Some("memory")) => r.memory = false,
(Some("max_mb"), Some(n)) => {
if let Ok(n) = n.parse::<u64>() {
r.max_target_bytes = n << 20;
}
}
(Some("frames"), Some(n)) => match n.parse::<u32>() {
Ok(n @ 1..=MAX_FRAMES) => r.frames = n,
_ => log!("capture request: frames {n:?} is not 1 to {MAX_FRAMES}; one frame"),
},
(Some("targets"), None) => {}
(Some(other), _) => log!("capture request: unknown option {other:?} ignored"),
(None, _) => {}
}
}
r
}
}
#[derive(Clone, Copy)]
pub struct Ctx<'a> {
pub c: &'a ID3D11DeviceContext4,
pub id: u32,
pub deferred: bool,
}
#[derive(Clone)]
pub(super) struct CbCopy {
binding: usize,
source: Option<ID3D11Buffer>,
first: u32,
count: u32,
empty: bool,
copy: Option<(u32, u32, u32)>,
}
#[derive(Clone, Default)]
pub(super) struct Draw {
shaders: [Option<String>; 6],
bindings: Vec<db::Binding>,
cb_copies: Vec<CbCopy>,
viewports: Vec<db::Viewport>,
scissors: Vec<db::Scissor>,
topology: i32,
layout: Option<u64>,
rasterizer: Option<u64>,
blend: Option<u64>,
depth_stencil: Option<u64>,
blend_factor: Option<[f32; 4]>,
sample_mask: Option<u32>,
stencil_ref: Option<u32>,
predicate: Option<u64>,
predicate_value: Option<bool>,
after: Vec<targets::Target>,
after_events: u32,
bound: Vec<targets::Bound>,
closed: bool,
order_dependent: bool,
overrides: Vec<String>,
}
#[derive(Clone)]
pub(super) struct Event {
kind: &'static str,
call: &'static str,
context: u32,
deferred: bool,
args: Value,
data: Option<String>,
data_readback: Option<contents::Readback>,
stack: Vec<u64>,
draw: Option<Box<Draw>>,
reads: Vec<u64>,
writes: Vec<u64>,
counters: Vec<u64>,
list: Option<u64>,
results: Vec<(u64, Result<contents::Readback, String>)>,
results_taken: bool,
}
struct List {
events: Vec<Event>,
complete: bool,
before: bool,
executions: u32,
}
pub(super) struct Tracked {
resource: ID3D11Resource,
dims: Dims,
touched: bool,
}
struct State {
phase: Phase,
frame: u64,
request: Request,
pending: BTreeMap<u32, Vec<Event>>,
lists: BTreeMap<usize, List>,
timeline: Vec<Event>,
lists_spliced: u32,
lists_before: u32,
lists_unrecorded: u32,
staging: Vec<ID3D11Buffer>,
used: u32,
copy_failures: u32,
target_bytes: u64,
targets_skipped: u32,
targets_skipped_bytes: u64,
captures: u64,
views: Vec<db::View>,
states: Vec<db::State>,
resources: BTreeMap<u64, Tracked>,
in_flight: Vec<(u64, Result<contents::Readback, String>)>,
in_flight_bytes: u64,
contents: BTreeMap<u64, contents::Read>,
counter_views: BTreeMap<u64, ID3D11UnorderedAccessView>,
counter_seen: BTreeMap<u64, ID3D11UnorderedAccessView>,
counter_copies: BTreeMap<u64, Result<ID3D11Buffer, String>>,
gaps: BTreeMap<(String, String), u64>,
tile_initialized: BTreeSet<u64>,
tile_seeded: BTreeSet<(u64, u32)>,
tile_packed_seeded: BTreeSet<(u64, u64, u32, Vec<u32>)>,
tile_seen: BTreeMap<u64, (u64, u64)>,
tile_seeds: Vec<usize>,
presented: Option<u64>,
index: u32,
}
pub(super) struct Carried {
pending: BTreeMap<u32, Vec<Event>>,
lists: BTreeMap<usize, List>,
staging: Vec<ID3D11Buffer>,
used: u32,
views: Vec<db::View>,
states: Vec<db::State>,
counters: BTreeMap<u64, ID3D11UnorderedAccessView>,
resources: BTreeMap<u64, Tracked>,
}
static STATE: Mutex<State> = Mutex::new(State {
phase: Phase::Idle,
frame: 0,
request: Request {
label: None,
targets: false,
contents: false,
memory: false,
max_target_bytes: 0,
frames: 1,
},
pending: BTreeMap::new(),
lists: BTreeMap::new(),
timeline: Vec::new(),
lists_spliced: 0,
lists_before: 0,
lists_unrecorded: 0,
staging: Vec::new(),
used: 0,
copy_failures: 0,
target_bytes: 0,
targets_skipped: 0,
targets_skipped_bytes: 0,
captures: 0,
views: Vec::new(),
states: Vec::new(),
resources: BTreeMap::new(),
in_flight: Vec::new(),
in_flight_bytes: 0,
contents: BTreeMap::new(),
counter_views: BTreeMap::new(),
counter_seen: BTreeMap::new(),
counter_copies: BTreeMap::new(),
gaps: BTreeMap::new(),
tile_initialized: BTreeSet::new(),
tile_seeded: BTreeSet::new(),
tile_packed_seeded: BTreeSet::new(),
tile_seen: BTreeMap::new(),
tile_seeds: Vec::new(),
presented: None,
index: 0,
});
fn state() -> std::sync::MutexGuard<'static, State> {
STATE.lock().unwrap_or_else(|e| e.into_inner())
}
impl State {
fn records(&self, deferred: bool) -> bool {
match self.phase {
Phase::Idle => false,
Phase::Before => deferred,
Phase::Recording => true,
}
}
fn reset(&mut self) {
self.pending.clear();
self.lists.clear();
self.timeline.clear();
self.lists_spliced = 0;
self.lists_before = 0;
self.lists_unrecorded = 0;
self.staging.clear();
self.used = 0;
self.copy_failures = 0;
self.target_bytes = 0;
self.targets_skipped = 0;
self.targets_skipped_bytes = 0;
self.views.clear();
self.states.clear();
self.resources.clear();
self.in_flight.clear();
self.in_flight_bytes = 0;
self.contents.clear();
self.counter_views.clear();
self.counter_seen.clear();
self.counter_copies.clear();
self.gaps.clear();
self.tile_initialized.clear();
self.tile_seeded.clear();
self.tile_packed_seeded.clear();
self.tile_seen.clear();
self.tile_seeds.clear();
self.presented = None;
}
fn continues(&self) -> bool {
self.index < self.request.frames
}
fn carry(&mut self) -> Carried {
let pending = std::mem::take(&mut self.pending);
let mut lists = std::mem::take(&mut self.lists);
for l in lists.values_mut() {
l.before = true;
l.executions = 0;
}
let referred: BTreeSet<u64> = pending
.values()
.flatten()
.chain(lists.values().flat_map(|l| &l.events))
.flat_map(|e| e.reads.iter().chain(&e.writes).copied())
.collect();
let resources = referred
.iter()
.filter_map(|id| {
let t = self.resources.get(id)?;
Some((
*id,
Tracked {
resource: t.resource.clone(),
dims: t.dims.clone(),
touched: false,
},
))
})
.collect();
Carried {
pending,
lists,
staging: self.staging.clone(),
used: self.used,
views: self.views.clone(),
states: self.states.clone(),
counters: self.counter_seen.clone(),
resources,
}
}
fn take_on(&mut self, c: Carried) {
self.pending = c.pending;
self.lists = c.lists;
self.staging = c.staging;
self.used = c.used;
self.views = c.views;
self.states = c.states;
self.counter_views = c.counters.clone();
self.counter_seen = c.counters;
self.resources = c.resources;
}
}
pub fn next_context_id() -> u32 {
static NEXT: AtomicU32 = AtomicU32::new(0);
NEXT.fetch_add(1, Ordering::Relaxed)
}
pub fn abandon(why: &str) {
if !armed() {
return;
}
let mut s = state();
if s.phase == Phase::Idle {
return;
}
ARMED.store(false, Ordering::Relaxed);
s.phase = Phase::Idle;
s.reset();
drop(s);
ops::reset();
log!("capture abandoned: {why}");
}
pub fn before_present(presented: impl FnOnce() -> Option<u64>) -> bool {
let presented = if armed() { presented() } else { None };
let mut s = state();
s.frame += 1;
if s.phase == Phase::Recording {
s.presented = presented;
if s.continues() {
s.phase = Phase::Before;
} else {
ARMED.store(false, Ordering::Relaxed);
s.phase = Phase::Idle;
}
return true;
}
false
}
pub fn after_present(finished: bool, immediate: &ID3D11DeviceContext4) {
if finished {
write::finish(immediate);
let mut s = state();
if s.phase == Phase::Before {
s.index += 1;
s.captures += 1;
log!("capture: frame {} of {}", s.index, s.request.frames);
drop(s);
start();
}
return;
}
let phase = state().phase;
match phase {
Phase::Idle => {
let mut s = state();
if !s.frame.is_multiple_of(30) {
return;
}
let Some(path) = folder().map(|d| d.join("capture")) else {
return;
};
let Ok(text) = std::fs::read_to_string(&path) else {
return;
};
if std::fs::remove_file(&path).is_err() {
return;
}
s.reset();
s.request = Request::parse(&text);
s.index = 1;
s.phase = Phase::Before;
s.captures += 1;
EPOCH.store(s.captures, Ordering::Relaxed);
ARMED.store(true, Ordering::Relaxed);
log!(
"capture requested at frame {}: {:?}; recording frame {}",
s.frame,
s.request,
s.frame + 2
);
log!(" {}", crate::cost::line());
}
Phase::Before => start(),
Phase::Recording => {}
}
}
fn start() {
state().phase = Phase::Recording;
log!("capture: recording");
log!(" getting ready, {}", crate::cost::line());
}
fn first_touch(immediate: &ID3D11DeviceContext4, event: &Event) {
let started = std::time::Instant::now();
let todo: Vec<(u64, ID3D11Resource, u64)> = {
let mut s = state();
if s.phase != Phase::Recording || !s.request.contents {
return;
}
let mut todo = Vec::new();
for id in event.reads.iter().chain(&event.writes) {
if let Some(t) = s.resources.get_mut(id)
&& (!t.touched
|| t.dims.misc
& (D3D11_RESOURCE_MISC_TILED.0 as u32 | D3D11_RESOURCE_MISC_TILE_POOL.0 as u32)
!= 0)
{
t.touched = true;
let bytes = t.dims.subs().map_or(0, |s| locum_capture::layout::total(&s));
todo.push((*id, t.resource.clone(), bytes));
}
}
todo
};
counters::touch(immediate, &event.counters);
if todo.is_empty() {
return;
}
let mut bytes = 0;
for (id, resource, size) in todo {
if tiled::special(&resource) {
tiled::touch(immediate, &resource);
continue;
}
if write::upload_of(id).is_some() {
continue;
}
let full = state().in_flight_bytes + size > contents::IN_FLIGHT;
if full {
read_in_flight(immediate);
}
let copy = unsafe { contents::issue(immediate, &resource) };
bytes += size;
let mut s = state();
s.in_flight.push((id, copy));
s.in_flight_bytes += size;
}
COPIED.add(started, bytes);
}
fn read_in_flight(immediate: &ID3D11DeviceContext4) {
let copies = {
let mut s = state();
s.in_flight_bytes = 0;
std::mem::take(&mut s.in_flight)
};
let read = unsafe { contents::read_all(immediate, copies) };
state().contents.extend(read);
let seeds: Vec<_> = {
let mut s = state();
let at = std::mem::take(&mut s.tile_seeds);
at.into_iter()
.filter_map(|seq| Some((seq, s.timeline.get_mut(seq)?.data_readback.take()?)))
.collect()
};
for (seq, rb) in seeds {
match unsafe { contents::read(immediate, &rb) } {
Ok((key, _)) => {
if let Some(e) = state().timeline.get_mut(seq) {
e.data = Some(key);
}
}
Err(error) => {
*state().gaps.entry(("tiled".into(), error)).or_default() += 1;
}
}
}
}
pub static COPIED: Cost = Cost::new();
pub(super) fn record(c: &ID3D11DeviceContext4, mut event: Event) {
let started = std::time::Instant::now();
if !state().records(event.deferred) {
return;
}
if event.deferred {
state().pending.entry(event.context).or_default().push(event);
} else {
tiled::dependencies(&mut event);
first_touch(c, &event);
state().timeline.push(event);
}
RECORDED.add(started, 0);
}
pub static RECORDED: Cost = Cost::new();
pub(super) fn touch_only(c: &ID3D11DeviceContext4, event: &Event) {
if !event.deferred {
first_touch(c, event);
}
}
fn new_event(kind: &'static str, call: &'static str, context: u32, deferred: bool, args: Value) -> Event {
let mut counters: Vec<_> = args["view"].as_u64().into_iter().collect();
counters.extend(
args["set"]
.as_array()
.into_iter()
.flatten()
.filter_map(|s| s["view"].as_u64()),
);
Event {
kind,
call,
context,
deferred,
args,
data: None,
data_readback: None,
stack: Vec::new(),
draw: None,
reads: Vec::new(),
writes: Vec::new(),
counters,
list: None,
results: Vec::new(),
results_taken: false,
}
}
pub fn on_draw(x: Ctx, call: &'static str, args: &[i64]) {
draw_event(x, call, args.iter().map(|&a| Value::from(a)).collect(), &[]);
}
pub unsafe fn on_indirect(x: Ctx, call: &'static str, buffer: *mut c_void, offset: u32) {
let id = unsafe { buffer_id(buffer) };
let reads: Vec<u64> = (id != 0).then_some(id).into_iter().collect();
draw_event(x, call, vec![Value::from(id), Value::from(offset)], &reads);
}
fn draw_event(
Ctx {
c: context,
id,
deferred,
}: Ctx,
call: &'static str,
args: Vec<Value>,
extra_reads: &[u64],
) {
let (records, targets) = {
let s = state();
(s.records(deferred), s.request.targets)
};
let compute = call.starts_with("Dispatch");
let mut draw = Box::<Draw>::default();
let touches = unsafe { snapshot::snapshot(context, compute, &mut draw) };
if records {
if targets && !compute {
draw.bound = unsafe { targets::bound(context) };
}
for cb in &mut draw.cb_copies {
cb.copy = unsafe { snapshot::copy_cb(context, cb) };
}
}
let kind = if compute { "dispatch" } else { "draw" };
let mut event = new_event(kind, call, id, deferred, Value::Array(args));
event.reads = touches.reads;
event.reads.extend_from_slice(extra_reads);
event.writes = touches.writes;
event.counters.extend(
draw.bindings
.iter()
.filter(|b| b.kind == "uav")
.filter_map(|b| b.view),
);
event.draw = Some(draw);
record(context, event);
}
pub fn after_draw(ctx: Ctx) {
after_output(ctx, false);
}
pub fn after_clear(ctx: Ctx) {
after_output(ctx, true);
}
pub fn rule_applied(Ctx { id, deferred, .. }: Ctx, rule: &str) {
let mut s = state();
if !s.records(deferred) {
return;
}
let event = if deferred {
s.pending.get_mut(&id).and_then(|p| p.last_mut())
} else {
s.timeline.last_mut()
};
if let Some(draw) = event.and_then(|e| e.draw.as_deref_mut()) {
draw.overrides.push(rule.to_owned());
}
}
fn after_output(
Ctx {
c: context,
id,
deferred,
}: Ctx,
clear: bool,
) {
let outputs: Vec<(u64, ID3D11Resource)> = {
let mut s = state();
if !s.records(deferred)
|| !(if clear {
s.request.targets
} else {
s.request.contents
})
{
return;
}
let last = if deferred {
s.pending.get_mut(&id).and_then(|p| p.last_mut())
} else {
s.timeline.last_mut()
};
let Some(e) = last else {
return;
};
if e.results_taken || (clear && e.kind != "clear") || (!clear && e.draw.is_none()) {
return;
}
e.results_taken = true;
let mut ids: Vec<u64> = if clear {
e.writes.clone()
} else {
e.draw
.as_ref()
.expect("draw")
.bindings
.iter()
.filter(|b| b.kind == "uav")
.filter_map(|b| b.resource)
.collect()
};
ids.sort_unstable();
ids.dedup();
ids.into_iter()
.filter_map(|i| s.resources.get(&i).map(|t| (i, t.resource.clone())))
.collect()
};
let results: Vec<(u64, Result<contents::Readback, String>)> = outputs
.into_iter()
.map(|(i, r)| (i, unsafe { output_copy(context, &r) }))
.collect();
let mut s = state();
let last = if deferred {
s.pending.get_mut(&id).and_then(|p| p.last_mut())
} else {
s.timeline.last_mut()
};
if let Some(e) = last {
e.results = results;
}
}
unsafe fn output_copy(
context: &ID3D11DeviceContext4,
resource: &ID3D11Resource,
) -> Result<contents::Readback, String> {
let bytes = locum_d3d11::readback::bytes(resource)?;
{
let mut s = state();
if s.target_bytes + bytes > s.request.max_target_bytes {
return Err(OVER_LIMIT.into());
}
s.target_bytes += bytes;
}
unsafe { contents::issue(context, resource) }
}
pub(super) const OVER_LIMIT: &str = "over the capture's memory limit (max_mb)";
pub unsafe fn targets_changing(
Ctx {
c: context,
id,
deferred,
}: Ctx,
rtvs: &[*mut c_void],
dsv: *mut c_void,
) {
if !state().request.targets {
return;
}
let next = unsafe { targets::from_raw(rtvs, dsv) };
if !deferred {
close_timeline_pass(context, Some(&next));
return;
}
let (index, bound) = {
let s = state();
if !s.records(true) {
return;
}
let Some(pending) = s.pending.get(&id) else {
return;
};
let Some(index) = pending.iter().rposition(|e| e.kind == "draw") else {
return;
};
match pending[index].draw.as_deref() {
Some(last) if !last.closed && !last.bound.is_empty() && !targets::same(&last.bound, &next) => {
(index, last.bound.clone())
}
_ => return,
}
};
let copied = unsafe { targets::copy(context, &bound) };
let mut s = state();
let Some(pending) = s.pending.get_mut(&id) else {
return;
};
let after_events = (pending.len() - 1 - index) as u32;
if let Some(last) = pending.get_mut(index).and_then(|e| e.draw.as_deref_mut()) {
last.after = copied;
last.after_events = after_events;
last.closed = true;
}
}
fn last_timeline_draw(s: &mut State) -> Option<(usize, &mut Draw)> {
let index = s.timeline.iter().rposition(|e| e.kind == "draw")?;
let draw = s.timeline[index].draw.as_deref_mut()?;
Some((index, draw))
}
fn close_timeline_pass(immediate: &ID3D11DeviceContext4, next: Option<&[targets::Bound]>) {
let (index, bound) = {
let mut s = state();
if !s.request.targets || s.phase != Phase::Recording {
return;
}
let Some((index, last)) = last_timeline_draw(&mut s) else {
return;
};
if last.closed || last.bound.is_empty() || next.is_some_and(|n| targets::same(&last.bound, n)) {
return;
}
(index, last.bound.clone())
};
let copied = unsafe { targets::copy(immediate, &bound) };
let mut s = state();
let after_events = (s.timeline.len() - 1 - index) as u32;
if let Some(d) = s.timeline[index].draw.as_deref_mut() {
d.after = copied;
d.after_events = after_events;
d.closed = true;
}
}
pub fn end_frame(immediate: &ID3D11DeviceContext4) {
if armed() {
close_timeline_pass(immediate, None);
}
}
pub fn on_finish_list(id: u32, list: *mut c_void, complete: bool) {
let mut s = state();
let events = s.pending.remove(&id).unwrap_or_default();
let before = s.phase == Phase::Before;
if !list.is_null() {
s.lists.insert(
list as usize,
List {
events,
complete,
before,
executions: 0,
},
);
}
}
pub fn on_execute_list(
Ctx {
c: context,
id,
deferred,
}: Ctx,
list: *mut c_void,
) {
let first = {
let mut s = state();
if !s.records(deferred) {
return;
}
match s.lists.get(&(list as usize)) {
Some(l) => l
.events
.iter()
.find_map(|e| e.draw.as_deref().filter(|_| e.kind == "draw"))
.map(|d| d.bound.clone()),
None => {
if !deferred {
s.lists_unrecorded += 1;
}
return;
}
}
};
if !deferred {
if let Some(first) = first {
close_timeline_pass(context, Some(&first));
}
let touches = {
let s = state();
let mut e = new_event("execute", "ExecuteCommandList", id, false, Value::Null);
if let Some(l) = s.lists.get(&(list as usize)) {
for le in &l.events {
e.reads.extend_from_slice(&le.reads);
e.writes.extend_from_slice(&le.writes);
e.counters.extend_from_slice(&le.counters);
}
}
e
};
first_touch(context, &touches);
}
let mut s = state();
let number = u64::from(s.lists_spliced);
let Some(l) = s.lists.get_mut(&(list as usize)) else {
return;
};
l.executions += 1;
let (mut events, before, complete) = (l.events.clone(), l.before, l.complete);
if !deferred && !complete {
*s.gaps.entry(("partial_list".into(), format!("command list {number}: commands before capture arming or from an earlier capture were not recorded"))).or_default() += 1;
}
if deferred {
s.pending.entry(id).or_default().extend(events);
} else {
for e in &mut events {
e.list = Some(number);
}
s.lists_spliced += 1;
s.lists_before += u32::from(before);
drop(s);
for e in &mut events {
tiled::dependencies(e);
}
state().timeline.extend(events);
}
}
pub(super) fn touched(timeline: &[Event]) -> (BTreeSet<u64>, BTreeSet<u64>) {
let mut used = BTreeSet::new();
let mut written = BTreeSet::new();
for e in timeline {
used.extend(e.reads.iter().copied());
used.extend(e.writes.iter().copied());
written.extend(e.writes.iter().copied());
}
(used, written)
}
pub fn recorder_gap(reason: String) {
*state()
.gaps
.entry(("deferred_recorder".into(), reason))
.or_default() += 1;
}
pub fn discard_pending(context: u32) {
state().pending.remove(&context);
}
pub fn list_recorded_before(list: *mut c_void, before: bool) {
if let Some(list) = state().lists.get_mut(&(list as usize)) {
list.before = before;
}
}