V2 working
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m54s
CI / Windows Release Package (push) Successful in 3m14s

This commit is contained in:
Aiden
2026-05-12 01:59:02 +10:00
parent 2531d871e8
commit e0ca548ef5
32 changed files with 3492 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
#include "Bgra8ReadbackPipeline.h"
#include "../frames/SystemFrameTypes.h"
#include <cstring>
Bgra8ReadbackPipeline::~Bgra8ReadbackPipeline()
{
Shutdown();
}
bool Bgra8ReadbackPipeline::Initialize(unsigned width, unsigned height, std::size_t pboDepth)
{
Shutdown();
mWidth = width;
mHeight = height;
mRowBytes = VideoIORowBytes(VideoIOPixelFormat::Bgra8, width);
if (mWidth == 0 || mHeight == 0 || mRowBytes == 0)
return false;
if (!CreateRenderTarget())
{
Shutdown();
return false;
}
const std::size_t byteCount = static_cast<std::size_t>(mRowBytes) * static_cast<std::size_t>(mHeight);
if (!mPboRing.Initialize(pboDepth, byteCount))
{
Shutdown();
return false;
}
return true;
}
void Bgra8ReadbackPipeline::Shutdown()
{
mPboRing.Shutdown();
DestroyRenderTarget();
mWidth = 0;
mHeight = 0;
mRowBytes = 0;
}
bool Bgra8ReadbackPipeline::RenderAndQueue(uint64_t frameIndex, const RenderCallback& renderFrame)
{
if (mFramebuffer == 0 || !renderFrame)
return false;
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
renderFrame(frameIndex);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
}
void Bgra8ReadbackPipeline::ConsumeCompleted(
const AcquireFrameCallback& acquireFrame,
const PublishFrameCallback& publishFrame,
const CounterCallback& onAcquireMiss,
const CounterCallback& onCompleted)
{
if (!acquireFrame || !publishFrame)
return;
PboReadbackRing::CompletedReadback readback;
while (mPboRing.TryAcquireCompleted(readback))
{
glBindBuffer(GL_PIXEL_PACK_BUFFER, readback.pbo);
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
if (!mapped)
{
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
mPboRing.ReleaseCompleted(readback);
continue;
}
SystemFrame frame;
if (acquireFrame(frame))
{
const std::size_t byteCount = static_cast<std::size_t>(frame.rowBytes) * static_cast<std::size_t>(frame.height);
if (frame.bytes != nullptr && byteCount <= readback.byteCount)
{
std::memcpy(frame.bytes, mapped, byteCount);
frame.frameIndex = readback.frameIndex;
frame.pixelFormat = VideoIOPixelFormat::Bgra8;
publishFrame(frame);
if (onCompleted)
onCompleted();
}
}
else if (onAcquireMiss)
{
onAcquireMiss();
}
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
mPboRing.ReleaseCompleted(readback);
}
}
bool Bgra8ReadbackPipeline::CreateRenderTarget()
{
glGenFramebuffers(1, &mFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
glGenTextures(1, &mTexture);
glBindTexture(GL_TEXTURE_2D, mTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA8,
static_cast<GLsizei>(mWidth),
static_cast<GLsizei>(mHeight),
0,
GL_BGRA,
GL_UNSIGNED_INT_8_8_8_8_REV,
nullptr);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture, 0);
const bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
glBindTexture(GL_TEXTURE_2D, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return complete;
}
void Bgra8ReadbackPipeline::DestroyRenderTarget()
{
if (mFramebuffer != 0)
glDeleteFramebuffers(1, &mFramebuffer);
if (mTexture != 0)
glDeleteTextures(1, &mTexture);
mFramebuffer = 0;
mTexture = 0;
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include "PboReadbackRing.h"
#include "VideoIOFormat.h"
#include <cstddef>
#include <cstdint>
#include <functional>
struct SystemFrame;
class Bgra8ReadbackPipeline
{
public:
using RenderCallback = std::function<void(uint64_t frameIndex)>;
using AcquireFrameCallback = std::function<bool(SystemFrame& frame)>;
using PublishFrameCallback = std::function<bool(const SystemFrame& frame)>;
using CounterCallback = std::function<void()>;
Bgra8ReadbackPipeline() = default;
Bgra8ReadbackPipeline(const Bgra8ReadbackPipeline&) = delete;
Bgra8ReadbackPipeline& operator=(const Bgra8ReadbackPipeline&) = delete;
~Bgra8ReadbackPipeline();
bool Initialize(unsigned width, unsigned height, std::size_t pboDepth);
void Shutdown();
bool RenderAndQueue(uint64_t frameIndex, const RenderCallback& renderFrame);
void ConsumeCompleted(
const AcquireFrameCallback& acquireFrame,
const PublishFrameCallback& publishFrame,
const CounterCallback& onAcquireMiss = {},
const CounterCallback& onCompleted = {});
GLuint Framebuffer() const { return mFramebuffer; }
unsigned Width() const { return mWidth; }
unsigned Height() const { return mHeight; }
unsigned RowBytes() const { return mRowBytes; }
VideoIOPixelFormat PixelFormat() const { return VideoIOPixelFormat::Bgra8; }
uint64_t PboQueueMisses() const { return mPboRing.QueueMisses(); }
private:
bool CreateRenderTarget();
void DestroyRenderTarget();
unsigned mWidth = 0;
unsigned mHeight = 0;
unsigned mRowBytes = 0;
GLuint mFramebuffer = 0;
GLuint mTexture = 0;
PboReadbackRing mPboRing;
};

View File

@@ -0,0 +1,138 @@
#include "PboReadbackRing.h"
#include <algorithm>
PboReadbackRing::~PboReadbackRing()
{
Shutdown();
}
bool PboReadbackRing::Initialize(std::size_t depth, std::size_t byteCount)
{
Shutdown();
if (depth == 0 || byteCount == 0)
return false;
mSlots.resize(depth);
mByteCount = byteCount;
for (Slot& slot : mSlots)
{
glGenBuffers(1, &slot.pbo);
if (slot.pbo == 0)
{
Shutdown();
return false;
}
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(mByteCount), nullptr, GL_STREAM_READ);
}
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
return true;
}
void PboReadbackRing::Shutdown()
{
for (Slot& slot : mSlots)
{
if (slot.fence)
glDeleteSync(slot.fence);
if (slot.pbo != 0)
glDeleteBuffers(1, &slot.pbo);
slot = {};
}
mSlots.clear();
mWriteIndex = 0;
mReadIndex = 0;
mByteCount = 0;
}
bool PboReadbackRing::QueueReadback(GLuint framebuffer, unsigned width, unsigned height, uint64_t frameIndex)
{
if (mSlots.empty())
return false;
Slot& slot = mSlots[mWriteIndex];
if (slot.inFlight || slot.acquired)
{
++mQueueMisses;
return false;
}
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(mByteCount), nullptr, GL_STREAM_READ);
glReadPixels(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr);
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
slot.inFlight = slot.fence != nullptr;
slot.frameIndex = frameIndex;
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
if (!slot.inFlight)
return false;
mWriteIndex = (mWriteIndex + 1) % mSlots.size();
return true;
}
bool PboReadbackRing::TryAcquireCompleted(CompletedReadback& readback)
{
if (mSlots.empty())
return false;
for (std::size_t checked = 0; checked < mSlots.size(); ++checked)
{
Slot& slot = mSlots[mReadIndex];
if (!slot.inFlight || slot.acquired || slot.fence == nullptr)
{
mReadIndex = (mReadIndex + 1) % mSlots.size();
continue;
}
const GLenum waitResult = glClientWaitSync(slot.fence, 0, 0);
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
return false;
slot.acquired = true;
readback.pbo = slot.pbo;
readback.frameIndex = slot.frameIndex;
readback.byteCount = mByteCount;
return true;
}
return false;
}
void PboReadbackRing::ReleaseCompleted(const CompletedReadback& readback)
{
for (std::size_t index = 0; index < mSlots.size(); ++index)
{
Slot& slot = mSlots[index];
if (!slot.acquired || slot.pbo != readback.pbo)
continue;
ResetSlot(slot);
mReadIndex = (index + 1) % mSlots.size();
return;
}
}
void PboReadbackRing::DrainCompleted()
{
for (std::size_t pass = 0; pass < mSlots.size() * 2; ++pass)
{
CompletedReadback readback;
if (!TryAcquireCompleted(readback))
break;
ReleaseCompleted(readback);
}
}
void PboReadbackRing::ResetSlot(Slot& slot)
{
if (slot.fence)
glDeleteSync(slot.fence);
slot.fence = nullptr;
slot.inFlight = false;
slot.acquired = false;
slot.frameIndex = 0;
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include "GLExtensions.h"
#include <cstddef>
#include <cstdint>
#include <vector>
class PboReadbackRing
{
public:
struct CompletedReadback
{
GLuint pbo = 0;
uint64_t frameIndex = 0;
std::size_t byteCount = 0;
};
PboReadbackRing() = default;
PboReadbackRing(const PboReadbackRing&) = delete;
PboReadbackRing& operator=(const PboReadbackRing&) = delete;
~PboReadbackRing();
bool Initialize(std::size_t depth, std::size_t byteCount);
void Shutdown();
bool QueueReadback(GLuint framebuffer, unsigned width, unsigned height, uint64_t frameIndex);
bool TryAcquireCompleted(CompletedReadback& readback);
void ReleaseCompleted(const CompletedReadback& readback);
void DrainCompleted();
std::size_t Depth() const { return mSlots.size(); }
uint64_t QueueMisses() const { return mQueueMisses; }
private:
struct Slot
{
GLuint pbo = 0;
GLsync fence = nullptr;
bool inFlight = false;
bool acquired = false;
uint64_t frameIndex = 0;
};
void ResetSlot(Slot& slot);
std::vector<Slot> mSlots;
std::size_t mWriteIndex = 0;
std::size_t mReadIndex = 0;
std::size_t mByteCount = 0;
uint64_t mQueueMisses = 0;
};

View File

@@ -0,0 +1,45 @@
#include "RenderCadenceClock.h"
#include <algorithm>
RenderCadenceClock::RenderCadenceClock(double frameDurationMilliseconds)
{
mFrameDuration = std::chrono::duration_cast<Duration>(std::chrono::duration<double, std::milli>(frameDurationMilliseconds));
if (mFrameDuration <= Duration::zero())
mFrameDuration = std::chrono::milliseconds(16);
Reset();
}
void RenderCadenceClock::Reset(TimePoint now)
{
mNextRenderTime = now;
mOverrunCount = 0;
mSkippedFrameCount = 0;
}
RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
{
Tick tick;
if (now < mNextRenderTime)
{
tick.sleepFor = std::min(Duration(std::chrono::milliseconds(1)), mNextRenderTime - now);
return tick;
}
tick.due = true;
const Duration lateBy = now - mNextRenderTime;
if (lateBy > mFrameDuration)
{
tick.skippedFrames = static_cast<uint64_t>(lateBy / mFrameDuration);
++mOverrunCount;
mSkippedFrameCount += tick.skippedFrames;
}
return tick;
}
void RenderCadenceClock::MarkRendered(TimePoint now)
{
mNextRenderTime += mFrameDuration;
if (now - mNextRenderTime > mFrameDuration * 4)
mNextRenderTime = now + mFrameDuration;
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include <chrono>
#include <cstdint>
class RenderCadenceClock
{
public:
using Clock = std::chrono::steady_clock;
using Duration = Clock::duration;
using TimePoint = Clock::time_point;
struct Tick
{
bool due = false;
uint64_t skippedFrames = 0;
Duration sleepFor = Duration::zero();
};
explicit RenderCadenceClock(double frameDurationMilliseconds = 1000.0 / 60.0);
void Reset(TimePoint now = Clock::now());
Tick Poll(TimePoint now = Clock::now());
void MarkRendered(TimePoint now = Clock::now());
Duration FrameDuration() const { return mFrameDuration; }
TimePoint NextRenderTime() const { return mNextRenderTime; }
uint64_t OverrunCount() const { return mOverrunCount; }
uint64_t SkippedFrameCount() const { return mSkippedFrameCount; }
private:
Duration mFrameDuration;
TimePoint mNextRenderTime = Clock::now();
uint64_t mOverrunCount = 0;
uint64_t mSkippedFrameCount = 0;
};

View File

@@ -0,0 +1,181 @@
#include "RenderThread.h"
#include "../frames/SystemFrameExchange.h"
#include "../frames/SystemFrameTypes.h"
#include "../platform/HiddenGlWindow.h"
#include "Bgra8ReadbackPipeline.h"
#include "GLExtensions.h"
#include "SimpleMotionRenderer.h"
#include <algorithm>
#include <thread>
RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) :
mFrameExchange(frameExchange),
mConfig(config)
{
}
RenderThread::~RenderThread()
{
Stop();
}
bool RenderThread::Start(std::string& error)
{
if (mThread.joinable())
return true;
{
std::lock_guard<std::mutex> lock(mStartupMutex);
mStarted = false;
mStartupError.clear();
}
mStopping.store(false, std::memory_order_release);
mThread = std::thread([this]() { ThreadMain(); });
std::unique_lock<std::mutex> lock(mStartupMutex);
if (!mStartupCondition.wait_for(lock, std::chrono::seconds(3), [this]() {
return mStarted || !mStartupError.empty();
}))
{
error = "Timed out starting render thread.";
return false;
}
if (!mStartupError.empty())
{
error = mStartupError;
lock.unlock();
if (mThread.joinable())
mThread.join();
return false;
}
return true;
}
void RenderThread::Stop()
{
mStopping.store(true, std::memory_order_release);
if (mThread.joinable())
mThread.join();
}
RenderThread::Metrics RenderThread::GetMetrics() const
{
std::lock_guard<std::mutex> lock(mMetricsMutex);
return mMetrics;
}
void RenderThread::ThreadMain()
{
HiddenGlWindow window;
std::string error;
if (!window.Create(mConfig.width, mConfig.height, error) || !window.MakeCurrent())
{
SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error);
return;
}
if (!ResolveGLExtensions())
{
SignalStartupFailure("OpenGL extension resolution failed.");
return;
}
SimpleMotionRenderer renderer;
Bgra8ReadbackPipeline readback;
if (!renderer.InitializeGl(mConfig.width, mConfig.height) || !readback.Initialize(mConfig.width, mConfig.height, mConfig.pboDepth))
{
SignalStartupFailure("Render pipeline initialization failed.");
return;
}
RenderCadenceClock clock(mConfig.frameDurationMilliseconds);
uint64_t frameIndex = 0;
mRunning.store(true, std::memory_order_release);
SignalStarted();
while (!mStopping.load(std::memory_order_acquire))
{
readback.ConsumeCompleted(
[this](SystemFrame& frame) { return mFrameExchange.AcquireForRender(frame); },
[this](const SystemFrame& frame) { return mFrameExchange.PublishCompleted(frame); },
[this]() {
CountAcquireMiss();
},
[this]() { CountCompleted(); });
const auto now = RenderCadenceClock::Clock::now();
const RenderCadenceClock::Tick tick = clock.Poll(now);
if (!tick.due)
{
if (tick.sleepFor > RenderCadenceClock::Duration::zero())
std::this_thread::sleep_for(tick.sleepFor);
continue;
}
if (!readback.RenderAndQueue(frameIndex, [&renderer](uint64_t index) { renderer.RenderFrame(index); }))
{
std::lock_guard<std::mutex> lock(mMetricsMutex);
++mMetrics.pboQueueMisses;
}
CountRendered();
++frameIndex;
clock.MarkRendered(RenderCadenceClock::Clock::now());
{
std::lock_guard<std::mutex> lock(mMetricsMutex);
mMetrics.clockOverruns = clock.OverrunCount();
mMetrics.skippedFrames = clock.SkippedFrameCount();
}
}
for (std::size_t i = 0; i < mConfig.pboDepth * 2; ++i)
{
readback.ConsumeCompleted(
[this](SystemFrame& frame) { return mFrameExchange.AcquireForRender(frame); },
[this](const SystemFrame& frame) { return mFrameExchange.PublishCompleted(frame); },
[this]() {
CountAcquireMiss();
},
[this]() { CountCompleted(); });
}
readback.Shutdown();
renderer.ShutdownGl();
window.ClearCurrent();
mRunning.store(false, std::memory_order_release);
}
void RenderThread::SignalStarted()
{
std::lock_guard<std::mutex> lock(mStartupMutex);
mStarted = true;
mStartupCondition.notify_all();
}
void RenderThread::SignalStartupFailure(const std::string& error)
{
std::lock_guard<std::mutex> lock(mStartupMutex);
mStartupError = error;
mStartupCondition.notify_all();
}
void RenderThread::CountRendered()
{
std::lock_guard<std::mutex> lock(mMetricsMutex);
++mMetrics.renderedFrames;
}
void RenderThread::CountCompleted()
{
std::lock_guard<std::mutex> lock(mMetricsMutex);
++mMetrics.completedReadbacks;
}
void RenderThread::CountAcquireMiss()
{
std::lock_guard<std::mutex> lock(mMetricsMutex);
++mMetrics.acquireMisses;
}

