Overview#

Deterministic multiplayer retro engine running on ESP32-S3 using ESP-IDF.

Architecture: Fixed-step simulation (host-only) -> snapshot replication -> pure snapshot rendering -> DMA present.

No client prediction. No ISR rendering. Pixel-identical frames.

Technical Highlights#

  • Double-buffered rendering
  • DMA display output
  • Tile/sprite-first pipeline
  • Deterministic sync over ESP-NOW
  • Minimal network overhead
  • Real hardware validation

Key Lessons#

  • Determinism simplifies debugging
  • Rendering must be separated from simulation
  • Network simplicity beats clever hacks

Technologies#

  • ESP32-S3
  • ESP-IDF
  • ESP-NOW
  • SPI DMA display pipeline
  • ILI9341
  • Double-buffered rendering
Flowchart
flowchart TD
    classDef phase fill:#eef4ff,stroke:#2563eb,stroke-width:1px,color:#0f172a;
    classDef io fill:#e8fff0,stroke:#16a34a,stroke-width:1px,color:#0f172a;
    classDef invariant fill:#fff7e6,stroke:#d97706,stroke-width:1px,color:#0f172a;

    A["Phase 1: Simulation (Host Only)"] --> B["Phase 2: Snapshot and Replication"]
    B --> C["Phase 3: Rendering (All Devices)"]
    C --> D["Phase 4: Present via SPI DMA"]

    A --> A1["Fixed-step update of gameplay and animation state"]
    B --> B1["Complete frozen snapshot is network truth"]
    C --> C1["Pure function of snapshot (idempotent)"]
    D --> D1["Render back -> Present back -> Sync front -> Swap"]

    D --> E["LCD sees only complete coherent frames"]
    E --> F["Pixel-identical output on identical snapshots"]

    class A,B,C,D phase;
    class A1,B1,C1,D1 io;
    class E,F invariant;
Details
RetroLink follows a strict contract. These are architectural rules, not preferences. ### Phase Order (Never Changes) - `Simulation -> Snapshot -> Rendering -> Present` - Host only simulates authoritative state. - Clients never simulate, predict, smooth, or invent state. - Rendering is snapshot-only and must be idempotent. ### Deterministic Rendering Rules - Every visible pixel change must come from exactly one simulation tick. - `render()` and draw helpers must not use `millis()`, `micros()`, timers, or randomness. - Rendering must not mutate gameplay state. - No role-based branching in rendering logic. ### Display and Buffer Rules - Always render to back buffer only. - Always present back buffer through RetroGoDisplay SPI DMA queue. - After present: copy back to front, then swap. - Front buffer is safety sync only, never a render target. - Any tearing, flicker, ghosting, or frame desync is a critical defect. ### Interrupt and Networking Safety - No display API calls from ISR or ESP-NOW callbacks. - ISR path may only store packet data and set flags. - Display work is deferred to main loop / task context. - SPI transaction payloads must be stable memory, never stack buffers. ### Memory and Asset Policy - DMA buffers must be internal DMA-capable RAM, not PSRAM. - Large transient decode buffers may use PSRAM. - Core gameplay visuals are tiles and sprites, not full-frame RGB565 assets. - PNG assets are allowed in SPIFFS for UI and loading paths, decoded outside `render()`. ### Why This Works This contract is what keeps multiplayer output deterministic across devices while preserving display stability on constrained hardware. The architecture was shaped by failures (white screens, bus corruption, stack-lifetime DMA bugs, and desync) and is intentionally strict to prevent regressions.

Gallery

4 images