- 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
18 KiB
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_ecsonly (not full Bevy)no_stdcompatible 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/