PNG writer
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m35s
CI / Windows Release Package (push) Successful in 2m17s

This commit is contained in:
2026-05-08 15:33:40 +10:00
parent 6ea70d9497
commit 05d0bcbedd
18 changed files with 509 additions and 14 deletions

View File

@@ -99,7 +99,7 @@
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<TargetMachine>MachineX86</TargetMachine>
@@ -121,7 +121,7 @@
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<TargetMachine>MachineX64</TargetMachine>
@@ -141,7 +141,7 @@
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<OptimizeReferences>true</OptimizeReferences>
@@ -166,7 +166,7 @@
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<OptimizeReferences>true</OptimizeReferences>
@@ -181,6 +181,7 @@
<ClCompile Include="gl\OpenGLRenderPass.cpp" />
<ClCompile Include="gl\OpenGLRenderer.cpp" />
<ClCompile Include="gl\OpenGLShaderPrograms.cpp" />
<ClCompile Include="gl\PngScreenshotWriter.cpp" />
<ClCompile Include="gl\ShaderBuildQueue.cpp" />
<ClCompile Include="gl\TemporalHistoryBuffers.cpp" />
<ClCompile Include="stdafx.cpp">
@@ -202,6 +203,7 @@
<ClInclude Include="gl\OpenGLRenderPass.h" />
<ClInclude Include="gl\OpenGLRenderer.h" />
<ClInclude Include="gl\OpenGLShaderPrograms.h" />
<ClInclude Include="gl\PngScreenshotWriter.h" />
<ClInclude Include="gl\ShaderBuildQueue.h" />
<ClInclude Include="gl\TemporalHistoryBuffers.h" />
<ClInclude Include="resource.h" />

View File

@@ -36,6 +36,9 @@
<ClCompile Include="gl\OpenGLShaderPrograms.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\PngScreenshotWriter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\ShaderBuildQueue.cpp">
<Filter>Source Files</Filter>
</ClCompile>
@@ -80,6 +83,9 @@
<ClInclude Include="gl\OpenGLShaderPrograms.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\PngScreenshotWriter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\ShaderBuildQueue.h">
<Filter>Header Files</Filter>
</ClInclude>

View File

@@ -433,6 +433,11 @@ bool ControlServer::InvokePostRoute(const std::string& path, const JsonValue& ro
{
return mCallbacks.reloadShader && mCallbacks.reloadShader(error);
}
},
{ "/api/screenshot", [this](const JsonValue&, std::string& error)
{
return mCallbacks.requestScreenshot && mCallbacks.requestScreenshot(error);
}
}
};

View File

@@ -32,6 +32,7 @@ public:
std::function<bool(const std::string&, std::string&)> saveStackPreset;
std::function<bool(const std::string&, std::string&)> loadStackPreset;
std::function<bool(std::string&)> reloadShader;
std::function<bool(std::string&)> requestScreenshot;
};
ControlServer();

View File

