SharedUserState shared_user_state = ZI; //////////////////////////////// //~ Startup void StartupUser(void) { __prof; SharedUserState *g = &shared_user_state; SetGstat(GSTAT_DEBUG_STEPS, U64Max); g->arena = AcquireArena(Gibi(64)); g->real_time_ns = TimeNs(); /* TODO: Remove this */ String connect_address_str = Lit(""); g->connect_address_str = PushString(g->arena, connect_address_str); /* Initialize average dt to a reasonable value */ g->average_local_to_user_snapshot_publish_dt_ns = NsFromSeconds(1) / SIM_TICKS_PER_SECOND; /* User blend clients */ g->user_client_store = AcquireClientStore(); g->user_unblended_client = AcquireClient(g->user_client_store); g->user_blended_client = AcquireClient(g->user_client_store); g->ss_blended = NilSnapshot(); /* Renderer data arenas */ g->material_instances_arena = AcquireArena(Gibi(64)); g->ui_rect_instances_arena = AcquireArena(Gibi(64)); g->ui_shape_verts_arena = AcquireArena(Gibi(64)); g->ui_shape_indices_arena = AcquireArena(Gibi(64)); g->grids_arena = AcquireArena(Gibi(64)); /* Local to user client */ g->local_to_user_client_store = AcquireClientStore(); g->local_to_user_client = AcquireClient(g->local_to_user_client_store); g->world_to_ui_xf = XformIdentity; g->world_to_render_xf = XformIdentity; g->console_logs_arena = AcquireArena(Gibi(64)); //P_RegisterLogCallback(ConsoleLogCallback, P_LogLevel_Success); P_RegisterLogCallback(ConsoleLogCallback, P_LogLevel_Debug); g->window = P_AcquireWindow(); g->swapchain = GPU_AcquireSwapchain(g->window, VEC2I32(100, 100)); P_ShowWindow(g->window); /* Start jobs */ RunJob(UpdateUserOrSleep, .pool = JobPool_User, .counter = &g->shutdown_job_counter); RunJob(UpdateSim, .pool = JobPool_Sim, .counter = &g->shutdown_job_counter); OnExit(&ShutdownUser); } //////////////////////////////// //~ Shutdown ExitFuncDef(ShutdownUser) { __prof; SharedUserState *g = &shared_user_state; Atomic32Set(&g->shutdown, 1); YieldOnJobs(&g->shutdown_job_counter); P_ReleaseWindow(g->window); } //////////////////////////////// //~ Debug draw //- Draw xform void DrawDebugXform(Xform xf, u32 color_x, u32 color_y) { SharedUserState *g = &shared_user_state; f32 thickness = 2.f; f32 arrowhead_len = 15.f; Vec2 pos = MulXformV2(g->world_to_ui_xf, xf.og); Vec2 x_ray = MulXformBasisV2(g->world_to_ui_xf, RightFromXform(xf)); Vec2 y_ray = MulXformBasisV2(g->world_to_ui_xf, UpFromXform(xf)); f32 ray_scale = 1; x_ray = MulVec2(x_ray, ray_scale); y_ray = MulVec2(y_ray, ray_scale); /* FIXME: Enable this */ #if 0 D_DrawArrowRay(g->render_sig, pos, x_ray, thickness, arrowhead_len, color_x); D_DrawArrowRay(g->render_sig, pos, y_ray, thickness, arrowhead_len, color_y); #else LAX x_ray; LAX y_ray; LAX thickness; LAX arrowhead_len; LAX pos; LAX color_x; LAX color_y; #endif //u32 color_quad = Rgba32F(0, 1, 1, 0.3); //Quad quad = QuadFromRect(RectFromScalar(0, 0, 1, -1)); //quad = MulXformQuad(xf, ScaleQuad(quad, 0.075f)); //D_DrawQuad(g->render_sig, quad, color); } //- Draw movement void DrawDebugMovement(Entity *ent) { SharedUserState *g = &shared_user_state; f32 thickness = 2.f; f32 arrow_len = 15.f; u32 color_vel = ColorOrange; Xform xf = XformFromEntity(ent); Vec2 velocity = ent->linear_velocity; Vec2 pos = MulXformV2(g->world_to_ui_xf, xf.og); Vec2 vel_ray = MulXformBasisV2(g->world_to_ui_xf, velocity); if (Vec2Len(vel_ray) > 0.00001) { /* FIXME: Enable this */ #if 0 D_DrawArrowRay(g->render_sig, pos, vel_ray, thickness, arrow_len, color_vel); #else LAX thickness; LAX arrow_len; LAX color_vel; LAX xf; LAX velocity; LAX pos; LAX vel_ray; #endif } } //- Entity debug string String DebugStringFromEntity(Arena *arena, Entity *ent) { TempArena scratch = BeginScratch(arena); Snapshot *ss = ent->ss; const u8 hex[] = "0123456789abcdef"; String result = ZI; result.text = PushDry(arena, u8); //result.len += StringF(arena, "[%F]", FmtUid(ent->id.uid)).len; { b32 transmitting = HasProp(ent, Prop_SyncSrc); b32 receiving = HasProp(ent, Prop_SyncDst); if (transmitting & receiving) { result.len += StringF(arena, " networked (sending & receiving)").len; } else if (transmitting) { result.len += StringF(arena, " networked (sending)").len; } else if (receiving) { result.len += StringF(arena, " networked (receiving)").len; } else { result.len += StringF(arena, " local").len; } } result.len += StringF(arena, "\n").len; result.len += StringF(arena, "owner: [%F]\n", FmtUid(ent->owner.uid)).len; result.len += StringF(arena, "\n").len; { result.len += StringF(arena, "props: 0x").len; for (u64 chunk_index = countof(ent->props); chunk_index-- > 0;) { u64 chunk = ent->props[chunk_index]; for (u64 part_index = 8; part_index-- > 0;) { if ((chunk_index != (countof(ent->props) - 1)) || ((chunk_index * 64) + (part_index * 8)) <= Prop_Count) { u8 part = (chunk >> (part_index * 8)) & 0xFF; StringFromChar(arena, hex[(part >> 4) & 0x0F]); StringFromChar(arena, hex[(part >> 0) & 0x0F]); result.len += 2; } } } result.len += StringF(arena, "\n").len; } if (!EqId(ent->parent, RootEntityId)) { result.len += StringF(arena, "parent: [%F]\n", FmtUid(ent->parent.uid)).len; } if (!IsNilId(ent->next) || !IsNilId(ent->prev)) { result.len += StringF(arena, "prev: [%F]\n", FmtUid(ent->prev.uid)).len; result.len += StringF(arena, "next: [%F]\n", FmtUid(ent->next.uid)).len; } result.len += StringF(arena, "\n").len; /* Pos */ Xform xf = XformFromEntity(ent); Vec2 linear_velocity = ent->linear_velocity; f32 angular_velocity = ent->angular_velocity; result.len += StringF(arena, "pos: (%F, %F)\n", FmtFloat(xf.og.x), FmtFloat(xf.og.y)).len; result.len += StringF(arena, "linear velocity: (%F, %F)\n", FmtFloat(linear_velocity.x), FmtFloat(linear_velocity.y)).len; result.len += StringF(arena, "angular velocity: %F\n", FmtFloat(angular_velocity)).len; /* Test */ result.len += StringF(arena, "collision dir: (%F, %F)\n", FmtFloat(ent->collision_dir.x), FmtFloat(ent->collision_dir.y)).len; /* Children */ if (!IsNilId(ent->first) || !IsNilId(ent->last)) { Entity *child = EntityFromId(ss, ent->first); if (!EqId(ent->first, ent->last) || !child->valid) { result.len += StringF(arena, "first child: [%F]\n", FmtUid(ent->first.uid)).len; result.len += StringF(arena, "last child: [%F]\n", FmtUid(ent->last.uid)).len; } while (child->valid) { result.len += StringF(arena, "\n---------------------------------\n").len; result.len += StringF(arena, "CHILD\n").len; String child_text = DebugStringFromEntity(scratch.arena, child); result.len += IndentString(arena, child_text, 4).len; child = EntityFromId(ss, child->next); } } EndScratch(scratch); return result; } //////////////////////////////// //~ Console //- Console log callback P_LogEventCallbackFuncDef(ConsoleLogCallback, log) { __prof; SharedUserState *g = &shared_user_state; Lock lock = LockE(&g->console_logs_mutex); { ConsoleLog *clog = PushStruct(g->console_logs_arena, ConsoleLog); clog->level = log.level; clog->msg = PushString(g->console_logs_arena, log.msg); clog->datetime = log.datetime; clog->time_ns = log.time_ns; if (g->last_console_log) { g->last_console_log->next = clog; clog->prev = g->last_console_log; /* Alternating color index between logs of same level */ i32 *color_index = &g->console_log_color_indices[log.level]; clog->color_index = *color_index; *color_index = 1 - *color_index; } else { g->first_console_log = clog; } g->last_console_log = clog; } Unlock(&lock); } //- Draw console void DrawDebugConsole(i32 level, b32 minimized) { /* FIXME: Enable this */ #if 0 __prof; SharedUserState *g = &shared_user_state; TempArena scratch = BeginScratchNoConflict(); Vec2 desired_start_pos = VEC2(10, minimized ? 100 : 600); i64 fade_time_ns = NsFromSeconds(10); f32 fade_curve = 0.5; f32 spacing = 0; f32 bg_margin = 5; u32 colors[P_LogLevel_Count][2] = ZI; SetBytes(colors, 0xFF, sizeof(colors)); #if 1 colors[P_LogLevel_Debug][0] = Rgb32F(0.4, 0.1, 0.4); colors[P_LogLevel_Debug][1] = Rgb32F(0.5, 0.2, 0.5); colors[P_LogLevel_Info][0] = Rgb32F(0.4, 0.4, 0.4); colors[P_LogLevel_Info][1] = Rgb32F(0.5, 0.5, 0.5); colors[P_LogLevel_Success][0] = Rgb32F(0.1, 0.3, 0.1); colors[P_LogLevel_Success][1] = Rgb32F(0.2, 0.4, 0.2); colors[P_LogLevel_Warning][0] = Rgb32F(0.4, 0.4, 0.1); colors[P_LogLevel_Warning][1] = Rgb32F(0.5, 0.5, 0.2); colors[P_LogLevel_Error][0] = Rgb32F(0.4, 0.1, 0.1); colors[P_LogLevel_Error][1] = Rgb32F(0.5, 0.2, 0.2); #else u32 info_colors[2] = { Rgb32F(0.4, 0.4, 0.4), Rgb32F(0.5, 0.5, 0.5) }; u32 success_colors[2] = { Rgb32F(0.1, 0.3, 0.1), Rgb32F(0.2, 0.4, 0.2) }; u32 warning_colors[2] = { Rgb32F(0.4, 0.4, 0.1), Rgb32F(0.5, 0.5, 0.2) }; u32 error_colors[2] = { Rgb32F(0.4, 0.1, 0.1), Rgb32F(0.5, 0.2, 0.2) }; #endif Vec2 draw_pos = desired_start_pos; f32 bounds_top = F32Infinity; f32 bounds_bottom = -F32Infinity; if (g->console_logs_height < desired_start_pos.y) { draw_pos.y = g->console_logs_height; } g->console_logs_height = 0; i64 now_ns = TimeNs(); F_Font *font = F_LoadFontAsync(Lit("font/fixedsys.ttf"), 12.0f); if (font) { Lock lock = LockE(&g->console_logs_mutex); { for (ConsoleLog *log = g->last_console_log; log; log = log->prev) { f32 opacity = 0.75; if (minimized) { f32 lin = 1.0 - ClampF64((f64)(now_ns - log->time_ns) / (f64)fade_time_ns, 0, 1); opacity *= PowF32(lin, fade_curve); } if (draw_pos.y > -desired_start_pos.y && opacity > 0) { if (log->level <= level) { /* Draw background */ u32 color = colors[log->level][log->color_index]; D_DrawQuad(g->render_sig, QuadFromRect(log->bounds), Alpha32F(color, opacity)); /* Draw text */ String text = log->msg; if (!minimized) { P_DateTime datetime = log->datetime; text = StringF( scratch.arena, "[%F:%F:%F.%F] %F", FmtUintZ(datetime.hour, 2), FmtUintZ(datetime.minute, 2), FmtUintZ(datetime.second, 2), FmtUintZ(datetime.milliseconds, 3), FmtString(text)); } D_TextParams params = D_TEXTPARAMS(.font = font, .pos = draw_pos, .offset_y = DRAW_TEXT_OFFSET_Y_BOTTOM, .color = Alpha32F(ColorWhite, opacity), .str = text); Rect bounds = draw_text(g->render_sig, params); Rect draw_bounds = bounds; draw_bounds.x -= bg_margin; draw_bounds.y -= bg_margin; draw_bounds.width += bg_margin * 2.f; draw_bounds.height += bg_margin * 2.f; draw_pos.y -= draw_bounds.height + spacing; log->bounds = draw_bounds; bounds_top = MinF32(bounds_top, draw_bounds.y); bounds_bottom = MaxF32(bounds_bottom, draw_bounds.y + draw_bounds.height); } } else { break; } } } Unlock(&lock); } if (bounds_top < F32Infinity && bounds_bottom > -F32Infinity) { g->console_logs_height = bounds_bottom - bounds_top; } EndScratch(scratch); #else LAX level; LAX minimized; #endif } //////////////////////////////// //~ Render buffers //- Gbuffer GPU_Resource *AcquireGbuffer(GPU_Format format, Vec2I32 size) { __prof; GPU_ResourceDesc desc = ZI; desc.kind = GPU_ResourceKind_Texture2D; desc.flags = GPU_ResourceFlag_AllowSrv | GPU_ResourceFlag_AllowUav | GPU_ResourceFlag_AllowRtv; desc.texture.format = format; desc.texture.size = VEC3I32(size.x, size.y, 1); desc.texture.mip_levels = 1; return GPU_AcquireResource(desc); } //- Upload buffer GPU_Resource *AcquireUploadBuffer(u32 element_count, u32 element_size, void *src) { __prof; u64 size = element_size * element_count; GPU_ResourceDesc desc = ZI; desc.kind = GPU_ResourceKind_Buffer; desc.flags = GPU_ResourceFlag_None; desc.buffer.heap_kind = GPU_HeapKind_Upload; desc.buffer.element_size = size; desc.buffer.element_count = element_count; desc.buffer.element_size = element_size; GPU_Resource *r = GPU_AcquireResource(desc); { __profn("Copy to transfer buffer"); GPU_Mapped m = GPU_Map(r); GPU_CopyToMapped(&m, STRING(element_size * element_count, src)); GPU_Unmap(&m); } return r; } GPU_Resource *AcquireUploadBufferFromArena(u32 element_count, Arena *arena) { __prof; u64 element_size = element_count > 0 ? arena->pos / element_count : 0; GPU_Resource *r = AcquireUploadBuffer(element_count, element_size, (void *)ArenaBase(arena)); return r; } //////////////////////////////// //~ Sort entities MergesortCompareFuncDef(EntitySortCmp, arg_a, arg_b, _) { Entity *a = *(Entity **)arg_a; Entity *b = *(Entity **)arg_b; i32 result = 0; if (result == 0) { /* Sort by light */ b32 a_cmp = HasProp(a, Prop_LightTest); b32 b_cmp = HasProp(b, Prop_LightTest); result = (a_cmp > b_cmp) - (a_cmp < b_cmp); } if (result == 0) { /* Sort by layer */ i32 a_cmp = a->layer; i32 b_cmp = b->layer; result = (a_cmp < b_cmp) - (a_cmp > b_cmp); } if (result == 0) { /* Sort by sprite */ u64 a_cmp = a->sprite.hash; u64 b_cmp = b->sprite.hash; result = (a_cmp < b_cmp) - (a_cmp > b_cmp); } if (result == 0) { /* Sort by activation */ u64 a_cmp = a->activation_tick; u64 b_cmp = b->activation_tick; result = (a_cmp < b_cmp) - (a_cmp > b_cmp); } return result; } //////////////////////////////// //~ User update void UpdateUser(P_Window *window) { __prof; SharedUserState *g = &shared_user_state; TempArena scratch = BeginScratchNoConflict(); //- Begin frame g->real_dt_ns = TimeNs() - g->real_time_ns; g->real_time_ns += g->real_dt_ns; g->screen_size = P_GetWindowSize(window); //- Pull latest local sim snapshot { __profn("Pull snapshot"); Lock lock = LockE(&g->local_to_user_client_mutex); u64 old_last_tick = g->user_unblended_client->last_tick; u64 last_tick = g->local_to_user_client->last_tick; if (last_tick > old_last_tick) { Snapshot *src = SnapshotFromTick(g->local_to_user_client, last_tick); AcquireSnapshot(g->user_unblended_client, src, src->tick); g->last_local_to_user_snapshot_published_at_ns = g->local_to_user_client_publish_time_ns; g->average_local_to_user_snapshot_publish_dt_ns -= g->average_local_to_user_snapshot_publish_dt_ns / 50; g->average_local_to_user_snapshot_publish_dt_ns += g->local_to_user_client_publish_dt_ns / 50; } Unlock(&lock); } //- Create user world from blended snapshots { __profn("Blend snapshots"); /* How along are we between sim ticks (0 = start of tick, 1 = end of tick) */ f64 tick_progress = 0; i64 next_tick_expected_ns = g->last_local_to_user_snapshot_published_at_ns + g->average_local_to_user_snapshot_publish_dt_ns; if (next_tick_expected_ns > g->last_local_to_user_snapshot_published_at_ns) { tick_progress = (f64)(g->real_time_ns - g->last_local_to_user_snapshot_published_at_ns) / (f64)(next_tick_expected_ns - g->last_local_to_user_snapshot_published_at_ns); } /* Predict local sim time based on average snapshot publish dt. */ Snapshot *newest_snapshot = SnapshotFromTick(g->user_unblended_client, g->user_unblended_client->last_tick); g->local_sim_last_known_time_ns = newest_snapshot->sim_time_ns; g->local_sim_last_known_tick = newest_snapshot->tick; if (Atomic32Fetch(&g->user_paused)) { g->local_sim_predicted_time_ns = g->local_sim_last_known_tick; } else { g->local_sim_predicted_time_ns = newest_snapshot->sim_time_ns + (newest_snapshot->sim_dt_ns * tick_progress); } if (USER_INTERP_ENABLED && !Atomic32Fetch(&g->user_paused)) { /* Determine render time */ g->render_time_target_ns = g->local_sim_predicted_time_ns - (USER_INTERP_RATIO * g->average_local_to_user_snapshot_publish_dt_ns); if (g->average_local_to_user_snapshot_publish_dt_ns > 0) { /* Increment render time based on average publish dt */ f64 sim_publish_timescale = (f64)newest_snapshot->sim_dt_ns / (f64)g->average_local_to_user_snapshot_publish_dt_ns; g->render_time_ns += g->real_dt_ns * sim_publish_timescale; } i64 render_time_target_diff_ns = g->render_time_target_ns - g->render_time_ns; if (render_time_target_diff_ns > NsFromSeconds(0.010) || render_time_target_diff_ns < NsFromSeconds(-0.005)) { /* Snap render time if it gets too out of sync with target render time */ g->render_time_ns = g->render_time_target_ns; } /* Get two snapshots nearest to render time */ Snapshot *left_snapshot = NilSnapshot(); Snapshot *right_snapshot = newest_snapshot; { Snapshot *ss = SnapshotFromTick(g->user_unblended_client, g->user_unblended_client->first_tick); while (ss->valid) { u64 next_tick = ss->next_tick; i64 ss_time_ns = ss->sim_time_ns; if (ss_time_ns < g->render_time_ns && ss_time_ns > left_snapshot->sim_time_ns) { left_snapshot = ss; } if (ss_time_ns > g->render_time_ns && ss_time_ns < right_snapshot->sim_time_ns) { right_snapshot = ss; } ss = SnapshotFromTick(g->user_unblended_client, next_tick); } } /* Create world from blended snapshots */ if (left_snapshot->valid && right_snapshot->valid) { f64 blend = (f64)(g->render_time_ns - left_snapshot->sim_time_ns) / (f64)(right_snapshot->sim_time_ns - left_snapshot->sim_time_ns); g->ss_blended = AcquireSnapshotFromLerp(g->user_blended_client, left_snapshot, right_snapshot, blend); } else if (left_snapshot->valid) { g->ss_blended = AcquireSnapshot(g->user_blended_client, left_snapshot, left_snapshot->tick); } else if (right_snapshot->valid) { g->ss_blended = AcquireSnapshot(g->user_blended_client, right_snapshot, right_snapshot->tick); } /* Release unneeded unblended snapshots */ if (left_snapshot->tick > 0) { ReleaseSnapshotsInRange(g->user_unblended_client, 0, left_snapshot->tick - 1); } } else { /* Interp disabled, just copy latest snapshot */ g->render_time_target_ns = newest_snapshot->sim_time_ns; g->render_time_ns = newest_snapshot->sim_time_ns; g->ss_blended = AcquireSnapshot(g->user_blended_client, newest_snapshot, newest_snapshot->tick); /* Release unneeded unblended snapshots */ if (newest_snapshot->tick > 0) { ReleaseSnapshotsInRange(g->user_unblended_client, 0, newest_snapshot->tick - 1); } } /* Release unneeded blended snapshots */ if (g->ss_blended->tick > 0) { ReleaseSnapshotsInRange(g->user_blended_client, 0, g->ss_blended->tick - 1); ReleaseSnapshotsInRange(g->user_blended_client, g->ss_blended->tick + 1, U64Max); } } //- Process sys events into user bind state { __profn("Process sys events"); P_WindowEventArray events = P_PopWindowEvents(scratch.arena, window); /* Reset bind pressed / released states */ for (u32 i = 0; i < countof(g->bind_states); ++i) { g->bind_states[i] = (BindState) { .is_held = g->bind_states[i].is_held }; } for (u64 ent_index = 0; ent_index < events.count; ++ent_index) { P_WindowEvent *event = &events.events[ent_index]; if (event->kind == P_WindowEventKind_Quit) { SignalExit(0); } if (event->kind == P_WindowEventKind_ButtonUp) { /* Escape quit */ if (event->button == P_Btn_ESC) { SignalExit(0); } } /* Update mouse pos */ if (event->kind == P_WindowEventKind_CursorMove) { g->screen_cursor = event->cursor_position; } /* Update bind states */ if ((event->kind == P_WindowEventKind_ButtonDown || event->kind == P_WindowEventKind_ButtonUp)) { P_Btn button = event->button; button = button >= P_Btn_Count ? P_Btn_None : button; BindKind bind = g_binds[button]; if (bind) { b32 pressed = event->kind == P_WindowEventKind_ButtonDown; #if 0 b32 out_of_bounds = button >= P_Btn_M1 && button <= P_Btn_M5 && (g->ui_cursor.x < 0 || g->ui_cursor.y < 0 || g->ui_cursor.x > g->ui_size.x || g->ui_cursor.y > g->ui_size.y); #else b32 out_of_bounds = 0; #endif g->bind_states[bind].is_held = pressed && !out_of_bounds; if (pressed) { if (!out_of_bounds) { ++g->bind_states[bind].num_presses_and_repeats; if (event->is_repeat) { ++g->bind_states[bind].num_repeats; } else { ++g->bind_states[bind].num_presses; } } } else { ++g->bind_states[bind].num_releases; } } } } } //- Find local entities Entity *local_player = EntityFromId(g->ss_blended, g->ss_blended->local_player); Entity *local_control = EntityFromId(g->ss_blended, local_player->player_control_ent); Entity *local_camera = EntityFromId(g->ss_blended, local_player->player_camera_ent); //- Find hovered entity Entity *hovered_ent = NilEntity(); { Xform mouse_xf = XformFromPos(g->world_cursor); CLD_Shape mouse_shape = ZI; mouse_shape.points[0] = VEC2(0, 0); mouse_shape.count = 1; mouse_shape.radius = 0.01f; for (u64 ent_index = 0; ent_index < g->ss_blended->num_ents_reserved; ++ent_index) { Entity *ent = &g->ss_blended->ents[ent_index]; if (!IsValidAndActive(ent)) continue; CLD_Shape ent_collider = ent->local_collider; if (ent_collider.count > 0) { /* TODO: Can just use boolean GJK */ Xform ent_xf = XformFromEntity(ent); CLD_CollisionData collision_result = CLD_CollisionDataFromShapes(&ent_collider, &mouse_shape, ent_xf, mouse_xf); if (collision_result.num_points > 0) { hovered_ent = EntityFromId(g->ss_blended, ent->top); break; } } } } //- Update user state from binds /* Test fullscreen */ { if (g->bind_states[BindKind_Fullscreen].num_presses && g->bind_states[BindKind_FullscreenMod].is_held) { P_WindowSettings settings = P_GetWindowSettings(window); settings.flags ^= P_WindowSettingsFlag_Fullscreen; P_UpdateWindowSettings(window, &settings); } } if (g->bind_states[BindKind_DebugDraw].num_presses > 0) { g->debug_draw = !g->debug_draw; } if (g->bind_states[BindKind_DebugToggleTopmost].num_presses > 0) { P_ToggleWindowTopmost(window); P_LogSuccessF("Toggle topmost"); } if (g->bind_states[BindKind_DebugConsole].num_presses > 0) { g->debug_console = !g->debug_console; } if (g->bind_states[BindKind_DebugCamera].num_presses > 0) { g->debug_camera = !g->debug_camera; } { if (g->bind_states[BindKind_DebugFollow].num_presses > 0) { if (IsNilId(g->debug_following)) { g->debug_following = hovered_ent->id; } else { g->debug_following = NilEntityId; } } if (!IsNilId(g->debug_following)) { Entity *follow_ent = EntityFromId(g->ss_blended, g->debug_following); Entity *follow_camera = NilEntity(); for (u64 i = 0; i < g->ss_blended->num_ents_reserved; ++i) { Entity *ent = &g->ss_blended->ents[i]; Entity *ent_camera_follow = EntityFromId(g->ss_blended, ent->camera_follow); if (ent_camera_follow->valid && ent_camera_follow == follow_ent) { follow_camera = ent; break; } } if (follow_camera->valid) { local_camera = follow_camera; } else { g->debug_following = NilEntityId; } } } //- Apply shake for (u64 ent_index = 0; ent_index < g->ss_blended->num_ents_reserved; ++ent_index) { Entity *ent = &g->ss_blended->ents[ent_index]; if (!IsValidAndActive(ent)) continue; /* How much time between camera shakes */ i64 frequency_ns = NsFromSeconds(0.01f); f32 shake = ent->shake; if (shake > 0) { u64 angle_seed0 = ent->id.uid.lo + (u64)(g->ss_blended->sim_time_ns / frequency_ns); u64 angle_seed1 = angle_seed0 + 1; f32 angle0 = RandF64FromSeed(angle_seed0, 0, Tau); f32 angle1 = RandF64FromSeed(angle_seed1, 0, Tau); Vec2 vec0 = Vec2WithLen(Vec2FromAngle(angle0), shake); /* NOTE: vec1 not completely accurate since shake can change between frames, it's just a prediction */ Vec2 vec1 = Vec2WithLen(Vec2FromAngle(angle1), shake); /* TODO: Cubic interp? */ f32 blend = (f32)(g->ss_blended->sim_time_ns % frequency_ns) / (f32)frequency_ns; Vec2 vec = LerpVec2(vec0, vec1, blend); Xform xf = XformFromEntity(ent); xf.og = AddVec2(xf.og, MulVec2(vec, shake)); SetXform(ent, xf); } } //- Update ui to screen xform from screen size if (g->debug_camera) { g->ui_size = g->screen_size; g->ui_to_screen_xf = XformIdentity; g->ui_to_screen_xf.og = RoundVec2(g->ui_to_screen_xf.og); } else { /* Determine ui size by camera & window dimensions */ f32 aspect_ratio = (f32)(DEFAULT_CAMERA_WIDTH / DEFAULT_CAMERA_HEIGHT); if (local_camera->valid) { Xform quad_xf = MulXform(XformFromEntity(local_camera), local_camera->camera_quad_xform); Vec2 camera_size = ScaleFromXform(quad_xf); if (!IsVec2Zero(camera_size)) { aspect_ratio = camera_size.x / camera_size.y; } } f32 width = g->screen_size.x; f32 height = g->screen_size.y; if (width / height > aspect_ratio) { width = height * aspect_ratio; } else { height = CeilF32(width / aspect_ratio); } g->ui_size = VEC2I32(width, height); /* Center ui in window */ f32 x = RoundF32(g->screen_size.x / 2 - width / 2); f32 y = RoundF32(g->screen_size.y / 2 - height / 2); g->ui_to_screen_xf = XformFromTrs(TRS(.t = VEC2(x, y))); g->ui_to_screen_xf.og = RoundVec2(g->ui_to_screen_xf.og); } g->ui_cursor = MulXformV2(InvertXform(g->ui_to_screen_xf), g->screen_cursor); //- Update world to ui xform from camera if (g->debug_camera) { g->world_to_ui_xf = XformWIthWorldRotation(g->world_to_ui_xf, 0); Vec2 world_cursor = InvertXformMulV2(g->world_to_ui_xf, g->ui_cursor); /* Pan view */ if (g->bind_states[BindKind_Pan].is_held) { if (!g->debug_camera_panning) { g->debug_camera_pan_start = world_cursor; g->debug_camera_panning = 1; } Vec2 offset = NegVec2(SubVec2(g->debug_camera_pan_start, world_cursor)); g->world_to_ui_xf = TranslateXform(g->world_to_ui_xf, offset); world_cursor = InvertXformMulV2(g->world_to_ui_xf, g->ui_cursor); g->debug_camera_pan_start = world_cursor; } else { g->debug_camera_panning = 0; } /* Zoom view */ i32 input_zooms = g->bind_states[BindKind_ZoomIn].num_presses - g->bind_states[BindKind_ZoomOut].num_presses; if (input_zooms != 0) { /* Zoom to cursor */ f32 zoom_rate = 2; f32 zoom = PowF32(zoom_rate, input_zooms); g->world_to_ui_xf = TranslateXform(g->world_to_ui_xf, world_cursor); g->world_to_ui_xf = ScaleXform(g->world_to_ui_xf, VEC2(zoom, zoom)); g->world_to_ui_xf = TranslateXform(g->world_to_ui_xf, NegVec2(world_cursor)); } g->world_to_ui_xf.og = RoundVec2(g->world_to_ui_xf.og); } else { Xform xf = XformFromEntity(local_camera); Vec2 world_center = xf.og; f32 rot = RotationFromXform(xf); /* Scale view into viewport based on camera size */ Vec2 scale = VEC2(g->ui_size.x, g->ui_size.y); { Xform quad_xf = MulXform(xf, local_camera->camera_quad_xform); Vec2 camera_size = ScaleFromXform(quad_xf); if (!IsVec2Zero(camera_size)) { scale = DivVec2Vec2(scale, camera_size); } } scale.x = MinF32(scale.x, scale.y); scale.y = scale.x; Vec2 ui_center = MulVec2(VEC2(g->ui_size.x, g->ui_size.y), 0.5); Trs trs = TRS(.t = SubVec2(ui_center, world_center), .r = rot, .s = scale); Vec2 pivot = world_center; g->world_to_ui_xf = XformIdentity; g->world_to_ui_xf = TranslateXform(g->world_to_ui_xf, pivot); g->world_to_ui_xf = TranslateXform(g->world_to_ui_xf, trs.t); g->world_to_ui_xf = RotateXform(g->world_to_ui_xf, trs.r); g->world_to_ui_xf = ScaleXform(g->world_to_ui_xf, trs.s); g->world_to_ui_xf = TranslateXform(g->world_to_ui_xf, NegVec2(pivot)); g->world_to_ui_xf.og = RoundVec2(g->world_to_ui_xf.og); } g->world_cursor = InvertXformMulV2(g->world_to_ui_xf, g->ui_cursor); //- Update world to render xform from world to ui xform b32 effects_disabled = 0; g->render_size = RoundVec2ToVec2I32(VEC2(RENDER_WIDTH, RENDER_HEIGHT)); if (g->debug_camera) { g->render_size = g->ui_size; effects_disabled = 1; g->world_to_render_xf = g->world_to_ui_xf; } else { Xform ui_to_world_xf = InvertXform(g->world_to_ui_xf); Vec2 world_center = MulXformV2(ui_to_world_xf, MulVec2(VEC2(g->ui_size.x, g->ui_size.y), 0.5)); Vec2 scale = VEC2(PIXELS_PER_UNIT, PIXELS_PER_UNIT); Xform xf = XformIdentity; xf = TranslateXform(xf, MulVec2(VEC2(g->render_size.x, g->render_size.y), 0.5)); xf = ScaleXform(xf, scale); xf = TranslateXform(xf, MulVec2(world_center, -1)); xf.og = RoundVec2(xf.og); g->world_to_render_xf = xf; } //- Update render to ui xform { Xform world_to_ui_xf = g->world_to_ui_xf; Xform world_to_render_xf = g->world_to_render_xf; Xform render_to_world_xf = InvertXform(world_to_render_xf); Xform render_to_ui_xf = MulXform(world_to_ui_xf, render_to_world_xf); g->render_to_ui_xf = render_to_ui_xf; } //- Update listener from view { Vec2 up = VEC2(0, -1); Vec2 ui_center = MulVec2(VEC2(g->ui_size.x, g->ui_size.y), 0.5f); Vec2 listener_pos = InvertXformMulV2(g->world_to_ui_xf, ui_center); Vec2 listener_dir = NormVec2(InvertXformBasisMulV2(g->world_to_ui_xf, up)); MIX_UpdateListener(listener_pos, listener_dir); } //- Draw grid /* FIXME: Enable this */ #if 0 { f32 thickness = 2; Vec2 offset = NegVec2(MulXformV2(g->world_to_render_xf, VEC2(0, 0))); f32 spacing = ScaleFromXform(g->world_to_render_xf).x; Vec2 pos = InvertXformMulV2(g->world_to_render_xf, VEC2(0, 0)); Vec2 size = InvertXformBasisMulV2(g->world_to_render_xf, VEC2(g->render_size.x, g->render_size.y)); u32 color0 = Rgba32F(0.17f, 0.17f, 0.17f, 1.f); u32 color1 = Rgba32F(0.15f, 0.15f, 0.15f, 1.f); D_DrawGrid(g->render_sig, XformFromRect(RectFromVec2(pos, size)), color0, color1, Rgba32(0x3f, 0x3f, 0x3f, 0xFF), ColorRed, ColorGreen, thickness, spacing, offset); } #else #endif #if 0 //- Acquire / release tile cache entries /* Acquire entries from new sim chunks */ for (u64 ent_index = 0; ent_index < g->ss_blended->num_ents_reserved; ++ent_index) { Entity *chunk_ent = &g->ss_blended->ents[ent_index]; if (IsValidAndActive(chunk_ent) && HasProp(chunk_ent, Prop_TileChunk)) { struct user_tile_cache_entry *entry = user_tile_cache_entry_from_chunk_pos(chunk_ent->tile_chunk_pos); if (!entry->valid) { entry = user_tile_cache_entry_acquire(chunk_ent->tile_chunk_pos); } } } /* Release entries with invalid sim chunks */ for (u64 entry_index = 0; entry_index < g->tile_cache.num_reserved_entries; ++entry_index) { struct tile_cache_entry *entry = &g->tile_cache.entries[entry_index]; if (entry->valid) { Entity *chunk_ent = sim_ent_from_chunk_pos(entry->pos); if (!chunk_ent->valid) { user_tile_cache_entry_release(entry); } } } //- Draw dirty tile cache entries for (u64 entry_index = 0; entry_index < g->tile_cache.num_reserved_entries; ++entry_index) { struct tile_cache_entry *entry = &g->tile_cache.entries[entry_index]; if (entry->valid) { Vec2I32 chunk_pos = entry->pos; Entity *chunk_ent = sim_ent_from_chunk_pos(chunk_pos); if (entry->applied_dirty_gen != chunk_ent->dirty_gen) { entry->applied_dirty_gen = chunk_ent->dirty_gen; /* TODO: Autotiling */ String data = sim_ent_get_chunk_tile_data(chunk_ent); u64 tile_count = data.len; if (tile_count == SIM_TILES_PER_CHUNK_SQRT * SIM_TILES_PER_CHUNK_SQRT) { for (u64 y_in_chunk = 0; y_in_chunk < SIM_TILES_PER_CHUNK_SQRT; ++y_in_chunk) { for (u64 x_in_chunk = 0; x_in_chunk < SIM_TILES_PER_CHUNK_SQRT; ++x_in_chunk) { } } } else { /* TODO: Clear gpu buffer if it exists */ } } } } #if 0 for (u64 entry_index = 0; entry_index < g->tile_cache.num_reserved_entries; ++entry_index) { struct tile_cache_entry *entry = &g->tile_cache.entries[entry_index]; if (entry->valid) { Vec2I32 chunk_pos = entry->pos; Entity *chunk_ent = sim_ent_from_chunk_pos(chunk_pos); if (entry->applied_dirty_gen != chunk_ent->dirty_gen) { entry->applied_dirty_gen = chunk_ent->dirty_gen; /* Retreive surrounding chunk info since we're auto-tiling * [TL] [T] [TR] * [L ] X [R ] * [BL] [B] [BR] */ Vec2I32 chunk_pos_tl = VEC2I32(chunk_pos.x - 1, chunk_pos.y - 1); Vec2I32 chunk_pos_t = VEC2I32(chunk_pos.x, chunk_pos.y - 1); Vec2I32 chunk_pos_tr = VEC2I32(chunk_pos.x + 1, chunk_pos.y - 1); Vec2I32 chunk_pos_l = VEC2I32(chunk_pos.x - 1, chunk_pos.y); Vec2I32 chunk_pos_r = VEC2I32(chunk_pos.x + 1, chunk_pos.y); Vec2I32 chunk_pos_bl = VEC2I32(chunk_pos.x - 1, chunk_pos.y + 1); Vec2I32 chunk_pos_b = VEC2I32(chunk_pos.x, chunk_pos.y + 1); Vec2I32 chunk_pos_br = VEC2I32(chunk_pos.x + 1, chunk_pos.y + 1); Entity *chunk_ent_tl = sim_ent_from_chunk_pos(chunk_pos_tl); Entity *chunk_ent_t = sim_ent_from_chunk_pos(chunk_pos_t); Entity *chunk_ent_tr = sim_ent_from_chunk_pos(chunk_pos_tr); Entity *chunk_ent_l = sim_ent_from_chunk_pos(chunk_pos_l); Entity *chunk_ent_r = sim_ent_from_chunk_pos(chunk_pos_r); Entity *chunk_ent_bl = sim_ent_from_chunk_pos(chunk_pos_bl); Entity *chunk_ent_b = sim_ent_from_chunk_pos(chunk_pos_b); Entity *chunk_ent_br = sim_ent_from_chunk_pos(chunk_pos_br); String data = sim_ent_get_chunk_tile_data(chunk_ent); } } } #endif #endif //- Sort drawable entities Entity **sorted = PushDry(scratch.arena, Entity *); u64 sorted_count = 0; { /* Copy valid entities */ { __profn("Build ents list for sorting"); for (u64 ent_index = 0; ent_index < g->ss_blended->num_ents_reserved; ++ent_index) { Entity *ent = &g->ss_blended->ents[ent_index]; if (IsValidAndActive(ent)) { *PushStructNoZero(scratch.arena, Entity *) = ent; ++sorted_count; } } } /* Sort */ { __profn("Sort ents"); Mergesort(sorted, sorted_count, sizeof(*sorted), EntitySortCmp, 0); } } //- Draw entities { __profn("Draw entities"); for (u64 sorted_index = 0; sorted_index < sorted_count; ++sorted_index) { Entity *ent = sorted[sorted_index]; if (!IsValidAndActive(ent)) continue; //if (S_IsNil(ent->sprite)) continue; Resource sprite = ent->sprite; Entity *parent = EntityFromId(g->ss_blended, ent->parent); Xform xf = XformFromEntity(ent); UNUSED Xform parent_xf = XformFromEntity(parent); b32 skip_debug_draw = !g->debug_camera && ent == local_camera; skip_debug_draw = skip_debug_draw || HasProp(ent, Prop_MotorJoint); b32 skip_debug_draw_transform = HasProp(ent, Prop_Camera); skip_debug_draw_transform = 1; UNUSED Xform sprite_xform = MulXform(xf, ent->sprite_local_xform); /* Draw tracer */ /* TODO: Enable this */ #if 0 if (HasProp(ent, Prop_Tracer)) { Vec2 velocity = ent->tracer_start_velocity; Vec2 a = ent->tracer_start; Vec2 b = xf.og; Vec2 c = ent->tracer_gradient_start; Vec2 d = ent->tracer_gradient_end; Vec2 vcd = SubVec2(d, c); Vec2 vca = SubVec2(a, c); Vec2 vdb = SubVec2(b, d); Vec2 vdc = NegVec2(vcd); f32 opacity_a = 1; f32 opacity_b = 1; if (Vec2LenSq(vcd) != 0) { if (DotVec2(velocity, vca) <= 0) { a = c; opacity_a = 0; } else { opacity_a = DotVec2(vcd, vca) / Vec2LenSq(vcd); } opacity_a = ClampF32(opacity_a, 0, 1); opacity_b = ClampF32(1.f - (DotVec2(vdc, vdb) / Vec2LenSq(vdc)), 0, 1); } f32 thickness = 0.01f; u32 color_start = Rgba32F(1, 0.5, 0, opacity_a); u32 color_end = Rgba32F(1, 0.8, 0.4, opacity_b); if (opacity_b > 0.99f) { D_DrawCircle(g->render_sig, b, thickness / 2, color_end, 20); } D_DrawLineGradient(g->render_sig, a, b, thickness, color_start, color_end); } #endif /* Draw sprite */ if (!IsResourceNil(sprite)) { S_Sheet *sheet = S_SheetFromResourceAsync(sprite); S_Texture *texture = S_TextureFromResourceAsync(sprite); /* TODO: Fade in placeholder if texture isn't loaded */ if (sheet->loaded && texture->loaded) { b32 is_light = HasProp(ent, Prop_LightTest); Vec3 emittance = ent->sprite_emittance; u32 tint = ent->sprite_tint; S_Frame frame = S_FrameFromIndex(sheet, ent->animation_frame); /* FIXME: Enable this */ #if 0 D_MaterialParams params = D_MATERIALPARAMS(.xf = sprite_xform, .texture = texture->gpu_resource, .tint = tint, .clip = frame.clip, .is_light = is_light, .light_emittance = emittance); D_DrawMaterial(g->render_sig, params); #else LAX is_light; LAX emittance; LAX tint; LAX frame; #endif } } /* Draw tiles */ /* TODO: Something better */ if (HasProp(ent, Prop_TileChunk)) { Vec2I32 chunk_index = ent->tile_chunk_index; Resource tile_sprite = ResourceFromStore(&GameResources, Lit("sprite/tile.ase")); S_Texture *tile_texture = S_TextureFromResourceAsync(tile_sprite); if (tile_texture->loaded) { f32 tile_size = 1.f / SIM_TILES_PER_UNIT_SQRT; for (i32 tile_y = 0; tile_y < SIM_TILES_PER_CHUNK_SQRT; ++tile_y) { for (i32 tile_x = 0; tile_x < SIM_TILES_PER_CHUNK_SQRT; ++tile_x) { Vec2I32 local_tile_index = VEC2I32(tile_x, tile_y); TileKind tile = ent->tile_chunk_tiles[local_tile_index.x + (local_tile_index.y * SIM_TILES_PER_CHUNK_SQRT)]; /* FIXME: Enable this */ #if 0 //if (tile > -1) if (tile == TileKind_Wall) { Vec2I32 world_tile_index = WorldTileIndexFromLocalTileIndex(chunk_index, local_tile_index); Vec2 pos = PosFromWorldTileIndex(world_tile_index); Xform tile_xf = XformFromRect(RectFromVec2(pos, VEC2(tile_size, tile_size))); D_MaterialParams params = D_MATERIALPARAMS(.xf = tile_xf, .texture = tile_texture->gpu_resource, .is_light = 1, .light_emittance = VEC3(0, 0, 0)); D_DrawMaterial(g->render_sig, params); } #else LAX local_tile_index; LAX tile; LAX tile_size; LAX chunk_index; #endif } } } } /* Debug draw entity info */ if (g->debug_draw && !skip_debug_draw) { TempArena temp = BeginTempArena(scratch.arena); if (HasProp(ent, Prop_Kinematic) || HasProp(ent, Prop_Dynamic)) { DrawDebugMovement(ent); } /* Draw xform */ if (!skip_debug_draw_transform) { u32 color_x = Rgba32F(1, 0, 0, 0.5); u32 color_y = Rgba32F(0, 1, 0, 0.5); DrawDebugXform(xf, color_x, color_y); } /* Draw AABB */ /* FIXME: Enable this */ #if 0 if (ent->local_collider.count > 0) { Aabb aabb = CLD_AabbFromShape(&ent->local_collider, xf); f32 thickness = 1; u32 color = Rgba32F(1, 0, 1, 0.5); Quad quad = QuadFromAabb(aabb); quad = MulXformQuad(g->world_to_ui_xf, quad); D_DrawQuadLine(g->render_sig, quad, thickness, color); } /* Draw focus arrow */ if (ent == local_control || EqId(ent->id, g->debug_following)) { S_Sheet *sheet = S_SheetFromResourceAsync(ent->sprite); S_Slice slice = S_SliceFromNameIndex(sheet, Lit("attach.wep"), ent->animation_frame); Vec2 start = MulXformV2(sprite_xform, slice.center); start = MulXformV2(g->world_to_ui_xf, start); Vec2 end = AddVec2(xf.og, ent->control.focus); end = MulXformV2(g->world_to_ui_xf, end); D_DrawArrowLine(g->render_sig, start, end, 3, 10, Rgba32F(1, 1, 1, 0.5)); } #endif #if 0 /* Draw slices */ if (!S_IsNil(ent->sprite)) { S_Sheet *sheet = S_SheetFromResourceAsync(sprite); u32 quad_color = Rgba32F(1, 0, 0.5, 1); u32 point_color = Rgba32F(1, 0, 0, 1); u32 ray_color = Rgba32F(1, 0, 0.5, 1); for (u64 i = 0; i < sheet->slice_groups_count; ++i) { S_SheetSliceGroup *group = &sheet->slice_groups[i]; if (StringEndsWith(group->name, Lit(".ray"))) continue; for (u32 j = 0; j < group->per_frame_count; ++j) { S_Slice slice = group->frame_slices[(ent->animation_frame * group->per_frame_count) + j]; Vec2 center = MulXformV2(sprite_xform, slice.center); center = MulXformV2(g->world_to_ui_xf, center); if (!slice.has_ray) { Quad quad = QuadFromRect(slice.rect); quad = MulXformQuad(sprite_xform, quad); quad = MulXformQuad(g->world_to_ui_xf, quad); D_DrawQuadLine(g->render_sig, quad, 2, quad_color); } D_DrawCircle(g->render_sig, center, 3, point_color, 20); if (slice.has_ray) { Vec2 ray = MulXformBasisV2(sprite_xform, slice.dir); ray = MulXformBasisV2(g->world_to_ui_xf, ray); ray = Vec2WithLen(ray, 25); D_DrawArrowRay(g->render_sig, center, ray, 2, 10, ray_color); } } } } #endif /* Draw weld joint */ #if 0 if (HasProp(ent, Prop_WeldJoint)) { Entity *e1 = EntityFromId(g->ss_blended, ent->weld_joint_data.e1); Xform e1_xf = XformFromEntity(e1); u32 color = ColorYellow; f32 radius = 3; Vec2 point = MulXformV2(e1_xf, ent->weld_joint_data.point_local_e1); point = MulXformV2(g->world_to_ui_xf, point); D_DrawCircle(g->render_sig, point, radius, color, 10); DEBUGBREAKABLE; } #endif /* Draw mouse joint */ /* FIXME: Enable this */ #if 0 if (HasProp(ent, Prop_MouseJoint)) { Entity *target = EntityFromId(g->ss_blended, ent->mouse_joint_data.target); Xform target_xf = XformFromEntity(target); u32 color = ColorWhite; Vec2 point_start = MulXformV2(target_xf, ent->mouse_joint_data.point_local_start); Vec2 point_end = g->world_cursor; point_start = MulXformV2(g->world_to_ui_xf, point_start); point_end = MulXformV2(g->world_to_ui_xf, point_end); D_DrawArrowLine(g->render_sig, point_start, point_end, 3, 10, color); D_DrawCircle(g->render_sig, point_start, 4, color, 10); } #endif /* Draw collider */ /* FIXME: Enable this */ #if 0 if (ent->local_collider.count > 0) { CLD_Shape collider = ent->local_collider; u32 color = Rgba32F(1, 1, 0, 0.5); f32 thickness = 2; { /* Draw collider using support points */ u32 detail = 32; Xform collider_draw_xf = MulXform(g->world_to_ui_xf, xf); D_DrawColliderLine(g->render_sig, collider, collider_draw_xf, thickness, color, detail); } { /* Draw collider shape points */ for (u32 i = 0; i < collider.count; ++i) { Vec2 p = MulXformV2(MulXform(g->world_to_ui_xf, xf), collider.points[i]); D_DrawCircle(g->render_sig, p, 3, ColorBlue, 10); } } if (collider.count == 1 && collider.radius > 0) { /* Draw upwards line for circle */ Vec2 start = xf.og; Vec2 end = CLD_SupportPointFromDir(&collider, xf, NegVec2(xf.by)).p; start = MulXformV2(g->world_to_ui_xf, start); end = MulXformV2(g->world_to_ui_xf, end); D_DrawLine(g->render_sig, start, end, thickness, color); } #if 0 /* Draw support point at focus dir */ { Vec2 p = collider_support_point(&collider, xf, ent->control.focus); p = MulXformV2(g->world_to_ui_xf, p); D_DrawCircle(g->render_sig, p, 3, ColorRed, 10); } #endif } #endif /* Draw contact constraint */ /* FIXME: Enable this */ #if 0 if (HasProp(ent, Prop_ContactConstraint)) { ContactConstraint *data = &ent->contact_constraint_data; Entity *e0 = EntityFromId(g->ss_blended, data->e0); Entity *e1 = EntityFromId(g->ss_blended, data->e1); LAX e0; LAX e1; #if DeveloperIsEnabled /* Draw contact points */ { f32 radius = 5; for (u32 i = 0; i < data->num_points; ++i) { u32 color = (data->skip_solve || data->wrong_dir) ? Alpha32F(ColorYellow, 0.3) : Rgba32F(0.8, 0.2, 0.2, 1); ContactPoint point = data->points[i]; Vec2 dbg_pt = point.dbg_pt; /* Draw point */ { D_DrawCircle(g->render_sig, MulXformV2(g->world_to_ui_xf, dbg_pt), radius, color, 10); } /* Draw normal */ { f32 len = 0.1f; f32 arrow_thickness = 2; f32 arrow_height = 5; Vec2 start = MulXformV2(g->world_to_ui_xf, dbg_pt); Vec2 end = MulXformV2(g->world_to_ui_xf, AddVec2(dbg_pt, MulVec2(NormVec2(data->normal), len))); D_DrawArrowLine(g->render_sig, start, end, arrow_thickness, arrow_height, color); } #if 0 /* Draw contact info */ { F_Font *disp_font = F_LoadFontAsync(Lit("font/fixedsys.ttf"), 12.0f); if (disp_font) { f32 offset_px = 10; String fmt = Lit( "e0 index: %F\n" "e1 index: %F\n" "id: 0x%F\n" "impulse (n): %F\n" "impulse (t): %F\n" "separation: %F\n" "normal: (%F, %F)\n" "num contacts: %F" ); String text = StringF(temp.arena, fmt, FmtUint(e0->handle.idx), FmtUint(e1->handle.idx), FmtHex(point.id), FmtFloat(point.normal_impulse), FmtFloat(point.tangent_impulse), FmtFloatP(point.starting_separation, 6), FmtFloatP(data->normal.x, 6), FmtFloatP(data->normal.y, 6), FmtUint(data->num_points)); draw_text(g->render_sig, disp_font, AddVec2(RoundVec2(MulXformV2(g->world_to_ui_xf, dbg_pt)), VEC2(0, offset_px)), text); } } #endif } } #endif } #endif /* Draw collision debug */ #if COLLIDER_DEBUG if (HasProp(ent, Prop_CollisionDebug)) { ContactDebugData *data = &ent->collision_debug_data; CLD_CollisionData collision_reuslt = data->collision_result; Entity *e0 = EntityFromId(g->ss_blended, data->e0); Entity *e1 = EntityFromId(g->ss_blended, data->e1); CLD_Shape e0_collider = e0->local_collider; CLD_Shape e1_collider = e1->local_collider; LAX e0_collider; LAX e1_collider; /* Draw closest points */ #if 0 { f32 radius = 4; u32 color = Rgba32F(1, 1, 0, 0.5); Vec2 a = MulXformV2(g->world_to_ui_xf, data->closest0); Vec2 b = MulXformV2(g->world_to_ui_xf, data->closest1); D_DrawCircle(g->render_sig, a, radius, color, 10); D_DrawCircle(g->render_sig, b, radius, color, 10); } #endif /* Draw clipping */ { f32 thickness = 4; f32 radius = 4; u32 color_line = Rgba32F(1, 0, 1, 0.75); u32 color_a = Rgba32F(1, 0, 0, 0.25); u32 color_b = Rgba32F(0, 1, 0, 0.25); u32 color_line_clipped = Rgba32F(1, 0, 1, 1); u32 color_a_clipped = Rgba32F(1, 0, 0, 1); u32 color_b_clipped = Rgba32F(0, 1, 0, 1); { Vec2 a = MulXformV2(g->world_to_ui_xf, collision_reuslt.a0); Vec2 b = MulXformV2(g->world_to_ui_xf, collision_reuslt.b0); D_DrawLine(g->render_sig, a, b, thickness, color_line); D_DrawCircle(g->render_sig, a, radius, color_a, 10); D_DrawCircle(g->render_sig, b, radius, color_b, 10); Vec2 a_clipped = MulXformV2(g->world_to_ui_xf, collision_reuslt.a0_clipped); Vec2 b_clipped = MulXformV2(g->world_to_ui_xf, collision_reuslt.b0_clipped); D_DrawLine(g->render_sig, a_clipped, b_clipped, thickness, color_line_clipped); D_DrawCircle(g->render_sig, a_clipped, radius, color_a_clipped, 10); D_DrawCircle(g->render_sig, b_clipped, radius, color_b_clipped, 10); } { Vec2 a = MulXformV2(g->world_to_ui_xf, collision_reuslt.a1); Vec2 b = MulXformV2(g->world_to_ui_xf, collision_reuslt.b1); D_DrawLine(g->render_sig, a, b, thickness, color_line); D_DrawCircle(g->render_sig, a, radius, color_a, 10); D_DrawCircle(g->render_sig, b, radius, color_b, 10); Vec2 a_clipped = MulXformV2(g->world_to_ui_xf, collision_reuslt.a1_clipped); Vec2 b_clipped = MulXformV2(g->world_to_ui_xf, collision_reuslt.b1_clipped); D_DrawLine(g->render_sig, a_clipped, b_clipped, thickness, color_line_clipped); D_DrawCircle(g->render_sig, a_clipped, radius, color_a_clipped, 10); D_DrawCircle(g->render_sig, b_clipped, radius, color_b_clipped, 10); } } #if COLLIDER_DEBUG_DETAILED_DRAW_MENKOWSKI Xform e0_xf = data->xf0; Xform e1_xf = data->xf1; #if 0 /* Only draw points with large separation */ b32 should_draw = 0; for (u32 i = 0; i < data->num_points; ++i) { if (data->points[i].starting_separation < -0.1) { should_draw = 1; break; } } #else b32 should_draw = 1; #endif if (should_draw) { #if 0 /* Test info */ { F_Font *disp_font = F_LoadFontAsync(Lit("font/fixedsys.ttf"), 12.0f); if (disp_font) { f32 offset_px = 10; String fmt = Lit( "e0 pos: (%F, %F)\n" "e0 rot: %F\n" "e1 pos: (%F, %F)\n" "e1 rot: %F\n" ); String text = StringF(temp.arena, fmt, FmtFloatP(e0_xf.og.x, 24), FmtFloatP(e0_xf.og.y, 24), FmtFloatP(RotationFromXform(e0_xf), 24), FmtFloatP(e1_xf.og.x, 24), FmtFloatP(e1_xf.og.y, 24), FmtFloatP(RotationFromXform(e1_xf), 24)); draw_text(g->render_sig, disp_font, AddVec2(RoundVec2(MulXformV2(g->world_to_ui_xf, VEC2(0, 0))), VEC2(0, offset_px)), text); } } #endif /* Draw CLD_Menkowski */ { u32 color = collision_reuslt.solved ? Rgba32F(0, 0, 0.25, 1) : Rgba32F(0, 0.25, 0.25, 1); f32 thickness = 2; u32 detail = 512; LAX thickness; Vec2Array m = CLD_Menkowski(temp.arena, &e0_collider, &e1_collider, e0_xf, e1_xf, detail); for (u64 i = 0; i < m.count; ++i) m.points[i] = MulXformV2(g->world_to_ui_xf, m.points[i]); D_DrawPolyLine(g->render_sig, m, 1, thickness, color); //D_DrawPoly(g->render_sig, m, color); } /* Draw CLD_PointCloud */ { u32 color = Rgba32F(1, 1, 1, 1); f32 radius = 2; Vec2Array m = CLD_PointCloud(temp.arena, &e0_collider, &e1_collider, e0_xf, e1_xf); for (u64 i = 0; i < m.count; ++i) { Vec2 p = MulXformV2(g->world_to_ui_xf, m.points[i]); D_DrawCircle(g->render_sig, p, radius, color, 10); } } /* Draw prototype */ { f32 thickness = 2; u32 color = Rgba32F(1, 1, 1, 0.25); Vec2Array m = { .points = collision_reuslt.prototype.points, .count = collision_reuslt.prototype.len }; for (u64 i = 0; i < m.count; ++i) m.points[i] = MulXformV2(g->world_to_ui_xf, m.points[i]); D_DrawPolyLine(g->render_sig, m, 1, thickness, color); for (u64 i = 0; i < m.count; ++i) D_DrawCircle(g->render_sig, m.points[i], 10, color, 10); } /* Draw simplex */ { f32 thickness = 2; u32 line_color = ColorYellow; u32 color_first = Rgba32F(1, 0, 0, 0.75); u32 color_second = Rgba32F(0, 1, 0, 0.75); u32 color_third = Rgba32F(0, 0, 1, 0.75); CLD_MenkowskiSimplex simplex = collision_reuslt.simplex; Vec2 simplex_points[] = { simplex.a.p, simplex.b.p, simplex.c.p }; for (u64 i = 0; i < countof(simplex_points); ++i) simplex_points[i] = MulXformV2(g->world_to_ui_xf, simplex_points[i]); Vec2Array simplex_array = { .count = simplex.len, .points = simplex_points }; if (simplex.len >= 1) { u32 color = simplex.len == 1 ? color_first : (simplex.len == 2 ? color_second : color_third); D_DrawCircle(g->render_sig, simplex_array.points[0], thickness * 3, color, 10); } if (simplex.len >= 2) { u32 color = simplex.len == 2 ? color_first : color_second; D_DrawCircle(g->render_sig, simplex_array.points[1], thickness * 3, color, 10); } if (simplex.len >= 3) { u32 color = color_first; D_DrawCircle(g->render_sig, simplex_array.points[2], thickness * 3, color, 10); } if (simplex.len >= 2) { D_DrawPolyLine(g->render_sig, simplex_array, simplex.len > 2, thickness, line_color); } } /* Draw normal */ { u32 color = ColorWhite; f32 len = 0.1f; f32 arrow_thickness = 4; f32 arrowhead_height = 10; Vec2 start = MulXformV2(g->world_to_ui_xf, VEC2(0, 0)); Vec2 end = MulXformV2(g->world_to_ui_xf, MulVec2(NormVec2(collision_reuslt.normal), len)); D_DrawArrowLine(g->render_sig, start, end, arrow_thickness, arrowhead_height, color); } } #endif } #endif /* Draw hierarchy */ /* FIXME: Enable this */ #if 0 if (HasProp(parent, Prop_Active) && !parent->is_root) { u32 color = Rgba32F(0.6, 0.6, 1, 0.75); f32 thickness = 2; f32 arrow_height = 15; Vec2 start = MulXformV2(g->world_to_ui_xf, xf.og); Vec2 end = MulXformV2(g->world_to_ui_xf, parent_xf.og); D_DrawArrowLine(g->render_sig, start, end, thickness, arrow_height, color); } /* Draw camera rect */ if (HasProp(ent, Prop_Camera)) { u32 color = ent == local_camera ? Rgba32F(1, 1, 1, 0.5) : Rgba32F(0, 0.75, 0, 0.5); f32 thickness = 3; Xform quad_xf = MulXform(xf, ent->camera_quad_xform); Quad quad = MulXformQuad(quad_xf, CenteredUnitSquareQuad); quad = MulXformQuad(g->world_to_ui_xf, quad); D_DrawQuadLine(g->render_sig, quad, thickness, color); } #endif EndTempArena(temp); } } } /* Draw crosshair or show cursor */ /* FIXME: Enable this */ #if 0 if (!g->debug_camera) { __profn("Draw crosshair"); Vec2 crosshair_pos = g->ui_cursor; Resource crosshair = ResourceFromStore(GameResources, Lit("sprite/crosshair.ase")); S_Texture *t = S_TextureFromResourceAsync(crosshair); Vec2 size = VEC2(t->width, t->height); Xform xf = XformFromTrs(TRS(.t = crosshair_pos, .s = size)); D_DrawUiRect(g->render_sig, D_UIRECTPARAMS(.xf = xf, .texture = t->gpu_resource)); } #endif /* FIXME: Enable this */ #if 0 { __profn("Update window cursor"); if (g->debug_camera) { P_DisableWindoweCursorClip(g->window); P_ShowWindowCursor(g->window); } else { S_Texture *t = S_TextureFromResourceAsync(ResourceFromStore(GameResources, Lit("sprite/crosshair.ase"))); Vec2 size = VEC2(t->width, t->height); Rect cursor_clip = RectFromVec2(g->ui_screen_offset, g->ui_size); cursor_clip.pos = AddVec2(cursor_clip.pos, MulVec2(size, 0.5f)); cursor_clip.pos = AddVec2(cursor_clip.pos, VEC2(1, 1)); cursor_clip.size = SubVec2(cursor_clip.size, size); P_HideWindowCursor(g->window); P_EnableWindoweCursorClip(g->window, cursor_clip); } } #endif //- Create user sim cmd { /* Queue player move cmd */ f32 move_speed = 1.0f; //if (g->bind_states[BindKind_Walk].is_held) if (g->bind_states[BindKind_FullscreenMod].is_held) { //const f32 walk_ratio = 0.25f; const f32 walk_ratio = 0.05f; move_speed *= walk_ratio; } Vec2 input_move_dir = ZI; { for (BindKind bind = 0; bind < (i32)countof(g->bind_states); ++bind) { BindState state = g->bind_states[bind]; if (!state.is_held && state.num_presses <= 0) { continue; } switch (bind) { default: break; /* Movement */ case BindKind_MoveUp: { input_move_dir.y -= 1; } break; case BindKind_MoveDown: { input_move_dir.y += 1; } break; case BindKind_MoveLeft: { input_move_dir.x -= 1; } break; case BindKind_MoveRight: { input_move_dir.x += 1; } break; } } input_move_dir = InvertXformBasisMulV2(g->world_to_ui_xf, input_move_dir); /* Make move dir relative to world view */ input_move_dir = MulVec2(NormVec2(input_move_dir), move_speed); } if (!g->debug_camera) { g->focus_send = SubVec2(g->world_cursor, XformFromEntity(local_control).og); } Vec2 input_aim_dir = g->focus_send; /* Queue player control cmd */ { ControlData control = ZI; control.move = input_move_dir; control.focus = input_aim_dir; control.dbg_cursor = g->world_cursor; BindState fire_state = g->bind_states[BindKind_Fire]; BindState fire_alt_state = g->bind_states[BindKind_AltFire]; BindState drag_state = g->bind_states[BindKind_DebugDrag]; BindState delete_state = g->bind_states[BindKind_DebugDelete]; BindState clear_state = g->bind_states[BindKind_DebugClear]; BindState spawn1_state = g->bind_states[BindKind_DebugSpawn1]; BindState spawn2_state = g->bind_states[BindKind_DebugSpawn2]; BindState spawn3_state = g->bind_states[BindKind_DebugSpawn3]; BindState spawn4_state = g->bind_states[BindKind_DebugSpawn4]; BindState walls_state = g->bind_states[BindKind_DebugWalls]; BindState pause_state = g->bind_states[BindKind_DebugPause]; BindState step_state = g->bind_states[BindKind_DebugStep]; BindState tile_state = g->bind_states[BindKind_TestTile]; BindState explode_state = g->bind_states[BindKind_DebugExplode]; BindState teleport_state = g->bind_states[BindKind_DebugTeleport]; if (fire_state.num_presses || fire_state.is_held) { control.flags |= ControlFlag_Fire; } if (fire_alt_state.num_presses || fire_alt_state.is_held) { control.flags |= ControlFlag_AltFire; } if (drag_state.num_presses || drag_state.is_held) { control.flags |= ControlFlag_Drag; } if (delete_state.num_presses || delete_state.is_held) { control.flags |= ControlFlag_Delete; } if (clear_state.num_presses_and_repeats) { control.flags |= ControlFlag_ClearAll; } if (spawn1_state.num_presses_and_repeats) { control.flags |= ControlFlag_SpawnTest1; } if (spawn2_state.num_presses_and_repeats) { control.flags |= ControlFlag_SpawnTest2; } if (spawn3_state.num_presses_and_repeats) { control.flags |= ControlFlag_SpawnTest3; } if (spawn4_state.num_presses_and_repeats) { control.flags |= ControlFlag_SpawnTest4; } if (walls_state.num_presses_and_repeats) { control.flags |= ControlFlag_TestWalls; } if (tile_state.num_presses || tile_state.is_held) { control.flags |= ControlFlag_TestTiles; } if (explode_state.num_presses_and_repeats) { control.flags |= ControlFlag_TestExplode; } if (teleport_state.num_presses_and_repeats || (g->debug_camera && teleport_state.is_held)) { control.flags |= ControlFlag_TestTeleport; } if (pause_state.num_presses) { Atomic32FetchXor(&g->user_paused, 1); } Atomic32FetchAdd(&g->user_paused_steps, step_state.num_presses_and_repeats); /* Set user sim control */ { Lock lock = LockE(&g->user_sim_cmd_mutex); /* Reset flags */ if (g->user_sim_cmd_gen != g->last_user_sim_cmd_gen) { g->user_sim_cmd_control.flags = 0; g->last_user_sim_cmd_gen = g->user_sim_cmd_gen; } u32 old_flags = g->user_sim_cmd_control.flags; g->user_sim_cmd_control = control; g->user_sim_cmd_control.flags |= old_flags; g->user_hovered_ent = hovered_ent->id; Unlock(&lock); } } #if RtcIsEnabled /* Gjk steps */ { if (g->bind_states[BindKind_ResetDebugSteps].num_presses_and_repeats > 0) { SetGstat(GSTAT_DEBUG_STEPS, 0); } i32 add_steps = 0; add_steps += g->bind_states[BindKind_IncrementDebugSteps].num_presses_and_repeats; add_steps -= g->bind_states[BindKind_DecrementDebugSteps].num_presses_and_repeats; if (add_steps != 0) { AddGstat(GSTAT_DEBUG_STEPS, add_steps); } } #endif } { /* Update network usage stats */ i64 stat_now_ns = TimeNs(); g->net_bytes_read.last_second_end = GetGstat(GSTAT_SOCK_BYTES_RECEIVED); g->net_bytes_sent.last_second_end = GetGstat(GSTAT_SOCK_BYTES_SENT); if (stat_now_ns - g->last_second_reset_ns > NsFromSeconds(1)) { g->last_second_reset_ns = stat_now_ns; g->net_bytes_read.last_second = g->net_bytes_read.last_second_end - g->net_bytes_read.last_second_start; g->net_bytes_sent.last_second = g->net_bytes_sent.last_second_end - g->net_bytes_sent.last_second_start; g->net_bytes_read.last_second_start = g->net_bytes_read.last_second_end; g->net_bytes_sent.last_second_start = g->net_bytes_sent.last_second_end; } } //- Draw ent debug info /* FIXME: Enable this */ #if 0 if (g->debug_draw && hovered_ent->valid) { Entity *ent = hovered_ent; Vec2 pos = AddVec2(g->ui_cursor, VEC2(15, 15)); F_Font *font = F_LoadFontAsync(Lit("font/fixedsys.ttf"), 12.0f); if (font) { TempArena temp = BeginTempArena(scratch.arena); String dbg_text = ZI; dbg_text.text = PushDry(temp.arena, u8); dbg_text.len += DebugStringFromEntity(temp.arena, ent).len; draw_text(g->render_sig, D_TEXTPARAMS(.font = font, .pos = pos, .str = dbg_text)); EndTempArena(temp); } } #endif //- Query vram GPU_MemoryInfo vram = GPU_QueryMemoryInfo(); //- Draw global debug info /* FIXME: Enable this */ #if 0 if (g->debug_draw) { __profn("Draw debug info"); F_Font *font = F_LoadFontAsync(Lit("font/fixedsys.ttf"), 12.0f); if (font) { TempArena temp = BeginTempArena(scratch.arena); String text = ZI; text.text = PushDry(temp.arena, u8); #if BITBUFF_DEBUG text.len += StringF(temp.arena, "(bitbuff debug enabled)").len; text.len += StringF(temp.arena, "\n").len; #endif text.len += StringF(temp.arena, "blended world entities: %F/%F", FmtUint(g->ss_blended->num_ents_allocated), FmtUint(g->ss_blended->num_ents_reserved)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "blended world tick: %F", FmtUint(g->ss_blended->tick)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "blended world time: %F", FmtFloat(SecondsFromNs(g->ss_blended->sim_time_ns))).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "average local sim publish dt: %F", FmtFloat(SecondsFromNs(g->average_local_to_user_snapshot_publish_dt_ns))).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "local sim last known tick: %F", FmtUint(g->local_sim_last_known_tick)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "local sim last known time: %F", FmtFloat(SecondsFromNs(g->local_sim_last_known_time_ns))).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "local sim predicted time: %F", FmtFloat(SecondsFromNs(g->local_sim_predicted_time_ns))).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "render time target: %F", FmtFloat(SecondsFromNs(g->render_time_target_ns))).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "render time: %F", FmtFloat(SecondsFromNs(g->render_time_ns))).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "local player: [%F]", FmtUid(local_player->id.uid)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "\n").len; Vec2 world_cursor = g->world_cursor; text.len += StringF(temp.arena, "cursor world: %F, %F", FmtFloat(world_cursor.x), FmtFloat(world_cursor.y)).len; text.len += StringF(temp.arena, "\n").len; Vec2I32 world_tile_cursor = WorldTileIndexFromPos(world_cursor); text.len += StringF(temp.arena, "cursor world tile: %F, %F", FmtSint(world_tile_cursor.x), FmtSint(world_tile_cursor.y)).len; text.len += StringF(temp.arena, "\n").len; Vec2I32 local_tile_cursor = LocalTileIndexFromWorldTileIndex(world_tile_cursor); text.len += StringF(temp.arena, "cursor local tile: %F, %F", FmtSint(local_tile_cursor.x), FmtSint(local_tile_cursor.y)).len; text.len += StringF(temp.arena, "\n").len; Vec2I32 tile_chunk_cursor = TileChunkIndexFromWorldTileIndex(world_tile_cursor); text.len += StringF(temp.arena, "cursor tile chunk: %F, %F", FmtSint(tile_chunk_cursor.x), FmtSint(tile_chunk_cursor.y)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "Network read: %F mbit/s", FmtFloat((f64)g->net_bytes_read.last_second * 8 / 1000 / 1000)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "Network write: %F mbit/s", FmtFloat((f64)g->net_bytes_sent.last_second * 8 / 1000 / 1000)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "Ping (real): %F ms", FmtFloat(SecondsFromNs(local_player->player_last_rtt_ns) * 1000)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "Ping (average): %F ms", FmtFloat(local_player->player_average_rtt_seconds * 1000)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "Memory committed: %F MiB", FmtFloat((f64)GetGstat(GSTAT_MEMORY_COMMITTED) / 1024 / 1024)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "Virtual memory reserved: %F TiB", FmtFloat((f64)GetGstat(GSTAT_MEMORY_RESERVED) / 1024 / 1024 / 1024 / 1024)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "Arenas allocated: %F", FmtUint(GetGstat(GSTAT_NUM_ARENAS))).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "Video memory (GPU): %F MiB", FmtFloat((f64)vram.local_used / 1024 / 1024)).len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "Video memory (shared): %F MiB", FmtFloat((f64)vram.non_local_used / 1024 / 1024)).len; //text.len += StringF(temp.arena, \n")).len; //text.len += StringF(temp.arena, \n")).len; #if RtcIsEnabled text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "\n").len; text.len += StringF(temp.arena, "Debug steps: %F", FmtUint(GetGstat(GSTAT_DEBUG_STEPS))).len; //text.len += StringF(temp.arena, \n")).len; #endif //draw_text(g->render_sig, font, pos, StringF(temp.arena, "blended world entities: %F/%F", FmtUint(g->ss_blended->num_ents_allocated), FmtUint(g->ss_blended->num_ents_reserved))); //draw_text(g->render_sig, font, pos, text); Vec2 pos = VEC2(10, g->ui_size.y); D_TextOffsetY offset_y = DRAW_TEXT_OFFSET_Y_BOTTOM; draw_text(g->render_sig, D_TEXTPARAMS(.font = font, .pos = pos, .str = text, .offset_y = offset_y, .color = ColorWhite)); EndTempArena(temp); } } #else LAX vram; #endif { #if DeveloperIsEnabled b32 console_minimized = !g->debug_console; i32 console_level = console_minimized ? P_LogLevel_Success : P_LogLevel_Debug; DrawDebugConsole(console_level, console_minimized); #else if (g->debug_draw) { DrawDebugConsole(P_LogLevel_Info, 0); } #endif } //- Render { __profn("Render"); Rect ui_viewport = RectFromVec2(VEC2(0, 0), VEC2(g->ui_size.x, g->ui_size.y)); Rect render_viewport = RectFromVec2(VEC2(0, 0), VEC2(g->render_size.x, g->render_size.y)); /* Acquire gbuffers */ if (g->shade_target && !EqVec2I32(g->render_size, GPU_GetTextureSize(g->shade_target))) { __profn("Release render resources"); /* FIXME: Yield on render fence */ GPU_ReleaseResource(g->albedo, GPU_ReleaseFlag_None); GPU_ReleaseResource(g->emittance, GPU_ReleaseFlag_None); GPU_ReleaseResource(g->emittance_flood_read, GPU_ReleaseFlag_None); GPU_ReleaseResource(g->emittance_flood_target, GPU_ReleaseFlag_None); GPU_ReleaseResource(g->shade_read, GPU_ReleaseFlag_None); GPU_ReleaseResource(g->shade_target, GPU_ReleaseFlag_None); g->shade_target = 0; } if (!g->shade_target) { __profn("Acquire sig resources"); g->albedo = AcquireGbuffer(GPU_Format_R8G8B8A8_Unorm, g->render_size); g->emittance = AcquireGbuffer(GPU_Format_R16G16B16A16_Float, g->render_size); g->emittance_flood_read = AcquireGbuffer(GPU_Format_R16G16_Uint, g->render_size); g->emittance_flood_target = AcquireGbuffer(GPU_Format_R16G16_Uint, g->render_size); g->shade_read = AcquireGbuffer(GPU_Format_R16G16B16A16_Float, g->render_size); g->shade_target = AcquireGbuffer(GPU_Format_R16G16B16A16_Float, g->render_size); } /* Acquire ui buffers */ if (g->ui_target && !EqVec2I32(g->ui_size, GPU_GetTextureSize(g->ui_target))) { /* FIXME: Wait on render fence */ GPU_ReleaseResource(g->ui_target, GPU_ReleaseFlag_None); g->ui_target = 0; } if (!g->ui_target) { g->ui_target = AcquireGbuffer(GPU_Format_R8G8B8A8_Unorm, g->ui_size); } /* Acquire transfer buffers */ /* TODO: Make these static */ u16 quad_indices[6] = { 0, 1, 2, 0, 2, 3 }; GPU_Resource *quad_index_buffer = AcquireUploadBuffer(countof(quad_indices), sizeof(*quad_indices), quad_indices); GPU_Resource *material_instance_buffer = AcquireUploadBufferFromArena(g->material_instances_count, g->material_instances_arena); GPU_Resource *ui_rect_instance_buffer = AcquireUploadBufferFromArena(g->ui_rect_instances_count, g->ui_rect_instances_arena); GPU_Resource *ui_shape_verts_buffer = AcquireUploadBufferFromArena(g->ui_shape_verts_count, g->ui_shape_verts_arena); GPU_Resource *ui_shape_indices_buffer = AcquireUploadBufferFromArena(g->ui_shape_indices_count, g->ui_shape_indices_arena); GPU_Resource *grids_buffer = AcquireUploadBufferFromArena(g->grids_count, g->grids_arena); GPU_CommandList *cl = GPU_BeginCommandList(); { __profn("Run render"); GPU_ProfN(cl, Lit("Run render")); Mat4x4 world_to_render_vp_matrix = ProjectMat4x4View(g->world_to_render_xf, render_viewport.width, render_viewport.height); Mat4x4 ui_vp_matrix = ProjectMat4x4View(XformIdentity, ui_viewport.width, ui_viewport.height); Mat4x4 blit_vp_matrix = ZI; { Xform xf = g->render_to_ui_xf; xf = ScaleXform(xf, VEC2(g->render_size.x, g->render_size.y)); xf = TranslateXform(xf, VEC2(0.5, 0.5)); blit_vp_matrix = ProjectMat4x4View(xf, ui_viewport.width, ui_viewport.height); } //- Prep material pass { __profn("Clear gbuffers"); GPU_ProfN(cl, Lit("Clear gbuffers")); GPU_TransitionToRtv(cl, g->albedo); GPU_TransitionToRtv(cl, g->emittance); GPU_ClearResource(cl, g->albedo, VEC4(0, 0, 0, 0)); GPU_ClearResource(cl, g->emittance, VEC4(0, 0, 0, 0)); } //- Material pass { __profn("Material pass"); GPU_ProfN(cl, Lit("Material pass")); GPU_Resource *rts[] = { g->albedo, g->emittance }; GPU_Viewport viewport = GPU_ViewportFromRect(render_viewport); GPU_Scissor scissor = GPU_ScissorFromRect(render_viewport); MaterialSig sig = ZI; /* FIXME: set sampler urid id here */ sig.projection = world_to_render_vp_matrix; sig.instances_urid = GPU_GetResourceId(material_instance_buffer, GPU_ResourceIdKind_Srv); sig.grids_urid = GPU_GetResourceId(grids_buffer, GPU_ResourceIdKind_Srv); GPU_Rasterize(cl, &sig, MaterialVS, MaterialPS, countof(rts), rts, viewport, scissor, g->material_instances_count, quad_index_buffer, GPU_RasterizeMode_TriangleList); } //- Prep flood pass { GPU_TransitionToSrv(cl, g->emittance); GPU_TransitionToUav(cl, g->emittance_flood_read); GPU_TransitionToUav(cl, g->emittance_flood_target); } //- Flood pass if (!effects_disabled) { __profn("Flood pass"); GPU_ProfN(cl, Lit("Flood pass")); i32 step_length = -1; /* TODO: Remove this */ u64 max_steps = GetGstat(GSTAT_DEBUG_STEPS); u64 step = 0; while (step_length != 0 && step < max_steps) { __profn("Flood step"); GPU_ProfN(cl, Lit("Flood step")); GPU_FlushUav(cl, g->emittance_flood_read); FloodSig sig = ZI; sig.step_len = step_length; sig.emittance_tex_urid = GPU_GetResourceId(g->emittance, GPU_ResourceIdKind_Srv); sig.read_flood_tex_urid = GPU_GetResourceId(g->emittance_flood_read, GPU_ResourceIdKind_Uav); sig.target_flood_tex_urid = GPU_GetResourceId(g->emittance_flood_target, GPU_ResourceIdKind_Srv); sig.tex_width = g->render_size.x; sig.tex_height = g->render_size.y; GPU_Compute(cl, &sig, FloodCS, (g->render_size.x + 7) / 8, (g->render_size.y + 7) / 8, 1); /* Swap buffers */ GPU_Resource *swp = g->emittance_flood_read; g->emittance_flood_read = g->emittance_flood_target; g->emittance_flood_target = swp; /* Update step */ if (step_length == -1) { step_length = MaxI32(g->render_size.x, g->render_size.y) / 2; } else { step_length /= 2; } ++step; } } //- Prep shade pass { __profn("Clear shade target"); GPU_ProfN(cl, Lit("Clear shade target")); GPU_TransitionToSrv(cl, g->albedo); GPU_TransitionToSrv(cl, g->emittance); GPU_TransitionToUav(cl, g->shade_target); GPU_FlushUav(cl, g->emittance_flood_read); GPU_FlushUav(cl, g->shade_read); GPU_ClearResource(cl, g->shade_target, VEC4(0, 0, 0, 0)); } //- Shade pass { __profn("Shade pass"); GPU_ProfN(cl, Lit("Shade pass")); u32 shade_flags = ShadeFlag_None; if (effects_disabled) { shade_flags |= ShadeFlag_DisableEffects; } ShadeSig sig = ZI; sig.flags = shade_flags; sig.tex_width = g->render_size.x; sig.tex_height = g->render_size.y; sig.frame_seed = VEC4I32((u32)(RandU64FromState(&g->frame_rand) & 0xFFFFFFFF), (u32)(RandU64FromState(&g->frame_rand) & 0xFFFFFFFF), (u32)(RandU64FromState(&g->frame_rand) & 0xFFFFFFFF), (u32)(RandU64FromState(&g->frame_rand) & 0xFFFFFFFF)); sig.frame_index = g->frame_index; sig.camera_offset = g->world_to_render_xf.og; sig.albedo_tex_urid = GPU_GetResourceId(g->albedo, GPU_ResourceIdKind_Srv); sig.emittance_tex_urid = GPU_GetResourceId(g->emittance, GPU_ResourceIdKind_Srv); sig.emittance_flood_tex_urid = GPU_GetResourceId(g->emittance_flood_read, GPU_ResourceIdKind_Uav); sig.read_tex_urid = GPU_GetResourceId(g->shade_read, GPU_ResourceIdKind_Uav); sig.target_tex_urid = GPU_GetResourceId(g->shade_target, GPU_ResourceIdKind_Uav); GPU_Compute(cl, &sig, ShadeCS, (g->render_size.x + 7) / 8, (g->render_size.y + 7) / 8, 1); /* Swap */ GPU_Resource *swp = g->shade_read; g->shade_read = g->shade_target; g->shade_target = swp; } //- Prep ui pass { __profn("Clear ui target"); GPU_ProfN(cl, Lit("Clear ui target")); GPU_TransitionToRtv(cl, g->ui_target); GPU_FlushUav(cl, g->shade_read); GPU_ClearResource(cl, g->ui_target, VEC4(0, 0, 0, 0)); } //- Ui blit pass { __profn("UI blit pass"); GPU_ProfN(cl, Lit("UI blit pass")); GPU_Viewport viewport = GPU_ViewportFromRect(ui_viewport); GPU_Scissor scissor = GPU_ScissorFromRect(ui_viewport); UiBlitSig sig = ZI; sig.projection = blit_vp_matrix; sig.flags = UiBlitFlag_ToneMap | UiBlitFlag_GammaCorrect; sig.exposure = 2.0; sig.gamma = (f32)2.2; sig.tex_urid = GPU_GetResourceId(g->shade_read, GPU_ResourceIdKind_Uav); GPU_Rasterize(cl, &sig, UiBlitVS, UiBlitPS, 1, &g->ui_target, viewport, scissor, 1, quad_index_buffer, GPU_RasterizeMode_TriangleList); } //- Ui rect pass { __profn("UI rect pass"); GPU_ProfN(cl, Lit("UI rect pass")); GPU_Viewport viewport = GPU_ViewportFromRect(ui_viewport); GPU_Scissor scissor = GPU_ScissorFromRect(ui_viewport); UiRectSig sig = ZI; sig.projection = ui_vp_matrix; sig.instances_urid = GPU_GetResourceId(ui_rect_instance_buffer, GPU_ResourceIdKind_Srv); GPU_Rasterize(cl, &sig, UiRectVS, UiRectPS, 1, &g->ui_target, viewport, scissor, g->ui_rect_instances_count, quad_index_buffer, GPU_RasterizeMode_TriangleList); } //- Ui shape pass { __profn("UI shape pass"); GPU_ProfN(cl, Lit("UI shape pass")); GPU_Viewport viewport = GPU_ViewportFromRect(ui_viewport); GPU_Scissor scissor = GPU_ScissorFromRect(ui_viewport); UiShapeSig sig = ZI; sig.projection = ui_vp_matrix; sig.verts_urid = GPU_GetResourceId(ui_shape_verts_buffer, GPU_ResourceIdKind_Srv); GPU_Rasterize(cl, &sig, UiShapeVS, UiShapePS, 1, &g->ui_target, viewport, scissor, 1, ui_shape_indices_buffer, GPU_RasterizeMode_TriangleList); } } /* FIXME: Enable this */ #if 0 g->last_gpu_barrier = GPU_EndCommandList(cl); /* Release transfer buffers */ { { /* FIXME: Release resources */ GPU_Resource *release_resources[] = { quad_index_buffer, material_instance_buffer, ui_rect_instance_buffer, ui_shape_verts_buffer, ui_shape_indices_buffer, }; Job *job = OpenJob(ReleaseRenderResources); { ReleaseRenderResources_Sig *sig = PushStruct(job->arena, ReleaseRenderResources_Sig); job->count = countof(resources); sig->barrier = g->last_gpu_barrier; sig->resources = PushStructsNoZero(sig->arena, GPU_Resource *, job->count); sig->flags = GPU_ReleaseFlag_Reuse; CopyBytes(sig->resources, resources, sizeof(resources)); job->sig = sig; } CloseJob(job); } ResetArena(g->material_instances_arena); ResetArena(g->ui_rect_instances_arena); ResetArena(g->ui_shape_verts_arena); ResetArena(g->ui_shape_indices_arena); ResetArena(g->grids_arena); g->material_instances_count = 0; g->ui_rect_instances_count = 0; g->ui_shape_verts_count = 0; g->ui_shape_indices_count = 0; g->grids_count = 0; } #endif } EndScratch(scratch); } //////////////////////////////// //~ User update job JobDef(UpdateUserOrSleep, UNUSED sig, UNUSED id) { SharedUserState *g = &shared_user_state; i64 time_ns = TimeNs(); while (!Atomic32Fetch(&g->shutdown)) { P_Window *window = g->window; { __profn("User sleep"); { __profn("Swapchain wait"); GPU_WaitOnSwapchain(g->swapchain); } { __profn("Frame limiter wait"); P_SleepFrame(time_ns, 1000000000 / FPS_LIMIT); time_ns = TimeNs(); } } UpdateUser(window); } } //////////////////////////////// //~ Generate user input cmds void GenerateuserInputCmds(Client *user_input_client, u64 tick) { SharedUserState *g = &shared_user_state; Snapshot *prev_user_input_ss = SnapshotFromTick(user_input_client, user_input_client->last_tick); Snapshot *user_input_ss = AcquireSnapshot(user_input_client, prev_user_input_ss, tick); Entity *user_input_root = EntityFromId(user_input_ss, RootEntityId); /* Find / create local control cmd ent */ Entity *control_cmd = FirstWithProp(user_input_ss, Prop_Cmd); if (!control_cmd->valid) { control_cmd = AcquireSyncSrc(user_input_root); control_cmd->cmd_kind = CmdKind_Control; control_cmd->predictor = user_input_client->player_id; EnableProp(control_cmd, Prop_Cmd); Activate(control_cmd, user_input_ss->tick); } { Lock lock = LockE(&g->user_sim_cmd_mutex); /* Update control cmd */ { control_cmd->cmd_control = g->user_sim_cmd_control; control_cmd->cmd_control_hovered_ent = g->user_hovered_ent; } #if 0 /* Create chat cmd */ if (g->user_sim_cmd_chat.len > 0) { Entity *chat_cmd = AcquireSyncSrc(user_input_root); chat_cmd->cmd_kind = CmdKind_Chat; //chat_cmd->chat_msg = ZI } #endif ++g->user_sim_cmd_gen; Unlock(&lock); } } //////////////////////////////// //~ Sim update JobDef(UpdateSim, UNUSED sig, UNUSED id) { SharedUserState *g = &shared_user_state; #if 0 struct host_listen_address local_listen_addr = host_listen_address_from_local_name(Lit("LOCAL_SIM")); struct host_listen_address net_listen_addr = host_listen_address_from_net_port(12345); //N_Host *host = N_AcquireHost(); /* TODO: Host system should allocate & copy string stored in local_listen_addr */ //host_listen(host, local_listen_addr); //host_listen(host, net_listen_addr); #endif b32 is_master = 0; N_Host *host; if (g->connect_address_str.len > 0) { host = N_AcquireHost(0); P_Address addr = P_AddressFromString(g->connect_address_str); N_Connect(host, addr); } else { host = N_AcquireHost(12345); is_master = 1; } BB_Buff msg_writer_bb = BB_AcquireBuff(Gibi(64)); BB_Buff snapshot_writer_bb = BB_AcquireBuff(Gibi(64)); SimAccel accel = AcquireSimAccel(); ClientStore *store = AcquireClientStore(); Client *user_input_client = AcquireClient(store); /* Stores snapshots containing commands to be published to local client */ Client *local_client = AcquireClient(store); /* Stores snapshots produced locally */ Client *publish_client = AcquireClient(store); /* Stores versions of local snapshots that will be published to remote sims */ Client *master_client = NilClient(); /* Stores snapshots received from master */ Client *master_blended_client = NilClient(); /* Stores interpolated master snapshots */ b32 initialized_from_master = 0; i64 master_blend_time_ns = 0; i64 average_master_receive_dt_ns = 0; i64 last_tick_from_master_received_at_ns = 0; i64 last_publish_to_user_ns = 0; i64 real_time_ns = 0; i64 real_dt_ns = 0; i64 step_dt_ns = NsFromSeconds(1) / SIM_TICKS_PER_SECOND; f64 compute_timescale = 1.0; while (!Atomic32Fetch(&g->shutdown)) { TempArena scratch = BeginScratchNoConflict(); { __profn("Sim sleep"); P_SleepFrame(real_time_ns, step_dt_ns * compute_timescale); } { __profn("Sim update"); real_dt_ns = TimeNs() - real_time_ns; real_time_ns += real_dt_ns; N_EventList host_events = N_BeginUpdate(scratch.arena, host); /* Read net messages */ DecodeQueue queue = ZI; { for (N_Event *event = host_events.first; event; event = event->next) { N_ChannelId channel_id = event->channel_id; Client *client = ClientFromChannelId(store, channel_id); switch (event->kind) { case N_EventKind_ChannelOpened: { if (!client->valid) { if (is_master) { /* Create remote client */ client = AcquireClient(store); SetClientChannelId(client, channel_id); } else { /* Create master client */ if (!master_client->valid) { client = AcquireClient(store); SetClientChannelId(client, channel_id); master_client = client; master_blended_client = AcquireClient(store); } else { /* We already have a master client */ Assert(0); } } } } break; case N_EventKind_Msg: { if (client->valid) { BB_Buff msg_bb = BB_BuffFromString(event->msg); BB_Reader msg_br = BB_ReaderFromBuff(&msg_bb); u64 ack = BB_ReadUV(&msg_br); u64 double_ack = BB_ReadUV(&msg_br); if (ack > client->ack) { client->ack = ack; } if (double_ack > client->double_ack) { client->double_ack = double_ack; } /* Read & queue incoming snapshots for decoding */ u64 tmp_encoded_len = BB_ReadUV(&msg_br); while (tmp_encoded_len > 0) { u8 *tmp_encoded_bytes = BB_ReadBytesRaw(&msg_br, tmp_encoded_len); if (!tmp_encoded_bytes) break; BB_Buff decoder_bb = BB_BuffFromString(STRING(tmp_encoded_len, tmp_encoded_bytes)); BB_Reader decoder_br = BB_ReaderFromBuff(&decoder_bb); u64 base_tick = BB_ReadUV(&decoder_br); u64 tick = BB_ReadUV(&decoder_br); String tmp_encoded = ZI; tmp_encoded.len = BB_NumBytesRemaining(&decoder_br); tmp_encoded.text = BB_ReadBytesRaw(&decoder_br, tmp_encoded.len); if (!tmp_encoded.text) tmp_encoded.len = 0; Snapshot *base_ss = SnapshotFromTick(client, base_tick); if (base_ss->tick == base_tick) { if (is_master) { /* Queue incoming slave client snapshot for decoding */ //b32 should_decode = tick == client->highest_received_tick + 1 || client->highest_received_tick == 0; b32 should_decode = tick > client->highest_received_tick; if (should_decode) { DecodeQueueNode *node = PushStruct(scratch.arena, DecodeQueueNode); node->client = client; node->tick = tick; node->base_tick = base_tick; node->tmp_encoded = tmp_encoded; if (queue.last) { queue.last->next = node; } else { queue.first = node; } queue.last = node; if (tick > client->highest_received_tick) { client->highest_received_tick = tick; } } } else { /* Decode incoming master client snapshots for decoding (only the newest one) */ b32 should_decode = client == master_client && tick > client->highest_received_tick; if (should_decode) { DecodeQueueNode *node = queue.first ? queue.first : PushStruct(scratch.arena, DecodeQueueNode); node->client = client; node->tick = tick; node->base_tick = base_tick; node->tmp_encoded = tmp_encoded; queue.first = node; queue.last = node; if (tick > client->highest_received_tick) { client->highest_received_tick = tick; if (average_master_receive_dt_ns == 0) { average_master_receive_dt_ns = NsFromSeconds(1) / SIM_TICKS_PER_SECOND; } else { average_master_receive_dt_ns -= average_master_receive_dt_ns / 50; average_master_receive_dt_ns += (real_time_ns - last_tick_from_master_received_at_ns) / 50; } last_tick_from_master_received_at_ns = real_time_ns; } } } } else { /* We do not have the tick that the incoming delta is based from */ Assert(0); } tmp_encoded_len = BB_ReadUV(&msg_br); } } } break; default: break; } } } /* Decode incoming snapshots */ for (DecodeQueueNode *n = queue.first; n; n = n->next) { Client *client = n->client; u64 base_tick = n->base_tick; u64 tick = n->tick; Snapshot *base_ss = SnapshotFromTick(client, base_tick); if (base_ss->tick == base_tick) { BB_Buff bb = BB_BuffFromString(n->tmp_encoded); BB_Reader br = BB_ReaderFromBuff(&bb); /* Acquire & decode snapshot */ Snapshot *ss = AcquireSnapshot(client, base_ss, tick); DecodeSnapshot(&br, ss); /* Assume all incoming ents want to be sync srcs */ for (u64 i = 0; i < ss->num_ents_reserved; ++i) { Entity *ent = &ss->ents[i]; if (ent->valid && HasProp(ent, Prop_SyncDst)) { DisableProp(ent, Prop_SyncDst); EnableProp(ent, Prop_SyncSrc); } } } else { /* We do not have the tick that the incoming delta is based from. * This decode should never have been queued in the first place. */ Assert(0); } } if (!is_master && !initialized_from_master) { if (master_client->valid && master_client->last_tick > 0) { initialized_from_master = 1; } else { goto skip_step; } } b32 should_step = !Atomic32Fetch(&g->user_paused); if (Atomic32Fetch(&g->user_paused_steps) > 0) { should_step = 1; Atomic32FetchAdd(&g->user_paused_steps, -1); } if (!should_step) { goto skip_step; } /* Update networked clients */ u64 oldest_client_ack = 0; for (u64 i = 0; i < store->num_clients_reserved; ++i) { Client *client = &store->clients[i]; if (client->valid && client != local_client && client != publish_client && client != user_input_client && client != master_client) { client->last_rtt_ns = N_GetChannelLastRttNs(host, client->channel_id); /* Release unneeded received snapshots */ /* TDOO: Cap how many client snapshots we're willing to retain */ if (client->double_ack > 0) { u64 keep_tick = MinU64(client->double_ack, local_client->last_tick); if (keep_tick > 0) { ReleaseSnapshotsInRange(client, 0, keep_tick - 1); } } if (client->ack < oldest_client_ack || oldest_client_ack == 0) { oldest_client_ack = client->ack; } } } /* Release unneeded published snapshots */ { u64 keep_tick = oldest_client_ack; if (keep_tick == 0 && publish_client->last_tick > 0) { keep_tick = publish_client->last_tick - 1; } if (keep_tick > 0) { --keep_tick; } ReleaseSnapshotsInRange(publish_client, 0, keep_tick); } /* Release old local snapshots */ { u64 keep_range = 50; if (local_client->last_tick > keep_range) { u64 keep_tick = local_client->last_tick - keep_range; ReleaseSnapshotsInRange(local_client, 0, keep_tick); } } /* Release unneeded user input snapshots */ ReleaseSnapshotsInRange(user_input_client, 0, local_client->first_tick - 1); if (is_master) { /* Step master */ u64 prev_tick = local_client->last_tick; u64 next_tick = prev_tick + 1; SimStepCtx ctx = ZI; ctx.is_master = is_master; ctx.sim_dt_ns = step_dt_ns; ctx.accel = &accel; ctx.user_input_client = user_input_client; ctx.master_client = master_client; ctx.publish_client = publish_client; Snapshot *prev_world = SnapshotFromTick(local_client, prev_tick); ctx.world = AcquireSnapshot(local_client, prev_world, next_tick); GenerateuserInputCmds(user_input_client, next_tick); StepSim(&ctx); } else if (master_client->valid) { /* Step client */ /* TODO: Eventually determine master tick based on a delay to allow for jitter and also interpolation so we can lower snapshot publish frequency */ b32 master_ss_is_blended = 0; Snapshot *master_ss = NilSnapshot(); { /* How along are we between master sim ticks (0 = start of tick, 1 = end of tick) */ f64 tick_progress = 0; i64 next_tick_expected_ns = last_tick_from_master_received_at_ns + average_master_receive_dt_ns; if (next_tick_expected_ns > last_tick_from_master_received_at_ns) { tick_progress = (f64)(real_time_ns - last_tick_from_master_received_at_ns) / (f64)(next_tick_expected_ns - last_tick_from_master_received_at_ns); } /* Predict master sim time based on average snapshot publish dt. */ Snapshot *newest_snapshot = SnapshotFromTick(master_client, master_client->last_tick); i64 master_sim_predicted_time_ns = newest_snapshot->sim_time_ns + (newest_snapshot->sim_dt_ns * tick_progress); /* Determine blend time */ i64 master_blend_time_target_ns = master_sim_predicted_time_ns - (SIM_CLIENT_INTERP_RATIO * average_master_receive_dt_ns); if (average_master_receive_dt_ns > 0) { master_blend_time_ns += real_dt_ns; } i64 blend_time_target_diff_ns = master_blend_time_target_ns - master_blend_time_ns; if (blend_time_target_diff_ns > NsFromSeconds(0.100) || blend_time_target_diff_ns < NsFromSeconds(-0.100)) { /* Snap blend time if it gets too far from target blend time */ master_blend_time_ns = master_blend_time_target_ns; } u64 master_blend_tick = master_blend_time_ns / newest_snapshot->sim_dt_ns; /* Get snapshot nearest to master blend time */ /* TODO: Blend */ Snapshot *left_snapshot = NilSnapshot(); Snapshot *right_snapshot = newest_snapshot; { Snapshot *ss = SnapshotFromTick(master_client, master_client->first_tick); while (ss->valid) { u64 next_tick = ss->next_tick; i64 ss_time_ns = ss->sim_time_ns; if (ss_time_ns < master_blend_time_ns && ss_time_ns > left_snapshot->sim_time_ns) { left_snapshot = ss; } if (ss_time_ns > master_blend_time_ns && ss_time_ns < right_snapshot->sim_time_ns) { right_snapshot = ss; } ss = SnapshotFromTick(master_client, next_tick); } } /* Create world from blended master snapshots */ f64 blend = 0; if (left_snapshot->valid && right_snapshot->valid && right_snapshot->tick > left_snapshot->tick) { blend = (f64)(master_blend_tick - left_snapshot->tick) / (f64)(right_snapshot->tick - left_snapshot->tick); f64 epsilon = 0.001; if (blend < epsilon) { master_ss_is_blended = 0; master_ss = left_snapshot; } else if (blend > 1 - epsilon) { master_ss_is_blended = 0; master_ss = right_snapshot; } else { master_ss_is_blended = 1; master_ss = AcquireSnapshotFromLerp(master_blended_client, left_snapshot, right_snapshot, blend); /* Release unneeded blended master snapshots */ if (master_ss->tick > 0) { ReleaseSnapshotsInRange(master_blended_client, 0, master_ss->tick - 1); ReleaseSnapshotsInRange(master_blended_client, master_ss->tick + 1, U64Max); } } } else { master_ss_is_blended = 0; master_ss = left_snapshot->valid ? left_snapshot : right_snapshot; } /* Release unneeded master snapshots */ u64 keep_master_tick = MinU64(left_snapshot->tick, master_client->double_ack); if (keep_master_tick > 0) { ReleaseSnapshotsInRange(master_client, 0, keep_master_tick - 1); } #if 0 DEBUGBREAKABLE; P_LogDebugF("*************************************************"); P_LogDebugF("local_client->last_tick: %F", FmtUint(local_client->last_tick)); P_LogDebugF("master_sim_predicted_time_ns: %F", FmtSint(master_sim_predicted_time_ns)); P_LogDebugF("tick_progress: %F", FmtFloat(tick_progress)); P_LogDebugF("sim_publish_timescale: %F", FmtFloat(sim_publish_timescale)); P_LogDebugF("last_tick_from_master_received_at_ns: %F", FmtSint(last_tick_from_master_received_at_ns)); P_LogDebugF("average_master_receive_dt_ns: %F", FmtSint(average_master_receive_dt_ns)); P_LogDebugF("next_tick_expected_ns: %F", FmtSint(next_tick_expected_ns)); P_LogDebugF("master_blend_time_target_ns: %F", FmtSint(master_blend_time_target_ns)); P_LogDebugF("blend_time_target_diff_ns: %F", FmtSint(blend_time_target_diff_ns)); P_LogDebugF("master_blend_time_ns: %F", FmtSint(master_blend_time_ns)); P_LogDebugF("left_snapshot->tick: %F", FmtUint(left_snapshot->tick)); P_LogDebugF("right_snapshot->tick: %F", FmtUint(right_snapshot->tick)); P_LogDebugF("master_ss->tick: %F", FmtUint(master_ss->tick)); #endif } if (master_ss->valid) { Entity *master_player = FirstWithProp(master_ss, Prop_IsMaster); /* Update ent id from master */ { user_input_client->player_id = master_ss->local_player; local_client->player_id = master_ss->local_player; } /* Check for misprediction */ u64 mispredicted_tick = 0; if (!master_ss_is_blended) { /* TODO: Actually check for misprediction rather than triggering mispredict any time a new master snapshot is received */ mispredicted_tick = master_ss->tick; } u64 step_base_tick = local_client->last_tick; u64 step_end_tick = step_base_tick + 1; if (mispredicted_tick > 0) { step_base_tick = mispredicted_tick; if (step_end_tick <= step_base_tick) { step_end_tick = step_base_tick + 1; } } /* We want to simulate the ahead of the server to predict client input. * How many ticks ahead we want to simulate is a balance between added latency and the server not receiving our inputs on time. * We can take the server's ack minus the server's tick to determine how many cmds of ours the server has buffered. * * If this buffer gets too low (because we are lagging behind or the connection is unstable), meaning the server is not getting our input on time: * - Shorten local compute rate to increase the rate at which we predict ahead & produce cmds, until the server's ack indicates a buffer size within desired range. * * If this buffer gets too large (because the client predicts too far ahead), meaning unneeded latency is being introduced: * - Dilate local compute rate to decrease the rate at which we predict ahead & produce cmds until the server's ack indicates a buffer size within desired range. */ { i64 cmds_ahead_on_master = (i64)master_client->ack - (i64)master_client->last_tick; if (cmds_ahead_on_master < -3 || cmds_ahead_on_master > 10) { /* Cmds are too far from master time, snap step end tick */ i64 rtt_ns = master_client->last_rtt_ns; f64 rtt_tick_ratio = (f64)(rtt_ns + (step_dt_ns - 1)) / (f64)step_dt_ns; i64 num_predict_ticks = RoundF64ToI64(rtt_tick_ratio) + 5; step_end_tick = master_client->last_tick + num_predict_ticks; compute_timescale = 1.1; } else if (cmds_ahead_on_master > 2) { /* Slow down simulation to dial back how far ahead we are predicting and bring local sim time closer to master sim time */ compute_timescale = 1.1; } else if (cmds_ahead_on_master < 1) { /* Speed up simulation rate predict more ticks and give master more inputs to work with */ compute_timescale = 0.9; } else { /* Server's cmd buffer is in a healthy range */ compute_timescale = 1; } } /* Sync master with local base tick */ Snapshot *base_ss = SnapshotFromTick(local_client, step_base_tick); if (mispredicted_tick) { if (base_ss->valid) { SyncSnapshotEntities(base_ss, master_ss, master_player->id, 0); } else { base_ss = AcquireSnapshot(local_client, master_ss, step_base_tick); } } /* Release any existing ticks that are about to be simulated */ ReleaseSnapshotsInRange(local_client, step_base_tick + 1, U64Max); /* Step */ GenerateuserInputCmds(user_input_client, step_end_tick); { SimStepCtx ctx = ZI; ctx.is_master = is_master; ctx.sim_dt_ns = step_dt_ns; ctx.accel = &accel; ctx.user_input_client = user_input_client; ctx.master_client = master_client; ctx.publish_client = publish_client; u64 step_tick = step_base_tick + 1; Snapshot *prev_ss = base_ss; while (step_tick <= step_end_tick) { ctx.world = AcquireSnapshot(local_client, prev_ss, step_tick); if (!mispredicted_tick && step_tick == step_end_tick) { SyncSnapshotEntities(ctx.world, master_ss, master_player->id, SyncFlag_NoSyncPredictables); } StepSim(&ctx); prev_ss = ctx.world; ++step_tick; } } } } /* Publish snapshot to remote clients */ for (u64 i = 0; i < store->num_clients_reserved; ++i) { Client *client = &store->clients[i]; if (client->valid && client != user_input_client && client != local_client && client != publish_client) { BB_Writer msg_bw = BB_WriterFromBuff(&msg_writer_bb); BB_WriteUV(&msg_bw, client->highest_received_tick); /* ack */ BB_WriteUV(&msg_bw, client->ack); /* double ack */ Snapshot *base_ss = SnapshotFromTick(publish_client, client->ack); Snapshot *publish_ss; if (client == master_client) { /* If sending to master, start sending all snapshots since last ack */ publish_ss = SnapshotFromClosestTickGte(publish_client, base_ss->tick + 1); } else { /* If sending to slave, only send latest snapshot */ publish_ss = SnapshotFromTick(publish_client, publish_client->last_tick); } while (publish_ss->valid) { BB_Writer snapshot_bw = BB_WriterFromBuff(&snapshot_writer_bb); String tmp_snapshot_encoded = ZI; { BB_WriteUV(&snapshot_bw, base_ss->tick); BB_WriteUV(&snapshot_bw, publish_ss->tick); EncodeSnapshot(&snapshot_bw, client, base_ss, publish_ss); tmp_snapshot_encoded.len = BB_GetNumBytesWritten(&snapshot_bw); tmp_snapshot_encoded.text = BB_GetWrittenRaw(&snapshot_bw); } BB_WriteUV(&msg_bw, tmp_snapshot_encoded.len); BB_WriteBytes(&msg_bw, tmp_snapshot_encoded); publish_ss = SnapshotFromTick(publish_client, publish_ss->tick + 1); } BB_WriteUV(&msg_bw, 0); String encoded = ZI; encoded.len = BB_GetNumBytesWritten(&msg_bw); encoded.text = BB_GetWrittenRaw(&msg_bw); N_Write(host, client->channel_id, encoded, 0); } } /* Copy local snapshot to user client */ { Snapshot *local_ss = SnapshotFromTick(local_client, local_client->last_tick); if (local_ss->valid) { /* TODO: Double buffer */ Lock lock = LockE(&g->local_to_user_client_mutex); AcquireSnapshot(g->local_to_user_client, local_ss, local_ss->tick); i64 publish_ns = TimeNs(); if (last_publish_to_user_ns == 0) { last_publish_to_user_ns = publish_ns - g->average_local_to_user_snapshot_publish_dt_ns; } g->local_to_user_client_publish_dt_ns = publish_ns - last_publish_to_user_ns; g->local_to_user_client_publish_time_ns = publish_ns; last_publish_to_user_ns = publish_ns; ReleaseSnapshotsInRange(g->local_to_user_client, 0, local_ss->tick - 1); Unlock(&lock); } } skip_step: /* Send host messages */ N_EndUpdate(host); __profframe("Local sim"); EndScratch(scratch); } } ReleaseClientStore(store); ReleaseSimAccel(&accel); BB_ReleaseBuff(&snapshot_writer_bb); BB_ReleaseBuff(&msg_writer_bb); N_ReleaseHost(host); }