use core::ffi::c_void;
use core::mem::ManuallyDrop;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, mpsc};
use std::time::{Duration, Instant};
use locum_capture::d3d12::FirstUse;
use locum_capture::layout::Sub;
use serde_json::{Value, json};
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Graphics::Direct3D12::*;
use windows::Win32::Graphics::Dxgi::Common::DXGI_SAMPLE_DESC;
use windows::core::Interface;
use super::states::{Access, COMMON, COPY_SOURCE, UNDEFINED};
use crate::log::log;
pub struct Copy {
pub resource: u64,
pub offset: u64,
pub len: u64,
pub whole: bool,
}
pub struct Snapshot {
pub resource: u64,
pub states: Vec<Access>,
pub subresources: u32,
pub layout: Vec<Sub>,
pub joined: Option<u32>,
}
#[derive(Default)]
pub struct Batch {
pub submission: u64,
pub uploads: Vec<(u64, Value)>,
pub contents: Vec<(u64, FirstUse)>,
pub checkpoints: Vec<(u64, Result<String, String>)>,
pub continued: Vec<u64>,
}
pub type Results = Arc<Mutex<Vec<Batch>>>;
const CHUNK: u64 = 256 << 20;
const BUDGET: u64 = 1 << 30;
const PATIENCE: Duration = Duration::from_secs(10);
static IN_FLIGHT: AtomicU64 = AtomicU64::new(0);
const ALIGN: u64 = D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT as u64;
enum Item {
Upload {
copy: Copy,
resource: ID3D12Resource,
},
Contents {
snapshot: Snapshot,
resource: ID3D12Resource,
footprints: Vec<(D3D12_PLACED_SUBRESOURCE_FOOTPRINT, u32, u64)>,
},
}
struct Placed {
item: Item,
at: u64,
bytes: u64,
}
struct Pending {
submission: u64,
placed: Vec<Placed>,
readback: ID3D12Resource,
fence: ID3D12Fence,
bytes: u64,
_list: ID3D12GraphicsCommandList,
_allocator: ID3D12CommandAllocator,
}
pub unsafe fn submit(
queue: *mut c_void,
submission: u64,
copies: Vec<Copy>,
snapshots: Vec<Snapshot>,
results: &Results,
) {
let mut failed = Batch {
submission,
..Batch::default()
};
let items = copies
.into_iter()
.map(Ok)
.chain(snapshots.into_iter().map(Err))
.collect::<Vec<_>>();
let context = unsafe { ID3D12CommandQueue::from_raw_borrowed(&queue) }.ok_or("no queue".to_string());
let context = context.and_then(|queue| {
let mut device: Option<ID3D12Device> = None;
unsafe { queue.GetDevice(&mut device) }.map_err(|e| format!("GetDevice: {e}"))?;
Ok((queue, device.expect("GetDevice succeeded")))
});
let (queue, device) = match context {
Ok(c) => c,
Err(reason) => {
for item in items {
fail(&mut failed, item, &reason);
}
results.lock().unwrap_or_else(|e| e.into_inner()).push(failed);
return;
}
};
let kind = unsafe { queue.GetDesc() }.Type;
let (here, direct): (Vec<_>, Vec<_>) = items.into_iter().partition(|item| match item {
Ok(_) => true,
Err(snapshot) => !needs_direct(snapshot, kind),
});
let start = Instant::now();
let chunks = unsafe { place(&device, kind, here, submission, &mut failed) };
unsafe {
run(
queue,
&device,
kind,
submission,
chunks,
start,
results,
&mut failed,
)
};
if !direct.is_empty() {
let chunks = unsafe {
place(
&device,
D3D12_COMMAND_LIST_TYPE_DIRECT,
direct,
submission,
&mut failed,
)
};
if !chunks.is_empty() {
unsafe { beside(queue, &device, submission, chunks, start, results, &mut failed) };
}
}
if !failed.uploads.is_empty() || !failed.contents.is_empty() {
results.lock().unwrap_or_else(|e| e.into_inner()).push(failed);
}
}
fn needs_direct(snapshot: &Snapshot, kind: D3D12_COMMAND_LIST_TYPE) -> bool {
kind != D3D12_COMMAND_LIST_TYPE_DIRECT
&& snapshot.states.iter().any(|s| unreadable(*s, kind).is_some())
&& snapshot
.states
.iter()
.all(|s| unreadable(*s, D3D12_COMMAND_LIST_TYPE_DIRECT).is_none())
}
unsafe fn place(
device: &ID3D12Device,
kind: D3D12_COMMAND_LIST_TYPE,
items: Vec<Result<Copy, Snapshot>>,
submission: u64,
failed: &mut Batch,
) -> Vec<Vec<Placed>> {
let mut chunks: Vec<Vec<Placed>> = vec![Vec::new()];
let mut chunk_bytes = 0;
for item in items {
let placed = match item {
Ok(copy) => match super::real(copy.resource).and_then(|r| r.cast::<ID3D12Resource>().ok()) {
Some(resource) => {
let len = copy.len;
Ok((Item::Upload { copy, resource }, len))
}
None => Err(Ok(copy)),
},
Err(snapshot) => unsafe { contents(device, kind, snapshot) }.map_err(Err),
};
let (item, bytes) = match placed {
Ok(placed) => placed,
Err(Ok(copy)) => {
failed.uploads.push(upload_failed(copy, "no live upload buffer"));
continue;
}
Err(Err((snapshot, reason))) => {
failed
.contents
.push(first_use(submission, &snapshot, Err(reason)));
continue;
}
};
if chunk_bytes > 0 && chunk_bytes + bytes > CHUNK {
chunks.push(Vec::new());
chunk_bytes = 0;
}
chunks.last_mut().unwrap().push(Placed {
item,
at: chunk_bytes,
bytes,
});
chunk_bytes = (chunk_bytes + bytes).next_multiple_of(ALIGN);
}
chunks.retain(|c| !c.is_empty());
chunks
}
struct Helper {
device: usize,
queue: ID3D12CommandQueue,
ready: ID3D12Fence,
done: ID3D12Fence,
next: u64,
}
unsafe impl Send for Helper {}
unsafe fn beside(
queue: &ID3D12CommandQueue,
device: &ID3D12Device,
submission: u64,
chunks: Vec<Vec<Placed>>,
start: Instant,
results: &Results,
failed: &mut Batch,
) {
static HELPERS: OnceLock<Mutex<std::collections::HashMap<usize, Helper>>> = OnceLock::new();
let mut helpers = HELPERS
.get_or_init(Default::default)
.lock()
.unwrap_or_else(|e| e.into_inner());
let key = queue.as_raw() as usize;
let device_key = device.as_raw() as usize;
if helpers.get(&key).is_some_and(|h| h.device != device_key) {
helpers.remove(&key);
}
let helper = match helpers.entry(key) {
std::collections::hash_map::Entry::Occupied(e) => Ok(e.into_mut()),
std::collections::hash_map::Entry::Vacant(e) => {
unsafe { helper(device, device_key) }.map(|h| e.insert(h))
}
};
let helper = match helper {
Ok(h) => h,
Err(reason) => {
for p in chunks.into_iter().flatten() {
fail_item(failed, p.item, &reason);
}
return;
}
};
helper.next += 1;
let n = helper.next;
if let Err(e) = unsafe { queue.Signal(&helper.ready, n) } {
for p in chunks.into_iter().flatten() {
fail_item(failed, p.item, &format!("helper queue: Signal: {e}"));
}
return;
}
if let Err(e) = unsafe { helper.queue.Wait(&helper.ready, n) } {
for p in chunks.into_iter().flatten() {
fail_item(failed, p.item, &format!("helper queue: Wait: {e}"));
}
return;
}
unsafe {
run(
&helper.queue,
device,
D3D12_COMMAND_LIST_TYPE_DIRECT,
submission,
chunks,
start,
results,
failed,
)
};
if unsafe { helper.queue.Signal(&helper.done, n) }.is_ok() {
if let Err(e) = unsafe { queue.Wait(&helper.done, n) } {
log!("D3D12 helper queue: the application's queue could not wait for its copies: {e}");
}
} else {
log!(
"D3D12 helper queue: Signal after the copies failed; the application's queue does not wait for them"
);
}
}
unsafe fn helper(device: &ID3D12Device, device_key: usize) -> Result<Helper, String> {
let queue = unsafe {
device.CreateCommandQueue(&D3D12_COMMAND_QUEUE_DESC {
Type: D3D12_COMMAND_LIST_TYPE_DIRECT,
..Default::default()
})
}
.map_err(|e| format!("helper queue: CreateCommandQueue: {e}"))?;
let fence = || unsafe { device.CreateFence::<ID3D12Fence>(0, D3D12_FENCE_FLAG_NONE) };
Ok(Helper {
device: device_key,
queue,
ready: fence().map_err(|e| format!("helper queue: CreateFence: {e}"))?,
done: fence().map_err(|e| format!("helper queue: CreateFence: {e}"))?,
next: 0,
})
}
#[allow(clippy::too_many_arguments)]
unsafe fn run(
queue: &ID3D12CommandQueue,
device: &ID3D12Device,
kind: D3D12_COMMAND_LIST_TYPE,
submission: u64,
chunks: Vec<Vec<Placed>>,
start: Instant,
results: &Results,
failed: &mut Batch,
) {
for placed in chunks {
let bytes = placed.last().map_or(0, |p| p.at + p.bytes);
loop {
let in_flight = IN_FLIGHT.load(Ordering::Acquire);
if in_flight == 0 || in_flight + bytes <= BUDGET {
break;
}
if start.elapsed() > PATIENCE {
break;
}
std::thread::sleep(Duration::from_millis(1));
}
let in_flight = IN_FLIGHT.load(Ordering::Acquire);
if in_flight != 0 && in_flight + bytes > BUDGET {
let reason = format!(
"readback budget: {in_flight} bytes of earlier copies still waited for the GPU after {} s",
PATIENCE.as_secs()
);
for p in placed {
fail_item(failed, p.item, &reason);
}
continue;
}
match unsafe { record(queue, device, kind, submission, placed, bytes) } {
Ok(pending) => {
IN_FLIGHT.fetch_add(bytes, Ordering::AcqRel);
let results = results.clone();
collector(Box::new(move || {
let batch = collect(pending);
IN_FLIGHT.fetch_sub(bytes, Ordering::AcqRel);
results.lock().unwrap_or_else(|e| e.into_inner()).push(batch);
}));
}
Err((placed, reason)) => {
for p in placed {
fail_item(failed, p.item, &reason);
}
}
}
}
}
pub fn then(f: impl FnOnce() + Send + 'static) {
collector(Box::new(f));
}
type Job = Box<dyn FnOnce() + Send>;
pub(super) fn collector(job: Job) {
static QUEUE: OnceLock<Mutex<mpsc::Sender<Job>>> = OnceLock::new();
QUEUE
.get_or_init(|| {
let (tx, rx) = mpsc::channel::<Job>();
std::thread::Builder::new()
.name("locum-readback".into())
.spawn(move || {
for job in rx {
job();
}
})
.expect("readback collector thread");
Mutex::new(tx)
})
.lock()
.unwrap_or_else(|e| e.into_inner())
.send(job)
.expect("readback collector alive");
}
unsafe fn contents(
device: &ID3D12Device,
kind: D3D12_COMMAND_LIST_TYPE,
snapshot: Snapshot,
) -> Result<(Item, u64), (Snapshot, String)> {
let Some(resource) = super::real(snapshot.resource).and_then(|r| r.cast::<ID3D12Resource>().ok()) else {
return Err((snapshot, "no live resource".into()));
};
if let Some(reason) = snapshot.states.iter().find_map(|s| unreadable(*s, kind)) {
return Err((snapshot, reason));
}
let desc = unsafe { resource.GetDesc() };
if desc.Dimension == D3D12_RESOURCE_DIMENSION_BUFFER {
return Ok((
Item::Contents {
snapshot,
resource,
footprints: Vec::new(),
},
desc.Width,
));
}
let n = snapshot.subresources as usize;
let mut layouts = vec![D3D12_PLACED_SUBRESOURCE_FOOTPRINT::default(); n];
let mut rows = vec![0u32; n];
let mut row_bytes = vec![0u64; n];
let mut total = 0u64;
unsafe {
device.GetCopyableFootprints(
&desc,
0,
n as u32,
0,
Some(layouts.as_mut_ptr()),
Some(rows.as_mut_ptr()),
Some(row_bytes.as_mut_ptr()),
Some(&mut total),
)
};
if total == u64::MAX || row_bytes.contains(&u64::MAX) {
return Err((
snapshot,
format!("GetCopyableFootprints refused the description ({n} subresources)"),
));
}
Ok((
Item::Contents {
snapshot,
resource,
footprints: layouts
.into_iter()
.zip(rows)
.zip(row_bytes)
.map(|((f, r), b)| (f, r, b))
.collect(),
},
total,
))
}
fn unreadable(state: Access, kind: D3D12_COMMAND_LIST_TYPE) -> Option<String> {
match state {
Access::State(s) => {
let legal = match kind {
D3D12_COMMAND_LIST_TYPE_COMPUTE => 0x1 | 0x8 | 0x40 | 0x200 | 0x400 | 0x800 | 0x40_0000,
D3D12_COMMAND_LIST_TYPE_COPY => 0x400 | 0x800,
_ => !0,
};
(s & !legal != 0).then(|| {
format!(
"state {s:#x} cannot be transitioned on a queue of type {}",
kind.0
)
})
}
Access::Layout(UNDEFINED) => Some("layout UNDEFINED: no defined contents".into()),
Access::Layout(l) => {
let legal = match kind {
D3D12_COMMAND_LIST_TYPE_COMPUTE => matches!(l, 0 | 1 | 3 | 6 | 7 | 8 | 24..=29),
D3D12_COMMAND_LIST_TYPE_COPY => matches!(l, 0 | 7 | 8),
_ => !matches!(l, 24..=29),
};
(!legal).then(|| format!("layout {l} cannot be transitioned on a queue of type {}", kind.0))
}
}
}
fn transition(state: Access) -> bool {
match state {
Access::State(s) => s != COMMON && s & COPY_SOURCE == 0,
Access::Layout(l) => !matches!(l, 0 | 1 | 7 | 18 | 19 | 22 | 24 | 25 | 28),
}
}
fn borrowed(r: &ID3D12Resource) -> ManuallyDrop<Option<ID3D12Resource>> {
unsafe { core::mem::transmute_copy(r) }
}
unsafe fn barriers(list: &ID3D12GraphicsCommandList, placed: &[Placed], to_copy: bool) -> Result<(), String> {
let mut legacy = Vec::new();
let mut layouts = Vec::new();
for p in placed {
let Item::Contents {
snapshot, resource, ..
} = &p.item
else {
continue;
};
for (i, state) in snapshot.states.iter().enumerate() {
if !transition(*state) {
continue;
}
match *state {
Access::State(s) => {
let (before, after) = if to_copy {
(s, COPY_SOURCE)
} else {
(COPY_SOURCE, s)
};
legacy.push(D3D12_RESOURCE_BARRIER {
Type: D3D12_RESOURCE_BARRIER_TYPE_TRANSITION,
Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE,
Anonymous: D3D12_RESOURCE_BARRIER_0 {
Transition: ManuallyDrop::new(D3D12_RESOURCE_TRANSITION_BARRIER {
pResource: borrowed(resource),
Subresource: i as u32,
StateBefore: D3D12_RESOURCE_STATES(before),
StateAfter: D3D12_RESOURCE_STATES(after),
}),
},
});
}
Access::Layout(l) => {
let copy = (
D3D12_BARRIER_SYNC_COPY,
D3D12_BARRIER_ACCESS_COPY_SOURCE,
D3D12_BARRIER_LAYOUT_COPY_SOURCE,
);
let none = (
D3D12_BARRIER_SYNC_NONE,
D3D12_BARRIER_ACCESS_NO_ACCESS,
D3D12_BARRIER_LAYOUT(l),
);
let (before, after) = if to_copy { (none, copy) } else { (copy, none) };
layouts.push(D3D12_TEXTURE_BARRIER {
SyncBefore: before.0,
SyncAfter: after.0,
AccessBefore: before.1,
AccessAfter: after.1,
LayoutBefore: before.2,
LayoutAfter: after.2,
pResource: borrowed(resource),
Subresources: D3D12_BARRIER_SUBRESOURCE_RANGE {
IndexOrFirstMipLevel: i as u32,
NumMipLevels: 0,
..Default::default()
},
Flags: D3D12_TEXTURE_BARRIER_FLAG_NONE,
});
}
}
}
}
if !legacy.is_empty() {
unsafe { list.ResourceBarrier(&legacy) };
}
if !layouts.is_empty() {
let list7: ID3D12GraphicsCommandList7 = list
.cast()
.map_err(|e| format!("layout barriers need ID3D12GraphicsCommandList7: {e}"))?;
let group = D3D12_BARRIER_GROUP {
Type: D3D12_BARRIER_TYPE_TEXTURE,
NumBarriers: layouts.len() as u32,
Anonymous: D3D12_BARRIER_GROUP_0 {
pTextureBarriers: layouts.as_ptr(),
},
};
unsafe { list7.Barrier(&[group]) };
}
Ok(())
}
unsafe fn record(
queue: &ID3D12CommandQueue,
device: &ID3D12Device,
kind: D3D12_COMMAND_LIST_TYPE,
submission: u64,
placed: Vec<Placed>,
bytes: u64,
) -> Result<Pending, (Vec<Placed>, String)> {
let fail = |placed, step: &str, e: windows::core::Error| Err((placed, format!("readback {step}: {e}")));
let mut readback: Option<ID3D12Resource> = None;
if let Err(e) = unsafe {
device.CreateCommittedResource(
&D3D12_HEAP_PROPERTIES {
Type: D3D12_HEAP_TYPE_READBACK,
..Default::default()
},
D3D12_HEAP_FLAG_NONE,
&D3D12_RESOURCE_DESC {
Dimension: D3D12_RESOURCE_DIMENSION_BUFFER,
Width: bytes.max(1),
Height: 1,
DepthOrArraySize: 1,
MipLevels: 1,
SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
Layout: D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
..Default::default()
},
D3D12_RESOURCE_STATE_COPY_DEST,
None,
&mut readback,
)
} {
return fail(placed, &format!("buffer of {bytes} bytes"), e);
}
let readback = readback.expect("created");
let allocator: ID3D12CommandAllocator = match unsafe { device.CreateCommandAllocator(kind) } {
Ok(a) => a,
Err(e) => return fail(placed, "CreateCommandAllocator", e),
};
let list: ID3D12GraphicsCommandList = match unsafe { device.CreateCommandList(0, kind, &allocator, None) }
{
Ok(l) => l,
Err(e) => return fail(placed, "CreateCommandList", e),
};
if let Err(reason) = unsafe { barriers(&list, &placed, true) } {
return Err((placed, reason));
}
for p in &placed {
match &p.item {
Item::Upload { copy, resource } => unsafe {
list.CopyBufferRegion(&readback, p.at, resource, copy.offset, copy.len)
},
Item::Contents {
resource, footprints, ..
} if footprints.is_empty() => unsafe {
list.CopyBufferRegion(&readback, p.at, resource, 0, p.bytes)
},
Item::Contents {
resource, footprints, ..
} => {
for (i, (f, _, _)) in footprints.iter().enumerate() {
let mut f = *f;
f.Offset += p.at;
let dst = D3D12_TEXTURE_COPY_LOCATION {
pResource: borrowed(&readback),
Type: D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT,
Anonymous: D3D12_TEXTURE_COPY_LOCATION_0 { PlacedFootprint: f },
};
let src = D3D12_TEXTURE_COPY_LOCATION {
pResource: borrowed(resource),
Type: D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX,
Anonymous: D3D12_TEXTURE_COPY_LOCATION_0 {
SubresourceIndex: i as u32,
},
};
unsafe { list.CopyTextureRegion(&dst, 0, 0, 0, &src, None) };
}
}
}
}
if let Err(reason) = unsafe { barriers(&list, &placed, false) } {
return Err((placed, reason));
}
if let Err(e) = unsafe { list.Close() } {
return fail(placed, "Close", e);
}
let fence: ID3D12Fence = match unsafe { device.CreateFence(0, D3D12_FENCE_FLAG_NONE) } {
Ok(f) => f,
Err(e) => return fail(placed, "CreateFence", e),
};
unsafe { queue.ExecuteCommandLists(&[Some(list.cast().expect("a graphics list is a command list"))]) };
if let Err(e) = unsafe { queue.Signal(&fence, 1) } {
return fail(placed, "Signal", e);
}
Ok(Pending {
submission,
placed,
readback,
fence,
bytes,
_list: list,
_allocator: allocator,
})
}
fn collect(pending: Pending) -> Batch {
let Pending {
submission,
placed,
readback,
fence,
bytes,
..
} = pending;
let mut batch = Batch {
submission,
..Batch::default()
};
let mapped = unsafe { fence.SetEventOnCompletion(1, HANDLE::default()) }
.map_err(|e| format!("readback wait: {e}"))
.and_then(|()| {
let mut p = core::ptr::null_mut();
unsafe {
readback.Map(
0,
Some(&D3D12_RANGE {
Begin: 0,
End: bytes as usize,
}),
Some(&mut p),
)
}
.map_err(|e| format!("readback Map: {e}"))
.map(|()| p.cast::<u8>())
});
let base = match mapped {
Ok(base) => base,
Err(reason) => {
for p in placed {
fail_item(&mut batch, p.item, &reason);
}
return batch;
}
};
for p in placed {
let data = unsafe { base.add(p.at as usize) };
match p.item {
Item::Upload { copy: c, .. } => {
let start = Instant::now();
let mut content = unsafe { super::pipelines::bytes(data.cast(), c.len as usize) };
super::journal::UPLOAD_COST.add(start, c.len);
content["offset"] = json!(c.offset);
if c.whole {
content["whole"] = json!(true);
}
batch.uploads.push((c.resource, content));
}
Item::Contents {
snapshot, footprints, ..
} => {
let content =
unsafe { canonical(&snapshot.layout, snapshot.joined, &footprints, data, p.bytes) }.map(
|bytes| {
let subs = snapshot
.layout
.iter()
.map(|s| {
locum_capture::hash::key(
&bytes[s.offset as usize..(s.offset + s.size) as usize],
)
})
.collect();
let key = locum_capture::hash::key(&bytes);
locum_capture::writer::blob(
&crate::settings::settings().blobs,
key.clone(),
bytes,
true,
);
(key, subs)
},
);
batch.contents.push(first_use(submission, &snapshot, content));
}
}
}
unsafe { readback.Unmap(0, Some(&D3D12_RANGE { Begin: 0, End: 0 })) };
batch
}
pub(super) unsafe fn canonical(
layout: &[Sub],
joined: Option<u32>,
footprints: &[(D3D12_PLACED_SUBRESOURCE_FOOTPRINT, u32, u64)],
data: *const u8,
bytes: u64,
) -> Result<Vec<u8>, String> {
let footprints: Vec<locum_capture::layout::Footprint> = footprints
.iter()
.map(|(f, rows, row_bytes)| locum_capture::layout::Footprint {
offset: f.Offset,
row_pitch: f.Footprint.RowPitch,
depth: f.Footprint.Depth,
rows: *rows,
row_bytes: *row_bytes,
})
.collect();
let data = unsafe { core::slice::from_raw_parts(data, bytes as usize) };
locum_capture::layout::from_footprints(layout, joined, &footprints, data)
}
fn first_use(
submission: u64,
snapshot: &Snapshot,
content: Result<(String, Vec<String>), String>,
) -> (u64, FirstUse) {
(
snapshot.resource,
FirstUse {
submission,
states: Value::Array(snapshot.states.iter().map(|s| s.value()).collect()),
content,
},
)
}
fn fail(batch: &mut Batch, item: Result<Copy, Snapshot>, reason: &str) {
match item {
Ok(c) => batch.uploads.push(upload_failed(c, reason)),
Err(s) => batch
.contents
.push(first_use(batch.submission, &s, Err(reason.into()))),
}
}
fn fail_item(batch: &mut Batch, item: Item, reason: &str) {
match item {
Item::Upload { copy, .. } => batch.uploads.push(upload_failed(copy, reason)),
Item::Contents { snapshot, .. } => {
batch
.contents
.push(first_use(batch.submission, &snapshot, Err(reason.into())))
}
}
}
fn upload_failed(c: Copy, reason: &str) -> (u64, Value) {
let mut content = json!({"diagnostic":reason,"offset":c.offset,"size":c.len});
if c.whole {
content["whole"] = json!(true);
}
(c.resource, content)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn copies_transition_only_from_states_that_cannot_be_read() {
assert!(!transition(Access::State(COMMON)));
assert!(!transition(Access::State(0xAC3)));
assert!(transition(Access::State(0x4)));
assert!(!transition(Access::Layout(7)));
assert!(transition(Access::Layout(2)));
assert!(unreadable(Access::State(0x4), D3D12_COMMAND_LIST_TYPE_COMPUTE).is_some());
assert!(unreadable(Access::State(0x8), D3D12_COMMAND_LIST_TYPE_COMPUTE).is_none());
assert!(unreadable(Access::State(0x80), D3D12_COMMAND_LIST_TYPE_DIRECT).is_none());
assert!(unreadable(Access::Layout(UNDEFINED), D3D12_COMMAND_LIST_TYPE_DIRECT).is_some());
assert!(unreadable(Access::Layout(24), D3D12_COMMAND_LIST_TYPE_DIRECT).is_some());
}
}