Phase 4
This commit is contained in:
@@ -23,10 +23,8 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
||||||
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
|
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC)
|
||||||
mScreenshotRequested(false)
|
|
||||||
{
|
{
|
||||||
InitializeCriticalSection(&pMutex);
|
|
||||||
mRuntimeStore = std::make_unique<RuntimeStore>();
|
mRuntimeStore = std::make_unique<RuntimeStore>();
|
||||||
mRuntimeEventDispatcher = std::make_unique<RuntimeEventDispatcher>();
|
mRuntimeEventDispatcher = std::make_unique<RuntimeEventDispatcher>();
|
||||||
mRuntimeSnapshotProvider = std::make_unique<RuntimeSnapshotProvider>(mRuntimeStore->GetRenderSnapshotBuilder(), *mRuntimeEventDispatcher);
|
mRuntimeSnapshotProvider = std::make_unique<RuntimeSnapshotProvider>(mRuntimeStore->GetRenderSnapshotBuilder(), *mRuntimeEventDispatcher);
|
||||||
@@ -34,11 +32,10 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
|||||||
mRenderEngine = std::make_unique<RenderEngine>(
|
mRenderEngine = std::make_unique<RenderEngine>(
|
||||||
*mRuntimeSnapshotProvider,
|
*mRuntimeSnapshotProvider,
|
||||||
mRuntimeStore->GetHealthTelemetry(),
|
mRuntimeStore->GetHealthTelemetry(),
|
||||||
pMutex,
|
|
||||||
hGLDC,
|
hGLDC,
|
||||||
hGLRC,
|
hGLRC,
|
||||||
[this]() { renderEffect(); },
|
[this]() { renderEffect(); },
|
||||||
[this]() { ProcessScreenshotRequest(); },
|
[]() {},
|
||||||
[this]() { paintGL(false); });
|
[this]() { paintGL(false); });
|
||||||
mVideoBackend = std::make_unique<VideoBackend>(*mRenderEngine, mRuntimeStore->GetHealthTelemetry(), *mRuntimeEventDispatcher);
|
mVideoBackend = std::make_unique<VideoBackend>(*mRenderEngine, mRuntimeStore->GetHealthTelemetry(), *mRuntimeEventDispatcher);
|
||||||
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeSnapshotProvider, *mRuntimeEventDispatcher);
|
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeSnapshotProvider, *mRuntimeEventDispatcher);
|
||||||
@@ -61,8 +58,6 @@ OpenGLComposite::~OpenGLComposite()
|
|||||||
mShaderBuildQueue->Stop();
|
mShaderBuildQueue->Stop();
|
||||||
if (mVideoBackend)
|
if (mVideoBackend)
|
||||||
mVideoBackend->ReleaseResources();
|
mVideoBackend->ReleaseResources();
|
||||||
|
|
||||||
DeleteCriticalSection(&pMutex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::InitDeckLink()
|
bool OpenGLComposite::InitDeckLink()
|
||||||
@@ -294,8 +289,50 @@ bool OpenGLComposite::ReloadShader(bool preserveFeedbackState)
|
|||||||
|
|
||||||
bool OpenGLComposite::RequestScreenshot(std::string& error)
|
bool OpenGLComposite::RequestScreenshot(std::string& error)
|
||||||
{
|
{
|
||||||
(void)error;
|
if (!mRenderEngine || !mVideoBackend)
|
||||||
mScreenshotRequested.store(true);
|
{
|
||||||
|
error = "The render engine is not ready.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned width = mVideoBackend->OutputFrameWidth();
|
||||||
|
const unsigned height = mVideoBackend->OutputFrameHeight();
|
||||||
|
if (width == 0 || height == 0)
|
||||||
|
{
|
||||||
|
error = "The output frame size is not available.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path outputPath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
outputPath = BuildScreenshotPath();
|
||||||
|
std::filesystem::create_directories(outputPath.parent_path());
|
||||||
|
}
|
||||||
|
catch (const std::exception& exception)
|
||||||
|
{
|
||||||
|
error = exception.what();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mRenderEngine->RequestScreenshotCapture(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
[outputPath](unsigned captureWidth, unsigned captureHeight, std::vector<unsigned char> topDownPixels) {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
WritePngFileAsync(outputPath, captureWidth, captureHeight, std::move(topDownPixels));
|
||||||
|
}
|
||||||
|
catch (const std::exception& exception)
|
||||||
|
{
|
||||||
|
OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str());
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
error = "Screenshot capture request failed.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,32 +379,6 @@ void OpenGLComposite::RenderFrame(const RenderFrameInput& frameInput)
|
|||||||
mRenderEngine->RenderPreparedFrame(frameState);
|
mRenderEngine->RenderPreparedFrame(frameState);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLComposite::ProcessScreenshotRequest()
|
|
||||||
{
|
|
||||||
if (!mScreenshotRequested.exchange(false))
|
|
||||||
return;
|
|
||||||
|
|
||||||
const unsigned width = mVideoBackend ? mVideoBackend->OutputFrameWidth() : 0;
|
|
||||||
const unsigned height = mVideoBackend ? mVideoBackend->OutputFrameHeight() : 0;
|
|
||||||
if (width == 0 || height == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::vector<unsigned char> topDownPixels;
|
|
||||||
if (!mRenderEngine->CaptureOutputFrameRgbaTopDown(width, height, topDownPixels))
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const std::filesystem::path outputPath = BuildScreenshotPath();
|
|
||||||
std::filesystem::create_directories(outputPath.parent_path());
|
|
||||||
WritePngFileAsync(outputPath, width, height, std::move(topDownPixels));
|
|
||||||
}
|
|
||||||
catch (const std::exception& exception)
|
|
||||||
{
|
|
||||||
OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::filesystem::path OpenGLComposite::BuildScreenshotPath() const
|
std::filesystem::path OpenGLComposite::BuildScreenshotPath() const
|
||||||
{
|
{
|
||||||
const std::filesystem::path root = mRuntimeStore && !mRuntimeStore->GetRuntimeDataRoot().empty()
|
const std::filesystem::path root = mRuntimeStore && !mRuntimeStore->GetRuntimeDataRoot().empty()
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
#include "RenderFrameState.h"
|
#include "RenderFrameState.h"
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <atomic>
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -73,7 +72,6 @@ private:
|
|||||||
HWND hGLWnd;
|
HWND hGLWnd;
|
||||||
HDC hGLDC;
|
HDC hGLDC;
|
||||||
HGLRC hGLRC;
|
HGLRC hGLRC;
|
||||||
CRITICAL_SECTION pMutex;
|
|
||||||
|
|
||||||
std::unique_ptr<RuntimeStore> mRuntimeStore;
|
std::unique_ptr<RuntimeStore> mRuntimeStore;
|
||||||
std::unique_ptr<RuntimeCoordinator> mRuntimeCoordinator;
|
std::unique_ptr<RuntimeCoordinator> mRuntimeCoordinator;
|
||||||
@@ -84,13 +82,11 @@ private:
|
|||||||
std::unique_ptr<RuntimeServices> mRuntimeServices;
|
std::unique_ptr<RuntimeServices> mRuntimeServices;
|
||||||
std::unique_ptr<RuntimeUpdateController> mRuntimeUpdateController;
|
std::unique_ptr<RuntimeUpdateController> mRuntimeUpdateController;
|
||||||
std::unique_ptr<VideoBackend> mVideoBackend;
|
std::unique_ptr<VideoBackend> mVideoBackend;
|
||||||
std::atomic<bool> mScreenshotRequested;
|
|
||||||
|
|
||||||
bool InitOpenGLState();
|
bool InitOpenGLState();
|
||||||
void renderEffect();
|
void renderEffect();
|
||||||
RenderFrameInput BuildRenderFrameInput() const;
|
RenderFrameInput BuildRenderFrameInput() const;
|
||||||
void RenderFrame(const RenderFrameInput& frameInput);
|
void RenderFrame(const RenderFrameInput& frameInput);
|
||||||
void ProcessScreenshotRequest();
|
|
||||||
std::filesystem::path BuildScreenshotPath() const;
|
std::filesystem::path BuildScreenshotPath() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
enum class RenderCommandResetScope
|
enum class RenderCommandResetScope
|
||||||
{
|
{
|
||||||
@@ -31,6 +32,7 @@ struct RenderInputUploadRequest
|
|||||||
{
|
{
|
||||||
VideoIOFrame inputFrame;
|
VideoIOFrame inputFrame;
|
||||||
VideoIOState videoState;
|
VideoIOState videoState;
|
||||||
|
std::vector<unsigned char> ownedBytes;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RenderOutputFrameRequest
|
struct RenderOutputFrameRequest
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
#include <gl/gl.h>
|
#include <gl/gl.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
RenderEngine::RenderEngine(
|
RenderEngine::RenderEngine(
|
||||||
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
||||||
HealthTelemetry& healthTelemetry,
|
HealthTelemetry& healthTelemetry,
|
||||||
CRITICAL_SECTION& mutex,
|
|
||||||
HDC hdc,
|
HDC hdc,
|
||||||
HGLRC hglrc,
|
HGLRC hglrc,
|
||||||
RenderEffectCallback renderEffect,
|
RenderEffectCallback renderEffect,
|
||||||
@@ -17,7 +17,6 @@ RenderEngine::RenderEngine(
|
|||||||
mRenderPass(mRenderer),
|
mRenderPass(mRenderer),
|
||||||
mRenderPipeline(mRenderer, runtimeSnapshotProvider, healthTelemetry, std::move(renderEffect), std::move(screenshotReady), std::move(previewPaint)),
|
mRenderPipeline(mRenderer, runtimeSnapshotProvider, healthTelemetry, std::move(renderEffect), std::move(screenshotReady), std::move(previewPaint)),
|
||||||
mShaderPrograms(mRenderer, runtimeSnapshotProvider),
|
mShaderPrograms(mRenderer, runtimeSnapshotProvider),
|
||||||
mMutex(mutex),
|
|
||||||
mHdc(hdc),
|
mHdc(hdc),
|
||||||
mHglrc(hglrc),
|
mHglrc(hglrc),
|
||||||
mFrameStateResolver(runtimeSnapshotProvider)
|
mFrameStateResolver(runtimeSnapshotProvider)
|
||||||
@@ -136,6 +135,24 @@ void RenderEngine::ReportRenderThreadRequestFailure(const char* operationName, c
|
|||||||
OutputDebugStringA(message.str().c_str());
|
OutputDebugStringA(message.str().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RenderEngine::IsRenderThreadAccessExpected() const
|
||||||
|
{
|
||||||
|
return !mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderEngine::ReportWrongThreadRenderAccess(const char* operationName) const
|
||||||
|
{
|
||||||
|
if (IsRenderThreadAccessExpected())
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::ostringstream message;
|
||||||
|
message << "Wrong-thread render access detected";
|
||||||
|
if (operationName && operationName[0] != '\0')
|
||||||
|
message << " [" << operationName << "]";
|
||||||
|
message << ".\n";
|
||||||
|
OutputDebugStringA(message.str().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
bool RenderEngine::CompileDecodeShader(int errorMessageSize, char* errorMessage)
|
bool RenderEngine::CompileDecodeShader(int errorMessageSize, char* errorMessage)
|
||||||
{
|
{
|
||||||
return InvokeOnRenderThread([this, errorMessageSize, errorMessage]() {
|
return InvokeOnRenderThread([this, errorMessageSize, errorMessage]() {
|
||||||
@@ -241,11 +258,13 @@ void RenderEngine::ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderRe
|
|||||||
|
|
||||||
void RenderEngine::ResetTemporalHistoryStateOnRenderThread()
|
void RenderEngine::ResetTemporalHistoryStateOnRenderThread()
|
||||||
{
|
{
|
||||||
|
ReportWrongThreadRenderAccess("reset-temporal-history");
|
||||||
mShaderPrograms.ResetTemporalHistoryState();
|
mShaderPrograms.ResetTemporalHistoryState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RenderEngine::ResetShaderFeedbackStateOnRenderThread()
|
void RenderEngine::ResetShaderFeedbackStateOnRenderThread()
|
||||||
{
|
{
|
||||||
|
ReportWrongThreadRenderAccess("reset-shader-feedback");
|
||||||
mShaderPrograms.ResetShaderFeedbackState();
|
mShaderPrograms.ResetShaderFeedbackState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +295,124 @@ void RenderEngine::ProcessRenderResetCommandsOnRenderThread()
|
|||||||
ApplyRenderResetOnRenderThread(resetScope);
|
ApplyRenderResetOnRenderThread(resetScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RenderEngine::EnqueuePreviewPresentWake()
|
||||||
|
{
|
||||||
|
if (!mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool shouldNotify = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||||
|
if (!mRenderThreadStopping && !mPreviewPresentWakePending)
|
||||||
|
{
|
||||||
|
mPreviewPresentWakePending = true;
|
||||||
|
mRenderThreadTasks.push([this]() {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||||
|
mPreviewPresentWakePending = false;
|
||||||
|
}
|
||||||
|
ProcessPreviewPresentCommandsOnRenderThread();
|
||||||
|
});
|
||||||
|
shouldNotify = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldNotify)
|
||||||
|
mRenderThreadCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderEngine::ProcessPreviewPresentCommandsOnRenderThread()
|
||||||
|
{
|
||||||
|
RenderPreviewPresentRequest request;
|
||||||
|
if (mRenderCommandQueue.TryTakePreviewPresent(request))
|
||||||
|
PresentPreviewOnRenderThread(request.outputFrameWidth, request.outputFrameHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderEngine::EnqueueInputUploadWake()
|
||||||
|
{
|
||||||
|
if (!mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool shouldNotify = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||||
|
if (!mRenderThreadStopping && !mInputUploadWakePending)
|
||||||
|
{
|
||||||
|
mInputUploadWakePending = true;
|
||||||
|
mRenderThreadTasks.push([this]() {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||||
|
mInputUploadWakePending = false;
|
||||||
|
}
|
||||||
|
ProcessInputUploadCommandsOnRenderThread();
|
||||||
|
});
|
||||||
|
shouldNotify = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldNotify)
|
||||||
|
mRenderThreadCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderEngine::ProcessInputUploadCommandsOnRenderThread()
|
||||||
|
{
|
||||||
|
RenderInputUploadRequest request;
|
||||||
|
while (mRenderCommandQueue.TryTakeInputUpload(request))
|
||||||
|
{
|
||||||
|
if (request.ownedBytes.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
request.inputFrame.bytes = request.ownedBytes.data();
|
||||||
|
UploadInputFrameOnRenderThread(request.inputFrame, request.videoState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderEngine::EnqueueScreenshotCaptureWake()
|
||||||
|
{
|
||||||
|
if (!mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool shouldNotify = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||||
|
if (!mRenderThreadStopping && !mScreenshotCaptureWakePending)
|
||||||
|
{
|
||||||
|
mScreenshotCaptureWakePending = true;
|
||||||
|
mRenderThreadTasks.push([this]() {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||||
|
mScreenshotCaptureWakePending = false;
|
||||||
|
}
|
||||||
|
ProcessScreenshotCaptureCommandsOnRenderThread();
|
||||||
|
});
|
||||||
|
shouldNotify = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldNotify)
|
||||||
|
mRenderThreadCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderEngine::ProcessScreenshotCaptureCommandsOnRenderThread()
|
||||||
|
{
|
||||||
|
RenderScreenshotCaptureRequest request;
|
||||||
|
ScreenshotCaptureCallback completion;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||||
|
completion = mScreenshotCaptureCompletion;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (mRenderCommandQueue.TryTakeScreenshotCapture(request))
|
||||||
|
{
|
||||||
|
if (!completion)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
std::vector<unsigned char> topDownPixels;
|
||||||
|
if (CaptureOutputFrameRgbaTopDownOnRenderThread(request.width, request.height, topDownPixels))
|
||||||
|
completion(request.width, request.height, std::move(topDownPixels));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RenderEngine::ClearOscOverlayState()
|
void RenderEngine::ClearOscOverlayState()
|
||||||
{
|
{
|
||||||
mRuntimeLiveState.Clear();
|
mRuntimeLiveState.Clear();
|
||||||
@@ -323,32 +460,62 @@ bool RenderEngine::TryPresentPreview(bool force, unsigned previewFps, unsigned o
|
|||||||
|
|
||||||
if (mRenderThreadRunning)
|
if (mRenderThreadRunning)
|
||||||
{
|
{
|
||||||
return TryInvokeOnRenderThread("preview-present", [this, outputFrameWidth, outputFrameHeight]() {
|
|
||||||
mRenderCommandQueue.RequestPreviewPresent({ outputFrameWidth, outputFrameHeight });
|
mRenderCommandQueue.RequestPreviewPresent({ outputFrameWidth, outputFrameHeight });
|
||||||
RenderPreviewPresentRequest request;
|
if (GetCurrentThreadId() == mRenderThreadId)
|
||||||
return mRenderCommandQueue.TryTakePreviewPresent(request) &&
|
ProcessPreviewPresentCommandsOnRenderThread();
|
||||||
PresentPreviewOnRenderThread(request.outputFrameWidth, request.outputFrameHeight);
|
else
|
||||||
});
|
EnqueuePreviewPresentWake();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryEnterCriticalSection(&mMutex))
|
ReportRenderThreadRequestFailure("preview-present", "render thread is not running");
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
mRenderCommandQueue.RequestPreviewPresent({ outputFrameWidth, outputFrameHeight });
|
|
||||||
RenderPreviewPresentRequest request;
|
|
||||||
const bool presented = mRenderCommandQueue.TryTakePreviewPresent(request) &&
|
|
||||||
PresentPreviewOnRenderThread(request.outputFrameWidth, request.outputFrameHeight);
|
|
||||||
LeaveCriticalSection(&mMutex);
|
|
||||||
return presented;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RenderEngine::PresentPreviewOnRenderThread(unsigned outputFrameWidth, unsigned outputFrameHeight)
|
bool RenderEngine::PresentPreviewOnRenderThread(unsigned outputFrameWidth, unsigned outputFrameHeight)
|
||||||
{
|
{
|
||||||
|
ReportWrongThreadRenderAccess("preview-present");
|
||||||
mRenderer.PresentToWindow(mHdc, outputFrameWidth, outputFrameHeight);
|
mRenderer.PresentToWindow(mHdc, outputFrameWidth, outputFrameHeight);
|
||||||
mLastPreviewPresentTime = std::chrono::steady_clock::now();
|
mLastPreviewPresentTime = std::chrono::steady_clock::now();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RenderEngine::RequestScreenshotCapture(unsigned width, unsigned height, ScreenshotCaptureCallback completion)
|
||||||
|
{
|
||||||
|
if (width == 0 || height == 0 || !completion)
|
||||||
|
return false;
|
||||||
|
if (!mRenderThreadRunning)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||||
|
mScreenshotCaptureCompletion = std::move(completion);
|
||||||
|
}
|
||||||
|
mRenderCommandQueue.RequestScreenshotCapture({ width, height });
|
||||||
|
EnqueueScreenshotCaptureWake();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderEngine::QueueInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState)
|
||||||
|
{
|
||||||
|
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
||||||
|
return true;
|
||||||
|
if (inputFrame.rowBytes <= 0 || inputFrame.height == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const std::size_t byteCount = static_cast<std::size_t>(inputFrame.rowBytes) * inputFrame.height;
|
||||||
|
RenderInputUploadRequest request;
|
||||||
|
request.inputFrame = inputFrame;
|
||||||
|
request.videoState = videoState;
|
||||||
|
request.ownedBytes.resize(byteCount);
|
||||||
|
std::memcpy(request.ownedBytes.data(), inputFrame.bytes, byteCount);
|
||||||
|
request.inputFrame.bytes = nullptr;
|
||||||
|
|
||||||
|
mRenderCommandQueue.RequestInputUpload(request);
|
||||||
|
EnqueueInputUploadWake();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool RenderEngine::TryUploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState)
|
bool RenderEngine::TryUploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState)
|
||||||
{
|
{
|
||||||
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
||||||
@@ -364,21 +531,13 @@ bool RenderEngine::TryUploadInputFrame(const VideoIOFrame& inputFrame, const Vid
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryEnterCriticalSection(&mMutex))
|
ReportRenderThreadRequestFailure("input-upload", "render thread is not running");
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
wglMakeCurrent(mHdc, mHglrc);
|
|
||||||
mRenderCommandQueue.RequestInputUpload({ inputFrame, videoState });
|
|
||||||
RenderInputUploadRequest request;
|
|
||||||
const bool uploaded = mRenderCommandQueue.TryTakeInputUpload(request) &&
|
|
||||||
UploadInputFrameOnRenderThread(request.inputFrame, request.videoState);
|
|
||||||
wglMakeCurrent(NULL, NULL);
|
|
||||||
LeaveCriticalSection(&mMutex);
|
|
||||||
return uploaded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RenderEngine::UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame, const VideoIOState& videoState)
|
bool RenderEngine::UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame, const VideoIOState& videoState)
|
||||||
{
|
{
|
||||||
|
ReportWrongThreadRenderAccess("input-upload");
|
||||||
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
|
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
@@ -395,7 +554,7 @@ bool RenderEngine::UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RenderEngine::RenderOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
bool RenderEngine::RequestOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
||||||
{
|
{
|
||||||
if (mRenderThreadRunning)
|
if (mRenderThreadRunning)
|
||||||
{
|
{
|
||||||
@@ -407,20 +566,20 @@ bool RenderEngine::RenderOutputFrame(const RenderPipelineFrameContext& context,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
EnterCriticalSection(&mMutex);
|
ReportRenderThreadRequestFailure("output-render", "render thread is not running");
|
||||||
wglMakeCurrent(mHdc, mHglrc);
|
return false;
|
||||||
mRenderCommandQueue.RequestOutputFrame({ context.videoState, context.completion });
|
}
|
||||||
RenderOutputFrameRequest request;
|
|
||||||
const bool rendered = mRenderCommandQueue.TryTakeOutputFrame(request) &&
|
bool RenderEngine::RenderOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
||||||
RenderOutputFrameOnRenderThread({ request.videoState, request.completion }, outputFrame);
|
{
|
||||||
wglMakeCurrent(NULL, NULL);
|
return RequestOutputFrame(context, outputFrame);
|
||||||
LeaveCriticalSection(&mMutex);
|
|
||||||
return rendered;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RenderEngine::RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
bool RenderEngine::RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
||||||
{
|
{
|
||||||
|
ReportWrongThreadRenderAccess("output-render");
|
||||||
ProcessRenderResetCommandsOnRenderThread();
|
ProcessRenderResetCommandsOnRenderThread();
|
||||||
|
ProcessInputUploadCommandsOnRenderThread();
|
||||||
return mRenderPipeline.RenderFrame(context, outputFrame);
|
return mRenderPipeline.RenderFrame(context, outputFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,6 +625,7 @@ void RenderEngine::RenderLayerStack(
|
|||||||
VideoIOPixelFormat inputPixelFormat,
|
VideoIOPixelFormat inputPixelFormat,
|
||||||
unsigned historyCap)
|
unsigned historyCap)
|
||||||
{
|
{
|
||||||
|
ReportWrongThreadRenderAccess("render-layer-stack");
|
||||||
mRenderPass.Render(
|
mRenderPass.Render(
|
||||||
hasInputSource,
|
hasInputSource,
|
||||||
layerStates,
|
layerStates,
|
||||||
@@ -484,6 +644,7 @@ void RenderEngine::RenderLayerStack(
|
|||||||
|
|
||||||
bool RenderEngine::ReadOutputFrameRgbaOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& bottomUpPixels)
|
bool RenderEngine::ReadOutputFrameRgbaOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& bottomUpPixels)
|
||||||
{
|
{
|
||||||
|
ReportWrongThreadRenderAccess("read-output-frame-rgba");
|
||||||
if (width == 0 || height == 0)
|
if (width == 0 || height == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -510,15 +671,8 @@ bool RenderEngine::CaptureOutputFrameRgbaTopDown(unsigned width, unsigned height
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
EnterCriticalSection(&mMutex);
|
ReportRenderThreadRequestFailure("screenshot-capture", "render thread is not running");
|
||||||
wglMakeCurrent(mHdc, mHglrc);
|
return false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RenderEngine::CaptureOutputFrameRgbaTopDownOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels)
|
bool RenderEngine::CaptureOutputFrameRgbaTopDownOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class RenderEngine
|
|||||||
public:
|
public:
|
||||||
using RenderEffectCallback = std::function<void()>;
|
using RenderEffectCallback = std::function<void()>;
|
||||||
using ScreenshotCallback = std::function<void()>;
|
using ScreenshotCallback = std::function<void()>;
|
||||||
|
using ScreenshotCaptureCallback = std::function<void(unsigned, unsigned, std::vector<unsigned char>)>;
|
||||||
using PreviewPaintCallback = std::function<void()>;
|
using PreviewPaintCallback = std::function<void()>;
|
||||||
|
|
||||||
struct OscOverlayUpdate
|
struct OscOverlayUpdate
|
||||||
@@ -61,7 +62,6 @@ public:
|
|||||||
RenderEngine(
|
RenderEngine(
|
||||||
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
||||||
HealthTelemetry& healthTelemetry,
|
HealthTelemetry& healthTelemetry,
|
||||||
CRITICAL_SECTION& mutex,
|
|
||||||
HDC hdc,
|
HDC hdc,
|
||||||
HGLRC hglrc,
|
HGLRC hglrc,
|
||||||
RenderEffectCallback renderEffect,
|
RenderEffectCallback renderEffect,
|
||||||
@@ -100,7 +100,10 @@ public:
|
|||||||
const std::vector<OscOverlayCommitCompletion>& completedCommits);
|
const std::vector<OscOverlayCommitCompletion>& completedCommits);
|
||||||
void ResizeView(int width, int height);
|
void ResizeView(int width, int height);
|
||||||
bool TryPresentPreview(bool force, unsigned previewFps, unsigned outputFrameWidth, unsigned outputFrameHeight);
|
bool TryPresentPreview(bool force, unsigned previewFps, unsigned outputFrameWidth, unsigned outputFrameHeight);
|
||||||
|
bool RequestScreenshotCapture(unsigned width, unsigned height, ScreenshotCaptureCallback completion);
|
||||||
|
bool QueueInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
|
||||||
bool TryUploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
|
bool TryUploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
|
||||||
|
bool RequestOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||||
bool RenderOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
bool RenderOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||||
bool ResolveRenderFrameState(
|
bool ResolveRenderFrameState(
|
||||||
const RenderFrameInput& input,
|
const RenderFrameInput& input,
|
||||||
@@ -178,6 +181,8 @@ private:
|
|||||||
|
|
||||||
void RenderThreadMain(std::promise<bool> ready);
|
void RenderThreadMain(std::promise<bool> ready);
|
||||||
void ReportRenderThreadRequestFailure(const char* operationName, const char* reason);
|
void ReportRenderThreadRequestFailure(const char* operationName, const char* reason);
|
||||||
|
bool IsRenderThreadAccessExpected() const;
|
||||||
|
void ReportWrongThreadRenderAccess(const char* operationName) const;
|
||||||
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
|
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
|
||||||
void RenderLayerStack(
|
void RenderLayerStack(
|
||||||
bool hasInputSource,
|
bool hasInputSource,
|
||||||
@@ -191,6 +196,12 @@ private:
|
|||||||
void ResetShaderFeedbackStateOnRenderThread();
|
void ResetShaderFeedbackStateOnRenderThread();
|
||||||
void ApplyRenderResetOnRenderThread(RenderCommandResetScope resetScope);
|
void ApplyRenderResetOnRenderThread(RenderCommandResetScope resetScope);
|
||||||
void ProcessRenderResetCommandsOnRenderThread();
|
void ProcessRenderResetCommandsOnRenderThread();
|
||||||
|
void EnqueuePreviewPresentWake();
|
||||||
|
void ProcessPreviewPresentCommandsOnRenderThread();
|
||||||
|
void EnqueueInputUploadWake();
|
||||||
|
void ProcessInputUploadCommandsOnRenderThread();
|
||||||
|
void EnqueueScreenshotCaptureWake();
|
||||||
|
void ProcessScreenshotCaptureCommandsOnRenderThread();
|
||||||
bool PresentPreviewOnRenderThread(unsigned outputFrameWidth, unsigned outputFrameHeight);
|
bool PresentPreviewOnRenderThread(unsigned outputFrameWidth, unsigned outputFrameHeight);
|
||||||
bool UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
|
bool UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
|
||||||
bool RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
bool RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||||
@@ -201,7 +212,6 @@ private:
|
|||||||
OpenGLRenderPass mRenderPass;
|
OpenGLRenderPass mRenderPass;
|
||||||
OpenGLRenderPipeline mRenderPipeline;
|
OpenGLRenderPipeline mRenderPipeline;
|
||||||
OpenGLShaderPrograms mShaderPrograms;
|
OpenGLShaderPrograms mShaderPrograms;
|
||||||
CRITICAL_SECTION& mMutex;
|
|
||||||
HDC mHdc;
|
HDC mHdc;
|
||||||
HGLRC mHglrc;
|
HGLRC mHglrc;
|
||||||
|
|
||||||
@@ -216,5 +226,9 @@ private:
|
|||||||
std::queue<std::function<void()>> mRenderThreadTasks;
|
std::queue<std::function<void()>> mRenderThreadTasks;
|
||||||
std::atomic<bool> mRenderThreadRunning = false;
|
std::atomic<bool> mRenderThreadRunning = false;
|
||||||
bool mRenderThreadStopping = false;
|
bool mRenderThreadStopping = false;
|
||||||
|
bool mPreviewPresentWakePending = false;
|
||||||
|
bool mInputUploadWakePending = false;
|
||||||
|
bool mScreenshotCaptureWakePending = false;
|
||||||
|
ScreenshotCaptureCallback mScreenshotCaptureCompletion;
|
||||||
bool mResourcesDestroyed = false;
|
bool mResourcesDestroyed = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ void OpenGLVideoIOBridge::UploadInputFrame(const VideoIOFrame& inputFrame, const
|
|||||||
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
||||||
return; // don't transfer texture when there's no input
|
return; // don't transfer texture when there's no input
|
||||||
|
|
||||||
mRenderEngine.TryUploadInputFrame(inputFrame, state);
|
mRenderEngine.QueueInputFrame(inputFrame, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLVideoIOBridge::RenderScheduledFrame(const VideoIOState& state, const VideoIOCompletion& completion, VideoIOOutputFrame& outputFrame)
|
bool OpenGLVideoIOBridge::RenderScheduledFrame(const VideoIOState& state, const VideoIOCompletion& completion, VideoIOOutputFrame& outputFrame)
|
||||||
{
|
{
|
||||||
RenderPipelineFrameContext frameContext;
|
RenderPipelineFrameContext frameContext;
|
||||||
frameContext.videoState = state;
|
frameContext.videoState = state;
|
||||||
frameContext.completion = completion;
|
frameContext.completion = completion;
|
||||||
|
|
||||||
mRenderEngine.RenderOutputFrame(frameContext, outputFrame);
|
return mRenderEngine.RequestOutputFrame(frameContext, outputFrame);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public:
|
|||||||
explicit OpenGLVideoIOBridge(RenderEngine& renderEngine);
|
explicit OpenGLVideoIOBridge(RenderEngine& renderEngine);
|
||||||
|
|
||||||
void UploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& state);
|
void UploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& state);
|
||||||
void RenderScheduledFrame(const VideoIOState& state, const VideoIOCompletion& completion, VideoIOOutputFrame& outputFrame);
|
bool RenderScheduledFrame(const VideoIOState& state, const VideoIOCompletion& completion, VideoIOOutputFrame& outputFrame);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
RenderEngine& mRenderEngine;
|
RenderEngine& mRenderEngine;
|
||||||
|
|||||||
@@ -232,11 +232,17 @@ void VideoBackend::HandleOutputFrameCompletion(const VideoIOCompletion& completi
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
const VideoIOState& state = mVideoIODevice->State();
|
const VideoIOState& state = mVideoIODevice->State();
|
||||||
|
bool rendered = true;
|
||||||
if (mBridge)
|
if (mBridge)
|
||||||
mBridge->RenderScheduledFrame(state, completion, outputFrame);
|
rendered = mBridge->RenderScheduledFrame(state, completion, outputFrame);
|
||||||
|
|
||||||
EndOutputFrame(outputFrame);
|
EndOutputFrame(outputFrame);
|
||||||
AccountForCompletionResult(completion.result);
|
AccountForCompletionResult(completion.result);
|
||||||
|
if (!rendered)
|
||||||
|
{
|
||||||
|
PublishBackendStateChanged("output-render-failed", "Output frame render request failed; skipping schedule for this frame.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Schedule the next frame after render work is complete so device-side
|
// Schedule the next frame after render work is complete so device-side
|
||||||
// bookkeeping stays with the backend seam and the bridge stays render-only.
|
// bookkeeping stays with the backend seam and the bridge stays render-only.
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ Phase 1 named the subsystems. Phase 2 added the typed event substrate. Phase 3 m
|
|||||||
## Status
|
## Status
|
||||||
|
|
||||||
- Phase 4 design package: proposed.
|
- Phase 4 design package: proposed.
|
||||||
- Phase 4 implementation: Step 3 started. The existing synchronous `RenderEngine` entrypoints delegate their GL bodies to named `...OnRenderThread(...)` helpers, preview/screenshot/render-reset/input-upload/output-render requests pass through a small `RenderCommandQueue` compatibility mailbox, and `RenderEngine` now starts a dedicated render thread for normal runtime GL work.
|
- Phase 4 implementation: Step 7 started. The existing synchronous `RenderEngine` entrypoints delegate their GL bodies to named `...OnRenderThread(...)` helpers, preview/screenshot/render-reset/input-upload/output-render requests pass through a small `RenderCommandQueue` compatibility mailbox, and `RenderEngine` now starts a dedicated render thread for normal runtime GL work.
|
||||||
- Current alignment: the repo has a named frame-state contract and cleaner render-state preparation. Normal runtime GL work is routed through the render thread after startup, while startup initialization still runs before the render thread is started.
|
- Current alignment: the repo has a named frame-state contract and cleaner render-state preparation. Normal runtime GL work is routed through the render thread after startup, while startup initialization still runs before the render thread is started.
|
||||||
|
|
||||||
Current GL ownership footholds:
|
Current GL ownership footholds:
|
||||||
|
|
||||||
- `RenderEngine` owns GL resources, a dedicated render thread, the current synchronous compatibility shims, a small render command mailbox, and named render-thread helper methods.
|
- `RenderEngine` owns GL resources, a dedicated render thread, the current synchronous compatibility shims, a small render command mailbox, named render-thread helper methods, and wrong-thread diagnostics for those helpers.
|
||||||
- `RenderFrameInput` / `RenderFrameState` provide the frame-state contract that a render thread can consume.
|
- `RenderFrameInput` / `RenderFrameState` provide the frame-state contract that a render thread can consume.
|
||||||
- `RenderFrameStateResolver` prepares the render-facing layer state before drawing.
|
- `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.
|
- `OpenGLVideoIOBridge` calls `RenderEngine::QueueInputFrame(...)` from the input path and `RenderEngine::RequestOutputFrame(...)` from the output path.
|
||||||
- `OpenGLComposite::paintGL(...)`, screenshot capture, input upload, and output rendering still call synchronous `RenderEngine` methods, but those methods now invoke render-thread work once `OpenGLComposite::Start()` has started the render thread.
|
- `OpenGLComposite::paintGL(...)`, screenshot capture, input upload, and output rendering enter render work through explicit `RenderEngine` requests. After `OpenGLComposite::Start()` starts the render thread, those requests do not bind the GL context on the caller thread.
|
||||||
|
|
||||||
## Why Phase 4 Exists
|
## Why Phase 4 Exists
|
||||||
|
|
||||||
@@ -65,11 +65,11 @@ The current code paths that matter most are:
|
|||||||
| Entry point | Current behavior | Phase 4 direction |
|
| Entry point | Current behavior | Phase 4 direction |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `RenderEngine::TryUploadInputFrame(...)` | synchronous compatibility shim; after render-thread startup it queues input upload work and waits for render-thread completion | enqueue latest input frame; render thread uploads without callback-owned GL |
|
| `RenderEngine::TryUploadInputFrame(...)` | synchronous compatibility shim; after render-thread startup it queues input upload work and waits for render-thread completion | enqueue latest input frame; render thread uploads without callback-owned GL |
|
||||||
| `RenderEngine::RenderOutputFrame(...)` | synchronous compatibility shim; after render-thread startup it queues output render work and waits for render-thread completion | render thread executes output frame production |
|
| `RenderEngine::RequestOutputFrame(...)` | synchronous output request; after render-thread startup it queues output render work and waits for render-thread completion with timeout/failure reporting | render thread executes output frame production |
|
||||||
| `RenderEngine::TryPresentPreview(...)` | synchronous compatibility shim; after render-thread startup it queues preview presentation and waits for render-thread completion | render thread or preview presenter consumes latest completed frame |
|
| `RenderEngine::TryPresentPreview(...)` | best-effort compatibility shim; after render-thread startup non-render callers queue preview presentation and return | render thread or preview presenter consumes latest completed frame |
|
||||||
| `RenderEngine::CaptureOutputFrameRgbaTopDown(...)` | synchronous compatibility shim; after render-thread startup it queues screenshot readback and waits for render-thread completion | screenshot request becomes render-thread command |
|
| `RenderEngine::CaptureOutputFrameRgbaTopDown(...)` | synchronous compatibility shim; after render-thread startup it queues screenshot readback and waits for render-thread completion | screenshot request becomes render-thread command |
|
||||||
| `OpenGLVideoIOBridge::UploadInputFrame(...)` | calls render upload directly | push input frame into render queue/mailbox |
|
| `OpenGLVideoIOBridge::UploadInputFrame(...)` | copies the latest input frame into the render mailbox and returns without waiting for GL | render thread uploads the latest queued input frame |
|
||||||
| `OpenGLVideoIOBridge::RenderScheduledFrame(...)` | calls render output directly from backend path | request/consume render-produced output without callback-owned GL |
|
| `OpenGLVideoIOBridge::RenderScheduledFrame(...)` | requests render-thread output production and reports success/failure to the backend | consume render-produced output without callback-owned GL |
|
||||||
|
|
||||||
## Target Ownership Model
|
## Target Ownership Model
|
||||||
|
|
||||||
@@ -260,10 +260,12 @@ Change `OpenGLVideoIOBridge::UploadInputFrame(...)` so it enqueues or replaces t
|
|||||||
|
|
||||||
Policy targets:
|
Policy targets:
|
||||||
|
|
||||||
- bounded memory
|
- [x] bounded memory
|
||||||
- latest-frame wins under load
|
- [x] latest-frame wins under load
|
||||||
- input upload skip count is observable
|
- [x] input upload skip count is observable through render command coalescing metrics
|
||||||
- input callback never waits for GL
|
- [x] input callback never waits for GL
|
||||||
|
|
||||||
|
Current implementation: `OpenGLVideoIOBridge::UploadInputFrame(...)` calls `RenderEngine::QueueInputFrame(...)`, which copies the input bytes into the latest-value render mailbox and schedules one bounded render-thread wakeup to upload the newest pending frame.
|
||||||
|
|
||||||
### Step 5. Move Output Rendering To The Render Thread
|
### Step 5. Move Output Rendering To The Render Thread
|
||||||
|
|
||||||
@@ -271,32 +273,38 @@ Change `OpenGLVideoIOBridge::RenderScheduledFrame(...)` so it requests render-th
|
|||||||
|
|
||||||
Transitional option:
|
Transitional option:
|
||||||
|
|
||||||
- synchronous request/response through the render thread
|
- [x] synchronous request/response through the render thread
|
||||||
|
|
||||||
Better follow-up:
|
Better follow-up:
|
||||||
|
|
||||||
- render ahead into a bounded output queue and let backend callbacks consume ready frames
|
- render ahead into a bounded output queue and let backend callbacks consume ready frames
|
||||||
|
|
||||||
|
Current implementation: `OpenGLVideoIOBridge::RenderScheduledFrame(...)` calls `RenderEngine::RequestOutputFrame(...)` and returns whether the render-thread request produced an output frame. `VideoBackend` skips scheduling that frame when render production fails or times out.
|
||||||
|
|
||||||
### Step 6. Decouple Preview And Screenshot Requests
|
### Step 6. Decouple Preview And Screenshot Requests
|
||||||
|
|
||||||
Preview should become best-effort:
|
Preview should become best-effort:
|
||||||
|
|
||||||
- request preview presentation from the render thread
|
- [x] request preview presentation from the render thread
|
||||||
- skip when render is busy or output deadline pressure is high
|
- [x] skip/coalesce when render is busy or output deadline pressure is high
|
||||||
- record preview skips
|
- [x] record preview skips through render command coalescing metrics
|
||||||
|
|
||||||
Screenshot should become:
|
Screenshot should become:
|
||||||
|
|
||||||
- queued render-thread capture request
|
- [x] queued render-thread capture request
|
||||||
- async disk write remains outside render thread
|
- [x] async disk write remains outside render thread
|
||||||
|
|
||||||
|
Current implementation: `OpenGLComposite::RequestScreenshot(...)` builds the output path, queues `RenderEngine::RequestScreenshotCapture(...)`, and the render thread captures pixels before handing them to the existing async PNG writer. Preview presentation is a latest-value best-effort render command; non-render callers enqueue and return, while render-thread callers drain the latest preview command inline.
|
||||||
|
|
||||||
### Step 7. Remove Shared GL Lock From Normal Paths
|
### Step 7. Remove Shared GL Lock From Normal Paths
|
||||||
|
|
||||||
Once all GL entrypoints are render-thread-owned:
|
Once all GL entrypoints are render-thread-owned:
|
||||||
|
|
||||||
- remove normal dependence on `pMutex` for render correctness
|
- [x] remove normal dependence on `pMutex` for render correctness
|
||||||
- keep assertions or diagnostics that detect wrong-thread GL calls
|
- [x] keep diagnostics that detect wrong-thread render-thread helper calls
|
||||||
- leave only lifecycle synchronization where needed
|
- [x] leave only lifecycle context binding where needed
|
||||||
|
|
||||||
|
Current implementation: `OpenGLComposite` no longer owns or passes a shared `CRITICAL_SECTION`, and `RenderEngine` no longer has caller-thread GL fallback paths for preview, input upload, output render, or screenshot capture. Runtime callers must go through the render thread; pre-start direct GL fallback is limited to startup initialization while the app explicitly owns the context.
|
||||||
|
|
||||||
## Testing Strategy
|
## Testing Strategy
|
||||||
|
|
||||||
@@ -366,12 +374,12 @@ Preview can remain a hidden budget consumer if it stays in the output frame path
|
|||||||
|
|
||||||
Phase 4 can be considered complete once the project can say:
|
Phase 4 can be considered complete once the project can say:
|
||||||
|
|
||||||
- [ ] one render thread owns the GL context during normal operation
|
- [x] one render thread owns the GL context during normal operation
|
||||||
- [ ] input callbacks do not bind GL or wait on GL upload
|
- [x] input callbacks do not bind GL or wait on GL upload
|
||||||
- [ ] output callbacks do not bind GL directly
|
- [x] output callbacks do not bind GL directly
|
||||||
- [ ] preview and screenshot requests enter render through explicit render-thread requests
|
- [x] preview and screenshot requests enter render through explicit render-thread requests
|
||||||
- [ ] `RenderFrameInput` / `RenderFrameState` remain the frame-state contract
|
- [ ] `RenderFrameInput` / `RenderFrameState` remain the frame-state contract
|
||||||
- [ ] normal frame production no longer depends on a shared GL `CRITICAL_SECTION`
|
- [x] normal frame production no longer depends on a shared GL `CRITICAL_SECTION`
|
||||||
- [ ] render-thread queue/mailbox behavior has non-GL tests
|
- [ ] render-thread queue/mailbox behavior has non-GL tests
|
||||||
- [ ] shutdown order is explicit and tested or manually verified
|
- [ ] shutdown order is explicit and tested or manually verified
|
||||||
|
|
||||||
|
|||||||
@@ -70,12 +70,14 @@ void TestInputUploadRequestUsesLatestValue()
|
|||||||
firstRequest.inputFrame.bytes = &firstPixel;
|
firstRequest.inputFrame.bytes = &firstPixel;
|
||||||
firstRequest.inputFrame.width = 1920;
|
firstRequest.inputFrame.width = 1920;
|
||||||
firstRequest.videoState.captureTextureWidth = 1920;
|
firstRequest.videoState.captureTextureWidth = 1920;
|
||||||
|
firstRequest.ownedBytes = { 1, 2, 3, 4 };
|
||||||
queue.RequestInputUpload(firstRequest);
|
queue.RequestInputUpload(firstRequest);
|
||||||
|
|
||||||
RenderInputUploadRequest secondRequest;
|
RenderInputUploadRequest secondRequest;
|
||||||
secondRequest.inputFrame.bytes = &secondPixel;
|
secondRequest.inputFrame.bytes = &secondPixel;
|
||||||
secondRequest.inputFrame.width = 1280;
|
secondRequest.inputFrame.width = 1280;
|
||||||
secondRequest.videoState.captureTextureWidth = 1280;
|
secondRequest.videoState.captureTextureWidth = 1280;
|
||||||
|
secondRequest.ownedBytes = { 5, 6 };
|
||||||
queue.RequestInputUpload(secondRequest);
|
queue.RequestInputUpload(secondRequest);
|
||||||
|
|
||||||
const RenderCommandQueueMetrics metrics = queue.GetMetrics();
|
const RenderCommandQueueMetrics metrics = queue.GetMetrics();
|
||||||
@@ -88,6 +90,7 @@ void TestInputUploadRequestUsesLatestValue()
|
|||||||
Expect(request.inputFrame.bytes == &secondPixel, "latest input upload bytes pointer wins");
|
Expect(request.inputFrame.bytes == &secondPixel, "latest input upload bytes pointer wins");
|
||||||
Expect(request.inputFrame.width == 1280, "latest input upload frame wins");
|
Expect(request.inputFrame.width == 1280, "latest input upload frame wins");
|
||||||
Expect(request.videoState.captureTextureWidth == 1280, "latest input upload state wins");
|
Expect(request.videoState.captureTextureWidth == 1280, "latest input upload state wins");
|
||||||
|
Expect(request.ownedBytes.size() == 2 && request.ownedBytes[0] == 5 && request.ownedBytes[1] == 6, "latest input upload owned bytes win");
|
||||||
Expect(!queue.TryTakeInputUpload(request), "input upload request is removed after consume");
|
Expect(!queue.TryTakeInputUpload(request), "input upload request is removed after consume");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user