This guide covers how to run the record/execute pipeline with deterministic memory budgets, lightweight telemetry, and CI enforcement.
Embedded targets need fixed, predictable frame costs. Different Cortex-M classes have different practical limits. Telemetry lets you measure record-time and execute-time pressure without heap allocation.
The two-phase API separates scene traversal from rasterization:
- Record — traverse the scene graph, emit draw commands into a bounded
CommandBuffer<N> - Execute — rasterize the command buffer against your draw target and z-buffer
This keeps traversal and raster execution explicit and avoids hidden double work. Command buffers can be replayed without re-traversing the scene graph.
Apply default caps immediately after creating the engine:
use embedded_3dgfx::engine::K3dengine;
use embedded_3dgfx::config::apply_default_caps;
let mut engine = K3dengine::new(320, 240);
apply_default_caps(&mut engine);Resolution:
desktop-unboundedfeature enabled → caps disabled- otherwise → embedded balanced profile (defaults to M33 balanced)
Set caps explicitly with profile constants from embedded_3dgfx::config:
engine.set_caps(embedded_3dgfx::config::ProfileCaps { /* ... */ });Telemetry structs are plain counters passed into record/execute calls:
RecordTelemetry
meshes_total,meshes_visible,unique_textures,draw_commands,fallback_used
ExecuteTelemetry
commands_total,draw_commands,clear_color_commands,clear_depth_commands
let mut record_tel = embedded_3dgfx::telemetry::RecordTelemetry::default();
let mut execute_tel = embedded_3dgfx::telemetry::ExecuteTelemetry::default();
engine.record(meshes.iter(), &mut commands, Some(&mut record_tel))?;
engine.execute(&mut display, &mut frame_ctx, &commands, Some(&mut execute_tel))?;Pass None in hot paths to eliminate instrumentation overhead.
When caps are exceeded, calls return RenderError::OutOfBudget(BudgetKind::...).
Common BudgetKind variants:
MeshesPerFrame— too many visible meshesTrianglesPerMesh— individual LOD too denseVerticesPerMesh— per-mesh geometry too largeTextures— too many unique textures in one frameZBufferLength— z-buffer slice size mismatch
Typical responses: reduce mesh complexity, lower visible count, increase CommandBuffer<N> capacity, or pick a less strict profile.
Choose N from measured draw command counts:
- Measure
record_telemetry.draw_commandsin steady and stress scenes - Add 20–40% margin
- Keep
Nfixed per profile/build for deterministic memory use
triple-buffering — exposes TripleSwapChain APIs for smoother frame pacing at higher memory cost:
cargo check --lib --features triple-bufferingfixed-transform — routes projection through 16.16 fixed-point helpers (src/fixed_math.rs) for non-FPU targets:
cargo check --lib --features fixed-transformThe fixed-point path uses 16.16 arithmetic. Unit tests enforce near-roundtrip fidelity; pixel-space tolerance target is ≤ 1 px for typical scene ranges.
dwt-profiler / rtt-trace / itm-trace — board instrumentation hooks (see docs/backend-integration.md):
cargo check --lib --no-default-features --features "row_width_240 perfcounter dwt-profiler"Telemetry HUD formatting is shared in examples/shared/perf_hud.rs. Current telemetry demos: dma_rendering_demo, lod_demo.
Integration tests emit structured telemetry lines consumed by .github/scripts/check_telemetry_budget.py:
| Class | Meaning | Expected fallback_used |
|---|---|---|
CI_TELEMETRY |
Steady scene | 0 |
CI_TELEMETRY_STRESS |
High-load scene | 0 |
CI_TELEMETRY_FAILSOFT |
Intentionally over-budget | 1 |
The checker validates all matching lines in the stream (not just the last), so multiple snapshot tests can be enforced in one CI step.
| Lane | record_draw_commands | execute_commands_total | execute_draw_commands |
|---|---|---|---|
CI_TELEMETRY |
≤ 3 | ≤ 4 | ≤ 3 |
CI_TELEMETRY_STRESS |
≤ 24 | ≤ 25 | ≤ 24 |
CI_TELEMETRY_FAILSOFT |
≤ 1 | ≤ 2 | ≤ 1 |
CI_TELEMETRY_FAILSOFT also requires primary_budget_kind=MeshesPerFrame (configurable via TELEMETRY_FAILSOFT_EXPECTED_BUDGET_KIND).
| Profile lane | record_draw_commands | execute_commands_total | execute_draw_commands |
|---|---|---|---|
m3 |
≤ 3 | ≤ 4 | ≤ 3 |
m4 |
≤ 3 | ≤ 4 | ≤ 3 |
m33 |
≤ 3 | ≤ 4 | ≤ 3 |
m55 (profile-budget) |
≤ 3 | ≤ 4 | ≤ 3 |
m55-perf-budget |
≤ 4 | ≤ 5 | ≤ 4 |
| Scene class | Test function |
|---|---|
| Points hash | test_golden_hash_points_scene_record_execute |
| Lines hash | test_golden_hash_lines_scene_record_execute |
| Solid hash | test_golden_hash_solid_scene_record_execute |
| Gouraud hash | test_golden_hash_gouraud_scene_record_execute |
| Telemetry baseline | test_ci_telemetry_snapshot_record_execute |
| Telemetry lines baseline | test_ci_telemetry_snapshot_lines_record_execute |
| Telemetry stress baseline | test_ci_telemetry_snapshot_stress_points_record_execute |
| Telemetry fail-soft baseline | test_ci_telemetry_snapshot_failsoft_record_execute |
- Dirty-region reporting:
test_execute_recorded_frame_reports_dirty_region,test_dirty_region_smaller_than_full_frame_for_small_scene - Tile/bin pipeline:
test_build_tile_bins_for_recorded_commands,test_tiled_execute_matches_non_tiled_pixel_count
cargo test --test integration_tests test_ci_telemetry_snapshot_ -- --nocapture \
2>&1 \
| .github/scripts/check_telemetry_budget.py \
--max-record-draw-commands 3 \
--max-execute-commands-total 4 \
--max-execute-draw-commands 3Environment-variable form:
TELEMETRY_MAX_RECORD_DRAW_COMMANDS=3 \
TELEMETRY_MAX_EXECUTE_COMMANDS_TOTAL=4 \
TELEMETRY_MAX_EXECUTE_DRAW_COMMANDS=3 \
TELEMETRY_STRESS_MAX_RECORD_DRAW_COMMANDS=24 \
TELEMETRY_STRESS_MAX_EXECUTE_COMMANDS_TOTAL=25 \
TELEMETRY_STRESS_MAX_EXECUTE_DRAW_COMMANDS=24 \
cargo test --test integration_tests test_ci_telemetry_snapshot_ -- --nocapture 2>&1 \
| .github/scripts/check_telemetry_budget.pyM55 perf lane:
EMBEDDED_3DGFX_CAPS=m55 \
TELEMETRY_MAX_RECORD_DRAW_COMMANDS=4 \
TELEMETRY_MAX_EXECUTE_COMMANDS_TOTAL=5 \
TELEMETRY_MAX_EXECUTE_DRAW_COMMANDS=4 \
cargo test --test integration_tests test_ci_telemetry_snapshot_ -- --nocapture 2>&1 \
| .github/scripts/check_telemetry_budget.pyDefined in .github/workflows/rust.yml:
test— default profileembedded-budgetmatrix —row_width_96/160/240/320profile-budgetmatrix —m3/m4/m33/m55m55-perf-budget
Only update thresholds for intentional architectural changes:
- Capture old/new telemetry snapshots in PR notes
- Explain why the change is necessary
- Update workflow env thresholds
- Keep
docs/backend-integration.mdcompatibility matrix aligned
PRs that change telemetry thresholds must include the checklist in .github/PULL_REQUEST_TEMPLATE.md. A threshold change should not merge without: a short rationale, a profile impact note, and confirmation that fail-soft semantics remain correct.
TELEMETRY_MAX_RECORD_DRAW_COMMANDS
TELEMETRY_MAX_EXECUTE_COMMANDS_TOTAL
TELEMETRY_MAX_EXECUTE_DRAW_COMMANDS
TELEMETRY_STRESS_MAX_RECORD_DRAW_COMMANDS
TELEMETRY_STRESS_MAX_EXECUTE_COMMANDS_TOTAL
TELEMETRY_STRESS_MAX_EXECUTE_DRAW_COMMANDS
TELEMETRY_FAILSOFT_MAX_RECORD_DRAW_COMMANDS
TELEMETRY_FAILSOFT_MAX_EXECUTE_COMMANDS_TOTAL
TELEMETRY_FAILSOFT_MAX_EXECUTE_DRAW_COMMANDS
TELEMETRY_FAILSOFT_EXPECTED_BUDGET_KIND