diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp index 672918a..6672f20 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp @@ -195,9 +195,15 @@ bool RenderEngine::TryPresentPreview(bool force, unsigned previewFps, unsigned o if (!TryEnterCriticalSection(&mMutex)) return false; + const bool presented = PresentPreviewOnRenderThread(outputFrameWidth, outputFrameHeight); + LeaveCriticalSection(&mMutex); + return presented; +} + +bool RenderEngine::PresentPreviewOnRenderThread(unsigned outputFrameWidth, unsigned outputFrameHeight) +{ mRenderer.PresentToWindow(mHdc, outputFrameWidth, outputFrameHeight); mLastPreviewPresentTime = std::chrono::steady_clock::now(); - LeaveCriticalSection(&mMutex); return true; } @@ -206,12 +212,19 @@ bool RenderEngine::TryUploadInputFrame(const VideoIOFrame& inputFrame, const Vid if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr) return true; - const long textureSize = inputFrame.rowBytes * static_cast(inputFrame.height); if (!TryEnterCriticalSection(&mMutex)) return false; wglMakeCurrent(mHdc, mHglrc); + const bool uploaded = UploadInputFrameOnRenderThread(inputFrame, videoState); + wglMakeCurrent(NULL, NULL); + LeaveCriticalSection(&mMutex); + return uploaded; +} +bool RenderEngine::UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame, const VideoIOState& videoState) +{ + const long textureSize = inputFrame.rowBytes * static_cast(inputFrame.height); glPixelStorei(GL_UNPACK_ALIGNMENT, 4); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer.TextureUploadBuffer()); @@ -224,8 +237,6 @@ bool RenderEngine::TryUploadInputFrame(const VideoIOFrame& inputFrame, const Vid glBindTexture(GL_TEXTURE_2D, 0); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - wglMakeCurrent(NULL, NULL); - LeaveCriticalSection(&mMutex); return true; } @@ -233,12 +244,17 @@ bool RenderEngine::RenderOutputFrame(const RenderPipelineFrameContext& context, { EnterCriticalSection(&mMutex); wglMakeCurrent(mHdc, mHglrc); - const bool rendered = mRenderPipeline.RenderFrame(context, outputFrame); + const bool rendered = RenderOutputFrameOnRenderThread(context, outputFrame); wglMakeCurrent(NULL, NULL); LeaveCriticalSection(&mMutex); return rendered; } +bool RenderEngine::RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame) +{ + return mRenderPipeline.RenderFrame(context, outputFrame); +} + bool RenderEngine::ResolveRenderFrameState( const RenderFrameInput& input, std::vector* commitRequests, @@ -297,14 +313,11 @@ void RenderEngine::RenderLayerStack( }); } -bool RenderEngine::ReadOutputFrameRgba(unsigned width, unsigned height, std::vector& bottomUpPixels) +bool RenderEngine::ReadOutputFrameRgbaOnRenderThread(unsigned width, unsigned height, std::vector& bottomUpPixels) { if (width == 0 || height == 0) return false; - EnterCriticalSection(&mMutex); - wglMakeCurrent(mHdc, mHglrc); - bottomUpPixels.resize(static_cast(width) * height * 4); glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer()); glReadBuffer(GL_COLOR_ATTACHMENT0); @@ -313,15 +326,23 @@ bool RenderEngine::ReadOutputFrameRgba(unsigned width, unsigned height, std::vec glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, bottomUpPixels.data()); glPixelStorei(GL_PACK_ALIGNMENT, 4); - wglMakeCurrent(NULL, NULL); - LeaveCriticalSection(&mMutex); return true; } bool RenderEngine::CaptureOutputFrameRgbaTopDown(unsigned width, unsigned height, std::vector& topDownPixels) +{ + EnterCriticalSection(&mMutex); + wglMakeCurrent(mHdc, mHglrc); + const bool captured = CaptureOutputFrameRgbaTopDownOnRenderThread(width, height, topDownPixels); + wglMakeCurrent(NULL, NULL); + LeaveCriticalSection(&mMutex); + return captured; +} + +bool RenderEngine::CaptureOutputFrameRgbaTopDownOnRenderThread(unsigned width, unsigned height, std::vector& topDownPixels) { std::vector bottomUpPixels; - if (!ReadOutputFrameRgba(width, height, bottomUpPixels)) + if (!ReadOutputFrameRgbaOnRenderThread(width, height, bottomUpPixels)) return false; topDownPixels.resize(bottomUpPixels.size()); diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h b/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h index abb4519..35ba55f 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h @@ -118,10 +118,15 @@ public: unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat, unsigned historyCap); - bool ReadOutputFrameRgba(unsigned width, unsigned height, std::vector& bottomUpPixels); bool CaptureOutputFrameRgbaTopDown(unsigned width, unsigned height, std::vector& topDownPixels); private: + bool PresentPreviewOnRenderThread(unsigned outputFrameWidth, unsigned outputFrameHeight); + bool UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame, const VideoIOState& videoState); + bool RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame); + bool ReadOutputFrameRgbaOnRenderThread(unsigned width, unsigned height, std::vector& bottomUpPixels); + bool CaptureOutputFrameRgbaTopDownOnRenderThread(unsigned width, unsigned height, std::vector& topDownPixels); + OpenGLRenderer mRenderer; OpenGLRenderPass mRenderPass; OpenGLRenderPipeline mRenderPipeline; diff --git a/docs/PHASE_4_RENDER_THREAD_OWNERSHIP_DESIGN.md b/docs/PHASE_4_RENDER_THREAD_OWNERSHIP_DESIGN.md index 93b9d16..ae3fc70 100644 --- a/docs/PHASE_4_RENDER_THREAD_OWNERSHIP_DESIGN.md +++ b/docs/PHASE_4_RENDER_THREAD_OWNERSHIP_DESIGN.md @@ -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: not started. +- 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. - 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 and the current context-binding helpers. +- `RenderEngine` owns GL resources, the current context-binding compatibility shims, 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. @@ -64,10 +64,10 @@ The current code paths that matter most are: | Entry point | Current behavior | Phase 4 direction | | --- | --- | --- | -| `RenderEngine::TryUploadInputFrame(...)` | attempts to take the GL lock, binds the context, uploads input texture | enqueue latest input frame; render thread uploads | -| `RenderEngine::RenderOutputFrame(...)` | takes the GL lock, binds the context, renders, packs/readbacks output | render thread executes output frame production | -| `RenderEngine::TryPresentPreview(...)` | attempts to take the GL lock and presents preview | render thread or preview presenter consumes latest completed frame | -| `RenderEngine::CaptureOutputFrameRgbaTopDown(...)` | takes the GL lock and reads output pixels | screenshot request becomes render-thread command | +| `RenderEngine::TryUploadInputFrame(...)` | attempts to take the GL lock, binds the context, delegates upload to `UploadInputFrameOnRenderThread(...)` | enqueue latest input frame; render thread uploads | +| `RenderEngine::RenderOutputFrame(...)` | takes the GL lock, binds the context, delegates render/readback to `RenderOutputFrameOnRenderThread(...)` | render thread executes output frame production | +| `RenderEngine::TryPresentPreview(...)` | attempts to take the GL lock and delegates presentation to `PresentPreviewOnRenderThread(...)` | render thread or preview presenter consumes latest completed frame | +| `RenderEngine::CaptureOutputFrameRgbaTopDown(...)` | takes the GL lock, binds the context, delegates readback to `CaptureOutputFrameRgbaTopDownOnRenderThread(...)` | screenshot request becomes render-thread command | | `OpenGLVideoIOBridge::UploadInputFrame(...)` | calls render upload directly | push input frame into render queue/mailbox | | `OpenGLVideoIOBridge::RenderScheduledFrame(...)` | calls render output directly from backend path | request/consume render-produced output without callback-owned GL | @@ -184,7 +184,7 @@ Render-thread-only methods should be private or clearly named: - `RenderEngine::UploadInputFrameOnRenderThread(...)` - `RenderEngine::RenderOutputFrameOnRenderThread(...)` -- `RenderEngine::CaptureOutputFrameOnRenderThread(...)` +- `RenderEngine::CaptureOutputFrameRgbaTopDownOnRenderThread(...)` The current `TryUploadInputFrame`, `RenderOutputFrame`, `TryPresentPreview`, and `CaptureOutputFrameRgbaTopDown` methods can remain as compatibility shims during migration, but their implementations should move toward enqueue-and-wait or enqueue-and-return behavior instead of binding GL directly from the caller's thread. @@ -214,9 +214,9 @@ Split existing direct GL methods into public request methods and private render- Initial target: -- keep current synchronous behavior where callers need a result -- move GL bodies into clearly render-thread-owned helpers -- make future queue migration mechanical +- [x] keep current synchronous behavior where callers need a result +- [x] move GL bodies into clearly render-thread-owned helpers for upload, output render, preview presentation, and screenshot readback +- [x] make future queue migration mechanical ### Step 2. Add Render Command Queue