View File

@@ -0,0 +1,68 @@
#pragma once
#include "RenderCadenceClock.h"
#include <atomic>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <string>
#include <thread>
class SystemFrameExchange;
class RenderThread
{
public:
struct Config
{
unsigned width = 1920;
unsigned height = 1080;
double frameDurationMilliseconds = 1000.0 / 59.94;
std::size_t pboDepth = 6;
};
struct Metrics
{
uint64_t renderedFrames = 0;
uint64_t completedReadbacks = 0;
uint64_t acquireMisses = 0;
uint64_t pboQueueMisses = 0;
uint64_t clockOverruns = 0;
uint64_t skippedFrames = 0;
};
RenderThread(SystemFrameExchange& frameExchange, Config config);
RenderThread(const RenderThread&) = delete;
RenderThread& operator=(const RenderThread&) = delete;
~RenderThread();
bool Start(std::string& error);
void Stop();
Metrics GetMetrics() const;
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
private:
void ThreadMain();
void SignalStarted();
void SignalStartupFailure(const std::string& error);
void CountRendered();
void CountCompleted();
void CountAcquireMiss();
SystemFrameExchange& mFrameExchange;
Config mConfig;
std::thread mThread;
std::atomic<bool> mStopping{ false };
std::atomic<bool> mRunning{ false };
mutable std::mutex mStartupMutex;
std::condition_variable mStartupCondition;
bool mStarted = false;
std::string mStartupError;
mutable std::mutex mMetricsMutex;
Metrics mMetrics;
};

