From 91a6d02d1fc42224287f53d0bc910002d8a5445d Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Thu, 8 Jan 2026 18:11:21 +0000 Subject: [PATCH] feat: Enhance rendering and crash recovery configurations - Added new rendering budget configurations including VRAM limits and texture dimensions in seed_runtime.json and seed_runtime_opengl.json. - Introduced crash recovery parameters such as heartbeat timeouts and memory limits in the configuration files. - Updated cube logic to utilize new camera and control settings from the configuration. - Modified bgfx graphics backend to respect new rendering budget limits and handle texture loading accordingly. - Implemented crash recovery service enhancements to utilize new configuration parameters for better resource management. - Added unit tests to validate the integration of new rendering budgets and crash recovery configurations. --- config/seed_runtime.json | 38 +++++++ config/seed_runtime_opengl.json | 75 +++++++++++++ scripts/cube_logic.lua | 74 ++++++++----- src/app/service_based_app.cpp | 3 +- src/services/impl/bgfx_graphics_backend.cpp | 77 +++++++++++--- src/services/impl/bgfx_graphics_backend.hpp | 10 +- src/services/impl/bgfx_gui_service.cpp | 28 +++++ src/services/impl/bgfx_gui_service.hpp | 1 + src/services/impl/crash_recovery_service.cpp | 58 +++++++--- src/services/impl/crash_recovery_service.hpp | 5 +- src/services/impl/json_config_service.cpp | 100 ++++++++++++++++++ src/services/impl/json_config_service.hpp | 12 +++ .../impl/json_config_writer_service.cpp | 27 +++++ src/services/interfaces/config_types.hpp | 26 +++++ src/services/interfaces/i_config_service.hpp | 12 +++ tests/test_bgfx_gui_service.cpp | 4 + tests/test_cube_script.cpp | 8 ++ 17 files changed, 499 insertions(+), 59 deletions(-) diff --git a/config/seed_runtime.json b/config/seed_runtime.json index f73333d..8ed12be 100644 --- a/config/seed_runtime.json +++ b/config/seed_runtime.json @@ -73,6 +73,28 @@ }, "scene": { "rotation_speed": 0.9, + "cube_mesh": { + "path": "models/cube.stl", + "double_sided": true + }, + "camera": { + "position": [0.0, 1.6, 10.0], + "fov": 0.78, + "near": 0.1, + "far": 50.0 + }, + "controls": { + "move_speed": 16.0, + "fly_speed": 3.0, + "jump_speed": 5.5, + "gravity": -12.0, + "max_fall_speed": -20.0, + "mouse_sensitivity": 0.0025, + "gamepad_look_speed": 2.5, + "stick_deadzone": 0.2, + "max_pitch_degrees": 85.0, + "move_forward_uses_pitch": true + }, "room": { "half_size": 15.0, "wall_thickness": 0.5, @@ -163,6 +185,22 @@ "pbr_metallic": 0.08 } }, + "budgets": { + "vram_mb": 512, + "max_texture_dim": 4096, + "gui_text_cache_entries": 256, + "gui_svg_cache_entries": 64 + }, + "crash_recovery": { + "heartbeat_timeout_ms": 5000, + "heartbeat_poll_interval_ms": 200, + "memory_limit_mb": 1024, + "gpu_hang_frame_time_multiplier": 10.0, + "max_consecutive_gpu_timeouts": 5, + "max_lua_failures": 3, + "max_file_format_errors": 2, + "max_memory_warnings": 3 + }, "gui": { "font": { "use_freetype": true, diff --git a/config/seed_runtime_opengl.json b/config/seed_runtime_opengl.json index ca948c4..f6d29b7 100644 --- a/config/seed_runtime_opengl.json +++ b/config/seed_runtime_opengl.json @@ -70,6 +70,65 @@ "gamepad_axis_action_threshold": 0.5 } }, + "scene": { + "rotation_speed": 0.9, + "cube_mesh": { + "path": "models/cube.stl", + "double_sided": true + }, + "camera": { + "position": [0.0, 1.6, 10.0], + "fov": 0.78, + "near": 0.1, + "far": 50.0 + }, + "controls": { + "move_speed": 16.0, + "fly_speed": 3.0, + "jump_speed": 5.5, + "gravity": -12.0, + "max_fall_speed": -20.0, + "mouse_sensitivity": 0.0025, + "gamepad_look_speed": 2.5, + "stick_deadzone": 0.2, + "max_pitch_degrees": 85.0, + "move_forward_uses_pitch": true + }, + "room": { + "half_size": 15.0, + "wall_thickness": 0.5, + "wall_height": 4.0, + "floor_half_thickness": 0.3, + "floor_top": 0.0, + "floor_subdivisions": 20, + "floor_color": [1.0, 1.0, 1.0], + "wall_color": [1.0, 1.0, 1.0], + "ceiling_color": [1.0, 1.0, 1.0] + }, + "physics_cube": { + "enabled": true, + "half_extents": [1.5, 1.5, 1.5], + "mass": 1.0, + "color": [0.92, 0.34, 0.28], + "kick_strength": 6.0, + "gravity": [0.0, -9.8, 0.0], + "max_sub_steps": 10 + }, + "spinning_cube": { + "enabled": true, + "scale": 1.5, + "height": 5.0, + "color": [0.75, 0.45, 0.25] + }, + "lanterns": { + "enabled": true, + "height": 8.0, + "size": 0.2, + "color": [1.0, 0.9, 0.6], + "corner_offset": 2.0, + "wall_offset": 2.0 + } + }, "rendering": { "bgfx": { "renderer": "opengl" @@ -125,6 +184,22 @@ "pbr_metallic": 0.08 } }, + "budgets": { + "vram_mb": 512, + "max_texture_dim": 4096, + "gui_text_cache_entries": 256, + "gui_svg_cache_entries": 64 + }, + "crash_recovery": { + "heartbeat_timeout_ms": 5000, + "heartbeat_poll_interval_ms": 200, + "memory_limit_mb": 1024, + "gpu_hang_frame_time_multiplier": 10.0, + "max_consecutive_gpu_timeouts": 5, + "max_lua_failures": 3, + "max_file_format_errors": 2, + "max_memory_warnings": 3 + }, "gui": { "font": { "use_freetype": true, diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index 79b4c26..f4d8bfa 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -2,8 +2,19 @@ local scene_framework = require("scene_framework") local math3d = require("math3d") local config_resolver = require("config_resolver") +local resolve_number = scene_framework.resolve_number +local resolve_boolean = scene_framework.resolve_boolean +local resolve_string = scene_framework.resolve_string +local resolve_table = scene_framework.resolve_table +local resolve_vec3 = scene_framework.resolve_vec3 + +local scene_config = resolve_table(config_resolver.resolve_scene(config)) +local cube_mesh_config = resolve_table(scene_config.cube_mesh) +local cube_mesh_path = resolve_string(cube_mesh_config.path, "models/cube.stl") +local cube_mesh_double_sided = resolve_boolean(cube_mesh_config.double_sided, true) + local cube_mesh_info = { - path = "models/cube.stl", + path = cube_mesh_path, loaded = false, vertex_count = 0, index_count = 0, @@ -63,7 +74,11 @@ local function load_cube_mesh() cube_vertices = mesh.vertices cube_indices = mesh.indices - cube_indices_double_sided = build_double_sided_indices(cube_indices) + if cube_mesh_double_sided then + cube_indices_double_sided = build_double_sided_indices(cube_indices) + else + cube_indices_double_sided = {} + end cube_mesh_info.loaded = true cube_mesh_info.vertex_count = #mesh.vertices cube_mesh_info.index_count = #mesh.indices @@ -108,11 +123,6 @@ start_music() local Gui = require("gui") local string_format = string.format -local resolve_number = scene_framework.resolve_number -local resolve_boolean = scene_framework.resolve_boolean -local resolve_table = scene_framework.resolve_table -local resolve_vec3 = scene_framework.resolve_vec3 - local function resolve_positive_int(value, fallback) local candidate = resolve_number(value, fallback) local rounded = math.floor(candidate) @@ -275,26 +285,37 @@ if cube_mesh_info.loaded then end end +local camera_config = resolve_table(scene_config.camera) +local controls_config = resolve_table(scene_config.controls) +local max_pitch_degrees = resolve_number(controls_config.max_pitch_degrees, nil) +local max_pitch = resolve_number(controls_config.max_pitch, nil) +if max_pitch_degrees then + max_pitch = math.rad(max_pitch_degrees) +end +if not max_pitch then + max_pitch = math.rad(85.0) +end + local camera = { position = {0.0, 0.0, 5.0}, - yaw = math.pi, -- Face toward -Z (center of room) - pitch = 0.0, - fov = 0.78, - near = 0.1, - far = 50.0, + yaw = resolve_number(camera_config.yaw, math.pi), + pitch = resolve_number(camera_config.pitch, 0.0), + fov = resolve_number(camera_config.fov, 0.78), + near = resolve_number(camera_config.near, 0.1), + far = resolve_number(camera_config.far, 50.0), } local controls = { - move_speed = 16.0, - fly_speed = 3.0, - jump_speed = 5.5, - gravity = -12.0, - max_fall_speed = -20.0, - mouse_sensitivity = 0.0025, - gamepad_look_speed = 2.5, - stick_deadzone = 0.2, - max_pitch = math.rad(85.0), - move_forward_uses_pitch = true, + move_speed = resolve_number(controls_config.move_speed, 16.0), + fly_speed = resolve_number(controls_config.fly_speed, 3.0), + jump_speed = resolve_number(controls_config.jump_speed, 5.5), + gravity = resolve_number(controls_config.gravity, -12.0), + max_fall_speed = resolve_number(controls_config.max_fall_speed, -20.0), + mouse_sensitivity = resolve_number(controls_config.mouse_sensitivity, 0.0025), + gamepad_look_speed = resolve_number(controls_config.gamepad_look_speed, 2.5), + stick_deadzone = resolve_number(controls_config.stick_deadzone, 0.2), + max_pitch = max_pitch, + move_forward_uses_pitch = resolve_boolean(controls_config.move_forward_uses_pitch, true), } local last_frame_time = nil @@ -310,7 +331,6 @@ local function get_time_seconds() end local movement_log_cooldown = 0.0 local world_up = {0.0, 1.0, 0.0} -local scene_config = resolve_table(config_resolver.resolve_scene(config)) local room_config = resolve_table(scene_config.room) local lantern_config = resolve_table(scene_config.lanterns) local physics_config = resolve_table(scene_config.physics_cube) @@ -385,9 +405,11 @@ local physics_state = { gravity = physics_gravity, } -camera.position[1] = 0.0 -camera.position[2] = room.floor_top + player_state.eye_height -camera.position[3] = 10.0 +camera.position = resolve_vec3(camera_config.position, { + 0.0, + room.floor_top + player_state.eye_height, + 10.0, +}) local function clamp(value, minValue, maxValue) if value < minValue then diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index b87d945..2ff4066 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -194,7 +194,8 @@ void ServiceBasedApp::RegisterServices() { // Crash recovery service (needed early for crash detection) registry_.RegisterService( - registry_.GetService()); + registry_.GetService(), + runtimeConfig_.crashRecovery); // Lifecycle service registry_.RegisterService( diff --git a/src/services/impl/bgfx_graphics_backend.cpp b/src/services/impl/bgfx_graphics_backend.cpp index 3c482fe..f9eeade 100644 --- a/src/services/impl/bgfx_graphics_backend.cpp +++ b/src/services/impl/bgfx_graphics_backend.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -284,6 +285,16 @@ BgfxGraphicsBackend::BgfxGraphicsBackend(std::shared_ptr configS "configService=" + std::string(configService_ ? "set" : "null") + ", platformService=" + std::string(platformService_ ? "set" : "null")); } + if (configService_) { + const auto& budgets = configService_->GetRenderBudgetConfig(); + textureMemoryTracker_.SetMaxBytes(budgets.vramMB * 1024 * 1024); + maxTextureDim_ = budgets.maxTextureDim; + if (logger_) { + logger_->Trace("BgfxGraphicsBackend", "BgfxGraphicsBackend", + "vramMB=" + std::to_string(budgets.vramMB) + + ", maxTextureDim=" + std::to_string(maxTextureDim_)); + } + } vertexLayout_.begin() .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) .add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Float) @@ -735,10 +746,14 @@ bgfx::ShaderHandle BgfxGraphicsBackend::CreateShader(const std::string& label, } bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string& path, - uint64_t samplerFlags) const { + uint64_t samplerFlags, + size_t* outSizeBytes) const { if (logger_) { logger_->Trace("BgfxGraphicsBackend", "LoadTextureFromFile", "path=" + path); } + if (outSizeBytes) { + *outSizeBytes = 0; + } if (!HasProcessedFrame()) { if (logger_) { logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: Attempted to load texture BEFORE first " @@ -762,6 +777,18 @@ bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string& return BGFX_INVALID_HANDLE; } + if (maxTextureDim_ > 0) { + if (width > static_cast(maxTextureDim_) || height > static_cast(maxTextureDim_)) { + if (logger_) { + logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: texture " + path + + " size (" + std::to_string(width) + "x" + std::to_string(height) + + ") exceeds config max texture dim (" + std::to_string(maxTextureDim_) + ")"); + } + stbi_image_free(pixels); + return BGFX_INVALID_HANDLE; + } + } + // Validate texture dimensions against GPU capabilities const bgfx::Caps* caps = bgfx::getCaps(); if (caps) { @@ -777,7 +804,16 @@ bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string& } } - const uint32_t size = static_cast(width * height * 4); + const size_t size = static_cast(width) * static_cast(height) * 4u; + if (size > std::numeric_limits::max()) { + if (logger_) { + logger_->Error("BgfxGraphicsBackend::LoadTextureFromFile: texture " + path + + " exceeds supported size for upload (" + std::to_string(size) + " bytes)"); + } + stbi_image_free(pixels); + return BGFX_INVALID_HANDLE; + } + const uint32_t sizeBytes = static_cast(size); // Check memory budget before allocation if (!textureMemoryTracker_.CanAllocate(size)) { @@ -791,7 +827,7 @@ bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string& return BGFX_INVALID_HANDLE; } - const bgfx::Memory* mem = bgfx::copy(pixels, size); + const bgfx::Memory* mem = bgfx::copy(pixels, sizeBytes); stbi_image_free(pixels); // Validate bgfx::copy() succeeded @@ -830,6 +866,10 @@ bgfx::TextureHandle BgfxGraphicsBackend::LoadTextureFromFile(const std::string& ", memoryMB=" + std::to_string(size / 1024 / 1024)); } + if (outSizeBytes) { + *outSizeBytes = size; + } + return handle; } @@ -955,23 +995,34 @@ GraphicsPipelineHandle BgfxGraphicsBackend::CreatePipeline(GraphicsDeviceHandle } // Try to load texture from file - binding.texture = LoadTextureFromFile(binding.sourcePath, samplerFlags); + size_t textureSizeBytes = 0; + binding.texture = LoadTextureFromFile(binding.sourcePath, samplerFlags, &textureSizeBytes); if (bgfx::isValid(binding.texture)) { - // Estimate texture memory size (assume RGBA8 format, no mipmaps for now) - // In production, should query actual texture info from bgfx - // For now, estimate based on typical 2048x2048 textures - binding.memorySizeBytes = 2048 * 2048 * 4; // Conservative estimate - textureMemoryTracker_.Allocate(binding.memorySizeBytes); + binding.memorySizeBytes = textureSizeBytes; + if (binding.memorySizeBytes > 0) { + textureMemoryTracker_.Allocate(binding.memorySizeBytes); + } } else { if (logger_) { logger_->Warn("BgfxGraphicsBackend::CreatePipeline: texture load failed for " + binding.sourcePath + ", creating fallback texture"); } // Use fallback magenta texture (1x1) - binding.texture = CreateSolidTexture(0xff00ffff, samplerFlags); - if (bgfx::isValid(binding.texture)) { - binding.memorySizeBytes = 1 * 1 * 4; // 1x1 RGBA8 - textureMemoryTracker_.Allocate(binding.memorySizeBytes); + binding.memorySizeBytes = 1 * 1 * 4; // 1x1 RGBA8 + if (!textureMemoryTracker_.CanAllocate(binding.memorySizeBytes)) { + if (logger_) { + logger_->Warn("BgfxGraphicsBackend::CreatePipeline: budget prevents fallback texture for " + + binding.sourcePath); + } + binding.texture = BGFX_INVALID_HANDLE; + binding.memorySizeBytes = 0; + } else { + binding.texture = CreateSolidTexture(0xff00ffff, samplerFlags); + if (bgfx::isValid(binding.texture)) { + textureMemoryTracker_.Allocate(binding.memorySizeBytes); + } else { + binding.memorySizeBytes = 0; + } } } diff --git a/src/services/impl/bgfx_graphics_backend.hpp b/src/services/impl/bgfx_graphics_backend.hpp index ecf0b10..ddf0cf4 100644 --- a/src/services/impl/bgfx_graphics_backend.hpp +++ b/src/services/impl/bgfx_graphics_backend.hpp @@ -61,6 +61,9 @@ private: TextureMemoryTracker() = default; bool CanAllocate(size_t bytes) const { + if (maxBytes_ == 0) { + return true; + } return (totalBytes_ + bytes) <= maxBytes_; } @@ -78,7 +81,7 @@ private: size_t GetUsedBytes() const { return totalBytes_; } size_t GetMaxBytes() const { return maxBytes_; } - size_t GetAvailableBytes() const { return maxBytes_ - totalBytes_; } + size_t GetAvailableBytes() const { return maxBytes_ == 0 ? 0 : maxBytes_ - totalBytes_; } void SetMaxBytes(size_t max) { maxBytes_ = max; } @@ -140,7 +143,9 @@ private: bgfx::ShaderHandle CreateShader(const std::string& label, const std::string& source, bool isVertex) const; - bgfx::TextureHandle LoadTextureFromFile(const std::string& path, uint64_t samplerFlags) const; + bgfx::TextureHandle LoadTextureFromFile(const std::string& path, + uint64_t samplerFlags, + size_t* outSizeBytes = nullptr) const; void InitializeUniforms(); void DestroyUniforms(); void ApplyMaterialXUniforms(const std::array& modelMatrix); @@ -168,6 +173,7 @@ private: bgfx::PlatformData platformData_{}; bool loggedInitFailureDiagnostics_ = false; mutable TextureMemoryTracker textureMemoryTracker_{}; + uint32_t maxTextureDim_ = 0; }; } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/bgfx_gui_service.cpp b/src/services/impl/bgfx_gui_service.cpp index 6e48543..7417b3a 100644 --- a/src/services/impl/bgfx_gui_service.cpp +++ b/src/services/impl/bgfx_gui_service.cpp @@ -125,6 +125,18 @@ BgfxGuiService::BgfxGuiService(std::shared_ptr configService, "configService=" + std::string(configService_ ? "set" : "null") + ", pipelineCompiler=" + std::string(pipelineCompiler_ ? "set" : "null")); } + if (configService_) { + const auto& budgets = configService_->GetRenderBudgetConfig(); + maxTextCacheEntries_ = budgets.guiTextCacheEntries; + maxSvgCacheEntries_ = budgets.guiSvgCacheEntries; + maxTextureDim_ = budgets.maxTextureDim; + if (logger_) { + logger_->Trace("BgfxGuiService", "BgfxGuiService", + "guiTextCacheEntries=" + std::to_string(maxTextCacheEntries_) + + ", guiSvgCacheEntries=" + std::to_string(maxSvgCacheEntries_) + + ", maxTextureDim=" + std::to_string(maxTextureDim_)); + } + } } BgfxGuiService::~BgfxGuiService() { @@ -967,6 +979,22 @@ bgfx::TextureHandle BgfxGuiService::CreateTexture(const uint8_t* rgba, if (!rgba || width == 0 || height == 0) { return BGFX_INVALID_HANDLE; } + uint32_t maxDim = maxTextureDim_; + if (const bgfx::Caps* caps = bgfx::getCaps()) { + if (caps->limits.maxTextureSize > 0) { + maxDim = (maxDim == 0) + ? caps->limits.maxTextureSize + : std::min(maxDim, caps->limits.maxTextureSize); + } + } + if (maxDim > 0 && (width > maxDim || height > maxDim)) { + if (logger_) { + logger_->Error("BgfxGuiService::CreateTexture: texture size (" + + std::to_string(width) + "x" + std::to_string(height) + + ") exceeds max texture dim (" + std::to_string(maxDim) + ")"); + } + return BGFX_INVALID_HANDLE; + } const uint32_t size = width * height * 4; const bgfx::Memory* mem = bgfx::copy(rgba, size); return bgfx::createTexture2D(static_cast(width), diff --git a/src/services/impl/bgfx_gui_service.hpp b/src/services/impl/bgfx_gui_service.hpp index 463b11c..2569ff9 100644 --- a/src/services/impl/bgfx_gui_service.hpp +++ b/src/services/impl/bgfx_gui_service.hpp @@ -179,6 +179,7 @@ private: uint64_t frameIndex_ = 0; size_t maxTextCacheEntries_ = 256; size_t maxSvgCacheEntries_ = 64; + uint32_t maxTextureDim_ = 0; }; } // namespace sdl3cpp::services::impl diff --git a/src/services/impl/crash_recovery_service.cpp b/src/services/impl/crash_recovery_service.cpp index 092af7d..0b938b3 100644 --- a/src/services/impl/crash_recovery_service.cpp +++ b/src/services/impl/crash_recovery_service.cpp @@ -35,8 +35,8 @@ int64_t GetSteadyClockNs() { // Static instance for signal handler CrashRecoveryService* CrashRecoveryService::instance_ = nullptr; -CrashRecoveryService::CrashRecoveryService(std::shared_ptr logger) - : logger_(logger) +CrashRecoveryService::CrashRecoveryService(std::shared_ptr logger, CrashRecoveryConfig config) + : logger_(std::move(logger)) , crashDetected_(false) , lastSignal_(0) , lastHeartbeatNs_(0) @@ -48,10 +48,19 @@ CrashRecoveryService::CrashRecoveryService(std::shared_ptr logger) , fileFormatErrors_(0) , memoryWarnings_(0) , lastHealthCheck_(std::chrono::steady_clock::now()) - , signalHandlersInstalled_(false) { - logger_->Trace("CrashRecoveryService", "CrashRecoveryService", - "logger=" + std::string(logger_ ? "set" : "null"), - "Created"); + , signalHandlersInstalled_(false) + , config_(std::move(config)) { + heartbeatTimeout_ = std::chrono::milliseconds(config_.heartbeatTimeoutMs); + heartbeatPollInterval_ = std::chrono::milliseconds(config_.heartbeatPollIntervalMs); + memoryLimitBytes_ = config_.memoryLimitMB * 1024 * 1024; + + if (logger_) { + logger_->Trace("CrashRecoveryService", "CrashRecoveryService", + "logger=" + std::string(logger_ ? "set" : "null") + + ", heartbeatTimeoutMs=" + std::to_string(config_.heartbeatTimeoutMs) + + ", heartbeatPollIntervalMs=" + std::to_string(config_.heartbeatPollIntervalMs) + + ", memoryLimitMB=" + std::to_string(config_.memoryLimitMB)); + } } CrashRecoveryService::~CrashRecoveryService() { @@ -69,8 +78,17 @@ void CrashRecoveryService::Initialize() { heartbeatSeen_ = false; crashReport_.clear(); - if (!heartbeatMonitorRunning_.exchange(true)) { - heartbeatMonitorThread_ = std::thread(&CrashRecoveryService::MonitorHeartbeats, this); + const bool heartbeatEnabled = config_.heartbeatTimeoutMs > 0 && config_.heartbeatPollIntervalMs > 0; + if (heartbeatEnabled) { + if (!heartbeatMonitorRunning_.exchange(true)) { + heartbeatMonitorThread_ = std::thread(&CrashRecoveryService::MonitorHeartbeats, this); + } + } else { + heartbeatMonitorRunning_ = false; + if (logger_) { + logger_->Trace("CrashRecoveryService", "Initialize", + "heartbeatEnabled=false", "Heartbeat monitor disabled via config"); + } } logger_->Info("CrashRecoveryService::Initialize: Crash recovery service initialized"); @@ -370,10 +388,16 @@ bool CrashRecoveryService::CheckGpuHealth(double lastFrameTime, double expectedF ", expectedFrameTime=" + std::to_string(expectedFrameTime)); UpdateHealthMetrics(); + if (config_.gpuHangFrameTimeMultiplier <= 0.0 || config_.maxConsecutiveGpuTimeouts == 0) { + consecutiveFrameTimeouts_ = 0; + lastSuccessfulFrameTime_ = lastFrameTime; + return true; + } + // Check for GPU hangs - if we haven't had a successful frame in too long - if (lastFrameTime > expectedFrameTime * 10.0) { // 10x expected frame time + if (lastFrameTime > expectedFrameTime * config_.gpuHangFrameTimeMultiplier) { consecutiveFrameTimeouts_++; - if (consecutiveFrameTimeouts_ > 5) { // 5 consecutive timeouts + if (consecutiveFrameTimeouts_ > config_.maxConsecutiveGpuTimeouts) { std::lock_guard lock(crashMutex_); crashDetected_ = true; crashReport_ += "\nGPU HANG DETECTED: No successful frames for " + @@ -406,7 +430,7 @@ bool CrashRecoveryService::ValidateLuaExecution(bool scriptResult, const std::st logger_->Error("CrashRecoveryService::ValidateLuaExecution: Lua script '" + scriptName + "' failed to execute"); - if (luaExecutionFailures_ > 3) { // Multiple Lua failures indicate a serious issue + if (config_.maxLuaFailures > 0 && luaExecutionFailures_ > config_.maxLuaFailures) { crashDetected_ = true; crashReport_ += "CRITICAL: Multiple Lua execution failures detected\n"; } @@ -442,7 +466,7 @@ bool CrashRecoveryService::ValidateFileFormat(const std::string& filePath, const logger_->Warn("CrashRecoveryService::ValidateFileFormat: Invalid format for " + filePath + " - expected " + expectedFormat + ", got " + extension); - if (fileFormatErrors_ > 2) { // Multiple format errors + if (config_.maxFileFormatErrors > 0 && fileFormatErrors_ > config_.maxFileFormatErrors) { crashDetected_ = true; crashReport_ += "CRITICAL: Multiple file format errors detected\n"; } @@ -476,13 +500,15 @@ bool CrashRecoveryService::CheckMemoryHealth() { logger_->Trace("CrashRecoveryService", "CheckMemoryHealth"); UpdateHealthMetrics(); + if (memoryLimitBytes_ == 0) { + return true; + } + size_t currentMemory = GetCurrentMemoryUsage(); // Basic memory usage check (this is a simplified version) // In a real implementation, you'd track memory growth over time - const size_t MAX_MEMORY_MB = 1024; // 1GB limit for this example - - if (currentMemory > MAX_MEMORY_MB * 1024 * 1024) { + if (currentMemory > memoryLimitBytes_) { memoryWarnings_++; std::lock_guard lock(crashMutex_); crashReport_ += "\nHIGH MEMORY USAGE: " + std::to_string(currentMemory / (1024*1024)) + " MB\n"; @@ -490,7 +516,7 @@ bool CrashRecoveryService::CheckMemoryHealth() { logger_->Warn("CrashRecoveryService::CheckMemoryHealth: High memory usage detected: " + std::to_string(currentMemory / (1024*1024)) + " MB"); - if (memoryWarnings_ > 3) { + if (config_.maxMemoryWarnings > 0 && memoryWarnings_ > config_.maxMemoryWarnings) { crashDetected_ = true; crashReport_ += "CRITICAL: Persistent high memory usage detected\n"; return false; diff --git a/src/services/impl/crash_recovery_service.hpp b/src/services/impl/crash_recovery_service.hpp index 05bd36d..077af23 100644 --- a/src/services/impl/crash_recovery_service.hpp +++ b/src/services/impl/crash_recovery_service.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../interfaces/config_types.hpp" #include "../interfaces/i_crash_recovery_service.hpp" #include "../interfaces/i_logger.hpp" #include @@ -21,7 +22,7 @@ namespace sdl3cpp::services::impl { */ class CrashRecoveryService : public ICrashRecoveryService { public: - explicit CrashRecoveryService(std::shared_ptr logger); + CrashRecoveryService(std::shared_ptr logger, CrashRecoveryConfig config); ~CrashRecoveryService() override; // ICrashRecoveryService interface @@ -65,6 +66,8 @@ private: std::chrono::milliseconds heartbeatPollInterval_{200}; std::string crashReport_; mutable std::mutex crashMutex_; + CrashRecoveryConfig config_{}; + size_t memoryLimitBytes_ = 0; // Health monitoring state std::atomic lastSuccessfulFrameTime_; diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index 8135e7e..ff10c66 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -959,6 +959,79 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, config.guiOpacity = static_cast(value.GetDouble()); } + auto readSizeT = [](const rapidjson::Value& parent, + const char* name, + const std::string& path, + size_t& target) { + if (!parent.HasMember(name)) { + return; + } + const auto& value = parent[name]; + if (!value.IsNumber()) { + throw std::runtime_error("JSON member '" + path + "." + std::string(name) + "' must be a number"); + } + const double rawValue = value.GetDouble(); + if (rawValue < 0.0) { + throw std::runtime_error("JSON member '" + path + "." + std::string(name) + "' must be non-negative"); + } + target = static_cast(rawValue); + }; + + auto readUint32 = [](const rapidjson::Value& parent, + const char* name, + const std::string& path, + uint32_t& target) { + if (!parent.HasMember(name)) { + return; + } + const auto& value = parent[name]; + if (!value.IsNumber()) { + throw std::runtime_error("JSON member '" + path + "." + std::string(name) + "' must be a number"); + } + const double rawValue = value.GetDouble(); + if (rawValue < 0.0) { + throw std::runtime_error("JSON member '" + path + "." + std::string(name) + "' must be non-negative"); + } + target = static_cast(rawValue); + }; + + const auto* budgetsValue = getObjectMember(document, "budgets", "budgets"); + if (budgetsValue) { + readSizeT(*budgetsValue, "vram_mb", "budgets", config.budgets.vramMB); + readUint32(*budgetsValue, "max_texture_dim", "budgets", config.budgets.maxTextureDim); + readSizeT(*budgetsValue, "gui_text_cache_entries", "budgets", config.budgets.guiTextCacheEntries); + readSizeT(*budgetsValue, "gui_svg_cache_entries", "budgets", config.budgets.guiSvgCacheEntries); + } + + const auto* crashRecoveryValue = getObjectMember(document, "crash_recovery", "crash_recovery"); + if (crashRecoveryValue) { + readUint32(*crashRecoveryValue, "heartbeat_timeout_ms", + "crash_recovery", config.crashRecovery.heartbeatTimeoutMs); + readUint32(*crashRecoveryValue, "heartbeat_poll_interval_ms", + "crash_recovery", config.crashRecovery.heartbeatPollIntervalMs); + readSizeT(*crashRecoveryValue, "memory_limit_mb", + "crash_recovery", config.crashRecovery.memoryLimitMB); + readSizeT(*crashRecoveryValue, "max_consecutive_gpu_timeouts", + "crash_recovery", config.crashRecovery.maxConsecutiveGpuTimeouts); + readSizeT(*crashRecoveryValue, "max_lua_failures", + "crash_recovery", config.crashRecovery.maxLuaFailures); + readSizeT(*crashRecoveryValue, "max_file_format_errors", + "crash_recovery", config.crashRecovery.maxFileFormatErrors); + readSizeT(*crashRecoveryValue, "max_memory_warnings", + "crash_recovery", config.crashRecovery.maxMemoryWarnings); + if (crashRecoveryValue->HasMember("gpu_hang_frame_time_multiplier")) { + const auto& value = (*crashRecoveryValue)["gpu_hang_frame_time_multiplier"]; + if (!value.IsNumber()) { + throw std::runtime_error("JSON member 'crash_recovery.gpu_hang_frame_time_multiplier' must be a number"); + } + const double rawValue = value.GetDouble(); + if (rawValue < 0.0) { + throw std::runtime_error("JSON member 'crash_recovery.gpu_hang_frame_time_multiplier' must be non-negative"); + } + config.crashRecovery.gpuHangFrameTimeMultiplier = rawValue; + } + } + return config; } @@ -1091,6 +1164,33 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config, renderingObject.AddMember("atmospherics", atmosphericsObject, allocator); document.AddMember("rendering", renderingObject, allocator); + rapidjson::Value budgetsObject(rapidjson::kObjectType); + budgetsObject.AddMember("vram_mb", static_cast(config.budgets.vramMB), allocator); + budgetsObject.AddMember("max_texture_dim", config.budgets.maxTextureDim, allocator); + budgetsObject.AddMember("gui_text_cache_entries", + static_cast(config.budgets.guiTextCacheEntries), + allocator); + budgetsObject.AddMember("gui_svg_cache_entries", + static_cast(config.budgets.guiSvgCacheEntries), + allocator); + document.AddMember("budgets", budgetsObject, allocator); + + rapidjson::Value crashObject(rapidjson::kObjectType); + crashObject.AddMember("heartbeat_timeout_ms", config.crashRecovery.heartbeatTimeoutMs, allocator); + crashObject.AddMember("heartbeat_poll_interval_ms", config.crashRecovery.heartbeatPollIntervalMs, allocator); + crashObject.AddMember("memory_limit_mb", static_cast(config.crashRecovery.memoryLimitMB), allocator); + crashObject.AddMember("gpu_hang_frame_time_multiplier", + config.crashRecovery.gpuHangFrameTimeMultiplier, allocator); + crashObject.AddMember("max_consecutive_gpu_timeouts", + static_cast(config.crashRecovery.maxConsecutiveGpuTimeouts), allocator); + crashObject.AddMember("max_lua_failures", + static_cast(config.crashRecovery.maxLuaFailures), allocator); + crashObject.AddMember("max_file_format_errors", + static_cast(config.crashRecovery.maxFileFormatErrors), allocator); + crashObject.AddMember("max_memory_warnings", + static_cast(config.crashRecovery.maxMemoryWarnings), allocator); + document.AddMember("crash_recovery", crashObject, allocator); + rapidjson::Value bindingsObject(rapidjson::kObjectType); auto addBindingMember = [&](const char* name, const std::string& value) { rapidjson::Value nameValue(name, allocator); diff --git a/src/services/impl/json_config_service.hpp b/src/services/impl/json_config_service.hpp index 300f8b3..ffd0e04 100644 --- a/src/services/impl/json_config_service.hpp +++ b/src/services/impl/json_config_service.hpp @@ -111,6 +111,18 @@ public: } return config_.guiFont; } + const RenderBudgetConfig& GetRenderBudgetConfig() const override { + if (logger_) { + logger_->Trace("JsonConfigService", "GetRenderBudgetConfig"); + } + return config_.budgets; + } + const CrashRecoveryConfig& GetCrashRecoveryConfig() const override { + if (logger_) { + logger_->Trace("JsonConfigService", "GetCrashRecoveryConfig"); + } + return config_.crashRecovery; + } const std::string& GetConfigJson() const override { if (logger_) { logger_->Trace("JsonConfigService", "GetConfigJson"); diff --git a/src/services/impl/json_config_writer_service.cpp b/src/services/impl/json_config_writer_service.cpp index dd0ed2c..a10a4c9 100644 --- a/src/services/impl/json_config_writer_service.cpp +++ b/src/services/impl/json_config_writer_service.cpp @@ -225,6 +225,33 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std renderingObject.AddMember("atmospherics", atmosphericsObject, allocator); document.AddMember("rendering", renderingObject, allocator); + rapidjson::Value budgetsObject(rapidjson::kObjectType); + budgetsObject.AddMember("vram_mb", static_cast(config.budgets.vramMB), allocator); + budgetsObject.AddMember("max_texture_dim", config.budgets.maxTextureDim, allocator); + budgetsObject.AddMember("gui_text_cache_entries", + static_cast(config.budgets.guiTextCacheEntries), + allocator); + budgetsObject.AddMember("gui_svg_cache_entries", + static_cast(config.budgets.guiSvgCacheEntries), + allocator); + document.AddMember("budgets", budgetsObject, allocator); + + rapidjson::Value crashObject(rapidjson::kObjectType); + crashObject.AddMember("heartbeat_timeout_ms", config.crashRecovery.heartbeatTimeoutMs, allocator); + crashObject.AddMember("heartbeat_poll_interval_ms", config.crashRecovery.heartbeatPollIntervalMs, allocator); + crashObject.AddMember("memory_limit_mb", static_cast(config.crashRecovery.memoryLimitMB), allocator); + crashObject.AddMember("gpu_hang_frame_time_multiplier", + config.crashRecovery.gpuHangFrameTimeMultiplier, allocator); + crashObject.AddMember("max_consecutive_gpu_timeouts", + static_cast(config.crashRecovery.maxConsecutiveGpuTimeouts), allocator); + crashObject.AddMember("max_lua_failures", + static_cast(config.crashRecovery.maxLuaFailures), allocator); + crashObject.AddMember("max_file_format_errors", + static_cast(config.crashRecovery.maxFileFormatErrors), allocator); + crashObject.AddMember("max_memory_warnings", + static_cast(config.crashRecovery.maxMemoryWarnings), allocator); + document.AddMember("crash_recovery", crashObject, allocator); + rapidjson::Value guiObject(rapidjson::kObjectType); rapidjson::Value fontObject(rapidjson::kObjectType); fontObject.AddMember("use_freetype", config.guiFont.useFreeType, allocator); diff --git a/src/services/interfaces/config_types.hpp b/src/services/interfaces/config_types.hpp index c1dcf30..ee24eb2 100644 --- a/src/services/interfaces/config_types.hpp +++ b/src/services/interfaces/config_types.hpp @@ -120,6 +120,30 @@ struct GuiFontConfig { float fontSize = 18.0f; }; +/** + * @brief Resource budget and rendering limits. + */ +struct RenderBudgetConfig { + size_t vramMB = 512; + uint32_t maxTextureDim = 0; + size_t guiTextCacheEntries = 256; + size_t guiSvgCacheEntries = 64; +}; + +/** + * @brief Crash recovery tuning parameters. + */ +struct CrashRecoveryConfig { + uint32_t heartbeatTimeoutMs = 5000; + uint32_t heartbeatPollIntervalMs = 200; + size_t memoryLimitMB = 1024; + double gpuHangFrameTimeMultiplier = 10.0; + size_t maxConsecutiveGpuTimeouts = 5; + size_t maxLuaFailures = 3; + size_t maxFileFormatErrors = 2; + size_t maxMemoryWarnings = 3; +}; + /** * @brief Runtime configuration values used across services. */ @@ -135,8 +159,10 @@ struct RuntimeConfig { BgfxConfig bgfx{}; MaterialXConfig materialX{}; std::vector materialXMaterials{}; + RenderBudgetConfig budgets{}; GuiFontConfig guiFont{}; float guiOpacity = 1.0f; + CrashRecoveryConfig crashRecovery{}; }; } // namespace sdl3cpp::services diff --git a/src/services/interfaces/i_config_service.hpp b/src/services/interfaces/i_config_service.hpp index 8bbf556..2bcbcf1 100644 --- a/src/services/interfaces/i_config_service.hpp +++ b/src/services/interfaces/i_config_service.hpp @@ -85,6 +85,18 @@ public: */ virtual const GuiFontConfig& GetGuiFontConfig() const = 0; + /** + * @brief Get rendering budgets. + * @return Render budget configuration + */ + virtual const RenderBudgetConfig& GetRenderBudgetConfig() const = 0; + + /** + * @brief Get crash recovery configuration. + * @return Crash recovery configuration + */ + virtual const CrashRecoveryConfig& GetCrashRecoveryConfig() const = 0; + /** * @brief Get the full JSON configuration as a string. * diff --git a/tests/test_bgfx_gui_service.cpp b/tests/test_bgfx_gui_service.cpp index 5e5f86e..b513e19 100644 --- a/tests/test_bgfx_gui_service.cpp +++ b/tests/test_bgfx_gui_service.cpp @@ -148,6 +148,8 @@ public: return materialXMaterials_; } const sdl3cpp::services::GuiFontConfig& GetGuiFontConfig() const override { return guiFontConfig_; } + const sdl3cpp::services::RenderBudgetConfig& GetRenderBudgetConfig() const override { return budgets_; } + const sdl3cpp::services::CrashRecoveryConfig& GetCrashRecoveryConfig() const override { return crashRecovery_; } const std::string& GetConfigJson() const override { return configJson_; } void DisableFreeType() { @@ -173,6 +175,8 @@ private: sdl3cpp::services::MaterialXConfig materialXConfig_{}; std::vector materialXMaterials_{}; sdl3cpp::services::GuiFontConfig guiFontConfig_{}; + sdl3cpp::services::RenderBudgetConfig budgets_{}; + sdl3cpp::services::CrashRecoveryConfig crashRecovery_{}; std::string configJson_{}; }; diff --git a/tests/test_cube_script.cpp b/tests/test_cube_script.cpp index 66a2f27..88d0ecd 100644 --- a/tests/test_cube_script.cpp +++ b/tests/test_cube_script.cpp @@ -111,6 +111,8 @@ public: return materialXMaterials_; } const sdl3cpp::services::GuiFontConfig& GetGuiFontConfig() const override { return guiFontConfig_; } + const sdl3cpp::services::RenderBudgetConfig& GetRenderBudgetConfig() const override { return budgets_; } + const sdl3cpp::services::CrashRecoveryConfig& GetCrashRecoveryConfig() const override { return crashRecovery_; } const std::string& GetConfigJson() const override { return configJson_; } private: @@ -125,6 +127,8 @@ private: sdl3cpp::services::MaterialXConfig materialXConfig_{}; std::vector materialXMaterials_{}; sdl3cpp::services::GuiFontConfig guiFontConfig_{}; + sdl3cpp::services::RenderBudgetConfig budgets_{}; + sdl3cpp::services::CrashRecoveryConfig crashRecovery_{}; std::string configJson_{}; }; @@ -154,6 +158,8 @@ public: return materialXMaterials_; } const sdl3cpp::services::GuiFontConfig& GetGuiFontConfig() const override { return guiFontConfig_; } + const sdl3cpp::services::RenderBudgetConfig& GetRenderBudgetConfig() const override { return budgets_; } + const sdl3cpp::services::CrashRecoveryConfig& GetCrashRecoveryConfig() const override { return crashRecovery_; } const std::string& GetConfigJson() const override { return configJson_; } private: @@ -170,6 +176,8 @@ private: sdl3cpp::services::MaterialXConfig materialXConfig_{}; std::vector materialXMaterials_{}; sdl3cpp::services::GuiFontConfig guiFontConfig_{}; + sdl3cpp::services::RenderBudgetConfig budgets_{}; + sdl3cpp::services::CrashRecoveryConfig crashRecovery_{}; std::string renderer_; };