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

600 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```c
// 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
```c
// 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:
```c
// 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
```c
// 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
```c
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
```rust
// 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.*`:
```fennel
;; 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:
```lua
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
```fennel
;; 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/
```