Skip to content

Safe DMA transfer API: ownership-based framebuffer locking#31

Merged
leftger merged 3 commits into
masterfrom
safe-dma-transfer-api
May 30, 2026
Merged

Safe DMA transfer API: ownership-based framebuffer locking#31
leftger merged 3 commits into
masterfrom
safe-dma-transfer-api

Conversation

@leftger

@leftger leftger commented May 30, 2026

Copy link
Copy Markdown
Owner

Closes #30.

Summary

  • DisplayBackend::start_dma_transfer now takes FrameBuf by value and returns a DmaTransfer token
  • The buffer is locked inside the token until DmaTransfer::wait(self) returns it — the compiler makes concurrent access impossible
  • SwapChain is restructured with a FrontState<FB, Xfer> enum (Idle / InFlight) so the front buffer is always either owned by the swap chain or owned by the transfer token, never both
  • SimulatorBackend gains a CompletedTransfer<FB> type (immediately done, zero hardware cost)
  • TripleSwapChain follows the same pattern
  • All existing call sites (present, try_present, get_back_buffer, dma_rendering_demo.rs) are unchanged

Migration guide for DisplayBackend implementors

Before:
```rust
fn start_dma_transfer(&mut self, framebuffer: &FrameBuf<Rgb565, FB>) -> Result<(), DisplayError>;
fn wait_for_dma(&mut self);
fn is_dma_ready(&self) -> bool;
```

After:
```rust
type Transfer: DmaTransfer<Buffer = FrameBuf<Rgb565, FB>>;

fn start_dma_transfer(
&mut self,
framebuffer: FrameBuf<Rgb565, FB>, // by value, not by reference
) -> Result<Self::Transfer, TransferError>; // return token, not ()
```

Implement DmaTransfer for your transfer type:
```rust
impl DmaTransfer for MyTransfer {
type Buffer = FrameBuf<Rgb565, MyFB>;
fn is_done(&self) -> bool { /* poll DMA done flag */ }
fn wait(self) -> FrameBuf<Rgb565, MyFB> {
while !self.is_done() {}
self.framebuffer
}
}
```

Implement Drop on your transfer type to cancel DMA if the token is dropped before wait is called.

Test plan

  • cargo test passes (46 tests without physics, 50 with --features physics)
  • cargo test --features triple-buffering passes
  • cargo check --example dma_rendering_demo --features std passes with no changes to the example

Made with Cursor

leftger added 3 commits May 30, 2026 13:07
…aces

The previous `start_dma_transfer(&self, framebuffer: &FrameBuf<...>)`
released the borrow immediately on return, leaving the caller free to
write to the buffer while DMA was still reading it — a data race the
compiler could not catch.

The new design follows the embedded-hal ecosystem pattern (stm32f4xx-hal,
nrf-hal, Embedonomicon): `start_dma_transfer` takes the framebuffer by
value and returns a `DmaTransfer` token. The buffer is locked inside the
token until `DmaTransfer::wait` returns it, making concurrent access a
compile error.

Changes:
- New `DmaTransfer` trait with `is_done(&self)` and `wait(self) -> Buffer`
- New `TransferError<FB>` carries the buffer back on failure so it is never lost
- `DisplayBackend::start_dma_transfer` now takes `FrameBuf` by value and
  returns `Result<Self::Transfer, TransferError<FB>>`
- `wait_for_dma`, `is_dma_ready`, `present`, `present_region` removed from
  the trait — waiting/polling moves to `DmaTransfer`, presenting stays in
  `SwapChain`
- `SimulatorBackend` gains `CompletedTransfer<FB>` — an immediately-done token
- `SwapChain` restructured with a `FrontState<FB, Xfer>` enum: the front
  buffer is either `Idle(FrameBuf)` or `InFlight(Transfer)`, preventing
  any access path to the buffer while DMA is active
- `TripleSwapChain` follows the same pattern
- All call sites (`dma_rendering_demo.rs`, `present()` etc.) are unchanged
@leftger
leftger merged commit 820fd1d into master May 30, 2026
19 checks passed
@leftger
leftger deleted the safe-dma-transfer-api branch May 30, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DisplayBackend::start_dma_transfer allows data races via shared reference

1 participant