#include "pxl8_world.h" #include #include "pxl8_hal.h" #ifdef PXL8_ASYNC_THREADS #include #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_sim.h" #include "pxl8_vxl.h" #include "pxl8_vxl_render.h" #define PXL8_WORLD_ENTITY_CAPACITY 256 #define VOXEL_SCALE 16.0f #define VOXEL_CHUNK_SIZE 32.0f #define WORLD_CHUNK_SIZE (VOXEL_CHUNK_SIZE * VOXEL_SCALE) struct pxl8_world { pxl8_world_chunk* active_chunk; pxl8_vxl_block_registry* block_registry; pxl8_world_chunk_cache* chunk_cache; pxl8_entity_pool* entities; 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; pxl8_vec2 pointer_motion; #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->block_registry = pxl8_vxl_block_registry_create(); world->chunk_cache = pxl8_world_chunk_cache_create(); world->entities = pxl8_entity_pool_create(PXL8_WORLD_ENTITY_CAPACITY); world->vxl_render_state = pxl8_vxl_render_state_create(); world->render_distance = 3; world->sim_distance = 4; if (!world->block_registry || !world->chunk_cache || !world->entities || !world->vxl_render_state) { pxl8_world_destroy(world); return NULL; } return world; } void pxl8_world_destroy(pxl8_world* world) { if (!world) return; pxl8_vxl_block_registry_destroy(world->block_registry); pxl8_world_chunk_cache_destroy(world->chunk_cache); pxl8_entity_pool_destroy(world->entities); pxl8_bsp_render_state_destroy(world->bsp_render_state); pxl8_vxl_render_state_destroy(world->vxl_render_state); 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; } void pxl8_world_set_active_chunk(pxl8_world* world, pxl8_world_chunk* chunk) { if (!world) return; world->active_chunk = chunk; } pxl8_vxl_block_registry* pxl8_world_block_registry(pxl8_world* world) { if (!world) return NULL; return world->block_registry; } pxl8_entity_pool* pxl8_world_entities(pxl8_world* world) { if (!world) return NULL; return world->entities; } pxl8_entity pxl8_world_spawn(pxl8_world* world) { if (!world || !world->entities) return PXL8_ENTITY_INVALID; return pxl8_entity_spawn(world->entities); } 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); } 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, const pxl8_input_state* input, f32 dt) { if (!world) return; 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; msg.tick = world->client_tick; msg.timestamp = pxl8_get_ticks_ns(); if (world->net) { pxl8_net_send_input(world->net, &msg); } pxl8_sim_world sim = make_sim_world(world, world->local_player.pos); pxl8_sim_move_player(&world->local_player, &msg, &sim, dt); world->client_tick++; } } void pxl8_world_render(pxl8_world* world, pxl8_gfx* gfx, pxl8_vec3 camera_pos) { if (!world || !gfx) return; 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); } } } } } } void pxl8_world_sync(pxl8_world* world, pxl8_net* net) { if (!world || !net) return; u8 chunk_type = pxl8_net_chunk_type(net); u32 chunk_id = pxl8_net_chunk_id(net); if (chunk_type == PXL8_CHUNK_TYPE_BSP && chunk_id != 0) { pxl8_world_chunk* chunk = pxl8_world_chunk_cache_get_bsp(world->chunk_cache, chunk_id); 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); 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); } } } else if (chunk_id == 0 && world->active_chunk != NULL) { world->active_chunk = NULL; if (world->bsp_render_state) { pxl8_bsp_render_state_destroy(world->bsp_render_state); world->bsp_render_state = NULL; } } } static void ensure_bsp_render_state(pxl8_world* world) { if (!world || world->bsp_render_state) return; if (!world->active_chunk) return; if (world->active_chunk->type != PXL8_WORLD_CHUNK_BSP) return; 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; #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 = 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; 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 = 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) { 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 = 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; 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; 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