UI updates and preroll buffer to 8 frames
This commit is contained in:
@@ -63,6 +63,7 @@ constexpr GLuint kDecodedVideoTextureUnit = 1;
|
|||||||
constexpr GLuint kSourceHistoryTextureUnitBase = 2;
|
constexpr GLuint kSourceHistoryTextureUnitBase = 2;
|
||||||
constexpr GLuint kPackedVideoTextureUnit = 2;
|
constexpr GLuint kPackedVideoTextureUnit = 2;
|
||||||
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||||
|
constexpr unsigned kPrerollFrameCount = 8;
|
||||||
const char* kVertexShaderSource =
|
const char* kVertexShaderSource =
|
||||||
"#version 430 core\n"
|
"#version 430 core\n"
|
||||||
"out vec2 vTexCoord;\n"
|
"out vec2 vTexCoord;\n"
|
||||||
@@ -1233,7 +1234,7 @@ bool OpenGLComposite::Start()
|
|||||||
mTotalPlayoutFrames = 0;
|
mTotalPlayoutFrames = 0;
|
||||||
|
|
||||||
// Preroll frames
|
// Preroll frames
|
||||||
for (unsigned i = 0; i < 5; i++)
|
for (unsigned i = 0; i < kPrerollFrameCount; i++)
|
||||||
{
|
{
|
||||||
// Take each video frame from the front of the queue and move it to the back
|
// Take each video frame from the front of the queue and move it to the back
|
||||||
IDeckLinkMutableVideoFrame* outputVideoFrame = mDLOutputVideoFrameQueue.front();
|
IDeckLinkMutableVideoFrame* outputVideoFrame = mDLOutputVideoFrameQueue.front();
|
||||||
|
|||||||
@@ -53,6 +53,25 @@ bool MatchesControlKey(const std::string& candidate, const std::string& key)
|
|||||||
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number)
|
||||||
|
{
|
||||||
|
const std::string prefix = "layer-";
|
||||||
|
if (layerId.rfind(prefix, 0) != 0 || layerId.size() == prefix.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint64_t parsed = 0;
|
||||||
|
for (std::size_t index = prefix.size(); index < layerId.size(); ++index)
|
||||||
|
{
|
||||||
|
const unsigned char ch = static_cast<unsigned char>(layerId[index]);
|
||||||
|
if (!std::isdigit(ch))
|
||||||
|
return false;
|
||||||
|
parsed = parsed * 10 + static_cast<uint64_t>(ch - '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
number = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
|
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
|
||||||
{
|
{
|
||||||
std::vector<double> numbers;
|
std::vector<double> numbers;
|
||||||
@@ -583,6 +602,7 @@ bool RuntimeHost::Initialize(std::string& error)
|
|||||||
return false;
|
return false;
|
||||||
if (!ScanShaderPackages(error))
|
if (!ScanShaderPackages(error))
|
||||||
return false;
|
return false;
|
||||||
|
NormalizePersistentLayerIdsLocked();
|
||||||
|
|
||||||
for (LayerPersistentState& layer : mPersistentState.layers)
|
for (LayerPersistentState& layer : mPersistentState.layers)
|
||||||
{
|
{
|
||||||
@@ -1469,15 +1489,31 @@ bool RuntimeHost::WriteTextFile(const std::filesystem::path& path, const std::st
|
|||||||
std::error_code fsError;
|
std::error_code fsError;
|
||||||
std::filesystem::create_directories(path.parent_path(), fsError);
|
std::filesystem::create_directories(path.parent_path(), fsError);
|
||||||
|
|
||||||
std::ofstream output(path, std::ios::binary);
|
const std::filesystem::path temporaryPath = path.string() + ".tmp";
|
||||||
|
std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc);
|
||||||
if (!output)
|
if (!output)
|
||||||
{
|
{
|
||||||
error = "Could not write file: " + path.string();
|
error = "Could not write file: " + temporaryPath.string();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
output << contents;
|
output << contents;
|
||||||
return output.good();
|
output.close();
|
||||||
|
if (!output.good())
|
||||||
|
{
|
||||||
|
error = "Could not finish writing file: " + temporaryPath.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MoveFileExA(temporaryPath.string().c_str(), path.string().c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
|
||||||
|
{
|
||||||
|
const DWORD lastError = GetLastError();
|
||||||
|
std::filesystem::remove(temporaryPath, fsError);
|
||||||
|
error = "Could not replace file: " + path.string() + " (Win32 error " + std::to_string(lastError) + ")";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeHost::ResolvePaths(std::string& error)
|
bool RuntimeHost::ResolvePaths(std::string& error)
|
||||||
@@ -1728,6 +1764,38 @@ bool RuntimeHost::DeserializeLayerStackLocked(const JsonValue& layersValue, std:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::NormalizePersistentLayerIdsLocked()
|
||||||
|
{
|
||||||
|
std::set<std::string> usedIds;
|
||||||
|
uint64_t maxLayerNumber = mNextLayerId;
|
||||||
|
|
||||||
|
for (LayerPersistentState& layer : mPersistentState.layers)
|
||||||
|
{
|
||||||
|
uint64_t layerNumber = 0;
|
||||||
|
const bool hasReusableId = !layer.id.empty() &&
|
||||||
|
usedIds.find(layer.id) == usedIds.end() &&
|
||||||
|
TryParseLayerIdNumber(layer.id, layerNumber);
|
||||||
|
|
||||||
|
if (hasReusableId)
|
||||||
|
{
|
||||||
|
usedIds.insert(layer.id);
|
||||||
|
maxLayerNumber = std::max(maxLayerNumber, layerNumber);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
++maxLayerNumber;
|
||||||
|
layer.id = "layer-" + std::to_string(maxLayerNumber);
|
||||||
|
}
|
||||||
|
while (usedIds.find(layer.id) != usedIds.end());
|
||||||
|
|
||||||
|
usedIds.insert(layer.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
mNextLayerId = maxLayerNumber;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> RuntimeHost::GetStackPresetNamesLocked() const
|
std::vector<std::string> RuntimeHost::GetStackPresetNamesLocked() const
|
||||||
{
|
{
|
||||||
std::vector<std::string> presetNames;
|
std::vector<std::string> presetNames;
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ private:
|
|||||||
JsonValue BuildStateValue() const;
|
JsonValue BuildStateValue() const;
|
||||||
JsonValue SerializeLayerStackLocked() const;
|
JsonValue SerializeLayerStackLocked() const;
|
||||||
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
|
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
|
||||||
|
void NormalizePersistentLayerIdsLocked();
|
||||||
std::vector<std::string> GetStackPresetNamesLocked() const;
|
std::vector<std::string> GetStackPresetNamesLocked() const;
|
||||||
std::string MakeSafePresetFileStem(const std::string& presetName) const;
|
std::string MakeSafePresetFileStem(const std::string& presetName) const;
|
||||||
JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const;
|
JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const;
|
||||||
|
|||||||
@@ -47,8 +47,11 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<main className="layout">
|
<main className="layout">
|
||||||
<section className="panel">
|
<section className="panel">
|
||||||
<h2>Loading</h2>
|
<h3>Loading</h3>
|
||||||
<p className="muted">Waiting for control state from the native host.</p>
|
<p className="muted">Waiting for control state from the native host.</p>
|
||||||
|
<div className="progress-track" aria-hidden="true">
|
||||||
|
<div className="progress-bar is-indeterminate" />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
@@ -58,7 +61,7 @@ function App() {
|
|||||||
<main className="layout">
|
<main className="layout">
|
||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
<div>
|
<div>
|
||||||
<h1>Video Shader Toys</h1>
|
<h2>Video Shader Toys</h2>
|
||||||
<p className="muted">Live shader stack, DeckLink status, and runtime controls.</p>
|
<p className="muted">Live shader stack, DeckLink status, and runtime controls.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={`status-pill${runtime.compileSucceeded ? " status-pill--ready" : " status-pill--error"}`}>
|
<div className={`status-pill${runtime.compileSucceeded ? " status-pill--ready" : " status-pill--error"}`}>
|
||||||
@@ -66,6 +69,27 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<section className="panel app-summary" aria-label="Runtime summary">
|
||||||
|
<dl className="summary-grid">
|
||||||
|
<div className="summary-item">
|
||||||
|
<dt>Shaders</dt>
|
||||||
|
<dd>{shaders.length}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="summary-item">
|
||||||
|
<dt>Layers</dt>
|
||||||
|
<dd>{layers.length}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="summary-item">
|
||||||
|
<dt>Signal</dt>
|
||||||
|
<dd>{video.hasSignal ? "Present" : "Missing"}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="summary-item">
|
||||||
|
<dt>Render</dt>
|
||||||
|
<dd>{Number(performance.renderMs ?? 0).toFixed(2)} ms</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="dashboard-grid">
|
<section className="dashboard-grid">
|
||||||
<StatusPanels app={app} performance={performance} runtime={runtime} video={video} />
|
<StatusPanels app={app} performance={performance} runtime={runtime} video={video} />
|
||||||
<StackPresetToolbar
|
<StackPresetToolbar
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
export function KvList({ values }) {
|
export function KvList({ values, variant = "cards" }) {
|
||||||
return (
|
return (
|
||||||
<dl className="kv">
|
<dl className={variant === "rows" ? "kv-rows" : "definition-grid compact"}>
|
||||||
{values.map(([key, value]) => (
|
{values.map(([key, value]) => (
|
||||||
<FragmentRow key={key} label={key} value={value} />
|
<div className={variant === "rows" ? "kv-row" : "definition-card"} key={key}>
|
||||||
|
<dt>{key}</dt>
|
||||||
|
<dd>{value}</dd>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FragmentRow({ label, value }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<dt>{label}</dt>
|
|
||||||
<dd>{value}</dd>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -83,8 +83,10 @@ export function LayerStack({
|
|||||||
return (
|
return (
|
||||||
<section className="panel">
|
<section className="panel">
|
||||||
<div className="panel__header">
|
<div className="panel__header">
|
||||||
<h2>Layers</h2>
|
<div>
|
||||||
<p className="muted">Drag layers to reorder them. Each layer processes the output of the one above it.</p>
|
<h3>Layers</h3>
|
||||||
|
<p className="muted">Drag layers to reorder them. Each layer processes the output of the one above it.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="layer-stack">
|
<div className="layer-stack">
|
||||||
|
|||||||
@@ -11,73 +11,73 @@ export function StackPresetToolbar({
|
|||||||
<div className="panel stack-panel">
|
<div className="panel stack-panel">
|
||||||
<div className="panel__header stack-panel__header">
|
<div className="panel__header stack-panel__header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Stack Presets</h2>
|
<h3>Stack presets</h3>
|
||||||
<p className="muted">Save or recall the current layer chain.</p>
|
<p className="muted">Save or recall the current layer chain.</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="stack-panel__reload" onClick={() => postJson("/api/reload", {})}>
|
<button type="button" className="stack-panel__reload" onClick={() => postJson("/api/reload", {})}>
|
||||||
Reload Shader
|
Reload shader
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stack-panel__grid">
|
<div className="stack-panel__grid">
|
||||||
<div className="toolbar__group">
|
<div className="toolbar__group">
|
||||||
<label htmlFor="preset-name">Save Stack</label>
|
<label htmlFor="preset-name">Save stack</label>
|
||||||
<div className="toolbar__inline">
|
<div className="toolbar__inline">
|
||||||
<input
|
<input
|
||||||
id="preset-name"
|
id="preset-name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Preset name"
|
placeholder="Preset name"
|
||||||
value={presetName}
|
value={presetName}
|
||||||
onChange={(event) => onPresetNameChange(event.target.value)}
|
onChange={(event) => onPresetNameChange(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!presetName.trim()}
|
disabled={!presetName.trim()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const trimmedName = presetName.trim();
|
const trimmedName = presetName.trim();
|
||||||
if (!trimmedName) {
|
if (!trimmedName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
postJson("/api/stack-presets/save", { presetName: trimmedName });
|
postJson("/api/stack-presets/save", { presetName: trimmedName });
|
||||||
onSelectedPresetNameChange(
|
onSelectedPresetNameChange(
|
||||||
trimmedName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""),
|
trimmedName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="toolbar__group">
|
<div className="toolbar__group">
|
||||||
<label htmlFor="preset-select">Recall Stack</label>
|
<label htmlFor="preset-select">Recall stack</label>
|
||||||
<div className="toolbar__inline">
|
<div className="toolbar__inline">
|
||||||
<select
|
<select
|
||||||
id="preset-select"
|
id="preset-select"
|
||||||
value={selectedPresetName}
|
value={selectedPresetName}
|
||||||
onChange={(event) => onSelectedPresetNameChange(event.target.value)}
|
onChange={(event) => onSelectedPresetNameChange(event.target.value)}
|
||||||
>
|
>
|
||||||
{stackPresets.length === 0 ? <option value="">No presets</option> : null}
|
{stackPresets.length === 0 ? <option value="">No presets</option> : null}
|
||||||
{stackPresets.map((preset) => (
|
{stackPresets.map((preset) => (
|
||||||
<option key={preset} value={preset}>
|
<option key={preset} value={preset}>
|
||||||
{preset}
|
{preset}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!selectedPresetName}
|
disabled={!selectedPresetName}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectedPresetName) {
|
if (selectedPresetName) {
|
||||||
postJson("/api/stack-presets/load", { presetName: selectedPresetName });
|
postJson("/api/stack-presets/load", { presetName: selectedPresetName });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Recall
|
Recall
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,40 +5,66 @@ function formatNumber(value, digits = 3) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function StatusPanels({ app, performance, runtime, video }) {
|
export function StatusPanels({ app, performance, runtime, video }) {
|
||||||
|
const budgetUsedPercent = Math.max(0, Math.min(100, Number(performance.budgetUsedPercent) || 0));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="panel panel--runtime">
|
<div className="panel panel--telemetry">
|
||||||
<h2>Runtime</h2>
|
<div className="telemetry-header">
|
||||||
<KvList
|
<h3>Status</h3>
|
||||||
values={[
|
<div className="status-badges" aria-label="Current status">
|
||||||
["Layer Count", `${runtime.layerCount || 0}`],
|
<span className={`mini-status${runtime.compileSucceeded ? " mini-status--ready" : " mini-status--error"}`}>
|
||||||
["Auto Reload", app.autoReload ? "On" : "Off"],
|
{runtime.compileSucceeded ? "Ready" : "Error"}
|
||||||
["Temporal Cap", `${app.maxTemporalHistoryFrames ?? 0}`],
|
</span>
|
||||||
["Control URL", `http://127.0.0.1:${app.serverPort}`],
|
<span className={`mini-status${video.hasSignal ? " mini-status--ready" : " mini-status--error"}`}>
|
||||||
["Compile Status", runtime.compileSucceeded ? "Ready" : "Error"],
|
{video.hasSignal ? "Signal" : "No signal"}
|
||||||
["Render Time", `${formatNumber(performance.renderMs, 2)} ms`],
|
</span>
|
||||||
["Smoothed Time", `${formatNumber(performance.smoothedRenderMs, 2)} ms`],
|
</div>
|
||||||
["Frame Budget", `${formatNumber(performance.frameBudgetMs, 2)} ms`],
|
</div>
|
||||||
["Budget Used", `${formatNumber(performance.budgetUsedPercent, 1)}%`],
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="panel panel--video">
|
<div className="telemetry-sections">
|
||||||
<h2>Video</h2>
|
<section className="telemetry-section" aria-labelledby="runtime-status-heading">
|
||||||
<KvList
|
<h4 id="runtime-status-heading">Runtime</h4>
|
||||||
values={[
|
<KvList
|
||||||
["Signal", video.hasSignal ? "Present" : "Missing"],
|
variant="rows"
|
||||||
["Input Mode", video.modeName || "Unknown"],
|
values={[
|
||||||
["Input Resolution", `${video.width || 0} x ${video.height || 0}`],
|
["Layers", `${runtime.layerCount || 0}`],
|
||||||
["Output Mode", `${app.outputVideoFormat || "Unknown"}${app.outputFrameRate ? ` ${app.outputFrameRate}` : ""}`],
|
["Auto reload", app.autoReload ? "On" : "Off"],
|
||||||
]}
|
["Temporal cap", `${app.maxTemporalHistoryFrames ?? 0}`],
|
||||||
/>
|
["Control URL", `127.0.0.1:${app.serverPort}`],
|
||||||
|
["Render", `${formatNumber(performance.renderMs, 2)} ms`],
|
||||||
|
["Smoothed", `${formatNumber(performance.smoothedRenderMs, 2)} ms`],
|
||||||
|
["Frame budget", `${formatNumber(performance.frameBudgetMs, 2)} ms`],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="meter-row">
|
||||||
|
<span>Budget used</span>
|
||||||
|
<div className="progress-track" aria-hidden="true">
|
||||||
|
<div className="progress-bar" style={{ width: `${budgetUsedPercent}%` }} />
|
||||||
|
</div>
|
||||||
|
<strong>{formatNumber(performance.budgetUsedPercent, 1)}%</strong>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="telemetry-section" aria-labelledby="video-status-heading">
|
||||||
|
<h4 id="video-status-heading">Video</h4>
|
||||||
|
<KvList
|
||||||
|
variant="rows"
|
||||||
|
values={[
|
||||||
|
["Input mode", video.modeName || "Unknown"],
|
||||||
|
["Input size", `${video.width || 0} x ${video.height || 0}`],
|
||||||
|
["Output", `${app.outputVideoFormat || "Unknown"}${app.outputFrameRate ? ` ${app.outputFrameRate}` : ""}`],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="panel panel--compiler">
|
<div className="panel panel--compiler">
|
||||||
<h2>Compiler</h2>
|
<h3>Compiler</h3>
|
||||||
<pre>{runtime.compileMessage || "No compiler output."}</pre>
|
<pre className="log-panel" aria-live="polite">
|
||||||
|
{runtime.compileMessage || "No compiler output."}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user