Add spdlog trace logging, ioq3 native resolution, and macOS 26 Metal fixes

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-04 21:19:06 +01:00
parent 083d57177b
commit 6d2a032db9
13 changed files with 265 additions and 312 deletions
+3
View File
@@ -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()
+4 -2
View File
@@ -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"
]
}
}
+2 -1
View File
@@ -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",
@@ -0,0 +1,74 @@
#include <metal_stdlib>
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<float> currentFrame [[texture(0)]],
texture2d<float> 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);
}
@@ -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": [
@@ -1,118 +1,137 @@
#include "services/interfaces/diagnostics/logger_service.hpp"
#include <iostream>
#include <chrono>
#include <iomanip>
#include <thread>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <cstdlib>
#include <string>
#include <vector>
namespace sdl3cpp::services::impl {
LoggerService::LoggerService() : impl_(std::make_unique<LoggerImpl>()) {}
// ── 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<spdlog::sink_ptr> sinks;
if (consoleOn_) {
auto s = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
s->set_pattern("[%H:%M:%S.%e] [%^%-5l%$] %v");
sinks.push_back(s);
}
if (!filePath_.empty()) {
try {
auto s = std::make_shared<spdlog::sinks::basic_file_sink_mt>(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<spdlog::logger>("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<std::mutex> 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<std::mutex> 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<std::mutex> 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
@@ -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;
@@ -4,6 +4,7 @@
#include <SDL3/SDL_gpu.h>
#include <SDL3/SDL.h>
#include <nlohmann/json.hpp>
#include <cstdlib>
#include <stdexcept>
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: " +
@@ -4,21 +4,17 @@
#include <SDL3/SDL_gpu.h>
// ─────────────────────────────────────────────────────────────────────────────
// 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 {
@@ -5,8 +5,20 @@
#include <nlohmann/json.hpp>
#include <glm/glm.hpp>
#include <cmath>
#include <cstdlib>
#include <string>
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<float>();
if (v.is_string()) { try { return std::stof(v.get<std::string>()); } catch (...) {} }
return def;
}
}
namespace sdl3cpp::services::impl {
WorkflowQ3MoversInitStep::WorkflowQ3MoversInitStep(std::shared_ptr<ILogger> 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;
@@ -4,6 +4,7 @@
#include <nlohmann/json.hpp>
#include <glm/glm.hpp>
#include <cmath>
#include <cstdlib>
#include <string>
#include <vector>
@@ -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<float>();
if (v.is_string()) { try { return std::stof(v.get<std::string>()); } 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);
}
@@ -1,225 +1,42 @@
#pragma once
#include "services/interfaces/i_logger.hpp"
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <memory>
#include <mutex>
#include <sstream>
#include <thread>
#if defined(_WIN32)
#include <io.h>
#include <fcntl.h>
#else
#include <fcntl.h>
#include <unistd.h>
#endif
#include <spdlog/spdlog.h>
#include <spdlog/logger.h>
#include <memory>
#include <string>
namespace sdl3cpp::services::impl {
// Implementation class that holds all the logging logic
class LoggerImpl {
public:
static constexpr size_t kDefaultMaxLinesPerFile = 10000;
std::atomic<LogLevel> level_;
bool consoleEnabled_;
std::unique_ptr<std::ofstream> 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<std::chrono::milliseconds>(
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<std::ofstream>(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<LoggerImpl> 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<spdlog::logger> log_;
};
} // namespace sdl3cpp::services::impl
@@ -13,7 +13,7 @@ struct ArenaInfo { std::string longname; std::string bot; };
using ArenaMap = std::unordered_map<std::string, ArenaInfo>;
using LevelshotCache = std::unordered_map<std::string, SDL_Texture*>;
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;