@@ -26,6 +26,7 @@ bool StartRuntimeControlServices(
callbacks.resetLayerParameters = [&composite](const std::string& layerId, std::string& actionError) { return composite.ResetLayerParameters(layerId, actionError); };
callbacks.saveStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.SaveStackPreset(presetName, actionError); };
callbacks.loadStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.LoadStackPreset(presetName, actionError); };
callbacks.requestScreenshot = [&composite](std::string& actionError) { return composite.RequestScreenshot(actionError); };
callbacks.reloadShader = [&composite](std::string& actionError) {
if (!composite.ReloadShader())
{

View File

@@ -6,10 +6,17 @@
#include "OpenGLDeckLinkBridge.h"
#include "OpenGLRenderPass.h"
#include "OpenGLShaderPrograms.h"
#include "PngScreenshotWriter.h"
#include "RuntimeServices.h"
#include "ShaderBuildQueue.h"
#include <algorithm>
#include <chrono>
#include <ctime>
#include <filesystem>
#include <iomanip>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
@@ -17,7 +24,8 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
mDeckLink(std::make_unique<DeckLinkSession>()),
mRenderer(std::make_unique<OpenGLRenderer>()),
mUseCommittedLayerStates(false)
mUseCommittedLayerStates(false),
mScreenshotRequested(false)
{
InitializeCriticalSection(&pMutex);
mRuntimeHost = std::make_unique<RuntimeHost>();
@@ -29,6 +37,7 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
hGLDC,
hGLRC,
[this]() { renderEffect(); },
[this]() { ProcessScreenshotRequest(); },
[this]() { paintGL(); });
mRenderPass = std::make_unique<OpenGLRenderPass>(*mRenderer);
mShaderPrograms = std::make_unique<OpenGLShaderPrograms>(*mRenderer, *mRuntimeHost);
@@ -283,6 +292,13 @@ bool OpenGLComposite::ReloadShader()
return true;
}
bool OpenGLComposite::RequestScreenshot(std::string& error)
{
(void)error;
mScreenshotRequested.store(true);
return true;
}
void OpenGLComposite::renderEffect()
{
ProcessRuntimePollResults();
@@ -324,6 +340,69 @@ void OpenGLComposite::renderEffect()
});
}
void OpenGLComposite::ProcessScreenshotRequest()
{
if (!mScreenshotRequested.exchange(false))
return;
const unsigned width = mDeckLink ? mDeckLink->OutputFrameWidth() : 0;
const unsigned height = mDeckLink ? mDeckLink->OutputFrameHeight() : 0;
if (width == 0 || height == 0)
return;
std::vector<unsigned char> bottomUpPixels(static_cast<std::size_t>(width) * height * 4);
std::vector<unsigned char> topDownPixels(bottomUpPixels.size());
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->OutputFramebuffer());
glReadBuffer(GL_COLOR_ATTACHMENT0);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, bottomUpPixels.data());
glPixelStorei(GL_PACK_ALIGNMENT, 4);
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4;
for (unsigned y = 0; y < height; ++y)
{
const unsigned sourceY = height - 1 - y;
std::copy(
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>(sourceY * rowBytes),
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>((sourceY + 1) * rowBytes),
topDownPixels.begin() + static_cast<std::ptrdiff_t>(y * rowBytes));
}
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
{
const std::filesystem::path root = mRuntimeHost && !mRuntimeHost->GetRuntimeRoot().empty()
? mRuntimeHost->GetRuntimeRoot()
: std::filesystem::current_path();
const auto now = std::chrono::system_clock::now();
const auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
const std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
std::tm localTime = {};
localtime_s(&localTime, &nowTime);
std::ostringstream filename;
filename << "video-shader-toys-"
<< std::put_time(&localTime, "%Y%m%d-%H%M%S")
<< "-" << std::setw(3) << std::setfill('0') << milliseconds.count()
<< ".png";
return root / "screenshots" / filename.str();
}
bool OpenGLComposite::ProcessRuntimePollResults()
{
if (!mRuntimeHost || !mRuntimeServices)

View File

@@ -18,6 +18,7 @@
#include <functional>
#include <atomic>
#include <filesystem>
#include <map>
#include <memory>
#include <string>
@@ -54,6 +55,7 @@ public:
bool ResetLayerParameters(const std::string& layerId, std::string& error);
bool SaveStackPreset(const std::string& presetName, std::string& error);
bool LoadStackPreset(const std::string& presetName, std::string& error);
bool RequestScreenshot(std::string& error);
unsigned short GetControlServerPort() const;
unsigned short GetOscPort() const;
std::string GetControlUrl() const;
@@ -87,11 +89,14 @@ private:
std::unique_ptr<RuntimeServices> mRuntimeServices;
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
std::atomic<bool> mUseCommittedLayerStates;
std::atomic<bool> mScreenshotRequested;
bool InitOpenGLState();
void renderEffect();
bool ProcessRuntimePollResults();
void RequestShaderBuild();
void ProcessScreenshotRequest();
std::filesystem::path BuildScreenshotPath() const;
void broadcastRuntimeState();
void resetTemporalHistoryState();
};

View File

@@ -15,6 +15,7 @@ OpenGLDeckLinkBridge::OpenGLDeckLinkBridge(
HDC hdc,
HGLRC hglrc,
RenderEffectCallback renderEffect,
OutputReadyCallback outputReady,
PaintCallback paint) :
mDeckLink(deckLink),
mRenderer(renderer),
@@ -23,6 +24,7 @@ OpenGLDeckLinkBridge::OpenGLDeckLinkBridge(
mHdc(hdc),
mHglrc(hglrc),
mRenderEffect(renderEffect),
mOutputReady(outputReady),
mPaint(paint)
{
}
@@ -129,6 +131,8 @@ void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedF
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer());
glBlitFramebuffer(0, 0, mDeckLink.InputFrameWidth(), mDeckLink.InputFrameHeight(), 0, 0, mDeckLink.OutputFrameWidth(), mDeckLink.OutputFrameHeight(), GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
if (mOutputReady)
mOutputReady();
if (mDeckLink.OutputIsTenBit())
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());

View File

