use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use locum_capture::tiles::{Layout, Pages};
use serde_json::Value;
use windows::Win32::Foundation::RECT;
use windows::Win32::Graphics::Direct3D12::*;
use super::D3d12;
use crate::open::Capture;
#[derive(Clone, Copy, Debug)]
pub(super) enum Placement {
Committed,
Placed {
heap: i64,
offset: u64,
bytes: Option<u64>,
},
Reserved,
}
impl Placement {
pub(super) fn of(d: &Value) -> Placement {
match d["heap"].as_i64().filter(|h| *h != 0) {
Some(heap) => Placement::Placed {
heap,
offset: d["heap_offset"].as_u64().unwrap_or(0),
bytes: d["bytes"].as_u64(),
},
None if !d["heap_properties"].is_object() => Placement::Reserved,
None => Placement::Committed,
}
}
}
struct Mapping {
cpu_seq: i64,
call: String,
resource: i64,
source: Option<i64>,
args: Value,
}
#[derive(Default)]
pub(super) struct Memory {
heaps: HashMap<i64, Result<(ID3D12Heap, u64), String>>,
pub(super) shared: HashSet<i64>,
pending: HashMap<i64, Vec<(i64, String)>>,
waiting: HashMap<i64, i64>,
blobs: Option<locum_capture::blobs::Blobs>,
mappings: Option<Vec<Mapping>>,
reached: usize,
pub(super) placed: usize,
pub(super) reserved: usize,
committed: BTreeMap<String, usize>,
inferred: usize,
pub(super) remaps: usize,
pub(super) not_remapped: usize,
}
impl Memory {
pub(super) fn warnings(&self) -> Vec<String> {
let mut out: Vec<String> = self
.committed
.iter()
.map(|(why, n)| {
format!("{n} placed or reserved resources were made committed, memory the game shared not shared: {why}")
})
.collect();
if self.inferred > 0 {
out.push(format!(
"{} heaps the capture does not describe (captured before heaps were described) were made from what \
the resources in them record: their heap properties and flags, and a size that holds each at its \
offset on this device",
self.inferred
));
}
if self.not_remapped > 0 {
out.push(format!(
"the frame's {} changes to the tile mappings of reserved resources made committed are not run",
self.not_remapped
));
}
out
}
pub(super) fn fell_back(&mut self, why: String) {
*self.committed.entry(why).or_default() += 1;
}
}
fn heap_properties(p: &Value) -> D3D12_HEAP_PROPERTIES {
let n = |v: &Value| v.as_i64().unwrap_or(0) as i32;
D3D12_HEAP_PROPERTIES {
Type: D3D12_HEAP_TYPE(n(&p["type"])),
CPUPageProperty: D3D12_CPU_PAGE_PROPERTY(n(&p["cpu_page"])),
MemoryPoolPreference: D3D12_MEMORY_POOL(n(&p["memory_pool"])),
..Default::default()
}
}
fn heap_flags(v: &Value) -> D3D12_HEAP_FLAGS {
const KEPT: i32 = 0x4 | 0x40 | 0x80 | 0x400;
D3D12_HEAP_FLAGS(v.as_i64().unwrap_or(0) as i32 & KEPT)
}
fn page_tables(c: &Capture) -> Vec<(i64, Pages)> {
let mut tables: Vec<(i64, String)> = Vec::new();
for table in ["d12_tile_pages", "d12_tile_mappings"] {
if c.has_table(table) {
tables.extend(c.rows(
&format!("SELECT resource, pages_json FROM {table} WHERE pages_json IS NOT NULL"),
[],
|r| Ok((r.get(0)?, r.get(1)?)),
));
}
}
tables
.into_iter()
.filter_map(|(id, json)| {
let pages = serde_json::from_str(&json)
.ok()
.and_then(|v| Pages::from_runs(&v))?;
Some((id, pages))
})
.collect()
}
fn coordinate(v: &Value) -> D3D12_TILED_RESOURCE_COORDINATE {
let n = |i: usize| v[i].as_u64().unwrap_or(0) as u32;
D3D12_TILED_RESOURCE_COORDINATE {
X: n(0),
Y: n(1),
Z: n(2),
Subresource: n(3),
}
}
fn size(v: &Value) -> D3D12_TILE_REGION_SIZE {
let n = |i: usize| v[i].as_u64().unwrap_or(0) as u32;
D3D12_TILE_REGION_SIZE {
NumTiles: n(0),
UseBox: v[1].as_bool().unwrap_or(false).into(),
Width: n(2),
Height: n(3) as u16,
Depth: n(4) as u16,
}
}
fn array<T>(v: &Value, f: impl Fn(&Value) -> T) -> Option<Vec<T>> {
v.as_array().map(|v| v.iter().map(f).collect())
}
impl D3d12 {
fn game_heap(&mut self, c: &Capture, id: i64) -> Result<(ID3D12Heap, u64), String> {
if !self.memory.heaps.contains_key(&id) {
let made = self.heap_desc(c, id).and_then(|(desc, inferred)| {
let mut heap: Option<ID3D12Heap> = None;
unsafe { self.device.CreateHeap(&desc, &mut heap) }
.map_err(|e| format!("their heap could not be made on this device: {e}"))?;
let heap = heap.ok_or("CreateHeap gave no heap")?;
super::name(&heap, format!("#{id}"));
Ok((heap, desc.SizeInBytes, inferred))
});
if matches!(made, Ok((_, _, true))) {
self.memory.inferred += 1;
}
self.memory
.heaps
.insert(id, made.map(|(heap, size, _)| (heap, size)));
}
self.memory.heaps[&id].clone()
}
fn heap_desc(&self, c: &Capture, id: i64) -> Result<(D3D12_HEAP_DESC, bool), String> {
let d: Value = c
.row(
"SELECT description FROM d12_objects WHERE id = ?1 AND kind LIKE 'ID3D12Heap%'",
[id],
|r| r.get::<_, String>(0),
)
.and_then(|d| serde_json::from_str(&d).ok())
.unwrap_or(Value::Null);
let Some(bytes) = d["bytes"].as_u64() else {
return self.inferred_heap(c, id).map(|desc| (desc, true));
};
let p = &d["heap_properties"];
Ok((
D3D12_HEAP_DESC {
SizeInBytes: bytes,
Properties: heap_properties(p),
Alignment: d["alignment"].as_u64().unwrap_or(0),
Flags: heap_flags(&d["flags"]),
},
false,
))
}
fn inferred_heap(&self, c: &Capture, id: i64) -> Result<D3D12_HEAP_DESC, String> {
const UNDESCRIBED: &str = "the capture does not describe their heap (captured before heaps were described), nor does any resource \
placed in it";
let placed: Vec<Value> = c
.rows(
"SELECT description FROM d12_objects WHERE kind LIKE 'ID3D12Resource%' \
AND json_extract(description, '$.heap') = ?1",
[id],
|r| r.get::<_, String>(0),
)
.iter()
.filter_map(|d| serde_json::from_str(d).ok())
.collect();
let said: BTreeSet<String> = placed.iter().map(|d| d["heap_properties"].to_string()).collect();
let p = match (placed.first(), said.len()) {
(Some(d), 1) if d["heap_properties"].is_object() => &d["heap_properties"],
(_, 0 | 1) => return Err(UNDESCRIBED.into()),
_ => {
return Err(
"the capture does not describe their heap, and the resources placed in it say different heap \
properties"
.into(),
)
}
};
let mut end = 0u64;
let mut multisampled = false;
for d in &placed {
let desc = super::resource_desc(d);
multisampled |= desc.SampleDesc.Count > 1;
let info = unsafe { self.device.GetResourceAllocationInfo(0, &[desc]) };
if info.SizeInBytes == u64::MAX {
continue;
}
let bytes = info.SizeInBytes.max(d["bytes"].as_u64().unwrap_or(0));
end = end.max(d["heap_offset"].as_u64().unwrap_or(0) + bytes);
}
for (_, pages) in page_tables(c) {
for &(heap, at) in pages.tiles.values() {
if heap as i64 == id {
end = end.max((u64::from(at) + 1) * locum_capture::tiles::TILE_BYTES);
}
}
}
let alignment: u64 = if multisampled { 4 << 20 } else { 64 << 10 };
Ok(D3D12_HEAP_DESC {
SizeInBytes: end.max(1).next_multiple_of(alignment),
Properties: heap_properties(p),
Alignment: alignment,
Flags: heap_flags(&p["flags"]),
})
}
pub(super) fn make_shared(
&mut self,
c: &Capture,
id: i64,
placement: Placement,
desc: &D3D12_RESOURCE_DESC,
state: D3D12_RESOURCE_STATES,
) -> Result<ID3D12Resource, String> {
let mut r: Option<ID3D12Resource> = None;
match placement {
Placement::Committed => return Err("committed".into()),
Placement::Placed { heap, offset, bytes } => {
let (h, size) = self.game_heap(c, heap)?;
let info = unsafe { self.device.GetResourceAllocationInfo(0, &[*desc]) };
if info.SizeInBytes == u64::MAX {
return Err("this device refuses their descriptions".into());
}
if bytes.is_some_and(|b| info.SizeInBytes > b) {
return Err("they take more of their heap on this device than in the game".into());
}
if info.Alignment == 0 || offset % info.Alignment != 0 || offset + info.SizeInBytes > size {
return Err("their offsets do not fit their heaps on this device".into());
}
unsafe {
self.device
.CreatePlacedResource(&h, offset, desc, state, None, &mut r)
}
.map_err(|e| format!("CreatePlacedResource refused them: {e}"))?;
self.memory.placed += 1;
}
Placement::Reserved => {
if !c.has_table("d12_tile_pages") {
return Err(
"the capture holds no page table of them (captured before they were followed)".into(),
);
}
let (layout, pages): (String, String) = c
.row(
"SELECT layout_json, pages_json FROM d12_tile_pages WHERE resource = ?1 \
AND layout_json IS NOT NULL AND pages_json IS NOT NULL",
[id],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.ok_or("the capture holds no page table of them")?;
let recorded = serde_json::from_str(&layout)
.ok()
.and_then(|v| Layout::from_json(&v))
.ok_or("their page tables do not read")?;
let pages = serde_json::from_str(&pages)
.ok()
.and_then(|v| Pages::from_runs(&v))
.ok_or("their page tables do not read")?;
unsafe { self.device.CreateReservedResource(desc, state, None, &mut r) }
.map_err(|e| format!("CreateReservedResource refused them: {e}"))?;
let resource = r.as_ref().ok_or("CreateReservedResource gave no resource")?;
let layout = locum_capture::tiles::d3d12(resource)?;
if layout != recorded {
return Err("this device tiles them otherwise than the game's".into());
}
let mut by_heap: BTreeMap<i64, (Vec<D3D12_TILED_RESOURCE_COORDINATE>, Vec<u32>)> =
BTreeMap::new();
for (&tile, &(heap, at)) in &pages.tiles {
let [x, y, z, sub] = layout.coordinate(tile)?;
let e = by_heap.entry(heap as i64).or_default();
e.0.push(D3D12_TILED_RESOURCE_COORDINATE {
X: x,
Y: y,
Z: z,
Subresource: sub,
});
e.1.push(at);
}
for (heap, (starts, offsets)) in by_heap {
let (h, _) = self.game_heap(c, heap)?;
let counts = vec![1u32; starts.len()];
unsafe {
self.queue.UpdateTileMappings(
resource,
starts.len() as u32,
Some(starts.as_ptr()),
None,
&h,
starts.len() as u32,
None,
Some(offsets.as_ptr()),
Some(counts.as_ptr()),
D3D12_TILE_MAPPING_FLAG_NONE,
)
};
}
self.memory.reserved += 1;
}
}
self.memory.shared.insert(id);
r.ok_or_else(|| "no resource made".into())
}
pub(super) fn defer(&mut self, c: &Capture, id: i64) -> bool {
let found: Option<(i64, String)> = c.row(
"SELECT f.submission, r.content FROM d12_first_use f JOIN resources r ON r.id = f.resource \
WHERE f.resource = ?1 AND r.content IS NOT NULL",
[id],
|r| Ok((r.get(0)?, r.get(1)?)),
);
let Some((submission, key)) = found else {
return false;
};
self.memory.pending.entry(submission).or_default().push((id, key));
self.memory.waiting.insert(id, submission);
self.memory.blobs.get_or_insert_with(|| c.blobs.clone());
true
}
fn waited(&self, key: &str) -> Result<Vec<u8>, String> {
let blobs = self.memory.blobs.as_ref().ok_or("no blob store")?;
blobs
.get(key)
.map_err(|e| format!("blob {key} is not in {} ({e})", blobs.root().display()))
}
pub(super) fn first_uses(&mut self, submission: i64) -> Result<(), String> {
for (id, key) in self.memory.pending.remove(&submission).unwrap_or_default() {
self.memory.waiting.remove(&id);
let bytes = self.waited(&key)?;
self.put(id, &bytes)?;
}
Ok(())
}
pub(super) fn first_use_now(&mut self, id: i64) -> Result<(), String> {
let Some(submission) = self.memory.waiting.remove(&id) else {
return Ok(());
};
let list = self.memory.pending.entry(submission).or_default();
let Some(i) = list.iter().position(|(r, _)| *r == id) else {
return Ok(());
};
let (_, key) = list.remove(i);
let bytes = self.waited(&key)?;
self.put(id, &bytes)
}
pub(super) fn remap_before(&mut self, c: &Capture, cpu_seq: i64) -> Result<(), String> {
let followed = c.has_table("d12_tile_mappings");
let mappings = self.memory.mappings.get_or_insert_with(|| {
if !followed {
return Vec::new();
}
c.rows(
"SELECT cpu_seq, call, resource, source, args_json FROM d12_tile_mappings ORDER BY cpu_seq, id",
[],
|r| {
Ok(Mapping {
cpu_seq: r.get(0)?,
call: r.get(1)?,
resource: r.get(2)?,
source: r.get(3)?,
args: r
.get::<_, Option<String>>(4)?
.and_then(|a| serde_json::from_str(&a).ok())
.unwrap_or(Value::Null),
})
},
)
});
let due: Vec<usize> = (self.memory.reached..mappings.len())
.take_while(|&i| mappings[i].cpu_seq < cpu_seq)
.collect();
for i in due {
self.memory.reached = i + 1;
let m = &self.memory.mappings.as_ref().expect("loaded")[i];
let (call, resource, source, a) = (m.call.clone(), m.resource, m.source, m.args.clone());
let shared = |id: i64| self.memory.shared.contains(&id) && self.resources.contains_key(&id);
if !shared(resource) || source.is_some_and(|s| !shared(s)) {
if self.resources.contains_key(&resource) {
self.memory.not_remapped += 1;
}
continue;
}
self.remap(c, &call, resource, source, &a)
.map_err(|e| format!("{call} of #{resource}: {e}"))?;
self.memory.remaps += 1;
}
Ok(())
}
fn remap(
&mut self,
c: &Capture,
call: &str,
resource: i64,
source: Option<i64>,
a: &Value,
) -> Result<(), String> {
let r = self.resource(resource)?;
let n = |v: &Value| v.as_u64().unwrap_or(0) as u32;
match call {
"UpdateTileMappings" => {
let heap = match a["heap"].as_i64().filter(|h| *h != 0) {
Some(h) => Some(self.game_heap(c, h)?.0),
None => None,
};
let starts = array(&a["starts"], coordinate);
let sizes = array(&a["sizes"], size);
let flags = array(&a["range_flags"], |v| D3D12_TILE_RANGE_FLAGS(n(v) as i32));
let offsets = array(&a["offsets"], n);
let ranges = n(&a["ranges"]);
let counts =
array(&a["counts"], n).or_else(|| (ranges > 1).then(|| vec![1; ranges as usize]));
unsafe {
self.queue.UpdateTileMappings(
&r,
n(&a["regions"]),
starts.as_ref().map(|v| v.as_ptr()),
sizes.as_ref().map(|v| v.as_ptr()),
heap.as_ref(),
ranges,
flags.as_ref().map(|v| v.as_ptr()),
offsets.as_ref().map(|v| v.as_ptr()),
counts.as_ref().map(|v| v.as_ptr()),
D3D12_TILE_MAPPING_FLAGS(n(&a["flags"]) as i32),
)
};
}
"CopyTileMappings" => {
let s = self.resource(source.ok_or("no source")?)?;
unsafe {
self.queue.CopyTileMappings(
&r,
&coordinate(&a["dst_at"]),
&s,
&coordinate(&a["src_at"]),
&size(&a["size"]),
D3D12_TILE_MAPPING_FLAGS(n(&a["flags"]) as i32),
)
};
}
other => return Err(format!("unknown tile mapping call {other}")),
}
Ok(())
}
pub(super) fn aliasing(&mut self, a: &Value) {
let shared = |id: i64| {
self.memory
.shared
.contains(&id)
.then(|| self.resources.get(&id).map(|m| m.resource.clone()))
.flatten()
};
let barriers: Vec<D3D12_RESOURCE_BARRIER> = a[0]
.as_array()
.into_iter()
.flatten()
.filter(|b| b["type"].as_i64() == Some(D3D12_RESOURCE_BARRIER_TYPE_ALIASING.0 as i64))
.filter_map(|b| {
let p = &b["payload"];
let (before, after) = (
shared(p["before"].as_i64().unwrap_or(0)),
shared(p["after"].as_i64().unwrap_or(0)),
);
(before.is_some() || after.is_some()).then(|| D3D12_RESOURCE_BARRIER {
Type: D3D12_RESOURCE_BARRIER_TYPE_ALIASING,
Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE,
Anonymous: D3D12_RESOURCE_BARRIER_0 {
Aliasing: std::mem::ManuallyDrop::new(D3D12_RESOURCE_ALIASING_BARRIER {
pResourceBefore: std::mem::ManuallyDrop::new(before),
pResourceAfter: std::mem::ManuallyDrop::new(after),
}),
},
})
})
.collect();
if barriers.is_empty() {
return;
}
unsafe { self.list.ResourceBarrier(&barriers) };
for mut b in barriers {
unsafe {
let a = &mut b.Anonymous.Aliasing;
std::mem::ManuallyDrop::drop(&mut a.pResourceBefore);
std::mem::ManuallyDrop::drop(&mut a.pResourceAfter);
}
}
}
pub(super) fn discard(&mut self, a: &Value) {
let id = a[0].as_i64().unwrap_or(0);
let Some(m) = self
.memory
.shared
.contains(&id)
.then(|| self.resources.get(&id))
.flatten()
else {
return;
};
let state = match m.desc.Flags.0 & 0x3 {
0x1 => 0x4,
0x2 => 0x10,
_ => return,
};
let (r, count) = (m.resource.clone(), m.extent.count());
let region = &a[1];
let rects: Vec<RECT> = super::rects(®ion["rects"]);
let (first, subs) = if region.is_object() {
(
region["first_subresource"].as_u64().unwrap_or(0) as u32,
region["subresources"].as_u64().unwrap_or(0) as u32,
)
} else {
(0, count)
};
let needs = [super::Need {
resource: id,
subs: (first..first + subs).collect(),
state,
}];
let d = D3D12_DISCARD_REGION {
NumRects: rects.len() as u32,
pRects: if rects.is_empty() {
std::ptr::null()
} else {
rects.as_ptr()
},
FirstSubresource: first,
NumSubresources: subs,
};
self.barriers(&needs, false);
unsafe { self.list.DiscardResource(&r, Some(&d)) };
self.barriers(&needs, true);
}
}
pub(super) fn aliases(c: &Capture) -> HashMap<i64, BTreeSet<i64>> {
let mut ranges: BTreeMap<i64, Vec<(u64, u64, i64)>> = BTreeMap::new();
for (id, d) in c.rows(
"SELECT id, description FROM d12_objects WHERE kind LIKE 'ID3D12Resource%' \
AND json_extract(description, '$.heap') != 0",
[],
|r| Ok((r.get::<_, i64>(0)?, r.get::<_, String>(1)?)),
) {
let Ok(d) = serde_json::from_str::<Value>(&d) else {
continue;
};
let (Some(heap), offset) = (d["heap"].as_i64(), d["heap_offset"].as_u64().unwrap_or(0)) else {
continue;
};
let bytes = d["bytes"]
.as_u64()
.or_else(|| crate::describe::shape(c, id).map(|s| locum_capture::layout::total(&s.subs())));
if let Some(bytes) = bytes.filter(|b| *b > 0) {
ranges.entry(heap).or_default().push((offset, offset + bytes, id));
}
}
let tile = locum_capture::tiles::TILE_BYTES;
let mut tiles: HashSet<(i64, u32, i64)> = HashSet::new();
for (id, pages) in page_tables(c) {
for &(heap, at) in pages.tiles.values() {
tiles.insert((heap as i64, at, id));
}
}
for (heap, at, id) in tiles {
let start = u64::from(at) * tile;
ranges.entry(heap).or_default().push((start, start + tile, id));
}
let mut out: HashMap<i64, BTreeSet<i64>> = HashMap::new();
for list in ranges.values_mut() {
list.sort_unstable();
for (i, &(_, end, a)) in list.iter().enumerate() {
for &(start, _, b) in &list[i + 1..] {
if start >= end {
break;
}
if a != b {
out.entry(a).or_default().insert(b);
out.entry(b).or_default().insert(a);
}
}
}
}
out
}