pxl8/src/world/pxl8_world.c

700 lines
23 KiB
C
Raw Normal View History

2025-11-13 07:15:41 -06:00
#include "pxl8_world.h"
2026-01-31 09:31:17 -06:00
#include <string.h>
2025-10-07 10:32:48 -05:00
2026-01-31 09:31:17 -06:00
#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"
2025-12-02 11:02:23 -06:00
#include "pxl8_log.h"
2026-01-21 23:19:50 -06:00
#include "pxl8_mem.h"
2026-01-31 09:31:17 -06:00
#include "pxl8_sim.h"
#include "pxl8_vxl.h"
#include "pxl8_vxl_render.h"
2025-10-07 10:32:48 -05:00
2026-01-25 09:26:30 -06:00
#define PXL8_WORLD_ENTITY_CAPACITY 256
2026-01-31 09:31:17 -06:00
#define VOXEL_SCALE 16.0f
#define VOXEL_CHUNK_SIZE 32.0f
#define WORLD_CHUNK_SIZE (VOXEL_CHUNK_SIZE * VOXEL_SCALE)
2026-01-25 09:26:30 -06:00
2025-10-07 10:32:48 -05:00
struct pxl8_world {
2026-01-31 09:31:17 -06:00
pxl8_world_chunk* active_chunk;
pxl8_vxl_block_registry* block_registry;
pxl8_world_chunk_cache* chunk_cache;
2026-01-25 09:26:30 -06:00
pxl8_entity_pool* entities;
2026-01-31 09:31:17 -06:00
pxl8_bsp_render_state* bsp_render_state;
pxl8_vxl_render_state* vxl_render_state;
i32 render_distance;
i32 sim_distance;
pxl8_sim_entity local_player;
u64 client_tick;
2026-02-02 17:48:25 -06:00
pxl8_vec2 pointer_motion;
2026-01-31 09:31:17 -06:00
#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
2025-10-07 10:32:48 -05:00
};
pxl8_world* pxl8_world_create(void) {
2026-01-25 09:26:30 -06:00
pxl8_world* world = pxl8_calloc(1, sizeof(pxl8_world));
if (!world) return NULL;
2026-01-31 09:31:17 -06:00
world->block_registry = pxl8_vxl_block_registry_create();
world->chunk_cache = pxl8_world_chunk_cache_create();
2026-01-25 09:26:30 -06:00
world->entities = pxl8_entity_pool_create(PXL8_WORLD_ENTITY_CAPACITY);
2026-01-31 09:31:17 -06:00
world->vxl_render_state = pxl8_vxl_render_state_create();
world->render_distance = 3;
world->sim_distance = 4;
2026-01-25 09:26:30 -06:00
2026-01-31 09:31:17 -06:00
if (!world->block_registry || !world->chunk_cache || !world->entities || !world->vxl_render_state) {
2026-01-25 09:26:30 -06:00
pxl8_world_destroy(world);
2025-10-07 10:32:48 -05:00
return NULL;
}
return world;
}
void pxl8_world_destroy(pxl8_world* world) {
if (!world) return;
2026-01-31 09:31:17 -06:00
pxl8_vxl_block_registry_destroy(world->block_registry);
pxl8_world_chunk_cache_destroy(world->chunk_cache);
2026-01-25 09:26:30 -06:00
pxl8_entity_pool_destroy(world->entities);
2026-01-31 09:31:17 -06:00
pxl8_bsp_render_state_destroy(world->bsp_render_state);
pxl8_vxl_render_state_destroy(world->vxl_render_state);
2026-01-21 23:19:50 -06:00
pxl8_free(world);
2025-10-07 10:32:48 -05:00
}
2026-01-31 09:31:17 -06:00
pxl8_world_chunk_cache* pxl8_world_get_chunk_cache(pxl8_world* world) {
2026-01-25 09:26:30 -06:00
if (!world) return NULL;
return world->chunk_cache;
}
2026-01-31 09:31:17 -06:00
pxl8_world_chunk* pxl8_world_active_chunk(pxl8_world* world) {
2026-01-25 09:26:30 -06:00
if (!world) return NULL;
return world->active_chunk;
2025-11-21 16:44:42 -06:00
}
2026-01-31 09:31:17 -06:00
void pxl8_world_set_active_chunk(pxl8_world* world, pxl8_world_chunk* chunk) {
2026-01-25 09:26:30 -06:00
if (!world) return;
world->active_chunk = chunk;
2025-11-21 16:44:42 -06:00
}
2026-01-31 09:31:17 -06:00
pxl8_vxl_block_registry* pxl8_world_block_registry(pxl8_world* world) {
2026-01-25 09:26:30 -06:00
if (!world) return NULL;
return world->block_registry;
}
2026-01-25 09:26:30 -06:00
pxl8_entity_pool* pxl8_world_entities(pxl8_world* world) {
if (!world) return NULL;
return world->entities;
}
2025-11-09 06:30:17 -06:00
2026-01-25 09:26:30 -06:00
pxl8_entity pxl8_world_spawn(pxl8_world* world) {
if (!world || !world->entities) return PXL8_ENTITY_INVALID;
return pxl8_entity_spawn(world->entities);
2025-11-09 06:30:17 -06:00
}
2026-01-31 09:31:17 -06:00
static bool voxel_point_solid(pxl8_world_chunk_cache* cache, f32 x, f32 y, f32 z) {
i32 cx = (i32)floorf(x / WORLD_CHUNK_SIZE);
i32 cy = (i32)floorf(y / WORLD_CHUNK_SIZE);
i32 cz = (i32)floorf(z / WORLD_CHUNK_SIZE);
f32 local_x = (x - cx * WORLD_CHUNK_SIZE) / VOXEL_SCALE;
f32 local_y = (y - cy * WORLD_CHUNK_SIZE) / VOXEL_SCALE;
f32 local_z = (z - cz * WORLD_CHUNK_SIZE) / VOXEL_SCALE;
i32 lx = (i32)floorf(local_x);
i32 ly = (i32)floorf(local_y);
i32 lz = (i32)floorf(local_z);
lx = lx < 0 ? 0 : (lx >= 32 ? 31 : lx);
ly = ly < 0 ? 0 : (ly >= 32 ? 31 : ly);
lz = lz < 0 ? 0 : (lz >= 32 ? 31 : lz);
pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_vxl(cache, cx, cy, cz);
if (!chunk || !chunk->voxels) {
return ly < 8;
}
return pxl8_vxl_block_get(chunk->voxels, lx, ly, lz) != PXL8_VXL_BLOCK_AIR;
}
static pxl8_sim_world make_sim_world(const pxl8_world* world, pxl8_vec3 pos) {
pxl8_sim_world sim = {0};
if (world->active_chunk && world->active_chunk->type == PXL8_WORLD_CHUNK_BSP) {
sim.bsp = world->active_chunk->bsp;
return sim;
}
if (world->chunk_cache) {
i32 cx = (i32)floorf(pos.x / WORLD_CHUNK_SIZE);
i32 cy = (i32)floorf(pos.y / WORLD_CHUNK_SIZE);
i32 cz = (i32)floorf(pos.z / WORLD_CHUNK_SIZE);
pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_vxl(world->chunk_cache, cx, cy, cz);
if (chunk && chunk->voxels) {
sim.vxl = chunk->voxels;
sim.vxl_cx = cx;
sim.vxl_cy = cy;
sim.vxl_cz = cz;
}
}
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) {
if (world->active_chunk->type == PXL8_WORLD_CHUNK_BSP && world->active_chunk->bsp) {
return pxl8_bsp_point_solid(world->active_chunk->bsp, (pxl8_vec3){x, y, z});
}
} else if (world->chunk_cache) {
return voxel_point_solid(world->chunk_cache, x, y, z);
2025-10-07 10:32:48 -05:00
}
2025-11-21 16:44:42 -06:00
return false;
}
2025-10-07 10:32:48 -05:00
2026-01-31 09:31:17 -06:00
pxl8_ray pxl8_world_ray(const pxl8_world* world, pxl8_vec3 from, pxl8_vec3 to) {
pxl8_ray result = {0};
2025-11-21 16:44:42 -06:00
2026-01-31 09:31:17 -06:00
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;
2025-10-07 10:32:48 -05:00
}
2026-01-31 09:31:17 -06:00
return result;
2026-01-25 09:26:30 -06:00
}
2025-10-07 10:32:48 -05:00
2026-01-31 09:31:17 -06:00
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, const pxl8_input_state* input, f32 dt) {
2026-01-25 09:26:30 -06:00
if (!world) return;
2025-11-21 16:44:42 -06:00
2026-01-31 09:31:17 -06:00
pxl8_world_chunk_cache_tick(world->chunk_cache);
if (input && (world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) {
pxl8_input_msg msg = {0};
msg.move_x = (pxl8_key_down(input, "d") ? 1.0f : 0.0f) - (pxl8_key_down(input, "a") ? 1.0f : 0.0f);
msg.move_y = (pxl8_key_down(input, "w") ? 1.0f : 0.0f) - (pxl8_key_down(input, "s") ? 1.0f : 0.0f);
msg.look_dx = (f32)pxl8_mouse_dx(input);
msg.look_dy = (f32)pxl8_mouse_dy(input);
msg.buttons = pxl8_key_down(input, "space") ? 1 : 0;
2026-02-02 17:48:25 -06:00
msg.tick = world->client_tick;
msg.timestamp = pxl8_get_ticks_ns();
if (world->net) {
pxl8_net_send_input(world->net, &msg);
}
2026-01-31 09:31:17 -06:00
pxl8_sim_world sim = make_sim_world(world, world->local_player.pos);
pxl8_sim_move_player(&world->local_player, &msg, &sim, dt);
2026-02-02 17:48:25 -06:00
world->client_tick++;
2026-01-31 09:31:17 -06:00
}
2026-01-25 09:26:30 -06:00
}
2025-11-21 16:44:42 -06:00
2026-01-25 09:26:30 -06:00
void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) {
2026-01-31 09:31:17 -06:00
if (!world || !gfx) return;
2025-11-21 16:44:42 -06:00
2026-01-31 09:31:17 -06:00
if (world->active_chunk) {
if (world->active_chunk->type == PXL8_WORLD_CHUNK_BSP && world->active_chunk->bsp) {
pxl8_3d_set_bsp(gfx, world->active_chunk->bsp);
pxl8_bsp_render(gfx, world->active_chunk->bsp,
world->bsp_render_state, camera_pos);
}
} else {
pxl8_3d_set_bsp(gfx, NULL);
i32 cx = (i32)floorf(camera_pos.x / WORLD_CHUNK_SIZE);
i32 cy = (i32)floorf(camera_pos.y / WORLD_CHUNK_SIZE);
i32 cz = (i32)floorf(camera_pos.z / WORLD_CHUNK_SIZE);
const pxl8_frustum* frustum = pxl8_3d_get_frustum(gfx);
pxl8_gfx_material mat = {
.texture_id = 0,
.dynamic_lighting = true,
.alpha = 255,
};
i32 r = world->render_distance;
for (i32 dy = -r; dy <= r; dy++) {
for (i32 dz = -r; dz <= r; dz++) {
for (i32 dx = -r; dx <= r; dx++) {
i32 chunk_cx = cx + dx;
i32 chunk_cy = cy + dy;
i32 chunk_cz = cz + dz;
f32 chunk_x = chunk_cx * WORLD_CHUNK_SIZE;
f32 chunk_y = chunk_cy * WORLD_CHUNK_SIZE;
f32 chunk_z = chunk_cz * WORLD_CHUNK_SIZE;
if (frustum) {
pxl8_vec3 aabb_min = {chunk_x, chunk_y, chunk_z};
pxl8_vec3 aabb_max = {chunk_x + WORLD_CHUNK_SIZE, chunk_y + WORLD_CHUNK_SIZE, chunk_z + WORLD_CHUNK_SIZE};
if (!pxl8_frustum_test_aabb(frustum, aabb_min, aabb_max)) continue;
}
pxl8_vxl_mesh_config config = PXL8_VXL_MESH_CONFIG_DEFAULT;
config.chunk_x = chunk_cx;
config.chunk_y = chunk_cy;
config.chunk_z = chunk_cz;
pxl8_mesh* mesh = pxl8_world_chunk_cache_get_mesh(
world->chunk_cache,
chunk_cx, chunk_cy, chunk_cz,
world->block_registry, &config);
if (mesh && mesh->index_count > 0) {
pxl8_mat4 scale = pxl8_mat4_scale(VOXEL_SCALE, VOXEL_SCALE, VOXEL_SCALE);
pxl8_mat4 translate = pxl8_mat4_translate(chunk_x, chunk_y, chunk_z);
pxl8_mat4 model = pxl8_mat4_multiply(translate, scale);
pxl8_3d_draw_mesh(gfx, mesh, &model, &mat);
}
}
}
}
2025-11-21 16:44:42 -06:00
}
2026-01-25 09:26:30 -06:00
}
2025-11-21 16:44:42 -06:00
2026-01-25 09:26:30 -06:00
void pxl8_world_sync(pxl8_world* world, pxl8_net* net) {
if (!world || !net) return;
2025-11-21 16:44:42 -06:00
2026-01-25 09:26:30 -06:00
u8 chunk_type = pxl8_net_chunk_type(net);
u32 chunk_id = pxl8_net_chunk_id(net);
2025-11-21 16:44:42 -06:00
2026-01-25 09:26:30 -06:00
if (chunk_type == PXL8_CHUNK_TYPE_BSP && chunk_id != 0) {
2026-01-31 09:31:17 -06:00
pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_bsp(world->chunk_cache, chunk_id);
2026-01-25 09:26:30 -06:00
if (chunk && chunk->bsp) {
if (world->active_chunk != chunk) {
world->active_chunk = chunk;
pxl8_debug("[CLIENT] Synced BSP chunk id=%u as active (verts=%u faces=%u)",
chunk_id, chunk->bsp->num_vertices, chunk->bsp->num_faces);
2026-02-02 17:48:25 -06:00
if (world->bsp_render_state) {
pxl8_bsp_render_state_destroy(world->bsp_render_state);
world->bsp_render_state = NULL;
}
world->bsp_render_state = pxl8_bsp_render_state_create(chunk->bsp->num_faces);
2025-11-21 16:44:42 -06:00
}
}
2026-01-31 09:31:17 -06:00
} else if (chunk_id == 0 && world->active_chunk != NULL) {
world->active_chunk = NULL;
2026-02-02 17:48:25 -06:00
if (world->bsp_render_state) {
pxl8_bsp_render_state_destroy(world->bsp_render_state);
world->bsp_render_state = NULL;
}
2025-11-21 16:44:42 -06:00
}
2025-10-07 10:32:48 -05:00
}
2026-01-31 09:31:17 -06:00
static void ensure_bsp_render_state(pxl8_world* world) {
if (!world || world->bsp_render_state) return;
2026-02-02 17:48:25 -06:00
if (!world->active_chunk) return;
if (world->active_chunk->type != PXL8_WORLD_CHUNK_BSP) return;
2026-01-31 09:31:17 -06:00
if (!world->active_chunk->bsp) return;
world->bsp_render_state = pxl8_bsp_render_state_create(world->active_chunk->bsp->num_faces);
}
void pxl8_world_set_bsp_material(pxl8_world* world, u16 material_id, const pxl8_gfx_material* material) {
if (!world || !material) return;
ensure_bsp_render_state(world);
if (!world->bsp_render_state) return;
pxl8_bsp_set_material(world->bsp_render_state, material_id, material);
}
i32 pxl8_world_get_render_distance(const pxl8_world* world) {
if (!world) return 3;
return world->render_distance;
}
void pxl8_world_set_render_distance(pxl8_world* world, i32 distance) {
if (!world) return;
if (distance < 1) distance = 1;
if (distance > 8) distance = 8;
world->render_distance = distance;
}
i32 pxl8_world_get_sim_distance(const pxl8_world* world) {
if (!world) return 4;
return world->sim_distance;
}
void pxl8_world_set_sim_distance(pxl8_world* world, i32 distance) {
if (!world) return;
if (distance < 1) distance = 1;
if (distance > 8) distance = 8;
world->sim_distance = distance;
}
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;
#ifdef PXL8_ASYNC_THREADS
world->render_state[0] = world->local_player;
world->render_state[1] = world->local_player;
#endif
}
pxl8_sim_entity* pxl8_world_local_player(pxl8_world* world) {
if (!world) return NULL;
2026-02-02 17:48:25 -06:00
#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
2026-01-31 09:31:17 -06:00
if (!(world->local_player.flags & PXL8_SIM_FLAG_ALIVE)) return NULL;
return &world->local_player;
2026-02-02 17:48:25 -06:00
#endif
2026-01-31 09:31:17 -06:00
}
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 = make_sim_world(world, world->local_player.pos);
pxl8_sim_move_player(&world->local_player, input, &sim, 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;
2026-02-02 17:48:25 -06:00
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;
2026-01-31 09:31:17 -06:00
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 = make_sim_world(world, world->local_player.pos);
pxl8_sim_move_player(&world->local_player, input, &sim, 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) {
2026-02-02 17:48:25 -06:00
bool alive = (world->local_player.flags & PXL8_SIM_FLAG_ALIVE) != 0;
2026-01-31 09:31:17 -06:00
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);
}
2026-02-02 17:48:25 -06:00
if (!alive) {
return;
}
2026-01-31 09:31:17 -06:00
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;
2026-02-02 17:48:25 -06:00
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;
2026-01-31 09:31:17 -06:00
pxl8_sim_world sim = make_sim_world(world, world->local_player.pos);
pxl8_sim_move_player(&world->local_player, &merged, &sim, 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;
2026-02-02 17:48:25 -06:00
world->pointer_motion.yaw -= input->look_dx * 0.008f;
f32 pitch = world->pointer_motion.pitch - input->look_dy * 0.008f;
if (pitch > PXL8_SIM_MAX_PITCH) pitch = PXL8_SIM_MAX_PITCH;
if (pitch < -PXL8_SIM_MAX_PITCH) pitch = -PXL8_SIM_MAX_PITCH;
world->pointer_motion.pitch = pitch;
2026-01-31 09:31:17 -06:00
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