- 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
600 lines
18 KiB
Markdown
600 lines
18 KiB
Markdown
# 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/
|
||
```
|