dma-api 0.8.0

Trait for DMA alloc and some collections
Documentation
#![cfg(all(test, any(unix, windows)))]

mod test_helpers;

use dma_api::*;
use test_helpers::{DmaOperation, TrackingDmaOp};

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(C)]
struct Descriptor {
    addr: u64,
    len: u32,
    flags: u32,
}

fn new_tracking_device() -> (DeviceDma, &'static TrackingDmaOp) {
    let tracker = Box::new(TrackingDmaOp::new());
    let tracker = Box::leak(tracker);
    (DeviceDma::new(u64::MAX, tracker), tracker)
}

#[test]
fn coherent_array_access_does_not_sync_cache() {
    let (dev, tracker) = new_tracking_device();
    let mut ring = dev
        .coherent_array_zero_with_align::<Descriptor>(8, 64)
        .unwrap();

    tracker.clear();
    ring.set_cpu(
        0,
        Descriptor {
            addr: 0x1000,
            len: 16,
            flags: 1,
        },
    );
    assert_eq!(ring.read_cpu(0).unwrap().addr, 0x1000);

    assert_eq!(tracker.count_sync_alloc_for_device(), 0);
    assert_eq!(tracker.count_sync_alloc_for_cpu(), 0);
}

#[test]
fn contiguous_array_cpu_accessors_do_not_sync_cache() {
    let (dev, tracker) = new_tracking_device();
    let mut buff = dev
        .contiguous_array_zero_with_align::<u32>(16, 64, DmaDirection::ToDevice)
        .unwrap();

    tracker.clear();
    buff.set_cpu(3, 0xA5A5_A5A5);
    assert_eq!(tracker.count_sync_alloc_for_device(), 0);
    assert_eq!(tracker.count_sync_alloc_for_cpu(), 0);

    buff.sync_for_device(3 * size_of::<u32>(), size_of::<u32>());
    assert_eq!(tracker.count_sync_alloc_for_device(), 1);
    assert_eq!(tracker.count_sync_alloc_for_cpu(), 0);

    let ops = tracker.operations();
    assert!(ops.iter().any(|op| matches!(
        op,
        DmaOperation::SyncAllocForDevice {
            size: 4,
            direction: DmaDirection::ToDevice,
            ..
        }
    )));
}

#[test]
fn contiguous_box_supports_cpu_sync() {
    let (dev, tracker) = new_tracking_device();
    let mut status = dev
        .contiguous_box_zero_with_align::<Descriptor>(64, DmaDirection::FromDevice)
        .unwrap();

    tracker.clear();
    status.write_cpu(Descriptor {
        addr: 0,
        len: 0,
        flags: 0,
    });
    status.sync_for_cpu_all();

    assert_eq!(tracker.count_sync_alloc_for_device(), 0);
    assert_eq!(tracker.count_sync_alloc_for_cpu(), 1);
}

#[test]
fn streaming_map_has_explicit_device_and_cpu_sync() {
    let tracker = Box::new(TrackingDmaOp::new().with_next_dma_addr(0x4000));
    let tracker = Box::leak(tracker);
    let dev = DeviceDma::new(u64::MAX, tracker);
    let mut backing = [0u8; 128];
    let map = dev
        .map_streaming_slice(&mut backing, 64, DmaDirection::Bidirectional)
        .unwrap();

    tracker.clear();
    map.prepare_for_device_all();
    map.complete_for_cpu_all();
    drop(map);

    assert_eq!(tracker.count_sync_map_for_device(), 1);
    assert_eq!(tracker.count_sync_map_for_cpu(), 1);
    assert!(
        tracker
            .operations()
            .iter()
            .any(|op| matches!(op, DmaOperation::UnmapStreaming { size: 128 }))
    );
}

