From 44b198b14d28b25e1171509481f321c8e902721b Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Tue, 12 May 2026 11:58:29 +1000 Subject: [PATCH] logging --- CMakeLists.txt | 18 ++ apps/RenderCadenceCompositor/README.md | 5 + .../RenderCadenceCompositor.cpp | 23 +- .../RenderCadenceCompositor/app/AppConfig.cpp | 6 + apps/RenderCadenceCompositor/app/AppConfig.h | 2 + .../app/RenderCadenceApp.h | 26 +- .../logging/Logger.cpp | 283 ++++++++++++++++++ apps/RenderCadenceCompositor/logging/Logger.h | 101 +++++++ .../render/RenderThread.cpp | 15 +- tests/RenderCadenceCompositorLoggerTests.cpp | 88 ++++++ 10 files changed, 555 insertions(+), 12 deletions(-) create mode 100644 apps/RenderCadenceCompositor/logging/Logger.cpp create mode 100644 apps/RenderCadenceCompositor/logging/Logger.h create mode 100644 tests/RenderCadenceCompositorLoggerTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ebd7f01..98258c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -308,6 +308,8 @@ set(RENDER_CADENCE_APP_SOURCES "${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.cpp" "${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.h" "${RENDER_CADENCE_APP_DIR}/frames/SystemFrameTypes.h" + "${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp" + "${RENDER_CADENCE_APP_DIR}/logging/Logger.h" "${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.cpp" "${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.h" "${RENDER_CADENCE_APP_DIR}/render/Bgra8ReadbackPipeline.cpp" @@ -349,6 +351,7 @@ target_include_directories(RenderCadenceCompositor PRIVATE "${RENDER_CADENCE_APP_DIR}" "${RENDER_CADENCE_APP_DIR}/app" "${RENDER_CADENCE_APP_DIR}/frames" + "${RENDER_CADENCE_APP_DIR}/logging" "${RENDER_CADENCE_APP_DIR}/platform" "${RENDER_CADENCE_APP_DIR}/render" "${RENDER_CADENCE_APP_DIR}/runtime" @@ -803,6 +806,21 @@ endif() add_test(NAME RenderCadenceCompositorRuntimeShaderParamsTests COMMAND RenderCadenceCompositorRuntimeShaderParamsTests) +add_executable(RenderCadenceCompositorLoggerTests + "${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorLoggerTests.cpp" +) + +target_include_directories(RenderCadenceCompositorLoggerTests PRIVATE + "${RENDER_CADENCE_APP_DIR}/logging" +) + +if(MSVC) + target_compile_options(RenderCadenceCompositorLoggerTests PRIVATE /W3) +endif() + +add_test(NAME RenderCadenceCompositorLoggerTests COMMAND RenderCadenceCompositorLoggerTests) + add_executable(SystemOutputFramePoolTests "${APP_DIR}/videoio/SystemOutputFramePool.cpp" "${APP_DIR}/videoio/VideoIOFormat.cpp" diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index d7ac6c6..32bd279 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -44,6 +44,7 @@ Included now: - render-thread-only GL commit once the artifact is ready - manifest-driven stateless single-pass shader packages - default float, vec2, color, boolean, enum, and trigger parameters +- background logging with `log`, `warning`, and `error` levels - compact telemetry - non-GL frame-exchange tests @@ -100,6 +101,10 @@ Use `--no-shader` to keep the simple motion fallback only. ## Expected Telemetry +Startup, shutdown, shader-build, and render-thread event messages are written through the app logger. Telemetry is intentionally separate and remains a compact once-per-second cadence line. + +The logger writes to the console, `OutputDebugStringA`, and `logs/render-cadence-compositor.log` by default. Render-thread log calls use the non-blocking path so diagnostics do not become cadence blockers. + The app prints one line per second: ```text diff --git a/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp b/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp index 036d98a..0dda354 100644 --- a/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp +++ b/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp @@ -1,12 +1,14 @@ #include "app/AppConfig.h" #include "app/RenderCadenceApp.h" #include "frames/SystemFrameExchange.h" +#include "logging/Logger.h" #include "render/RenderThread.h" #include "VideoIOFormat.h" #include #include +#include #include namespace @@ -38,17 +40,22 @@ private: int main(int argc, char** argv) { + RenderCadenceCompositor::AppConfig appConfig = RenderCadenceCompositor::DefaultAppConfig(); + RenderCadenceCompositor::Logger::Instance().Start(appConfig.logging); + RenderCadenceCompositor::Log( + "app", + "RenderCadenceCompositor starting. Starts render cadence, system-memory exchange, DeckLink scheduled output, and telemetry. Press Enter to stop."); + ComInitGuard com; if (!com.Initialize()) { - std::cerr << "COM initialization failed: 0x" << std::hex << com.Result() << std::dec << "\n"; + std::ostringstream message; + message << "COM initialization failed: 0x" << std::hex << com.Result(); + RenderCadenceCompositor::LogError("app", message.str()); + RenderCadenceCompositor::Logger::Instance().Stop(); return 1; } - std::cout << "RenderCadenceCompositor\n" - << " Starts render cadence, system-memory exchange, DeckLink scheduled output, and telemetry.\n" - << " Press Enter to stop.\n"; - SystemFrameExchangeConfig frameExchangeConfig; frameExchangeConfig.width = 1920; frameExchangeConfig.height = 1080; @@ -66,7 +73,6 @@ int main(int argc, char** argv) RenderThread renderThread(frameExchange, renderConfig); - RenderCadenceCompositor::AppConfig appConfig = RenderCadenceCompositor::DefaultAppConfig(); for (int index = 1; index < argc; ++index) { const std::string argument = argv[index]; @@ -87,12 +93,15 @@ int main(int argc, char** argv) std::string error; if (!app.Start(error)) { - std::cerr << "RenderCadenceCompositor start failed: " << error << "\n"; + RenderCadenceCompositor::LogError("app", "RenderCadenceCompositor start failed: " + error); + RenderCadenceCompositor::Logger::Instance().Stop(); return 1; } std::string line; std::getline(std::cin, line); app.Stop(); + RenderCadenceCompositor::Log("app", "RenderCadenceCompositor stopped."); + RenderCadenceCompositor::Logger::Instance().Stop(); return 0; } diff --git a/apps/RenderCadenceCompositor/app/AppConfig.cpp b/apps/RenderCadenceCompositor/app/AppConfig.cpp index f09cbea..90744d3 100644 --- a/apps/RenderCadenceCompositor/app/AppConfig.cpp +++ b/apps/RenderCadenceCompositor/app/AppConfig.cpp @@ -9,6 +9,12 @@ AppConfig DefaultAppConfig() config.deckLink.outputAlphaRequired = false; config.outputThread.targetBufferedFrames = 4; config.telemetry.interval = std::chrono::seconds(1); + config.logging.minimumLevel = LogLevel::Log; + config.logging.writeToConsole = true; + config.logging.writeToDebugOutput = true; + config.logging.writeToFile = true; + config.logging.filePath = "logs/render-cadence-compositor.log"; + config.logging.maxQueuedMessages = 1024; config.warmupCompletedFrames = 4; config.warmupTimeout = std::chrono::seconds(3); config.prerollTimeout = std::chrono::seconds(3); diff --git a/apps/RenderCadenceCompositor/app/AppConfig.h b/apps/RenderCadenceCompositor/app/AppConfig.h index 85c7dca..5839cb2 100644 --- a/apps/RenderCadenceCompositor/app/AppConfig.h +++ b/apps/RenderCadenceCompositor/app/AppConfig.h @@ -1,5 +1,6 @@ #pragma once +#include "../logging/Logger.h" #include "../telemetry/TelemetryPrinter.h" #include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutputThread.h" @@ -15,6 +16,7 @@ struct AppConfig DeckLinkOutputConfig deckLink; DeckLinkOutputThreadConfig outputThread; TelemetryPrinterConfig telemetry; + LoggerConfig logging; std::size_t warmupCompletedFrames = 4; std::chrono::milliseconds warmupTimeout = std::chrono::seconds(3); std::chrono::milliseconds prerollTimeout = std::chrono::seconds(3); diff --git a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h index 19cde6e..e9b74fd 100644 --- a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h +++ b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h @@ -1,13 +1,13 @@ #pragma once #include "AppConfig.h" +#include "../logging/Logger.h" #include "../runtime/RuntimeShaderBridge.h" #include "../telemetry/TelemetryPrinter.h" #include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutputThread.h" #include -#include #include #include #include @@ -65,6 +65,7 @@ public: bool Start(std::string& error) { + Log("app", "Initializing DeckLink output."); if (!mOutput.Initialize( mConfig.deckLink, [this](const VideoIOCompletion& completion) { @@ -72,44 +73,56 @@ public: }, error)) { + LogError("app", "DeckLink output initialization failed: " + error); return false; } + Log("app", "Starting render thread."); if (!detail::StartRenderThread(mRenderThread, error, 0)) { + LogError("app", "Render thread start failed: " + error); Stop(); return false; } StartRuntimeShaderBuild(); + Log("app", "Waiting for rendered warmup frames."); if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout)) { error = "Timed out waiting for rendered warmup frames."; + LogError("app", error); Stop(); return false; } + Log("app", "Starting DeckLink output thread."); if (!mOutputThread.Start()) { error = "DeckLink output thread failed to start."; + LogError("app", error); Stop(); return false; } + Log("app", "Waiting for DeckLink preroll frames."); if (!WaitForPreroll()) { error = "Timed out waiting for DeckLink preroll frames."; + LogError("app", error); Stop(); return false; } + Log("app", "Starting DeckLink scheduled playback."); if (!mOutput.StartScheduledPlayback(error)) { + LogError("app", "DeckLink scheduled playback failed: " + error); Stop(); return false; } mTelemetry.Start(mFrameExchange, mOutput, mOutputThread, mRenderThread); + Log("app", "RenderCadenceCompositor started."); mStarted = true; return true; } @@ -122,6 +135,8 @@ public: StopRuntimeShaderBuild(); mRenderThread.Stop(); mOutput.ReleaseResources(); + if (mStarted) + Log("app", "RenderCadenceCompositor shutdown complete."); mStarted = false; } @@ -143,13 +158,20 @@ private: void StartRuntimeShaderBuild() { + if (mConfig.runtimeShaderId.empty()) + { + Log("runtime-shader", "Runtime shader build disabled."); + return; + } + + Log("runtime-shader", "Starting background Slang build for shader '" + mConfig.runtimeShaderId + "'."); mShaderBridge.Start( mConfig.runtimeShaderId, [this](const RuntimeShaderArtifact& artifact) { mRenderThread.SubmitRuntimeShaderArtifact(artifact); }, [](const std::string& message) { - std::cout << "Runtime Slang build failed: " << message << "\n"; + LogError("runtime-shader", "Runtime Slang build failed: " + message); }); } diff --git a/apps/RenderCadenceCompositor/logging/Logger.cpp b/apps/RenderCadenceCompositor/logging/Logger.cpp new file mode 100644 index 0000000..d1013e7 --- /dev/null +++ b/apps/RenderCadenceCompositor/logging/Logger.cpp @@ -0,0 +1,283 @@ +#include "Logger.h" + +#include + +#include +#include +#include +#include +#include + +namespace RenderCadenceCompositor +{ +namespace +{ +int Rank(LogLevel level) +{ + switch (level) + { + case LogLevel::Log: + return 0; + case LogLevel::Warning: + return 1; + case LogLevel::Error: + return 2; + default: + return 0; + } +} + +std::string FormatTimestamp(std::chrono::system_clock::time_point timestamp) +{ + const std::time_t time = std::chrono::system_clock::to_time_t(timestamp); + std::tm localTime = {}; + localtime_s(&localTime, &time); + + std::ostringstream stream; + stream << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S"); + return stream.str(); +} + +std::string FormatRecord(const Logger::Record& record) +{ + std::ostringstream stream; + stream << FormatTimestamp(record.timestamp) + << " [" << LogLevelName(record.level) << "]" + << " [" << record.subsystem << "] " + << record.message; + return stream.str(); +} +} + +const char* LogLevelName(LogLevel level) +{ + switch (level) + { + case LogLevel::Log: + return "log"; + case LogLevel::Warning: + return "warning"; + case LogLevel::Error: + return "error"; + default: + return "log"; + } +} + +Logger& Logger::Instance() +{ + static Logger logger; + return logger; +} + +Logger::~Logger() +{ + Stop(); +} + +void Logger::Start(LoggerConfig config) +{ + std::lock_guard lock(mMutex); + if (mRunning) + return; + + mConfig = config; + mStopping = false; + mRunning = true; + OpenFileSink(); + { + std::lock_guard countersLock(mCountersMutex); + mCounters = LoggerCounters(); + } + mThread = std::thread([this]() { ThreadMain(); }); +} + +void Logger::Stop() +{ + { + std::lock_guard lock(mMutex); + if (!mRunning && !mThread.joinable()) + return; + mStopping = true; + } + mCondition.notify_all(); + + if (mThread.joinable()) + mThread.join(); + + std::lock_guard lock(mMutex); + CloseFileSink(); + mRunning = false; +} + +void Logger::Write(LogLevel level, const std::string& subsystem, const std::string& message) +{ + Enqueue(Record{ std::chrono::system_clock::now(), std::this_thread::get_id(), level, subsystem, message }, true); +} + +bool Logger::TryWrite(LogLevel level, const std::string& subsystem, const std::string& message) +{ + return Enqueue(Record{ std::chrono::system_clock::now(), std::this_thread::get_id(), level, subsystem, message }, false); +} + +void Logger::Log(const std::string& subsystem, const std::string& message) +{ + Write(LogLevel::Log, subsystem, message); +} + +void Logger::Warning(const std::string& subsystem, const std::string& message) +{ + Write(LogLevel::Warning, subsystem, message); +} + +void Logger::Error(const std::string& subsystem, const std::string& message) +{ + Write(LogLevel::Error, subsystem, message); +} + +LoggerCounters Logger::Counters() const +{ + std::lock_guard lock(mCountersMutex); + return mCounters; +} + +bool Logger::IsRunning() const +{ + std::lock_guard lock(mMutex); + return mRunning; +} + +bool Logger::ShouldWrite(LogLevel level) const +{ + return Rank(level) >= Rank(mConfig.minimumLevel); +} + +bool Logger::Enqueue(Record record, bool block) +{ + if (!ShouldWrite(record.level)) + return true; + + std::unique_lock lock(mMutex, std::defer_lock); + if (block) + { + lock.lock(); + } + else if (!lock.try_lock()) + { + std::lock_guard countersLock(mCountersMutex); + ++mCounters.dropped; + return false; + } + + if (!mRunning) + { + lock.unlock(); + WriteRecord(record); + return true; + } + + if (mQueue.size() >= mConfig.maxQueuedMessages) + { + std::lock_guard countersLock(mCountersMutex); + ++mCounters.dropped; + return false; + } + + mQueue.push_back(std::move(record)); + { + std::lock_guard countersLock(mCountersMutex); + ++mCounters.queued; + } + lock.unlock(); + mCondition.notify_one(); + return true; +} + +void Logger::ThreadMain() +{ + for (;;) + { + Record record; + { + std::unique_lock lock(mMutex); + mCondition.wait(lock, [this]() { + return mStopping || !mQueue.empty(); + }); + + if (mQueue.empty()) + { + if (mStopping) + return; + continue; + } + + record = std::move(mQueue.front()); + mQueue.pop_front(); + } + WriteRecord(record); + } +} + +void Logger::WriteRecord(const Record& record) +{ + const std::string line = FormatRecord(record); + + if (mConfig.writeToDebugOutput) + OutputDebugStringA((line + "\n").c_str()); + + if (mConfig.writeToConsole) + { + if (record.level == LogLevel::Error) + std::cerr << line << "\n"; + else + std::cout << line << "\n"; + } + if (mFile.is_open()) + { + mFile << line << "\n"; + mFile.flush(); + } + + std::lock_guard lock(mCountersMutex); + ++mCounters.written; +} + +void Logger::OpenFileSink() +{ + CloseFileSink(); + if (!mConfig.writeToFile || mConfig.filePath.empty()) + return; + + std::error_code error; + const std::filesystem::path logPath(mConfig.filePath); + if (logPath.has_parent_path()) + std::filesystem::create_directories(logPath.parent_path(), error); + mFile.open(logPath, std::ios::out | std::ios::app); +} + +void Logger::CloseFileSink() +{ + if (mFile.is_open()) + mFile.close(); +} + +bool TryLog(LogLevel level, const std::string& subsystem, const std::string& message) +{ + return Logger::Instance().TryWrite(level, subsystem, message); +} + +void Log(const std::string& subsystem, const std::string& message) +{ + Logger::Instance().Log(subsystem, message); +} + +void LogWarning(const std::string& subsystem, const std::string& message) +{ + Logger::Instance().Warning(subsystem, message); +} + +void LogError(const std::string& subsystem, const std::string& message) +{ + Logger::Instance().Error(subsystem, message); +} +} diff --git a/apps/RenderCadenceCompositor/logging/Logger.h b/apps/RenderCadenceCompositor/logging/Logger.h new file mode 100644 index 0000000..25c3a5f --- /dev/null +++ b/apps/RenderCadenceCompositor/logging/Logger.h @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RenderCadenceCompositor +{ +enum class LogLevel +{ + Log, + Warning, + Error +}; + +struct LoggerConfig +{ + LogLevel minimumLevel = LogLevel::Log; + bool writeToConsole = true; + bool writeToDebugOutput = true; + bool writeToFile = true; + std::string filePath = "logs/render-cadence-compositor.log"; + std::size_t maxQueuedMessages = 1024; +}; + +struct LoggerCounters +{ + uint64_t queued = 0; + uint64_t written = 0; + uint64_t dropped = 0; +}; + +const char* LogLevelName(LogLevel level); + +class Logger +{ +public: + struct Record + { + std::chrono::system_clock::time_point timestamp; + std::thread::id threadId; + LogLevel level = LogLevel::Log; + std::string subsystem; + std::string message; + }; + + static Logger& Instance(); + + Logger(const Logger&) = delete; + Logger& operator=(const Logger&) = delete; + + ~Logger(); + + void Start(LoggerConfig config = LoggerConfig()); + void Stop(); + + void Write(LogLevel level, const std::string& subsystem, const std::string& message); + bool TryWrite(LogLevel level, const std::string& subsystem, const std::string& message); + + void Log(const std::string& subsystem, const std::string& message); + void Warning(const std::string& subsystem, const std::string& message); + void Error(const std::string& subsystem, const std::string& message); + + LoggerCounters Counters() const; + bool IsRunning() const; + +private: + Logger() = default; + + bool ShouldWrite(LogLevel level) const; + bool Enqueue(Record record, bool block); + void ThreadMain(); + void WriteRecord(const Record& record); + void OpenFileSink(); + void CloseFileSink(); + + LoggerConfig mConfig; + std::ofstream mFile; + std::thread mThread; + mutable std::mutex mMutex; + std::condition_variable mCondition; + std::deque mQueue; + bool mStopping = false; + bool mRunning = false; + + mutable std::mutex mCountersMutex; + LoggerCounters mCounters; +}; + +bool TryLog(LogLevel level, const std::string& subsystem, const std::string& message); +void Log(const std::string& subsystem, const std::string& message); +void LogWarning(const std::string& subsystem, const std::string& message); +void LogError(const std::string& subsystem, const std::string& message); +} diff --git a/apps/RenderCadenceCompositor/render/RenderThread.cpp b/apps/RenderCadenceCompositor/render/RenderThread.cpp index 2b6fbeb..b5c50b7 100644 --- a/apps/RenderCadenceCompositor/render/RenderThread.cpp +++ b/apps/RenderCadenceCompositor/render/RenderThread.cpp @@ -2,6 +2,7 @@ #include "../frames/SystemFrameExchange.h" #include "../frames/SystemFrameTypes.h" +#include "../logging/Logger.h" #include "../platform/HiddenGlWindow.h" #include "Bgra8ReadbackPipeline.h" #include "GLExtensions.h" @@ -10,7 +11,6 @@ #include #include -#include RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) : mFrameExchange(frameExchange), @@ -71,6 +71,7 @@ RenderThread::Metrics RenderThread::GetMetrics() const void RenderThread::ThreadMain() { + RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread starting."); HiddenGlWindow window; std::string error; if (!window.Create(mConfig.width, mConfig.height, error) || !window.MakeCurrent()) @@ -156,6 +157,7 @@ void RenderThread::ThreadMain() renderer.ShutdownGl(); window.ClearCurrent(); mRunning.store(false, std::memory_order_release); + RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread stopped."); } void RenderThread::SignalStarted() @@ -167,6 +169,7 @@ void RenderThread::SignalStarted() void RenderThread::SignalStartupFailure(const std::string& error) { + RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Error, "render-thread", error); std::lock_guard lock(mStartupMutex); mStartupError = error; mStartupCondition.notify_all(); @@ -221,13 +224,19 @@ void RenderThread::TryCommitReadyRuntimeShader(RuntimeShaderRenderer& runtimeSha std::string commitError; if (!runtimeShaderRenderer.CommitShaderArtifact(artifact, commitError)) { - OutputDebugStringA(("Runtime shader GL commit failed: " + commitError + "\n").c_str()); + RenderCadenceCompositor::TryLog( + RenderCadenceCompositor::LogLevel::Error, + "render-thread", + "Runtime shader GL commit failed: " + commitError); std::lock_guard lock(mMetricsMutex); ++mMetrics.shaderBuildFailures; return; } - OutputDebugStringA(("Runtime shader committed: " + artifact.shaderId + ". " + artifact.message + "\n").c_str()); + RenderCadenceCompositor::TryLog( + RenderCadenceCompositor::LogLevel::Log, + "render-thread", + "Runtime shader committed: " + artifact.shaderId + ". " + artifact.message); std::lock_guard lock(mMetricsMutex); ++mMetrics.shaderBuildsCommitted; } diff --git a/tests/RenderCadenceCompositorLoggerTests.cpp b/tests/RenderCadenceCompositorLoggerTests.cpp new file mode 100644 index 0000000..99d32a9 --- /dev/null +++ b/tests/RenderCadenceCompositorLoggerTests.cpp @@ -0,0 +1,88 @@ +#include "Logger.h" + +#include +#include +#include +#include + +namespace +{ +int gFailures = 0; + +void Expect(bool condition, const std::string& message) +{ + if (condition) + return; + + ++gFailures; + std::cerr << "FAILED: " << message << "\n"; +} + +void TestLevelNames() +{ + using namespace RenderCadenceCompositor; + Expect(std::string(LogLevelName(LogLevel::Log)) == "log", "log level name"); + Expect(std::string(LogLevelName(LogLevel::Warning)) == "warning", "warning level name"); + Expect(std::string(LogLevelName(LogLevel::Error)) == "error", "error level name"); +} + +void TestLevelFiltering() +{ + using namespace RenderCadenceCompositor; + + LoggerConfig config; + config.minimumLevel = LogLevel::Warning; + config.writeToConsole = false; + config.writeToDebugOutput = false; + config.writeToFile = false; + config.maxQueuedMessages = 16; + + Logger& logger = Logger::Instance(); + logger.Start(config); + logger.Write(LogLevel::Log, "test", "filtered"); + logger.Write(LogLevel::Warning, "test", "kept"); + logger.Write(LogLevel::Error, "test", "kept"); + logger.Stop(); + + const LoggerCounters counters = logger.Counters(); + Expect(counters.queued == 2, "logger queues only messages at or above minimum level"); + Expect(counters.written == 2, "logger writes queued messages before stop returns"); + Expect(counters.dropped == 0, "logger does not drop under queue capacity"); +} + +void TestTryWriteDropsWhenQueueIsFull() +{ + using namespace RenderCadenceCompositor; + + LoggerConfig config; + config.minimumLevel = LogLevel::Log; + config.writeToConsole = false; + config.writeToDebugOutput = false; + config.writeToFile = false; + config.maxQueuedMessages = 0; + + Logger& logger = Logger::Instance(); + logger.Start(config); + + const bool sawDrop = !logger.TryWrite(LogLevel::Log, "test", "message"); + logger.Stop(); + + Expect(sawDrop || logger.Counters().dropped > 0, "try-write reports or counts queue pressure"); +} +} + +int main() +{ + TestLevelNames(); + TestLevelFiltering(); + TestTryWriteDropsWhenQueueIsFull(); + + if (gFailures != 0) + { + std::cerr << gFailures << " RenderCadenceCompositorLogger test failure(s).\n"; + return 1; + } + + std::cout << "RenderCadenceCompositorLogger tests passed.\n"; + return 0; +}