Skip to main content

rlvgl_platform/hwcore/
dca.rs

1//! DMA Cacheable Buffers — typestate ownership for cache-coherent DMA.
2//!
3//! Lands the contract ratified in
4//! [`docs/concepts/DCB-00-CONCEPTS.md`](../../../docs/concepts/DCB-00-CONCEPTS.md):
5//! D-cache maintenance for DMA buffers is a property of the type system
6//! rather than a property of the call site. Each [`DcaBuf`] is in exactly
7//! one of {[`Cpu`], [`DeviceRead`], [`DeviceWrite`], [`CircRead`],
8//! [`CircWrite`]} at a time, and the typestate transitions emit the
9//! correct cache op (clean before device-read, invalidate before
10//! CPU-read-after-device-write) automatically.
11//!
12//! ## Single-master scope
13//!
14//! Per DCB-00 §7 (non-goal): a `DcaBuf` has at most one DMA master at a
15//! time. Multi-master coherency and cross-core (CM7↔CM4) sharing live
16//! outside this contract — DAA-03 §7 / INV-D14 governs cross-core
17//! regions, which live in D3 SRAM4 and are non-cacheable from CM7.
18//!
19//! ## Cache-controller plumbing
20//!
21//! [`DcaCache`] abstracts the SCB primitives so host tests can run the
22//! typestate round-trip without a real Cortex-M present. On target,
23//! [`cortex_m::peripheral::SCB`] is the concrete implementer; the
24//! `DcaCache` impl is the **only** site in `rlvgl-platform` that may
25//! call `clean_dcache_by_*` / `invalidate_dcache_by_*` on the SCB
26//! (DCB-00 §9 INV-D8: scanner rule `raw_dcache` whitelists this module).
27//!
28//! ## INV-D13 — SCB ownership consolidation
29//!
30//! New code MUST NOT take a `&mut SCB` borrow outside of constructing a
31//! [`DcaCacheCtx`] or a DCB-owning engine driver. The follow-on scanner
32//! rule `raw_scb_for_cache` referenced in DCB-00 §9 INV-D13 is deferred
33//! to a separate phase; in DCB-01 the convention is enforced by review.
34//!
35//! ## Status
36//!
37//! DCB-01 lands the typestate API + the `raw_dcache` scanner rule with
38//! a starting `BASELINE` covering the three pre-existing manual cache
39//! sites (DCB-00 §4 / §9 INV-D10). DCB-02..DCB-02c retrofit those
40//! sites onto [`DcaBuf`] and shrink `BASELINE` to empty.
41
42use core::cell::UnsafeCell;
43use core::marker::PhantomData;
44use core::mem::size_of;
45
46use crate::hwcore::addr::{DmaAddr, PhysAddr};
47
48// ── Constants ──────────────────────────────────────────────────────────
49
50/// Cortex-M7 D-cache line size, in bytes.
51///
52/// Architectural per ARMv7-M ARM. DCB enforces alignment and padding to
53/// this granule via [`DcaBuf`]'s `#[repr(C, align(32))]` and a
54/// const-time size assertion.
55pub const CACHE_LINE: usize = 32;
56
57// ── Direction markers ──────────────────────────────────────────────────
58
59/// Direction marker for circular DMA: device reads RAM (CPU is the
60/// producer; e.g. SAI1 TX, DMA2D source).
61pub enum Read {}
62
63/// Direction marker for circular DMA: device writes RAM (CPU is the
64/// consumer; e.g. SAI1 RX, ADC stream).
65pub enum Write {}
66
67// ── DcaCache trait + DcaCacheCtx ───────────────────────────────────────
68
69/// Cache-controller abstraction.
70///
71/// On target, the `cortex-m` feature provides an implementation for
72/// [`cortex_m::peripheral::SCB`] (the only site in `rlvgl-platform`
73/// allowed to call the raw SCB cache APIs — DCB-00 §9 INV-D8).
74///
75/// On host, [`NullCache`] is a no-op implementer suitable for typestate
76/// round-trip tests where the DMA never actually runs.
77pub trait DcaCache {
78    /// Clean (write-back) cache lines covering `[addr, addr + len)`.
79    fn clean(&mut self, addr: usize, len: usize);
80
81    /// Invalidate cache lines covering `[addr, addr + len)`.
82    fn invalidate(&mut self, addr: usize, len: usize);
83
84    /// Clean+invalidate cache lines covering `[addr, addr + len)`.
85    ///
86    /// DCB-00 §6 INV-D5 documents this as idempotent over an aligned,
87    /// padded extent. DCB itself does not insert clean+invalidate at
88    /// any §5 transition (the directional ops are sufficient under
89    /// INV-D3 line-sharing prohibition); this method is exposed for
90    /// engine drivers that need bidirectional handoff for unrelated
91    /// reasons.
92    fn clean_invalidate(&mut self, addr: usize, len: usize);
93
94    /// Emit a memory barrier sufficient to drain pending writes from
95    /// the architectural write buffer (the AXI write buffer on
96    /// Cortex-M7; the equivalent on other targets).
97    ///
98    /// Required by DCB-00 §6 INV-D16: a `clean` alone does not flush
99    /// the AXI write buffer on STM32H7 + Write-Through SDRAM; pixel
100    /// writes can sit in the buffer past the cache write-back. The
101    /// `DeviceLtdcScan<T, N>` typestate's `present()` /
102    /// `start_ltdc_scan` / `stop_scan` boundaries call this *in
103    /// addition to* the `clean` to enforce ordering for
104    /// continuous-read consumers (LTDC, DCMI display-out, parallel
105    /// RGB).
106    ///
107    /// Routing the DSB through this trait keeps raw
108    /// `cortex_m::asm::dsb()` (or equivalent) call sites contained
109    /// to DCB's owning module — preserving the discipline scanner's
110    /// containment guarantee for the architecturally-mandated
111    /// barrier intrinsic.
112    fn barrier(&mut self);
113}
114
115/// No-op [`DcaCache`] for host tests.
116///
117/// Tracks last-call metadata so tests can assert the right op was
118/// emitted at the right transition without a real cache.
119#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
120pub struct NullCache {
121    /// Sequence-numbered last operation, useful in unit tests.
122    pub last: Option<NullCacheOp>,
123}
124
125/// Recorded operation kind for [`NullCache`] assertions.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum NullCacheOp {
128    /// `clean(addr, len)` was called.
129    Clean(usize, usize),
130    /// `invalidate(addr, len)` was called.
131    Invalidate(usize, usize),
132    /// `clean_invalidate(addr, len)` was called.
133    CleanInvalidate(usize, usize),
134    /// `barrier()` was called.
135    Barrier,
136}
137
138impl DcaCache for NullCache {
139    fn clean(&mut self, addr: usize, len: usize) {
140        self.last = Some(NullCacheOp::Clean(addr, len));
141    }
142    fn invalidate(&mut self, addr: usize, len: usize) {
143        self.last = Some(NullCacheOp::Invalidate(addr, len));
144    }
145    fn clean_invalidate(&mut self, addr: usize, len: usize) {
146        self.last = Some(NullCacheOp::CleanInvalidate(addr, len));
147    }
148    fn barrier(&mut self) {
149        self.last = Some(NullCacheOp::Barrier);
150    }
151}
152
153#[cfg(feature = "cortex-m")]
154impl DcaCache for cortex_m::peripheral::SCB {
155    fn clean(&mut self, addr: usize, len: usize) {
156        // SAFETY: caller (DCB typestate transition) has established that
157        // `[addr, addr+len)` is the extent of a `DcaBuf` whose alignment
158        // and padding satisfy INV-D1 / INV-D2 — i.e. addr is cache-line
159        // aligned and len is a multiple of CACHE_LINE. The DcaBuf is
160        // owned by a single typestate handle, so no concurrent CPU
161        // access can race the clean.
162        unsafe {
163            cortex_m::peripheral::SCB::clean_dcache_by_address(self, addr, len);
164        }
165    }
166    fn invalidate(&mut self, addr: usize, len: usize) {
167        // SAFETY: as above; INV-D3 forbids cache-line sharing so this
168        // invalidate cannot discard live data belonging to an adjacent
169        // owner.
170        unsafe {
171            cortex_m::peripheral::SCB::invalidate_dcache_by_address(self, addr, len);
172        }
173    }
174    fn clean_invalidate(&mut self, addr: usize, len: usize) {
175        // SAFETY: as above.
176        unsafe {
177            cortex_m::peripheral::SCB::clean_invalidate_dcache_by_address(self, addr, len);
178        }
179    }
180    fn barrier(&mut self) {
181        // The architecturally-mandated DSB barrier; on Cortex-M7 this
182        // drains the AXI write buffer (the load-bearing primitive
183        // for `DeviceLtdcScan<T, N>::present()` per DCB-00 §6
184        // INV-D16). Routing through this method (rather than
185        // letting consumers call `cortex_m::asm::dsb()` directly)
186        // keeps the discipline scanner's `compiler_fence` /
187        // barrier-intrinsic containment intact — the raw
188        // intrinsic stays inside `hwcore::dca`.
189        cortex_m::asm::dsb();
190    }
191}
192
193/// Owning wrapper around a [`DcaCache`].
194///
195/// Per DCB-00 §9 INV-D13, the canonical place to plumb a `&mut SCB`
196/// into the platform crate. Engine drivers (e.g. `AudioPlayer`,
197/// `SdmmcEngine`) build a `DcaCacheCtx` once at construction; their
198/// per-transfer methods take `&mut DcaCacheCtx<...>` rather than
199/// `&mut SCB` directly. Application code MUST NOT construct a bare
200/// `&mut SCB` outside of (a) a `DcaCacheCtx` constructor or (b) the
201/// pre-DCB grandfathered call sites.
202pub struct DcaCacheCtx<'a, C: DcaCache> {
203    cache: &'a mut C,
204}
205
206impl<'a, C: DcaCache> DcaCacheCtx<'a, C> {
207    /// Wrap a cache controller for use by DCB transitions.
208    #[inline]
209    pub fn new(cache: &'a mut C) -> Self {
210        Self { cache }
211    }
212
213    /// Borrow the underlying cache controller. Reserved for engine
214    /// drivers that need to issue DCB-internal cache ops on extents
215    /// that are not themselves [`DcaBuf`]s (e.g. an `MPU` carve-out
216    /// scratch zone). Call sites count against the discipline scanner
217    /// budget unless DCB owns them.
218    #[inline]
219    pub fn cache_mut(&mut self) -> &mut C {
220        self.cache
221    }
222}
223
224// ── DcaBuf storage ─────────────────────────────────────────────────────
225
226/// Owning, cache-line-aligned, cache-line-padded DMA buffer.
227///
228/// DCB-00 §3 / §6:
229///
230/// - **INV-D1** — aligned to [`CACHE_LINE`] via `#[repr(C, align(32))]`.
231/// - **INV-D2** — `T*N` MUST be a multiple of [`CACHE_LINE`]. Enforced
232///   at construction by [`DcaBuf::new`]; misuse produces a `const`
233///   panic at compile time. Pad `N` upward (or wrap `T` in a
234///   cache-line-sized newtype) if your element geometry forces a
235///   non-multiple.
236/// - **INV-D3** — no two `DcaBuf`s share a cache line. Implied by
237///   INV-D1 + INV-D2 because every `DcaBuf` starts on a line and
238///   covers an integer number of lines.
239/// - **INV-D4** — single owner. Enforced by the typestate handles
240///   ([`Cpu`] / [`DeviceRead`] / [`DeviceWrite`] / [`CircRead`] /
241///   [`CircWrite`]); see [`DcaBuf::cpu`].
242///
243/// `T` MUST be `Copy` so the storage can be initialised in `const`
244/// context without dropping uninitialised values, and so DMA can read
245/// or overwrite bytes without disturbing destructors.
246#[repr(C, align(32))]
247pub struct DcaBuf<T: Copy, const N: usize> {
248    storage: UnsafeCell<[T; N]>,
249}
250
251// SAFETY: `DcaBuf` is `Sync` because every access path goes through a
252// typestate handle that holds an `&mut DcaBuf`. The handle's exclusive
253// borrow excludes concurrent CPU access; the DMA-owned typestates
254// document that the CPU MUST NOT touch the buffer through the handle.
255// Static placement is supported (the typical SAI/audio pattern).
256unsafe impl<T: Copy + Send, const N: usize> Sync for DcaBuf<T, N> {}
257
258impl<T: Copy, const N: usize> DcaBuf<T, N> {
259    /// Construct a buffer in CPU-owned state from an initial value
260    /// array.
261    ///
262    /// `const fn` so the result is usable as a `static` initialiser.
263    /// The compiler-evaluated assertion below enforces INV-D2: the
264    /// total byte length MUST be a multiple of [`CACHE_LINE`]. A
265    /// failed assertion is a compile-time error, not a runtime panic.
266    #[inline]
267    pub const fn new(init: [T; N]) -> Self {
268        // INV-D2 / INV-D1: post-condition checks. `align_of::<Self>()`
269        // is forced to 32 by the `#[repr(C, align(32))]` on the
270        // struct; INV-D2 requires the size of the storage payload to
271        // be a whole multiple of CACHE_LINE so the buffer occupies an
272        // integer number of cache lines.
273        assert!(
274            (size_of::<T>() * N).is_multiple_of(CACHE_LINE),
275            "DcaBuf<T, N>: size_of::<T>() * N must be a multiple of CACHE_LINE (32). Pad N upward or wrap T in a cache-line-sized newtype.",
276        );
277        assert!(
278            size_of::<T>() * N > 0,
279            "DcaBuf<T, N>: empty buffers are not permitted (no DMA target).",
280        );
281        Self {
282            storage: UnsafeCell::new(init),
283        }
284    }
285
286    /// Total byte length of the storage payload.
287    ///
288    /// Equal to `size_of::<T>() * N` and, by INV-D2, a multiple of
289    /// [`CACHE_LINE`].
290    #[inline]
291    pub const fn byte_len(&self) -> usize {
292        size_of::<T>() * N
293    }
294
295    /// CPU virtual address of the storage payload, as a `usize`.
296    ///
297    /// Used by [`DcaBuf::dma_addr`] and by the typestate transitions
298    /// to compute the cache-op extent.
299    #[inline]
300    fn addr_usize(&self) -> usize {
301        self.storage.get() as *mut u8 as usize
302    }
303
304    /// DMA bus address of the storage payload.
305    ///
306    /// On Cortex-M7 / STM32H747 the DMA address space coincides
307    /// numerically with the CPU physical address space; this method
308    /// is the ratified conversion site (DCB-00 §4 source-of-truth row
309    /// for `hwcore::addr`). Asserts cache-line alignment, which
310    /// follows from INV-D1.
311    pub fn dma_addr(&self) -> DmaAddr {
312        let addr = self.addr_usize();
313        // On the embedded target `addr` fits in u32; on host
314        // (64-bit usize) the cast truncates, but the low 32 bits of
315        // an aligned address preserve cache-line alignment, so the
316        // DmaAddr conversion succeeds. Host tests that read this
317        // value must not feed it to real DMA hardware.
318        let phys = PhysAddr::new(addr as u32);
319        DmaAddr::from_phys(phys, CACHE_LINE)
320            .expect("DcaBuf is #[repr(C, align(32))]; storage address is always cache-line aligned")
321    }
322
323    /// Take ownership of the buffer as a CPU-owned typestate handle.
324    ///
325    /// Borrows `self` exclusively; while the returned [`Cpu`] (or any
326    /// typestate-transitioned descendant) is alive, no other code may
327    /// reach the storage. This is the entry point for all typestate
328    /// transitions.
329    #[inline]
330    pub fn cpu(&mut self) -> Cpu<'_, T, N> {
331        Cpu { buf: self }
332    }
333}
334
335// ── Cpu typestate ──────────────────────────────────────────────────────
336
337/// CPU-owned typestate handle.
338///
339/// Per DCB-00 §3 glossary: "the CPU may freely read and write the
340/// buffer; no DMA master is reading or writing it." Cache state is
341/// unconstrained — the CPU's view is authoritative.
342pub struct Cpu<'a, T: Copy, const N: usize> {
343    buf: &'a mut DcaBuf<T, N>,
344}
345
346impl<'a, T: Copy, const N: usize> Cpu<'a, T, N> {
347    /// Read-only view of the storage as a fixed-size array.
348    #[inline]
349    pub fn as_slice(&self) -> &[T; N] {
350        // SAFETY: typestate Cpu holds an `&mut DcaBuf` — no other
351        // borrow of the storage exists for the lifetime of `self`.
352        unsafe { &*self.buf.storage.get() }
353    }
354
355    /// Mutable view of the storage as a fixed-size array.
356    #[inline]
357    pub fn as_mut_slice(&mut self) -> &mut [T; N] {
358        // SAFETY: as above; `&mut self` is the unique borrow.
359        unsafe { &mut *self.buf.storage.get() }
360    }
361
362    /// DMA bus address of the storage.
363    ///
364    /// Available in CPU-owned state for callers that pre-program the
365    /// DMA engine before lending. Once lent, the same address is
366    /// returned by [`Cpu::lend_for_read`] / [`Cpu::lend_for_write`].
367    #[inline]
368    pub fn dma_addr(&self) -> DmaAddr {
369        self.buf.dma_addr()
370    }
371
372    /// Transition to [`DeviceRead`] (DMA reads RAM, CPU is producer).
373    ///
374    /// Cache op (DCB-00 §5): `clean_dcache_by_address` over the full
375    /// padded extent so any CPU-written cache lines are written back
376    /// to RAM before the DMA reads them.
377    pub fn lend_for_read<C: DcaCache>(
378        self,
379        ctx: &mut DcaCacheCtx<'_, C>,
380    ) -> (DeviceRead<'a, T, N>, DmaAddr) {
381        let addr = self.buf.addr_usize();
382        let len = self.buf.byte_len();
383        ctx.cache.clean(addr, len);
384        let dma_addr = self.buf.dma_addr();
385        (DeviceRead { buf: self.buf }, dma_addr)
386    }
387
388    /// Transition to [`DeviceWrite`] (DMA writes RAM, CPU is consumer).
389    ///
390    /// Cache op (DCB-00 §5): `invalidate_dcache_by_address` over the
391    /// full padded extent so any stale CPU cache lines are evicted
392    /// before the DMA writes; INV-D3 (no cache-line sharing) prevents
393    /// adjacent-line refill from reintroducing stale data during
394    /// transfer, so no exit-side op is needed in
395    /// [`DeviceWrite::complete`].
396    pub fn lend_for_write<C: DcaCache>(
397        self,
398        ctx: &mut DcaCacheCtx<'_, C>,
399    ) -> (DeviceWrite<'a, T, N>, DmaAddr) {
400        let addr = self.buf.addr_usize();
401        let len = self.buf.byte_len();
402        ctx.cache.invalidate(addr, len);
403        let dma_addr = self.buf.dma_addr();
404        (DeviceWrite { buf: self.buf }, dma_addr)
405    }
406
407    /// Transition to [`CircRead`] (continuous DMA read; CPU may access
408    /// the inactive half via [`HalfGuard`]).
409    ///
410    /// Entry cache op: `clean_dcache_by_address` over the full padded
411    /// extent (CPU may have pre-filled the buffer before arming).
412    pub fn start_circular_read<C: DcaCache>(
413        self,
414        ctx: &mut DcaCacheCtx<'_, C>,
415    ) -> CircRead<'a, T, N> {
416        let addr = self.buf.addr_usize();
417        let len = self.buf.byte_len();
418        ctx.cache.clean(addr, len);
419        CircRead { buf: self.buf }
420    }
421
422    /// Transition to [`CircWrite`] (continuous DMA write; CPU may
423    /// access the inactive half via [`HalfGuard`]).
424    ///
425    /// Entry cache op: `invalidate_dcache_by_address` over the full
426    /// padded extent so the CPU's first read after the first
427    /// half-period observes DMA-written data.
428    pub fn start_circular_write<C: DcaCache>(
429        self,
430        ctx: &mut DcaCacheCtx<'_, C>,
431    ) -> CircWrite<'a, T, N> {
432        let addr = self.buf.addr_usize();
433        let len = self.buf.byte_len();
434        ctx.cache.invalidate(addr, len);
435        CircWrite { buf: self.buf }
436    }
437
438    /// Transition to [`LtdcScan`] (continuous-read consumer; LTDC,
439    /// DCMI display-out, parallel RGB).
440    ///
441    /// Per DCB-00 §5 transition table + INV-D16: emits
442    /// [`DcaCache::clean`] over the full padded extent **and** a
443    /// [`DcaCache::barrier`] (DSB on Cortex-M; AXI-write-buffer
444    /// drain on STM32H7 + Write-Through SDRAM). Both are required;
445    /// the clean alone does not enforce ordering on architectures
446    /// where the cache write-back queues into a separate write
447    /// buffer.
448    pub fn start_ltdc_scan<C: DcaCache>(self, ctx: &mut DcaCacheCtx<'_, C>) -> LtdcScan<'a, T, N> {
449        let addr = self.buf.addr_usize();
450        let len = self.buf.byte_len();
451        ctx.cache.clean(addr, len);
452        ctx.cache.barrier();
453        LtdcScan { buf: self.buf }
454    }
455}
456
457// ── DeviceRead / DeviceWrite (one-shot) ────────────────────────────────
458
459/// One-shot DMA-read typestate handle.
460///
461/// While alive, the CPU MUST NOT access the buffer. Drop or
462/// [`DeviceRead::complete`] returns the buffer to [`Cpu`] state with
463/// no exit cache op (the device only read RAM; the CPU's cached copy
464/// is unchanged from before the transfer).
465pub struct DeviceRead<'a, T: Copy, const N: usize> {
466    buf: &'a mut DcaBuf<T, N>,
467}
468
469impl<'a, T: Copy, const N: usize> DeviceRead<'a, T, N> {
470    /// DMA bus address of the buffer.
471    #[inline]
472    pub fn dma_addr(&self) -> DmaAddr {
473        self.buf.dma_addr()
474    }
475
476    /// Transition back to [`Cpu`] state.
477    ///
478    /// Called by an engine completion handler once the DMA "transfer
479    /// complete" interrupt or status bit confirms the device has
480    /// stopped reading. Per DCB-00 §5 transition table, no cache op
481    /// is emitted — the device only read RAM.
482    #[inline]
483    pub fn complete(self) -> Cpu<'a, T, N> {
484        Cpu { buf: self.buf }
485    }
486}
487
488/// One-shot DMA-write typestate handle.
489///
490/// While alive, the CPU MUST NOT access the buffer. Drop or
491/// [`DeviceWrite::complete`] returns the buffer to [`Cpu`] state with
492/// no exit cache op (the entry-side invalidate already prepared the
493/// cache for the post-transfer CPU read).
494pub struct DeviceWrite<'a, T: Copy, const N: usize> {
495    buf: &'a mut DcaBuf<T, N>,
496}
497
498impl<'a, T: Copy, const N: usize> DeviceWrite<'a, T, N> {
499    /// DMA bus address of the buffer.
500    #[inline]
501    pub fn dma_addr(&self) -> DmaAddr {
502        self.buf.dma_addr()
503    }
504
505    /// Transition back to [`Cpu`] state.
506    ///
507    /// Per DCB-00 §5 transition table: no cache op at exit. The
508    /// entry-side invalidate evicted the buffer's cache lines, INV-D3
509    /// forbids adjacent-line refill from reintroducing stale data
510    /// during transfer, so the CPU's first read after this call hits
511    /// RAM and observes the DMA-written data.
512    #[inline]
513    pub fn complete(self) -> Cpu<'a, T, N> {
514        Cpu { buf: self.buf }
515    }
516}
517
518// ── CircRead / CircWrite + HalfGuard ───────────────────────────────────
519
520/// Identifies which half of a circular buffer is currently *inactive*
521/// (i.e. safe for CPU access via a [`HalfGuard`]).
522#[derive(Copy, Clone, Eq, PartialEq, Debug)]
523pub enum Half {
524    /// First half: indices `[0, N/2)`.
525    First,
526    /// Second half: indices `[N/2, N)`.
527    Second,
528}
529
530/// Continuous DMA-read typestate handle.
531///
532/// The DMA engine reads the buffer in circular mode (typically
533/// double-buffer for audio TX, video stream-out, etc.). The CPU is
534/// the producer and may fill the *inactive* half through a
535/// [`HalfGuard<Read, _, _>`] obtained from [`CircRead::half_guard`].
536pub struct CircRead<'a, T: Copy, const N: usize> {
537    buf: &'a mut DcaBuf<T, N>,
538}
539
540impl<'a, T: Copy, const N: usize> CircRead<'a, T, N> {
541    /// DMA bus address of the buffer (stable for the typestate's
542    /// lifetime; equal to the address returned during the
543    /// `start_circular_*` transition).
544    #[inline]
545    pub fn dma_addr(&self) -> DmaAddr {
546        self.buf.dma_addr()
547    }
548
549    /// Acquire a guard over the inactive half.
550    ///
551    /// `half` names which half is currently safe to access — the
552    /// caller knows from the engine's stream-position register
553    /// (`NDTR` for STM32 DMA, `LIVR` for SAI, etc.) which half the
554    /// DMA is currently servicing. Per DCB-00 §6 INV-D7, releasing
555    /// the guard with [`HalfGuard::release`] re-checks the
556    /// stream-position.
557    ///
558    /// **No cache op is emitted at construction.** Per DCB-00 §5
559    /// (DCB-01b-A 2026-05-03 amendment), the `Read` direction's
560    /// cache op fires at `release` — the CPU is about to write
561    /// new data via the guard's slice, so cleaning *now* would
562    /// publish stale data. The clean at release publishes the
563    /// just-completed CPU writes before the engine wraps to this
564    /// half on its next pass.
565    ///
566    /// `ctx` is unused at construction for `Read` direction (kept
567    /// in the signature for symmetry with `CircWrite::half_guard`,
568    /// which uses it for the entry-side invalidate).
569    pub fn half_guard<'b, C: DcaCache>(
570        &'b mut self,
571        _ctx: &mut DcaCacheCtx<'_, C>,
572        half: Half,
573    ) -> HalfGuard<'b, Read, T, N> {
574        // INV-D2 / half_extent debug_assert is still useful for
575        // sanity-checking the call-site geometry, even though no
576        // cache op fires here. Compute and discard.
577        let _ = half_extent::<T, N>(self.buf.addr_usize(), half);
578        HalfGuard {
579            buf: self.buf,
580            half,
581            _dir: PhantomData,
582        }
583    }
584
585    /// Stop the circular transfer and transition back to [`Cpu`].
586    ///
587    /// Caller MUST stop the engine before calling. Cache op:
588    /// [`DcaCache::clean`] over the full padded extent, ensuring any
589    /// CPU-written data still in cache is published to RAM before
590    /// the buffer leaves DCB ownership.
591    pub fn stop_circular<C: DcaCache>(self, ctx: &mut DcaCacheCtx<'_, C>) -> Cpu<'a, T, N> {
592        let addr = self.buf.addr_usize();
593        let len = self.buf.byte_len();
594        ctx.cache.clean(addr, len);
595        Cpu { buf: self.buf }
596    }
597}
598
599/// Continuous DMA-write typestate handle.
600///
601/// The DMA engine writes the buffer in circular mode. The CPU is the
602/// consumer and may drain the *inactive* half through a
603/// [`HalfGuard<Write, _, _>`] obtained from [`CircWrite::half_guard`].
604pub struct CircWrite<'a, T: Copy, const N: usize> {
605    buf: &'a mut DcaBuf<T, N>,
606}
607
608impl<'a, T: Copy, const N: usize> CircWrite<'a, T, N> {
609    /// DMA bus address of the buffer.
610    #[inline]
611    pub fn dma_addr(&self) -> DmaAddr {
612        self.buf.dma_addr()
613    }
614
615    /// Acquire a guard over the inactive half.
616    ///
617    /// Cache op for [`Write`] direction: [`DcaCache::invalidate`]
618    /// over the inactive half so the CPU's next read of that half
619    /// observes DMA-written data.
620    pub fn half_guard<'b, C: DcaCache>(
621        &'b mut self,
622        ctx: &mut DcaCacheCtx<'_, C>,
623        half: Half,
624    ) -> HalfGuard<'b, Write, T, N> {
625        let (addr, len) = half_extent::<T, N>(self.buf.addr_usize(), half);
626        ctx.cache.invalidate(addr, len);
627        HalfGuard {
628            buf: self.buf,
629            half,
630            _dir: PhantomData,
631        }
632    }
633
634    /// Stop the circular transfer and transition back to [`Cpu`].
635    ///
636    /// Caller MUST stop the engine before calling. Cache op:
637    /// [`DcaCache::invalidate`] over the full padded extent so the
638    /// CPU's next read after release observes the final DMA-written
639    /// state, not stale cache lines.
640    pub fn stop_circular<C: DcaCache>(self, ctx: &mut DcaCacheCtx<'_, C>) -> Cpu<'a, T, N> {
641        let addr = self.buf.addr_usize();
642        let len = self.buf.byte_len();
643        ctx.cache.invalidate(addr, len);
644        Cpu { buf: self.buf }
645    }
646}
647
648// ── LtdcScan (continuous-read consumer; DeviceLtdcScan typestate) ──────
649
650/// Continuous-read typestate handle for fixed-rate pixel-stream
651/// consumers (LTDC, DCMI display-out, parallel RGB).
652///
653/// Distinguished from [`CircRead`]:
654///
655/// - the consumer never pauses (no half-rotation; same buffer is
656///   read continuously),
657/// - the CPU paints in place between consumer reads rather than
658///   producing into an inactive half,
659/// - per-frame ordering is enforced by [`LtdcScan::present`] (clean
660///   plus DSB) at checkpoints chosen by the caller, not by an
661///   engine-side completion flag.
662///
663/// The `DIR` parameter is **deliberately omitted** — the CPU is
664/// always producer for this family. The inverse direction (camera
665/// frame-grab via DCMI) would be a separate `LtdcGrab` family;
666/// not in scope here, would ratify in a future §15 amendment with
667/// a named first user.
668pub struct LtdcScan<'a, T: Copy, const N: usize> {
669    buf: &'a mut DcaBuf<T, N>,
670}
671
672impl<'a, T: Copy, const N: usize> LtdcScan<'a, T, N> {
673    /// DMA bus address of the buffer (stable for the typestate's
674    /// lifetime; equal to the address returned during the
675    /// `start_ltdc_scan` transition). Pass to the consumer engine's
676    /// frame-buffer base register (e.g. LTDC `CFBAR`).
677    #[inline]
678    pub fn dma_addr(&self) -> DmaAddr {
679        self.buf.dma_addr()
680    }
681
682    /// In-place CPU access to the full buffer.
683    ///
684    /// Returns a `&mut [T; N]` valid until the next call that
685    /// borrows `self` mutably ([`LtdcScan::present`] /
686    /// [`LtdcScan::stop_scan`]). The borrow checker rejects
687    /// concurrent `paint_full` / `present` calls.
688    ///
689    /// No cache op is emitted here; ordering between the CPU's
690    /// writes and the consumer's reads is enforced at `present`
691    /// (DCB-00 §5 transition table).
692    #[inline]
693    pub fn paint_full(&mut self) -> &mut [T; N] {
694        // SAFETY: typestate LtdcScan holds an `&mut DcaBuf` — no
695        // other borrow of the storage exists for `self`'s lifetime.
696        // The consumer reads the same memory continuously but does
697        // so at engine-determined timing; aliasing with the consumer
698        // is the documented contract of `DeviceLtdcScan<T, N>` and
699        // is mediated by `present()`.
700        unsafe { &mut *self.buf.storage.get() }
701    }
702
703    /// Per-frame checkpoint: publish CPU writes to RAM for the
704    /// consumer's next read.
705    ///
706    /// Per DCB-00 §6 INV-D16 emits **both** [`DcaCache::clean`]
707    /// over the full padded extent AND [`DcaCache::barrier`]
708    /// (DSB-equivalent). Does not transition the typestate — the
709    /// consumer engine is still reading; only the ordering between
710    /// CPU writes and consumer reads is checkpointed.
711    pub fn present<C: DcaCache>(&mut self, ctx: &mut DcaCacheCtx<'_, C>) {
712        let addr = self.buf.addr_usize();
713        let len = self.buf.byte_len();
714        ctx.cache.clean(addr, len);
715        ctx.cache.barrier();
716    }
717
718    /// Transition back to [`Cpu`].
719    ///
720    /// Caller MUST stop the consumer engine before calling. Per
721    /// DCB-00 §5: emits a final [`DcaCache::clean`] +
722    /// [`DcaCache::barrier`] over the full padded extent so the
723    /// next phase's CPU read sees the buffer's terminal state.
724    pub fn stop_scan<C: DcaCache>(self, ctx: &mut DcaCacheCtx<'_, C>) -> Cpu<'a, T, N> {
725        let addr = self.buf.addr_usize();
726        let len = self.buf.byte_len();
727        ctx.cache.clean(addr, len);
728        ctx.cache.barrier();
729        Cpu { buf: self.buf }
730    }
731}
732
733/// RAII guard for half-buffer access in a circular DMA transfer.
734///
735/// Holds an `&'b mut` reborrow of the parent [`CircRead`] /
736/// [`CircWrite`], so a second `half_guard` call is rejected by the
737/// borrow checker until this guard is dropped.
738///
739/// The `DIR` parameter is one of [`Read`] / [`Write`] and selects
740/// which cache op is emitted at construction (DCB-00 §5 transition
741/// row for `DeviceActiveCirc<DIR>` HalfGuard).
742pub struct HalfGuard<'a, DIR, T: Copy, const N: usize> {
743    buf: &'a mut DcaBuf<T, N>,
744    half: Half,
745    _dir: PhantomData<DIR>,
746}
747
748impl<'a, DIR, T: Copy, const N: usize> HalfGuard<'a, DIR, T, N> {
749    /// Read-only view of the guarded half.
750    #[inline]
751    pub fn as_slice(&self) -> &[T] {
752        let (lo, hi) = half_indices::<N>(self.half);
753        // SAFETY: guard borrows the parent CircRead/CircWrite mutably,
754        // which itself holds an `&mut DcaBuf` — no other access path
755        // to the storage exists for `self`'s lifetime. Slicing
756        // [lo, hi) is in bounds: lo,hi ∈ [0, N] by half_indices.
757        unsafe {
758            let storage = &*self.buf.storage.get();
759            core::slice::from_raw_parts(storage.as_ptr().add(lo), hi - lo)
760        }
761    }
762
763    /// Mutable view of the guarded half.
764    #[inline]
765    pub fn as_mut_slice(&mut self) -> &mut [T] {
766        let (lo, hi) = half_indices::<N>(self.half);
767        // SAFETY: as above; `&mut self` is the unique borrow.
768        unsafe {
769            let storage = &mut *self.buf.storage.get();
770            core::slice::from_raw_parts_mut(storage.as_mut_ptr().add(lo), hi - lo)
771        }
772    }
773
774    /// Which half this guard exposes.
775    #[inline]
776    pub fn half(&self) -> Half {
777        self.half
778    }
779}
780
781/// Direction-specific `release` for `HalfGuard<Read, _, _>`.
782///
783/// Per DCB-00 §5 (DCB-01b-A 2026-05-03 amendment) the `Read`
784/// direction's cache op fires at `release`, not at construction:
785/// the CPU is the producer; cleaning before the writes would
786/// publish stale data. The clean here publishes the just-completed
787/// CPU writes before the engine wraps to this half on its next
788/// pass.
789impl<'a, T: Copy, const N: usize> HalfGuard<'a, Read, T, N> {
790    /// Release the guard. Emits [`DcaCache::clean`] over the
791    /// guarded half's extent (the cache op for the `Read`
792    /// direction; publishes CPU writes), then performs the
793    /// INV-D7 stream-position checkpoint.
794    ///
795    /// `current_half` is the half the DMA engine is **currently**
796    /// servicing; the caller reads it from `NDTR` / `LIVR` / etc.
797    /// immediately before this call. If the DMA has crossed into
798    /// the half this guard exposes (i.e. `current_half == self.half`),
799    /// the post-condition check fires per DCB-00 §6 INV-D7:
800    /// `panic!` in `debug_assertions`, error return in release.
801    pub fn release<C: DcaCache>(
802        self,
803        ctx: &mut DcaCacheCtx<'_, C>,
804        current_half: Half,
805    ) -> Result<(), HalfGuardOverrun> {
806        // Cache op first: publish the just-completed CPU writes
807        // before checking the live stream-position. If the engine
808        // has flipped, we still want the clean to fire so the
809        // stale-data window is bounded — the overrun is reported
810        // separately for the caller to act on.
811        let (addr, len) = half_extent::<T, N>(self.buf.addr_usize(), self.half);
812        ctx.cache.clean(addr, len);
813        check_half_overrun(self.half, current_half)
814    }
815}
816
817/// Direction-specific `release` for `HalfGuard<Write, _, _>`.
818///
819/// Per DCB-00 §5 the `Write` direction's cache op fires at
820/// construction (`invalidate` over the inactive half). `release`
821/// performs only the INV-D7 stream-position checkpoint — the CPU
822/// has only read; no dirty lines need publishing.
823impl<'a, T: Copy, const N: usize> HalfGuard<'a, Write, T, N> {
824    /// Release the guard. No cache op (the entry-side invalidate
825    /// already drained stale lines). Performs the INV-D7
826    /// stream-position checkpoint.
827    ///
828    /// `_ctx` is unused for `Write` direction (kept for symmetry
829    /// with `HalfGuard<Read>::release`).
830    pub fn release<C: DcaCache>(
831        self,
832        _ctx: &mut DcaCacheCtx<'_, C>,
833        current_half: Half,
834    ) -> Result<(), HalfGuardOverrun> {
835        check_half_overrun(self.half, current_half)
836    }
837}
838
839/// Internal helper: direction-independent INV-D7 live-recheck.
840#[inline]
841fn check_half_overrun(guard_half: Half, current_half: Half) -> Result<(), HalfGuardOverrun> {
842    if current_half == guard_half {
843        #[cfg(debug_assertions)]
844        {
845            panic!(
846                "DCB HalfGuard overrun: DMA crossed into the inactive half ({guard_half:?}) during the guard's lifetime; INV-D7 violated. Stream is faster than the CPU consumer/producer."
847            );
848        }
849        #[cfg(not(debug_assertions))]
850        {
851            return Err(HalfGuardOverrun { half: guard_half });
852        }
853    }
854    Ok(())
855}
856
857/// Error returned by [`HalfGuard::release`] in release builds when the
858/// DMA stream crossed into the guarded half during the guard's
859/// lifetime — INV-D7 violation.
860#[derive(Copy, Clone, Eq, PartialEq, Debug)]
861pub struct HalfGuardOverrun {
862    /// The half the guard exposed at construction.
863    pub half: Half,
864}
865
866// ── DcaDoubleBuf storage (DMA double-buffer mode) ──────────────────────
867
868/// Identifies which bank of a double-buffer-mode DMA buffer is the
869/// engine's *current target* (or, conversely, which bank is inactive
870/// and safe for CPU access via a [`BankGuard`]).
871///
872/// Maps to the STM32 DMA `CR.CT` bit (bit 19 on H7): `M0` ↔ `CT=0`,
873/// `M1` ↔ `CT=1`. Per DCB-00 §6 INV-D15 the bit is the source-of-truth
874/// for `BankGuard::release` live-recheck.
875#[derive(Copy, Clone, Eq, PartialEq, Debug)]
876pub enum Bank {
877    /// First bank (programmed into the DMA stream's `M0AR` register).
878    M0,
879    /// Second bank (programmed into the DMA stream's `M1AR` register).
880    M1,
881}
882
883/// Owning handle for **STM32 DMA double-buffer mode** storage: a pair
884/// of cache-line-aligned, cache-line-padded banks of `[T; N]`.
885///
886/// The two banks MAY live at arbitrary disjoint addresses (DCB-00 §6
887/// INV-D14): the typical embedded use is `from_addrs(m0_addr, m1_addr)`
888/// wrapping pre-existing fixed-address regions (e.g. SAI1 RX banks at
889/// `0x3000_0000` + `0x3000_1000`). Host tests use [`DcaDoubleBuf::new`]
890/// over two stack-allocated [`DcaBuf`]s.
891///
892/// Distinct from [`DcaBuf`] because the engine register layout (M0AR +
893/// M1AR + `CT` bit + `MBM` flag) is different from circular mode. A
894/// buffer family chooses one or the other at construction and cannot
895/// switch — see DCB-00 §5 "Parallel family: `DcaDoubleBuf<T, N>`".
896pub struct DcaDoubleBuf<'b, T: Copy, const N: usize> {
897    m0: &'b mut DcaBuf<T, N>,
898    m1: &'b mut DcaBuf<T, N>,
899}
900
901impl<'b, T: Copy, const N: usize> DcaDoubleBuf<'b, T, N> {
902    /// Wrap two pre-existing [`DcaBuf`]s as the M0 / M1 banks.
903    ///
904    /// The borrow checker enforces that no other access path to either
905    /// bank exists for the lifetime of the returned `DcaDoubleBuf`.
906    /// Host tests use this; embedded code typically uses
907    /// [`DcaDoubleBuf::from_addrs`].
908    #[inline]
909    pub fn new(m0: &'b mut DcaBuf<T, N>, m1: &'b mut DcaBuf<T, N>) -> Self {
910        Self { m0, m1 }
911    }
912
913    /// Take ownership of this storage as a CPU-owned typestate handle.
914    #[inline]
915    pub fn cpu(&mut self) -> DbufCpu<'_, 'b, T, N> {
916        DbufCpu { buf: self }
917    }
918}
919
920impl<T: Copy, const N: usize> DcaDoubleBuf<'static, T, N> {
921    /// Construct from two fixed bank addresses with `'static` lifetime.
922    ///
923    /// Both addresses are reinterpreted as `&'static mut DcaBuf<T, N>`;
924    /// the typical embedded use case is wrapping pre-existing fixed-
925    /// address DMA buffers (e.g. `0x3000_0000` and `0x3000_1000` for
926    /// SAI1 RX banks BUF0 and BUF1).
927    ///
928    /// # Safety
929    ///
930    /// The caller MUST ensure that:
931    ///
932    /// - both `m0_addr` and `m1_addr` name disjoint, mapped, writable
933    ///   RAM regions of `size_of::<DcaBuf<T, N>>()` bytes each, valid
934    ///   for `'static`,
935    /// - the two regions do not overlap each other and share no cache
936    ///   line with each other or with any other `DcaBuf` /
937    ///   `DcaDoubleBuf` (INV-D3 / INV-D14),
938    /// - both addresses are aligned to [`CACHE_LINE`] (32 bytes) — the
939    ///   `#[repr(C, align(32))]` requirement on `DcaBuf<T, N>`,
940    /// - no other `&mut` to overlapping bytes exists for `'static`,
941    /// - `from_addrs` is called *at most once* per pair of addresses;
942    ///   producing two `DcaDoubleBuf<'static, T, N>` instances over
943    ///   the same physical regions would alias the underlying
944    ///   `&'static mut`.
945    pub unsafe fn from_addrs(m0_addr: usize, m1_addr: usize) -> Self {
946        debug_assert!(
947            m0_addr.is_multiple_of(CACHE_LINE),
948            "DcaDoubleBuf::from_addrs: m0_addr must be cache-line aligned (INV-D14)",
949        );
950        debug_assert!(
951            m1_addr.is_multiple_of(CACHE_LINE),
952            "DcaDoubleBuf::from_addrs: m1_addr must be cache-line aligned (INV-D14)",
953        );
954        debug_assert!(
955            m0_addr != m1_addr,
956            "DcaDoubleBuf::from_addrs: m0_addr and m1_addr must be distinct",
957        );
958        // SAFETY: caller contract — addresses name disjoint, mapped,
959        // exclusive `'static` regions of `DcaBuf<T, N>` size and
960        // alignment.
961        let m0 = unsafe { &mut *(m0_addr as *mut DcaBuf<T, N>) };
962        // SAFETY: as above.
963        let m1 = unsafe { &mut *(m1_addr as *mut DcaBuf<T, N>) };
964        Self { m0, m1 }
965    }
966}
967
968// ── DbufCpu typestate ──────────────────────────────────────────────────
969
970/// CPU-owned typestate handle for a [`DcaDoubleBuf`].
971///
972/// Per DCB-00 §3 / §5: the CPU may freely read and write either bank;
973/// no DMA master is reading or writing them. Cache state is
974/// unconstrained — the CPU's view is authoritative.
975pub struct DbufCpu<'a, 'b, T: Copy, const N: usize> {
976    buf: &'a mut DcaDoubleBuf<'b, T, N>,
977}
978
979impl<'a, 'b, T: Copy, const N: usize> DbufCpu<'a, 'b, T, N> {
980    /// Read-only view of the M0 bank.
981    #[inline]
982    pub fn as_m0_slice(&self) -> &[T; N] {
983        // SAFETY: typestate DbufCpu holds `&mut DcaDoubleBuf` which
984        // itself holds `&mut DcaBuf` for each bank — no other borrow
985        // of the storage exists for `self`'s lifetime.
986        unsafe { &*self.buf.m0.storage.get() }
987    }
988
989    /// Read-only view of the M1 bank.
990    #[inline]
991    pub fn as_m1_slice(&self) -> &[T; N] {
992        // SAFETY: as above.
993        unsafe { &*self.buf.m1.storage.get() }
994    }
995
996    /// Mutable view of the M0 bank.
997    #[inline]
998    pub fn as_m0_mut_slice(&mut self) -> &mut [T; N] {
999        // SAFETY: as above; `&mut self` is the unique borrow.
1000        unsafe { &mut *self.buf.m0.storage.get() }
1001    }
1002
1003    /// Mutable view of the M1 bank.
1004    #[inline]
1005    pub fn as_m1_mut_slice(&mut self) -> &mut [T; N] {
1006        // SAFETY: as above.
1007        unsafe { &mut *self.buf.m1.storage.get() }
1008    }
1009
1010    /// DMA bus address of the M0 bank (program into the engine's `M0AR`).
1011    #[inline]
1012    pub fn m0_dma_addr(&self) -> DmaAddr {
1013        self.buf.m0.dma_addr()
1014    }
1015
1016    /// DMA bus address of the M1 bank (program into the engine's `M1AR`).
1017    #[inline]
1018    pub fn m1_dma_addr(&self) -> DmaAddr {
1019        self.buf.m1.dma_addr()
1020    }
1021
1022    /// Transition to [`DbufRead`] (DMA reads RAM, CPU is producer).
1023    ///
1024    /// Cache op (DCB-00 §5 transition table): `clean_dcache_by_address`
1025    /// over **both** banks (CPU may have pre-filled either bank before
1026    /// arming).
1027    pub fn start_double_buffer_read<C: DcaCache>(
1028        self,
1029        ctx: &mut DcaCacheCtx<'_, C>,
1030    ) -> DbufRead<'a, 'b, T, N> {
1031        let m0_addr = self.buf.m0.addr_usize();
1032        let m1_addr = self.buf.m1.addr_usize();
1033        let len = self.buf.m0.byte_len();
1034        ctx.cache.clean(m0_addr, len);
1035        ctx.cache.clean(m1_addr, len);
1036        DbufRead { buf: self.buf }
1037    }
1038
1039    /// Transition to [`DbufWrite`] (DMA writes RAM, CPU is consumer).
1040    ///
1041    /// Cache op (DCB-00 §5 transition table): `invalidate_dcache_by_address`
1042    /// over **both** banks so the CPU's first read after the first
1043    /// bank flip observes DMA-written data.
1044    pub fn start_double_buffer_write<C: DcaCache>(
1045        self,
1046        ctx: &mut DcaCacheCtx<'_, C>,
1047    ) -> DbufWrite<'a, 'b, T, N> {
1048        let m0_addr = self.buf.m0.addr_usize();
1049        let m1_addr = self.buf.m1.addr_usize();
1050        let len = self.buf.m0.byte_len();
1051        ctx.cache.invalidate(m0_addr, len);
1052        ctx.cache.invalidate(m1_addr, len);
1053        DbufWrite { buf: self.buf }
1054    }
1055}
1056
1057// ── DbufRead / DbufWrite + BankGuard ──────────────────────────────────
1058
1059/// Continuous DMA-read typestate handle for double-buffer mode.
1060///
1061/// The DMA engine reads M0 and M1 alternately in continuous transfer.
1062/// The CPU is the producer and may fill the *inactive* bank through a
1063/// [`BankGuard<Read, _, _>`] obtained from [`DbufRead::bank_guard`].
1064pub struct DbufRead<'a, 'b, T: Copy, const N: usize> {
1065    buf: &'a mut DcaDoubleBuf<'b, T, N>,
1066}
1067
1068impl<'a, 'b, T: Copy, const N: usize> DbufRead<'a, 'b, T, N> {
1069    /// DMA bus address of the M0 bank (stable for the typestate's
1070    /// lifetime).
1071    #[inline]
1072    pub fn m0_dma_addr(&self) -> DmaAddr {
1073        self.buf.m0.dma_addr()
1074    }
1075
1076    /// DMA bus address of the M1 bank.
1077    #[inline]
1078    pub fn m1_dma_addr(&self) -> DmaAddr {
1079        self.buf.m1.dma_addr()
1080    }
1081
1082    /// Acquire a guard over the inactive bank.
1083    ///
1084    /// `current_target` names the bank the engine is **currently**
1085    /// servicing (read from the stream's `CT` bit). The guard exposes
1086    /// the *opposite* bank.
1087    ///
1088    /// **No cache op is emitted at construction.** Per DCB-00 §5
1089    /// (DCB-01b-A 2026-05-03 amendment), the `Read` direction's
1090    /// cache op fires at `release` — the CPU is about to write
1091    /// new data via the guard's slice, so cleaning *now* would
1092    /// publish stale data. The clean at release publishes the
1093    /// just-completed CPU writes before the engine flips back to
1094    /// this bank.
1095    ///
1096    /// `_ctx` is unused at construction for `Read` direction (kept
1097    /// for symmetry with `DbufWrite::bank_guard`, which uses it
1098    /// for the entry-side invalidate).
1099    pub fn bank_guard<'g, C: DcaCache>(
1100        &'g mut self,
1101        _ctx: &mut DcaCacheCtx<'_, C>,
1102        current_target: Bank,
1103    ) -> BankGuard<'g, Read, T, N> {
1104        let exposed_bank = inactive_bank(current_target);
1105        let exposed = match exposed_bank {
1106            Bank::M0 => &mut *self.buf.m0,
1107            Bank::M1 => &mut *self.buf.m1,
1108        };
1109        BankGuard {
1110            bank_storage: exposed,
1111            bank: exposed_bank,
1112            _dir: PhantomData,
1113        }
1114    }
1115
1116    /// Stop the double-buffer transfer and transition back to
1117    /// [`DbufCpu`].
1118    ///
1119    /// Caller MUST stop the engine before calling. Cache op:
1120    /// [`DcaCache::clean`] over both banks.
1121    pub fn stop_double_buffer<C: DcaCache>(
1122        self,
1123        ctx: &mut DcaCacheCtx<'_, C>,
1124    ) -> DbufCpu<'a, 'b, T, N> {
1125        let m0_addr = self.buf.m0.addr_usize();
1126        let m1_addr = self.buf.m1.addr_usize();
1127        let len = self.buf.m0.byte_len();
1128        ctx.cache.clean(m0_addr, len);
1129        ctx.cache.clean(m1_addr, len);
1130        DbufCpu { buf: self.buf }
1131    }
1132}
1133
1134/// Continuous DMA-write typestate handle for double-buffer mode.
1135///
1136/// The DMA engine writes M0 and M1 alternately. The CPU is the
1137/// consumer and may drain the *inactive* bank through a
1138/// [`BankGuard<Write, _, _>`].
1139pub struct DbufWrite<'a, 'b, T: Copy, const N: usize> {
1140    buf: &'a mut DcaDoubleBuf<'b, T, N>,
1141}
1142
1143impl<'a, 'b, T: Copy, const N: usize> DbufWrite<'a, 'b, T, N> {
1144    /// DMA bus address of the M0 bank.
1145    #[inline]
1146    pub fn m0_dma_addr(&self) -> DmaAddr {
1147        self.buf.m0.dma_addr()
1148    }
1149
1150    /// DMA bus address of the M1 bank.
1151    #[inline]
1152    pub fn m1_dma_addr(&self) -> DmaAddr {
1153        self.buf.m1.dma_addr()
1154    }
1155
1156    /// Acquire a guard over the inactive bank.
1157    ///
1158    /// Cache op for [`Write`] direction: [`DcaCache::invalidate`] over
1159    /// the inactive bank's full extent so the CPU's next read of that
1160    /// bank observes DMA-written data.
1161    pub fn bank_guard<'g, C: DcaCache>(
1162        &'g mut self,
1163        ctx: &mut DcaCacheCtx<'_, C>,
1164        current_target: Bank,
1165    ) -> BankGuard<'g, Write, T, N> {
1166        let exposed_bank = inactive_bank(current_target);
1167        let exposed = match exposed_bank {
1168            Bank::M0 => &mut *self.buf.m0,
1169            Bank::M1 => &mut *self.buf.m1,
1170        };
1171        let addr = exposed.addr_usize();
1172        let len = exposed.byte_len();
1173        ctx.cache.invalidate(addr, len);
1174        BankGuard {
1175            bank_storage: exposed,
1176            bank: exposed_bank,
1177            _dir: PhantomData,
1178        }
1179    }
1180
1181    /// Stop the double-buffer transfer and transition back to
1182    /// [`DbufCpu`].
1183    pub fn stop_double_buffer<C: DcaCache>(
1184        self,
1185        ctx: &mut DcaCacheCtx<'_, C>,
1186    ) -> DbufCpu<'a, 'b, T, N> {
1187        let m0_addr = self.buf.m0.addr_usize();
1188        let m1_addr = self.buf.m1.addr_usize();
1189        let len = self.buf.m0.byte_len();
1190        ctx.cache.invalidate(m0_addr, len);
1191        ctx.cache.invalidate(m1_addr, len);
1192        DbufCpu { buf: self.buf }
1193    }
1194}
1195
1196/// RAII guard for inactive-bank access in a double-buffer DMA transfer.
1197///
1198/// Holds an `&'g mut` reborrow of one bank's [`DcaBuf`], obtained by
1199/// reborrowing through the parent [`DbufRead`] / [`DbufWrite`]. A
1200/// second concurrent `bank_guard` call is rejected by the borrow
1201/// checker until this guard is dropped.
1202///
1203/// The `DIR` parameter is one of [`Read`] / [`Write`] and selects
1204/// which cache op was emitted at construction (DCB-00 §5 transition
1205/// row for `DeviceActiveDoubleBuf<DIR>` BankGuard).
1206pub struct BankGuard<'a, DIR, T: Copy, const N: usize> {
1207    bank_storage: &'a mut DcaBuf<T, N>,
1208    bank: Bank,
1209    _dir: PhantomData<DIR>,
1210}
1211
1212impl<'a, DIR, T: Copy, const N: usize> BankGuard<'a, DIR, T, N> {
1213    /// Read-only view of the guarded bank.
1214    #[inline]
1215    pub fn as_slice(&self) -> &[T; N] {
1216        // SAFETY: guard borrows the parent DbufRead/DbufWrite mutably,
1217        // which itself holds `&mut DcaDoubleBuf` (and through it
1218        // `&mut DcaBuf` for each bank) — no other access path to this
1219        // bank exists for `self`'s lifetime.
1220        unsafe { &*self.bank_storage.storage.get() }
1221    }
1222
1223    /// Mutable view of the guarded bank.
1224    #[inline]
1225    pub fn as_mut_slice(&mut self) -> &mut [T; N] {
1226        // SAFETY: as above; `&mut self` is the unique borrow.
1227        unsafe { &mut *self.bank_storage.storage.get() }
1228    }
1229
1230    /// Which bank this guard exposes.
1231    #[inline]
1232    pub fn bank(&self) -> Bank {
1233        self.bank
1234    }
1235}
1236
1237/// Direction-specific `release` for `BankGuard<Read, _, _>`.
1238///
1239/// Per DCB-00 §5 (DCB-01b-A 2026-05-03 amendment) the `Read`
1240/// direction's cache op fires at `release`, not at construction:
1241/// the CPU is the producer; cleaning before the writes would
1242/// publish stale data. The clean here publishes the just-completed
1243/// CPU writes before the engine flips back to this bank.
1244impl<'a, T: Copy, const N: usize> BankGuard<'a, Read, T, N> {
1245    /// Release the guard. Emits [`DcaCache::clean`] over the
1246    /// guarded bank's full extent, then performs the INV-D15
1247    /// CT-bit checkpoint.
1248    ///
1249    /// `current_target` is the bank the DMA engine is **currently**
1250    /// servicing per the latest read of the stream's `CT` bit. If
1251    /// the engine has flipped CT into the bank this guard exposes
1252    /// (i.e. `current_target == self.bank`), the post-condition
1253    /// check fires: `panic!` in `debug_assertions` builds, error
1254    /// return in release builds.
1255    pub fn release<C: DcaCache>(
1256        self,
1257        ctx: &mut DcaCacheCtx<'_, C>,
1258        current_target: Bank,
1259    ) -> Result<(), BankGuardOverrun> {
1260        let addr = self.bank_storage.addr_usize();
1261        let len = self.bank_storage.byte_len();
1262        ctx.cache.clean(addr, len);
1263        check_bank_overrun(self.bank, current_target)
1264    }
1265}
1266
1267/// Direction-specific `release` for `BankGuard<Write, _, _>`.
1268///
1269/// Per DCB-00 §5 the `Write` direction's cache op fires at
1270/// construction (`invalidate` over the inactive bank). `release`
1271/// performs only the INV-D15 CT-bit checkpoint — the CPU has
1272/// only read; no dirty lines need publishing.
1273impl<'a, T: Copy, const N: usize> BankGuard<'a, Write, T, N> {
1274    /// Release the guard. No cache op (the entry-side invalidate
1275    /// already drained stale lines). Performs the INV-D15 CT-bit
1276    /// checkpoint.
1277    ///
1278    /// `_ctx` is unused for `Write` direction (kept for symmetry
1279    /// with `BankGuard<Read>::release`).
1280    pub fn release<C: DcaCache>(
1281        self,
1282        _ctx: &mut DcaCacheCtx<'_, C>,
1283        current_target: Bank,
1284    ) -> Result<(), BankGuardOverrun> {
1285        check_bank_overrun(self.bank, current_target)
1286    }
1287}
1288
1289/// Internal helper: direction-independent INV-D15 live-recheck.
1290#[inline]
1291fn check_bank_overrun(guard_bank: Bank, current_target: Bank) -> Result<(), BankGuardOverrun> {
1292    if current_target == guard_bank {
1293        #[cfg(debug_assertions)]
1294        {
1295            panic!(
1296                "DCB BankGuard overrun: DMA flipped CT into the inactive bank ({guard_bank:?}) during the guard's lifetime; INV-D15 violated. Stream is faster than the CPU consumer/producer."
1297            );
1298        }
1299        #[cfg(not(debug_assertions))]
1300        {
1301            return Err(BankGuardOverrun { bank: guard_bank });
1302        }
1303    }
1304    Ok(())
1305}
1306
1307/// Error returned by [`BankGuard::release`] in release builds when the
1308/// DMA engine flipped its `CT` bit into the guarded bank during the
1309/// guard's lifetime — INV-D15 violation.
1310#[derive(Copy, Clone, Eq, PartialEq, Debug)]
1311pub struct BankGuardOverrun {
1312    /// The bank the guard exposed at construction.
1313    pub bank: Bank,
1314}
1315
1316/// Internal helper: the inactive bank, given the current target.
1317#[inline]
1318const fn inactive_bank(current_target: Bank) -> Bank {
1319    match current_target {
1320        Bank::M0 => Bank::M1,
1321        Bank::M1 => Bank::M0,
1322    }
1323}
1324
1325// ── Internal helpers ──────────────────────────────────────────────────
1326
1327/// Compute (addr, len) of one half of a `DcaBuf<T, N>` starting at
1328/// `base_addr`. INV-D2 ensures the byte length is a multiple of
1329/// CACHE_LINE; for the half-extent to also be a multiple of CACHE_LINE
1330/// (so the cache op on one half doesn't touch the other), `N * sizeof(T)`
1331/// MUST be a multiple of `2 * CACHE_LINE`. Asserted at runtime here;
1332/// the constructor will be tightened to a `const` assertion when
1333/// `generic_const_exprs` stabilises.
1334fn half_extent<T: Copy, const N: usize>(base_addr: usize, half: Half) -> (usize, usize) {
1335    let total = size_of::<T>() * N;
1336    debug_assert!(
1337        total.is_multiple_of(2 * CACHE_LINE),
1338        "DcaBuf used with half_guard must have N*sizeof(T) divisible by 2*CACHE_LINE so each half is itself cache-line aligned",
1339    );
1340    let half_bytes = total / 2;
1341    let off = match half {
1342        Half::First => 0,
1343        Half::Second => half_bytes,
1344    };
1345    (base_addr + off, half_bytes)
1346}
1347
1348/// Half boundaries as element indices.
1349const fn half_indices<const N: usize>(half: Half) -> (usize, usize) {
1350    let mid = N / 2;
1351    match half {
1352        Half::First => (0, mid),
1353        Half::Second => (mid, N),
1354    }
1355}
1356
1357// ── Tests ──────────────────────────────────────────────────────────────
1358
1359#[cfg(test)]
1360mod tests {
1361    use super::*;
1362
1363    // 32 bytes (16 i16 elements) — minimum INV-D2-compliant size.
1364    type Buf = DcaBuf<i16, 16>;
1365
1366    #[test]
1367    fn dcabuf_alignment_is_32() {
1368        let buf = Buf::new([0; 16]);
1369        let addr = (&buf as *const Buf) as usize;
1370        assert_eq!(addr % CACHE_LINE, 0, "INV-D1: 32-byte alignment");
1371    }
1372
1373    #[test]
1374    fn dcabuf_size_is_multiple_of_cache_line() {
1375        assert_eq!(core::mem::size_of::<Buf>() % CACHE_LINE, 0);
1376    }
1377
1378    #[test]
1379    fn cpu_round_trip_through_device_read() {
1380        let mut buf = Buf::new([0; 16]);
1381        let mut cache = NullCache::default();
1382        let mut ctx = DcaCacheCtx::new(&mut cache);
1383        let cpu = buf.cpu();
1384        let (pending, _addr) = cpu.lend_for_read(&mut ctx);
1385        let _ = pending.complete();
1386    }
1387
1388    #[test]
1389    fn lend_for_read_emits_clean() {
1390        let mut buf = Buf::new([0; 16]);
1391        let mut cache = NullCache::default();
1392        {
1393            let mut ctx = DcaCacheCtx::new(&mut cache);
1394            let cpu = buf.cpu();
1395            let (pending, _addr) = cpu.lend_for_read(&mut ctx);
1396            let _ = pending.complete();
1397        }
1398        assert!(matches!(cache.last, Some(NullCacheOp::Clean(_, 32))));
1399    }
1400
1401    #[test]
1402    fn lend_for_write_emits_invalidate() {
1403        let mut buf = Buf::new([0; 16]);
1404        let mut cache = NullCache::default();
1405        {
1406            let mut ctx = DcaCacheCtx::new(&mut cache);
1407            let cpu = buf.cpu();
1408            let (pending, _addr) = cpu.lend_for_write(&mut ctx);
1409            let _ = pending.complete();
1410        }
1411        assert!(matches!(cache.last, Some(NullCacheOp::Invalidate(_, 32))));
1412    }
1413
1414    #[test]
1415    fn cpu_can_read_and_write_storage() {
1416        let mut buf = Buf::new([0; 16]);
1417        let mut cpu = buf.cpu();
1418        cpu.as_mut_slice()[3] = 42;
1419        assert_eq!(cpu.as_slice()[3], 42);
1420    }
1421
1422    #[test]
1423    fn cpu_dma_addr_is_cache_line_aligned() {
1424        let mut buf = Buf::new([0; 16]);
1425        let cpu = buf.cpu();
1426        let addr = cpu.dma_addr();
1427        assert_eq!(addr.raw() as usize % CACHE_LINE, 0);
1428    }
1429
1430    #[test]
1431    fn circ_read_half_guard_emits_clean_per_half() {
1432        // 64 bytes → two 32-byte halves, both cache-line aligned.
1433        type CircBuf = DcaBuf<u8, 64>;
1434        let mut buf = CircBuf::new([0; 64]);
1435        let mut cache = NullCache::default();
1436        let mut ctx = DcaCacheCtx::new(&mut cache);
1437        let cpu = buf.cpu();
1438        let mut circ = cpu.start_circular_read(&mut ctx);
1439        assert!(matches!(
1440            ctx.cache_mut().last,
1441            Some(NullCacheOp::Clean(_, 64))
1442        ));
1443
1444        // Per DCB-01b-A 2026-05-03: Read direction emits NO cache op
1445        // at half_guard construction; the clean fires at release.
1446        let mut guard = circ.half_guard(&mut ctx, Half::Second);
1447        // The last op observed is still the start_circular_read clean
1448        // (no new op fired by half_guard).
1449        assert!(matches!(
1450            ctx.cache_mut().last,
1451            Some(NullCacheOp::Clean(_, 64))
1452        ));
1453        guard.as_mut_slice()[0] = 0xAB;
1454        // Engine reports we're still in First half — release OK.
1455        // The release-time clean for Read direction publishes the
1456        // CPU writes we just made.
1457        guard.release(&mut ctx, Half::First).unwrap();
1458        assert!(matches!(
1459            ctx.cache_mut().last,
1460            Some(NullCacheOp::Clean(_, 32))
1461        ));
1462
1463        let _cpu = circ.stop_circular(&mut ctx);
1464        assert!(matches!(
1465            ctx.cache_mut().last,
1466            Some(NullCacheOp::Clean(_, 64))
1467        ));
1468    }
1469
1470    #[test]
1471    fn circ_write_half_guard_emits_invalidate_per_half() {
1472        type CircBuf = DcaBuf<u8, 64>;
1473        let mut buf = CircBuf::new([0; 64]);
1474        let mut cache = NullCache::default();
1475        let mut ctx = DcaCacheCtx::new(&mut cache);
1476        let cpu = buf.cpu();
1477        let mut circ = cpu.start_circular_write(&mut ctx);
1478        assert!(matches!(
1479            ctx.cache_mut().last,
1480            Some(NullCacheOp::Invalidate(_, 64))
1481        ));
1482
1483        {
1484            let _guard = circ.half_guard(&mut ctx, Half::First);
1485        }
1486        assert!(matches!(
1487            ctx.cache_mut().last,
1488            Some(NullCacheOp::Invalidate(_, 32))
1489        ));
1490
1491        let _cpu = circ.stop_circular(&mut ctx);
1492        assert!(matches!(
1493            ctx.cache_mut().last,
1494            Some(NullCacheOp::Invalidate(_, 64))
1495        ));
1496    }
1497
1498    #[test]
1499    fn half_indices_split_evenly() {
1500        assert_eq!(half_indices::<16>(Half::First), (0, 8));
1501        assert_eq!(half_indices::<16>(Half::Second), (8, 16));
1502    }
1503
1504    #[test]
1505    #[cfg(not(debug_assertions))]
1506    fn half_guard_release_returns_err_on_overrun_in_release() {
1507        type CircBuf = DcaBuf<u8, 64>;
1508        let mut buf = CircBuf::new([0; 64]);
1509        let mut cache = NullCache::default();
1510        let mut ctx = DcaCacheCtx::new(&mut cache);
1511        let cpu = buf.cpu();
1512        let mut circ = cpu.start_circular_read(&mut ctx);
1513        let guard = circ.half_guard(&mut ctx, Half::Second);
1514        // DMA crossed into Second — overrun. Per DCB-01b-A the
1515        // release-time clean still fires before the overrun check.
1516        assert_eq!(
1517            guard.release(&mut ctx, Half::Second),
1518            Err(HalfGuardOverrun { half: Half::Second })
1519        );
1520    }
1521
1522    // ── DcaDoubleBuf (DCB-01b) ─────────────────────────────────────────
1523
1524    type DbBank = DcaBuf<i16, 16>;
1525
1526    #[test]
1527    fn dbufcpu_round_trip_through_active_double_buffer_write() {
1528        let mut m0 = DbBank::new([0; 16]);
1529        let mut m1 = DbBank::new([0; 16]);
1530        let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1531        let mut cache = NullCache::default();
1532        let mut ctx = DcaCacheCtx::new(&mut cache);
1533        let cpu = dca.cpu();
1534        let active = cpu.start_double_buffer_write(&mut ctx);
1535        let _cpu_back = active.stop_double_buffer(&mut ctx);
1536    }
1537
1538    #[test]
1539    fn dbufcpu_can_read_and_write_either_bank() {
1540        let mut m0 = DbBank::new([0; 16]);
1541        let mut m1 = DbBank::new([0; 16]);
1542        let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1543        let mut cpu = dca.cpu();
1544        cpu.as_m0_mut_slice()[3] = 42;
1545        cpu.as_m1_mut_slice()[7] = 99;
1546        assert_eq!(cpu.as_m0_slice()[3], 42);
1547        assert_eq!(cpu.as_m1_slice()[7], 99);
1548    }
1549
1550    #[test]
1551    fn dbufcpu_dma_addrs_are_distinct_and_aligned() {
1552        let mut m0 = DbBank::new([0; 16]);
1553        let mut m1 = DbBank::new([0; 16]);
1554        let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1555        let cpu = dca.cpu();
1556        let a0 = cpu.m0_dma_addr().raw() as usize;
1557        let a1 = cpu.m1_dma_addr().raw() as usize;
1558        assert_eq!(a0 % CACHE_LINE, 0);
1559        assert_eq!(a1 % CACHE_LINE, 0);
1560        assert_ne!(a0, a1, "M0 / M1 must address distinct memory");
1561    }
1562
1563    #[test]
1564    fn start_double_buffer_read_cleans_both_banks() {
1565        let mut m0 = DbBank::new([0; 16]);
1566        let mut m1 = DbBank::new([0; 16]);
1567        let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1568        let mut cache = NullCache::default();
1569        {
1570            let mut ctx = DcaCacheCtx::new(&mut cache);
1571            let cpu = dca.cpu();
1572            let active = cpu.start_double_buffer_read(&mut ctx);
1573            let _ = active.stop_double_buffer(&mut ctx);
1574        }
1575        // The last operation observed by the cache during start_*_read
1576        // is the *second* clean (M1). The first clean (M0) was overwritten.
1577        // Accept either; both must be `Clean`.
1578        assert!(matches!(cache.last, Some(NullCacheOp::Clean(_, 32))));
1579    }
1580
1581    #[test]
1582    fn start_double_buffer_write_invalidates_both_banks() {
1583        let mut m0 = DbBank::new([0; 16]);
1584        let mut m1 = DbBank::new([0; 16]);
1585        let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1586        let mut cache = NullCache::default();
1587        {
1588            let mut ctx = DcaCacheCtx::new(&mut cache);
1589            let cpu = dca.cpu();
1590            let active = cpu.start_double_buffer_write(&mut ctx);
1591            let _ = active.stop_double_buffer(&mut ctx);
1592        }
1593        assert!(matches!(cache.last, Some(NullCacheOp::Invalidate(_, 32))));
1594    }
1595
1596    #[test]
1597    fn dbuf_read_bank_guard_emits_clean_on_inactive_bank() {
1598        let mut m0 = DbBank::new([0; 16]);
1599        let mut m1 = DbBank::new([0; 16]);
1600        let m0_addr = (&raw const m0) as usize;
1601        let m1_addr = (&raw const m1) as usize;
1602        let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1603        let mut cache = NullCache::default();
1604        let mut ctx = DcaCacheCtx::new(&mut cache);
1605        let cpu = dca.cpu();
1606        let mut active = cpu.start_double_buffer_read(&mut ctx);
1607        // Engine currently on M0 → inactive bank is M1.
1608        // Per DCB-01b-A 2026-05-03: Read direction emits NO cache op
1609        // at bank_guard construction; the clean fires at release.
1610        let mut guard = active.bank_guard(&mut ctx, Bank::M0);
1611        assert_eq!(guard.bank(), Bank::M1);
1612        guard.as_mut_slice()[0] = 0xAB;
1613        // Engine still on M0 — release OK. The release-time clean
1614        // for Read direction publishes the CPU writes we just made.
1615        guard.release(&mut ctx, Bank::M0).unwrap();
1616        // Confirm cache op was a Clean over M1's extent (32 bytes).
1617        let last = ctx.cache_mut().last.unwrap();
1618        match last {
1619            NullCacheOp::Clean(addr, len) => {
1620                assert_eq!(len, 32);
1621                assert_eq!(addr, m1_addr, "expected M1 (inactive) cleaned");
1622                assert_ne!(addr, m0_addr);
1623            }
1624            other => panic!("expected Clean, got {other:?}"),
1625        }
1626    }
1627
1628    #[test]
1629    fn dbuf_write_bank_guard_emits_invalidate_on_inactive_bank() {
1630        let mut m0 = DbBank::new([0; 16]);
1631        let mut m1 = DbBank::new([0; 16]);
1632        let m0_addr = (&raw const m0) as usize;
1633        let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1634        let mut cache = NullCache::default();
1635        let mut ctx = DcaCacheCtx::new(&mut cache);
1636        let cpu = dca.cpu();
1637        let mut active = cpu.start_double_buffer_write(&mut ctx);
1638        // Engine currently on M1 → inactive bank is M0.
1639        let guard = active.bank_guard(&mut ctx, Bank::M1);
1640        assert_eq!(guard.bank(), Bank::M0);
1641        let last = ctx.cache_mut().last.unwrap();
1642        match last {
1643            NullCacheOp::Invalidate(addr, len) => {
1644                assert_eq!(len, 32);
1645                assert_eq!(addr, m0_addr, "expected M0 (inactive) invalidated");
1646            }
1647            other => panic!("expected Invalidate, got {other:?}"),
1648        }
1649        guard.release(&mut ctx, Bank::M1).unwrap();
1650    }
1651
1652    #[test]
1653    fn inactive_bank_swaps_correctly() {
1654        assert_eq!(inactive_bank(Bank::M0), Bank::M1);
1655        assert_eq!(inactive_bank(Bank::M1), Bank::M0);
1656    }
1657
1658    #[test]
1659    #[cfg(not(debug_assertions))]
1660    fn bank_guard_release_returns_err_on_overrun_in_release() {
1661        let mut m0 = DbBank::new([0; 16]);
1662        let mut m1 = DbBank::new([0; 16]);
1663        let mut dca = DcaDoubleBuf::new(&mut m0, &mut m1);
1664        let mut cache = NullCache::default();
1665        let mut ctx = DcaCacheCtx::new(&mut cache);
1666        let cpu = dca.cpu();
1667        let mut active = cpu.start_double_buffer_read(&mut ctx);
1668        let guard = active.bank_guard(&mut ctx, Bank::M0);
1669        // Guard exposes M1; engine flipped CT into M1 → overrun.
1670        // Per DCB-01b-A the release-time clean still fires before
1671        // the overrun check.
1672        assert_eq!(
1673            guard.release(&mut ctx, Bank::M1),
1674            Err(BankGuardOverrun { bank: Bank::M1 })
1675        );
1676    }
1677
1678    // ── LtdcScan (DCB-01c) ──────────────────────────────────────────
1679
1680    type ScanBuf = DcaBuf<u8, 64>;
1681
1682    #[test]
1683    fn ltdc_scan_round_trip_through_cpu() {
1684        let mut buf = ScanBuf::new([0; 64]);
1685        let mut cache = NullCache::default();
1686        let mut ctx = DcaCacheCtx::new(&mut cache);
1687        let cpu = buf.cpu();
1688        let scan = cpu.start_ltdc_scan(&mut ctx);
1689        let _cpu_back = scan.stop_scan(&mut ctx);
1690    }
1691
1692    #[test]
1693    fn start_ltdc_scan_emits_clean_then_barrier() {
1694        let mut buf = ScanBuf::new([0; 64]);
1695        let mut cache = NullCache::default();
1696        {
1697            let mut ctx = DcaCacheCtx::new(&mut cache);
1698            let cpu = buf.cpu();
1699            let scan = cpu.start_ltdc_scan(&mut ctx);
1700            // After start: last op observed is the barrier (issued
1701            // *after* the clean per INV-D16).
1702            assert_eq!(ctx.cache_mut().last, Some(NullCacheOp::Barrier));
1703            let _ = scan.stop_scan(&mut ctx);
1704        }
1705        // After stop: the FINAL op is also a barrier (clean + barrier
1706        // emitted at exit).
1707        assert_eq!(cache.last, Some(NullCacheOp::Barrier));
1708    }
1709
1710    #[test]
1711    fn ltdc_scan_paint_full_yields_buffer_slice() {
1712        let mut buf = ScanBuf::new([0; 64]);
1713        let mut cache = NullCache::default();
1714        let mut ctx = DcaCacheCtx::new(&mut cache);
1715        let cpu = buf.cpu();
1716        let mut scan = cpu.start_ltdc_scan(&mut ctx);
1717        let pixels = scan.paint_full();
1718        pixels[0] = 0xAA;
1719        pixels[63] = 0x55;
1720        // Borrow ends here; subsequent paint_full / present ok.
1721        assert_eq!(scan.paint_full()[0], 0xAA);
1722        assert_eq!(scan.paint_full()[63], 0x55);
1723    }
1724
1725    #[test]
1726    fn ltdc_scan_present_emits_clean_then_barrier_no_transition() {
1727        let mut buf = ScanBuf::new([0; 64]);
1728        let mut cache = NullCache::default();
1729        let mut ctx = DcaCacheCtx::new(&mut cache);
1730        let cpu = buf.cpu();
1731        let mut scan = cpu.start_ltdc_scan(&mut ctx);
1732        // Drop the start_ltdc_scan op record by observing it then
1733        // calling present, which must emit clean + barrier again.
1734        scan.paint_full()[5] = 0x42;
1735        scan.present(&mut ctx);
1736        assert_eq!(ctx.cache_mut().last, Some(NullCacheOp::Barrier));
1737        // Typestate is still LtdcScan — paint_full should still work.
1738        scan.paint_full()[7] = 0x33;
1739        scan.present(&mut ctx);
1740        let _ = scan.stop_scan(&mut ctx);
1741    }
1742
1743    #[test]
1744    fn ltdc_scan_dma_addr_is_cache_line_aligned() {
1745        let mut buf = ScanBuf::new([0; 64]);
1746        let mut cache = NullCache::default();
1747        let mut ctx = DcaCacheCtx::new(&mut cache);
1748        let cpu = buf.cpu();
1749        let scan = cpu.start_ltdc_scan(&mut ctx);
1750        let addr = scan.dma_addr();
1751        assert_eq!(addr.raw() as usize % CACHE_LINE, 0);
1752        let _ = scan.stop_scan(&mut ctx);
1753    }
1754
1755    #[test]
1756    fn ltdc_scan_present_clean_extent_is_full_buffer() {
1757        // Track multiple ops by inspecting them in sequence — switch
1758        // to a custom local `LoggingCache` that records all calls.
1759        struct LoggingCache {
1760            ops: alloc::vec::Vec<NullCacheOp>,
1761        }
1762        impl DcaCache for LoggingCache {
1763            fn clean(&mut self, addr: usize, len: usize) {
1764                self.ops.push(NullCacheOp::Clean(addr, len));
1765            }
1766            fn invalidate(&mut self, addr: usize, len: usize) {
1767                self.ops.push(NullCacheOp::Invalidate(addr, len));
1768            }
1769            fn clean_invalidate(&mut self, addr: usize, len: usize) {
1770                self.ops.push(NullCacheOp::CleanInvalidate(addr, len));
1771            }
1772            fn barrier(&mut self) {
1773                self.ops.push(NullCacheOp::Barrier);
1774            }
1775        }
1776
1777        let mut buf = ScanBuf::new([0; 64]);
1778        let mut cache = LoggingCache {
1779            ops: alloc::vec::Vec::new(),
1780        };
1781        {
1782            let mut ctx = DcaCacheCtx::new(&mut cache);
1783            let cpu = buf.cpu();
1784            let mut scan = cpu.start_ltdc_scan(&mut ctx);
1785            scan.present(&mut ctx);
1786            let _ = scan.stop_scan(&mut ctx);
1787        }
1788        // Expected sequence: start (clean+barrier), present (clean+
1789        // barrier), stop (clean+barrier) = 6 ops, alternating.
1790        assert_eq!(cache.ops.len(), 6);
1791        for (i, op) in cache.ops.iter().enumerate() {
1792            if i % 2 == 0 {
1793                assert!(
1794                    matches!(op, NullCacheOp::Clean(_, 64)),
1795                    "expected Clean(_, 64) at index {i}, got {op:?}"
1796                );
1797            } else {
1798                assert_eq!(*op, NullCacheOp::Barrier);
1799            }
1800        }
1801    }
1802}