#[test]
fn streaming_map_for_device_syncs_after_mapping() {
    let tracker = Box::new(TrackingDmaOp::new().with_next_dma_addr(0x4000));
    let tracker = Box::leak(tracker);
    let dev = DeviceDma::new(u64::MAX, tracker);
    let mut backing = [0u8; 128];

    tracker.clear();
    let map = dev
        .map_streaming_slice_for_device(&mut backing, 64, DmaDirection::FromDevice)
        .unwrap();

    assert_eq!(tracker.count_sync_map_for_device(), 1);
    assert_eq!(tracker.count_sync_map_for_cpu(), 0);
    assert!(tracker.operations().iter().any(|op| matches!(
        op,
        DmaOperation::SyncMapForDevice {
            size: 128,
            direction: DmaDirection::FromDevice,
            ..
        }
    )));
    drop(map);
}

#[test]
fn streaming_write_for_device_syncs_after_cpu_write() {
    let tracker = Box::new(TrackingDmaOp::new().with_next_dma_addr(0x4000));
    let tracker = Box::leak(tracker);
    let dev = DeviceDma::new(u64::MAX, tracker);
    let mut backing = [0u8; 16];
    let mut map = dev
        .map_streaming_slice(&mut backing, 4, DmaDirection::ToDevice)
        .unwrap();

    tracker.clear();
    map.write_for_device(4, |data| data.copy_from_slice(&[1, 2, 3, 4]));

    assert_eq!(map.read_cpu(0), Some(1));
    assert_eq!(tracker.count_sync_map_for_device(), 1);
    assert!(tracker.operations().iter().any(|op| matches!(
        op,
        DmaOperation::SyncMapForDevice {
            size: 4,
            direction: DmaDirection::ToDevice,
            ..
        }
    )));
}

#[test]
fn streaming_read_from_device_syncs_before_cpu_read_and_copies_bounce_buffer() {
    let tracker = Box::new(TrackingDmaOp::new().with_next_dma_addr(0x80));
    let tracker = Box::leak(tracker);
    let dev = DeviceDma::new(0xff, tracker);
    let mut backing = [1u8; 16];
    let map = dev
        .map_streaming_slice(&mut backing, 16, DmaDirection::FromDevice)
        .unwrap();

    assert!(map.bounce_ptr().is_some());
    unsafe {
        map.bounce_ptr()
            .unwrap()
            .as_ptr()
            .write_bytes(0x5a, backing.len());
    }

    tracker.clear();
    let first = map.read_from_device(4, |data| data[0]);

    assert_eq!(first, 0x5a);
    assert_eq!(backing[0], 0x5a);
    assert_eq!(tracker.count_sync_map_for_cpu(), 1);
    assert!(tracker.operations().iter().any(|op| matches!(
        op,
        DmaOperation::SyncMapForCpu {
            size: 4,
            direction: DmaDirection::FromDevice,
            ..
        }
    )));
}

#[test]
fn streaming_bounce_buffer_copies_back_on_cpu_sync() {
    let tracker = Box::new(TrackingDmaOp::new().with_next_dma_addr(0x80));
    let tracker = Box::leak(tracker);
    let dev = DeviceDma::new(0xff, tracker);
    let mut backing = [1u8; 16];
    let map = dev
        .map_streaming_slice(&mut backing, 16, DmaDirection::FromDevice)
        .unwrap();

    assert!(map.bounce_ptr().is_some());
    unsafe {
        map.bounce_ptr()
            .unwrap()
            .as_ptr()
            .write_bytes(0x5a, backing.len());
    }
    map.complete_for_cpu_all();
    drop(map);

    assert_eq!(backing, [0x5a; 16]);
}

#[test]
fn contiguous_array_high_level_accessors_sync_expected_ranges() {
    let (dev, tracker) = new_tracking_device();
    let mut tx = dev
        .contiguous_array_zero_with_align::<u8>(16, 64, DmaDirection::ToDevice)
        .unwrap();

    tracker.clear();
    tx.write_for_device(4, |data| data.copy_from_slice(&[1, 2, 3, 4]));
    assert_eq!(tracker.count_sync_alloc_for_device(), 1);
    assert!(tracker.operations().iter().any(|op| matches!(
        op,
        DmaOperation::SyncAllocForDevice {
            size: 4,
            direction: DmaDirection::ToDevice,
            ..
        }
    )));

    let mut rx = dev
        .contiguous_array_zero_with_align::<u8>(16, 64, DmaDirection::FromDevice)
        .unwrap();
    rx.copy_from_slice_cpu(&[5, 6, 7, 8]);
    tracker.clear();
    let mut out = [0u8; 4];
    rx.copy_from_device_to_slice(&mut out);
    assert_eq!(out, [5, 6, 7, 8]);
    assert_eq!(tracker.count_sync_alloc_for_cpu(), 1);
    assert!(tracker.operations().iter().any(|op| matches!(
        op,
        DmaOperation::SyncAllocForCpu {
            size: 4,
            direction: DmaDirection::FromDevice,
            ..
        }
    )));
}

