#include "demo3d_world.h" #include #include "pxl8_platform.h" #ifdef PXL8_ASYNC_THREADS #include #include "pxl8_queue.h" #endif #include "demo3d_bsp_render.h" #include "pxl8_io.h" #include "pxl8_gfx3d.h" #include "pxl8_log.h" #include "pxl8_mem.h" #include "demo3d_protocol.h" #include "demo3d_sim.h" #define DEMO3D_VIS_MAX_NODES (DEMO3D_WORLD_MAX_LOADED_CHUNKS * 512) #define DEMO3D_VIS_MAX_QUEUE (DEMO3D_VIS_MAX_NODES * 4) #define DEMO3D_VIS_BYTES ((DEMO3D_VIS_MAX_NODES + 7) / 8) #define DEMO3D_WORLD_ENTITY_CAPACITY 256 typedef struct { u16 chunk_idx; u16 leaf_idx; pxl8_rect window; } world_vis_node; struct demo3d_world { demo3d_loaded_chunk loaded[DEMO3D_WORLD_MAX_LOADED_CHUNKS]; u32 loaded_count; demo3d_chunk* active_chunk; demo3d_bsp_render_state* active_render_state; pxl8_gfx_material shared_materials[16]; bool shared_material_set[16]; demo3d_chunk_cache* chunk_cache; demo3d_entity_pool* entities; demo3d_sim_entity local_player; u64 client_tick; pxl8_vec2 pointer_motion; demo3d_sim_config sim_config; u8 vis_bits[DEMO3D_VIS_BYTES]; u8* vis_ptrs[DEMO3D_WORLD_MAX_LOADED_CHUNKS]; pxl8_rect vis_windows[DEMO3D_VIS_MAX_NODES]; pxl8_rect* vis_win_ptrs[DEMO3D_WORLD_MAX_LOADED_CHUNKS]; world_vis_node vis_queue[DEMO3D_VIS_MAX_QUEUE]; #ifdef PXL8_ASYNC_THREADS demo3d_sim_entity render_state[2]; atomic_uint active_buffer; pxl8_thread* sim_thread; atomic_bool sim_running; atomic_bool sim_paused; demo3d_net* net; pxl8_queue input_queue; f32 sim_accumulator; #endif }; demo3d_world* demo3d_world_create(void) { demo3d_world* world = pxl8_calloc(1, sizeof(demo3d_world)); if (!world) return NULL; world->chunk_cache = demo3d_chunk_cache_create(); world->entities = demo3d_entity_pool_create(DEMO3D_WORLD_ENTITY_CAPACITY); world->sim_config = (demo3d_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) { demo3d_world_destroy(world); return NULL; } return world; } void demo3d_world_destroy(demo3d_world* world) { if (!world) return; for (u32 i = 0; i < world->loaded_count; i++) { demo3d_bsp_render_state_destroy(world->loaded[i].render_state); } demo3d_chunk_cache_destroy(world->chunk_cache); demo3d_entity_pool_destroy(world->entities); pxl8_free(world); } demo3d_chunk_cache* demo3d_world_get_chunk_cache(demo3d_world* world) { if (!world) return NULL; return world->chunk_cache; } demo3d_chunk* demo3d_world_active_chunk(demo3d_world* world) { if (!world) return NULL; return world->active_chunk; } demo3d_sim_world demo3d_world_sim_world(const demo3d_world* world, pxl8_vec3 pos) { demo3d_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 demo3d_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 demo3d_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, demo3d_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 demo3d_world_point_solid(const demo3d_world* world, f32 x, f32 y, f32 z) { if (!world) return false; if (world->active_chunk && world->active_chunk->bsp) { return demo3d_bsp_point_solid(world->active_chunk->bsp, (pxl8_vec3){x, y, z}); } return false; } pxl8_ray demo3d_world_ray(const demo3d_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 (demo3d_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 = demo3d_world_point_solid(world, pos.x - eps, pos.y, pos.z); bool sx_pos = demo3d_world_point_solid(world, pos.x + eps, pos.y, pos.z); bool sy_neg = demo3d_world_point_solid(world, pos.x, pos.y - eps, pos.z); bool sy_pos = demo3d_world_point_solid(world, pos.x, pos.y + eps, pos.z); bool sz_neg = demo3d_world_point_solid(world, pos.x, pos.y, pos.z - eps); bool sz_pos = demo3d_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 demo3d_world_sweep(const demo3d_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 = demo3d_world_point_solid(world, to.x, to.y, to.z) || demo3d_world_point_solid(world, to.x + radius, to.y, to.z) || demo3d_world_point_solid(world, to.x - radius, to.y, to.z) || demo3d_world_point_solid(world, to.x, to.y, to.z + radius) || demo3d_world_point_solid(world, to.x, to.y, to.z - radius) || demo3d_world_point_solid(world, to.x + diag, to.y, to.z + diag) || demo3d_world_point_solid(world, to.x + diag, to.y, to.z - diag) || demo3d_world_point_solid(world, to.x - diag, to.y, to.z + diag) || demo3d_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 demo3d_world_update(demo3d_world* world, f32 dt) { (void)dt; if (!world) return; demo3d_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(demo3d_loaded_chunk* lc) { const f32 CHUNK_SIZE = 16.0f * 64.0f; const demo3d_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 demo3d_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 demo3d_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 demo3d_bsp* bsp, demo3d_bsp_render_state* rs, u32 leaf_idx) { const demo3d_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(demo3d_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++) { demo3d_loaded_chunk* lc = &world->loaded[i]; if (!lc->chunk || !lc->chunk->bsp) continue; const demo3d_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 = demo3d_bsp_find_leaf(bsp, camera_pos); if (leaf >= 0 && (u32)leaf < bsp->num_leafs && bsp->leafs[leaf].contents == -2) { cam_ci = (i32)i; cam_li = leaf; break; } } if (cam_ci < 0 || !vp) { for (u32 i = 0; i < world->loaded_count; i++) { demo3d_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 > DEMO3D_VIS_BYTES || woff + nl > DEMO3D_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++) { demo3d_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 demo3d_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++]; demo3d_loaded_chunk* lc = &world->loaded[cur.chunk_idx]; const demo3d_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 demo3d_bsp_cell_portals* cp = &bsp->cell_portals[cur.leaf_idx]; for (u8 pi = 0; pi < cp->num_portals; pi++) { const demo3d_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 && !demo3d_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, DEMO3D_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, DEMO3D_VIS_MAX_QUEUE, cur.chunk_idx, (u16)target, nw); } } const demo3d_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 demo3d_bsp* nbsp = world->loaded[nci].chunk->bsp; const demo3d_edge_leafs* nedge = &world->loaded[nci].edges[opposite_edge[d]]; for (u8 k = 0; k < nedge->count; k++) { u16 nl = nedge->leafs[k]; const demo3d_bsp_leaf* nleaf =  ->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, DEMO3D_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, DEMO3D_VIS_MAX_QUEUE, (u16)nci, (u16)nl, nw); } } } } void demo3d_world_render(demo3d_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) { if (!world || !gfx) return; world_compute_visibility(world, gfx, camera_pos); for (u32 i = 0; i < world->loaded_count; i++) { demo3d_loaded_chunk* lc = &world->loaded[i]; if (!lc->chunk || !lc->chunk->bsp || !lc->render_state) continue; demo3d_bsp_render(gfx, lc->chunk->bsp, lc->render_state, NULL); } } static void apply_shared_materials(demo3d_world* world, demo3d_bsp_render_state* rs) { for (u16 i = 0; i < 16; i++) { if (world->shared_material_set[i]) { demo3d_bsp_set_material(rs, i, &world->shared_materials[i]); } } } void demo3d_world_sync(demo3d_world* world, demo3d_net* net) { if (!world || !net) return; if (!demo3d_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++) { demo3d_bsp_render_state_destroy(world->loaded[i].render_state); } return; } i32 center_cx = demo3d_net_chunk_cx(net); i32 center_cz = demo3d_net_chunk_cz(net); demo3d_loaded_chunk old_loaded[DEMO3D_WORLD_MAX_LOADED_CHUNKS]; u32 old_count = world->loaded_count; memcpy(old_loaded, world->loaded, sizeof(old_loaded)); demo3d_loaded_chunk new_loaded[DEMO3D_WORLD_MAX_LOADED_CHUNKS]; u32 new_count = 0; demo3d_chunk* new_active_chunk = NULL; demo3d_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 = demo3d_chunk_hash(cx, cz); demo3d_chunk* chunk = demo3d_chunk_cache_get_bsp(world->chunk_cache, id); if (!chunk || !chunk->bsp) { for (u32 j = 0; j < old_count; j++) { if (old_loaded[j].active && old_loaded[j].cx == cx && old_loaded[j].cz == cz) { u32 idx = new_count++; new_loaded[idx] = old_loaded[j]; old_loaded[j].active = false; if (dx == 0 && dz == 0) { new_active_chunk = new_loaded[idx].chunk; new_active_rs = new_loaded[idx].render_state; } break; } } continue; } demo3d_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 = demo3d_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] = (demo3d_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; } } } for (u32 j = 0; j < old_count; j++) { if (!old_loaded[j].active) continue; if (new_count < DEMO3D_WORLD_MAX_LOADED_CHUNKS && old_loaded[j].chunk && old_loaded[j].chunk->bsp && demo3d_chunk_cache_get_bsp(world->chunk_cache, old_loaded[j].chunk->id)) { new_loaded[new_count++] = old_loaded[j]; } else { demo3d_bsp_render_state_destroy(old_loaded[j].render_state); } } 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; } void demo3d_world_set_bsp_material(demo3d_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) { demo3d_bsp_set_material(world->loaded[i].render_state, material_id, material); } } } void demo3d_world_set_sim_config(demo3d_world* world, const demo3d_sim_config* config) { if (!world || !config) return; world->sim_config = *config; } void demo3d_world_init_local_player(demo3d_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 = DEMO3D_SIM_FLAG_ALIVE | DEMO3D_SIM_FLAG_PLAYER | DEMO3D_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 demo3d_world_set_look(demo3d_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 } demo3d_sim_entity* demo3d_world_local_player(demo3d_world* world) { if (!world) return NULL; #ifdef PXL8_ASYNC_THREADS const demo3d_sim_entity* state = demo3d_world_get_render_state(world); if (!state) return NULL; if (!(state->flags & DEMO3D_SIM_FLAG_ALIVE)) return NULL; return (demo3d_sim_entity*)state; #else if (!(world->local_player.flags & DEMO3D_SIM_FLAG_ALIVE)) return NULL; return &world->local_player; #endif } void demo3d_world_predict(demo3d_world* world, demo3d_net* net, const demo3d_input_msg* input, f32 dt) { if (!world || !net || !input) return; if (!(world->local_player.flags & DEMO3D_SIM_FLAG_ALIVE)) return; demo3d_sim_world sim = demo3d_world_sim_world(world, world->local_player.pos); demo3d_sim_move_player(&world->local_player, input, &sim, &world->sim_config, dt); world->client_tick++; entity_to_userdata(&world->local_player, demo3d_net_predicted_state(net)); demo3d_net_predicted_tick_set(net, world->client_tick); } void demo3d_world_reconcile(demo3d_world* world, demo3d_net* net, f32 dt) { if (!world || !net) return; if (!(world->local_player.flags & DEMO3D_SIM_FLAG_ALIVE)) return; if (!demo3d_net_needs_correction(net)) return; u64 player_id = demo3d_net_player_id(net); const u8* server_state = demo3d_net_entity_userdata(net, player_id); if (!server_state) return; demo3d_sim_entity server_player = {0}; userdata_to_entity(server_state, &server_player); if (!(server_player.flags & DEMO3D_SIM_FLAG_ALIVE)) { return; } world->local_player = server_player; const demo3d_snapshot_header* snap = demo3d_net_snapshot(net); u64 server_tick = snap ? snap->tick : 0; for (u64 tick = server_tick + 1; tick <= world->client_tick; tick++) { const demo3d_input_msg* input = demo3d_net_input_at(net, tick); if (!input) continue; demo3d_sim_world sim = demo3d_world_sim_world(world, world->local_player.pos); demo3d_sim_move_player(&world->local_player, input, &sim, &world->sim_config, dt); } entity_to_userdata(&world->local_player, demo3d_net_predicted_state(net)); } #ifdef PXL8_ASYNC_THREADS #define SIM_TIMESTEP (1.0f / 60.0f) static void demo3d_world_sim_tick(demo3d_world* world, f32 dt) { bool alive = (world->local_player.flags & DEMO3D_SIM_FLAG_ALIVE) != 0; demo3d_input_msg merged = {0}; demo3d_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) { demo3d_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; demo3d_sim_world sim = demo3d_world_sim_world(world, world->local_player.pos); demo3d_sim_move_player(&world->local_player, &merged, &sim, &world->sim_config, dt); if (world->net) { entity_to_userdata(&world->local_player, demo3d_net_predicted_state(world->net)); demo3d_net_predicted_tick_set(world->net, world->client_tick); } world->client_tick++; } static void demo3d_world_swap_buffers(demo3d_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 demo3d_world_sim_thread(void* data) { demo3d_world* world = (demo3d_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) { demo3d_chunk_cache_tick(world->chunk_cache); if (world->net) { pxl8_packet* pkt; while ((pkt = pxl8_net_pop_packet(world->net->transport))) { demo3d_net_dispatch_packet(world->net, pkt); pxl8_net_packet_free(pkt); } demo3d_world_sync(world, world->net); demo3d_world_reconcile(world, world->net, SIM_TIMESTEP); } demo3d_world_sim_tick(world, SIM_TIMESTEP); world->sim_accumulator -= SIM_TIMESTEP; } demo3d_world_swap_buffers(world); pxl8_sleep_ms(1); } return 0; } void demo3d_world_start_sim_thread(demo3d_world* world, demo3d_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(demo3d_world_sim_thread, "pxl8_sim", world); } void demo3d_world_stop_sim_thread(demo3d_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; demo3d_input_msg* input; while ((input = pxl8_queue_pop(&world->input_queue))) { pxl8_free(input); } } void demo3d_world_pause_sim(demo3d_world* world, bool paused) { if (!world) return; if (paused) { atomic_store(&world->sim_paused, true); } else { demo3d_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 demo3d_world_push_input(demo3d_world* world, const demo3d_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; demo3d_input_msg* copy = pxl8_malloc(sizeof(demo3d_input_msg)); if (copy) { *copy = *input; if (!pxl8_queue_push(&world->input_queue, copy)) { pxl8_free(copy); } } } const demo3d_sim_entity* demo3d_world_get_render_state(const demo3d_world* world) { if (!world) return NULL; u32 front = atomic_load(&((demo3d_world*)world)->active_buffer); return &world->render_state[front]; } f32 demo3d_world_get_interp_alpha(const demo3d_world* world) { if (!world) return 1.0f; return world->sim_accumulator / SIM_TIMESTEP; } #endif