@@ -16,6 +16,7 @@ class OpenGLDeckLinkBridge
{
public:
using RenderEffectCallback = std::function<void()>;
using OutputReadyCallback = std::function<void()>;
using PaintCallback = std::function<void()>;
OpenGLDeckLinkBridge(
@@ -26,6 +27,7 @@ public:
HDC hdc,
HGLRC hglrc,
RenderEffectCallback renderEffect,
OutputReadyCallback outputReady,
PaintCallback paint);
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
@@ -41,6 +43,7 @@ private:
HDC mHdc;
HGLRC mHglrc;
RenderEffectCallback mRenderEffect;
OutputReadyCallback mOutputReady;
PaintCallback mPaint;
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
double mCompletionIntervalMilliseconds = 0.0;

View File

@@ -0,0 +1,133 @@
#include "PngScreenshotWriter.h"
#include <windows.h>
#include <wincodec.h>
#include <atlbase.h>
#include <sstream>
#include <thread>
namespace
{
std::string HResultToString(HRESULT hr)
{
std::ostringstream stream;
stream << "HRESULT 0x" << std::hex << static_cast<unsigned long>(hr);
return stream.str();
}
bool WritePngFile(
const std::filesystem::path& outputPath,
unsigned width,
unsigned height,
const std::vector<unsigned char>& rgbaPixels,
std::string& error)
{
if (width == 0 || height == 0 || rgbaPixels.size() < static_cast<std::size_t>(width) * height * 4)
{
error = "Invalid screenshot dimensions or pixel buffer.";
return false;
}
HRESULT initializeResult = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
const bool shouldUninitialize = SUCCEEDED(initializeResult);
if (FAILED(initializeResult) && initializeResult != RPC_E_CHANGED_MODE)
{
error = "CoInitializeEx failed: " + HResultToString(initializeResult);
return false;
}
CComPtr<IWICImagingFactory> factory;
HRESULT result = CoCreateInstance(
CLSID_WICImagingFactory,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&factory));
if (FAILED(result))
{
error = "Could not create WIC imaging factory: " + HResultToString(result);
if (shouldUninitialize)
CoUninitialize();
return false;
}
CComPtr<IWICStream> stream;
result = factory->CreateStream(&stream);
if (SUCCEEDED(result))
result = stream->InitializeFromFilename(outputPath.wstring().c_str(), GENERIC_WRITE);
if (FAILED(result))
{
error = "Could not open screenshot output file: " + HResultToString(result);
if (shouldUninitialize)
CoUninitialize();
return false;
}
CComPtr<IWICBitmapEncoder> encoder;
result = factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder);
if (SUCCEEDED(result))
result = encoder->Initialize(stream, WICBitmapEncoderNoCache);
if (FAILED(result))
{
error = "Could not initialize PNG encoder: " + HResultToString(result);
if (shouldUninitialize)
CoUninitialize();
return false;
}
CComPtr<IWICBitmapFrameEncode> frame;
CComPtr<IPropertyBag2> propertyBag;
result = encoder->CreateNewFrame(&frame, &propertyBag);
if (SUCCEEDED(result))
result = frame->Initialize(propertyBag);
if (SUCCEEDED(result))
result = frame->SetSize(width, height);
WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppRGBA;
if (SUCCEEDED(result))
result = frame->SetPixelFormat(&pixelFormat);
if (SUCCEEDED(result) && pixelFormat != GUID_WICPixelFormat32bppRGBA)
{
error = "PNG encoder did not accept RGBA pixel format.";
result = E_FAIL;
}
const UINT stride = width * 4;
const UINT imageSize = stride * height;
if (SUCCEEDED(result))
result = frame->WritePixels(height, stride, imageSize, const_cast<BYTE*>(rgbaPixels.data()));
if (SUCCEEDED(result))
result = frame->Commit();
if (SUCCEEDED(result))
result = encoder->Commit();
if (shouldUninitialize)
CoUninitialize();
if (FAILED(result))
{
error = "Could not write screenshot PNG: " + HResultToString(result);
return false;
}
return true;
}
}
void WritePngFileAsync(
const std::filesystem::path& outputPath,
unsigned width,
unsigned height,
std::vector<unsigned char> rgbaPixels)
{
std::thread(
[outputPath, width, height, pixels = std::move(rgbaPixels)]() mutable
{
std::string error;
if (!WritePngFile(outputPath, width, height, pixels, error))
OutputDebugStringA(("Screenshot write failed: " + error + "\n").c_str());
else
OutputDebugStringA(("Screenshot written: " + outputPath.string() + "\n").c_str());
}).detach();
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
void WritePngFileAsync(
const std::filesystem::path& outputPath,
unsigned width,
unsigned height,
std::vector<unsigned char> rgbaPixels);