From aa33d72b6e6bf076f83affac412723b2267e57f5 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Sat, 30 May 2026 20:20:22 +1000 Subject: [PATCH] config UI updates --- config/runtime-host.json | 31 +++---- src/runtime/layers/RuntimeLayerSnapshot.cpp | 81 +++++++++++++++++-- .../shader/RuntimeSlangShaderCompiler.cpp | 19 ++++- ...adenceCompositorRuntimeLayerModelTests.cpp | 37 +++++++++ ui/src/components/ConfigEditor.jsx | 52 ++++++++---- ui/src/components/StatusPanels.jsx | 2 +- ui/src/styles.css | 2 +- 7 files changed, 187 insertions(+), 37 deletions(-) diff --git a/config/runtime-host.json b/config/runtime-host.json index 868a1da..7b4f7f8 100644 --- a/config/runtime-host.json +++ b/config/runtime-host.json @@ -1,29 +1,30 @@ { "$schema": "./runtime-host.schema.json", - "shaderLibrary": "shaders", - "serverPort": 8080, - "oscBindAddress": "0.0.0.0", - "oscPort": 9000, - "oscSmoothing": 0.18, + "autoReload": true, "input": { "backend": "ndi", "device": "AIDENLAPTOP (Test Pattern)", - "resolution": "1080p", - "frameRate": "59.94" + "frameRate": "59.94", + "resolution": "1080p" }, + "maxTemporalHistoryFrames": 12, + "oscBindAddress": "0.0.0.0", + "oscPort": 9000, + "oscSmoothing": 0.18, "output": { "backend": "ndi", "device": "Shader", - "resolution": "1080p", "frameRate": "59.94", - "pixelFormat": "auto", "keying": { - "external": false, - "alphaRequired": true - } + "alphaRequired": false, + "external": false + }, + "pixelFormat": "auto", + "resolution": "1080p" }, - "autoReload": true, - "maxTemporalHistoryFrames": 12, "previewEnabled": true, - "previewFps": 59.94 + "previewFps": 59.94, + "runtimeShaderId": "happy-accident", + "serverPort": 8080, + "shaderLibrary": "shaders" } diff --git a/src/runtime/layers/RuntimeLayerSnapshot.cpp b/src/runtime/layers/RuntimeLayerSnapshot.cpp index 536321d..24157bc 100644 --- a/src/runtime/layers/RuntimeLayerSnapshot.cpp +++ b/src/runtime/layers/RuntimeLayerSnapshot.cpp @@ -1,21 +1,65 @@ #include "RuntimeLayerModel.h" +#include #include namespace RenderCadenceCompositor { +namespace +{ +const char* LayerShaderLabel(std::size_t count) +{ + return count == 1 ? "layer shader" : "layer shaders"; +} + +const char* BuildStateLabel(RuntimeLayerBuildState state) +{ + switch (state) + { + case RuntimeLayerBuildState::Ready: + return "ready"; + case RuntimeLayerBuildState::Failed: + return "failed"; + case RuntimeLayerBuildState::Pending: + default: + return "pending"; + } +} +} + RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const { RuntimeLayerModelSnapshot snapshot; snapshot.compileSucceeded = true; + std::size_t readyCount = 0; + std::size_t failedCount = 0; + std::size_t pendingCount = 0; + std::vector layerMessages; - for (const Layer& layer : mLayers) + for (std::size_t layerIndex = 0; layerIndex < mLayers.size(); ++layerIndex) { + const Layer& layer = mLayers[layerIndex]; snapshot.displayLayers.push_back(ToReadModel(layer)); - if (!layer.message.empty() && snapshot.compileMessage.empty()) - snapshot.compileMessage = layer.message; if (layer.buildState == RuntimeLayerBuildState::Failed) + { snapshot.compileSucceeded = false; + ++failedCount; + } + else if (layer.buildState == RuntimeLayerBuildState::Ready) + { + ++readyCount; + } + else + { + ++pendingCount; + } + if (!layer.message.empty()) + { + std::ostringstream line; + line << "Layer " << (layerIndex + 1) << " " << (layer.shaderName.empty() ? layer.shaderId : layer.shaderName) + << " (" << BuildStateLabel(layer.buildState) << "): " << layer.message; + layerMessages.push_back(line.str()); + } if (layer.renderReady) { RuntimeRenderLayerModel renderLayer; @@ -30,8 +74,35 @@ RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const } } - if (snapshot.compileMessage.empty()) - snapshot.compileMessage = mLayers.empty() ? "Runtime shader build disabled." : "Runtime shader build has not completed yet."; + if (mLayers.empty()) + { + snapshot.compileMessage = "Runtime shader build disabled."; + } + else + { + std::ostringstream message; + if (failedCount > 0) + { + message << "Runtime stack build has failures: " << readyCount << "/" << mLayers.size() << " " + << LayerShaderLabel(mLayers.size()) << " render-ready, " << failedCount << " failed"; + if (pendingCount > 0) + message << ", " << pendingCount << " pending"; + message << "."; + } + else if (pendingCount > 0) + { + message << "Runtime stack build in progress: " << readyCount << "/" << mLayers.size() << " " + << LayerShaderLabel(mLayers.size()) << " render-ready, " << pendingCount << " pending."; + } + else + { + message << "Runtime stack ready: " << readyCount << "/" << mLayers.size() << " " + << LayerShaderLabel(mLayers.size()) << " compiled and render-ready."; + } + for (const std::string& layerMessage : layerMessages) + message << "\n" << layerMessage; + snapshot.compileMessage = message.str(); + } return snapshot; } diff --git a/src/runtime/shader/RuntimeSlangShaderCompiler.cpp b/src/runtime/shader/RuntimeSlangShaderCompiler.cpp index 4df89cc..a747b36 100644 --- a/src/runtime/shader/RuntimeSlangShaderCompiler.cpp +++ b/src/runtime/shader/RuntimeSlangShaderCompiler.cpp @@ -7,6 +7,8 @@ #include #include +#include +#include namespace { @@ -28,6 +30,18 @@ std::filesystem::path FindRepoRoot() } } +const char* PluralSuffix(std::size_t count) +{ + return count == 1 ? "" : "s"; +} + +std::string FormatMilliseconds(double milliseconds) +{ + std::ostringstream output; + output << std::fixed << std::setprecision(2) << milliseconds; + return output.str(); +} + } RuntimeSlangShaderCompiler::~RuntimeSlangShaderCompiler() @@ -159,7 +173,10 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin build.artifact.fontAtlases = std::move(fontAtlasOutputs); if (!build.artifact.passes.empty()) build.artifact.fragmentShaderSource = build.artifact.passes.front().fragmentShaderSource; - build.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms."; + build.artifact.message = + shaderPackage.displayName + " package build completed: " + + std::to_string(build.artifact.passes.size()) + " Slang pass" + PluralSuffix(build.artifact.passes.size()) + + " compiled in " + FormatMilliseconds(milliseconds) + " ms."; build.message = build.artifact.message; return build; } diff --git a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp index 44aef40..2b39b14 100644 --- a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp +++ b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp @@ -222,6 +222,42 @@ void TestAddAndRemoveLayers() std::filesystem::remove_all(root); } +void TestSnapshotCompileMessageSummarizesLayerStack() +{ + 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 for summary test"); + Expect(model.AddLayer(catalog, "solid", secondLayerId, error), "second layer can be added for summary test"); + + RuntimeShaderArtifact firstArtifact; + firstArtifact.layerId = firstLayerId; + firstArtifact.shaderId = "solid"; + firstArtifact.displayName = "Solid"; + firstArtifact.fragmentShaderSource = "void main(){}"; + firstArtifact.message = "Solid package build completed: 1 Slang pass compiled in 10.00 ms."; + Expect(model.MarkBuildReady(firstArtifact, error), "first ready artifact updates summary test model"); + + RuntimeShaderArtifact secondArtifact = firstArtifact; + secondArtifact.layerId = secondLayerId; + secondArtifact.message = "Solid package build completed: 1 Slang pass compiled in 11.00 ms."; + Expect(model.MarkBuildReady(secondArtifact, error), "second ready artifact updates summary test model"); + + const RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); + Expect(snapshot.compileMessage.find("Runtime stack ready: 2/2 layer shaders compiled and render-ready.") != std::string::npos, + "compile message summarizes the whole layer stack"); + Expect(snapshot.compileMessage.find("Layer 1 Solid (ready): Solid package build completed") != std::string::npos, + "compile message includes first layer detail"); + Expect(snapshot.compileMessage.find("Layer 2 Solid (ready): Solid package build completed") != std::string::npos, + "compile message includes second layer detail"); + + std::filesystem::remove_all(root); +} + void TestInitializeFromRuntimeStateRestoresLayerStack() { std::filesystem::path root = MakeTestRoot(); @@ -542,6 +578,7 @@ int main() TestRejectsUnsupportedStartupShader(); TestBuildFailureStaysDisplaySide(); TestAddAndRemoveLayers(); + TestSnapshotCompileMessageSummarizesLayerStack(); TestInitializeFromRuntimeStateRestoresLayerStack(); TestInvalidRuntimeStateCanFallBackToConfiguredShader(); TestLayerControlsUpdateDisplayAndRenderModels(); diff --git a/ui/src/components/ConfigEditor.jsx b/ui/src/components/ConfigEditor.jsx index c02d2e8..31c95f8 100644 --- a/ui/src/components/ConfigEditor.jsx +++ b/ui/src/components/ConfigEditor.jsx @@ -1,4 +1,4 @@ -import { Power, RefreshCw, Save, X } from "lucide-react"; +import { Pencil, Power, RefreshCw, Save, X } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { fetchJson, postJsonResult } from "../api/controlApi"; @@ -50,7 +50,15 @@ function TextField({ config, label, path, setConfig }) { ); } -function InputDeviceField({ config, ndiSources, ndiSourcesBusy, onRefreshNdiSources, setConfig }) { +function InputDeviceField({ + config, + manualOpen, + ndiSources, + ndiSourcesBusy, + onManualOpenChange, + onRefreshNdiSources, + setConfig, +}) { if (readPath(config, "input.backend") !== "ndi") { return ; } @@ -61,7 +69,9 @@ function InputDeviceField({ config, ndiSources, ndiSourcesBusy, onRefreshNdiSour (name) => !presetOptions.includes(name) ); const selectOptions = [...presetOptions, ...discoveredOptions]; - const selectValue = selectOptions.includes(currentDevice) ? currentDevice : currentDevice ? "__current__" : "default"; + const customDevice = Boolean(currentDevice) && !selectOptions.includes(currentDevice); + const selectValue = customDevice ? "__custom__" : currentDevice || "default"; + const showManualField = manualOpen || customDevice; return ( @@ -70,10 +80,12 @@ function InputDeviceField({ config, ndiSources, ndiSourcesBusy, onRefreshNdiSour setConfig((current) => writePath(current, "input.device", event.target.value))} - /> + {showManualField && ( + setConfig((current) => writePath(current, "input.device", event.target.value))} + /> + )} ); @@ -203,6 +222,7 @@ export function ConfigEditor({ onClose }) { const [restartRequired, setRestartRequired] = useState(false); const [status, setStatus] = useState(""); const [busy, setBusy] = useState(false); + const [manualInputDeviceOpen, setManualInputDeviceOpen] = useState(false); const [ndiSources, setNdiSources] = useState([]); const [ndiSourcesBusy, setNdiSourcesBusy] = useState(false); @@ -249,6 +269,8 @@ export function ConfigEditor({ onClose }) { useEffect(() => { if (inputBackend === "ndi") { loadNdiSources(); + } else { + setManualInputDeviceOpen(false); } }, [inputBackend]); @@ -340,8 +362,10 @@ export function ConfigEditor({ onClose }) { diff --git a/ui/src/components/StatusPanels.jsx b/ui/src/components/StatusPanels.jsx index 546eb2a..53d8478 100644 --- a/ui/src/components/StatusPanels.jsx +++ b/ui/src/components/StatusPanels.jsx @@ -74,7 +74,7 @@ export function StatusPanels({ app, performance, runtime, video, videoOutput }) { label: "Output", value: formatEndpoint(app.output), - meta: `${formatVideoMode(app.output)} / ${video.width || 0} x ${video.height || 0}`, + meta: formatVideoMode(app.output), }, { label: "Schedule", diff --git a/ui/src/styles.css b/ui/src/styles.css index dab9c1b..a614131 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -667,7 +667,7 @@ pre { .config-device-picker__source { display: grid; - grid-template-columns: minmax(0, 1fr) 38px; + grid-template-columns: minmax(0, 1fr) 38px 38px; gap: 0.45rem; align-items: stretch; }