#[test]
fn contiguous_box_high_level_accessors_sync_for_device_and_cpu() {
    let (dev, tracker) = new_tracking_device();
    let mut tx = dev
        .contiguous_box_zero_with_align::<Descriptor>(64, DmaDirection::ToDevice)
        .unwrap();

    tracker.clear();
    tx.write_for_device(Descriptor {
        addr: 0x1000,
        len: 64,
        flags: 1,
    });
    assert_eq!(tracker.count_sync_alloc_for_device(), 1);

    let mut rx = dev
        .contiguous_box_zero_with_align::<Descriptor>(64, DmaDirection::FromDevice)
        .unwrap();
    rx.write_cpu(Descriptor {
        addr: 0x2000,
        len: 128,
        flags: 2,
    });

    tracker.clear();
    let value = rx.read_from_device();
    assert_eq!(value.addr, 0x2000);
    assert_eq!(tracker.count_sync_alloc_for_cpu(), 1);
}

#[test]
fn allocation_rejects_backend_address_outside_mask() {
    let (dev, tracker) = new_tracking_device();
    tracker.force_next_dma_addr(0x1_0000_0000);
    let result = dev
        .with_constraints(DmaConstraints::new(u32::MAX as u64))
        .coherent_array_zero_with_align::<u8>(4096, 4096);

    assert!(matches!(result, Err(DmaError::DmaMaskNotMatch { .. })));
}

#[test]
fn low_32bit_allocations_are_validated() {
    let tracker = Box::new(TrackingDmaOp::new().with_next_dma_addr(0xffff_f000));
    let tracker = Box::leak(tracker);
    let dev = DeviceDma::new(u32::MAX as u64, tracker);
    let buff = dev
        .contiguous_array_zero_with_align::<u8>(0x1000, 0x1000, DmaDirection::ToDevice)
        .unwrap();

    assert!(buff.dma_addr().as_u64() <= u32::MAX as u64);
    assert!(tracker.operations().iter().any(|op| matches!(
        op,
        DmaOperation::AllocContiguous {
            mask,
            ..
        } if *mask == u32::MAX as u64
    )));
}

#[test]
fn streaming_map_rejects_backend_address_outside_mask() {
    let (dev, tracker) = new_tracking_device();
    let mut backing = [0u8; 128];
    tracker.force_next_dma_addr(0x1_0000_0000);

    let result = dev
        .with_constraints(DmaConstraints::new(u32::MAX as u64))
        .map_streaming_slice(&mut backing, 64, DmaDirection::FromDevice);

    assert!(matches!(result, Err(DmaError::DmaMaskNotMatch { .. })));
}

#[test]
fn pool_reuses_contiguous_buffers_without_implicit_zeroing() {
    let (dev, tracker) = new_tracking_device();
    let pool = dev.contiguous_buffer_pool(
        core::alloc::Layout::from_size_align(64, 64).unwrap(),
        DmaDirection::ToDevice,
        1,
    );

    {
        let mut buff = pool.alloc().unwrap();
        unsafe {
            buff.as_mut_slice_cpu()[0] = 0x7e;
        }
    }

    tracker.clear();
    let buff = pool.alloc().unwrap();
    assert_eq!(buff.as_slice_cpu()[0], 0x7e);
    assert_eq!(tracker.count_sync_alloc_for_device(), 0);
    assert_eq!(tracker.count_sync_alloc_for_cpu(), 0);
}