diff --git a/CMakeLists.txt b/CMakeLists.txt index 98258c6..d3a43de 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}/json/JsonWriter.cpp" + "${RENDER_CADENCE_APP_DIR}/json/JsonWriter.h" "${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp" "${RENDER_CADENCE_APP_DIR}/logging/Logger.h" "${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.cpp" @@ -330,6 +332,7 @@ set(RENDER_CADENCE_APP_SOURCES "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeShaderBridge.h" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.cpp" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.h" + "${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetryJson.h" "${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetry.h" "${RENDER_CADENCE_APP_DIR}/telemetry/TelemetryPrinter.h" "${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.cpp" @@ -351,6 +354,7 @@ target_include_directories(RenderCadenceCompositor PRIVATE "${RENDER_CADENCE_APP_DIR}" "${RENDER_CADENCE_APP_DIR}/app" "${RENDER_CADENCE_APP_DIR}/frames" + "${RENDER_CADENCE_APP_DIR}/json" "${RENDER_CADENCE_APP_DIR}/logging" "${RENDER_CADENCE_APP_DIR}/platform" "${RENDER_CADENCE_APP_DIR}/render" @@ -775,10 +779,12 @@ endif() add_test(NAME RenderCadenceCompositorClockTests COMMAND RenderCadenceCompositorClockTests) add_executable(RenderCadenceCompositorTelemetryTests + "${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorTelemetryTests.cpp" ) target_include_directories(RenderCadenceCompositorTelemetryTests PRIVATE + "${RENDER_CADENCE_APP_DIR}/json" "${RENDER_CADENCE_APP_DIR}/telemetry" ) @@ -821,6 +827,21 @@ endif() add_test(NAME RenderCadenceCompositorLoggerTests COMMAND RenderCadenceCompositorLoggerTests) +add_executable(RenderCadenceCompositorJsonWriterTests + "${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorJsonWriterTests.cpp" +) + +target_include_directories(RenderCadenceCompositorJsonWriterTests PRIVATE + "${RENDER_CADENCE_APP_DIR}/json" +) + +if(MSVC) + target_compile_options(RenderCadenceCompositorJsonWriterTests PRIVATE /W3) +endif() + +add_test(NAME RenderCadenceCompositorJsonWriterTests COMMAND RenderCadenceCompositorJsonWriterTests) + 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 32bd279..ebcc1d0 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -44,6 +44,8 @@ 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 +- small JSON writer for future HTTP/WebSocket payloads +- JSON serialization for cadence telemetry snapshots - background logging with `log`, `warning`, and `error` levels - compact telemetry - non-GL frame-exchange tests @@ -189,6 +191,7 @@ This app keeps the same core behavior but splits it into modules that can grow: - `frames/`: system-memory handoff - `platform/`: COM/Win32/hidden GL context support - `render/`: cadence, simple rendering, PBO readback +- `json/`: compact JSON serialization helpers - `video/`: DeckLink output wrapper and scheduling thread - `telemetry/`: cadence telemetry - `app/`: startup/shutdown orchestration diff --git a/apps/RenderCadenceCompositor/json/JsonWriter.cpp b/apps/RenderCadenceCompositor/json/JsonWriter.cpp new file mode 100644 index 0000000..bec29d9 --- /dev/null +++ b/apps/RenderCadenceCompositor/json/JsonWriter.cpp @@ -0,0 +1,235 @@ +#include "JsonWriter.h" + +#include +#include + +namespace RenderCadenceCompositor +{ +namespace +{ +constexpr int kMaxDepth = 32; + +void AppendHexEscape(std::ostringstream& stream, unsigned char value) +{ + stream << "\\u" + << std::hex << std::uppercase << std::setw(4) << std::setfill('0') + << static_cast(value) + << std::dec << std::nouppercase << std::setfill(' '); +} +} + +void JsonWriter::BeginObject() +{ + BeginValue(); + mStream << "{"; + PushScope(ScopeKind::Object); +} + +void JsonWriter::EndObject() +{ + PopScope(ScopeKind::Object); + mStream << "}"; +} + +void JsonWriter::BeginArray() +{ + BeginValue(); + mStream << "["; + PushScope(ScopeKind::Array); +} + +void JsonWriter::EndArray() +{ + PopScope(ScopeKind::Array); + mStream << "]"; +} + +void JsonWriter::Key(const std::string& name) +{ + BeginKey(); + mStream << "\"" << EscapeString(name) << "\":"; +} + +void JsonWriter::String(const std::string& value) +{ + BeginValue(); + mStream << "\"" << EscapeString(value) << "\""; +} + +void JsonWriter::Bool(bool value) +{ + BeginValue(); + mStream << (value ? "true" : "false"); +} + +void JsonWriter::Null() +{ + BeginValue(); + mStream << "null"; +} + +void JsonWriter::Int(int64_t value) +{ + BeginValue(); + mStream << value; +} + +void JsonWriter::UInt(uint64_t value) +{ + BeginValue(); + mStream << value; +} + +void JsonWriter::Double(double value) +{ + BeginValue(); + mStream << std::setprecision(15) << value; +} + +void JsonWriter::KeyString(const std::string& name, const std::string& value) +{ + Key(name); + String(value); +} + +void JsonWriter::KeyBool(const std::string& name, bool value) +{ + Key(name); + Bool(value); +} + +void JsonWriter::KeyNull(const std::string& name) +{ + Key(name); + Null(); +} + +void JsonWriter::KeyInt(const std::string& name, int64_t value) +{ + Key(name); + Int(value); +} + +void JsonWriter::KeyUInt(const std::string& name, uint64_t value) +{ + Key(name); + UInt(value); +} + +void JsonWriter::KeyDouble(const std::string& name, double value) +{ + Key(name); + Double(value); +} + +std::string JsonWriter::StringValue() const +{ + if (mScopeDepth != 0) + throw std::logic_error("JSON document has unclosed scopes."); + return mStream.str(); +} + +void JsonWriter::Reset() +{ + mStream.str(std::string()); + mStream.clear(); + mScopeDepth = 0; +} + +std::string JsonWriter::EscapeString(const std::string& value) +{ + std::ostringstream stream; + for (unsigned char character : value) + { + switch (character) + { + case '"': + stream << "\\\""; + break; + case '\\': + stream << "\\\\"; + break; + case '\b': + stream << "\\b"; + break; + case '\f': + stream << "\\f"; + break; + case '\n': + stream << "\\n"; + break; + case '\r': + stream << "\\r"; + break; + case '\t': + stream << "\\t"; + break; + default: + if (character < 0x20) + AppendHexEscape(stream, character); + else + stream << character; + break; + } + } + return stream.str(); +} + +void JsonWriter::BeginValue() +{ + if (mScopeDepth == 0) + return; + + Scope& scope = mScopes[mScopeDepth - 1]; + if (scope.kind == ScopeKind::Object) + { + if (!scope.expectingValue) + throw std::logic_error("JSON object value must follow a key."); + scope.expectingValue = false; + return; + } + + if (!scope.first) + mStream << ","; + scope.first = false; +} + +void JsonWriter::BeginKey() +{ + if (mScopeDepth == 0) + throw std::logic_error("JSON key cannot be written outside an object."); + + Scope& scope = mScopes[mScopeDepth - 1]; + if (scope.kind != ScopeKind::Object) + throw std::logic_error("JSON key cannot be written inside an array."); + if (scope.expectingValue) + throw std::logic_error("JSON object key cannot be written before its previous value."); + + if (!scope.first) + mStream << ","; + scope.first = false; + scope.expectingValue = true; +} + +void JsonWriter::PushScope(ScopeKind kind) +{ + if (mScopeDepth >= kMaxDepth) + throw std::logic_error("JSON nesting is too deep."); + + mScopes[mScopeDepth++] = Scope{ kind, true, false }; +} + +void JsonWriter::PopScope(ScopeKind kind) +{ + if (mScopeDepth == 0) + throw std::logic_error("JSON scope underflow."); + + Scope& scope = mScopes[mScopeDepth - 1]; + if (scope.kind != kind) + throw std::logic_error("JSON scope kind mismatch."); + if (scope.expectingValue) + throw std::logic_error("JSON object key is missing a value."); + + --mScopeDepth; +} +} diff --git a/apps/RenderCadenceCompositor/json/JsonWriter.h b/apps/RenderCadenceCompositor/json/JsonWriter.h new file mode 100644 index 0000000..4b24ea4 --- /dev/null +++ b/apps/RenderCadenceCompositor/json/JsonWriter.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +namespace RenderCadenceCompositor +{ +class JsonWriter +{ +public: + void BeginObject(); + void EndObject(); + void BeginArray(); + void EndArray(); + + void Key(const std::string& name); + void String(const std::string& value); + void Bool(bool value); + void Null(); + void Int(int64_t value); + void UInt(uint64_t value); + void Double(double value); + + void KeyString(const std::string& name, const std::string& value); + void KeyBool(const std::string& name, bool value); + void KeyNull(const std::string& name); + void KeyInt(const std::string& name, int64_t value); + void KeyUInt(const std::string& name, uint64_t value); + void KeyDouble(const std::string& name, double value); + + std::string StringValue() const; + void Reset(); + + static std::string EscapeString(const std::string& value); + +private: + enum class ScopeKind + { + Object, + Array + }; + + struct Scope + { + ScopeKind kind = ScopeKind::Object; + bool first = true; + bool expectingValue = false; + }; + + void BeginValue(); + void BeginKey(); + void PushScope(ScopeKind kind); + void PopScope(ScopeKind kind); + + std::ostringstream mStream; + Scope mScopes[32]; + int mScopeDepth = 0; +}; +} diff --git a/apps/RenderCadenceCompositor/telemetry/CadenceTelemetryJson.h b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetryJson.h new file mode 100644 index 0000000..66c3034 --- /dev/null +++ b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetryJson.h @@ -0,0 +1,45 @@ +#pragma once + +#include "CadenceTelemetry.h" +#include "../json/JsonWriter.h" + +#include +#include + +namespace RenderCadenceCompositor +{ +inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetrySnapshot& snapshot) +{ + writer.BeginObject(); + writer.KeyDouble("sampleSeconds", snapshot.sampleSeconds); + writer.KeyDouble("renderFps", snapshot.renderFps); + writer.KeyDouble("scheduleFps", snapshot.scheduleFps); + writer.KeyUInt("free", static_cast(snapshot.freeFrames)); + writer.KeyUInt("completed", static_cast(snapshot.completedFrames)); + writer.KeyUInt("scheduled", static_cast(snapshot.scheduledFrames)); + writer.KeyUInt("renderedTotal", snapshot.renderedTotal); + writer.KeyUInt("scheduledTotal", snapshot.scheduledTotal); + writer.KeyUInt("completedPollMisses", snapshot.completedPollMisses); + writer.KeyUInt("scheduleFailures", snapshot.scheduleFailures); + writer.KeyUInt("completions", snapshot.completions); + writer.KeyUInt("late", snapshot.displayedLate); + writer.KeyUInt("dropped", snapshot.dropped); + writer.KeyUInt("shaderCommitted", snapshot.shaderBuildsCommitted); + writer.KeyUInt("shaderFailures", snapshot.shaderBuildFailures); + writer.KeyBool("deckLinkBufferedAvailable", snapshot.deckLinkBufferedAvailable); + writer.Key("deckLinkBuffered"); + if (snapshot.deckLinkBufferedAvailable) + writer.UInt(snapshot.deckLinkBuffered); + else + writer.Null(); + writer.KeyDouble("scheduleCallMs", snapshot.deckLinkScheduleCallMilliseconds); + writer.EndObject(); +} + +inline std::string CadenceTelemetryToJson(const CadenceTelemetrySnapshot& snapshot) +{ + JsonWriter writer; + WriteCadenceTelemetryJson(writer, snapshot); + return writer.StringValue(); +} +} diff --git a/tests/RenderCadenceCompositorJsonWriterTests.cpp b/tests/RenderCadenceCompositorJsonWriterTests.cpp new file mode 100644 index 0000000..5561f42 --- /dev/null +++ b/tests/RenderCadenceCompositorJsonWriterTests.cpp @@ -0,0 +1,123 @@ +#include "JsonWriter.h" + +#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 ExpectEquals(const std::string& actual, const std::string& expected, const std::string& message) +{ + if (actual == expected) + return; + + ++gFailures; + std::cerr << "FAILED: " << message << "\n" + << "expected: " << expected << "\n" + << "actual: " << actual << "\n"; +} + +void TestEscapesStrings() +{ + using RenderCadenceCompositor::JsonWriter; + + ExpectEquals( + JsonWriter::EscapeString("quote\" slash\\ newline\n tab\t"), + "quote\\\" slash\\\\ newline\\n tab\\t", + "string escape handles common escaped characters"); + + std::string control; + control.push_back(static_cast(0x01)); + ExpectEquals(JsonWriter::EscapeString(control), "\\u0001", "string escape handles control characters"); +} + +void TestObjectSerialization() +{ + using RenderCadenceCompositor::JsonWriter; + + JsonWriter writer; + writer.BeginObject(); + writer.KeyString("name", "cadence"); + writer.KeyDouble("renderFps", 59.94); + writer.KeyBool("healthy", true); + writer.KeyNull("error"); + writer.EndObject(); + + ExpectEquals( + writer.StringValue(), + "{\"name\":\"cadence\",\"renderFps\":59.94,\"healthy\":true,\"error\":null}", + "object serialization is compact and ordered"); +} + +void TestNestedArrays() +{ + using RenderCadenceCompositor::JsonWriter; + + JsonWriter writer; + writer.BeginObject(); + writer.Key("levels"); + writer.BeginArray(); + writer.String("log"); + writer.String("warning"); + writer.String("error"); + writer.EndArray(); + writer.Key("counts"); + writer.BeginObject(); + writer.KeyUInt("queued", 3); + writer.KeyInt("delta", -1); + writer.EndObject(); + writer.EndObject(); + + ExpectEquals( + writer.StringValue(), + "{\"levels\":[\"log\",\"warning\",\"error\"],\"counts\":{\"queued\":3,\"delta\":-1}}", + "nested arrays and objects serialize correctly"); +} + +void TestMisuseThrows() +{ + using RenderCadenceCompositor::JsonWriter; + + JsonWriter writer; + bool threw = false; + try + { + writer.BeginObject(); + writer.Key("missing"); + writer.EndObject(); + } + catch (const std::logic_error&) + { + threw = true; + } + Expect(threw, "ending an object with a key missing a value throws"); +} +} + +int main() +{ + TestEscapesStrings(); + TestObjectSerialization(); + TestNestedArrays(); + TestMisuseThrows(); + + if (gFailures != 0) + { + std::cerr << gFailures << " RenderCadenceCompositorJsonWriter test failure(s).\n"; + return 1; + } + + std::cout << "RenderCadenceCompositorJsonWriter tests passed.\n"; + return 0; +} diff --git a/tests/RenderCadenceCompositorTelemetryTests.cpp b/tests/RenderCadenceCompositorTelemetryTests.cpp index 2fa0756..e92354e 100644 --- a/tests/RenderCadenceCompositorTelemetryTests.cpp +++ b/tests/RenderCadenceCompositorTelemetryTests.cpp @@ -1,4 +1,5 @@ #include "CadenceTelemetry.h" +#include "CadenceTelemetryJson.h" #include #include @@ -128,12 +129,60 @@ void TestTelemetryComputesRatesFromDeltas() Expect(snapshot.renderFps > 0.0, "render fps is computed from completed frame delta"); Expect(snapshot.scheduleFps > 0.0, "schedule fps is computed from scheduled frame delta"); } + +void TestTelemetrySerializesToJson() +{ + RenderCadenceCompositor::CadenceTelemetrySnapshot snapshot; + snapshot.sampleSeconds = 1.0; + snapshot.renderFps = 59.94; + snapshot.scheduleFps = 60.0; + snapshot.freeFrames = 7; + snapshot.completedFrames = 1; + snapshot.scheduledFrames = 4; + snapshot.renderedTotal = 120; + snapshot.scheduledTotal = 118; + snapshot.completedPollMisses = 3; + snapshot.scheduleFailures = 0; + snapshot.completions = 117; + snapshot.displayedLate = 1; + snapshot.dropped = 2; + snapshot.shaderBuildsCommitted = 1; + snapshot.shaderBuildFailures = 0; + snapshot.deckLinkBufferedAvailable = true; + snapshot.deckLinkBuffered = 4; + snapshot.deckLinkScheduleCallMilliseconds = 1.25; + + const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot); + const std::string expected = + "{\"sampleSeconds\":1,\"renderFps\":59.94,\"scheduleFps\":60," + "\"free\":7,\"completed\":1,\"scheduled\":4," + "\"renderedTotal\":120,\"scheduledTotal\":118," + "\"completedPollMisses\":3,\"scheduleFailures\":0," + "\"completions\":117,\"late\":1,\"dropped\":2," + "\"shaderCommitted\":1,\"shaderFailures\":0," + "\"deckLinkBufferedAvailable\":true,\"deckLinkBuffered\":4," + "\"scheduleCallMs\":1.25}"; + Expect(json == expected, "telemetry snapshot serializes to stable JSON"); +} + +void TestUnavailableDeckLinkBufferSerializesAsNull() +{ + RenderCadenceCompositor::CadenceTelemetrySnapshot snapshot; + snapshot.deckLinkBufferedAvailable = false; + + const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot); + Expect( + json.find("\"deckLinkBufferedAvailable\":false,\"deckLinkBuffered\":null") != std::string::npos, + "unavailable DeckLink buffer depth serializes as null"); +} } int main() { TestTelemetrySamplesCompletedPollMissesAndShaderCounts(); TestTelemetryComputesRatesFromDeltas(); + TestTelemetrySerializesToJson(); + TestUnavailableDeckLinkBufferSerializesAsNull(); if (gFailures != 0) {