#include "RuntimeLayerModel.h" #include #include #include #include #include namespace { int gFailures = 0; void Expect(bool condition, const std::string& message) { if (condition) return; ++gFailures; std::cerr << "FAIL: " << message << "\n"; } std::filesystem::path MakeTestRoot() { const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); const std::filesystem::path root = std::filesystem::temp_directory_path() / ("render-cadence-layer-model-tests-" + std::to_string(stamp)); std::filesystem::create_directories(root); return root; } void WriteFile(const std::filesystem::path& path, const std::string& contents) { std::filesystem::create_directories(path.parent_path()); std::ofstream output(path, std::ios::binary); output << contents; } std::string SolidShaderManifest(double gainDefault, bool includeMix) { std::string parameters = "{ \"id\": \"gain\", \"label\": \"Gain\", \"type\": \"float\", \"default\": " + std::to_string(gainDefault) + " },\n" "\t\t\t{ \"id\": \"drop\", \"label\": \"Drop\", \"type\": \"trigger\" }"; if (includeMix) parameters += ",\n\t\t\t{ \"id\": \"mix\", \"label\": \"Mix\", \"type\": \"float\", \"default\": 0.25 }"; return R"({ "id": "solid", "name": "Solid", "description": "Solid test shader", "category": "Tests", "entryPoint": "shadeVideo", "parameters": [ )" + parameters + R"( ] })"; } std::string AllParametersShaderManifest() { return R"({ "id": "all-params", "name": "All Params", "description": "All parameter restore test shader", "category": "Tests", "entryPoint": "shadeVideo", "fonts": [{ "id": "inter", "path": "Inter.ttf" }], "parameters": [ { "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0.0, "max": 1.0 }, { "id": "offset", "label": "Offset", "type": "vec2", "default": [0.0, 0.0], "min": [-1.0, -1.0], "max": [1.0, 1.0] }, { "id": "tint", "label": "Tint", "type": "color", "default": [1.0, 1.0, 1.0, 1.0], "min": [0.0, 0.0, 0.0, 0.0], "max": [1.0, 1.0, 1.0, 1.0] }, { "id": "enabled", "label": "Enabled", "type": "bool", "default": true }, { "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [ { "value": "soft", "label": "Soft" }, { "value": "hard", "label": "Hard" } ] }, { "id": "titleText", "label": "Title", "type": "text", "default": "DEFAULT", "font": "inter", "maxLength": 8 }, { "id": "drop", "label": "Drop", "type": "trigger" } ] })"; } RenderCadenceCompositor::SupportedShaderCatalog LoadCatalog(const std::filesystem::path& root) { RenderCadenceCompositor::SupportedShaderCatalog catalog; std::string error; Expect(catalog.Load(root, 4, error), error.empty() ? "catalog loads test shader" : error); return catalog; } RenderCadenceCompositor::SupportedShaderCatalog MakeCatalog(std::filesystem::path& root) { root = MakeTestRoot(); WriteFile(root / "solid" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n"); WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.5, false)); return LoadCatalog(root); } RenderCadenceCompositor::FontAtlasBuildOutput MakeFakeFontAtlas() { RenderCadenceCompositor::FontAtlasBuildOutput atlas; atlas.fontId = "inter"; atlas.width = 2; atlas.height = 2; atlas.ascender = -0.8; atlas.rgbaPixels = { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }; RenderCadenceCompositor::FontAtlasBuildOutput::Glyph glyph; glyph.advance = 0.5; glyph.planeLeft = 0.0; glyph.planeTop = 0.0; glyph.planeRight = 0.4; glyph.planeBottom = 0.6; glyph.atlasLeft = 0.0; glyph.atlasTop = 0.0; glyph.atlasRight = 1.0; glyph.atlasBottom = 1.0; glyph.hasBounds = true; atlas.glyphsByCodepoint['A'] = glyph; atlas.glyphsByCodepoint['B'] = glyph; atlas.glyphsByCodepoint['D'] = glyph; atlas.glyphsByCodepoint['E'] = glyph; atlas.glyphsByCodepoint['F'] = glyph; atlas.glyphsByCodepoint['L'] = glyph; atlas.glyphsByCodepoint['T'] = glyph; atlas.glyphsByCodepoint['U'] = glyph; return atlas; } void TestSingleLayerLifecycle() { std::filesystem::path root; RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); RenderCadenceCompositor::RuntimeLayerModel model; std::string error; Expect(model.InitializeSingleLayer(catalog, "solid", error), "model initializes a supported startup shader"); Expect(model.FirstLayerId() == "runtime-layer-1", "startup layer id is stable"); Expect(model.MarkBuildStarted(model.FirstLayerId(), "build started", error), "build start updates the layer"); RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); Expect(snapshot.displayLayers.size() == 1, "snapshot exposes the display layer"); Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Pending, "started layer is pending"); Expect(!snapshot.displayLayers[0].renderReady, "started layer is not render-ready yet"); RuntimeShaderArtifact artifact; artifact.shaderId = "solid"; artifact.displayName = "Solid"; artifact.fragmentShaderSource = "void main(){}"; artifact.message = "build ready"; Expect(model.MarkBuildReady(artifact, error), "ready artifact updates the matching layer"); snapshot = model.Snapshot(); Expect(snapshot.compileSucceeded, "ready layer reports compile success"); Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Ready, "ready layer is marked ready"); Expect(snapshot.displayLayers[0].renderReady, "ready layer exposes render readiness"); Expect(snapshot.renderLayers.size() == 1, "ready layer produces one render layer artifact"); Expect(snapshot.renderLayers[0].artifact.shaderId == "solid", "render layer carries the artifact"); std::filesystem::remove_all(root); } void TestRejectsUnsupportedStartupShader() { std::filesystem::path root; RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); RenderCadenceCompositor::RuntimeLayerModel model; std::string error; Expect(!model.InitializeSingleLayer(catalog, "missing", error), "model rejects unsupported shader ids"); Expect(!error.empty(), "unsupported shader rejection explains the problem"); Expect(model.Snapshot().displayLayers.empty(), "rejected startup shader leaves no display layer"); std::filesystem::remove_all(root); } void TestBuildFailureStaysDisplaySide() { std::filesystem::path root; RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); RenderCadenceCompositor::RuntimeLayerModel model; std::string error; Expect(model.InitializeSingleLayer(catalog, "solid", error), "model initializes for failure test"); Expect(model.MarkBuildFailed(model.FirstLayerId(), "compile failed", error), "build failure updates the layer"); const RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); Expect(!snapshot.compileSucceeded, "failed layer reports compile failure"); Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Failed, "failed layer is marked failed"); Expect(!snapshot.displayLayers[0].renderReady, "failed layer is not render-ready"); Expect(snapshot.renderLayers.empty(), "failed layer does not produce a render artifact"); std::filesystem::remove_all(root); } void TestAddAndRemoveLayers() { std::filesystem::path root; RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); RenderCadenceCompositor::RuntimeLayerModel model; std::string error; std::string firstLayerId; std::string secondLayerId; Expect(model.AddLayer(catalog, "solid", firstLayerId, error), "first layer can be added"); Expect(model.AddLayer(catalog, "solid", secondLayerId, error), "second layer can be added"); Expect(firstLayerId != secondLayerId, "added layers receive distinct ids"); Expect(model.Snapshot().displayLayers.size() == 2, "added layers appear in display snapshot"); Expect(model.RemoveLayer(firstLayerId, error), "existing layer can be removed"); RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); Expect(snapshot.displayLayers.size() == 1, "removed layer leaves snapshot"); Expect(snapshot.displayLayers[0].id == secondLayerId, "remaining layer identity is preserved"); Expect(!model.RemoveLayer(firstLayerId, error), "removed layer cannot be removed twice"); std::filesystem::remove_all(root); } void TestInitializeFromRuntimeStateRestoresLayerStack() { std::filesystem::path root = MakeTestRoot(); WriteFile(root / "solid" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n"); WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.5, false)); WriteFile(root / "all-params" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n"); WriteFile(root / "all-params" / "Inter.ttf", "not a real font, but enough for restore catalog support checks"); WriteFile(root / "all-params" / "shader.json", AllParametersShaderManifest()); RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root); JsonValue runtimeState; std::string error; Expect(ParseJson(R"({ "layers": [ { "id": "layer-31", "shaderId": "all-params", "bypass": true, "parameterValues": { "gain": 0.75, "offset": [0.25, -0.5], "tint": [0.1, 0.2, 0.3, 0.4], "enabled": false, "mode": "hard", "titleText": "RESTORED-TEXT", "drop": 4 } }, { "id": "layer-32", "shaderId": "missing", "parameterValues": { "gain": 0.9 } }, { "id": "layer-33", "shaderId": "solid", "parameterValues": { "gain": "bad" } } ] })", runtimeState, error), "runtime state fixture parses"); RenderCadenceCompositor::RuntimeLayerModel model; Expect(model.InitializeFromRuntimeState(catalog, runtimeState, error), "runtime state can initialize the layer model"); RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); Expect(snapshot.displayLayers.size() == 2, "restore keeps supported layers and skips missing shaders"); Expect(snapshot.displayLayers[0].id == "layer-31", "restore preserves saved layer id"); Expect(snapshot.displayLayers[0].shaderId == "all-params", "restore preserves shader id"); Expect(snapshot.displayLayers[0].bypass, "restore preserves bypass state"); Expect(snapshot.displayLayers[0].parameterValues.at("gain").numberValues.front() == 0.75, "restore preserves valid parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("offset").numberValues == std::vector({ 0.25, -0.5 }), "restore preserves vec2 parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("tint").numberValues == std::vector({ 0.1, 0.2, 0.3, 0.4 }), "restore preserves color parameter values"); Expect(!snapshot.displayLayers[0].parameterValues.at("enabled").booleanValue, "restore preserves boolean parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("mode").enumValue == "hard", "restore preserves enum parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("titleText").textValue == "RESTORED", "restore normalizes and preserves text parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("drop").numberValues.front() == 4.0, "restore preserves trigger counts"); Expect(snapshot.displayLayers[1].id == "layer-33", "restore preserves later supported layer order"); Expect(snapshot.displayLayers[1].shaderId == "solid", "restore preserves later layer shader id"); Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.5, "restore falls back to defaults for invalid parameter values"); const std::vector> builds = model.PendingLayerBuilds(); Expect(builds.size() == 2 && builds[0].first == "layer-31" && builds[0].second == "all-params" && builds[1].first == "layer-33" && builds[1].second == "solid", "restore queues startup builds for every restored layer in order"); std::filesystem::remove_all(root); } void TestInvalidRuntimeStateCanFallBackToConfiguredShader() { std::filesystem::path root; RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); JsonValue invalidRuntimeState = JsonValue::MakeObject(); invalidRuntimeState.set("layers", JsonValue("not-an-array")); RenderCadenceCompositor::RuntimeLayerModel model; std::string error; Expect(!model.InitializeFromRuntimeState(catalog, invalidRuntimeState, error), "invalid runtime state is rejected"); Expect(model.InitializeSingleLayer(catalog, "solid", error), "configured default shader can initialize after invalid runtime state"); RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); Expect(snapshot.displayLayers.size() == 1, "fallback startup creates one default layer"); Expect(snapshot.displayLayers[0].shaderId == "solid", "fallback layer uses the configured shader id"); Expect(snapshot.displayLayers[0].parameterValues.at("gain").numberValues.front() == 0.5, "fallback layer uses shader defaults"); std::filesystem::remove_all(root); } void TestLayerControlsUpdateDisplayAndRenderModels() { std::filesystem::path root; RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); RenderCadenceCompositor::RuntimeLayerModel model; std::string error; std::string firstLayerId; std::string secondLayerId; Expect(model.AddLayer(catalog, "solid", firstLayerId, error), "first control layer can be added"); Expect(model.AddLayer(catalog, "solid", secondLayerId, error), "second control layer can be added"); Expect(model.SetLayerBypass(firstLayerId, true, error), "bypass can be set"); Expect(model.ReorderLayer(firstLayerId, 1, error), "layer can be reordered"); RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); Expect(snapshot.displayLayers[1].id == firstLayerId, "reordered layer moves to requested index"); Expect(snapshot.displayLayers[1].bypass, "bypass state is visible in read model"); JsonValue gainValue(0.75); Expect(model.UpdateParameter(firstLayerId, "gain", gainValue, error), "parameter value can be updated"); snapshot = model.Snapshot(); Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.75, "updated parameter value is visible"); JsonValue dropPulse(true); Expect(model.UpdateParameter(firstLayerId, "drop", dropPulse, error), "trigger parameter can be pulsed"); snapshot = model.Snapshot(); const std::vector firstTrigger = snapshot.displayLayers[1].parameterValues.at("drop").numberValues; Expect(firstTrigger.size() == 2 && firstTrigger[0] == 1.0 && firstTrigger[1] >= 0.0, "trigger pulse increments count and records runtime time"); Expect(model.UpdateParameter(firstLayerId, "drop", dropPulse, error), "trigger parameter can be pulsed again"); snapshot = model.Snapshot(); const std::vector secondTrigger = snapshot.displayLayers[1].parameterValues.at("drop").numberValues; Expect(secondTrigger.size() == 2 && secondTrigger[0] == 2.0 && secondTrigger[1] >= firstTrigger[1], "second trigger pulse increments count again"); RuntimeShaderArtifact artifact; artifact.layerId = firstLayerId; artifact.shaderId = "solid"; artifact.displayName = "Solid"; artifact.fragmentShaderSource = "void main(){}"; artifact.parameterDefinitions = snapshot.displayLayers[1].parameterDefinitions; artifact.message = "build ready"; Expect(model.MarkBuildReady(artifact, error), "ready artifact keeps layer parameter state"); snapshot = model.Snapshot(); Expect(snapshot.renderLayers.size() == 1, "ready layer produces render model"); Expect(snapshot.renderLayers[0].bypass, "render model carries bypass state"); Expect(snapshot.renderLayers[0].artifact.parameterValues.at("gain").numberValues.front() == 0.75, "render artifact carries updated parameter value"); Expect(model.ResetParameters(firstLayerId, error), "parameters can reset to defaults"); snapshot = model.Snapshot(); Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.5, "reset restores default value"); std::filesystem::remove_all(root); } void TestReloadRefreshesChangedShaderMetadataAndPreservesValues() { std::filesystem::path root; RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); WriteFile(root / "passthrough" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 1.0, 1.0); }\n"); WriteFile(root / "passthrough" / "shader.json", R"({ "id": "passthrough", "name": "Passthrough", "description": "Passthrough test shader", "category": "Tests", "entryPoint": "shadeVideo", "parameters": [ { "id": "opacity", "label": "Opacity", "type": "float", "default": 1.0 } ] })"); catalog = LoadCatalog(root); RenderCadenceCompositor::RuntimeLayerModel model; std::string error; std::string layerId; std::string unchangedLayerId; Expect(model.AddLayer(catalog, "solid", layerId, error), "reload test layer can be added"); Expect(model.AddLayer(catalog, "passthrough", unchangedLayerId, error), "reload unchanged layer can be added"); Expect(model.UpdateParameter(layerId, "gain", JsonValue(0.75), error), "reload test parameter can be customized"); WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.1, true)); RenderCadenceCompositor::SupportedShaderCatalog reloadedCatalog = LoadCatalog(root); std::vector> buildsToStart; Expect(model.ReloadFromCatalog(reloadedCatalog, buildsToStart, error), "reload refreshes model from changed catalog"); Expect(buildsToStart.size() == 2 && buildsToStart[0].first == layerId && buildsToStart[0].second == "solid" && buildsToStart[1].first == unchangedLayerId && buildsToStart[1].second == "passthrough", "reload queues every available layer rebuild in order"); RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); Expect(snapshot.displayLayers[0].parameterDefinitions.size() == 3, "reload exposes new JSON parameter definitions to UI"); Expect(snapshot.displayLayers[0].parameterValues.at("gain").numberValues.front() == 0.75, "reload preserves compatible parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("mix").numberValues.front() == 0.25, "reload initializes newly added parameters"); Expect(snapshot.displayLayers[1].parameterValues.at("opacity").numberValues.front() == 1.0, "reload keeps unchanged layer parameter values"); std::filesystem::remove_all(root); } void TestTextTexturesArePreparedInRuntimeModel() { std::filesystem::path root = MakeTestRoot(); WriteFile(root / "all-params" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n"); WriteFile(root / "all-params" / "Inter.ttf", "not a real font, but enough for catalog support checks"); WriteFile(root / "all-params" / "shader.json", AllParametersShaderManifest()); RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root); RenderCadenceCompositor::RuntimeLayerModel model; std::string error; Expect(model.InitializeSingleLayer(catalog, "all-params", error), "text layer can initialize"); RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); RuntimeShaderArtifact artifact; artifact.layerId = model.FirstLayerId(); artifact.shaderId = "all-params"; artifact.displayName = "All Params"; artifact.fragmentShaderSource = "void main(){}"; artifact.parameterDefinitions = snapshot.displayLayers[0].parameterDefinitions; artifact.fontAtlases.push_back(MakeFakeFontAtlas()); artifact.message = "build ready"; Expect(model.MarkBuildReady(artifact, error), error.empty() ? "ready text artifact prepares textures" : error); snapshot = model.Snapshot(); Expect(snapshot.renderLayers.size() == 1, "text artifact is render-ready"); Expect(snapshot.renderLayers[0].artifact.preparedTextTextures.size() == 1, "render snapshot carries prepared text texture"); Expect(snapshot.renderLayers[0].artifact.fontAtlases.empty(), "render snapshot does not carry font atlas pixels"); const RuntimePreparedTextTexture preparedDefault = snapshot.renderLayers[0].artifact.preparedTextTextures[0]; Expect(preparedDefault.textValue == "DEFAULT", "default text is prepared"); Expect(preparedDefault.rgbaPixels && !preparedDefault.rgbaPixels->empty(), "prepared text has pixels"); Expect(model.UpdateParameter(model.FirstLayerId(), "titleText", JsonValue("AB"), error), error.empty() ? "text parameter update prepares texture" : error); snapshot = model.Snapshot(); const RuntimePreparedTextTexture preparedUpdated = snapshot.renderLayers[0].artifact.preparedTextTextures[0]; Expect(preparedUpdated.textValue == "AB", "updated text is prepared before render snapshot"); Expect(preparedUpdated.rgbaPixels && preparedUpdated.rgbaPixels != preparedDefault.rgbaPixels, "updated text receives a new prepared pixel payload"); std::filesystem::remove_all(root); } } int main() { TestSingleLayerLifecycle(); TestRejectsUnsupportedStartupShader(); TestBuildFailureStaysDisplaySide(); TestAddAndRemoveLayers(); TestInitializeFromRuntimeStateRestoresLayerStack(); TestInvalidRuntimeStateCanFallBackToConfiguredShader(); TestLayerControlsUpdateDisplayAndRenderModels(); TestReloadRefreshesChangedShaderMetadataAndPreservesValues(); TestTextTexturesArePreparedInRuntimeModel(); if (gFailures != 0) { std::cerr << gFailures << " RenderCadenceCompositorRuntimeLayerModel test failure(s).\n"; return 1; } std::cout << "RenderCadenceCompositorRuntimeLayerModel tests passed.\n"; return 0; }