View File

@@ -0,0 +1,50 @@
#include "SimpleMotionRenderer.h"
#include "GLExtensions.h"
#include <algorithm>
#include <cmath>
bool SimpleMotionRenderer::InitializeGl(unsigned width, unsigned height)
{
mWidth = width;
mHeight = height;
return mWidth > 0 && mHeight > 0;
}
void SimpleMotionRenderer::RenderFrame(uint64_t frameIndex)
{
const float t = static_cast<float>(frameIndex) / 60.0f;
const float red = 0.1f + 0.4f * (0.5f + 0.5f * std::sin(t));
const float green = 0.1f + 0.4f * (0.5f + 0.5f * std::sin(t * 0.73f + 1.0f));
const float blue = 0.15f + 0.3f * (0.5f + 0.5f * std::sin(t * 0.41f + 2.0f));
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
glDisable(GL_SCISSOR_TEST);
glClearColor(red, green, blue, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
const int boxWidth = (std::max)(1, static_cast<int>(mWidth / 6));
const int boxHeight = (std::max)(1, static_cast<int>(mHeight / 5));
const float phase = 0.5f + 0.5f * std::sin(t * 1.7f);
const int x = static_cast<int>(phase * static_cast<float>(mWidth - static_cast<unsigned>(boxWidth)));
const int y = static_cast<int>((0.5f + 0.5f * std::sin(t * 1.1f + 0.8f)) * static_cast<float>(mHeight - static_cast<unsigned>(boxHeight)));
glEnable(GL_SCISSOR_TEST);
glScissor(x, y, boxWidth, boxHeight);
glClearColor(1.0f - red, 0.85f, 0.15f + blue, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
const int stripeWidth = (std::max)(1, static_cast<int>(mWidth / 80));
const int stripeX = static_cast<int>((frameIndex % 120) * (mWidth - static_cast<unsigned>(stripeWidth)) / 119);
glScissor(stripeX, 0, stripeWidth, static_cast<GLsizei>(mHeight));
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_SCISSOR_TEST);
}
void SimpleMotionRenderer::ShutdownGl()
{
mWidth = 0;
mHeight = 0;
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
class SimpleMotionRenderer
{
public:
SimpleMotionRenderer() = default;
bool InitializeGl(unsigned width, unsigned height);
void RenderFrame(uint64_t frameIndex);
void ShutdownGl();
unsigned Width() const { return mWidth; }
unsigned Height() const { return mHeight; }
private:
unsigned mWidth = 0;
unsigned mHeight = 0;
};