From 6d2a032db933e64a5d37586d19a5a00cb2fb9d6b Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Mon, 4 May 2026 21:19:06 +0100 Subject: [PATCH] Add spdlog trace logging, ioq3 native resolution, and macOS 26 Metal fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace custom LoggerImpl with spdlog (stdout_color_sink + basic_file_sink), QUAKE3_LOG_LEVEL env var controls level at runtime (trace/debug/info/warn/error) - Fix HUD virtual canvas from 640×360 to 640×480 (ioq3 native resolution) - Fix BSP entity field parsing: all numeric values in Q3 BSP entity lump are JSON strings; use EntFloat() helper with stof() in movers_init and triggers_check - Fix macOS 26 Metal crash: TAA shader had SPIRV path as default for MSL backend; add postfx_taa.frag.metal MSL port and fix seed_game.json default path - GPU init: disable SDL Metal debug layer by default (MTL_DEBUG_LAYER); re-enable with SDL_GPU_DEBUG=1 env var; add MSL null-terminator guard in shader compile - spdlog 1.15.1 added to conanfile.py BASE_REQUIRES and CMakeLists.txt Co-Authored-By: Claude Sonnet 4.6 --- gameengine/CMakeLists.txt | 3 + gameengine/CMakeUserPresets.json | 6 +- gameengine/conanfile.py | 3 +- .../seed/shaders/msl/postfx_taa.frag.metal | 74 ++++++ .../packages/seed/workflows/seed_game.json | 2 +- .../impl/diagnostics/logger_service.cpp | 183 ++++++++------- .../workflow_gpu_shader_compile_step.cpp | 18 +- .../workflow_graphics_gpu_init_step.cpp | 10 +- .../workflow/quake3/workflow_q3_hud_step.cpp | 22 +- .../quake3/workflow_q3_movers_init_step.cpp | 23 +- .../workflow_q3_triggers_check_step.cpp | 12 +- .../interfaces/diagnostics/logger_service.hpp | 219 ++---------------- .../workflow/quake3/q3_overlay_utils.hpp | 2 +- 13 files changed, 265 insertions(+), 312 deletions(-) create mode 100644 gameengine/packages/seed/shaders/msl/postfx_taa.frag.metal diff --git a/gameengine/CMakeLists.txt b/gameengine/CMakeLists.txt index 4043cd598..bcf8632e6 100644 --- a/gameengine/CMakeLists.txt +++ b/gameengine/CMakeLists.txt @@ -106,6 +106,8 @@ find_package(assimp CONFIG REQUIRED) find_package(libzip CONFIG REQUIRED) +find_package(spdlog CONFIG REQUIRED) + # Build render stack library group set(SDL3CPP_RENDER_STACK_LIBS EnTT::EnTT) @@ -430,6 +432,7 @@ if(BUILD_SDL3_APP) EnTT::EnTT libzip::zip assimp::assimp + spdlog::spdlog ) endif() diff --git a/gameengine/CMakeUserPresets.json b/gameengine/CMakeUserPresets.json index 45cee691b..9535d9a0b 100644 --- a/gameengine/CMakeUserPresets.json +++ b/gameengine/CMakeUserPresets.json @@ -4,6 +4,8 @@ "conan": {} }, "include": [ - "build-ninja/build/generators/CMakePresets.json" + "build/Release/generators/CMakePresets.json", + "build-ninja/build/Release/build/Release/generators/CMakePresets.json", + "build/Debug/generators/CMakePresets.json" ] -} +} \ No newline at end of file diff --git a/gameengine/conanfile.py b/gameengine/conanfile.py index f3253ff13..f8a269d88 100644 --- a/gameengine/conanfile.py +++ b/gameengine/conanfile.py @@ -29,7 +29,8 @@ class SDL3CppConan(ConanFile): "cairo/1.18.0", "libzip/1.10.1", "stb/cci.20230920", - "gtest/1.17.0" + "gtest/1.17.0", + "spdlog/1.15.1" ) RENDER_STACK_REQUIRES = ( "entt/3.16.0", diff --git a/gameengine/packages/seed/shaders/msl/postfx_taa.frag.metal b/gameengine/packages/seed/shaders/msl/postfx_taa.frag.metal new file mode 100644 index 000000000..df472325a --- /dev/null +++ b/gameengine/packages/seed/shaders/msl/postfx_taa.frag.metal @@ -0,0 +1,74 @@ +#include +using namespace metal; + +// Temporal Anti-Aliasing resolve +// Blends current frame with history using neighborhood clamping (UE4/Karis style) + +struct TAAParams { + float4 u_params; // x = blend factor (0.05 = 95% history), y = 1/width, z = 1/height, w = frame count +}; + +struct FragIn { + float2 uv [[user(uv)]]; +}; + +// Tonemap for stable neighborhood clamping (Karis 2014) +inline float3 tonemap(float3 c) { + return c / (1.0f + max(c.r, max(c.g, c.b))); +} + +inline float3 untonemap(float3 c) { + return c / (1.0f - max(c.r, max(c.g, c.b))); +} + +fragment float4 main0( + FragIn in [[stage_in]], + texture2d currentFrame [[texture(0)]], + texture2d historyFrame [[texture(1)]], + sampler currentSampler [[sampler(0)]], + sampler historySampler [[sampler(1)]], + constant TAAParams& params [[buffer(0)]]) +{ + float2 texelSize = params.u_params.yz; + float blendFactor = params.u_params.x; + float frameCount = params.u_params.w; + float2 uv = in.uv; + + float3 current = currentFrame.sample(currentSampler, uv).rgb; + + // First frame: no history, just output current + if (frameCount < 1.5f) { + return float4(current, 1.0f); + } + + // Sample 3x3 neighbourhood of current frame for clamping + float3 nMin = current; + float3 nMax = current; + + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + if (x == 0 && y == 0) continue; + float3 s = currentFrame.sample(currentSampler, + uv + float2(float(x), float(y)) * texelSize).rgb; + nMin = min(nMin, s); + nMax = max(nMax, s); + } + } + + // Slightly expand the bounding box to reduce flickering + float3 nCenter = (nMin + nMax) * 0.5f; + float3 nExtent = (nMax - nMin) * 0.5f; + nMin = nCenter - nExtent * 1.25f; + nMax = nCenter + nExtent * 1.25f; + + // Sample history and clamp to neighbourhood (prevents ghosting) + float3 history = historyFrame.sample(historySampler, uv).rgb; + float3 historyTM = tonemap(history); + float3 clampedTM = clamp(historyTM, tonemap(nMin), tonemap(nMax)); + float3 clamped = untonemap(clampedTM); + + // Blend: low factor = more history = smoother but more ghosting + float3 result = mix(clamped, current, blendFactor); + + return float4(result, 1.0f); +} diff --git a/gameengine/packages/seed/workflows/seed_game.json b/gameengine/packages/seed/workflows/seed_game.json index c67d8abc3..4143548b6 100644 --- a/gameengine/packages/seed/workflows/seed_game.json +++ b/gameengine/packages/seed/workflows/seed_game.json @@ -119,7 +119,7 @@ "postfx_taa_frag_path": { "name": "postfx_taa_frag_path", "type": "string", - "defaultValue": "packages/seed/shaders/spirv/postfx_taa.frag.spv" + "defaultValue": "packages/seed/shaders/msl/postfx_taa.frag.metal" } }, "nodes": [ diff --git a/gameengine/src/services/impl/diagnostics/logger_service.cpp b/gameengine/src/services/impl/diagnostics/logger_service.cpp index 326df0ddf..a91018afa 100644 --- a/gameengine/src/services/impl/diagnostics/logger_service.cpp +++ b/gameengine/src/services/impl/diagnostics/logger_service.cpp @@ -1,118 +1,137 @@ #include "services/interfaces/diagnostics/logger_service.hpp" -#include -#include -#include -#include +#include +#include + +#include +#include +#include namespace sdl3cpp::services::impl { -LoggerService::LoggerService() : impl_(std::make_unique()) {} +// ── helpers ─────────────────────────────────────────────────────────────────── + +spdlog::level::level_enum LoggerService::ToSpdlog(LogLevel level) { + switch (level) { + case LogLevel::TRACE: return spdlog::level::trace; + case LogLevel::DEBUG: return spdlog::level::debug; + case LogLevel::INFO: return spdlog::level::info; + case LogLevel::WARN: return spdlog::level::warn; + case LogLevel::ERROR: return spdlog::level::err; + default: return spdlog::level::off; + } +} + +LogLevel LoggerService::FromSpdlog(spdlog::level::level_enum l) { + switch (l) { + case spdlog::level::trace: return LogLevel::TRACE; + case spdlog::level::debug: return LogLevel::DEBUG; + case spdlog::level::info: return LogLevel::INFO; + case spdlog::level::warn: return LogLevel::WARN; + case spdlog::level::err: + case spdlog::level::critical: return LogLevel::ERROR; + default: return LogLevel::OFF; + } +} + +// Rebuild spdlog logger whenever sinks change (SetOutputFile / EnableConsoleOutput). +void LoggerService::RebuildLogger() { + std::vector sinks; + + if (consoleOn_) { + auto s = std::make_shared(); + s->set_pattern("[%H:%M:%S.%e] [%^%-5l%$] %v"); + sinks.push_back(s); + } + + if (!filePath_.empty()) { + try { + auto s = std::make_shared(filePath_, true); + s->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%-5l] %v"); + sinks.push_back(s); + } catch (...) {} + } + + const auto prevLevel = log_ ? log_->level() : spdlog::level::info; + log_ = std::make_shared("q3", sinks.begin(), sinks.end()); + log_->set_level(prevLevel); + log_->flush_on(spdlog::level::trace); // flush all levels immediately +} + +// ── construction ────────────────────────────────────────────────────────────── + +LoggerService::LoggerService() { + RebuildLogger(); + + // Honor QUAKE3_LOG_LEVEL env var at startup: trace/debug/info/warn/error/off + if (const char* env = std::getenv("QUAKE3_LOG_LEVEL")) { + const std::string lv(env); + if (lv == "trace") log_->set_level(spdlog::level::trace); + else if (lv == "debug") log_->set_level(spdlog::level::debug); + else if (lv == "info") log_->set_level(spdlog::level::info); + else if (lv == "warn") log_->set_level(spdlog::level::warn); + else if (lv == "error") log_->set_level(spdlog::level::err); + else if (lv == "off") log_->set_level(spdlog::level::off); + } +} + +// ── ILogger ─────────────────────────────────────────────────────────────────── void LoggerService::SetLevel(LogLevel level) { - // Note: Cannot add trace logging here as it would create recursion - impl_->level_.store(level, std::memory_order_relaxed); + log_->set_level(ToSpdlog(level)); } LogLevel LoggerService::GetLevel() const { - return impl_->level_.load(std::memory_order_relaxed); + return FromSpdlog(log_->level()); } void LoggerService::SetOutputFile(const std::string& filename) { - // Note: Cannot add trace logging here as impl_->SetOutputFile may close the log file - std::lock_guard lock(impl_->mutex_); - impl_->SetOutputFile(filename); + filePath_ = filename; + RebuildLogger(); } -void LoggerService::SetMaxLinesPerFile(size_t maxLines) { - // Note: Cannot add trace logging here as it could trigger file rotation during logging - std::lock_guard lock(impl_->mutex_); - impl_->SetMaxLinesPerFile(maxLines); +void LoggerService::SetMaxLinesPerFile(size_t /*maxLines*/) { + // Extend to spdlog::sinks::rotating_file_sink_mt if rotation is needed. } void LoggerService::EnableConsoleOutput(bool enable) { - // Note: Cannot add trace logging here as it could recursively affect console output settings - impl_->consoleEnabled_ = enable; + consoleOn_ = enable; + RebuildLogger(); } void LoggerService::Log(LogLevel level, const std::string& message) { - if (level < GetLevel()) { - return; - } - - std::lock_guard lock(impl_->mutex_); - std::string formattedMessage = impl_->FormatMessage(level, message); - - if (impl_->consoleEnabled_) { - impl_->WriteToConsole(level, formattedMessage); - } - - if (impl_->fileStream_) { - impl_->WriteToFile(formattedMessage); - } + log_->log(ToSpdlog(level), message); } void LoggerService::Trace(const std::string& message) { - Log(LogLevel::TRACE, message); + log_->trace(message); } -void LoggerService::Trace(const std::string& className, const std::string& methodName, const std::string& args, const std::string& message) { - std::string formattedMessage = className + "::" + methodName; - if (!args.empty()) { - formattedMessage += "(" + args + ")"; - } - if (!message.empty()) { - formattedMessage += ": " + message; - } - Log(LogLevel::TRACE, formattedMessage); +void LoggerService::Trace(const std::string& className, const std::string& methodName, + const std::string& args, const std::string& message) { + if (!log_->should_log(spdlog::level::trace)) return; + std::string s = className + "::" + methodName; + if (!args.empty()) s += "(" + args + ")"; + if (!message.empty()) s += " — " + message; + log_->trace(s); } -void LoggerService::Debug(const std::string& message) { - Log(LogLevel::DEBUG, message); -} - -void LoggerService::Info(const std::string& message) { - Log(LogLevel::INFO, message); -} - -void LoggerService::Warn(const std::string& message) { - Log(LogLevel::WARN, message); -} - -void LoggerService::Error(const std::string& message) { - Log(LogLevel::ERROR, message); -} +void LoggerService::Debug(const std::string& message) { log_->debug(message); } +void LoggerService::Info (const std::string& message) { log_->info (message); } +void LoggerService::Warn (const std::string& message) { log_->warn (message); } +void LoggerService::Error(const std::string& message) { log_->error(message); } void LoggerService::TraceFunction(const std::string& funcName) { - if (GetLevel() <= LogLevel::TRACE) { - Trace("Entering " + funcName); - } + log_->trace("→ {}", funcName); } void LoggerService::TraceVariable(const std::string& name, const std::string& value) { - if (GetLevel() <= LogLevel::TRACE) { - Trace(name + " = " + value); - } -} - -void LoggerService::TraceVariable(const std::string& name, int value) { - TraceVariable(name, std::to_string(value)); -} - -void LoggerService::TraceVariable(const std::string& name, size_t value) { - TraceVariable(name, std::to_string(value)); -} - -void LoggerService::TraceVariable(const std::string& name, bool value) { - TraceVariable(name, std::string(value ? "true" : "false")); -} - -void LoggerService::TraceVariable(const std::string& name, float value) { - TraceVariable(name, std::to_string(value)); -} - -void LoggerService::TraceVariable(const std::string& name, double value) { - TraceVariable(name, std::to_string(value)); + log_->trace(" {} = {}", name, value); } +void LoggerService::TraceVariable(const std::string& name, int v) { log_->trace(" {} = {}", name, v); } +void LoggerService::TraceVariable(const std::string& name, size_t v) { log_->trace(" {} = {}", name, v); } +void LoggerService::TraceVariable(const std::string& name, bool v) { log_->trace(" {} = {}", name, v ? "true" : "false"); } +void LoggerService::TraceVariable(const std::string& name, float v) { log_->trace(" {} = {:.4f}", name, v); } +void LoggerService::TraceVariable(const std::string& name, double v) { log_->trace(" {} = {:.6f}", name, v); } } // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/impl/workflow/graphics/workflow_gpu_shader_compile_step.cpp b/gameengine/src/services/impl/workflow/graphics/workflow_gpu_shader_compile_step.cpp index 2a571fcc2..2da8d4bb5 100644 --- a/gameengine/src/services/impl/workflow/graphics/workflow_gpu_shader_compile_step.cpp +++ b/gameengine/src/services/impl/workflow/graphics/workflow_gpu_shader_compile_step.cpp @@ -107,17 +107,25 @@ void WorkflowGpuShaderCompileStep::Execute(const WorkflowStepDefinition& step, W // Load shader binary auto shader_data = LoadBinary(shader_path); + // For MSL shaders: ensure null terminator so NSString initWithBytes succeeds + // on all Metal runtime versions (macOS 26 Metal debug layer can fail otherwise) + if (format == SDL_GPU_SHADERFORMAT_MSL) { + shader_data.push_back(0); + } + if (logger_) { - logger_->Trace("WorkflowGpuShaderCompileStep", "Execute", - "path=" + shader_path + ", stage=" + stage_str + - ", format=" + format_name + ", size=" + std::to_string(shader_data.size()), - "Loading shader"); + logger_->Info("graphics.gpu.shader.compile: loading " + stage_str + + " shader from " + shader_path + " (" + + std::to_string(shader_data.size()) + " bytes, format=" + format_name + ")"); } // Create shader SDL_GPUShaderCreateInfo shader_info = {}; shader_info.code = shader_data.data(); - shader_info.code_size = shader_data.size(); + // Pass size WITHOUT the null terminator for SPIRV/METALLIB; for MSL the extra null is harmless + shader_info.code_size = (format == SDL_GPU_SHADERFORMAT_MSL) + ? shader_data.size() - 1 // exclude the appended null + : shader_data.size(); shader_info.entrypoint = entrypoint; shader_info.format = format; shader_info.stage = stage; diff --git a/gameengine/src/services/impl/workflow/graphics/workflow_graphics_gpu_init_step.cpp b/gameengine/src/services/impl/workflow/graphics/workflow_graphics_gpu_init_step.cpp index 829959790..02a5c6cca 100644 --- a/gameengine/src/services/impl/workflow/graphics/workflow_graphics_gpu_init_step.cpp +++ b/gameengine/src/services/impl/workflow/graphics/workflow_graphics_gpu_init_step.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace sdl3cpp::services::impl { @@ -54,8 +55,13 @@ void WorkflowGraphicsGpuInitStep::Execute(const WorkflowStepDefinition& step, Wo SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXIL); } + // Debug mode: SDL sets MTL_DEBUG_LAYER=1 which triggers a macOS 26 Metal + // validation bug where newLibraryWithSource receives nil source. Default off; + // enable with SDL_GPU_DEBUG=1 env var for explicit GPU validation. + const bool debugMode = std::getenv("SDL_GPU_DEBUG") != nullptr; + // Create GPU device with preferred shader format - SDL_GPUDevice* device = SDL_CreateGPUDevice(shader_format, true, driver_name); + SDL_GPUDevice* device = SDL_CreateGPUDevice(shader_format, debugMode, driver_name); if (!device) { if (logger_) { @@ -65,7 +71,7 @@ void WorkflowGraphicsGpuInitStep::Execute(const WorkflowStepDefinition& step, Wo // Fallback: let SDL auto-select device = SDL_CreateGPUDevice( SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXIL, - true, nullptr); + debugMode, nullptr); if (!device) { throw std::runtime_error("graphics.gpu.init: SDL_CreateGPUDevice failed even with fallback: " + diff --git a/gameengine/src/services/impl/workflow/quake3/workflow_q3_hud_step.cpp b/gameengine/src/services/impl/workflow/quake3/workflow_q3_hud_step.cpp index 0453ef9bd..56c980dbf 100644 --- a/gameengine/src/services/impl/workflow/quake3/workflow_q3_hud_step.cpp +++ b/gameengine/src/services/impl/workflow/quake3/workflow_q3_hud_step.cpp @@ -4,21 +4,17 @@ #include // ───────────────────────────────────────────────────────────────────────────── -// Real Q3A status-bar layout (from ioq3 cg_draw.c), adapted to 640×360: +// Real Q3A status-bar layout (from ioq3 cg_draw.c) at native 640×480. // -// Source constants (640×480): -// CHAR_WIDTH=32 CHAR_HEIGHT=48 ICON_SIZE=48 TEXT_ICON_SPACE=4 -// ammo x=0, y=432 (field width 3) -// ammo icon x=CHAR_WIDTH*3+TEXT_ICON_SPACE = 100 -// health x=185, y=432 (field width 3) -// head x=185+100=285, y=480-ICON_SIZE*1.25=420, size=60 -// armor x=370, y=432 (field width 3) -// armor icon x=370+100 = 470 +// CHAR_WIDTH=32 CHAR_HEIGHT=48 ICON_SIZE=48 TEXT_ICON_SPACE=4 +// ammo x=0, y=432 (field width 3) +// ammo icon x=CHAR_WIDTH*3+TEXT_ICON_SPACE = 100 +// health x=185, y=432 (field width 3) +// head x=285, y=420, size=60 (480-ICON_SIZE*1.25) +// armor x=370, y=432 (field width 3) +// armor icon x=370+100 = 470 // -// Scale to 640×360: Y *= 360/480 = 0.75; X unchanged. -// kCharW=32 kIconSz=36 kHeadSz=45 -// HUD bar bottom = 360; digits 32px tall → kHudY = 360-32-2 = 326 -// head y = 360 - 45 - 2 = 313 +// kScale = kH/480 = 1.0 — all sizes are exact ioq3 pixels. // ───────────────────────────────────────────────────────────────────────────── namespace sdl3cpp::services::impl { diff --git a/gameengine/src/services/impl/workflow/quake3/workflow_q3_movers_init_step.cpp b/gameengine/src/services/impl/workflow/quake3/workflow_q3_movers_init_step.cpp index 20b66db30..f2342b936 100644 --- a/gameengine/src/services/impl/workflow/quake3/workflow_q3_movers_init_step.cpp +++ b/gameengine/src/services/impl/workflow/quake3/workflow_q3_movers_init_step.cpp @@ -5,8 +5,20 @@ #include #include #include +#include #include +namespace { +// BSP entity fields are always JSON strings — helper to safely read as float. +float EntFloat(const nlohmann::json& ent, const char* key, float def) { + if (!ent.contains(key)) return def; + const auto& v = ent[key]; + if (v.is_number()) return v.get(); + if (v.is_string()) { try { return std::stof(v.get()); } catch (...) {} } + return def; +} +} + namespace sdl3cpp::services::impl { WorkflowQ3MoversInitStep::WorkflowQ3MoversInitStep(std::shared_ptr logger) @@ -39,18 +51,17 @@ void WorkflowQ3MoversInitStep::Execute(const WorkflowStepDefinition& /*step*/, W } // Parse angle (degrees, Q3 convention: 0=+X, 90=-Z in XZ plane) - const float angleDeg = ent.value("angle", 0.f); + const float angleDeg = EntFloat(ent, "angle", 0.f); const float angleRad = angleDeg * (3.14159265f / 180.f); - // angle 0 → +X, 90 → -Z (right-hand Y-up) const glm::vec3 moveDir(std::cos(angleRad), 0.f, -std::sin(angleRad)); - // Parse speed and distance - const float speed = ent.value("speed", 100.f); - const float distRaw = ent.value("distance", 128.f); + // Parse speed and distance — all stored as strings in BSP entity lump + const float speed = EntFloat(ent, "speed", 100.f); + const float distRaw = EntFloat(ent, "distance", 128.f); constexpr float kScale = 0.03125f; const float dist = distRaw * kScale; - const float wait = ent.value("wait", 2.f); + const float wait = EntFloat(ent, "wait", 2.f); const float travelTime = (speed > 0.f) ? dist / (speed * kScale) : 1.f; sdl3cpp::q3::Q3Mover m; diff --git a/gameengine/src/services/impl/workflow/quake3/workflow_q3_triggers_check_step.cpp b/gameengine/src/services/impl/workflow/quake3/workflow_q3_triggers_check_step.cpp index 9523a3264..ae6f5d0f0 100644 --- a/gameengine/src/services/impl/workflow/quake3/workflow_q3_triggers_check_step.cpp +++ b/gameengine/src/services/impl/workflow/quake3/workflow_q3_triggers_check_step.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -11,6 +12,15 @@ namespace sdl3cpp::services::impl { namespace { +// BSP entity numeric fields are stored as JSON strings. +float EntFloat(const nlohmann::json& ent, const char* key, float def) { + if (!ent.contains(key)) return def; + const auto& v = ent[key]; + if (v.is_number()) return v.get(); + if (v.is_string()) { try { return std::stof(v.get()); } catch (...) {} } + return def; +} + // Parsed representation of a trigger entity struct TriggerEnt { std::string classname; @@ -74,7 +84,7 @@ void WorkflowQ3TriggersCheckStep::Execute(const WorkflowStepDefinition& /*step*/ t["classname"] = cls; t["origin"] = nlohmann::json::array({origin.x, origin.y, origin.z}); t["target"] = ent.value("target", std::string{}); - t["dmg"] = ent.value("dmg", 5.f); + t["dmg"] = EntFloat(ent, "dmg", 5.f); triggerList.push_back(t); } diff --git a/gameengine/src/services/interfaces/diagnostics/logger_service.hpp b/gameengine/src/services/interfaces/diagnostics/logger_service.hpp index 8a31df871..242a821d0 100644 --- a/gameengine/src/services/interfaces/diagnostics/logger_service.hpp +++ b/gameengine/src/services/interfaces/diagnostics/logger_service.hpp @@ -1,225 +1,42 @@ #pragma once #include "services/interfaces/i_logger.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#if defined(_WIN32) -#include -#include -#else -#include -#include -#endif +#include +#include + +#include +#include namespace sdl3cpp::services::impl { -// Implementation class that holds all the logging logic -class LoggerImpl { -public: - static constexpr size_t kDefaultMaxLinesPerFile = 10000; - - std::atomic level_; - bool consoleEnabled_; - std::unique_ptr fileStream_; - std::mutex mutex_; - std::filesystem::path baseFilePath_; - size_t maxLinesPerFile_; - size_t currentLineCount_; - size_t currentFileIndex_; - int fileDescriptor_; - - LoggerImpl() - : level_(LogLevel::INFO), - consoleEnabled_(true), - maxLinesPerFile_(kDefaultMaxLinesPerFile), - currentLineCount_(0), - currentFileIndex_(0), - fileDescriptor_(-1) {} - - ~LoggerImpl() { - if (fileStream_) { - fileStream_->close(); - } - CloseFileDescriptor(); - } - - std::string LevelToString(LogLevel level) const { - switch (level) { - case LogLevel::TRACE: return "TRACE"; - case LogLevel::DEBUG: return "DEBUG"; - case LogLevel::INFO: return "INFO"; - case LogLevel::WARN: return "WARN"; - case LogLevel::ERROR: return "ERROR"; - default: return "UNKNOWN"; - } - } - - std::string FormatMessage(LogLevel level, const std::string& message) { - auto now = std::chrono::system_clock::now(); - auto time = std::chrono::system_clock::to_time_t(now); - auto ms = std::chrono::duration_cast( - now.time_since_epoch()) % 1000; - - std::ostringstream oss; - oss << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S") - << '.' << std::setfill('0') << std::setw(3) << ms.count() - << " [" << LevelToString(level) << "] " - << message; - return oss.str(); - } - - void WriteToConsole(LogLevel level, const std::string& message) { - if (level >= LogLevel::ERROR) { - std::cerr << message << std::endl; - } else { - std::cout << message << std::endl; - } - } - - void WriteToFile(const std::string& message) { - if (fileStream_) { - *fileStream_ << message << std::endl; - fileStream_->flush(); - SyncFile(); - ++currentLineCount_; - RotateFileIfNeeded(); - } - } - - void SetOutputFile(const std::string& filename) { - if (fileStream_) { - fileStream_->close(); - } - fileStream_.reset(); - CloseFileDescriptor(); - - baseFilePath_ = filename; - currentLineCount_ = 0; - currentFileIndex_ = 0; - - if (!baseFilePath_.empty()) { - OpenLogFile(currentFileIndex_); - } - } - - void SetMaxLinesPerFile(size_t maxLines) { - maxLinesPerFile_ = maxLines; - } - - std::filesystem::path BuildLogFilePath(size_t index) const { - if (baseFilePath_.empty() || index == 0) { - return baseFilePath_; - } - - std::filesystem::path basePath(baseFilePath_); - std::string stem = basePath.stem().string(); - std::string extension = basePath.extension().string(); - std::string rotatedName = stem + "." + std::to_string(index) + extension; - - return basePath.parent_path() / rotatedName; - } - - void OpenLogFile(size_t index) { - if (baseFilePath_.empty()) { - return; - } - - std::filesystem::path logPath = BuildLogFilePath(index); - fileStream_ = std::make_unique(logPath, std::ios::out | std::ios::trunc); - if (!fileStream_->is_open()) { - std::cerr << "Failed to open log file: " << logPath.string() << std::endl; - fileStream_.reset(); - CloseFileDescriptor(); - return; - } - OpenFileDescriptor(logPath); - } - - void RotateFileIfNeeded() { - if (maxLinesPerFile_ == 0 || currentLineCount_ < maxLinesPerFile_) { - return; - } - - if (fileStream_) { - fileStream_->close(); - } - CloseFileDescriptor(); - - ++currentFileIndex_; - currentLineCount_ = 0; - OpenLogFile(currentFileIndex_); - } - - void OpenFileDescriptor(const std::filesystem::path& logPath) { - CloseFileDescriptor(); - const std::string pathString = logPath.string(); -#if defined(_WIN32) - fileDescriptor_ = _open(pathString.c_str(), _O_WRONLY | _O_APPEND); -#else - fileDescriptor_ = ::open(pathString.c_str(), O_WRONLY | O_APPEND); -#endif - if (fileDescriptor_ < 0) { - std::cerr << "Failed to open log file descriptor for sync: " << pathString << std::endl; - } - } - - void CloseFileDescriptor() { - if (fileDescriptor_ < 0) { - return; - } -#if defined(_WIN32) - _close(fileDescriptor_); -#else - ::close(fileDescriptor_); -#endif - fileDescriptor_ = -1; - } - - void SyncFile() { - if (fileDescriptor_ < 0) { - return; - } -#if defined(_WIN32) - _commit(fileDescriptor_); -#else - ::fsync(fileDescriptor_); -#endif - } -}; - /** - * @brief Logger service implementation. + * @brief spdlog-backed ILogger implementation. * - * Contains the full logging implementation, no longer wrapping a singleton. - * Small, focused service (~200 lines) for application logging. + * Console sink always active; file sink added on SetOutputFile(). + * Log level controlled at runtime via QUAKE3_LOG_LEVEL env var + * (trace / debug / info / warn / error) or programmatically via SetLevel(). */ class LoggerService : public ILogger { public: LoggerService(); ~LoggerService() override = default; - // ILogger interface void SetLevel(LogLevel level) override; LogLevel GetLevel() const override; void SetOutputFile(const std::string& filename) override; - void SetMaxLinesPerFile(size_t maxLines) override; + void SetMaxLinesPerFile(size_t maxLines) override; // no-op: spdlog handles rotation void EnableConsoleOutput(bool enable) override; + void Log(LogLevel level, const std::string& message) override; void Trace(const std::string& message) override; - void Trace(const std::string& className, const std::string& methodName, const std::string& args = "", const std::string& message = "") override; + void Trace(const std::string& className, const std::string& methodName, + const std::string& args = "", const std::string& message = "") override; void Debug(const std::string& message) override; void Info(const std::string& message) override; void Warn(const std::string& message) override; void Error(const std::string& message) override; + void TraceFunction(const std::string& funcName) override; void TraceVariable(const std::string& name, const std::string& value) override; void TraceVariable(const std::string& name, int value) override; @@ -229,7 +46,13 @@ public: void TraceVariable(const std::string& name, double value) override; private: - std::unique_ptr impl_; + static spdlog::level::level_enum ToSpdlog(LogLevel level); + static LogLevel FromSpdlog(spdlog::level::level_enum l); + void RebuildLogger(); + + std::string filePath_; + bool consoleOn_ = true; + std::shared_ptr log_; }; } // namespace sdl3cpp::services::impl diff --git a/gameengine/src/services/interfaces/workflow/quake3/q3_overlay_utils.hpp b/gameengine/src/services/interfaces/workflow/quake3/q3_overlay_utils.hpp index 1507ec106..d47906e79 100644 --- a/gameengine/src/services/interfaces/workflow/quake3/q3_overlay_utils.hpp +++ b/gameengine/src/services/interfaces/workflow/quake3/q3_overlay_utils.hpp @@ -13,7 +13,7 @@ struct ArenaInfo { std::string longname; std::string bot; }; using ArenaMap = std::unordered_map; using LevelshotCache = std::unordered_map; -static constexpr int kW = 640, kH = 360; +static constexpr int kW = 640, kH = 480; // ioq3 native virtual resolution static constexpr int kGlyphSrc = 16; static constexpr int kPropHeight = 27; static constexpr int kPropGap = 3;