use super::com::hr;
use super::device::{
device, duplicate_handle, duplicate_raw_handle, wait_cpu_for, D3d11Device, GpuCompletion,
};
use crate::d3d11_layout::{image_d3d11_layout, D3d11ImageLayout};
use crate::{
BufferIdentity, CpuAccess, DType, Error, IdentityKind, PixelFormat, PixelLayout, Result,
TensorMemory, TensorTrait,
};
use num_traits::Num;
use std::cell::UnsafeCell;
use std::ffi::c_void;
use std::fmt;
use std::marker::PhantomData;
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle, RawHandle};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use windows::core::{IUnknown, Interface, PCWSTR};
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Graphics::Direct3D11::{
ID3D11Fence, ID3D11Texture2D, D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_BOX,
D3D11_CPU_ACCESS_READ, D3D11_CPU_ACCESS_WRITE, D3D11_MAPPED_SUBRESOURCE,
D3D11_MAP_FLAG_DO_NOT_WAIT, D3D11_MAP_READ, D3D11_MAP_READ_WRITE, D3D11_RESOURCE_MISC_SHARED,
D3D11_RESOURCE_MISC_SHARED_NTHANDLE, D3D11_TEXTURE2D_DESC, D3D11_USAGE, D3D11_USAGE_DEFAULT,
D3D11_USAGE_STAGING,
};
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT, DXGI_SAMPLE_DESC};
use windows::Win32::Graphics::Dxgi::{
IDXGIResource1, DXGI_ERROR_WAS_STILL_DRAWING, DXGI_SHARED_RESOURCE_READ,
DXGI_SHARED_RESOURCE_WRITE,
};
const BIND: u32 = (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32;
const MISC: u32 = (D3D11_RESOURCE_MISC_SHARED.0 | D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0) as u32;
#[derive(Default)]
struct MapState {
readers: usize,
writer: bool,
base: usize,
refresh_pending: bool,
}
impl MapState {
fn claim(&mut self, writes: bool) -> Result<Option<usize>> {
if self.writer || (writes && self.readers > 0) {
let held = if self.writer { "write" } else { "read" };
let wanted = if writes { "write" } else { "read" };
return Err(Error::InvalidOperation(format!(
"D3D11 texture tensor is already mapped for {held}; drop that map before taking a {wanted} map"
)));
}
if writes {
self.writer = true;
return Ok(None);
}
if self.readers > 0 {
self.readers += 1;
return Ok(Some(self.base));
}
self.readers = 1;
Ok(None)
}
fn release(&mut self, writes: bool) -> bool {
if writes {
self.writer = false;
} else {
self.readers -= 1;
}
let last = !self.writer && self.readers == 0;
if last {
self.base = 0;
}
last
}
}
struct Staging {
tex: ID3D11Texture2D,
row_pitch: usize,
rows: usize,
state: Mutex<MapState>,
}
impl Staging {
fn bytes(&self) -> usize {
self.row_pitch * self.rows
}
fn state(&self) -> std::sync::MutexGuard<'_, MapState> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
}
struct HostBuffer {
_bytes: UnsafeCell<Box<[u8]>>,
base: *mut u8,
len: usize,
row_pitch: usize,
state: Mutex<MapState>,
}
const HOST_PIN_ALIGN: usize = 64;
unsafe impl Sync for HostBuffer {}
unsafe impl Send for HostBuffer {}
impl HostBuffer {
fn new(row_pitch: usize, rows: usize) -> Self {
let len = row_pitch * rows;
let mut bytes = vec![0u8; len + HOST_PIN_ALIGN - 1].into_boxed_slice();
let start = bytes.as_mut_ptr();
let base = unsafe { start.add(start.align_offset(HOST_PIN_ALIGN)) };
HostBuffer {
_bytes: UnsafeCell::new(bytes),
base,
len,
row_pitch,
state: Mutex::new(MapState::default()),
}
}
fn state(&self) -> std::sync::MutexGuard<'_, MapState> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
fn span(&self) -> (*mut u8, usize) {
(self.base, self.len)
}
}
pub struct D3d11TextureTensor<T> {
device: &'static D3d11Device,
texture: ID3D11Texture2D,
layout: D3d11ImageLayout,
desc_bind_flags: u32,
width: usize,
height: usize,
format: PixelFormat,
dtype: DType,
shape: Vec<usize>,
name: String,
access: CpuAccess,
staging: Option<Arc<Staging>>,
host: Arc<OnceLock<HostBuffer>>,
nt_handle: Arc<OwnedHandle>,
identity: BufferIdentity,
last_gpu_write: Arc<AtomicU64>,
imported_fence: Option<ID3D11Fence>,
wrote_host: Arc<AtomicBool>,
view_offset: usize,
_marker: PhantomData<T>,
}
const IMPORT_WAIT_MS: u32 = 5_000;
fn texture_desc(
layout: &D3d11ImageLayout,
usage: D3D11_USAGE,
bind: u32,
cpu: u32,
misc: u32,
) -> D3D11_TEXTURE2D_DESC {
D3D11_TEXTURE2D_DESC {
Width: layout.texture_width as u32,
Height: layout.texture_height as u32,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT(layout.dxgi_format as i32),
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: usage,
BindFlags: bind,
CPUAccessFlags: cpu,
MiscFlags: misc,
}
}
fn create_texture(d: &D3d11Device, desc: &D3D11_TEXTURE2D_DESC) -> Result<ID3D11Texture2D> {
let mut out: Option<ID3D11Texture2D> = None;
hr("ID3D11Device::CreateTexture2D", unsafe {
d.dev().CreateTexture2D(desc, None, Some(&mut out))
})?;
out.ok_or_else(|| {
Error::IoError(std::io::Error::other(
"ID3D11Device::CreateTexture2D returned no texture",
))
})
}
fn create_nt_handle(tex: &ID3D11Texture2D) -> Result<OwnedHandle> {
let res = hr(
"ID3D11Texture2D::QueryInterface(IDXGIResource1)",
tex.cast::<IDXGIResource1>(),
)?;
let handle = hr("IDXGIResource1::CreateSharedHandle", unsafe {
res.CreateSharedHandle(
None,
(DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE).0,
PCWSTR::null(),
)
})?;
Ok(unsafe { OwnedHandle::from_raw_handle(handle.0) })
}
fn create_staging_texture(d: &D3d11Device, layout: &D3d11ImageLayout) -> Result<ID3D11Texture2D> {
let cpu = (D3D11_CPU_ACCESS_READ.0 | D3D11_CPU_ACCESS_WRITE.0) as u32;
create_texture(d, &texture_desc(layout, D3D11_USAGE_STAGING, 0, cpu, 0))
}
fn staging_row_pitch(d: &D3d11Device, tex: &ID3D11Texture2D) -> Result<usize> {
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
hr("ID3D11DeviceContext::Map (pitch query)", unsafe {
d.ctx().Map(tex, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
})?;
let row_pitch = mapped.RowPitch as usize;
unsafe { d.ctx().Unmap(tex, 0) };
Ok(row_pitch)
}
fn create_staging(d: &D3d11Device, layout: &D3d11ImageLayout) -> Result<Staging> {
let tex = create_staging_texture(d, layout)?;
let row_pitch = staging_row_pitch(d, &tex)?;
Ok(Staging {
tex,
row_pitch,
rows: layout.texture_height,
state: Mutex::new(MapState::default()),
})
}
fn widen_semi_planar_to_pitch(
d: &D3d11Device,
layout: D3d11ImageLayout,
) -> Result<D3d11ImageLayout> {
let pitch = staging_row_pitch(d, &create_staging_texture(d, &layout)?)?;
if pitch <= layout.tight_row_bytes() {
return Ok(layout);
}
let widened = layout.widened_to(pitch.next_multiple_of(2));
let second = staging_row_pitch(d, &create_staging_texture(d, &widened)?)?;
if second != widened.tight_row_bytes() {
return Err(Error::InvalidOperation(format!(
"D3D11 semi-planar allocation: a {}-texel-wide R8 staging texture still has a row \
pitch of {second}, so no texture width equals the pitch a CPU map sees",
widened.texture_width
)));
}
Ok(widened)
}
fn tex_key(tex: &ID3D11Texture2D) -> u64 {
tex.as_raw() as usize as u64
}
fn validate_external(
d: &D3d11Device,
tex: &ID3D11Texture2D,
width: usize,
height: usize,
format: PixelFormat,
dtype: DType,
) -> Result<(D3d11ImageLayout, u32)> {
let layout = image_d3d11_layout(format, dtype, width, height).ok_or_else(|| {
Error::InvalidArgument(format!(
"external D3D11 texture: no zero-copy D3D11 texture layout exists for \
{format:?}/{dtype:?} {width}x{height}"
))
})?;
let mut desc = D3D11_TEXTURE2D_DESC::default();
unsafe { tex.GetDesc(&mut desc) };
if desc.Usage != D3D11_USAGE_DEFAULT {
return Err(Error::InvalidArgument(format!(
"external D3D11 texture: Usage is {} but D3D11_USAGE_DEFAULT is required",
desc.Usage.0
)));
}
if desc.BindFlags & D3D11_BIND_SHADER_RESOURCE.0 as u32 == 0 {
return Err(Error::InvalidArgument(format!(
"external D3D11 texture: BindFlags {:#x} lack D3D11_BIND_SHADER_RESOURCE",
desc.BindFlags
)));
}
if desc.MipLevels != 1 {
return Err(Error::InvalidArgument(format!(
"external D3D11 texture: MipLevels is {} but 1 is required",
desc.MipLevels
)));
}
if desc.ArraySize != 1 {
return Err(Error::InvalidArgument(format!(
"external D3D11 texture: ArraySize is {} but 1 is required",
desc.ArraySize
)));
}
if desc.SampleDesc.Count != 1 {
return Err(Error::InvalidArgument(format!(
"external D3D11 texture: SampleDesc.Count is {} but 1 is required (no multisampling)",
desc.SampleDesc.Count
)));
}
if desc.Format.0 as u32 != layout.dxgi_format {
return Err(Error::InvalidArgument(format!(
"external D3D11 texture: Format is {} but {format:?}/{dtype:?} needs DXGI format {}",
desc.Format.0, layout.dxgi_format
)));
}
if desc.Height as usize != layout.texture_height {
return Err(Error::InvalidArgument(format!(
"external D3D11 texture: texture is {} rows tall but {format:?}/{dtype:?} \
{width}x{height} needs {}",
desc.Height, layout.texture_height
)));
}
let layout = if format.layout() == PixelLayout::SemiPlanar {
let texture_width = desc.Width as usize;
if texture_width < layout.texture_width {
return Err(Error::InvalidArgument(format!(
"external D3D11 texture: texture is {texture_width} texels wide but \
{format:?}/{dtype:?} {width}x{height} needs at least {} (the image's even row)",
layout.texture_width
)));
}
let wrapped = layout.widened_to(texture_width);
if !texture_width.is_multiple_of(2) {
return Err(Error::InvalidArgument(format!(
"external D3D11 texture: a {format:?} texture is {texture_width} texels wide, \
which is odd; a semi-planar row holds whole chroma pairs, so the width must \
be even"
)));
}
let pitch = staging_row_pitch(d, &create_staging_texture(d, &wrapped)?)?;
if pitch != texture_width {
return Err(Error::InvalidArgument(format!(
"external D3D11 texture: a {format:?} texture is {texture_width} texels wide with \
a staging row pitch of {pitch}; semi-planar textures must be as wide as the \
driver row pitch; allocate through the HAL or match the pitch"
)));
}
wrapped
} else {
if desc.Width as usize != layout.texture_width {
return Err(Error::InvalidArgument(format!(
"external D3D11 texture: texture is {} texels wide but {format:?}/{dtype:?} \
{width}x{height} needs {}",
desc.Width, layout.texture_width
)));
}
layout
};
let owner = hr("ID3D11Texture2D::GetDevice", unsafe { tex.GetDevice() })?;
let owner = hr(
"ID3D11Device::QueryInterface(IUnknown)",
owner.cast::<IUnknown>(),
)?;
let ours = hr(
"ID3D11Device::QueryInterface(IUnknown)",
d.dev().cast::<IUnknown>(),
)?;
if owner != ours {
return Err(Error::InvalidArgument(
"external D3D11 texture: GetDevice is not the HAL's process device".into(),
));
}
Ok((layout, desc.BindFlags))
}
impl<T> D3d11TextureTensor<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
pub(crate) fn new_image(
width: usize,
height: usize,
format: PixelFormat,
dtype: DType,
shape: &[usize],
name: Option<&str>,
access: CpuAccess,
) -> Result<Self> {
let layout = image_d3d11_layout(format, dtype, width, height).ok_or_else(|| {
Error::InvalidArgument(format!(
"Tensor::image: no zero-copy D3D11 texture layout exists for {format:?}/{dtype:?} \
{width}x{height} (see edgefirst_tensor::d3d11_layout); pass memory=None to \
auto-select or Some(TensorMemory::Mem) for a CPU tensor"
))
})?;
let d = device()?;
let layout = if format.layout() == PixelLayout::SemiPlanar {
widen_semi_planar_to_pitch(d, layout)?
} else {
layout
};
let tex = create_texture(
d,
&texture_desc(&layout, D3D11_USAGE_DEFAULT, BIND, 0, MISC),
)?;
let handle = create_nt_handle(&tex)?;
let key = tex_key(&tex);
Self::finish(
d, tex, BIND, layout, width, height, format, dtype, shape, name, access, handle, key,
)
}
#[allow(clippy::too_many_arguments)] pub(crate) unsafe fn from_d3d11_texture(
texture: *mut c_void,
width: usize,
height: usize,
format: PixelFormat,
dtype: DType,
shape: &[usize],
access: CpuAccess,
name: Option<&str>,
) -> Result<Self> {
let d = device()?;
let tex = unsafe { ID3D11Texture2D::from_raw_borrowed(&texture) }
.cloned()
.ok_or_else(|| Error::InvalidArgument("from_d3d11_texture: null texture".into()))?;
let (layout, bind) = validate_external(d, &tex, width, height, format, dtype)?;
let handle = create_nt_handle(&tex).map_err(|e| {
Error::InvalidArgument(format!(
"from_d3d11_texture: the texture was not created with \
D3D11_RESOURCE_MISC_SHARED_NTHANDLE: {e}"
))
})?;
let key = tex_key(&tex);
Self::finish(
d, tex, bind, layout, width, height, format, dtype, shape, name, access, handle, key,
)
}
#[allow(clippy::too_many_arguments)] pub(crate) unsafe fn from_d3d11_shared_handle(
handle: RawHandle,
width: usize,
height: usize,
format: PixelFormat,
dtype: DType,
shape: &[usize],
access: CpuAccess,
completion: Option<(RawHandle, u64)>,
name: Option<&str>,
) -> Result<Self> {
let d = device()?;
let dev1 = d.dev1().ok_or_else(|| {
Error::NotImplemented("ID3D11Device1 required to open shared handles".into())
})?;
let tex: ID3D11Texture2D = hr("ID3D11Device1::OpenSharedResource1", unsafe {
dev1.OpenSharedResource1(HANDLE(handle))
})?;
let (layout, bind) = validate_external(d, &tex, width, height, format, dtype)?;
let own = duplicate_raw_handle(handle).map_err(Error::IoError)?;
let key = tex_key(&tex);
let mut t = Self::finish(
d, tex, bind, layout, width, height, format, dtype, shape, name, access, own, key,
)?;
if let Some((fence_handle, value)) = completion {
unsafe { t.order_behind(d, fence_handle, value) }?;
}
Ok(t)
}
unsafe fn order_behind(
&mut self,
d: &'static D3d11Device,
fence_handle: RawHandle,
value: u64,
) -> Result<()> {
let Some(dev5) = d.dev5() else {
log::warn!(
"D3D11 import: no ID3D11Device5 on the process device, so the producer's fence \
value {value} cannot be opened or waited on; the tensor records no completion"
);
return Ok(());
};
let mut fence: Option<ID3D11Fence> = None;
hr("ID3D11Device5::OpenSharedFence", unsafe {
dev5.OpenSharedFence(HANDLE(fence_handle), &mut fence)
})?;
let fence = fence.ok_or_else(|| {
Error::IoError(std::io::Error::other(
"ID3D11Device5::OpenSharedFence returned no fence",
))
})?;
let Some(ctx4) = d.ctx4().filter(|_| d.signal_supported()) else {
if let Err(e) = wait_cpu_for(&fence, value, IMPORT_WAIT_MS) {
log::warn!(
"D3D11 import: this device cannot queue a wait or signal a fence, and \
blocking on the producer's value {value} failed ({e}); the texture may \
still be being written"
);
}
return Ok(());
};
hr("ID3D11DeviceContext4::Wait", unsafe {
ctx4.Wait(&fence, value)
})?;
self.imported_fence = Some(fence);
self.last_gpu_write
.store(d.signal().unwrap_or(0), Ordering::Release);
Ok(())
}
#[allow(clippy::too_many_arguments)] fn finish(
d: &'static D3d11Device,
texture: ID3D11Texture2D,
bind_flags: u32,
layout: D3d11ImageLayout,
width: usize,
height: usize,
format: PixelFormat,
dtype: DType,
shape: &[usize],
name: Option<&str>,
access: CpuAccess,
handle: OwnedHandle,
key: u64,
) -> Result<Self> {
let staging = match access {
CpuAccess::Read | CpuAccess::ReadWrite => Some(Arc::new(create_staging(d, &layout)?)),
CpuAccess::None | CpuAccess::Write => None,
};
Ok(Self {
device: d,
texture,
layout,
desc_bind_flags: bind_flags,
width,
height,
format,
dtype,
shape: shape.to_vec(),
name: name.unwrap_or("d3d11_tensor").to_owned(),
access,
staging,
host: Arc::new(OnceLock::new()),
nt_handle: Arc::new(handle),
identity: BufferIdentity::derived(IdentityKind::D3d11Texture, key),
last_gpu_write: Arc::new(AtomicU64::new(0)),
imported_fence: None,
wrote_host: Arc::new(AtomicBool::new(false)),
view_offset: 0,
_marker: PhantomData,
})
}
fn require_access(&self, access: CpuAccess) -> Result<()> {
if self.access == CpuAccess::None {
return Err(Error::InvalidOperation(
"map on a CpuAccess::None D3D11 texture tensor; allocate with Read, Write or \
ReadWrite"
.into(),
));
}
if access.writes() && !self.access.writes() {
return Err(Error::InvalidOperation(
"writable map on a read-only D3D11 texture tensor".into(),
));
}
Ok(())
}
fn image_row_bytes(&self) -> usize {
if self.format.layout() == PixelLayout::SemiPlanar {
self.width * self.dtype.size()
} else {
self.layout.tight_row_bytes()
}
}
fn backing_pitch(&self) -> usize {
self.staging
.as_ref()
.map_or(self.layout.tight_row_bytes(), |s| s.row_pitch)
}
fn backing_bytes(&self) -> usize {
self.backing_pitch() * self.layout.texture_height
}
fn host_buffer(&self) -> &HostBuffer {
self.host
.get_or_init(|| HostBuffer::new(self.backing_pitch(), self.layout.texture_height))
}
fn wait_for_recorded_write(&self) {
let value = self.last_gpu_write();
if value == 0 {
return;
}
if !self.device.fence_is_shared() {
log::warn!(
"D3D11 map: this copy signals a fence of its own, so the recorded write \
{value} names a point it may never reach; the staging refresh is not \
ordered behind it"
);
return;
}
let allocated = self.device.last_signalled();
if value > allocated {
log::warn!(
"D3D11 map: the recorded write {value} is above the newest value this \
device's counter has allocated ({allocated}), so nothing will signal it; \
the wait is skipped and the staging refresh is not ordered behind it"
);
return;
}
if value <= self.device.completed_value() {
return;
}
let (Some(fence), Some(ctx4)) = (self.device.fence(), self.device.ctx4()) else {
return;
};
if let Err(e) = unsafe { ctx4.Wait(fence, value) } {
log::warn!(
"D3D11 map: queuing a wait on the recorded write {value} failed ({e}); \
the staging refresh may read the texture while the GPU still writes it"
);
}
}
pub(crate) fn sync_for_cpu(&self, access: CpuAccess) -> Result<()> {
if !access.reads() {
return Ok(());
}
let (Some(buffer), Some(st)) = (self.host.get(), self.staging.as_ref()) else {
return Ok(());
};
let mut state = st.state();
if state.writer || state.readers > 0 {
return Err(Error::InvalidOperation(
"sync_for_cpu on a D3D11 texture tensor that is already mapped; drop the map first"
.into(),
));
}
self.wait_for_recorded_write();
unsafe { self.device.ctx().CopyResource(&st.tex, &self.texture) };
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
hr("ID3D11DeviceContext::Map (host buffer refresh)", unsafe {
self.device
.ctx()
.Map(&st.tex, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
})?;
state.refresh_pending = false;
let src_pitch = mapped.RowPitch as usize;
let (base, _) = buffer.span();
let span = src_pitch.min(buffer.row_pitch);
for row in 0..self.layout.texture_height {
unsafe {
std::ptr::copy_nonoverlapping(
(mapped.pData as *const u8).add(row * src_pitch),
base.add(row * buffer.row_pitch),
span,
);
}
}
unsafe { self.device.ctx().Unmap(&st.tex, 0) };
Ok(())
}
pub(crate) fn sync_for_device(&self, access: CpuAccess) -> Result<()> {
if !access.writes() {
return Ok(());
}
let _staging_claim = match self.staging.as_ref() {
Some(st) => {
let state = st.state();
if state.writer || state.readers > 0 {
return Err(Error::InvalidOperation(
"sync_for_device on a D3D11 texture tensor whose staging copy is \
mapped; drop the map first"
.into(),
));
}
Some(state)
}
None => None,
};
let _host_claim = match self.host.get() {
Some(buffer) => {
let state = buffer.state();
if state.writer || state.readers > 0 {
return Err(Error::InvalidOperation(
"sync_for_device on a D3D11 texture tensor whose host buffer is \
mapped; drop the map first"
.into(),
));
}
Some(state)
}
None => None,
};
match (self.wrote_host.load(Ordering::Acquire), self.host.get()) {
(true, Some(buffer)) => upload_buffer_window(
self.device,
&self.texture,
buffer,
self.layout.texture_width,
0,
self.layout.texture_height,
),
_ => match &self.staging {
Some(st) => unsafe { self.device.ctx().CopyResource(&self.texture, &st.tex) },
None => return Ok(()),
},
}
publish(self.device);
Ok(())
}
pub(crate) fn host_pin<'a>(&self, access: CpuAccess) -> Result<crate::pin::HostPin<'a>> {
self.require_access(self.access)?;
self.check_view_offset()?;
if access.writes() {
self.wrote_host.store(true, Ordering::Release);
}
let (base, len) = self.host_buffer().span();
let at = unsafe { base.add(self.view_offset) };
let keepalive: Arc<dyn Send + Sync> = self.host.clone();
Ok(crate::pin::HostPin::new(
keepalive,
at,
len.saturating_sub(self.view_offset),
))
}
fn pin_impl<'a>(
&self,
access: CpuAccess,
non_blocking: bool,
) -> Result<crate::pin::HostPin<'a>> {
self.require_access(access)?;
self.check_view_offset()?;
self.refuse_partial_write(access)?;
match self.staging.as_ref() {
Some(st) => self.staging_pin(st, access, non_blocking),
None => self.buffer_pin(access),
}
}
fn addressable_span(&self) -> usize {
let tight_row = self.image_row_bytes();
if tight_row == 0 {
return 0;
}
let tight = self.shape.iter().product::<usize>() * self.dtype.size();
let rows = tight.div_ceil(tight_row);
if rows == 0 {
return 0;
}
let last_row = tight - (rows - 1) * tight_row;
(rows - 1) * self.backing_pitch() + last_row
}
fn check_view_offset(&self) -> Result<()> {
let capacity = self.backing_bytes();
let end = self.view_offset.checked_add(self.addressable_span());
match end {
Some(end) if end <= capacity => Ok(()),
_ => Err(Error::InsufficientCapacity {
needed: end.unwrap_or(usize::MAX),
capacity,
}),
}
}
pub(crate) fn set_view_offset(&mut self, offset: usize) {
self.view_offset = offset;
}
fn row_window(&self) -> Option<(usize, usize)> {
let pitch = self.backing_pitch();
if pitch == 0 || !self.view_offset.is_multiple_of(pitch) {
return None;
}
Some((self.view_offset / pitch, self.pitched_extent() / pitch))
}
fn refuse_partial_write(&self, access: CpuAccess) -> Result<()> {
if access != CpuAccess::Write || self.staging.is_none() {
return Ok(());
}
let backing = self.backing_bytes();
if self.view_offset == 0 && self.pitched_extent() >= backing {
return Ok(());
}
Err(Error::PartialWriteRequiresReadWrite(format!(
"write-only map of a D3D11 texture tensor window ({} bytes at offset {}) that is \
shorter than the {backing}-byte backing: unmapping publishes the whole texture, \
so the rows outside the window would become undefined; map the window with \
CpuAccess::ReadWrite instead",
self.pitched_extent(),
self.view_offset
)))
}
fn staging_pin<'a>(
&self,
st: &Arc<Staging>,
access: CpuAccess,
non_blocking: bool,
) -> Result<crate::pin::HostPin<'a>> {
let writes = access.writes();
let base = {
let mut state = st.state();
match state.claim(writes)? {
Some(base) => base,
None => self.establish_map(st, &mut state, access, non_blocking)?,
}
};
let guard = StagingGuard {
device: self.device,
texture: self.texture.clone(),
staging: Arc::clone(st),
writer: writes,
};
Ok(self.pin_over(base as *mut u8, st.bytes(), guard))
}
fn establish_map(
&self,
st: &Staging,
state: &mut MapState,
access: CpuAccess,
non_blocking: bool,
) -> Result<usize> {
if access.reads() && !(non_blocking && state.refresh_pending) {
self.wait_for_recorded_write();
unsafe { self.device.ctx().CopyResource(&st.tex, &self.texture) };
state.refresh_pending = true;
if non_blocking {
publish(self.device);
}
}
let mode = if access.writes() {
D3D11_MAP_READ_WRITE
} else {
D3D11_MAP_READ
};
let flags = if non_blocking {
D3D11_MAP_FLAG_DO_NOT_WAIT.0 as u32
} else {
0
};
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
let result = unsafe {
self.device
.ctx()
.Map(&st.tex, 0, mode, flags, Some(&mut mapped))
};
if let Err(e) = result {
state.release(access.writes());
if e.code() == DXGI_ERROR_WAS_STILL_DRAWING {
return Err(Error::IoError(std::io::Error::from(
std::io::ErrorKind::WouldBlock,
)));
}
return hr("ID3D11DeviceContext::Map", Err(e));
}
state.refresh_pending = false;
state.base = mapped.pData as usize;
Ok(state.base)
}
fn buffer_pin<'a>(&self, access: CpuAccess) -> Result<crate::pin::HostPin<'a>> {
let write_region = if access.writes() {
Some(self.row_window().ok_or_else(|| {
Error::InvalidArgument(format!(
"writable map of a D3D11 texture tensor window ({} bytes at offset {}) not \
aligned to the {}-byte backing pitch: the host-buffer path can only \
publish whole rows",
self.pitched_extent(),
self.view_offset,
self.backing_pitch()
))
})?)
} else {
None
};
let buffer = self.host_buffer();
buffer.state().claim(access.writes())?;
let (base, len) = buffer.span();
let guard = HostBufferGuard {
device: self.device,
texture: self.texture.clone(),
host: Arc::clone(&self.host),
texture_width: self.layout.texture_width,
writeback: write_region,
};
Ok(self.pin_over(base, len, guard))
}
fn pin_over<'a, G: Send + Sync + 'static>(
&self,
base: *mut u8,
len: usize,
guard: G,
) -> crate::pin::HostPin<'a> {
let at = unsafe { base.add(self.view_offset) };
crate::pin::HostPin::new(Arc::new(guard), at, len.saturating_sub(self.view_offset))
}
pub(crate) fn scoped_pin<'a>(&self, access: CpuAccess) -> Result<crate::pin::HostPin<'a>> {
self.pin_impl(access, false)
}
pub(crate) fn try_scoped_pin<'a>(&self, access: CpuAccess) -> Result<crate::pin::HostPin<'a>> {
self.pin_impl(access, true)
}
fn pitched_extent(&self) -> usize {
let tight_row = self.image_row_bytes();
if tight_row == 0 {
return 0;
}
let tight = self.shape.iter().product::<usize>() * self.dtype.size();
tight.div_ceil(tight_row) * self.backing_pitch()
}
fn view_of<'a>(
&self,
pin: crate::pin::HostPin<'a>,
access: CpuAccess,
byte_size_override: Option<usize>,
) -> crate::view::HostView<'a, T>
where
T: 'a,
{
let bytes = byte_size_override.unwrap_or_else(|| self.pitched_extent());
crate::view::HostView::new(pin, self.shape.clone(), Some(bytes), access)
}
fn check_byte_size(&self, bytes: usize) -> Result<()> {
let capacity = self.backing_bytes().saturating_sub(self.view_offset);
if bytes > capacity {
return Err(Error::InsufficientCapacity {
needed: bytes,
capacity,
});
}
Ok(())
}
pub(crate) fn map_with_byte_size<'a>(
&self,
bytes: usize,
access: CpuAccess,
) -> Result<crate::view::HostView<'a, T>>
where
T: 'a,
{
self.check_byte_size(bytes)?;
Ok(self.view_of(self.scoped_pin(access)?, access, Some(bytes)))
}
pub(crate) fn try_map_with_byte_size<'a>(
&self,
bytes: usize,
access: CpuAccess,
) -> Result<crate::view::HostView<'a, T>>
where
T: 'a,
{
self.check_byte_size(bytes)?;
Ok(self.view_of(self.try_scoped_pin(access)?, access, Some(bytes)))
}
pub(crate) fn texture_ptr(&self) -> *mut c_void {
self.texture.as_raw()
}
pub(crate) fn layout(&self) -> D3d11ImageLayout {
self.layout
}
#[allow(dead_code)]
pub(crate) fn is_render_target(&self) -> bool {
self.desc_bind_flags & D3D11_BIND_RENDER_TARGET.0 as u32 != 0
}
pub(crate) fn shared_handle(&self) -> Result<OwnedHandle> {
duplicate_handle(&self.nt_handle).map_err(Error::IoError)
}
pub(crate) fn shared_handle_value(&self) -> usize {
self.nt_handle.as_raw_handle() as usize
}
pub(crate) fn gpu_completion(&self) -> Result<Option<GpuCompletion>> {
let value = self.last_gpu_write();
if value == 0 {
return Ok(None);
}
Ok(Some(GpuCompletion {
fence: self.device.fence_shared_handle().map_err(Error::IoError)?,
value,
}))
}
pub(crate) fn set_gpu_write(&self, value: u64) {
self.last_gpu_write.fetch_max(value, Ordering::AcqRel);
}
pub(crate) fn last_gpu_write(&self) -> u64 {
self.last_gpu_write.load(Ordering::Acquire)
}
pub(crate) fn image_backing_row_stride(&self) -> Option<usize> {
if let Some(st) = self.staging.as_ref() {
return Some(st.row_pitch);
}
(self.format.layout() == PixelLayout::SemiPlanar).then(|| self.layout.tight_row_bytes())
}
pub(crate) fn tight_bytes(&self) -> usize {
self.image_row_bytes() * self.layout.texture_height
}
pub(crate) fn try_init_cuda(&self) -> Option<crate::cuda::CudaHandle> {
if !crate::cuda::is_cuda_available() {
return None;
}
let fence = self.device.fence_shared_handle().ok();
crate::cuda::import_d3d11_texture(
self.nt_handle.as_raw_handle(),
self.layout,
self.device.adapter_ptr(),
fence,
Arc::clone(&self.last_gpu_write),
)
}
}
impl<T> fmt::Debug for D3d11TextureTensor<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("D3d11TextureTensor")
.field("name", &self.name)
.field("shape", &self.shape)
.field("layout", &self.layout)
.field("access", &self.access)
.field("texture", &self.texture.as_raw())
.finish()
}
}
fn publish(device: &D3d11Device) {
unsafe { device.ctx().Flush() };
}
fn upload_buffer_window(
device: &D3d11Device,
texture: &ID3D11Texture2D,
buffer: &HostBuffer,
texture_width: usize,
row_offset: usize,
row_count: usize,
) {
let (base, _) = buffer.span();
let src = unsafe { base.add(row_offset * buffer.row_pitch) };
let region = D3D11_BOX {
left: 0,
top: row_offset as u32,
front: 0,
right: texture_width as u32,
bottom: (row_offset + row_count) as u32,
back: 1,
};
unsafe {
device.ctx().UpdateSubresource(
texture,
0,
Some(®ion),
src.cast(),
buffer.row_pitch as u32,
0,
);
}
}
struct StagingGuard {
device: &'static D3d11Device,
texture: ID3D11Texture2D,
staging: Arc<Staging>,
writer: bool,
}
impl Drop for StagingGuard {
fn drop(&mut self) {
let mut state = self.staging.state();
if !state.release(self.writer) {
return;
}
unsafe { self.device.ctx().Unmap(&self.staging.tex, 0) };
if self.writer {
unsafe {
self.device
.ctx()
.CopyResource(&self.texture, &self.staging.tex)
};
publish(self.device);
}
}
}
struct HostBufferGuard {
device: &'static D3d11Device,
texture: ID3D11Texture2D,
host: Arc<OnceLock<HostBuffer>>,
texture_width: usize,
writeback: Option<(usize, usize)>,
}
impl Drop for HostBufferGuard {
fn drop(&mut self) {
let buffer = self
.host
.get()
.expect("the guard was built from a live host buffer");
let mut state = buffer.state();
if let Some((row_offset, row_count)) = self.writeback {
upload_buffer_window(
self.device,
&self.texture,
buffer,
self.texture_width,
row_offset,
row_count,
);
publish(self.device);
}
state.release(self.writeback.is_some());
}
}
impl<T> TensorTrait<T> for D3d11TextureTensor<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
fn new(_shape: &[usize], _name: Option<&str>) -> Result<Self> {
Err(Error::NotImplemented(
"D3D11 texture tensors are image-formatted; use Tensor::image with \
TensorMemory::DmaBuf"
.to_owned(),
))
}
fn memory(&self) -> TensorMemory {
TensorMemory::DmaBuf
}
fn name(&self) -> String {
self.name.clone()
}
fn shape(&self) -> &[usize] {
&self.shape
}
fn reshape(&mut self, shape: &[usize]) -> Result<()> {
let new_elems: usize = shape.iter().product();
let cur_elems: usize = self.shape.iter().product();
if new_elems != cur_elems {
return Err(Error::InvalidShape(format!(
"reshape: element count mismatch ({cur_elems} -> {new_elems})"
)));
}
self.shape = shape.to_vec();
Ok(())
}
fn map_with<'a>(&self, access: CpuAccess) -> Result<crate::view::HostView<'a, T>>
where
T: 'a,
{
let _span = tracing::trace_span!("tensor.map", memory = "d3d11_texture", ?access).entered();
Ok(self.view_of(self.scoped_pin(access)?, access, None))
}
fn try_map_with<'a>(&self, access: CpuAccess) -> Result<crate::view::HostView<'a, T>>
where
T: 'a,
{
Ok(self.view_of(self.try_scoped_pin(access)?, access, None))
}
fn buffer_identity(&self) -> &BufferIdentity {
&self.identity
}
fn capacity_bytes(&self) -> usize {
self.backing_bytes().saturating_sub(self.view_offset)
}
fn set_logical_shape(&mut self, shape: &[usize]) -> Result<()> {
if shape.is_empty() {
return Err(Error::InvalidSize(0));
}
let needed = shape.iter().product::<usize>() * std::mem::size_of::<T>();
let capacity = self.tight_bytes();
if needed > capacity {
return Err(Error::InsufficientCapacity { needed, capacity });
}
self.shape = shape.to_vec();
Ok(())
}
fn view(&self, offset_bytes: usize, shape: &[usize]) -> Result<Self> {
if !offset_bytes.is_multiple_of(std::mem::align_of::<T>()) {
return Err(Error::InvalidOperation(format!(
"D3d11TextureTensor::view: offset {offset_bytes} not aligned to \
align_of::<T>()={}",
std::mem::align_of::<T>()
)));
}
let abs_offset = self
.view_offset
.checked_add(offset_bytes)
.ok_or(Error::InvalidSize(offset_bytes))?;
let capacity = self.backing_bytes();
if abs_offset > capacity {
return Err(Error::InsufficientCapacity {
needed: abs_offset,
capacity,
});
}
let logical = shape.iter().product::<usize>() * std::mem::size_of::<T>();
let needed = abs_offset
.checked_add(logical)
.ok_or(Error::InvalidSize(logical))?;
if needed > capacity {
return Err(Error::InsufficientCapacity { needed, capacity });
}
Ok(Self {
device: self.device,
texture: self.texture.clone(),
layout: self.layout,
desc_bind_flags: self.desc_bind_flags,
width: self.width,
height: self.height,
format: self.format,
dtype: self.dtype,
shape: shape.to_vec(),
name: self.name.clone(),
access: self.access,
staging: self.staging.clone(),
host: Arc::clone(&self.host),
nt_handle: Arc::clone(&self.nt_handle),
identity: self.identity.clone(),
last_gpu_write: Arc::clone(&self.last_gpu_write),
imported_fence: self.imported_fence.clone(),
wrote_host: Arc::clone(&self.wrote_host),
view_offset: abs_offset,
_marker: PhantomData,
})
}
}
#[cfg(test)]
mod tests_support {
use std::os::windows::io::{FromRawHandle, OwnedHandle};
use windows::core::{Interface, PCWSTR};
use windows::Win32::Foundation::{GENERIC_ALL, HANDLE, HMODULE};
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP};
use windows::Win32::Graphics::Direct3D11::{
D3D11CreateDevice, ID3D11Device, ID3D11Device1, ID3D11Device5, ID3D11DeviceContext,
ID3D11DeviceContext4, ID3D11Fence, ID3D11Texture2D, D3D11_CPU_ACCESS_READ,
D3D11_CREATE_DEVICE_FLAG, D3D11_FENCE_FLAG_SHARED, D3D11_MAPPED_SUBRESOURCE,
D3D11_MAP_READ, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING,
};
use windows::Win32::Graphics::Dxgi::IDXGIAdapter;
pub(super) struct SecondDevice {
pub dev: ID3D11Device,
pub dev1: ID3D11Device1,
pub ctx: ID3D11DeviceContext,
}
pub(super) fn second_device() -> SecondDevice {
let driver = if super::device().unwrap().is_warp() {
D3D_DRIVER_TYPE_WARP
} else {
D3D_DRIVER_TYPE_HARDWARE
};
let mut dev: Option<ID3D11Device> = None;
let mut ctx: Option<ID3D11DeviceContext> = None;
unsafe {
D3D11CreateDevice(
None::<&IDXGIAdapter>,
driver,
HMODULE::default(),
D3D11_CREATE_DEVICE_FLAG(0),
None,
D3D11_SDK_VERSION,
Some(&mut dev),
None,
Some(&mut ctx),
)
}
.expect("second D3D11 device");
let dev = dev.expect("D3D11CreateDevice returned a device");
let dev1 = dev.cast::<ID3D11Device1>().expect("ID3D11Device1");
SecondDevice {
dev,
dev1,
ctx: ctx.expect("D3D11CreateDevice returned a context"),
}
}
pub(super) fn read_through(
other: &SecondDevice,
handle: HANDLE,
w: usize,
h: usize,
bpp: usize,
) -> Vec<u8> {
let tex: ID3D11Texture2D = unsafe { other.dev1.OpenSharedResource1(handle) }
.expect("ID3D11Device1::OpenSharedResource1");
let mut desc = D3D11_TEXTURE2D_DESC::default();
unsafe { tex.GetDesc(&mut desc) };
desc.Usage = D3D11_USAGE_STAGING;
desc.BindFlags = 0;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ.0 as u32;
desc.MiscFlags = 0;
let mut staging: Option<ID3D11Texture2D> = None;
unsafe { other.dev.CreateTexture2D(&desc, None, Some(&mut staging)) }
.expect("staging texture on the second device");
let staging = staging.expect("CreateTexture2D returned a texture");
unsafe { other.ctx.CopyResource(&staging, &tex) };
let mut m = D3D11_MAPPED_SUBRESOURCE::default();
unsafe { other.ctx.Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut m)) }
.expect("map the staging copy");
let row = w * bpp;
let mut out = vec![0u8; row * h];
for r in 0..h {
unsafe {
std::ptr::copy_nonoverlapping(
(m.pData as *const u8).add(r * m.RowPitch as usize),
out.as_mut_ptr().add(r * row),
row,
);
}
}
unsafe { other.ctx.Unmap(&staging, 0) };
out
}
pub(super) fn shared_fence(other: &SecondDevice) -> (ID3D11Fence, OwnedHandle) {
let d5 = other.dev.cast::<ID3D11Device5>().expect("ID3D11Device5");
let mut f: Option<ID3D11Fence> = None;
unsafe { d5.CreateFence(0, D3D11_FENCE_FLAG_SHARED, &mut f) }.expect("CreateFence");
let fence = f.expect("CreateFence returned a fence");
let handle = unsafe { fence.CreateSharedHandle(None, GENERIC_ALL.0, PCWSTR::null()) }
.expect("ID3D11Fence::CreateSharedHandle");
(fence, unsafe { OwnedHandle::from_raw_handle(handle.0) })
}
#[allow(clippy::too_many_arguments)] pub(super) fn write_through(
other: &SecondDevice,
handle: HANDLE,
w: usize,
h: usize,
bpp: usize,
byte: u8,
churn: usize,
fence: &ID3D11Fence,
value: u64,
) {
let tex: ID3D11Texture2D = unsafe { other.dev1.OpenSharedResource1(handle) }
.expect("ID3D11Device1::OpenSharedResource1");
let zeros = vec![0u8; w * bpp * h];
let rows = vec![byte; w * bpp * h];
for source in std::iter::repeat_n(&zeros, churn).chain(std::iter::once(&rows)) {
unsafe {
other.ctx.UpdateSubresource(
&tex,
0,
None,
source.as_ptr().cast(),
(w * bpp) as u32,
0,
)
};
}
let ctx4 = other
.ctx
.cast::<ID3D11DeviceContext4>()
.expect("ID3D11DeviceContext4");
unsafe { ctx4.Signal(fence, value) }.expect("ID3D11DeviceContext4::Signal");
unsafe { other.ctx.Flush() };
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::TensorMapTrait;
use std::time::{Duration, Instant};
use windows::Win32::Foundation::WAIT_OBJECT_0;
use windows::Win32::System::Threading::WaitForSingleObject;
fn rgba(w: usize, h: usize, access: CpuAccess) -> D3d11TextureTensor<u8> {
let shape = PixelFormat::Rgba.allocation_shape(w, h).unwrap();
D3d11TextureTensor::<u8>::new_image(
w,
h,
PixelFormat::Rgba,
DType::U8,
&shape,
None,
access,
)
.unwrap()
}
#[test]
fn a_blocking_map_refreshes_past_an_abandoned_try_maps_copy() {
let d = device().unwrap();
let old = rgba(64, 64, CpuAccess::ReadWrite);
let new = rgba(64, 64, CpuAccess::ReadWrite);
for (t, byte) in [(&old, 0x11u8), (&new, 0x22u8)] {
let mut m = t.map_with(CpuAccess::Write).unwrap();
m.as_mut_slice().fill(byte);
}
{
let st = old.staging.as_ref().unwrap();
let mut state = st.state();
unsafe { d.ctx().CopyResource(&st.tex, &old.texture) };
state.refresh_pending = true;
}
unsafe { d.ctx().CopyResource(&old.texture, &new.texture) };
d.signal();
let mapped = old.map_with(CpuAccess::Read).unwrap();
let pitch = old.image_backing_row_stride().unwrap();
let row_bytes = old.layout().tight_row_bytes();
let bytes = mapped.as_slice();
let logical = |r: usize| &bytes[r * pitch..r * pitch + row_bytes];
let stale = (0..old.layout().texture_height)
.flat_map(|r| logical(r).iter())
.filter(|&&b| b == 0x11)
.count();
assert_eq!(
stale, 0,
"the blocking map returned {stale} logical bytes of the copy queued before the GPU write"
);
for r in 0..old.layout().texture_height {
assert!(
logical(r).iter().all(|&b| b == 0x22),
"row {r} is not the byte the GPU write put there"
);
}
}
#[test]
fn write_only_partial_row_window_does_not_clobber_rows_outside_it() {
let d = device().unwrap();
const W: usize = 64;
const H: usize = 12;
const WINDOW_START_ROW: usize = 4;
const WINDOW_ROWS: usize = 4;
let pool = rgba(W, H, CpuAccess::Write);
{
let mut m = pool.map_with(CpuAccess::Write).unwrap();
m.as_mut_slice().fill(0xAA);
}
{
let offset = WINDOW_START_ROW * pool.backing_pitch();
let shape = vec![WINDOW_ROWS, W, 4];
let window = pool
.view(offset, &shape)
.expect("a CpuAccess::Write tensor's own row-aligned sub-window must be viewable");
let mut m = window
.map_with(CpuAccess::Write)
.expect("a row-aligned partial window must be mappable for write");
m.as_mut_slice().fill(0x55);
}
pool.sync_for_device(CpuAccess::Write).unwrap();
let readback = rgba(W, H, CpuAccess::ReadWrite);
unsafe { d.ctx().CopyResource(&readback.texture, &pool.texture) };
let mapped = readback.map_with(CpuAccess::Read).unwrap();
let pitch = readback.backing_pitch();
let row_bytes = readback.layout().tight_row_bytes();
let bytes = mapped.as_slice();
let logical = |r: usize| &bytes[r * pitch..r * pitch + row_bytes];
for r in 0..H {
let expect = if (WINDOW_START_ROW..WINDOW_START_ROW + WINDOW_ROWS).contains(&r) {
0x55
} else {
0xAA
};
assert!(
logical(r).iter().all(|&b| b == expect),
"row {r}: expected every byte == {expect:#x}, the windowed write touched a row \
outside its own range"
);
}
}
fn rows() -> Vec<(PixelFormat, DType, usize, usize)> {
vec![
(PixelFormat::Rgba, DType::U8, 640, 480),
(PixelFormat::Bgra, DType::U8, 641, 3),
(PixelFormat::Rgb, DType::U8, 640, 480),
(PixelFormat::Grey, DType::U8, 641, 481),
(PixelFormat::Nv12, DType::U8, 640, 480),
(PixelFormat::Nv16, DType::U8, 640, 480),
(PixelFormat::Nv24, DType::U8, 640, 480),
(PixelFormat::Yuyv, DType::U8, 640, 480),
(PixelFormat::PlanarRgb, DType::F16, 640, 480),
(PixelFormat::PlanarRgb, DType::F32, 640, 480),
(PixelFormat::Rgb, DType::F32, 640, 480),
(PixelFormat::Rgba, DType::F16, 640, 480),
]
}
#[test]
fn every_layout_row_allocates_and_round_trips_through_map() {
for (fmt, dt, w, h) in rows() {
let shape = fmt.allocation_shape(w, h).unwrap();
let t = D3d11TextureTensor::<u8>::new_image(
w,
h,
fmt,
dt,
&shape,
None,
CpuAccess::ReadWrite,
)
.unwrap_or_else(|e| panic!("{fmt:?}/{dt:?}: {e}"));
let tight = t.tight_bytes();
let pitch = t
.image_backing_row_stride()
.expect("mappable tensors know their pitch");
assert!(pitch >= t.layout().tight_row_bytes());
let pattern: Vec<u8> = (0..tight).map(|i| (i * 7 + 13) as u8).collect();
{
let mut v = t.map_with(CpuAccess::Write).unwrap();
let rows = t.layout().texture_height;
let row_bytes = t.layout().tight_row_bytes();
let dst = v.as_mut_slice();
for r in 0..rows {
dst[r * pitch..r * pitch + row_bytes]
.copy_from_slice(&pattern[r * row_bytes..(r + 1) * row_bytes]);
}
}
let v = t.map_with(CpuAccess::Read).unwrap();
let rows = t.layout().texture_height;
let row_bytes = t.layout().tight_row_bytes();
for r in 0..rows {
assert_eq!(
&v.as_slice()[r * pitch..r * pitch + row_bytes],
&pattern[r * row_bytes..(r + 1) * row_bytes],
"{fmt:?}/{dt:?} row {r}"
);
}
}
}
fn nv_sizes() -> [(PixelFormat, usize, usize); 4] {
[
(PixelFormat::Nv12, 64, 64),
(PixelFormat::Nv16, 64, 64),
(PixelFormat::Nv24, 64, 64),
(PixelFormat::Nv12, 321, 241),
]
}
fn nv_tensor(
fmt: PixelFormat,
w: usize,
h: usize,
access: CpuAccess,
) -> D3d11TextureTensor<u8> {
let shape = fmt.allocation_shape(w, h).unwrap();
D3d11TextureTensor::<u8>::new_image(w, h, fmt, DType::U8, &shape, None, access)
.unwrap_or_else(|e| panic!("{fmt:?} {w}x{h}: {e}"))
}
#[test]
fn nv_textures_are_as_wide_as_their_pitch() {
for (fmt, w, h) in nv_sizes() {
let t = nv_tensor(fmt, w, h, CpuAccess::ReadWrite);
let tex_w = t.layout().texture_width;
let pitch = t
.image_backing_row_stride()
.expect("a readable tensor has staging");
assert_eq!(tex_w, pitch, "{fmt:?} {w}x{h}: texture width vs map pitch");
assert!(
tex_w.is_multiple_of(2),
"{fmt:?} {w}x{h}: texture width {tex_w} splits a chroma pair"
);
assert!(
tex_w >= w,
"{fmt:?} {w}x{h}: texture width {tex_w} is narrower than the image"
);
let img = crate::Tensor::<u8>::image(
w,
h,
fmt,
Some(crate::TensorMemory::DmaBuf),
CpuAccess::ReadWrite,
)
.unwrap_or_else(|e| panic!("{fmt:?} {w}x{h}: {e}"));
assert_eq!(
img.d3d11_layout().unwrap().texture_width,
tex_w,
"{fmt:?} {w}x{h}: Tensor::image chose another texture width"
);
let natural = w.next_multiple_of(2);
let expected = (tex_w > natural).then_some(tex_w);
assert_eq!(
img.row_stride(),
expected,
"{fmt:?} {w}x{h}: recorded row stride (texture width {tex_w}, natural {natural})"
);
}
}
#[test]
fn nv_textures_with_cpu_access_none_share_the_stride_rule() {
for (fmt, w, h) in nv_sizes() {
let readable = nv_tensor(fmt, w, h, CpuAccess::ReadWrite);
let expected = readable.layout().texture_width;
for access in [CpuAccess::None, CpuAccess::Write] {
let other = nv_tensor(fmt, w, h, access);
assert_eq!(
other.layout().texture_width,
expected,
"{fmt:?} {w}x{h} with {access:?}"
);
assert_eq!(
other.image_backing_row_stride(),
Some(expected),
"{fmt:?} {w}x{h} with {access:?} reports no backing pitch"
);
}
}
}
#[test]
fn a_write_only_nv_tensor_uploads_its_chroma_lines_at_the_texture_pitch() {
let (w, h) = (64usize, 64usize);
let fmt = PixelFormat::Nv24;
let t = crate::Tensor::<u8>::image(
w,
h,
fmt,
Some(crate::TensorMemory::DmaBuf),
CpuAccess::Write,
)
.unwrap();
let stride = t
.effective_row_stride()
.expect("an image tensor knows its stride");
let texture = t.d3d11_texture().expect("a texture-backed tensor");
assert_eq!(
t.row_stride(),
(stride > w).then_some(stride),
"a padded texture records its width as the row stride"
);
let line = 2 * w;
let chroma_at = |i: usize| (h + 2 * i) * stride;
let byte = |i: usize, k: usize| ((i * 5 + k * 3) % 251) as u8;
{
let mut m = t.map_write().unwrap();
let dst = m.as_mut_slice();
for i in 0..h {
let at = chroma_at(i);
for k in 0..line {
dst[at + k] = byte(i, k);
}
}
}
let shape = fmt.allocation_shape(w, h).unwrap();
let r = unsafe {
D3d11TextureTensor::<u8>::from_d3d11_texture(
texture,
w,
h,
fmt,
DType::U8,
&shape,
CpuAccess::Read,
None,
)
}
.unwrap();
assert_eq!(
r.image_backing_row_stride(),
Some(stride),
"the reader must address the same rows as the writer"
);
let m = r.map_with(CpuAccess::Read).unwrap();
let src = m.as_slice();
for i in 0..h {
let at = chroma_at(i);
let expected: Vec<u8> = (0..line).map(|k| byte(i, k)).collect();
assert_eq!(
&src[at..at + line],
expected.as_slice(),
"chroma line {i} did not land at row {} of a {stride}-wide texture",
h + 2 * i
);
}
}
#[test]
fn an_external_semi_planar_texture_is_wrapped_at_its_own_width() {
let backing = nv_tensor(PixelFormat::Nv12, 128, 64, CpuAccess::None);
assert_eq!(backing.layout().texture_width, 128);
let shape = PixelFormat::Nv12.allocation_shape(100, 64).unwrap();
let t = unsafe {
D3d11TextureTensor::<u8>::from_d3d11_texture(
backing.texture_ptr(),
100,
64,
PixelFormat::Nv12,
DType::U8,
&shape,
CpuAccess::Read,
None,
)
}
.unwrap();
assert_eq!(
t.layout().texture_width,
128,
"the host's texture width is the wrapped tensor's stride"
);
}
#[test]
fn an_external_semi_planar_texture_narrower_than_its_pitch_is_refused() {
let (w, h) = (100usize, 64usize);
let fmt = PixelFormat::Nv12;
let d = device().unwrap();
let raw = D3d11ImageLayout {
texture_width: w,
..image_d3d11_layout(fmt, DType::U8, w, h).unwrap()
};
assert_eq!(raw.texture_height, 96, "NV12 64 rows plus 32 chroma rows");
let tex =
create_texture(d, &texture_desc(&raw, D3D11_USAGE_DEFAULT, BIND, 0, MISC)).unwrap();
let pitch = staging_row_pitch(d, &create_staging_texture(d, &raw).unwrap()).unwrap();
let wrapped = unsafe {
crate::Tensor::<u8>::from_d3d11_texture(tex.as_raw(), w, h, fmt, CpuAccess::Read, None)
};
if pitch == w {
let t = wrapped.expect("a texture already at its own pitch is wrappable");
assert_eq!(t.effective_row_stride(), Some(w));
} else {
let err = wrapped.unwrap_err();
assert!(
matches!(err, Error::InvalidArgument(_))
&& err.to_string().contains("driver row pitch"),
"{err}"
);
}
}
#[test]
fn wrapped_nv_tensors_record_the_textures_width_as_their_row_stride() {
for (w, h) in [(321usize, 241usize), (64, 64)] {
let fmt = PixelFormat::Nv12;
let src = nv_tensor(fmt, w, h, CpuAccess::None);
let texture_width = src.layout().texture_width;
let by_ptr = unsafe {
crate::Tensor::<u8>::from_d3d11_texture(
src.texture_ptr(),
w,
h,
fmt,
CpuAccess::Read,
None,
)
}
.unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} by pointer: {e}"));
assert_eq!(
by_ptr.effective_row_stride(),
Some(texture_width),
"{fmt:?} {w}x{h} wrapped by pointer"
);
let handle = src.shared_handle().unwrap();
let by_handle = unsafe {
crate::Tensor::<u8>::from_d3d11_shared_handle(
handle.as_raw_handle(),
w,
h,
fmt,
CpuAccess::Read,
None,
None,
)
}
.unwrap_or_else(|e| panic!("{fmt:?} {w}x{h} by handle: {e}"));
assert_eq!(
by_handle.effective_row_stride(),
Some(texture_width),
"{fmt:?} {w}x{h} wrapped by shared handle"
);
}
}
#[test]
fn an_external_semi_planar_texture_narrower_than_the_image_is_refused() {
let backing = nv_tensor(PixelFormat::Nv12, 128, 64, CpuAccess::None);
let shape = PixelFormat::Nv12.allocation_shape(129, 64).unwrap();
let err = unsafe {
D3d11TextureTensor::<u8>::from_d3d11_texture(
backing.texture_ptr(),
129,
64,
PixelFormat::Nv12,
DType::U8,
&shape,
CpuAccess::Read,
None,
)
}
.unwrap_err();
assert!(matches!(err, Error::InvalidArgument(_)), "{err}");
}
#[test]
fn write_only_uses_the_shadow_and_lands_in_the_texture() {
let shape = PixelFormat::Rgba.allocation_shape(64, 32).unwrap();
let w = rgba(64, 32, CpuAccess::Write);
assert!(
w.image_backing_row_stride().is_none(),
"no staging, tight rows"
);
{
let mut v = w.map_with(CpuAccess::Write).unwrap();
v.as_mut_slice().fill(0xA5);
}
let r = unsafe {
D3d11TextureTensor::<u8>::from_d3d11_texture(
w.texture_ptr(),
64,
32,
PixelFormat::Rgba,
DType::U8,
&shape,
CpuAccess::Read,
None,
)
}
.unwrap();
let v = r.map_with(CpuAccess::Read).unwrap();
assert!(v.as_slice()[..64 * 4].iter().all(|&b| b == 0xA5));
}
#[test]
fn hardware_only_tensors_refuse_maps_and_have_no_staging() {
let t = rgba(64, 32, CpuAccess::None);
assert!(matches!(
t.map_with(CpuAccess::Read).unwrap_err(),
Error::InvalidOperation(_)
));
assert!(t.image_backing_row_stride().is_none());
}
#[test]
fn shared_handle_opens_on_a_second_device_and_reads_the_bytes() {
let t = rgba(64, 32, CpuAccess::ReadWrite);
{
let mut v = t.map_with(CpuAccess::Write).unwrap();
v.as_mut_slice().fill(0x5A);
}
let value = device().unwrap().signal().expect("fence signal");
let event = device().unwrap().event_for(value).unwrap();
let waited = unsafe { WaitForSingleObject(HANDLE(event.as_raw_handle()), 5_000) };
assert_eq!(waited, WAIT_OBJECT_0, "fence value {value} within 5 s");
let h = t.shared_handle().unwrap();
let other = super::tests_support::second_device();
let bytes =
super::tests_support::read_through(&other, HANDLE(h.as_raw_handle()), 64, 32, 4);
assert!(bytes.iter().all(|&b| b == 0x5A));
}
#[test]
fn completion_is_absent_until_recorded_then_carries_the_value() {
let t = rgba(8, 8, CpuAccess::None);
assert!(t.gpu_completion().unwrap().is_none());
let v = device().unwrap().signal().unwrap();
t.set_gpu_write(v);
let c = t.gpu_completion().unwrap().unwrap();
assert_eq!(c.value, v);
}
#[test]
fn a_map_does_not_block_on_a_recorded_write_the_fence_has_passed() {
let t = rgba(16, 16, CpuAccess::ReadWrite);
let v = device().unwrap().signal().unwrap();
let ev = device().unwrap().event_for(v).unwrap();
let w = unsafe { WaitForSingleObject(HANDLE(ev.as_raw_handle()), 5000) };
assert_eq!(w, WAIT_OBJECT_0, "the signalled value completes");
assert!(device().unwrap().completed_value() >= v);
t.set_gpu_write(v);
let started = Instant::now();
let mapped = t.map_with(CpuAccess::Read).unwrap();
let elapsed = started.elapsed();
drop(mapped);
assert!(
elapsed < Duration::from_secs(2),
"mapping over an already-complete recorded write took {elapsed:?}"
);
}
#[test]
fn a_map_sees_a_second_device_write_the_recorded_completion_covers() {
assert!(
device().unwrap().fence_is_shared(),
"this host's copy signals the process's shared fence, so the map \
path's wait is live rather than skipped"
);
const W: usize = 512;
const H: usize = 512;
let shape = PixelFormat::Rgba.allocation_shape(W, H).unwrap();
let t = rgba(W, H, CpuAccess::ReadWrite);
let h = t.shared_handle().unwrap();
let other = super::tests_support::second_device();
let (fence, fence_handle) = super::tests_support::shared_fence(&other);
super::tests_support::write_through(
&other,
HANDLE(h.as_raw_handle()),
W,
H,
4,
0x5A,
16,
&fence,
1,
);
let imported = unsafe {
D3d11TextureTensor::<u8>::from_d3d11_shared_handle(
h.as_raw_handle(),
W,
H,
PixelFormat::Rgba,
DType::U8,
&shape,
CpuAccess::Read,
Some((fence_handle.as_raw_handle(), 1)),
Some("second-device producer"),
)
}
.unwrap();
let mapped = imported.map_with(CpuAccess::Read).unwrap();
let pitch = imported.image_backing_row_stride().unwrap();
let row_bytes = imported.layout().tight_row_bytes();
for r in 0..imported.layout().texture_height {
assert!(
mapped.as_slice()[r * pitch..r * pitch + row_bytes]
.iter()
.all(|&b| b == 0x5A),
"row {r} of the second device's write"
);
}
}
#[test]
fn a_recorded_write_above_the_counter_does_not_block_the_next_map() {
let t = rgba(32, 32, CpuAccess::ReadWrite);
{
let mut v = t.map_with(CpuAccess::Write).unwrap();
v.as_mut_slice().fill(0x77);
}
let unreachable = device().unwrap().last_signalled() + 1_000_000;
t.set_gpu_write(unreachable);
let deadline = Instant::now() + Duration::from_secs(5);
let mapped = loop {
match t.try_map_with(CpuAccess::Read) {
Ok(v) => break v,
Err(Error::IoError(e)) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("try_map_with returned an unexpected error: {e}"),
}
assert!(
Instant::now() < deadline,
"a recorded write of {unreachable}, above the counter's {}, blocked \
the map, so a wait was queued on a value nothing will ever signal",
device().unwrap().last_signalled()
);
std::thread::yield_now();
};
let row_bytes = t.layout().tight_row_bytes();
assert!(
mapped.as_slice()[..row_bytes].iter().all(|&b| b == 0x77),
"the map still reads the texture, it just does not wait on the value"
);
drop(mapped);
assert_eq!(t.last_gpu_write(), unreachable);
}
#[test]
fn from_shared_handle_round_trips_and_honours_a_completion() {
let shape = PixelFormat::Rgba.allocation_shape(16, 16).unwrap();
let src = rgba(16, 16, CpuAccess::ReadWrite);
{
let mut v = src.map_with(CpuAccess::Write).unwrap();
v.as_mut_slice().fill(7);
}
let v = device().unwrap().signal().unwrap();
src.set_gpu_write(v);
let h = src.shared_handle().unwrap();
let c = src.gpu_completion().unwrap().unwrap();
let dst = unsafe {
D3d11TextureTensor::<u8>::from_d3d11_shared_handle(
h.as_raw_handle(),
16,
16,
PixelFormat::Rgba,
DType::U8,
&shape,
CpuAccess::Read,
Some((c.fence.as_raw_handle(), c.value)),
None,
)
}
.unwrap();
assert!(
dst.last_gpu_write() > v,
"the import recorded {} on the local timeline, past the producer's {v}",
dst.last_gpu_write()
);
let ev = device().unwrap().event_for(dst.last_gpu_write()).unwrap();
let w = unsafe { WaitForSingleObject(HANDLE(ev.as_raw_handle()), 5000) };
assert_eq!(w, WAIT_OBJECT_0, "the recorded value is reachable");
let mapped = dst.map_with(CpuAccess::Read).unwrap();
let pitch = dst.image_backing_row_stride().unwrap();
let row_bytes = dst.layout().tight_row_bytes();
for r in 0..dst.layout().texture_height {
assert!(
mapped.as_slice()[r * pitch..r * pitch + row_bytes]
.iter()
.all(|&b| b == 7),
"row {r} of the shared-handle round trip"
);
}
}
#[test]
fn external_texture_on_a_mismatched_description_is_refused() {
let shape = PixelFormat::Rgba.allocation_shape(16, 16).unwrap();
let t = rgba(16, 16, CpuAccess::None);
let err = unsafe {
D3d11TextureTensor::<u8>::from_d3d11_texture(
t.texture_ptr(),
32,
16,
PixelFormat::Rgba,
DType::U8,
&shape,
CpuAccess::None,
None,
)
}
.unwrap_err();
assert!(matches!(err, crate::Error::InvalidArgument(_)));
}
#[test]
fn second_reader_shares_the_map_and_a_writer_is_refused_while_mapped() {
let t = rgba(16, 16, CpuAccess::ReadWrite);
{
let mut v = t.map_with(CpuAccess::Write).unwrap();
v.as_mut_slice().fill(0x3C);
}
let row = t.layout().tight_row_bytes();
let a = t.map_with(CpuAccess::Read).unwrap();
let b = t.map_with(CpuAccess::Read).unwrap();
assert_eq!(a.as_slice().as_ptr(), b.as_slice().as_ptr());
assert_eq!(a.as_slice()[..row], b.as_slice()[..row]);
assert!(a.as_slice()[..row].iter().all(|&x| x == 0x3C));
assert!(matches!(
t.map_with(CpuAccess::Write).unwrap_err(),
Error::InvalidOperation(_)
));
drop(a);
assert!(
matches!(
t.map_with(CpuAccess::Write).unwrap_err(),
Error::InvalidOperation(_)
),
"the second reader still holds the mapping"
);
drop(b);
let mut w = t.map_with(CpuAccess::Write).unwrap();
w.as_mut_slice()[..row].fill(0x4D);
drop(w);
assert!(t.map_with(CpuAccess::Read).unwrap().as_slice()[..row]
.iter()
.all(|&x| x == 0x4D));
}
#[test]
fn a_view_shares_the_map_state_with_its_parent() {
let shape = PixelFormat::Rgba.allocation_shape(16, 16).unwrap();
let t = rgba(16, 16, CpuAccess::ReadWrite);
let sub = t.view(0, &shape).unwrap();
let row = t.layout().tight_row_bytes();
let a = t.map_with(CpuAccess::Read).unwrap();
let b = sub.map_with(CpuAccess::Read).unwrap();
assert_eq!(a.as_slice().as_ptr(), b.as_slice().as_ptr());
assert!(matches!(
sub.map_with(CpuAccess::Write).unwrap_err(),
Error::InvalidOperation(_)
));
assert!(matches!(
t.map_with(CpuAccess::Write).unwrap_err(),
Error::InvalidOperation(_)
));
drop(a);
drop(b);
let mut w = sub.map_with(CpuAccess::Write).unwrap();
w.as_mut_slice()[..row].fill(0x71);
drop(w);
assert!(t.map_with(CpuAccess::Read).unwrap().as_slice()[..row]
.iter()
.all(|&x| x == 0x71));
}
#[test]
fn host_pin_keeps_one_address_across_syncs() {
let t = rgba(16, 16, CpuAccess::ReadWrite);
let row = t.layout().tight_row_bytes();
let pin = t.host_pin(CpuAccess::ReadWrite).unwrap();
let address = pin.as_ptr();
{
let mut v = t.map_with(CpuAccess::Write).unwrap();
v.as_mut_slice().fill(0x5E);
}
t.sync_for_cpu(CpuAccess::Read).unwrap();
assert_eq!(pin.as_ptr(), address, "a pin's address survives a sync");
assert!(unsafe { pin.as_slice() }[..row].iter().all(|&b| b == 0x5E));
unsafe { std::ptr::write_bytes(pin.as_mut_ptr(), 0x27, pin.len()) };
t.sync_for_device(CpuAccess::Write).unwrap();
assert_eq!(pin.as_ptr(), address, "and survives an upload");
let v = t.map_with(CpuAccess::Read).unwrap();
assert!(v.as_slice()[..row].iter().all(|&b| b == 0x27));
}
#[test]
fn a_map_and_a_pin_agree_row_for_row_at_the_backing_pitch() {
let w = 64;
let h = 48;
let shape = PixelFormat::Rgb.allocation_shape(w, h).unwrap();
let t = D3d11TextureTensor::<u8>::new_image(
w,
h,
PixelFormat::Rgb,
DType::U8,
&shape,
None,
CpuAccess::ReadWrite,
)
.unwrap();
let pitch = t
.image_backing_row_stride()
.expect("a readable tensor has staging");
let row_bytes = t.layout().tight_row_bytes();
assert!(
pitch >= row_bytes,
"the staging pitch must cover a whole row"
);
{
let mut v = t.map_with(CpuAccess::Write).unwrap();
let buf = v.as_mut_slice();
assert_eq!(buf.len(), pitch * h, "a map covers the pitched rows");
for y in 0..h {
buf[y * pitch..y * pitch + row_bytes].fill(y as u8);
}
}
let pin = t.host_pin(CpuAccess::ReadWrite).unwrap();
t.sync_for_cpu(CpuAccess::Read).unwrap();
let bytes = unsafe { pin.as_slice() };
for y in 0..h {
assert!(
bytes[y * pitch..y * pitch + row_bytes]
.iter()
.all(|&b| b == y as u8),
"row {y} of the pin is not the row the map wrote at pitch {pitch}"
);
}
}
#[test]
fn a_host_pin_is_64_byte_aligned() {
for (w, h) in [(64usize, 64usize), (37, 5), (1, 1)] {
let t = rgba(w, h, CpuAccess::ReadWrite);
let pin = t.host_pin(CpuAccess::ReadWrite).unwrap();
assert!(
pin.alignment() >= 64,
"{w}x{h}: pin is {}-byte aligned",
pin.alignment()
);
}
}
#[test]
fn a_map_covers_this_tensors_rows_not_the_parents_tail() {
let t = rgba(32, 8, CpuAccess::ReadWrite);
let pitch = t.image_backing_row_stride().unwrap();
let whole = t.map_with(CpuAccess::Read).unwrap();
assert_eq!(whole.as_slice().len(), pitch * 8);
drop(whole);
let v = t.view(pitch * 2, &[2, 32, 4]).unwrap();
let mapped = v.map_with(CpuAccess::Read).unwrap();
assert_eq!(mapped.as_slice().len(), pitch * 2);
drop(mapped);
assert!(matches!(
v.map_with_byte_size(pitch * 7, CpuAccess::Read),
Err(Error::InsufficientCapacity { .. })
));
}
#[test]
fn try_map_eventually_succeeds_and_returns_the_written_bytes() {
let t = rgba(64, 64, CpuAccess::ReadWrite);
let row = t.layout().tight_row_bytes();
{
let mut v = t.map_with(CpuAccess::Write).unwrap();
v.as_mut_slice().fill(0x6B);
}
match t.try_map_with(CpuAccess::Read) {
Ok(_) => {}
Err(Error::IoError(e)) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("first try_map_with returned an unexpected error: {e}"),
}
let deadline = Instant::now() + Duration::from_secs(2);
let mut attempts = 0usize;
let mapped = loop {
attempts += 1;
match t.try_map_with(CpuAccess::Read) {
Ok(v) => break v,
Err(Error::IoError(e)) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("try_map_with returned an unexpected error: {e}"),
}
assert!(
Instant::now() < deadline,
"try_map_with never succeeded within 2s ({attempts} attempts)"
);
std::thread::yield_now();
};
assert!(mapped.as_slice()[..row].iter().all(|&b| b == 0x6B));
}
#[test]
fn read_map_of_a_write_only_tensor_does_not_upload() {
let shape = PixelFormat::Rgba.allocation_shape(64, 32).unwrap();
let w = rgba(64, 32, CpuAccess::Write);
{
let mut v = w.map_with(CpuAccess::Write).unwrap();
v.as_mut_slice().fill(0xA5);
}
let r = unsafe {
D3d11TextureTensor::<u8>::from_d3d11_texture(
w.texture_ptr(),
64,
32,
PixelFormat::Rgba,
DType::U8,
&shape,
CpuAccess::ReadWrite,
None,
)
}
.unwrap();
let pitch = r.image_backing_row_stride().unwrap();
let row = r.layout().tight_row_bytes();
assert!(r.map_with(CpuAccess::Read).unwrap().as_slice()[..row]
.iter()
.all(|&b| b == 0xA5));
drop(w.map_with(CpuAccess::Read).unwrap());
{
let mut v = r.map_with(CpuAccess::Write).unwrap();
let dst = v.as_mut_slice();
for n in 0..32 {
dst[n * pitch..n * pitch + row].fill(0x11);
}
}
drop(w.map_with(CpuAccess::Read).unwrap());
assert!(
r.map_with(CpuAccess::Read).unwrap().as_slice()[..row]
.iter()
.all(|&b| b == 0x11),
"a read map of a write-only tensor overwrote the texture"
);
}
#[test]
fn view_at_a_row_offset_maps_the_right_bytes() {
let t = rgba(16, 16, CpuAccess::ReadWrite);
let pitch = t.image_backing_row_stride().unwrap();
let row = t.layout().tight_row_bytes();
{
let mut v = t.map_with(CpuAccess::Write).unwrap();
let dst = v.as_mut_slice();
for n in 0..16 {
dst[n * pitch..n * pitch + row].fill(0xB0 + n as u8);
}
}
let sub = t.view(3 * pitch, &[13, 16, 4]).unwrap();
assert_eq!(sub.capacity_bytes(), 16 * pitch - 3 * pitch);
let v = sub.map_with(CpuAccess::Read).unwrap();
for n in 0..13 {
assert!(
v.as_slice()[n * pitch..n * pitch + row]
.iter()
.all(|&b| b == 0xB0 + (n + 3) as u8),
"view row {n} should be texture row {}",
n + 3
);
}
}
}