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
This commit is contained in:
asrael 2026-01-12 21:46:31 -06:00
parent 272e0bc615
commit 49256db1bd
107 changed files with 6678 additions and 3715 deletions

600
resilient-sparking-wirth.md Normal file
View file

@ -0,0 +1,600 @@
# 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/
```