pxl8/resilient-sparking-wirth.md
asrael 49256db1bd refactor: reorganize pxl8 into client/src/ module structure
- core/: main entry, types, logging, I/O, RNG
- asset/: ase loader, cart, save, embed
- gfx/: graphics, animation, atlas, fonts, tilemap, transitions
- sfx/: audio
- script/: lua/fennel runtime, REPL
- hal/: platform abstraction (SDL3)
- world/: BSP, world, procedural gen
- math/: math utilities
- game/: GUI, replay
- lua/: Lua API modules
2026-01-15 08:41:59 -06:00

18 KiB
Raw Blame History

Prydain Architecture Restructuring Plan

Vision: pxl8 as Console, Games as Mods

┌─────────────────────────────────────────────────────────────┐
│  prydain/ (a mod - game content)                            │
│  ├── cart.fnl       # Manifest                              │
│  ├── main.fnl       # Entry point                           │
│  ├── mod/           # Game modules (Fennel)                 │
│  └── res/           # Resources (models, textures, sounds)  │
└────────────────────────┬────────────────────────────────────┘
                         │ runs on
┌────────────────────────┴────────────────────────────────────┐
│  pxl8 (the console/platform)                                │
│  ├── client/        # C client (gfx, sfx, hal, script)      │
│  │   └── src/       # C source files                        │
│  ├── server/        # Rust bevy_ecs game server             │
│  │   └── src/       # Rust source files                     │
│  └── demo/          # Example mod                           │
└─────────────────────────────────────────────────────────────┘

Client/Server Split (Quake-style):

┌─────────────────────────────────────────────────────────────┐
│  Fennel mod/ (runs on CLIENT via pxl8/LuaJIT)               │
│  Game logic, behaviors, content definitions                 │
│  Sends commands to server, receives snapshots               │
└────────────────────────┬────────────────────────────────────┘
                         │ LuaJIT FFI
┌────────────────────────┴────────────────────────────────────┐
│  C pxl8 client - rendering, audio, input, networking        │
└────────────────────────┬────────────────────────────────────┘
                         │ protocol (commands ↑, snapshots ↓)
┌────────────────────────┴────────────────────────────────────┐
│  Rust bevy_ecs server - authoritative simulation            │
│  (ships with pxl8, games extend via Fennel + optional Rust) │
└─────────────────────────────────────────────────────────────┘

Key Architectural Properties

  • pxl8 is the console: Ships C client + Rust server
  • Mods are games: cart.fnl + mod/.fnl + res/
  • prydain is just a mod: Game content, not engine code
  • Server ships with pxl8: Generic bevy_ecs, extended by mods
  • Simple mods: Just Fennel, no server needed (client-only)
  • Complex mods: Use server for authoritative simulation

Phase 1: Reorganize C pxl8 into Modules

Goal: Clean up pxl8's flat structure into logical modules.

New Directory Structure

