Phase 4 step 2a
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m36s
CI / Windows Release Package (push) Successful in 2m42s

This commit is contained in:
Aiden
2026-05-11 17:26:24 +10:00
parent 539fcd3351
commit 0ec5a4cfed
7 changed files with 358 additions and 67 deletions

View File

@@ -64,6 +64,8 @@ set(APP_SOURCES
"${APP_DIR}/gl/OpenGLComposite.cpp"
"${APP_DIR}/gl/OpenGLComposite.h"
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
"${APP_DIR}/gl/RenderCommandQueue.cpp"
"${APP_DIR}/gl/RenderCommandQueue.h"
"${APP_DIR}/gl/RenderEngine.cpp"
"${APP_DIR}/gl/RenderEngine.h"
"${APP_DIR}/gl/RenderFrameState.h"
@@ -368,6 +370,22 @@ endif()
add_test(NAME Std140BufferTests COMMAND Std140BufferTests)
add_executable(RenderCommandQueueTests
"${APP_DIR}/gl/RenderCommandQueue.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCommandQueueTests.cpp"
)
target_include_directories(RenderCommandQueueTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/gl"
)
if(MSVC)
target_compile_options(RenderCommandQueueTests PRIVATE /W3)
endif()
add_test(NAME RenderCommandQueueTests COMMAND RenderCommandQueueTests)
add_executable(ShaderPackageRegistryTests
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"

View File

@@ -0,0 +1,116 @@
#include "RenderCommandQueue.h"
void RenderCommandQueue::RequestPreviewPresent(const RenderPreviewPresentRequest& request)
{
std::lock_guard<std::mutex> lock(mMutex);
if (mHasPreviewPresentRequest)
++mCoalescedCount;
else
++mEnqueuedCount;
mPreviewPresentRequest = request;
mHasPreviewPresentRequest = true;
}
bool RenderCommandQueue::TryTakePreviewPresent(RenderPreviewPresentRequest& request)
{
std::lock_guard<std::mutex> lock(mMutex);
if (!mHasPreviewPresentRequest)
return false;
request = mPreviewPresentRequest;
mPreviewPresentRequest = {};
mHasPreviewPresentRequest = false;
return true;
}
void RenderCommandQueue::RequestScreenshotCapture(const RenderScreenshotCaptureRequest& request)
{
std::lock_guard<std::mutex> lock(mMutex);
if (mHasScreenshotCaptureRequest)
++mCoalescedCount;
else
++mEnqueuedCount;
mScreenshotCaptureRequest = request;
mHasScreenshotCaptureRequest = true;
}
bool RenderCommandQueue::TryTakeScreenshotCapture(RenderScreenshotCaptureRequest& request)
{
std::lock_guard<std::mutex> lock(mMutex);
if (!mHasScreenshotCaptureRequest)
return false;
request = mScreenshotCaptureRequest;
mScreenshotCaptureRequest = {};
mHasScreenshotCaptureRequest = false;
return true;
}
void RenderCommandQueue::RequestRenderReset(RenderCommandResetScope scope)
{
if (scope == RenderCommandResetScope::None)
return;
std::lock_guard<std::mutex> lock(mMutex);
if (mRenderResetScope != RenderCommandResetScope::None)
++mCoalescedCount;
else
++mEnqueuedCount;
mRenderResetScope = MergeResetScopes(mRenderResetScope, scope);
}
bool RenderCommandQueue::TryTakeRenderReset(RenderCommandResetScope& scope)
{
std::lock_guard<std::mutex> lock(mMutex);
if (mRenderResetScope == RenderCommandResetScope::None)
return false;
scope = mRenderResetScope;
mRenderResetScope = RenderCommandResetScope::None;
return true;
}
RenderCommandQueueMetrics RenderCommandQueue::GetMetrics() const
{
std::lock_guard<std::mutex> lock(mMutex);
RenderCommandQueueMetrics metrics;
metrics.depth =
(mHasPreviewPresentRequest ? 1u : 0u) +
(mHasScreenshotCaptureRequest ? 1u : 0u) +
(mRenderResetScope != RenderCommandResetScope::None ? 1u : 0u);
metrics.enqueuedCount = mEnqueuedCount;
metrics.coalescedCount = mCoalescedCount;
return metrics;
}
RenderCommandResetScope RenderCommandQueue::MergeResetScopes(RenderCommandResetScope current, RenderCommandResetScope requested)
{
if (current == RenderCommandResetScope::TemporalHistoryAndFeedback ||
requested == RenderCommandResetScope::TemporalHistoryAndFeedback)
{
return RenderCommandResetScope::TemporalHistoryAndFeedback;
}
if ((current == RenderCommandResetScope::TemporalHistoryOnly && requested == RenderCommandResetScope::ShaderFeedbackOnly) ||
(current == RenderCommandResetScope::ShaderFeedbackOnly && requested == RenderCommandResetScope::TemporalHistoryOnly))
{
return RenderCommandResetScope::TemporalHistoryAndFeedback;
}
if (current == RenderCommandResetScope::TemporalHistoryOnly ||
requested == RenderCommandResetScope::TemporalHistoryOnly)
{
return RenderCommandResetScope::TemporalHistoryOnly;
}
if (current == RenderCommandResetScope::ShaderFeedbackOnly ||
requested == RenderCommandResetScope::ShaderFeedbackOnly)
{
return RenderCommandResetScope::ShaderFeedbackOnly;
}
return RenderCommandResetScope::None;
}

View File

@@ -0,0 +1,59 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <mutex>
enum class RenderCommandResetScope
{
None,
ShaderFeedbackOnly,
TemporalHistoryOnly,
TemporalHistoryAndFeedback
};
struct RenderPreviewPresentRequest
{
unsigned outputFrameWidth = 0;
unsigned outputFrameHeight = 0;
};
struct RenderScreenshotCaptureRequest
{
unsigned width = 0;
unsigned height = 0;
};
struct RenderCommandQueueMetrics
{
std::size_t depth = 0;
uint64_t enqueuedCount = 0;
uint64_t coalescedCount = 0;
};
class RenderCommandQueue
{
public:
void RequestPreviewPresent(const RenderPreviewPresentRequest& request);
bool TryTakePreviewPresent(RenderPreviewPresentRequest& request);
void RequestScreenshotCapture(const RenderScreenshotCaptureRequest& request);
bool TryTakeScreenshotCapture(RenderScreenshotCaptureRequest& request);
void RequestRenderReset(RenderCommandResetScope scope);
bool TryTakeRenderReset(RenderCommandResetScope& scope);
RenderCommandQueueMetrics GetMetrics() const;
private:
static RenderCommandResetScope MergeResetScopes(RenderCommandResetScope current, RenderCommandResetScope requested);
mutable std::mutex mMutex;
bool mHasPreviewPresentRequest = false;
RenderPreviewPresentRequest mPreviewPresentRequest;
bool mHasScreenshotCaptureRequest = false;
RenderScreenshotCaptureRequest mScreenshotCaptureRequest;
RenderCommandResetScope mRenderResetScope = RenderCommandResetScope::None;
uint64_t mEnqueuedCount = 0;
uint64_t mCoalescedCount = 0;
};

View File

@@ -1,7 +1,5 @@
#include "RenderEngine.h"
#include "ShaderBuildQueue.h"
#include <gl/gl.h>
#include <algorithm>
@@ -88,48 +86,16 @@ bool RenderEngine::ApplyPreparedShaderBuild(
return true;
}
RenderEngine::PreparedShaderBuildApplyResult RenderEngine::TryApplyReadyShaderBuild(
ShaderBuildQueue& shaderBuildQueue,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
bool preserveFeedbackState)
{
PreparedShaderBuildApplyResult result;
PreparedShaderBuild readyBuild;
if (!shaderBuildQueue.TryConsumeReadyBuild(readyBuild))
return result;
result.hadReadyBuild = true;
char compilerErrorMessage[1024] = {};
if (!ApplyPreparedShaderBuild(
readyBuild,
inputFrameWidth,
inputFrameHeight,
preserveFeedbackState,
sizeof(compilerErrorMessage),
compilerErrorMessage))
{
result.errorMessage = compilerErrorMessage;
return result;
}
result.applied = true;
return result;
}
const std::vector<RuntimeRenderState>& RenderEngine::CommittedLayerStates() const
{
return mShaderPrograms.CommittedLayerStates();
}
void RenderEngine::ResetTemporalHistoryState()
{
mShaderPrograms.ResetTemporalHistoryState();
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryOnly);
ProcessRenderResetCommandsOnRenderThread();
}
void RenderEngine::ResetShaderFeedbackState()
{
mShaderPrograms.ResetShaderFeedbackState();
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::ShaderFeedbackOnly);
ProcessRenderResetCommandsOnRenderThread();
}
void RenderEngine::ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope)
@@ -137,11 +103,12 @@ void RenderEngine::ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderRe
switch (resetScope)
{
case RuntimeCoordinatorRenderResetScope::TemporalHistoryOnly:
ResetTemporalHistoryState();
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryOnly);
ProcessRenderResetCommandsOnRenderThread();
break;
case RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback:
ResetTemporalHistoryState();
ResetShaderFeedbackState();
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryAndFeedback);
ProcessRenderResetCommandsOnRenderThread();
break;
case RuntimeCoordinatorRenderResetScope::None:
default:
@@ -149,6 +116,43 @@ void RenderEngine::ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderRe
}
}
void RenderEngine::ResetTemporalHistoryStateOnRenderThread()
{
mShaderPrograms.ResetTemporalHistoryState();
}
void RenderEngine::ResetShaderFeedbackStateOnRenderThread()
{
mShaderPrograms.ResetShaderFeedbackState();
}
void RenderEngine::ApplyRenderResetOnRenderThread(RenderCommandResetScope resetScope)
{
switch (resetScope)
{
case RenderCommandResetScope::ShaderFeedbackOnly:
ResetShaderFeedbackStateOnRenderThread();
break;
case RenderCommandResetScope::TemporalHistoryOnly:
ResetTemporalHistoryStateOnRenderThread();
break;
case RenderCommandResetScope::TemporalHistoryAndFeedback:
ResetTemporalHistoryStateOnRenderThread();
ResetShaderFeedbackStateOnRenderThread();
break;
case RenderCommandResetScope::None:
default:
break;
}
}
void RenderEngine::ProcessRenderResetCommandsOnRenderThread()
{
RenderCommandResetScope resetScope = RenderCommandResetScope::None;
while (mRenderCommandQueue.TryTakeRenderReset(resetScope))
ApplyRenderResetOnRenderThread(resetScope);
}
void RenderEngine::ClearOscOverlayState()
{
mRuntimeLiveState.Clear();
@@ -195,7 +199,10 @@ bool RenderEngine::TryPresentPreview(bool force, unsigned previewFps, unsigned o
if (!TryEnterCriticalSection(&mMutex))
return false;
const bool presented = PresentPreviewOnRenderThread(outputFrameWidth, outputFrameHeight);
mRenderCommandQueue.RequestPreviewPresent({ outputFrameWidth, outputFrameHeight });
RenderPreviewPresentRequest request;
const bool presented = mRenderCommandQueue.TryTakePreviewPresent(request) &&
PresentPreviewOnRenderThread(request.outputFrameWidth, request.outputFrameHeight);
LeaveCriticalSection(&mMutex);
return presented;
}
@@ -252,6 +259,7 @@ bool RenderEngine::RenderOutputFrame(const RenderPipelineFrameContext& context,
bool RenderEngine::RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
{
ProcessRenderResetCommandsOnRenderThread();
return mRenderPipeline.RenderFrame(context, outputFrame);
}
@@ -333,7 +341,10 @@ bool RenderEngine::CaptureOutputFrameRgbaTopDown(unsigned width, unsigned height
{
EnterCriticalSection(&mMutex);
wglMakeCurrent(mHdc, mHglrc);
const bool captured = CaptureOutputFrameRgbaTopDownOnRenderThread(width, height, topDownPixels);
mRenderCommandQueue.RequestScreenshotCapture({ width, height });
RenderScreenshotCaptureRequest request;
const bool captured = mRenderCommandQueue.TryTakeScreenshotCapture(request) &&
CaptureOutputFrameRgbaTopDownOnRenderThread(request.width, request.height, topDownPixels);
wglMakeCurrent(NULL, NULL);
LeaveCriticalSection(&mMutex);
return captured;

View File

@@ -4,6 +4,7 @@
#include "OpenGLRenderPipeline.h"
#include "OpenGLRenderer.h"
#include "OpenGLShaderPrograms.h"
#include "RenderCommandQueue.h"
#include "RenderFrameState.h"
#include "RenderFrameStateResolver.h"
#include "HealthTelemetry.h"
@@ -18,8 +19,6 @@
#include <string>
#include <vector>
class ShaderBuildQueue;
class RenderEngine
{
public:
@@ -50,13 +49,6 @@ public:
uint64_t generation = 0;
};
struct PreparedShaderBuildApplyResult
{
bool hadReadyBuild = false;
bool applied = false;
std::string errorMessage;
};
RenderEngine(
RuntimeSnapshotProvider& runtimeSnapshotProvider,
HealthTelemetry& healthTelemetry,
@@ -79,7 +71,6 @@ public:
unsigned outputPackTextureWidth,
std::string& error);
bool CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
bool ApplyPreparedShaderBuild(
const PreparedShaderBuild& preparedBuild,
unsigned inputFrameWidth,
@@ -87,13 +78,7 @@ public:
bool preserveFeedbackState,
int errorMessageSize,
char* errorMessage);
PreparedShaderBuildApplyResult TryApplyReadyShaderBuild(
ShaderBuildQueue& shaderBuildQueue,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
bool preserveFeedbackState);
const std::vector<RuntimeRenderState>& CommittedLayerStates() const;
void ResetTemporalHistoryState();
void ResetShaderFeedbackState();
void ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope);
@@ -110,6 +95,10 @@ public:
std::vector<OscOverlayCommitRequest>* commitRequests,
RenderFrameState& frameState);
void RenderPreparedFrame(const RenderFrameState& frameState);
bool CaptureOutputFrameRgbaTopDown(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels);
private:
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
void RenderLayerStack(
bool hasInputSource,
const std::vector<RuntimeRenderState>& layerStates,
@@ -118,9 +107,10 @@ public:
unsigned captureTextureWidth,
VideoIOPixelFormat inputPixelFormat,
unsigned historyCap);
bool CaptureOutputFrameRgbaTopDown(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels);
private:
void ResetTemporalHistoryStateOnRenderThread();
void ResetShaderFeedbackStateOnRenderThread();
void ApplyRenderResetOnRenderThread(RenderCommandResetScope resetScope);
void ProcessRenderResetCommandsOnRenderThread();
bool PresentPreviewOnRenderThread(unsigned outputFrameWidth, unsigned outputFrameHeight);
bool UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
bool RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
@@ -136,6 +126,7 @@ private:
HGLRC mHglrc;
std::chrono::steady_clock::time_point mLastPreviewPresentTime;
RenderCommandQueue mRenderCommandQueue;
RenderFrameStateResolver mFrameStateResolver;
RuntimeLiveState mRuntimeLiveState;
};

View File

@@ -7,12 +7,12 @@ Phase 1 named the subsystems. Phase 2 added the typed event substrate. Phase 3 m
## Status
- Phase 4 design package: proposed.
- Phase 4 implementation: Step 1 started. The existing synchronous `RenderEngine` entrypoints now delegate their GL bodies to named `...OnRenderThread(...)` helpers, but no command queue or dedicated render thread exists yet.
- Phase 4 implementation: Step 2 started. The existing synchronous `RenderEngine` entrypoints delegate their GL bodies to named `...OnRenderThread(...)` helpers, and preview/screenshot/render-reset requests now pass through a small `RenderCommandQueue` compatibility mailbox. No dedicated render thread exists yet.
- Current alignment: the repo has a named frame-state contract and cleaner render-state preparation, but GL work is still entered through multiple paths protected by one shared `CRITICAL_SECTION`.
Current GL ownership footholds:
- `RenderEngine` owns GL resources, the current context-binding compatibility shims, and named render-thread helper methods.
- `RenderEngine` owns GL resources, the current context-binding compatibility shims, a small render command mailbox, and named render-thread helper methods.
- `RenderFrameInput` / `RenderFrameState` provide the frame-state contract that a render thread can consume.
- `RenderFrameStateResolver` prepares the render-facing layer state before drawing.
- `OpenGLVideoIOBridge` still calls `RenderEngine::TryUploadInputFrame(...)` from the input path and `RenderEngine::RenderOutputFrame(...)` from the output path.
@@ -129,6 +129,13 @@ Non-responsibilities:
Small bounded queue or command mailbox for render-thread work.
Current implementation:
- `RenderCommandQueue` exists as a pure C++ mailbox helper.
- Preview present and screenshot capture requests use latest-value coalescing.
- Render-local reset requests coalesce to the strongest pending reset scope.
- `RenderEngine` drains these commands synchronously as compatibility shims until a dedicated render thread is introduced.
Possible commands:
- `UploadInputFrame`
@@ -224,9 +231,9 @@ Introduce a small queue/mailbox for render commands.
Start with low-risk commands:
- preview present request
- screenshot request
- render-local reset requests
- [x] preview present request
- [x] screenshot request
- [x] render-local reset requests
Then move input upload and output render requests once the queue and wakeup behavior are proven.

View File

@@ -0,0 +1,89 @@
#include "RenderCommandQueue.h"
#include <iostream>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
void TestPreviewRequestUsesLatestValue()
{
RenderCommandQueue queue;
queue.RequestPreviewPresent({ 1920, 1080 });
queue.RequestPreviewPresent({ 1280, 720 });
const RenderCommandQueueMetrics metrics = queue.GetMetrics();
Expect(metrics.depth == 1, "preview requests coalesce to one pending command");
Expect(metrics.enqueuedCount == 1, "first preview request is counted as enqueued");
Expect(metrics.coalescedCount == 1, "second preview request is counted as coalesced");
RenderPreviewPresentRequest request;
Expect(queue.TryTakePreviewPresent(request), "preview request can be consumed");
Expect(request.outputFrameWidth == 1280 && request.outputFrameHeight == 720, "latest preview request wins");
Expect(!queue.TryTakePreviewPresent(request), "preview request is removed after consume");
Expect(queue.GetMetrics().depth == 0, "preview consume empties queue depth");
}
void TestScreenshotRequestUsesLatestValue()
{
RenderCommandQueue queue;
queue.RequestScreenshotCapture({ 640, 360 });
queue.RequestScreenshotCapture({ 3840, 2160 });
RenderScreenshotCaptureRequest request;
Expect(queue.TryTakeScreenshotCapture(request), "screenshot request can be consumed");
Expect(request.width == 3840 && request.height == 2160, "latest screenshot request wins");
Expect(!queue.TryTakeScreenshotCapture(request), "screenshot request is removed after consume");
}
void TestRenderResetScopesCoalesceToStrongestRequest()
{
RenderCommandQueue queue;
queue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryOnly);
queue.RequestRenderReset(RenderCommandResetScope::ShaderFeedbackOnly);
RenderCommandResetScope scope = RenderCommandResetScope::None;
Expect(queue.TryTakeRenderReset(scope), "render reset request can be consumed");
Expect(scope == RenderCommandResetScope::TemporalHistoryAndFeedback, "temporal and feedback reset requests merge");
Expect(!queue.TryTakeRenderReset(scope), "render reset request is removed after consume");
queue.RequestRenderReset(RenderCommandResetScope::None);
Expect(queue.GetMetrics().depth == 0, "none reset request is ignored");
}
void TestIndependentCommandKindsShareDepth()
{
RenderCommandQueue queue;
queue.RequestPreviewPresent({ 1, 2 });
queue.RequestScreenshotCapture({ 3, 4 });
queue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryOnly);
Expect(queue.GetMetrics().depth == 3, "independent command kinds each contribute to depth");
}
}
int main()
{
TestPreviewRequestUsesLatestValue();
TestScreenshotRequestUsesLatestValue();
TestRenderResetScopesCoalesceToStrongestRequest();
TestIndependentCommandKindsShareDepth();
if (gFailures != 0)
{
std::cerr << gFailures << " RenderCommandQueue test failure(s).\n";
return 1;
}
std::cout << "RenderCommandQueue tests passed.\n";
return 0;
}