embedded-3dgfx 0.7.2

3D graphics rendering for embedded systems (fork of embedded-gfx by Kezii)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# Application Integration

How to put `embedded-3dgfx` inside an application you already have.

The engine has **no `run()`**. It is a library of stages you call: you own the
loop, the buffers, and the clock. This guide walks the six seams where your code
and the engine meet, in the order you will hit them.

Every snippet below has a complete, runnable counterpart in
[`examples/integration_*.rs`](../examples) — see the [example index](#runnable-examples)
at the end.

---

## The contract at a glance

You provide:

| You own | Type | Notes |
|---------|------|-------|
| Framebuffer | any `DrawTarget<Color = Rgb565> + OriginDimensions` | your panel, your `&mut [Rgb565]`, or the simulator |
| Depth buffer | `&mut [ZDepth]` | exactly `width * height` entries |
| Command buffer | `CommandBuffer<MAX>` | `heapless`; capacity is primitives, not bytes |

You call, per frame:

```text
clear → record → execute → present
```

---

## Imports: one line for the engine

Everything the frame loop touches comes from the prelude:

```rust
use embedded_3dgfx::prelude::*;
```

That single import covers the frame driver (`K3dengine`), the buffers
(`CommandBuffer`, `FrameCtx`, `ZDepth`, `Z_MAX_VALUE`), geometry (`Geometry`,
`K3dMesh`, `RenderMode`, `Camera`), primitives (`DrawPrimitive`), per-pass
configuration (`RasterState`, `draw_zbuffered_with_state`, `FogConfig`,
`DitherConfig`, `apply_default_caps`), the error taxonomy (`RenderError`,
`BudgetKind`, `DisplayError`) and the stage markers (`Stage`, `StageKind`).
Feature-gated types (`PointLight`, `Texture` / `TextureManager`) join it when the
feature is enabled.

Writing a **display backend** needs exactly one more import. The output stage
gathers its own vocabulary, so a driver never reaches into four submodules:

```rust
use embedded_3dgfx::pipeline::output::{
    DMACapableFrameBufferBackend, DisplayBackend, DisplayRegion, DmaTransfer, FrameBuf,
    StandardSwapChain, TransferError,
};
```

`FrameBuf` and `DMACapableFrameBufferBackend` are re-exported there on purpose:
they appear in the backend trait signatures, so implementing a backend does not
require `embedded-graphics-framebuf` in your own `Cargo.toml`.

Everything else — `telemetry`, `config` policies, `scene_format`, `physics`,
or an individual stage module like `pipeline::shade::shader` — stays explicitly
named, so it is always obvious which subsystem you reached into.

---

## 1. Framebuffer, depth buffer, command buffer

The engine needs a pixel sink. The only trait involved is
`embedded_graphics_core::draw_target::DrawTarget`; the whole adapter is ~30
lines:

```rust
struct Framebuffer<'a> {
    pixels: &'a mut [Rgb565],
    width: usize,
    height: usize,
}

impl OriginDimensions for Framebuffer<'_> {
    fn size(&self) -> Size {
        Size::new(self.width as u32, self.height as u32)
    }
}

impl DrawTarget for Framebuffer<'_> {
    type Color = Rgb565;
    type Error = core::convert::Infallible;

    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
    where
        I: IntoIterator<Item = Pixel<Self::Color>>,
    {
        for Pixel(point, color) in pixels {
            if point.x < 0 || point.y < 0 {
                continue; // the rasterizer can emit off-screen pixels
            }
            let (x, y) = (point.x as usize, point.y as usize);
            if x < self.width && y < self.height {
                self.pixels[y * self.width + x] = color;
            }
        }
        Ok(())
    }

    fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
        self.pixels.fill(color); // override the default per-pixel path
        Ok(())
    }
}
```

Full version: [`integration_minimal.rs`](../examples/integration_minimal.rs).

**Depth buffer.** `ZDepth` is `u32`, or `u16` with the `depth-u16` feature
(halves the memory). Fill it with `Z_MAX_VALUE` each frame:

```rust
let mut zbuffer = vec![Z_MAX_VALUE; WIDTH * HEIGHT];
```

`FrameCtx` validates the length when you execute, so a mismatch is a clean
`RenderError::OutOfBudget(BudgetKind::ZBufferLength { .. })` rather than a
silent wrap.

**Command buffer.** `CommandBuffer<MAX>` holds up to `MAX` primitives. Size it
above the largest frame you expect, e.g. `MAX ≈ max_draw_primitives` from your
[profile](#4-budgets-telemetry-and-degradation):

```rust
let mut commands = CommandBuffer::<8192>::new();
```

Overflow is reported, not fatal: `record` returns
`RenderError::OutOfBudget(BudgetKind::DrawPrimitives { attempted, max })`, and
`record_with_fallback` / `record_with_degradation` can shed work instead.

**Memory math per frame** — a `320x240` configuration with `depth-u16`:

| Buffer | Bytes |
|--------|-------|
| Framebuffer (`Rgb565`) | 153,600 |
| Z-buffer (`u16`) | 153,600 |
| Command buffer | `MAX * size_of::<RenderCommand>()` |
| Double buffer | +153,600 |

See [`docs/feature-size.md`](feature-size.md) for measured `.text` budgets.

---

## 2. The frame loop

Two calls, deliberately split:

```rust
// Record: CPU work — transforms, culling, primitive assembly.
engine.record(core::iter::once(&mesh), &mut commands, None)?;

// Execute: rasterize the recorded commands into your target.
let mut frame = FrameCtx { zbuffer: &mut zbuffer, width: WIDTH, height: HEIGHT };
engine.execute::<_, 8192>(&mut fb, &mut frame, &commands, None)?;
```

Why the split matters for integration:

- `record` takes `&self` and never touches the framebuffer, so it is safe to run
  **while DMA is still pushing the previous frame** — the CPU is not idle.
- `execute` writes only to the target you pass. It does not allocate and does not
  own your buffers.
- You must clear both buffers yourself each frame; the engine does not clear
  unless you record a clear command.

`record_with_degradation` takes `&mut self` because applying a degradation step
changes engine state (quality tier).

Full version: [`integration_app_loop.rs`](../examples/integration_app_loop.rs).

---

## 3. Presenting to a panel

### The simple case

You already own the pixel slice, so presentation is whatever your platform does:
`memcpy` into panel RAM, stream over SPI, or hand the slice to a DMA channel.

### Swap chain

For double buffering with an ownership-safe handoff, use a swap chain:

```rust
use embedded_3dgfx::pipeline::output::swapchain::StandardSwapChain;

let mut swap = StandardSwapChain::<320, 240, _>::from_static_slices(
    front_data,   // &'static mut [Rgb565]
    back_data,    // &'static mut [Rgb565]
    false,        // true if your panel wants big-endian pixels
    MyPanelBackend::new(),
);

loop {
    // The back buffer is always available; DMA only ever touches the front.
    let back = swap.get_back_buffer();
    back.clear(Rgb565::BLACK)?;
    // …record + execute into `back`…

    swap.present()?;          // blocking: waits for the previous transfer
    // or: swap.try_present()?  -> Err(DisplayError::Busy) if still transferring
}
```

`DisplayError::Busy` is **normal back-pressure**, not a failure: wait with
`swap.wait_for_vsync()` (or drop the frame) and retry.

### Your own display backend

Presentation goes through two traits in the output stage:

- `DisplayBackend<W, H, FB>` — starts a transfer of a framebuffer.
- `DmaTransfer` — the in-flight token. It **owns** the framebuffer until the
  hardware finishes, so the borrow checker prevents writing to a buffer the
  panel is still reading. Implement `Drop` to cancel the DMA channel, so a
  dropped token can never leave the controller pointing at freed memory.

```rust
impl<FB: DMACapableFrameBufferBackend<Color = Rgb565>> DmaTransfer for MyTransfer<FB> {
    type Buffer = FrameBuf<Rgb565, FB>;

    fn is_done(&self) -> bool { /* read the DMA status flag */ true }
    fn wait(self) -> Self::Buffer { /* WFI until the ISR fires, then return it */ }
}
```

Full version (including back-pressure handling):
[`integration_custom_backend.rs`](../examples/integration_custom_backend.rs).

### Bare metal: signalling completion from an ISR

`CompletionSlot` is runtime-agnostic. Signal it from the DMA interrupt, then use
`WaitTransfer` as the token — it polls the same slot synchronously, and doubles
as a future under an async executor.

```rust
use embedded_3dgfx::pipeline::output::completion::{CompletionSlot, WaitTransfer};

static DMA_DONE: CompletionSlot = CompletionSlot::new();

#[interrupt]
fn DMA_IRQ() {
    DMA_DONE.signal();          // safe from interrupt context
}

// in your backend:
fn start_dma_transfer(&mut self, fb: FrameBuf<Rgb565, FB>) -> Result<Self::Transfer, _> {
    // …program the DMA channel with `fb`'s address and length…
    Ok(WaitTransfer::new(fb, &DMA_DONE))   // resets the slot for you
}
```

### Async

If your executor can await, both the transfer and the present are awaitable:

```rust
swap.present_async().await?;          // requires AsyncDmaTransfer on the token
swap.wait_for_vsync_async().await;
```

### Triple buffering

The `triple-buffering` feature adds `TripleSwapChain`, which rotates render /
ready / display buffers so the CPU never waits for scan-out. Use it when your
frame times are bursty.

---

## 4. Budgets, telemetry and degradation

`apply_default_caps` adopts a per-MCU budget profile (defaults to
`PROFILE_M33_BALANCED`, which caps a frame at **320×240**):

```rust
use embedded_3dgfx::config::{apply_default_caps, PROFILE_M55_PERF};

apply_default_caps(&mut engine);        // default profile
engine.set_caps(PROFILE_M55_PERF);      // or pick one explicitly
engine.clear_caps();                    // or lift the caps entirely
```

Runtime override for desktop builds: `EMBEDDED_3DGFX_CAPS=off` (or
`=m0|m3|m4|m33|m55`). Compile-time: the `desktop-unbounded` feature.

Telemetry is opt-in and free when unused — pass `None`:

```rust
let mut record_tel = RecordTelemetry::default();
let mut execute_tel = ExecuteTelemetry::default();
engine.record(&meshes, &mut commands, Some(&mut record_tel))?;
engine.execute::<_, 8192>(&mut fb, &mut frame, &commands, Some(&mut execute_tel))?;
```

`RecordTelemetry` gives `meshes_visible`, `unique_textures`, `draw_commands`;
`ExecuteTelemetry` gives `commands_total` and the clear counts. Feed them to a
HUD or a host-side logger.

**Graceful degradation** — instead of failing a heavy frame, shed work:

```rust
static DEGRADATION: [DegradationStep; 2] = [
    DegradationStep::MeshDecimationStride(2),
    DegradationStep::DowngradeQuality,
];

let outcome = engine.record_with_degradation(
    &meshes,
    &mut commands,
    DegradationPolicy { steps: &DEGRADATION },
    Some(&mut record_tel),
)?;
if outcome.used_degradation {
    // surface it: the frame is cheaper but visibly different
}
```

Both steps are no-ops on a frame that fits, so the policy can stay enabled
permanently.

---

## 5. Assets & textures

Textures live in a fixed-capacity `TextureManager<N>` holding `'static` pixel
data, and are looked up by the id on `Geometry::texture_id`:

```rust
use embedded_3dgfx::pipeline::rasterize::texture::{Texture, TextureManager};

let mut textures = TextureManager::<8>::new();
let id = textures.add_texture(Texture::new(include_bytes!(...), 32, 32)).unwrap();

let mut frame = FrameCtx { zbuffer: &mut zbuffer, width: W, height: H };
engine.execute_with_textures::<_, 8192, 8>(
    &mut fb, &mut frame, &commands, &textures, None,
)?;
```

`Geometry` borrows its slices, so keep vertex/face/texture arrays alive for at
least as long as the `K3dMesh` — `&'static` data straight from flash is ideal
(see [`docs/asset-pipeline.md`](asset-pipeline.md) for the offline generator).

---

## 6. Overlays and custom shading

**HUD / 2D overlay.** Anything that implements `DrawTarget` can draw on top of
the 3D frame — including `embedded-graphics` primitives and text:

```rust
engine.execute::<_, 8192>(&mut display, &mut frame, &commands, None)?;

let style = MonoTextStyle::new(&FONT_6X10, Rgb565::CSS_WHITE);
Text::new(&format!("{fps:.1} fps"), Point::new(4, 10), style).draw(&mut display)?;
```

Draw it *after* `execute`, and remember the z-buffer is independent of your 2D
overlay.

**Custom passes that match the engine's effects.** `engine.raster_state(w, h)`
returns the engine's own per-pass configuration — fog, dither, tint, palette,
stipple, depth bias, depth interpolation. Feed it to a raster entry point and
your hand-rolled geometry is pixel-identical to the engine's:

```rust
use embedded_3dgfx::pipeline::rasterize::draw::draw_zbuffered_with_state;

let state = engine.raster_state(WIDTH, HEIGHT);
draw_zbuffered_with_state(primitive, &mut display, &mut zbuffer, &state);
```

**Custom materials.** `FragmentShader` is the seam for procedural shading. The
built-in decorators wrap any shader, so fog/dither apply to your material too:

```rust
let material = MyWaveShader { base_color, time };
let fogged   = FogShader   { inner: material, fog: &fog_config };
let shaded   = DitherShader{ inner: fogged,   dither: &dither_config };

let color = shaded.shade(x, y, z, interpolants);
```

Note this is an evaluation API: you compose the stack and sample it per
fragment. The engine's built-in rasterization is driven by `DrawPrimitive`, so a
custom material is used either to compute colours you feed into primitives, or
in your own raster pass on top of `RasterState`.

---

## Choosing features per target

| Target | Suggestion |
|--------|------------|
| Desktop / simulator | `std`, `desktop-unbounded` |
| Cortex-M0/M0+ | `row_width_96`, `depth-u16`, no `lighting`/`textured` |
| Cortex-M4F | `row_width_160`, `depth-u16`, `lighting` |
| Cortex-M33/M55 | `row_width_240`, `depth-u16`, `lighting`, `textured` |
| Doom-style raycaster | add `raycast`, `hud` |
| Physics / animation | add `physics`, `scene` |

`row_width_*` sizes the internal scanline buffer; it must cover the frame width.
If several are enabled the widest wins deterministically.

---

## Gotchas

1. **Clear both buffers before `execute`.** The engine does not clear for you.
2. **`FrameCtx` depth length must equal `width * height`** — otherwise
   `RenderError::OutOfBudget(BudgetKind::ZBufferLength)`.
3. **Default caps are 320×240.** A larger frame fails with
   `OutOfBudget(FramebufferDimensions { .. })` until you raise the caps
   (`desktop-unbounded`, `EMBEDDED_3DGFX_CAPS=off`, or `set_caps`).
4. **Command buffer capacity is primitives, not pixels.** Undersizing it
   surfaces as `BudgetKind::DrawPrimitives`.
5. **`DisplayError::Busy` is routine.** Handle it as back-pressure.
6. **Public enums are `#[non_exhaustive]`.** Match with a `_` arm — including
   `DisplayError`, `RenderMode` and the error taxonomy.
7. **`record_with_degradation` needs `&mut engine`**, unlike plain `record`.
8. **Geometry borrows its data.** `K3dMesh` cannot outlive the arrays behind its
   `Geometry`. Build one with `Geometry::new(&vertices, &faces)` and add the
   optional slots with the `with_*` builders — or use
   `Geometry { vertices, faces, ..Default::default() }` if you already hold the
   data as named struct fields.
9. **With the simulator, call `window.update()` once before `window.events()`** —
   the event iterator panics otherwise.

---

## Runnable examples

| Example | Seam it demonstrates | Run |
|---------|---------------------|-----|
| [`integration_minimal`]../examples/integration_minimal.rs | your own `DrawTarget`, one frame, no SDL | `cargo run --example integration_minimal --features std` |
| [`integration_app_loop`]../examples/integration_app_loop.rs | fixed timestep, input, telemetry, degradation, `RasterState` overlay | `cargo run --example integration_app_loop --features std` |
| [`integration_custom_backend`]../examples/integration_custom_backend.rs | `DisplayBackend` / `DmaTransfer`, swap chain, back-pressure | `cargo run --example integration_custom_backend --features std` |

All three accept `E3DGFX_EXAMPLE_FRAMES=<n>` to exit after `n` frames, which
makes them usable as headless smoke tests:

```bash
# No display server needed.
SDL_VIDEODRIVER=dummy E3DGFX_EXAMPLE_FRAMES=60 \
  cargo run --example integration_app_loop --features std
```

### Related reading

| Document | Topic |
|----------|-------|
| [`MIGRATION.md`]../MIGRATION.md | Upgrading from the pre-`pipeline` module layout |
| [`docs/backend-integration.md`]backend-integration.md | Board bring-up, memory sizing, hardware profiling |
| [`docs/caps-and-telemetry.md`]caps-and-telemetry.md | Caps, telemetry, CI budgets |
| [`docs/feature-size.md`]feature-size.md | Slim vs full flash (`.text`) budgets |
| [`docs/asset-pipeline.md`]asset-pipeline.md | Offline assets, scene streaming |