refactor separate framework from game code, add demo3d

This commit is contained in:
asrael 2026-04-14 01:28:38 -05:00
parent 19ae869769
commit 40f5cdcaa5
92 changed files with 2665 additions and 6547 deletions

View file

@ -1,986 +0,0 @@
#include "pxl8_world.h"
#include <string.h>
#include "pxl8_hal.h"
#ifdef PXL8_ASYNC_THREADS
#include <stdatomic.h>
#include "pxl8_queue.h"
#endif
#include "pxl8_bsp_render.h"
#include "pxl8_io.h"
#include "pxl8_gfx3d.h"
#include "pxl8_log.h"
#include "pxl8_mem.h"
#include "pxl8_protocol.h"
#include "pxl8_sim.h"
#define PXL8_VIS_MAX_NODES (PXL8_WORLD_MAX_LOADED_CHUNKS * 512)
#define PXL8_VIS_MAX_QUEUE (PXL8_VIS_MAX_NODES * 4)
#define PXL8_VIS_BYTES ((PXL8_VIS_MAX_NODES + 7) / 8)
#define PXL8_WORLD_ENTITY_CAPACITY 256
typedef struct {
u16 chunk_idx;
u16 leaf_idx;
pxl8_rect window;
} world_vis_node;
struct pxl8_world {
pxl8_loaded_chunk loaded[PXL8_WORLD_MAX_LOADED_CHUNKS];
u32 loaded_count;
pxl8_world_chunk* active_chunk;
pxl8_bsp_render_state* active_render_state;
pxl8_gfx_material shared_materials[16];
bool shared_material_set[16];
pxl8_world_chunk_cache* chunk_cache;
pxl8_entity_pool* entities;
pxl8_sim_entity local_player;
u64 client_tick;
pxl8_vec2 pointer_motion;
pxl8_sim_config sim_config;
u8 vis_bits[PXL8_VIS_BYTES];
u8* vis_ptrs[PXL8_WORLD_MAX_LOADED_CHUNKS];
pxl8_rect vis_windows[PXL8_VIS_MAX_NODES];
pxl8_rect* vis_win_ptrs[PXL8_WORLD_MAX_LOADED_CHUNKS];
world_vis_node vis_queue[PXL8_VIS_MAX_QUEUE];
#ifdef PXL8_ASYNC_THREADS
pxl8_sim_entity render_state[2];
atomic_uint active_buffer;
pxl8_thread* sim_thread;
atomic_bool sim_running;
atomic_bool sim_paused;
pxl8_net* net;
pxl8_queue input_queue;
f32 sim_accumulator;
#endif
};
pxl8_world* pxl8_world_create(void) {
pxl8_world* world = pxl8_calloc(1, sizeof(pxl8_world));
if (!world) return NULL;
world->chunk_cache = pxl8_world_chunk_cache_create();
world->entities = pxl8_entity_pool_create(PXL8_WORLD_ENTITY_CAPACITY);
world->sim_config = (pxl8_sim_config){
.move_speed = 180.0f,
.ground_accel = 10.0f,
.air_accel = 1.0f,
.stop_speed = 100.0f,
.friction = 6.0f,
.gravity = 800.0f,
.jump_velocity = 200.0f,
.player_radius = 16.0f,
.player_height = 72.0f,
.max_pitch = 1.5f,
};
if (!world->chunk_cache || !world->entities) {
pxl8_world_destroy(world);
return NULL;
}
return world;
}
void pxl8_world_destroy(pxl8_world* world) {
if (!world) return;
for (u32 i = 0; i < world->loaded_count; i++) {
pxl8_bsp_render_state_destroy(world->loaded[i].render_state);
}
pxl8_world_chunk_cache_destroy(world->chunk_cache);
pxl8_entity_pool_destroy(world->entities);
pxl8_free(world);
}
pxl8_world_chunk_cache* pxl8_world_get_chunk_cache(pxl8_world* world) {
if (!world) return NULL;
return world->chunk_cache;
}
pxl8_world_chunk* pxl8_world_active_chunk(pxl8_world* world) {
if (!world) return NULL;
return world->active_chunk;
}
pxl8_sim_world pxl8_world_sim_world(const pxl8_world* world, pxl8_vec3 pos) {
pxl8_sim_world sim = {0};
const f32 chunk_size = 16.0f * 64.0f;
sim.chunk_size = chunk_size;
i32 pcx = (i32)floorf(pos.x / chunk_size);
i32 pcz = (i32)floorf(pos.z / chunk_size);
sim.center_cx = pcx;
sim.center_cz = pcz;
for (u32 i = 0; i < world->loaded_count; i++) {
const pxl8_loaded_chunk* lc = &world->loaded[i];
if (!lc->chunk || !lc->chunk->bsp) continue;
i32 dx = lc->cx - pcx + 1;
i32 dz = lc->cz - pcz + 1;
if (dx >= 0 && dx <= 2 && dz >= 0 && dz <= 2) {
sim.chunks[dz * 3 + dx] = lc->chunk->bsp;
}
}
return sim;
}
static void entity_to_userdata(const pxl8_sim_entity* ent, u8* userdata) {
u8* p = userdata;
memcpy(p, &ent->pos.x, 4); p += 4;
memcpy(p, &ent->pos.y, 4); p += 4;
memcpy(p, &ent->pos.z, 4); p += 4;
memcpy(p, &ent->yaw, 4); p += 4;
memcpy(p, &ent->pitch, 4); p += 4;
memcpy(p, &ent->vel.x, 4); p += 4;
memcpy(p, &ent->vel.y, 4); p += 4;
memcpy(p, &ent->vel.z, 4); p += 4;
memcpy(p, &ent->flags, 4); p += 4;
memcpy(p, &ent->kind, 2);
}
static void userdata_to_entity(const u8* userdata, pxl8_sim_entity* ent) {
const u8* p = userdata;
memcpy(&ent->pos.x, p, 4); p += 4;
memcpy(&ent->pos.y, p, 4); p += 4;
memcpy(&ent->pos.z, p, 4); p += 4;
memcpy(&ent->yaw, p, 4); p += 4;
memcpy(&ent->pitch, p, 4); p += 4;
memcpy(&ent->vel.x, p, 4); p += 4;
memcpy(&ent->vel.y, p, 4); p += 4;
memcpy(&ent->vel.z, p, 4); p += 4;
memcpy(&ent->flags, p, 4); p += 4;
memcpy(&ent->kind, p, 2);
}
bool pxl8_world_point_solid(const pxl8_world* world, f32 x, f32 y, f32 z) {
if (!world) return false;
if (world->active_chunk && world->active_chunk->bsp) {
return pxl8_bsp_point_solid(world->active_chunk->bsp, (pxl8_vec3){x, y, z});
}
return false;
}
pxl8_ray pxl8_world_ray(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to) {
pxl8_ray result = {0};
if (!world) return result;
pxl8_vec3 dir = pxl8_vec3_sub(to, from);
f32 length = pxl8_vec3_length(dir);
if (length < 0.001f) return result;
f32 step_size = 1.0f;
f32 traveled = 0.0f;
while (traveled < length) {
f32 t = traveled / length;
pxl8_vec3 pos = {
from.x + dir.x * t,
from.y + dir.y * t,
from.z + dir.z * t
};
if (pxl8_world_point_solid(world, pos.x, pos.y, pos.z)) {
result.hit = true;
result.fraction = t;
result.point = pos;
f32 eps = 0.1f;
bool sx_neg = pxl8_world_point_solid(world, pos.x - eps, pos.y, pos.z);
bool sx_pos = pxl8_world_point_solid(world, pos.x + eps, pos.y, pos.z);
bool sy_neg = pxl8_world_point_solid(world, pos.x, pos.y - eps, pos.z);
bool sy_pos = pxl8_world_point_solid(world, pos.x, pos.y + eps, pos.z);
bool sz_neg = pxl8_world_point_solid(world, pos.x, pos.y, pos.z - eps);
bool sz_pos = pxl8_world_point_solid(world, pos.x, pos.y, pos.z + eps);
result.normal = (pxl8_vec3){
(sx_neg && !sx_pos) ? 1.0f : (!sx_neg && sx_pos) ? -1.0f : 0.0f,
(sy_neg && !sy_pos) ? 1.0f : (!sy_neg && sy_pos) ? -1.0f : 0.0f,
(sz_neg && !sz_pos) ? 1.0f : (!sz_neg && sz_pos) ? -1.0f : 0.0f
};
f32 nl = pxl8_vec3_length(result.normal);
if (nl > 0.001f) {
result.normal = pxl8_vec3_scale(result.normal, 1.0f / nl);
} else {
result.normal = (pxl8_vec3){0, 1, 0};
}
return result;
}
traveled += step_size;
}
return result;
}
pxl8_ray pxl8_world_sweep(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to, f32 radius) {
pxl8_ray result = {0};
if (!world) return result;
f32 diag = radius * 0.707f;
bool dest_blocked = pxl8_world_point_solid(world, to.x, to.y, to.z) ||
pxl8_world_point_solid(world, to.x + radius, to.y, to.z) ||
pxl8_world_point_solid(world, to.x - radius, to.y, to.z) ||
pxl8_world_point_solid(world, to.x, to.y, to.z + radius) ||
pxl8_world_point_solid(world, to.x, to.y, to.z - radius) ||
pxl8_world_point_solid(world, to.x + diag, to.y, to.z + diag) ||
pxl8_world_point_solid(world, to.x + diag, to.y, to.z - diag) ||
pxl8_world_point_solid(world, to.x - diag, to.y, to.z + diag) ||
pxl8_world_point_solid(world, to.x - diag, to.y, to.z - diag);
if (dest_blocked) {
result.hit = true;
result.fraction = 0;
result.point = from;
pxl8_vec3 dir = pxl8_vec3_sub(to, from);
f32 length = pxl8_vec3_length(dir);
if (length > 0.001f) {
result.normal = pxl8_vec3_scale(dir, -1.0f / length);
} else {
result.normal = (pxl8_vec3){0, 1, 0};
}
}
return result;
}
void pxl8_world_update(pxl8_world* world, f32 dt) {
(void)dt;
if (!world) return;
pxl8_world_chunk_cache_tick(world->chunk_cache);
}
static inline bool vr_valid(pxl8_rect r) {
return r.x0 < r.x1 && r.y0 < r.y1;
}
static inline pxl8_rect vr_intersect(pxl8_rect a, pxl8_rect b) {
return (pxl8_rect){
.x0 = a.x0 > b.x0 ? a.x0 : b.x0,
.y0 = a.y0 > b.y0 ? a.y0 : b.y0,
.x1 = a.x1 < b.x1 ? a.x1 : b.x1,
.y1 = a.y1 < b.y1 ? a.y1 : b.y1,
};
}
static pxl8_rect project_portal(f32 px0, f32 pz0, f32 px1, f32 pz1,
f32 y_lo, f32 y_hi, const pxl8_mat4* vp) {
pxl8_vec3 corners[4] = {
{px0, y_lo, pz0}, {px1, y_lo, pz1},
{px1, y_hi, pz1}, {px0, y_hi, pz0},
};
const f32 NEAR_W = 0.001f;
pxl8_vec4 clip[4];
bool front[4];
i32 fc = 0;
for (i32 i = 0; i < 4; i++) {
clip[i] = pxl8_mat4_multiply_vec4(*vp, (pxl8_vec4){
corners[i].x, corners[i].y, corners[i].z, 1.0f});
front[i] = clip[i].w > NEAR_W;
if (front[i]) fc++;
}
if (fc == 0) return (pxl8_rect){0, 0, 0, 0};
if (fc < 4) return (pxl8_rect){-1.0f, -1.0f, 1.0f, 1.0f};
pxl8_rect r = {1e30f, 1e30f, -1e30f, -1e30f};
for (i32 i = 0; i < 4; i++) {
f32 iw = 1.0f / clip[i].w;
f32 nx = clip[i].x * iw, ny = clip[i].y * iw;
if (nx < r.x0) r.x0 = nx; if (nx > r.x1) r.x1 = nx;
if (ny < r.y0) r.y0 = ny; if (ny > r.y1) r.y1 = ny;
}
if (r.x0 < -1.0f) r.x0 = -1.0f;
if (r.y0 < -1.0f) r.y0 = -1.0f;
if (r.x1 > 1.0f) r.x1 = 1.0f;
if (r.y1 > 1.0f) r.y1 = 1.0f;
return r;
}
static void compute_edge_leafs(pxl8_loaded_chunk* lc) {
const f32 CHUNK_SIZE = 16.0f * 64.0f;
const pxl8_bsp* bsp = lc->chunk->bsp;
f32 cx0 = lc->cx * CHUNK_SIZE;
f32 cz0 = lc->cz * CHUNK_SIZE;
f32 cx1 = cx0 + CHUNK_SIZE;
f32 cz1 = cz0 + CHUNK_SIZE;
memset(lc->edges, 0, sizeof(lc->edges));
for (u32 i = 0; i < bsp->num_leafs; i++) {
const pxl8_bsp_leaf* leaf = &bsp->leafs[i];
if (bsp->leafs[i].contents == -1) continue;
if ((f32)leaf->mins[2] <= (f32)((i16)cz0) + 1.0f && lc->edges[0].count < 16)
lc->edges[0].leafs[lc->edges[0].count++] = (u16)i;
if ((f32)leaf->maxs[2] >= (f32)((i16)cz1) - 1.0f && lc->edges[1].count < 16)
lc->edges[1].leafs[lc->edges[1].count++] = (u16)i;
if ((f32)leaf->mins[0] <= (f32)((i16)cx0) + 1.0f && lc->edges[2].count < 16)
lc->edges[2].leafs[lc->edges[2].count++] = (u16)i;
if ((f32)leaf->maxs[0] >= (f32)((i16)cx1) - 1.0f && lc->edges[3].count < 16)
lc->edges[3].leafs[lc->edges[3].count++] = (u16)i;
}
}
static i32 world_find_chunk(const pxl8_world* world, i32 cx, i32 cz) {
for (u32 i = 0; i < world->loaded_count; i++) {
if (world->loaded[i].cx == cx && world->loaded[i].cz == cz &&
world->loaded[i].chunk && world->loaded[i].chunk->bsp)
return (i32)i;
}
return -1;
}
static void world_mark_leaf_faces(const pxl8_bsp* bsp, pxl8_bsp_render_state* rs, u32 leaf_idx) {
const pxl8_bsp_leaf* leaf = &bsp->leafs[leaf_idx];
for (u32 i = 0; i < leaf->num_marksurfaces; i++) {
u32 si = leaf->first_marksurface + i;
if (si < bsp->num_marksurfaces) {
u32 fi = bsp->marksurfaces[si];
if (fi < bsp->num_faces && rs)
rs->render_face_flags[fi] = 1;
}
}
}
static bool vis_try_enqueue(u8** vis, pxl8_rect** windows, world_vis_node* queue,
u32* tail, u32 max_queue,
u16 ci, u16 li, pxl8_rect nw) {
if (!vis[ci] || !windows[ci]) return false;
u32 byte = li >> 3;
u32 bit = 1 << (li & 7);
if (vis[ci][byte] & bit) {
pxl8_rect* ex = &windows[ci][li];
bool expanded = false;
if (nw.x0 < ex->x0) { ex->x0 = nw.x0; expanded = true; }
if (nw.y0 < ex->y0) { ex->y0 = nw.y0; expanded = true; }
if (nw.x1 > ex->x1) { ex->x1 = nw.x1; expanded = true; }
if (nw.y1 > ex->y1) { ex->y1 = nw.y1; expanded = true; }
if (expanded && *tail < max_queue)
queue[(*tail)++] = (world_vis_node){ci, li, *ex};
return expanded;
}
vis[ci][byte] |= bit;
windows[ci][li] = nw;
if (*tail < max_queue)
queue[(*tail)++] = (world_vis_node){ci, li, nw};
return true;
}
static void world_compute_visibility(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) {
const f32 CHUNK_SIZE = 16.0f * 64.0f;
const f32 PORTAL_Y_HI = 192.0f;
const pxl8_mat4* vp = pxl8_3d_get_view_proj(gfx);
i32 cam_ci = -1, cam_li = -1;
for (u32 i = 0; i < world->loaded_count; i++) {
pxl8_loaded_chunk* lc = &world->loaded[i];
if (!lc->chunk || !lc->chunk->bsp) continue;
const pxl8_bsp* bsp = lc->chunk->bsp;
f32 cx0 = lc->cx * CHUNK_SIZE;
f32 cz0 = lc->cz * CHUNK_SIZE;
if (camera_pos.x < cx0 || camera_pos.x >= cx0 + CHUNK_SIZE ||
camera_pos.z < cz0 || camera_pos.z >= cz0 + CHUNK_SIZE) continue;
if (bsp->bounds_max_x > bsp->bounds_min_x &&
(camera_pos.x < bsp->bounds_min_x || camera_pos.x >= bsp->bounds_max_x ||
camera_pos.z < bsp->bounds_min_z || camera_pos.z >= bsp->bounds_max_z))
continue;
i32 leaf = pxl8_bsp_find_leaf(bsp, camera_pos);
if (leaf >= 0 && (u32)leaf < bsp->num_leafs &&
bsp->leafs[leaf].contents != -1 &&
(!(bsp->bounds_max_x > bsp->bounds_min_x) || bsp->leafs[leaf].contents == -2)) {
cam_ci = (i32)i;
cam_li = leaf;
break;
}
}
if (cam_ci < 0) {
for (u32 i = 0; i < world->loaded_count; i++) {
pxl8_loaded_chunk* lc = &world->loaded[i];
if (!lc->chunk || !lc->chunk->bsp) continue;
const pxl8_bsp* bsp = lc->chunk->bsp;
if (bsp->bounds_max_x > bsp->bounds_min_x) continue;
i32 leaf = pxl8_bsp_find_leaf(bsp, camera_pos);
if (leaf < 0 || (u32)leaf >= bsp->num_leafs) continue;
const pxl8_bsp_leaf* l = &bsp->leafs[leaf];
if (l->contents == -1) continue;
if (camera_pos.x < (f32)l->mins[0] || camera_pos.x > (f32)l->maxs[0] ||
camera_pos.z < (f32)l->mins[2] || camera_pos.z > (f32)l->maxs[2]) continue;
cam_ci = (i32)i;
cam_li = leaf;
break;
}
}
if (cam_ci < 0 || !vp) {
for (u32 i = 0; i < world->loaded_count; i++) {
pxl8_bsp_render_state* rs = world->loaded[i].render_state;
if (!rs) continue;
if (rs->render_face_flags)
memset(rs->render_face_flags, 1, rs->num_faces);
rs->exterior = true;
}
return;
}
memset(world->vis_bits, 0, sizeof(world->vis_bits));
memset(world->vis_ptrs, 0, sizeof(world->vis_ptrs));
memset(world->vis_win_ptrs, 0, sizeof(world->vis_win_ptrs));
u32 voff = 0, woff = 0;
for (u32 i = 0; i < world->loaded_count; i++) {
if (world->loaded[i].chunk && world->loaded[i].chunk->bsp) {
u32 nl = world->loaded[i].chunk->bsp->num_leafs;
u32 vbytes = (nl + 7) / 8;
if (voff + vbytes > PXL8_VIS_BYTES || woff + nl > PXL8_VIS_MAX_NODES)
continue;
world->vis_ptrs[i] = world->vis_bits + voff;
voff += vbytes;
world->vis_win_ptrs[i] = world->vis_windows + woff;
woff += nl;
}
}
if (!world->vis_ptrs[cam_ci] || !world->vis_win_ptrs[cam_ci]) return;
for (u32 i = 0; i < world->loaded_count; i++) {
pxl8_bsp_render_state* rs = world->loaded[i].render_state;
if (!rs) continue;
if (rs->render_face_flags)
memset(rs->render_face_flags, 0, rs->num_faces);
rs->exterior = false;
}
u32 head = 0, tail = 0;
pxl8_rect full_screen = {-1.0f, -1.0f, 1.0f, 1.0f};
const pxl8_bsp* cam_bsp = world->loaded[cam_ci].chunk->bsp;
bool cam_exterior = cam_bsp->leafs[cam_li].contents == 0 &&
!(cam_bsp->bounds_max_x > cam_bsp->bounds_min_x);
world->vis_ptrs[cam_ci][cam_li >> 3] |= (1 << (cam_li & 7));
world->vis_win_ptrs[cam_ci][cam_li] = full_screen;
world->vis_queue[tail++] = (world_vis_node){(u16)cam_ci, (u16)cam_li, full_screen};
while (head < tail) {
world_vis_node cur = world->vis_queue[head++];
pxl8_loaded_chunk* lc = &world->loaded[cur.chunk_idx];
const pxl8_bsp* bsp = lc->chunk->bsp;
world_mark_leaf_faces(bsp, lc->render_state, cur.leaf_idx);
if (bsp->cell_portals && cur.leaf_idx < bsp->num_cell_portals) {
bool is_cam_leaf = (cur.chunk_idx == (u16)cam_ci && cur.leaf_idx == (u16)cam_li);
bool is_cam_chunk = (cur.chunk_idx == (u16)cam_ci);
const pxl8_bsp_cell_portals* cp = &bsp->cell_portals[cur.leaf_idx];
for (u8 pi = 0; pi < cp->num_portals; pi++) {
const pxl8_bsp_portal* p = &cp->portals[pi];
u32 target = p->target_leaf;
if (target >= bsp->num_leafs) continue;
if (bsp->leafs[target].contents == -1) continue;
if (is_cam_chunk && !pxl8_bsp_is_leaf_visible(bsp, cam_li, (i32)target)) continue;
if (is_cam_leaf || cam_exterior) {
vis_try_enqueue(world->vis_ptrs, world->vis_win_ptrs,
world->vis_queue, &tail, PXL8_VIS_MAX_QUEUE,
cur.chunk_idx, (u16)target, full_screen);
continue;
}
pxl8_rect psr = project_portal(p->x0, p->z0, p->x1, p->z1,
0.0f, PORTAL_Y_HI, vp);
if (!vr_valid(psr)) continue;
pxl8_rect nw = vr_intersect(cur.window, psr);
if (!vr_valid(nw)) continue;
vis_try_enqueue(world->vis_ptrs, world->vis_win_ptrs,
world->vis_queue, &tail, PXL8_VIS_MAX_QUEUE,
cur.chunk_idx, (u16)target, nw);
}
}
const pxl8_bsp_leaf* leaf = &bsp->leafs[cur.leaf_idx];
f32 chunk_x0 = lc->cx * CHUNK_SIZE;
f32 chunk_z0 = lc->cz * CHUNK_SIZE;
f32 chunk_x1 = chunk_x0 + CHUNK_SIZE;
f32 chunk_z1 = chunk_z0 + CHUNK_SIZE;
struct { i32 dx, dz; bool at_edge; f32 bnd; } dirs[4] = {
{ 0, -1, (f32)leaf->mins[2] <= (f32)((i16)chunk_z0) + 1.0f, chunk_z0 },
{ 0, 1, (f32)leaf->maxs[2] >= (f32)((i16)chunk_z1) - 1.0f, chunk_z1 },
{-1, 0, (f32)leaf->mins[0] <= (f32)((i16)chunk_x0) + 1.0f, chunk_x0 },
{ 1, 0, (f32)leaf->maxs[0] >= (f32)((i16)chunk_x1) - 1.0f, chunk_x1 },
};
static const u8 opposite_edge[4] = {1, 0, 3, 2};
for (u32 d = 0; d < 4; d++) {
if (!dirs[d].at_edge) continue;
i32 nci = world_find_chunk(world, lc->cx + dirs[d].dx, lc->cz + dirs[d].dz);
if (nci < 0) continue;
const pxl8_bsp* nbsp = world->loaded[nci].chunk->bsp;
const pxl8_edge_leafs* nedge = &world->loaded[nci].edges[opposite_edge[d]];
for (u8 k = 0; k < nedge->count; k++) {
u16 nl = nedge->leafs[k];
const pxl8_bsp_leaf* nleaf = &nbsp->leafs[nl];
bool overlaps = false;
if (dirs[d].dx != 0) {
overlaps = leaf->maxs[2] > nleaf->mins[2] && leaf->mins[2] < nleaf->maxs[2];
} else {
overlaps = leaf->maxs[0] > nleaf->mins[0] && leaf->mins[0] < nleaf->maxs[0];
}
if (!overlaps) continue;
f32 bpx0, bpz0, bpx1, bpz1;
if (dirs[d].dx != 0) {
bpx0 = dirs[d].bnd; bpx1 = dirs[d].bnd;
i16 zlo = leaf->mins[2] > nleaf->mins[2] ? leaf->mins[2] : nleaf->mins[2];
i16 zhi = leaf->maxs[2] < nleaf->maxs[2] ? leaf->maxs[2] : nleaf->maxs[2];
bpz0 = (f32)zlo; bpz1 = (f32)zhi;
} else {
bpz0 = dirs[d].bnd; bpz1 = dirs[d].bnd;
i16 xlo = leaf->mins[0] > nleaf->mins[0] ? leaf->mins[0] : nleaf->mins[0];
i16 xhi = leaf->maxs[0] < nleaf->maxs[0] ? leaf->maxs[0] : nleaf->maxs[0];
bpx0 = (f32)xlo; bpx1 = (f32)xhi;
}
if (cam_exterior) {
vis_try_enqueue(world->vis_ptrs, world->vis_win_ptrs,
world->vis_queue, &tail, PXL8_VIS_MAX_QUEUE,
(u16)nci, (u16)nl, full_screen);
continue;
}
pxl8_rect psr = project_portal(bpx0, bpz0, bpx1, bpz1,
0.0f, PORTAL_Y_HI, vp);
if (!vr_valid(psr)) continue;
pxl8_rect nw = vr_intersect(cur.window, psr);
if (!vr_valid(nw)) continue;
vis_try_enqueue(world->vis_ptrs, world->vis_win_ptrs,
world->vis_queue, &tail, PXL8_VIS_MAX_QUEUE,
(u16)nci, (u16)nl, nw);
}
}
}
}
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) {
if (!world || !gfx) return;
if (world->active_chunk && world->active_chunk->bsp)
pxl8_3d_set_bsp(gfx, world->active_chunk->bsp);
else
pxl8_3d_set_bsp(gfx, NULL);
world_compute_visibility(world, gfx, camera_pos);
for (u32 i = 0; i < world->loaded_count; i++) {
pxl8_loaded_chunk* lc = &world->loaded[i];
if (!lc->chunk || !lc->chunk->bsp || !lc->render_state) continue;
pxl8_bsp_render(gfx, lc->chunk->bsp, lc->render_state, NULL);
}
}
static void apply_shared_materials(pxl8_world* world, pxl8_bsp_render_state* rs) {
for (u16 i = 0; i < 16; i++) {
if (world->shared_material_set[i]) {
pxl8_bsp_set_material(rs, i, &world->shared_materials[i]);
}
}
}
void pxl8_world_sync(pxl8_world* world, pxl8_net* net) {
if (!world || !net) return;
if (!pxl8_net_has_chunk(net)) {
u32 old_count = world->loaded_count;
world->loaded_count = 0;
world->active_chunk = NULL;
world->active_render_state = NULL;
for (u32 i = 0; i < old_count; i++) {
pxl8_bsp_render_state_destroy(world->loaded[i].render_state);
}
return;
}
i32 center_cx = pxl8_net_chunk_cx(net);
i32 center_cz = pxl8_net_chunk_cz(net);
pxl8_loaded_chunk old_loaded[PXL8_WORLD_MAX_LOADED_CHUNKS];
u32 old_count = world->loaded_count;
memcpy(old_loaded, world->loaded, sizeof(old_loaded));
pxl8_loaded_chunk new_loaded[PXL8_WORLD_MAX_LOADED_CHUNKS];
u32 new_count = 0;
pxl8_world_chunk* new_active_chunk = NULL;
pxl8_bsp_render_state* new_active_rs = NULL;
for (i32 dz = -2; dz <= 2; dz++) {
for (i32 dx = -2; dx <= 2; dx++) {
i32 cx = center_cx + dx;
i32 cz = center_cz + dz;
u32 id = pxl8_chunk_hash(cx, cz);
pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_bsp(world->chunk_cache, id);
if (!chunk || !chunk->bsp) continue;
pxl8_bsp_render_state* rs = NULL;
for (u32 j = 0; j < old_count; j++) {
if (old_loaded[j].active && old_loaded[j].cx == cx && old_loaded[j].cz == cz &&
old_loaded[j].chunk == chunk) {
rs = old_loaded[j].render_state;
old_loaded[j].active = false;
break;
}
}
if (!rs) {
rs = pxl8_bsp_render_state_create(chunk->bsp->num_faces);
if (rs && rs->render_face_flags)
memset(rs->render_face_flags, 1, rs->num_faces);
apply_shared_materials(world, rs);
}
u32 idx = new_count++;
new_loaded[idx] = (pxl8_loaded_chunk){
.chunk = chunk,
.render_state = rs,
.cx = cx,
.cz = cz,
.active = true,
};
compute_edge_leafs(&new_loaded[idx]);
if (dx == 0 && dz == 0) {
new_active_chunk = chunk;
new_active_rs = rs;
}
}
}
memcpy(world->loaded, new_loaded, sizeof(new_loaded));
world->active_chunk = new_active_chunk;
world->active_render_state = new_active_rs;
world->loaded_count = new_count;
for (u32 j = 0; j < old_count; j++) {
if (old_loaded[j].active) {
pxl8_bsp_render_state_destroy(old_loaded[j].render_state);
}
}
}
void pxl8_world_set_bsp_material(pxl8_world* world, u16 material_id, const pxl8_gfx_material* material) {
if (!world || !material || material_id >= 16) return;
world->shared_materials[material_id] = *material;
world->shared_material_set[material_id] = true;
for (u32 i = 0; i < world->loaded_count; i++) {
if (world->loaded[i].render_state) {
pxl8_bsp_set_material(world->loaded[i].render_state, material_id, material);
}
}
}
void pxl8_world_set_sim_config(pxl8_world* world, const pxl8_sim_config* config) {
if (!world || !config) return;
world->sim_config = *config;
}
void pxl8_world_init_local_player(pxl8_world* world, f32 x, f32 y, f32 z) {
if (!world) return;
world->local_player.pos = (pxl8_vec3){x, y, z};
world->local_player.vel = (pxl8_vec3){0, 0, 0};
world->local_player.yaw = 0;
world->local_player.pitch = 0;
world->local_player.flags = PXL8_SIM_FLAG_ALIVE | PXL8_SIM_FLAG_PLAYER | PXL8_SIM_FLAG_GROUNDED;
world->local_player.kind = 0;
world->client_tick = 0;
world->pointer_motion = (pxl8_vec2){0};
#ifdef PXL8_ASYNC_THREADS
world->render_state[0] = world->local_player;
world->render_state[1] = world->local_player;
#endif
}
void pxl8_world_set_look(pxl8_world* world, f32 yaw, f32 pitch) {
if (!world) return;
world->local_player.yaw = yaw;
world->local_player.pitch = pitch;
world->pointer_motion = (pxl8_vec2){.yaw = yaw, .pitch = pitch};
#ifdef PXL8_ASYNC_THREADS
world->render_state[0].yaw = yaw;
world->render_state[0].pitch = pitch;
world->render_state[1].yaw = yaw;
world->render_state[1].pitch = pitch;
#endif
}
pxl8_sim_entity* pxl8_world_local_player(pxl8_world* world) {
if (!world) return NULL;
#ifdef PXL8_ASYNC_THREADS
const pxl8_sim_entity* state = pxl8_world_get_render_state(world);
if (!state) return NULL;
if (!(state->flags & PXL8_SIM_FLAG_ALIVE)) return NULL;
return (pxl8_sim_entity*)state;
#else
if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return NULL;
return &world->local_player;
#endif
}
void pxl8_world_predict(pxl8_world* world, pxl8_net* net, const pxl8_input_msg* input, f32 dt) {
if (!world || !net || !input) return;
if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return;
pxl8_sim_world sim = pxl8_world_sim_world(world, world->local_player.pos);
pxl8_sim_move_player(&world->local_player, input, &sim, &world->sim_config, dt);
world->client_tick++;
entity_to_userdata(&world->local_player, pxl8_net_predicted_state(net));
pxl8_net_predicted_tick_set(net, world->client_tick);
}
void pxl8_world_reconcile(pxl8_world* world, pxl8_net* net, f32 dt) {
if (!world || !net) return;
if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return;
if (!pxl8_net_needs_correction(net)) return;
u64 player_id = pxl8_net_player_id(net);
const u8* server_state = pxl8_net_entity_userdata(net, player_id);
if (!server_state) return;
pxl8_sim_entity server_player = {0};
userdata_to_entity(server_state, &server_player);
if (!(server_player.flags & PXL8_SIM_FLAG_ALIVE)) {
return;
}
world->local_player = server_player;
const pxl8_snapshot_header* snap = pxl8_net_snapshot(net);
u64 server_tick = snap ? snap->tick : 0;
for (u64 tick = server_tick + 1; tick <= world->client_tick; tick++) {
const pxl8_input_msg* input = pxl8_net_input_at(net, tick);
if (!input) continue;
pxl8_sim_world sim = pxl8_world_sim_world(world, world->local_player.pos);
pxl8_sim_move_player(&world->local_player, input, &sim, &world->sim_config, dt);
}
entity_to_userdata(&world->local_player, pxl8_net_predicted_state(net));
}
#ifdef PXL8_ASYNC_THREADS
#define SIM_TIMESTEP (1.0f / 60.0f)
static void pxl8_world_sim_tick(pxl8_world* world, f32 dt) {
bool alive = (world->local_player.flags & PXL8_SIM_FLAG_ALIVE) != 0;
pxl8_input_msg merged = {0};
pxl8_input_msg* input = NULL;
while ((input = pxl8_queue_pop(&world->input_queue))) {
merged.look_dx += input->look_dx;
merged.look_dy += input->look_dy;
merged.move_x = input->move_x;
merged.move_y = input->move_y;
merged.buttons |= input->buttons;
pxl8_free(input);
}
if (!alive) {
return;
}
const f32 MAX_LOOK_DELTA = 100.0f;
if (merged.look_dx > MAX_LOOK_DELTA) merged.look_dx = MAX_LOOK_DELTA;
if (merged.look_dx < -MAX_LOOK_DELTA) merged.look_dx = -MAX_LOOK_DELTA;
if (merged.look_dy > MAX_LOOK_DELTA) merged.look_dy = MAX_LOOK_DELTA;
if (merged.look_dy < -MAX_LOOK_DELTA) merged.look_dy = -MAX_LOOK_DELTA;
merged.tick = world->client_tick;
merged.timestamp = pxl8_get_ticks_ns();
merged.yaw = world->pointer_motion.yaw;
if (world->net) {
pxl8_net_send_input(world->net, &merged);
}
world->local_player.yaw = world->pointer_motion.yaw;
world->local_player.pitch = world->pointer_motion.pitch;
merged.look_dx = 0;
merged.look_dy = 0;
pxl8_sim_world sim = pxl8_world_sim_world(world, world->local_player.pos);
pxl8_sim_move_player(&world->local_player, &merged, &sim, &world->sim_config, dt);
if (world->net) {
entity_to_userdata(&world->local_player, pxl8_net_predicted_state(world->net));
pxl8_net_predicted_tick_set(world->net, world->client_tick);
}
world->client_tick++;
}
static void pxl8_world_swap_buffers(pxl8_world* world) {
u32 back = atomic_load(&world->active_buffer) ^ 1;
world->render_state[back] = world->local_player;
atomic_store(&world->active_buffer, back);
}
static int pxl8_world_sim_thread(void* data) {
pxl8_world* world = (pxl8_world*)data;
u64 last_time = pxl8_get_ticks_ns();
while (atomic_load(&world->sim_running)) {
if (atomic_load(&world->sim_paused)) {
last_time = pxl8_get_ticks_ns();
pxl8_sleep_ms(1);
continue;
}
u64 now = pxl8_get_ticks_ns();
f32 dt = (f32)(now - last_time) / 1e9f;
last_time = now;
if (dt > 0.1f) dt = 0.1f;
if (dt < 0.0001f) dt = 0.0001f;
world->sim_accumulator += dt;
while (world->sim_accumulator >= SIM_TIMESTEP) {
pxl8_world_chunk_cache_tick(world->chunk_cache);
if (world->net) {
pxl8_packet* pkt;
while ((pkt = pxl8_net_pop_packet(world->net))) {
pxl8_net_process_packet(world->net, pkt);
pxl8_net_packet_free(pkt);
}
pxl8_world_sync(world, world->net);
pxl8_world_reconcile(world, world->net, SIM_TIMESTEP);
}
pxl8_world_sim_tick(world, SIM_TIMESTEP);
world->sim_accumulator -= SIM_TIMESTEP;
}
pxl8_world_swap_buffers(world);
pxl8_sleep_ms(1);
}
return 0;
}
void pxl8_world_start_sim_thread(pxl8_world* world, pxl8_net* net) {
if (!world || world->sim_thread) return;
world->net = net;
pxl8_queue_init(&world->input_queue);
atomic_store(&world->active_buffer, 0);
atomic_store(&world->sim_running, true);
world->sim_accumulator = 0.0f;
world->render_state[0] = world->local_player;
world->render_state[1] = world->local_player;
world->sim_thread = pxl8_thread_create(pxl8_world_sim_thread, "pxl8_sim", world);
}
void pxl8_world_stop_sim_thread(pxl8_world* world) {
if (!world || !world->sim_thread) return;
atomic_store(&world->sim_running, false);
pxl8_thread_wait(world->sim_thread, NULL);
world->sim_thread = NULL;
pxl8_input_msg* input;
while ((input = pxl8_queue_pop(&world->input_queue))) {
pxl8_free(input);
}
}
void pxl8_world_pause_sim(pxl8_world* world, bool paused) {
if (!world) return;
if (paused) {
atomic_store(&world->sim_paused, true);
} else {
pxl8_input_msg* input;
while ((input = pxl8_queue_pop(&world->input_queue))) {
pxl8_free(input);
}
world->sim_accumulator = 0.0f;
atomic_store(&world->sim_paused, false);
}
}
void pxl8_world_push_input(pxl8_world* world, const pxl8_input_msg* input) {
if (!world || !input) return;
world->pointer_motion.yaw -= input->look_dx * 0.008f;
f32 pitch = world->pointer_motion.pitch - input->look_dy * 0.008f;
if (pitch > world->sim_config.max_pitch) pitch = world->sim_config.max_pitch;
if (pitch < -world->sim_config.max_pitch) pitch = -world->sim_config.max_pitch;
world->pointer_motion.pitch = pitch;
pxl8_input_msg* copy = pxl8_malloc(sizeof(pxl8_input_msg));
if (copy) {
*copy = *input;
if (!pxl8_queue_push(&world->input_queue, copy)) {
pxl8_free(copy);
}
}
}
const pxl8_sim_entity* pxl8_world_get_render_state(const pxl8_world* world) {
if (!world) return NULL;
u32 front = atomic_load(&((pxl8_world*)world)->active_buffer);
return &world->render_state[front];
}
f32 pxl8_world_get_interp_alpha(const pxl8_world* world) {
if (!world) return 1.0f;
return world->sim_accumulator / SIM_TIMESTEP;
}
#endif