optional preview frame
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
2026-05-20 14:37:24 +10:00
parent 1d4eb7a34c
commit bfaa3f5e0e
25 changed files with 700 additions and 2740 deletions

View File

@@ -0,0 +1,28 @@
#pragma once
#include <string>
namespace RenderCadenceCompositor
{
constexpr double kDefaultPreviewFps = 30.0;
constexpr double kMinimumPreviewFps = 1.0;
struct PreviewWindowConfig
{
bool enabled = false;
double fps = kDefaultPreviewFps;
std::string title = "Render Cadence Preview";
};
inline double NormalizePreviewFps(double fps)
{
return fps >= kMinimumPreviewFps ? fps : kDefaultPreviewFps;
}
inline unsigned PreviewTimerIntervalMilliseconds(double fps)
{
const double normalizedFps = NormalizePreviewFps(fps);
const int intervalMilliseconds = static_cast<int>(1000.0 / normalizedFps);
return static_cast<unsigned>(intervalMilliseconds > 0 ? intervalMilliseconds : 1);
}
}

View File

@@ -0,0 +1,246 @@
#include "PreviewWindowThread.h"
#include "../frames/SystemFrameExchange.h"
#include "../logging/Logger.h"
#include <algorithm>
namespace RenderCadenceCompositor
{
namespace
{
const char* kPreviewWindowClassName = "RenderCadencePreviewWindow";
constexpr UINT_PTR kPreviewTimerId = 1;
constexpr UINT kPreviewStopMessage = WM_APP + 1;
void RegisterPreviewWindowClass()
{
static bool registered = false;
if (registered)
return;
WNDCLASSA windowClass = {};
windowClass.lpfnWndProc = PreviewWindowThread::WindowProc;
windowClass.hInstance = GetModuleHandleA(nullptr);
windowClass.lpszClassName = kPreviewWindowClassName;
windowClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
windowClass.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
RegisterClassA(&windowClass);
registered = true;
}
}
PreviewWindowThread::~PreviewWindowThread()
{
Stop();
}
bool PreviewWindowThread::Start(SystemFrameExchange& exchange, const PreviewWindowConfig& config, std::string& error)
{
if (!config.enabled)
return true;
if (mThread.joinable())
{
error = "Preview window thread is already running.";
return false;
}
mExchange = &exchange;
mConfig = config;
mStopRequested.store(false, std::memory_order_release);
mThread = std::thread(&PreviewWindowThread::ThreadMain, this);
return true;
}
void PreviewWindowThread::Stop()
{
mStopRequested.store(true, std::memory_order_release);
const DWORD threadId = mThreadId.load(std::memory_order_acquire);
if (threadId != 0)
PostThreadMessageA(threadId, kPreviewStopMessage, 0, 0);
if (mThread.joinable())
mThread.join();
mExchange = nullptr;
mRunning.store(false, std::memory_order_release);
}
LRESULT CALLBACK PreviewWindowThread::WindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
{
PreviewWindowThread* owner = reinterpret_cast<PreviewWindowThread*>(GetWindowLongPtrA(window, GWLP_USERDATA));
if (message == WM_NCCREATE)
{
const CREATESTRUCTA* create = reinterpret_cast<const CREATESTRUCTA*>(lParam);
owner = reinterpret_cast<PreviewWindowThread*>(create->lpCreateParams);
SetWindowLongPtrA(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(owner));
}
switch (message)
{
case WM_PAINT:
if (owner != nullptr)
{
owner->Paint(window);
return 0;
}
break;
case WM_TIMER:
if (owner != nullptr && wParam == kPreviewTimerId)
{
InvalidateRect(window, nullptr, FALSE);
return 0;
}
break;
case WM_CLOSE:
if (owner != nullptr)
{
if (!owner->mStopRequested.load(std::memory_order_acquire))
TryLog(LogLevel::Log, "preview", "Preview window closed by user.");
owner->mStopRequested.store(true, std::memory_order_release);
}
DestroyWindow(window);
return 0;
case WM_DESTROY:
if (owner != nullptr)
owner->mWindow = nullptr;
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcA(window, message, wParam, lParam);
}
void PreviewWindowThread::ThreadMain()
{
MSG message = {};
PeekMessageA(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
mThreadId.store(GetCurrentThreadId(), std::memory_order_release);
mRunning.store(true, std::memory_order_release);
std::string error;
if (!CreatePreviewWindow(error))
{
TryLog(LogLevel::Error, "preview", error.empty() ? "Preview window creation failed." : error);
mThreadId.store(0, std::memory_order_release);
mRunning.store(false, std::memory_order_release);
return;
}
if (!mStopRequested.load(std::memory_order_acquire))
{
if (SetTimer(mWindow, kPreviewTimerId, PreviewTimerIntervalMilliseconds(mConfig.fps), nullptr) == 0)
TryLog(LogLevel::Error, "preview", "Preview window timer could not be started.");
else
TryLog(LogLevel::Log, "preview", "Preview window started.");
}
while (!mStopRequested.load(std::memory_order_acquire))
{
const BOOL result = GetMessageA(&message, nullptr, 0, 0);
if (result <= 0)
break;
if (message.message == kPreviewStopMessage)
{
mStopRequested.store(true, std::memory_order_release);
break;
}
TranslateMessage(&message);
DispatchMessageA(&message);
}
if (mWindow != nullptr)
{
KillTimer(mWindow, kPreviewTimerId);
DestroyWindow(mWindow);
mWindow = nullptr;
}
mThreadId.store(0, std::memory_order_release);
mRunning.store(false, std::memory_order_release);
TryLog(LogLevel::Log, "preview", "Preview window thread stopped.");
}
bool PreviewWindowThread::CreatePreviewWindow(std::string& error)
{
RegisterPreviewWindowClass();
mWindow = CreateWindowExA(
0,
kPreviewWindowClassName,
mConfig.title.c_str(),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
960,
540,
nullptr,
nullptr,
GetModuleHandleA(nullptr),
this);
if (mWindow == nullptr)
{
error = "CreateWindowEx failed for preview window.";
return false;
}
ShowWindow(mWindow, SW_SHOW);
UpdateWindow(mWindow);
return true;
}
void PreviewWindowThread::Paint(HWND window)
{
PAINTSTRUCT paint = {};
HDC dc = BeginPaint(window, &paint);
RECT client = {};
GetClientRect(window, &client);
const int clientWidth = std::max(1L, client.right - client.left);
const int clientHeight = std::max(1L, client.bottom - client.top);
SystemFrame frame;
const bool frameAcquired = mExchange != nullptr && mExchange->TryAcquireLatestForPreview(frame);
const bool canPaintFrame =
frameAcquired &&
frame.bytes != nullptr &&
frame.pixelFormat == VideoIOPixelFormat::Bgra8 &&
frame.width > 0 &&
frame.height > 0;
if (canPaintFrame)
{
BITMAPINFO bitmapInfo = {};
bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bitmapInfo.bmiHeader.biWidth = static_cast<LONG>(frame.width);
bitmapInfo.bmiHeader.biHeight = -static_cast<LONG>(frame.height);
bitmapInfo.bmiHeader.biPlanes = 1;
bitmapInfo.bmiHeader.biBitCount = 32;
bitmapInfo.bmiHeader.biCompression = BI_RGB;
StretchDIBits(
dc,
0,
0,
clientWidth,
clientHeight,
0,
0,
static_cast<int>(frame.width),
static_cast<int>(frame.height),
frame.bytes,
&bitmapInfo,
DIB_RGB_COLORS,
SRCCOPY);
}
else
{
FillRect(dc, &client, reinterpret_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));
}
if (frameAcquired)
mExchange->ReleasePreviewFrame(frame);
EndPaint(window, &paint);
}
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include "PreviewConfig.h"
#include "../frames/SystemFrameTypes.h"
#include <atomic>
#include <thread>
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
class SystemFrameExchange;
namespace RenderCadenceCompositor
{
class PreviewWindowThread
{
public:
PreviewWindowThread() = default;
PreviewWindowThread(const PreviewWindowThread&) = delete;
PreviewWindowThread& operator=(const PreviewWindowThread&) = delete;
~PreviewWindowThread();
bool Start(SystemFrameExchange& exchange, const PreviewWindowConfig& config, std::string& error);
void Stop();
bool Running() const { return mRunning.load(std::memory_order_acquire); }
static LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam);
private:
void ThreadMain();
bool CreatePreviewWindow(std::string& error);
void Paint(HWND window);
SystemFrameExchange* mExchange = nullptr;
PreviewWindowConfig mConfig;
std::thread mThread;
std::atomic<bool> mStopRequested{ false };
std::atomic<bool> mRunning{ false };
std::atomic<DWORD> mThreadId{ 0 };
HWND mWindow = nullptr;
};
}