UI fix
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m26s
CI / Windows Release Package (push) Successful in 2m28s

This commit is contained in:
Aiden
2026-05-10 21:27:13 +10:00
parent 8fcb51d140
commit 46129a6044
6 changed files with 142 additions and 76 deletions

View File

@@ -131,6 +131,22 @@ bool RuntimeServices::QueueOscCommit(const std::string& routeKey, const std::str
return true;
}
void RuntimeServices::ClearOscState()
{
{
std::lock_guard<std::mutex> lock(mPendingOscMutex);
mPendingOscUpdates.clear();
}
{
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
mPendingOscCommits.clear();
}
{
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
mCompletedOscCommits.clear();
}
}
void RuntimeServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
{
completedCommits.clear();

View File

@@ -51,6 +51,7 @@ public:
bool QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
bool ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error);
bool QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error);
void ClearOscState();
void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits);
RuntimePollEvents ConsumePollEvents();

View File

@@ -1,4 +1,5 @@
#include "OpenGLComposite.h"
#include "RuntimeServices.h"
std::string OpenGLComposite::GetRuntimeStateJson() const
{
@@ -126,6 +127,11 @@ bool OpenGLComposite::ResetLayerParameters(const std::string& layerId, std::stri
if (!mRuntimeHost->ResetLayerParameters(layerId, error))
return false;
mOscOverlayStates.clear();
if (mRuntimeServices)
mRuntimeServices->ClearOscState();
resetTemporalHistoryState();
broadcastRuntimeState();
return true;
}

View File

@@ -1,80 +1,110 @@
{
"id": "feedback-highlight-accumulator",
"name": "Feedback Highlight Accumulator",
"description": "Demonstrates shader-local feedback by storing a rolling 4-frame per-pixel highlight history in RGBA and displaying only the newest 3 frames. The highlight selection is an approximation based on a luminance threshold rather than an exact percentile reduction.",
"name": "Feedback Background Memory",
"description": "Demonstrates writable full-frame shader feedback by learning a persistent per-pixel background model, then comparing the live frame against that learned plate. This cannot be reproduced by only reading ordinary history frames because the shader writes its own evolving state back each frame.",
"category": "Feedback",
"entryPoint": "buildHistory",
"entryPoint": "updateBackgroundModel",
"passes": [
{
"id": "history",
"id": "background",
"source": "shader.slang",
"entryPoint": "buildHistory",
"output": "historyBuffer"
"entryPoint": "updateBackgroundModel",
"output": "backgroundModel"
},
{
"id": "display",
"source": "shader.slang",
"entryPoint": "displayHistory",
"entryPoint": "displayBackgroundDifference",
"inputs": [
"historyBuffer"
"backgroundModel"
],
"output": "layerOutput"
}
],
"feedback": {
"enabled": true,
"writePass": "history"
"writePass": "background"
},
"parameters": [
{
"id": "highlightThreshold",
"label": "Highlight Threshold",
"id": "learnRate",
"label": "Learn Rate",
"type": "float",
"default": 0.92,
"min": 0.5,
"max": 1.5,
"default": 0.03,
"min": 0.001,
"max": 0.5,
"step": 0.001,
"description": "Approximate cutoff for the brightest parts of the frame."
"description": "How quickly the stored background model adapts toward the current frame."
},
{
"id": "differenceThreshold",
"label": "Difference Threshold",
"type": "float",
"default": 0.12,
"min": 0.001,
"max": 1.0,
"step": 0.001,
"description": "Minimum difference between the live frame and stored background before the overlay becomes visible."
},
{
"id": "softness",
"label": "Threshold Softness",
"type": "float",
"default": 0.05,
"default": 0.08,
"min": 0.001,
"max": 0.25,
"max": 0.5,
"step": 0.001,
"description": "Softens the threshold so near-highlights can contribute gradually."
"description": "Softens the transition around the difference threshold."
},
{
"id": "accumulateAmount",
"label": "Contribution Amount",
"id": "overlayOpacity",
"label": "Overlay Opacity",
"type": "float",
"default": 0.6,
"min": 0.0,
"max": 2.0,
"step": 0.001,
"description": "Strength of each frame's highlight contribution before it is shifted into the rolling history."
},
{
"id": "displayGain",
"label": "Display Gain",
"type": "float",
"default": 2.5,
"min": 0.0,
"max": 8.0,
"step": 0.01,
"description": "Brightness applied to the sum of the most recent three stored highlight samples."
},
{
"id": "baseMix",
"label": "Base Mix",
"type": "float",
"default": 0.25,
"default": 0.85,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"description": "Amount of the live source image kept under the recent highlight history display."
"description": "Strength of the motion/difference overlay on top of the live image."
},
{
"id": "backgroundMix",
"label": "Background Mix",
"type": "float",
"default": 0.15,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"description": "Amount of the learned background model shown underneath the live source."
},
{
"id": "overlayTint",
"label": "Overlay Tint",
"type": "color",
"default": [
1.0,
0.45,
0.08,
1.0
],
"min": [
0.0,
0.0,
0.0,
0.0
],
"max": [
1.0,
1.0,
1.0,
1.0
],
"step": [
0.01,
0.01,
0.01,
0.01
],
"description": "Tint used for areas that differ from the learned background."
}
]
}

View File

@@ -3,45 +3,37 @@ float luminance(float3 color)
return dot(color, float3(0.2126, 0.7152, 0.0722));
}
float4 buildHistory(ShaderContext context)
float4 updateBackgroundModel(ShaderContext context)
{
float4 previousFeedback = sampleFeedback(context.uv);
float4 previousHistory = context.feedbackAvailable > 0
? previousFeedback
: float4(0.0, 0.0, 0.0, 0.0);
float3 liveColor = context.sourceColor.rgb;
if (context.feedbackAvailable <= 0)
return float4(liveColor, 1.0);
// Highlight selection is always based on the live source image only.
// The stored feedback never feeds back into the threshold test.
float currentLuma = luminance(context.sourceColor.rgb);
float thresholdWidth = max(softness, 0.0001);
float brightMask = smoothstep(highlightThreshold - thresholdWidth, highlightThreshold + thresholdWidth, currentLuma);
// Treat RGBA as a 4-slot rolling scalar history:
// R = current frame, G = 1 frame ago, B = 2 frames ago, A = 3 frames ago.
// Each frame shifts the history forward and drops the oldest sample.
float currentContribution = currentLuma * brightMask * max(accumulateAmount, 0.0);
float4 nextHistory = float4(
currentContribution,
previousHistory.r,
previousHistory.g,
previousHistory.b
);
return saturate(nextHistory);
float3 previousBackground = sampleFeedback(context.uv).rgb;
float rate = saturate(learnRate);
float3 nextBackground = lerp(previousBackground, liveColor, rate);
return float4(saturate(nextBackground), 1.0);
}
float4 displayHistory(ShaderContext context)
float4 displayBackgroundDifference(ShaderContext context)
{
// In the display pass, context.sourceColor is the same-frame historyBuffer
// produced by buildHistory().
float4 history = context.sourceColor;
float recentEnergy = history.r + history.g + history.b;
float3 originalColor = sampleLayerInput(context.uv).rgb;
float highlightBoost = recentEnergy * max(displayGain, 0.0);
float3 sourceHue = originalColor / max(max(originalColor.r, originalColor.g), max(originalColor.b, 0.0001));
float3 displayColor =
originalColor +
sourceHue * highlightBoost * 1.5;
// In the display pass, context.sourceColor is the same-frame background
// model produced by updateBackgroundModel().
float3 backgroundModel = context.sourceColor.rgb;
float3 liveColor = sampleLayerInput(context.uv).rgb;
float3 delta = abs(liveColor - backgroundModel);
float difference = max(delta.r, max(delta.g, delta.b));
float thresholdWidth = max(softness, 0.0001);
float motionMask = smoothstep(
differenceThreshold - thresholdWidth,
differenceThreshold + thresholdWidth,
difference);
float3 baseColor = lerp(liveColor, backgroundModel, saturate(backgroundMix));
float3 overlayColor = overlayTint.rgb * max(luminance(liveColor), 0.15);
float overlayAmount = motionMask * saturate(overlayOpacity) * overlayTint.a;
float3 displayColor = lerp(baseColor, baseColor + overlayColor, overlayAmount);
return float4(saturate(displayColor), 1.0);
}

View File

@@ -34,6 +34,27 @@ export function useThrottledParameterValue(parameter, onParameterChange) {
}
}, [draftValue, currentValue]);
useEffect(() => {
if (isInteractingRef.current) {
return;
}
if (valuesMatch(currentValue, latestDraftRef.current)) {
return;
}
if (pendingTimeoutRef.current) {
clearTimeout(pendingTimeoutRef.current);
pendingTimeoutRef.current = null;
}
setDraftValue(currentValue);
setAppliedValue(currentValue);
latestDraftRef.current = currentValue;
isDirtyRef.current = false;
lastSentAtRef.current = 0;
}, [currentValue]);
useEffect(() => {
return () => {
if (pendingTimeoutRef.current) {