# 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, } 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/ ```