~/code/pxl8/
  client/                 # C client
    src/
      core/
        pxl8.c            # Main entry, lifecycle
        pxl8_types.h      # Core types
        pxl8_log.c/h      # Logging
        pxl8_io.c/h       # File I/O
      math/
        pxl8_math.c/h     # Vec3, Mat4, etc.
        pxl8_simd.h       # SIMD abstractions
      gfx/
        pxl8_gfx.c/h      # Graphics frontend (backend dispatch)
        pxl8_cpu.c        # CPU backend (prydain optimizations)
        pxl8_gpu.c        # GPU backend (SDL3_GPU, future)
        pxl8_colormap.c/h # 64×256 lighting LUT
        pxl8_dither.c/h   # Bayer dithering
        pxl8_atlas.c/h    # Texture atlas
        pxl8_vfx.c/h      # Particle effects
      sfx/
        pxl8_sfx.c/h      # Audio synthesis
      script/
        pxl8_script.c/h   # Lua/Fennel runtime
        pxl8_repl.c/h     # REPL
      hal/
        pxl8_hal.h        # Platform abstraction interface
        pxl8_sdl3.c/h     # SDL3 implementation
      world/
        pxl8_bsp.c/h      # BSP loading/rendering
        pxl8_world.c/h    # World management
      net/                # NEW
        pxl8_protocol.h   # Message definitions
        pxl8_net.c/h      # Networking (client side)
      lua/                # Existing Lua API
        pxl8.lua
        pxl8/*.lua
    include/
      pxl8.h              # Public API header
  pxl8.sh                 # Build script (bash)
  server/                 # Rust bevy_ecs server
    Cargo.toml
    src/
      lib.rs
      ...

Verification

  • All existing pxl8 demos still compile and run
  • No functionality regression
  • Build system finds all sources in new locations

Phase 2: Enhance pxl8_gfx with Prydain Optimizations

Goal: Port prydain's rendering optimizations into pxl8_gfx, with backend abstraction for future GPU support.

Backend Architecture

pxl8_gfx.c/h        # Frontend API (existing, enhanced)
    │
    ├── pxl8_cpu.c  # CPU/software backend (prydain techniques)
    │
    └── pxl8_gpu.c  # GPU backend (SDL3_GPU, future)

Files to Create/Modify

pxl8/client/src/gfx/
  pxl8_gfx.c/h      # EXISTING - add backend dispatch
  pxl8_cpu.c        # NEW - CPU rasterizer (prydain optimizations)
  pxl8_gpu.c        # FUTURE - SDL3_GPU backend
  pxl8_colormap.c/h # NEW - 64×256 lighting LUT
  pxl8_dither.c/h   # NEW - Bayer dithering

pxl8/client/src/math/
  pxl8_simd.h       # SSE2/NEON/scalar abstractions

Key Functions to Port

From prydain/src/gfx/backend/cpu/renderer.rs:

Function Lines Target
rasterize_triangle_opaque_fast 1184-1466 pxl8_raster.c
rasterize_span_simd_x4 1063-1180 pxl8_raster.c
render_glows 689-971 pxl8_vfx.c
clip_triangle_near 132-186 pxl8_raster.c

From prydain/src/gfx/backend/cpu/shader.rs:

Function Lines Target
calc_light 113-163 pxl8_lighting.c
fast_inv_sqrt 181-205 pxl8_math.c

From prydain/src/gfx/color/map.rs:

Function Target
ColorMap LUT generation pxl8_colormap.c
lookup() pxl8_colormap.c

C Rasterizer API

// pxl8_raster.h
typedef struct pxl8_rasterizer pxl8_rasterizer;

pxl8_rasterizer* pxl8_rasterizer_create(u32 width, u32 height);
void pxl8_rasterizer_destroy(pxl8_rasterizer* r);

void pxl8_rasterizer_set_frame(pxl8_rasterizer* r, const pxl8_frame* frame);
void pxl8_rasterizer_clear(pxl8_rasterizer* r, u8 color);
void pxl8_rasterizer_clear_depth(pxl8_rasterizer* r);

void pxl8_rasterizer_draw_triangles(
    pxl8_rasterizer* r,
    const pxl8_vertex* verts,
    u32 vert_count,
    const u16* indices,
    u32 index_count,
    const pxl8_material* material
);

void pxl8_rasterizer_draw_glows(
    pxl8_rasterizer* r,
    const pxl8_glow* glows,
    u32 count
);

u8* pxl8_rasterizer_framebuffer(pxl8_rasterizer* r);

Verification

  • Render same scene in Rust and C, pixel-diff comparison
  • Benchmark: C ≥ Rust performance
  • Test all render paths (opaque, alpha, passthrough)

Phase 3: Define Client/Server Protocol

Goal: Language-agnostic message format for client↔server communication.

Protocol Header

// pxl8/src/net/pxl8_protocol.h

// Message types
typedef enum {
    PXL8_MSG_INPUT = 1,        // Client → Server
    PXL8_MSG_COMMAND = 2,      // Client → Server (from Fennel scripts)
    PXL8_MSG_SNAPSHOT = 3,     // Server → Client
    PXL8_MSG_EVENT = 4,        // Server → Client (sounds, particles)
} pxl8_msg_type;

// Client → Server: Input
typedef struct {
    u64 tick;
    u64 timestamp;
    f32 move_x, move_y;        // Movement input
    f32 look_dx, look_dy;      // Mouse delta
    u32 buttons;               // Bitfield: attack, use, jump, etc.
} pxl8_input_msg;

// Client → Server: Script command
typedef struct {
    u64 tick;
    u16 cmd_type;              // Command enum
    u8 payload[64];            // Command-specific data
} pxl8_command_msg;

// Server → Client: Entity state
typedef struct {
    u64 entity_id;
    u16 archetype;
    f32 x, y, z;
    f32 yaw;
    u16 anim_id;
    f32 anim_time;
    u32 flags;
} pxl8_entity_state;

// Server → Client: World snapshot
typedef struct {
    u64 tick;
    f32 time;
    u16 entity_count;
    pxl8_entity_state entities[];  // Flexible array
} pxl8_snapshot_msg;

// Server → Client: Events
typedef struct {
    u8 event_type;             // Sound, particle, damage flash, etc.
    u8 payload[15];            // Event-specific data
} pxl8_event_msg;

Serialization

Simple binary format, network byte order:

// pxl8_protocol.c
size_t pxl8_serialize_input(const pxl8_input_msg* msg, u8* buf, size_t len);
size_t pxl8_deserialize_input(const u8* buf, size_t len, pxl8_input_msg* msg);
size_t pxl8_serialize_snapshot(const pxl8_snapshot_msg* msg, u8* buf, size_t len);
size_t pxl8_deserialize_snapshot(const u8* buf, size_t len, pxl8_snapshot_msg* msg);

Verification

  • Round-trip serialization test
  • Cross-language test (C client, Rust server)

Phase 4: Client Implementation

Goal: pxl8 client that connects to server, renders snapshots.

Client State

// pxl8/src/net/pxl8_client.h
typedef struct pxl8_client pxl8_client;

pxl8_client* pxl8_client_create(void);
void pxl8_client_destroy(pxl8_client* c);

// Connection
bool pxl8_client_connect(pxl8_client* c, const char* address, u16 port);
void pxl8_client_disconnect(pxl8_client* c);

// Input
void pxl8_client_send_input(pxl8_client* c, const pxl8_input_msg* input);
void pxl8_client_send_command(pxl8_client* c, const pxl8_command_msg* cmd);

// Receive
bool pxl8_client_poll(pxl8_client* c);  // Process incoming messages
pxl8_snapshot_msg* pxl8_client_latest_snapshot(pxl8_client* c);

// Interpolation
f32 pxl8_client_interp_time(pxl8_client* c);
void pxl8_client_get_entity_transform(
    pxl8_client* c,
    u64 entity_id,
    pxl8_vec3* pos,
    f32* yaw
);

Client Loop

void game_loop(pxl8_client* client, pxl8_rasterizer* raster) {
    while (running) {
        f32 dt = get_delta_time();

        // Poll input
        pxl8_input_state input = pxl8_hal_poll_input();

        // Send to server
        pxl8_input_msg msg = make_input_msg(&input);
        pxl8_client_send_input(client, &msg);

        // Receive snapshots
        pxl8_client_poll(client);

        // Render interpolated state
        pxl8_snapshot_msg* snap = pxl8_client_latest_snapshot(client);
        render_world(raster, client, snap);

        // Present
        pxl8_hal_present();
    }
}

Verification

  • Client connects to local server
  • Entities render at interpolated positions
  • Input reaches server, affects simulation

Phase 5: Rust Server (ships with pxl8)

Goal: Generic bevy_ecs server that mods can extend.

Crate Structure (inside pxl8)

pxl8/
  server/                   # Rust bevy_ecs game server
    Cargo.toml
    src/
      lib.rs
      server.rs             # Main server loop
      protocol.rs           # Protocol types (shared with C)
      systems/
        physics.rs          # Generic physics
        visibility.rs       # PVS, line of sight
      components.rs         # Generic components (Position, Velocity, etc.)

Minimal dependencies:

  • bevy_ecs only (not full Bevy)
  • no_std compatible where possible
  • No rendering, audio, or platform code (that's C pxl8's job)

Server is generic - mods extend it via Fennel scripts defining behaviors.

If Fennel is too slow (e.g., pathfinding for 100+ mobs), improve the generic server—that benefits all mods. No plugin system for now; can add later if there's demand from a modding community.

Server Loop

// prydain-server/src/server.rs
pub struct GameServer {
    world: World,
    schedule: Schedule,
    tick: u64,
    tick_accumulator: f32,
    clients: Vec<ClientConnection>,
}

impl GameServer {
    const TICK_RATE: f32 = 30.0;
    const TICK_DURATION: f32 = 1.0 / Self::TICK_RATE;

    pub fn update(&mut self, dt: f32) {
        self.tick_accumulator += dt;

        while self.tick_accumulator >= Self::TICK_DURATION {
            self.tick_accumulator -= Self::TICK_DURATION;
            self.tick += 1;

            // Process client inputs/commands
            for client in &mut self.clients {
                while let Some(msg) = client.recv_input() {
                    self.apply_input(client.player_entity, &msg);
                }
                while let Some(cmd) = client.recv_command() {
                    self.apply_command(&cmd);
                }
            }

            // Run simulation
            self.schedule.run(&mut self.world);

            // Send snapshots
            let snapshot = self.generate_snapshot();
            for client in &mut self.clients {
                client.send_snapshot(&snapshot);
            }
        }
    }
}

Verification

  • Server runs standalone (headless)
  • Multiple clients can connect
  • Simulation is deterministic

Phase 6: Mod System (Fennel + pxl8)

Goal: Mods run on client via pxl8/LuaJIT, send commands to server.

Mod API (extends existing pxl8 Lua API)

The existing pxl8 API (pxl8.world_*, pxl8.sfx_*, etc.) is extended with entity functions. Client/server split is an implementation detail—mods just call pxl8.*:

;; Entity management (server handles if networked)
(pxl8.entity_spawn :cauldron-born {:x 10 :y 0 :z 15})
(pxl8.entity_damage target-id 25)
(pxl8.entity_move_to entity-id {:x 50 :z 50})

;; Query entity state (from latest snapshot if networked)
(pxl8.entity_get_pos entity-id)  ; → {:x _ :y _ :z _}
(pxl8.entity_query {:range 100 :type :mob})
(pxl8.player_entity)

;; Effects (always local, immediate)
(pxl8.sfx_play_note ...)         ; existing API
(pxl8.particles_emit ...)        ; existing API
(pxl8.camera_shake 0.5)          ; new

This matches the existing pattern:

pxl8.world_render(w, camera_pos)
pxl8.sfx_compressor_create(...)
pxl8.particles_update(ps, dt)

Mod Structure (prydain example)

prydain/
  cart.fnl                # Mod manifest
  main.fnl                # Entry point
  mod/
    entities.fnl          # Entity archetype definitions
    combat.fnl            # Combat logic
    ai/
      patrol.fnl
      chase.fnl
  res/
    models/
    textures/
    sounds/

Entity Definition

;; prydain/mod/entities.fnl
(def cauldron-born
  {:archetype :cauldron-born
   :model "res/models/cauldron_born.glb"
   :stats {:health 45 :damage 12 :speed 2.5}
   :behavior :undead-warrior
   :sounds {:alert :undead-alert :attack :sword-swing}})

Verification

  • pxl8 prydain/ runs the mod
  • Fennel script spawns entity (command reaches server)
  • Entity appears (snapshot received, model rendered)
  • AI behavior runs, entity patrols/chases

Spike Milestones

Milestone Description Validates
M1 Reorganize pxl8 into module folders Build system, no regression
M2 Port colormap + dither to C Basic rendering building blocks
M3 Port triangle rasterizer to C Core rendering path
M4 C rasterizer has visual parity with Rust Full rendering correctness
M5 Protocol defined, serialize/deserialize works Client/server communication
M6 Client connects, receives snapshot, renders End-to-end data flow
M7 Rust server runs, sends snapshots Server implementation
M8 Fennel script spawns entity via command Content scripting
M9 Full game loop working Architecture validated

Files to Modify/Create

pxl8 (C) - New Files

Path Description
client/src/gfx/pxl8_cpu.c CPU backend (prydain optimizations)
client/src/gfx/pxl8_gpu.c GPU backend (SDL3_GPU, future)
client/src/gfx/pxl8_colormap.c/h 64×256 lighting LUT
client/src/gfx/pxl8_dither.c/h Bayer dithering
client/src/math/pxl8_simd.h SIMD abstractions
client/src/net/pxl8_protocol.h Message definitions
client/src/net/pxl8_net.c/h Networking
client/src/lua/pxl8/entity.lua Entity API (extends pxl8)

pxl8 (C) - Files to Move

From To
src/pxl8_gfx.c/h client/src/gfx/pxl8_gfx.c/h
src/pxl8_sfx.c/h client/src/sfx/pxl8_sfx.c/h
src/pxl8_script.c/h client/src/script/pxl8_script.c/h
src/pxl8_hal.h client/src/hal/pxl8_hal.h
src/pxl8_sdl3.c/h client/src/hal/pxl8_sdl3.c/h
src/pxl8_bsp.c/h client/src/world/pxl8_bsp.c/h
src/pxl8_math.c/h client/src/math/pxl8_math.c/h
src/lua/* client/src/lua/*

prydain - Becomes a Mod

Current prydain Rust code gets split:

Current Target Notes
src/gfx/backend/cpu/ pxl8/src/gfx/ Port to C pxl8
src/gfx/display.rs DELETE Use pxl8 HAL
src/game/systems/*.rs pxl8/server/ Generic bevy_ecs server
src/game/entities/*.rs prydain/mod/*.fnl Convert to Fennel

prydain becomes just a mod:

prydain/
  cart.fnl              # Manifest
  main.fnl              # Entry point
  mod/
    entities.fnl        # Entity definitions
    combat.fnl          # Combat logic
    ai/
      patrol.fnl
      chase.fnl
    quests/
      main_quest.fnl
  res/
    models/
    textures/
    sounds/
    levels/

pxl8 gains:

pxl8/
  client/               # C client (existing + ported renderer)
    src/
      gfx/              # Including new rasterizer from prydain
      lua/              # Existing + new entity.lua
      net/              # NEW: networking
      ...
    include/
    meson.build
  server/               # NEW: Rust bevy_ecs generic server
    Cargo.toml
    src/
      lib.rs
      server.rs
      protocol.rs
      systems/