config UI updates
This commit is contained in:
@@ -1,29 +1,30 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./runtime-host.schema.json",
|
"$schema": "./runtime-host.schema.json",
|
||||||
"shaderLibrary": "shaders",
|
"autoReload": true,
|
||||||
"serverPort": 8080,
|
|
||||||
"oscBindAddress": "0.0.0.0",
|
|
||||||
"oscPort": 9000,
|
|
||||||
"oscSmoothing": 0.18,
|
|
||||||
"input": {
|
"input": {
|
||||||
"backend": "ndi",
|
"backend": "ndi",
|
||||||
"device": "AIDENLAPTOP (Test Pattern)",
|
"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": {
|
"output": {
|
||||||
"backend": "ndi",
|
"backend": "ndi",
|
||||||
"device": "Shader",
|
"device": "Shader",
|
||||||
"resolution": "1080p",
|
|
||||||
"frameRate": "59.94",
|
"frameRate": "59.94",
|
||||||
"pixelFormat": "auto",
|
|
||||||
"keying": {
|
"keying": {
|
||||||
"external": false,
|
"alphaRequired": false,
|
||||||
"alphaRequired": true
|
"external": false
|
||||||
}
|
},
|
||||||
|
"pixelFormat": "auto",
|
||||||
|
"resolution": "1080p"
|
||||||
},
|
},
|
||||||
"autoReload": true,
|
|
||||||
"maxTemporalHistoryFrames": 12,
|
|
||||||
"previewEnabled": true,
|
"previewEnabled": true,
|
||||||
"previewFps": 59.94
|
"previewFps": 59.94,
|
||||||
|
"runtimeShaderId": "happy-accident",
|
||||||
|
"serverPort": 8080,
|
||||||
|
"shaderLibrary": "shaders"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,65 @@
|
|||||||
#include "RuntimeLayerModel.h"
|
#include "RuntimeLayerModel.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace RenderCadenceCompositor
|
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 RuntimeLayerModel::Snapshot() const
|
||||||
{
|
{
|
||||||
RuntimeLayerModelSnapshot snapshot;
|
RuntimeLayerModelSnapshot snapshot;
|
||||||
snapshot.compileSucceeded = true;
|
snapshot.compileSucceeded = true;
|
||||||
|
std::size_t readyCount = 0;
|
||||||
|
std::size_t failedCount = 0;
|
||||||
|
std::size_t pendingCount = 0;
|
||||||
|
std::vector<std::string> 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));
|
snapshot.displayLayers.push_back(ToReadModel(layer));
|
||||||
if (!layer.message.empty() && snapshot.compileMessage.empty())
|
|
||||||
snapshot.compileMessage = layer.message;
|
|
||||||
if (layer.buildState == RuntimeLayerBuildState::Failed)
|
if (layer.buildState == RuntimeLayerBuildState::Failed)
|
||||||
|
{
|
||||||
snapshot.compileSucceeded = false;
|
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)
|
if (layer.renderReady)
|
||||||
{
|
{
|
||||||
RuntimeRenderLayerModel renderLayer;
|
RuntimeRenderLayerModel renderLayer;
|
||||||
@@ -30,8 +74,35 @@ RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.compileMessage.empty())
|
if (mLayers.empty())
|
||||||
snapshot.compileMessage = mLayers.empty() ? "Runtime shader build disabled." : "Runtime shader build has not completed yet.";
|
{
|
||||||
|
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;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
namespace
|
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()
|
RuntimeSlangShaderCompiler::~RuntimeSlangShaderCompiler()
|
||||||
@@ -159,7 +173,10 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
|
|||||||
build.artifact.fontAtlases = std::move(fontAtlasOutputs);
|
build.artifact.fontAtlases = std::move(fontAtlasOutputs);
|
||||||
if (!build.artifact.passes.empty())
|
if (!build.artifact.passes.empty())
|
||||||
build.artifact.fragmentShaderSource = build.artifact.passes.front().fragmentShaderSource;
|
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;
|
build.message = build.artifact.message;
|
||||||
return build;
|
return build;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,6 +222,42 @@ void TestAddAndRemoveLayers()
|
|||||||
std::filesystem::remove_all(root);
|
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()
|
void TestInitializeFromRuntimeStateRestoresLayerStack()
|
||||||
{
|
{
|
||||||
std::filesystem::path root = MakeTestRoot();
|
std::filesystem::path root = MakeTestRoot();
|
||||||
@@ -542,6 +578,7 @@ int main()
|
|||||||
TestRejectsUnsupportedStartupShader();
|
TestRejectsUnsupportedStartupShader();
|
||||||
TestBuildFailureStaysDisplaySide();
|
TestBuildFailureStaysDisplaySide();
|
||||||
TestAddAndRemoveLayers();
|
TestAddAndRemoveLayers();
|
||||||
|
TestSnapshotCompileMessageSummarizesLayerStack();
|
||||||
TestInitializeFromRuntimeStateRestoresLayerStack();
|
TestInitializeFromRuntimeStateRestoresLayerStack();
|
||||||
TestInvalidRuntimeStateCanFallBackToConfiguredShader();
|
TestInvalidRuntimeStateCanFallBackToConfiguredShader();
|
||||||
TestLayerControlsUpdateDisplayAndRenderModels();
|
TestLayerControlsUpdateDisplayAndRenderModels();
|
||||||
|
|||||||
@@ -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 { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { fetchJson, postJsonResult } from "../api/controlApi";
|
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") {
|
if (readPath(config, "input.backend") !== "ndi") {
|
||||||
return <TextField config={config} label="Device" path="input.device" setConfig={setConfig} />;
|
return <TextField config={config} label="Device" path="input.device" setConfig={setConfig} />;
|
||||||
}
|
}
|
||||||
@@ -61,7 +69,9 @@ function InputDeviceField({ config, ndiSources, ndiSourcesBusy, onRefreshNdiSour
|
|||||||
(name) => !presetOptions.includes(name)
|
(name) => !presetOptions.includes(name)
|
||||||
);
|
);
|
||||||
const selectOptions = [...presetOptions, ...discoveredOptions];
|
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 (
|
return (
|
||||||
<Field label="Device">
|
<Field label="Device">
|
||||||
@@ -70,10 +80,12 @@ function InputDeviceField({ config, ndiSources, ndiSourcesBusy, onRefreshNdiSour
|
|||||||
<select
|
<select
|
||||||
value={selectValue}
|
value={selectValue}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
if (event.target.value === "__current__") return;
|
if (event.target.value === "__custom__") return;
|
||||||
|
onManualOpenChange(false);
|
||||||
setConfig((current) => writePath(current, "input.device", event.target.value));
|
setConfig((current) => writePath(current, "input.device", event.target.value));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{customDevice && <option value="__custom__">Custom source</option>}
|
||||||
<optgroup label="Preset">
|
<optgroup label="Preset">
|
||||||
{presetOptions.map((option) => (
|
{presetOptions.map((option) => (
|
||||||
<option key={option} value={option}>
|
<option key={option} value={option}>
|
||||||
@@ -81,9 +93,6 @@ function InputDeviceField({ config, ndiSources, ndiSourcesBusy, onRefreshNdiSour
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
{currentDevice && !selectOptions.includes(currentDevice) && (
|
|
||||||
<option value="__current__">Current: {currentDevice}</option>
|
|
||||||
)}
|
|
||||||
<optgroup label="Discovered NDI sources">
|
<optgroup label="Discovered NDI sources">
|
||||||
{discoveredOptions.length > 0 ? (
|
{discoveredOptions.length > 0 ? (
|
||||||
discoveredOptions.map((sourceName) => (
|
discoveredOptions.map((sourceName) => (
|
||||||
@@ -107,7 +116,16 @@ function InputDeviceField({ config, ndiSources, ndiSourcesBusy, onRefreshNdiSour
|
|||||||
>
|
>
|
||||||
<RefreshCw size={16} strokeWidth={1.9} aria-hidden="true" />
|
<RefreshCw size={16} strokeWidth={1.9} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-button"
|
||||||
|
onClick={() => onManualOpenChange(!manualOpen)}
|
||||||
|
title={showManualField ? "Hide manual source entry" : "Edit source manually"}
|
||||||
|
>
|
||||||
|
<Pencil size={15} strokeWidth={1.9} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{showManualField && (
|
||||||
<input
|
<input
|
||||||
aria-label="Manual NDI source name"
|
aria-label="Manual NDI source name"
|
||||||
placeholder="Manual source name"
|
placeholder="Manual source name"
|
||||||
@@ -115,6 +133,7 @@ function InputDeviceField({ config, ndiSources, ndiSourcesBusy, onRefreshNdiSour
|
|||||||
value={currentDevice}
|
value={currentDevice}
|
||||||
onChange={(event) => setConfig((current) => writePath(current, "input.device", event.target.value))}
|
onChange={(event) => setConfig((current) => writePath(current, "input.device", event.target.value))}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
@@ -203,6 +222,7 @@ export function ConfigEditor({ onClose }) {
|
|||||||
const [restartRequired, setRestartRequired] = useState(false);
|
const [restartRequired, setRestartRequired] = useState(false);
|
||||||
const [status, setStatus] = useState("");
|
const [status, setStatus] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [manualInputDeviceOpen, setManualInputDeviceOpen] = useState(false);
|
||||||
const [ndiSources, setNdiSources] = useState([]);
|
const [ndiSources, setNdiSources] = useState([]);
|
||||||
const [ndiSourcesBusy, setNdiSourcesBusy] = useState(false);
|
const [ndiSourcesBusy, setNdiSourcesBusy] = useState(false);
|
||||||
|
|
||||||
@@ -249,6 +269,8 @@ export function ConfigEditor({ onClose }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputBackend === "ndi") {
|
if (inputBackend === "ndi") {
|
||||||
loadNdiSources();
|
loadNdiSources();
|
||||||
|
} else {
|
||||||
|
setManualInputDeviceOpen(false);
|
||||||
}
|
}
|
||||||
}, [inputBackend]);
|
}, [inputBackend]);
|
||||||
|
|
||||||
@@ -340,8 +362,10 @@ export function ConfigEditor({ onClose }) {
|
|||||||
<SelectField config={draft} label="Backend" options={backendOptions} path="input.backend" setConfig={setDraft} />
|
<SelectField config={draft} label="Backend" options={backendOptions} path="input.backend" setConfig={setDraft} />
|
||||||
<InputDeviceField
|
<InputDeviceField
|
||||||
config={draft}
|
config={draft}
|
||||||
|
manualOpen={manualInputDeviceOpen}
|
||||||
ndiSources={ndiSources}
|
ndiSources={ndiSources}
|
||||||
ndiSourcesBusy={ndiSourcesBusy}
|
ndiSourcesBusy={ndiSourcesBusy}
|
||||||
|
onManualOpenChange={setManualInputDeviceOpen}
|
||||||
onRefreshNdiSources={loadNdiSources}
|
onRefreshNdiSources={loadNdiSources}
|
||||||
setConfig={setDraft}
|
setConfig={setDraft}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function StatusPanels({ app, performance, runtime, video, videoOutput })
|
|||||||
{
|
{
|
||||||
label: "Output",
|
label: "Output",
|
||||||
value: formatEndpoint(app.output),
|
value: formatEndpoint(app.output),
|
||||||
meta: `${formatVideoMode(app.output)} / ${video.width || 0} x ${video.height || 0}`,
|
meta: formatVideoMode(app.output),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Schedule",
|
label: "Schedule",
|
||||||
|
|||||||
@@ -667,7 +667,7 @@ pre {
|
|||||||
|
|
||||||
.config-device-picker__source {
|
.config-device-picker__source {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 38px;
|
grid-template-columns: minmax(0, 1fr) 38px 38px;
|
||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user