ADD: callback function

This commit is contained in:
2025-12-22 00:19:39 +09:00
parent 79f3a7f0f9
commit c85c0d790d
9 changed files with 760 additions and 128 deletions

View File

@@ -114,6 +114,12 @@ add_executable (vulkan_engine
# compute
compute/vk_compute.h
compute/vk_compute.cpp
# runtime
runtime/i_game_callbacks.h
runtime/time_manager.h
runtime/time_manager.cpp
runtime/game_runtime.h
runtime/game_runtime.cpp
)
set_property(TARGET vulkan_engine PROPERTY CXX_STANDARD 20)
@@ -270,6 +276,7 @@ target_include_directories(vulkan_engine PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/render/graph"
"${CMAKE_CURRENT_SOURCE_DIR}/scene"
"${CMAKE_CURRENT_SOURCE_DIR}/compute"
"${CMAKE_CURRENT_SOURCE_DIR}/runtime"
)
option(ENABLE_MIKKTS "Use MikkTSpace for tangent generation" ON)

View File

@@ -594,61 +594,9 @@ void VulkanEngine::init_default_data()
sphereMesh = _assetManager->createMesh(ci);
}
// Register default primitives as dynamic scene instances
if (_sceneManager)
{
_sceneManager->addMeshInstance("default.cube", cubeMesh,
glm::translate(glm::mat4(1.f), glm::vec3(-2.f, 0.f, -2.f)));
_sceneManager->addMeshInstance("default.sphere", sphereMesh,
glm::translate(glm::mat4(1.f), glm::vec3(2.f, 0.f, -2.f)),
BoundsType::Sphere);
}
// Test textured primitives
{
AssetManager::MeshMaterialDesc matDesc;
matDesc.kind = AssetManager::MeshMaterialDesc::Kind::Textured;
matDesc.options.albedoPath = "textures/grass_albedo.png";
matDesc.options.normalPath = "textures/grass_normal.png";
matDesc.options.metalRoughPath = "textures/grass_mro.png";
matDesc.options.occlusionPath = "textures/grass_ao.png";
addPrimitiveInstance("textured.cube",
AssetManager::MeshGeometryDesc::Type::Cube,
glm::translate(glm::mat4(1.f), glm::vec3(0.f, 1.f, -4.f)),
matDesc);
addPrimitiveInstance("textured.sphere",
AssetManager::MeshGeometryDesc::Type::Sphere,
glm::translate(glm::mat4(1.f), glm::vec3(3.f, 1.f, -4.f)),
matDesc);
addPrimitiveInstance("textured.plane",
AssetManager::MeshGeometryDesc::Type::Plane,
glm::scale(glm::translate(glm::mat4(1.f), glm::vec3(0.f, 0.f, -6.f)), glm::vec3(4.f)),
matDesc);
}
if (addGLTFInstance("mirage", "mirage2000/scene.gltf", glm::mat4(1.0f)))
{
preloadInstanceTextures("mirage");
}
// Windmill animation test
{
glm::mat4 windmillTransform = glm::translate(glm::mat4(1.0f), glm::vec3(10.0f, 0.0f, 0.0f));
windmillTransform = glm::scale(windmillTransform, glm::vec3(0.5f));
if (addGLTFInstance("windmill", "windmill/scene.gltf", windmillTransform))
{
preloadInstanceTextures("windmill");
// Enable first animation (index 0) with looping
if (_sceneManager)
{
_sceneManager->setGLTFInstanceAnimation("windmill", 0, true);
_sceneManager->setGLTFInstanceAnimationLoop("windmill", true);
}
}
}
// Note: Default primitive meshes (cubeMesh, sphereMesh) are created above
// but no longer spawned as scene instances. Use GameRuntime callbacks
// or GameAPI to add objects to the scene at runtime.
_mainDeletionQueue.push_function([&]() {
_resourceManager->destroy_image(_whiteImage);

View File

@@ -1840,80 +1840,171 @@ namespace
}
} // namespace
// Window visibility states for menu-bar toggles
namespace
{
struct DebugWindowStates
{
bool show_overview{false};
bool show_window{false};
bool show_background{false};
bool show_particles{false};
bool show_shadows{false};
bool show_render_graph{false};
bool show_pipelines{false};
bool show_ibl{false};
bool show_postfx{false};
bool show_scene{false};
bool show_async_assets{false};
bool show_textures{false};
};
static DebugWindowStates g_debug_windows;
} // namespace
void vk_engine_draw_debug_ui(VulkanEngine *eng)
{
if (!eng) return;
ImGuizmo::BeginFrame();
// Consolidated debug window with tabs
if (ImGui::Begin("Debug"))
// Main menu bar at the top
if (ImGui::BeginMainMenuBar())
{
const ImGuiTabBarFlags tf =
ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_AutoSelectNewTabs;
if (ImGui::BeginTabBar("DebugTabs", tf))
if (ImGui::BeginMenu("View"))
{
if (ImGui::BeginTabItem("Overview"))
ImGui::MenuItem("Overview", nullptr, &g_debug_windows.show_overview);
ImGui::MenuItem("Window", nullptr, &g_debug_windows.show_window);
ImGui::Separator();
ImGui::MenuItem("Scene", nullptr, &g_debug_windows.show_scene);
ImGui::MenuItem("Render Graph", nullptr, &g_debug_windows.show_render_graph);
ImGui::MenuItem("Pipelines", nullptr, &g_debug_windows.show_pipelines);
ImGui::Separator();
ImGui::MenuItem("Shadows", nullptr, &g_debug_windows.show_shadows);
ImGui::MenuItem("IBL", nullptr, &g_debug_windows.show_ibl);
ImGui::MenuItem("PostFX", nullptr, &g_debug_windows.show_postfx);
ImGui::MenuItem("Background", nullptr, &g_debug_windows.show_background);
ImGui::Separator();
ImGui::MenuItem("Particles", nullptr, &g_debug_windows.show_particles);
ImGui::MenuItem("Textures", nullptr, &g_debug_windows.show_textures);
ImGui::MenuItem("Async Assets", nullptr, &g_debug_windows.show_async_assets);
ImGui::EndMenu();
}
// Quick stats in menu bar
ImGui::Separator();
ImGui::Text("%.1f ms | %d tris | %d draws",
eng->stats.frametime,
eng->stats.triangle_count,
eng->stats.drawcall_count);
ImGui::EndMainMenuBar();
}
// Individual debug windows (only shown when toggled)
if (g_debug_windows.show_overview)
{
if (ImGui::Begin("Overview", &g_debug_windows.show_overview))
{
ui_overview(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Window"))
ImGui::End();
}
if (g_debug_windows.show_window)
{
if (ImGui::Begin("Window Settings", &g_debug_windows.show_window))
{
ui_window(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Background"))
ImGui::End();
}
if (g_debug_windows.show_background)
{
if (ImGui::Begin("Background", &g_debug_windows.show_background))
{
ui_background(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Particles"))
ImGui::End();
}
if (g_debug_windows.show_particles)
{
if (ImGui::Begin("Particles", &g_debug_windows.show_particles))
{
ui_particles(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Shadows"))
ImGui::End();
}
if (g_debug_windows.show_shadows)
{
if (ImGui::Begin("Shadows", &g_debug_windows.show_shadows))
{
ui_shadows(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Render Graph"))
ImGui::End();
}
if (g_debug_windows.show_render_graph)
{
if (ImGui::Begin("Render Graph", &g_debug_windows.show_render_graph))
{
ui_render_graph(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Pipelines"))
ImGui::End();
}
if (g_debug_windows.show_pipelines)
{
if (ImGui::Begin("Pipelines", &g_debug_windows.show_pipelines))
{
ui_pipelines(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("IBL"))
ImGui::End();
}
if (g_debug_windows.show_ibl)
{
if (ImGui::Begin("IBL", &g_debug_windows.show_ibl))
{
ui_ibl(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("PostFX"))
ImGui::End();
}
if (g_debug_windows.show_postfx)
{
if (ImGui::Begin("PostFX", &g_debug_windows.show_postfx))
{
ui_postfx(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Scene"))
ImGui::End();
}
if (g_debug_windows.show_scene)
{
if (ImGui::Begin("Scene", &g_debug_windows.show_scene))
{
ui_scene(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Async Assets"))
ImGui::End();
}
if (g_debug_windows.show_async_assets)
{
if (ImGui::Begin("Async Assets", &g_debug_windows.show_async_assets))
{
ui_async_assets(eng);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Textures"))
ImGui::End();
}
if (g_debug_windows.show_textures)
{
if (ImGui::Begin("Textures", &g_debug_windows.show_textures))
{
ui_textures(eng);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::End();
}

View File

@@ -1,14 +1,86 @@
// Main entry point for Vulkan Engine
//
// Two modes are available:
// 1. Legacy mode: Uses VulkanEngine::run() directly (simple, no game separation)
// 2. GameRuntime mode: Uses GameRuntime for clean game/engine separation
//
// Set USE_GAME_RUNTIME to 1 to enable GameRuntime with example callbacks.
#define USE_GAME_RUNTIME 1
#include "core/engine.h"
#if USE_GAME_RUNTIME
#include "runtime/game_runtime.h"
#include <glm/gtx/transform.hpp>
// Example game implementation using IGameCallbacks
class ExampleGame : public GameRuntime::IGameCallbacks
{
public:
void on_init(GameRuntime::Runtime& runtime) override
{
// Example: Set up initial scene
auto& api = runtime.api();
// Load a glTF model asynchronously
// api.load_gltf_async("example_model", "models/example.gltf",
// GameAPI::Transform{}.with_position({0, 0, 0}));
// Spawn a primitive
// api.spawn_mesh_instance("test_cube", api.primitive_cube(),
// GameAPI::Transform{}.with_position({2, 0, 0}));
// Set up camera
// api.set_camera_position({0, 5, -10});
// api.set_camera_rotation({-20, 0, 0});
}
void on_update(float dt) override
{
// Called every frame with variable delta time
// Use for rendering-dependent logic, input handling, camera control
_elapsed += dt;
}
void on_fixed_update(float fixed_dt) override
{
// Called at fixed intervals (default: 60Hz)
// Use for physics updates, AI tick, game state simulation
// Example: Apply physics forces, update AI state machines
}
void on_shutdown() override
{
// Called before engine shutdown
// Use for cleanup, saving game state, etc.
}
private:
float _elapsed{0.0f};
};
#endif // USE_GAME_RUNTIME
int main(int argc, char* argv[])
{
VulkanEngine engine;
(void)argc;
(void)argv;
VulkanEngine engine;
engine.init();
#if USE_GAME_RUNTIME
// GameRuntime mode: clean separation between engine and game logic
{
GameRuntime::Runtime runtime(&engine);
ExampleGame game;
runtime.run(&game);
}
#else
// Legacy mode: simple direct engine loop
engine.run();
#endif
engine.cleanup();
return 0;
}

View File

@@ -0,0 +1,211 @@
#include "game_runtime.h"
#include "core/engine.h"
#include "core/input/input_system.h"
#include "core/ui/imgui_system.h"
#include "scene/vk_scene.h"
#include "SDL2/SDL.h"
#include <thread>
#include <chrono>
namespace GameRuntime
{
Runtime::Runtime(VulkanEngine *renderer)
: _renderer(renderer)
{
_api = std::make_unique<GameAPI::Engine>(renderer);
}
Runtime::~Runtime() = default;
void Runtime::set_physics_world(IPhysicsWorld *physics)
{
_physics = physics;
}
void Runtime::set_audio_system(IAudioSystem *audio)
{
_audio = audio;
}
void Runtime::sync_physics_to_render()
{
// TODO: When physics integration is added, sync physics body transforms
// to their corresponding render instances here.
// For each physics body with a render instance:
// glm::mat4 transform;
// _physics->get_body_transform(bodyId, transform);
// _api->set_mesh_instance_transform(instanceName, GameAPI::Transform::from_matrix(transform));
}
void Runtime::update_audio_listener()
{
if (!_audio || !_renderer || !_renderer->_sceneManager)
{
return;
}
auto &cam = _renderer->_sceneManager->getMainCamera();
glm::vec3 pos = _renderer->_sceneManager->get_camera_local_position();
// Calculate forward and up from camera orientation quaternion
glm::vec3 forward = glm::normalize(cam.orientation * glm::vec3(0.0f, 0.0f, -1.0f));
glm::vec3 up = glm::normalize(cam.orientation * glm::vec3(0.0f, 1.0f, 0.0f));
_audio->set_listener(pos, forward, up);
}
void Runtime::run(IGameCallbacks *game)
{
if (!game || !_renderer)
{
return;
}
_quit_requested = false;
// Call game initialization
game->on_init(*this);
// Main game loop
while (!_quit_requested)
{
// --- Begin frame: time, input ---
_time.begin_frame();
InputSystem *input = _renderer->input();
if (input)
{
input->begin_frame();
input->pump_events();
if (input->quit_requested())
{
_quit_requested = true;
}
_renderer->freeze_rendering = input->window_minimized();
if (input->resize_requested())
{
_renderer->resize_requested = true;
input->clear_resize_request();
}
}
// --- Process UI and input capture ---
const bool ui_capture_mouse = _renderer->ui() && _renderer->ui()->want_capture_mouse();
const bool ui_capture_keyboard = _renderer->ui() && _renderer->ui()->want_capture_keyboard();
// Dispatch native events to UI and picking
if (input)
{
struct DispatchCtx
{
VulkanEngine *engine;
bool ui_capture_mouse;
} ctx{_renderer, ui_capture_mouse};
input->for_each_native_event([](void *user, InputSystem::NativeEventView view) {
auto *c = static_cast<DispatchCtx *>(user);
if (!c || !c->engine || view.backend != InputSystem::NativeBackend::SDL2 || view.data == nullptr)
{
return;
}
const SDL_Event &e = *static_cast<const SDL_Event *>(view.data);
if (c->engine->ui())
{
c->engine->ui()->process_event(e);
}
if (c->engine->picking())
{
c->engine->picking()->process_event(e, c->ui_capture_mouse);
}
}, &ctx);
}
// --- Camera input (if not captured by UI) ---
if (_renderer->_sceneManager && input)
{
_renderer->_sceneManager->getMainCamera().process_input(*input, ui_capture_keyboard, ui_capture_mouse);
}
// --- Throttle when minimized ---
if (_renderer->freeze_rendering)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue;
}
// --- Handle resize ---
if (_renderer->resize_requested)
{
if (_renderer->_swapchainManager)
{
_renderer->_swapchainManager->resize_swapchain(_renderer->_window);
if (_renderer->ui())
{
_renderer->ui()->on_swapchain_recreated();
}
_renderer->resize_requested = false;
}
}
// --- Fixed update loop (physics) ---
while (_time.consume_fixed_step())
{
game->on_fixed_update(_time.fixed_delta_time());
if (_physics)
{
_physics->step(_time.fixed_delta_time());
}
}
// --- Sync physics transforms to render ---
sync_physics_to_render();
// --- Variable update ---
game->on_update(_time.delta_time());
// --- Audio listener update ---
update_audio_listener();
if (_audio)
{
_audio->update();
}
// --- Wait for GPU and prepare frame ---
VK_CHECK(vkWaitForFences(_renderer->_deviceManager->device(), 1,
&_renderer->get_current_frame()._renderFence, true, 1000000000));
if (_renderer->_rayManager)
{
_renderer->_rayManager->flushPendingDeletes();
_renderer->_rayManager->pump_blas_builds(1);
}
// --- Flush per-frame resources ---
_renderer->get_current_frame()._deletionQueue.flush();
if (_renderer->_renderGraph)
{
_renderer->_renderGraph->resolve_timings();
}
_renderer->get_current_frame()._frameDescriptors.clear_pools(_renderer->_deviceManager->device());
// --- ImGui ---
if (_renderer->ui())
{
_renderer->ui()->begin_frame();
_renderer->ui()->end_frame();
}
// --- Draw ---
_renderer->draw();
}
// Call game shutdown
game->on_shutdown();
}
} // namespace GameRuntime

145
src/runtime/game_runtime.h Normal file
View File

@@ -0,0 +1,145 @@
#pragma once
// GameRuntime: High-level game loop manager
// Provides a clean separation between engine and game logic with proper
// time management, fixed timestep for physics, and game callbacks.
#include "i_game_callbacks.h"
#include "time_manager.h"
#include "core/game_api.h"
#include <memory>
class VulkanEngine;
namespace GameRuntime
{
// Forward declarations for future integrations
class IPhysicsWorld;
class IAudioSystem;
class Runtime
{
public:
explicit Runtime(VulkanEngine* renderer);
~Runtime();
// Non-copyable
Runtime(const Runtime&) = delete;
Runtime& operator=(const Runtime&) = delete;
// ------------------------------------------------------------------------
// External System Integration (optional)
// ------------------------------------------------------------------------
// Set physics world (e.g., Jolt, Bullet, PhysX wrapper)
void set_physics_world(IPhysicsWorld* physics);
IPhysicsWorld* physics() const { return _physics; }
// Set audio system (e.g., FMOD, OpenAL wrapper)
void set_audio_system(IAudioSystem* audio);
IAudioSystem* audio() const { return _audio; }
// ------------------------------------------------------------------------
// Time Management
// ------------------------------------------------------------------------
// Get time manager for direct access
TimeManager& time() { return _time; }
const TimeManager& time() const { return _time; }
// Convenience accessors
float delta_time() const { return _time.delta_time(); }
float fixed_delta_time() const { return _time.fixed_delta_time(); }
float time_scale() const { return _time.time_scale(); }
void set_time_scale(float scale) { _time.set_time_scale(scale); }
void set_fixed_delta_time(float dt) { _time.set_fixed_delta_time(dt); }
// ------------------------------------------------------------------------
// Game API Access
// ------------------------------------------------------------------------
// Get the high-level game API for engine interaction
GameAPI::Engine& api() { return *_api; }
const GameAPI::Engine& api() const { return *_api; }
// Get the underlying Vulkan engine (for advanced use)
VulkanEngine* renderer() const { return _renderer; }
// ------------------------------------------------------------------------
// Main Loop
// ------------------------------------------------------------------------
// Run the game loop with the given callback handler.
// Blocks until the game exits.
void run(IGameCallbacks* game);
// Request quit (sets quit flag, loop will exit next frame)
void request_quit() { _quit_requested = true; }
// Check if quit was requested
bool quit_requested() const { return _quit_requested; }
private:
VulkanEngine* _renderer{nullptr};
std::unique_ptr<GameAPI::Engine> _api;
TimeManager _time;
IPhysicsWorld* _physics{nullptr};
IAudioSystem* _audio{nullptr};
bool _quit_requested{false};
// Internal helpers
void sync_physics_to_render();
void update_audio_listener();
};
// ============================================================================
// Physics Interface (for future integration)
// ============================================================================
class IPhysicsWorld
{
public:
virtual ~IPhysicsWorld() = default;
// Step the physics simulation by dt seconds
virtual void step(float dt) = 0;
// Get transform of a physics body by ID
virtual void get_body_transform(uint32_t id, glm::mat4& out) = 0;
// Raycast into the physics world
struct RayHit
{
bool hit{false};
glm::vec3 position{0.0f};
glm::vec3 normal{0.0f};
float distance{0.0f};
uint32_t bodyId{0};
};
virtual RayHit raycast(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) = 0;
};
// ============================================================================
// Audio Interface (for future integration)
// ============================================================================
class IAudioSystem
{
public:
virtual ~IAudioSystem() = default;
// Set listener position and orientation
virtual void set_listener(const glm::vec3& position, const glm::vec3& forward, const glm::vec3& up) = 0;
// Play a 3D sound at a position
virtual void play_3d(const std::string& event, const glm::vec3& position) = 0;
// Update audio system (call once per frame)
virtual void update() = 0;
};
} // namespace GameRuntime

View File

@@ -0,0 +1,35 @@
#pragma once
// IGameCallbacks: Interface for game logic callbacks
// Implement this interface and pass to GameRuntime::run() to receive game loop events.
namespace GameRuntime
{
class Runtime;
class IGameCallbacks
{
public:
virtual ~IGameCallbacks() = default;
// Called once after runtime initialization, before the first update.
// Use this to load initial assets, spawn entities, set up the camera, etc.
virtual void on_init(Runtime& runtime) = 0;
// Called every frame with variable delta time.
// Use for rendering-dependent logic, input handling, camera control, etc.
// @param dt: Frame delta time in seconds (clamped to 0.0-0.1)
virtual void on_update(float dt) = 0;
// Called at fixed intervals for physics/simulation.
// Use for physics updates, AI tick, game state simulation, etc.
// @param fixed_dt: Fixed delta time in seconds (typically 1/60)
virtual void on_fixed_update(float fixed_dt) = 0;
// Called once before shutdown.
// Use for cleanup, saving state, etc.
virtual void on_shutdown() = 0;
};
} // namespace GameRuntime

View File

@@ -0,0 +1,52 @@
#include "time_manager.h"
#include <algorithm>
namespace GameRuntime
{
TimeManager::TimeManager()
{
_start_time = Clock::now();
_last_time = _start_time;
}
void TimeManager::begin_frame()
{
TimePoint now = Clock::now();
auto elapsed = std::chrono::duration<float>(now - _last_time);
_last_time = now;
// Clamp delta time to avoid spiral of death
_unscaled_delta_time = std::min(elapsed.count(), k_max_delta_time);
_delta_time = _unscaled_delta_time * _time_scale;
// Update total times
_total_time += _delta_time;
_unscaled_total_time += _unscaled_delta_time;
// Accumulate for fixed timestep
_fixed_accumulator += _delta_time;
++_frame_count;
}
void TimeManager::set_fixed_delta_time(float dt)
{
_fixed_delta_time = std::clamp(dt, 1.0f / 240.0f, 1.0f / 10.0f);
}
void TimeManager::set_time_scale(float scale)
{
_time_scale = std::max(0.0f, scale);
}
bool TimeManager::consume_fixed_step()
{
if (_fixed_accumulator >= _fixed_delta_time)
{
_fixed_accumulator -= _fixed_delta_time;
return true;
}
return false;
}
} // namespace GameRuntime

View File

@@ -0,0 +1,71 @@
#pragma once
// TimeManager: Manages game time, time scale, and fixed timestep accumulation.
#include <chrono>
namespace GameRuntime
{
class TimeManager
{
public:
TimeManager();
// Begin a new frame. Call at the start of each frame.
void begin_frame();
// Get delta time in seconds since last frame (scaled by time_scale, clamped).
float delta_time() const { return _delta_time; }
// Get unscaled delta time in seconds since last frame (clamped).
float unscaled_delta_time() const { return _unscaled_delta_time; }
// Get fixed delta time for physics updates.
float fixed_delta_time() const { return _fixed_delta_time; }
// Set fixed delta time for physics updates (default: 1/60).
void set_fixed_delta_time(float dt);
// Get time scale multiplier (default: 1.0).
float time_scale() const { return _time_scale; }
// Set time scale multiplier (0 = paused, 0.5 = half speed, 2 = double speed).
void set_time_scale(float scale);
// Get total elapsed time in seconds since start.
float total_time() const { return _total_time; }
// Get total unscaled elapsed time in seconds since start.
float unscaled_total_time() const { return _unscaled_total_time; }
// Get accumulated fixed time (for physics step loop).
float fixed_accumulator() const { return _fixed_accumulator; }
// Consume fixed timestep from accumulator. Returns true if step should run.
bool consume_fixed_step();
// Get frame count since start.
uint64_t frame_count() const { return _frame_count; }
private:
using Clock = std::chrono::high_resolution_clock;
using TimePoint = std::chrono::time_point<Clock>;
TimePoint _last_time;
TimePoint _start_time;
float _delta_time{0.0f};
float _unscaled_delta_time{0.0f};
float _fixed_delta_time{1.0f / 60.0f};
float _time_scale{1.0f};
float _total_time{0.0f};
float _unscaled_total_time{0.0f};
float _fixed_accumulator{0.0f};
uint64_t _frame_count{0};
static constexpr float k_max_delta_time = 0.1f; // 100ms cap
};
} // namespace GameRuntime