Compare commits
2 Commits
c5fd8e72b4
...
da7e1a93f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da7e1a93f6 | ||
|
|
334693f28c |
@@ -336,6 +336,9 @@ set(RENDER_CADENCE_APP_SOURCES
|
|||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.h"
|
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeRenderScene.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/RuntimeRenderScene.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeRenderScene.h"
|
"${RENDER_CADENCE_APP_DIR}/render/RuntimeRenderScene.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderPrepareWorker.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderPrepareWorker.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderProgram.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.h"
|
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp"
|
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp"
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ Included now:
|
|||||||
- app-owned display/render layer model for shader build readiness
|
- app-owned display/render layer model for shader build readiness
|
||||||
- app-owned submission of a completed shader artifact
|
- app-owned submission of a completed shader artifact
|
||||||
- render-thread-owned runtime render scene for ready shader layers
|
- render-thread-owned runtime render scene for ready shader layers
|
||||||
- render-thread-only GL commit once the artifact is ready
|
- shared-context GL prepare worker for runtime shader program compile/link
|
||||||
|
- render-thread-only GL program swap once a prepared program is ready
|
||||||
- manifest-driven stateless single-pass shader packages
|
- manifest-driven stateless single-pass shader packages
|
||||||
- HTTP shader list populated from supported stateless single-pass shader packages
|
- HTTP shader list populated from supported stateless single-pass shader packages
|
||||||
- default float, vec2, color, boolean, enum, and trigger parameters
|
- default float, vec2, color, boolean, enum, and trigger parameters
|
||||||
@@ -149,6 +150,7 @@ Current endpoints:
|
|||||||
|
|
||||||
- `GET /` and UI asset paths: serve the bundled control UI from `ui/dist`
|
- `GET /` and UI asset paths: serve the bundled control UI from `ui/dist`
|
||||||
- `GET /api/state`: returns OpenAPI-shaped display data with cadence telemetry, supported shaders, output status, and a read-only current runtime layer
|
- `GET /api/state`: returns OpenAPI-shaped display data with cadence telemetry, supported shaders, output status, and a read-only current runtime layer
|
||||||
|
- `GET /ws`: upgrades to a WebSocket and streams state snapshots when they change
|
||||||
- `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document
|
- `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document
|
||||||
- `GET /docs`: serves Swagger UI
|
- `GET /docs`: serves Swagger UI
|
||||||
- `POST /api/layers/add` and `POST /api/layers/remove` mutate the app-owned display layer model only
|
- `POST /api/layers/add` and `POST /api/layers/remove` mutate the app-owned display layer model only
|
||||||
@@ -197,7 +199,7 @@ Healthy first-run signs:
|
|||||||
|
|
||||||
On startup the app begins compiling the selected shader package on a background thread owned by the app orchestration layer. The default is `shaders/happy-accident`.
|
On startup the app begins compiling the selected shader package on a background thread owned by the app orchestration layer. The default is `shaders/happy-accident`.
|
||||||
|
|
||||||
The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. It only receives a completed shader artifact and attempts the OpenGL shader compile/link at a frame boundary. If either the Slang build or GL commit fails, the app keeps rendering the simple motion fallback.
|
The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. Once a completed shader artifact is published, the render-thread-owned runtime scene queues changed layers to a shared-context GL prepare worker. That worker compiles/links runtime shader programs off the cadence thread. The render thread only swaps in an already-prepared GL program at a frame boundary. If either the Slang build or GL preparation fails, the app keeps rendering the current renderer or simple motion fallback.
|
||||||
|
|
||||||
Current runtime shader support is deliberately limited to stateless single-pass packages:
|
Current runtime shader support is deliberately limited to stateless single-pass packages:
|
||||||
|
|
||||||
@@ -213,7 +215,7 @@ The `/api/state` shader list uses the same support rules as runtime shader compi
|
|||||||
|
|
||||||
Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter defaults. The model also records whether each layer has a render-ready artifact. Add/remove POST controls mutate this app-owned model and may start background shader builds.
|
Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter defaults. The model also records whether each layer has a render-ready artifact. Add/remove POST controls mutate this app-owned model and may start background shader builds.
|
||||||
|
|
||||||
When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, commits/removes GL programs, and renders the ready layers in order. Current layer rendering is still deliberately simple: each stateless full-frame shader draws to the output target using fallback source textures until proper layer-input texture handoff is designed.
|
When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, queues new or changed programs to the shared-context prepare worker, swaps in prepared programs when available, removes obsolete GL programs, and renders ready layers in order. Current layer rendering is still deliberately simple: each stateless full-frame shader draws to the output target using fallback source textures until proper layer-input texture handoff is designed.
|
||||||
|
|
||||||
Successful handoff signs:
|
Successful handoff signs:
|
||||||
|
|
||||||
@@ -264,6 +266,7 @@ This app keeps the same core behavior but splits it into modules that can grow:
|
|||||||
- `platform/`: COM/Win32/hidden GL context support
|
- `platform/`: COM/Win32/hidden GL context support
|
||||||
- `render/`: cadence, simple rendering, PBO readback
|
- `render/`: cadence, simple rendering, PBO readback
|
||||||
- `render/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
|
- `render/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
|
||||||
|
- `render/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker
|
||||||
- `runtime/`: app-owned shader layer readiness model, runtime Slang build bridge, and completed artifact handoff
|
- `runtime/`: app-owned shader layer readiness model, runtime Slang build bridge, and completed artifact handoff
|
||||||
- `control/`: local HTTP API edge and runtime-state JSON presentation
|
- `control/`: local HTTP API edge and runtime-state JSON presentation
|
||||||
- `json/`: compact JSON serialization helpers
|
- `json/`: compact JSON serialization helpers
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
#include <ws2tcpip.h>
|
#include <ws2tcpip.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cstdint>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace RenderCadenceCompositor
|
namespace RenderCadenceCompositor
|
||||||
{
|
{
|
||||||
@@ -41,6 +44,117 @@ bool IsKnownPostEndpoint(const std::string& path)
|
|||||||
|| path == "/api/reload"
|
|| path == "/api/reload"
|
||||||
|| path == "/api/screenshot";
|
|| path == "/api/screenshot";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::array<uint8_t, 20> Sha1(const std::string& input)
|
||||||
|
{
|
||||||
|
auto leftRotate = [](uint32_t value, uint32_t bits) {
|
||||||
|
return (value << bits) | (value >> (32U - bits));
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<uint8_t> data(input.begin(), input.end());
|
||||||
|
const uint64_t bitLength = static_cast<uint64_t>(data.size()) * 8ULL;
|
||||||
|
data.push_back(0x80);
|
||||||
|
while ((data.size() % 64) != 56)
|
||||||
|
data.push_back(0);
|
||||||
|
for (int shift = 56; shift >= 0; shift -= 8)
|
||||||
|
data.push_back(static_cast<uint8_t>((bitLength >> shift) & 0xff));
|
||||||
|
|
||||||
|
uint32_t h0 = 0x67452301;
|
||||||
|
uint32_t h1 = 0xefcdab89;
|
||||||
|
uint32_t h2 = 0x98badcfe;
|
||||||
|
uint32_t h3 = 0x10325476;
|
||||||
|
uint32_t h4 = 0xc3d2e1f0;
|
||||||
|
|
||||||
|
for (std::size_t offset = 0; offset < data.size(); offset += 64)
|
||||||
|
{
|
||||||
|
uint32_t words[80] = {};
|
||||||
|
for (std::size_t i = 0; i < 16; ++i)
|
||||||
|
{
|
||||||
|
const std::size_t index = offset + i * 4;
|
||||||
|
words[i] = (static_cast<uint32_t>(data[index]) << 24)
|
||||||
|
| (static_cast<uint32_t>(data[index + 1]) << 16)
|
||||||
|
| (static_cast<uint32_t>(data[index + 2]) << 8)
|
||||||
|
| static_cast<uint32_t>(data[index + 3]);
|
||||||
|
}
|
||||||
|
for (std::size_t i = 16; i < 80; ++i)
|
||||||
|
words[i] = leftRotate(words[i - 3] ^ words[i - 8] ^ words[i - 14] ^ words[i - 16], 1);
|
||||||
|
|
||||||
|
uint32_t a = h0;
|
||||||
|
uint32_t b = h1;
|
||||||
|
uint32_t c = h2;
|
||||||
|
uint32_t d = h3;
|
||||||
|
uint32_t e = h4;
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < 80; ++i)
|
||||||
|
{
|
||||||
|
uint32_t f = 0;
|
||||||
|
uint32_t k = 0;
|
||||||
|
if (i < 20)
|
||||||
|
{
|
||||||
|
f = (b & c) | ((~b) & d);
|
||||||
|
k = 0x5a827999;
|
||||||
|
}
|
||||||
|
else if (i < 40)
|
||||||
|
{
|
||||||
|
f = b ^ c ^ d;
|
||||||
|
k = 0x6ed9eba1;
|
||||||
|
}
|
||||||
|
else if (i < 60)
|
||||||
|
{
|
||||||
|
f = (b & c) | (b & d) | (c & d);
|
||||||
|
k = 0x8f1bbcdc;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
f = b ^ c ^ d;
|
||||||
|
k = 0xca62c1d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t temp = leftRotate(a, 5) + f + e + k + words[i];
|
||||||
|
e = d;
|
||||||
|
d = c;
|
||||||
|
c = leftRotate(b, 30);
|
||||||
|
b = a;
|
||||||
|
a = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
h0 += a;
|
||||||
|
h1 += b;
|
||||||
|
h2 += c;
|
||||||
|
h3 += d;
|
||||||
|
h4 += e;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<uint8_t, 20> digest = {};
|
||||||
|
const uint32_t parts[] = { h0, h1, h2, h3, h4 };
|
||||||
|
for (std::size_t i = 0; i < 5; ++i)
|
||||||
|
{
|
||||||
|
digest[i * 4] = static_cast<uint8_t>((parts[i] >> 24) & 0xff);
|
||||||
|
digest[i * 4 + 1] = static_cast<uint8_t>((parts[i] >> 16) & 0xff);
|
||||||
|
digest[i * 4 + 2] = static_cast<uint8_t>((parts[i] >> 8) & 0xff);
|
||||||
|
digest[i * 4 + 3] = static_cast<uint8_t>(parts[i] & 0xff);
|
||||||
|
}
|
||||||
|
return digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Base64Encode(const uint8_t* data, std::size_t size)
|
||||||
|
{
|
||||||
|
static constexpr char kAlphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
std::string output;
|
||||||
|
output.reserve(((size + 2) / 3) * 4);
|
||||||
|
for (std::size_t i = 0; i < size; i += 3)
|
||||||
|
{
|
||||||
|
const uint32_t a = data[i];
|
||||||
|
const uint32_t b = i + 1 < size ? data[i + 1] : 0;
|
||||||
|
const uint32_t c = i + 2 < size ? data[i + 2] : 0;
|
||||||
|
const uint32_t triple = (a << 16) | (b << 8) | c;
|
||||||
|
output.push_back(kAlphabet[(triple >> 18) & 0x3f]);
|
||||||
|
output.push_back(kAlphabet[(triple >> 12) & 0x3f]);
|
||||||
|
output.push_back(i + 1 < size ? kAlphabet[(triple >> 6) & 0x3f] : '=');
|
||||||
|
output.push_back(i + 2 < size ? kAlphabet[triple & 0x3f] : '=');
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UniqueSocket::UniqueSocket(SOCKET socket) :
|
UniqueSocket::UniqueSocket(SOCKET socket) :
|
||||||
@@ -157,6 +271,20 @@ void HttpControlServer::Stop()
|
|||||||
if (mThread.joinable())
|
if (mThread.joinable())
|
||||||
mThread.join();
|
mThread.join();
|
||||||
|
|
||||||
|
std::vector<std::thread> clientThreads;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||||
|
clientThreads.swap(mClientThreads);
|
||||||
|
for (std::thread& thread : mFinishedClientThreads)
|
||||||
|
clientThreads.push_back(std::move(thread));
|
||||||
|
mFinishedClientThreads.clear();
|
||||||
|
}
|
||||||
|
for (std::thread& thread : clientThreads)
|
||||||
|
{
|
||||||
|
if (thread.joinable())
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
if (mWinsockStarted)
|
if (mWinsockStarted)
|
||||||
{
|
{
|
||||||
WSACleanup();
|
WSACleanup();
|
||||||
@@ -185,6 +313,7 @@ void HttpControlServer::ThreadMain()
|
|||||||
{
|
{
|
||||||
while (mRunning.load(std::memory_order_acquire))
|
while (mRunning.load(std::memory_order_acquire))
|
||||||
{
|
{
|
||||||
|
JoinFinishedClientThreads();
|
||||||
TryAcceptClient();
|
TryAcceptClient();
|
||||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||||
}
|
}
|
||||||
@@ -212,9 +341,81 @@ bool HttpControlServer::HandleClient(UniqueSocket clientSocket)
|
|||||||
if (!ParseHttpRequest(std::string(buffer, buffer + received), request))
|
if (!ParseHttpRequest(std::string(buffer, buffer + received), request))
|
||||||
return SendResponse(clientSocket.get(), TextResponse("400 Bad Request", "Bad Request"));
|
return SendResponse(clientSocket.get(), TextResponse("400 Bad Request", "Bad Request"));
|
||||||
|
|
||||||
|
if (request.path == "/ws")
|
||||||
|
return HandleWebSocketClient(std::move(clientSocket), request);
|
||||||
|
|
||||||
return SendResponse(clientSocket.get(), RouteRequest(request));
|
return SendResponse(clientSocket.get(), RouteRequest(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool HttpControlServer::HandleWebSocketClient(UniqueSocket clientSocket, const HttpRequest& request)
|
||||||
|
{
|
||||||
|
const auto keyIt = request.headers.find("sec-websocket-key");
|
||||||
|
if (keyIt == request.headers.end() || keyIt->second.empty())
|
||||||
|
return SendResponse(clientSocket.get(), TextResponse("400 Bad Request", "Missing WebSocket key"));
|
||||||
|
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << "HTTP/1.1 101 Switching Protocols\r\n"
|
||||||
|
<< "Upgrade: websocket\r\n"
|
||||||
|
<< "Connection: Upgrade\r\n"
|
||||||
|
<< "Sec-WebSocket-Accept: " << WebSocketAcceptKey(keyIt->second) << "\r\n\r\n";
|
||||||
|
const std::string response = stream.str();
|
||||||
|
if (send(clientSocket.get(), response.c_str(), static_cast<int>(response.size()), 0) != static_cast<int>(response.size()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
u_long nonBlocking = 1;
|
||||||
|
ioctlsocket(clientSocket.get(), FIONBIO, &nonBlocking);
|
||||||
|
|
||||||
|
std::thread thread([this, socket = std::move(clientSocket)]() mutable {
|
||||||
|
WebSocketClientMain(std::move(socket));
|
||||||
|
});
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||||
|
mClientThreads.push_back(std::move(thread));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpControlServer::WebSocketClientMain(UniqueSocket clientSocket)
|
||||||
|
{
|
||||||
|
std::string previousState;
|
||||||
|
while (mRunning.load(std::memory_order_acquire))
|
||||||
|
{
|
||||||
|
const std::string state = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
|
||||||
|
if (state != previousState)
|
||||||
|
{
|
||||||
|
if (!SendWebSocketText(clientSocket.get(), state))
|
||||||
|
break;
|
||||||
|
previousState = state;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||||
|
const std::thread::id currentId = std::this_thread::get_id();
|
||||||
|
for (auto it = mClientThreads.begin(); it != mClientThreads.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it->get_id() != currentId)
|
||||||
|
continue;
|
||||||
|
mFinishedClientThreads.push_back(std::move(*it));
|
||||||
|
mClientThreads.erase(it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpControlServer::JoinFinishedClientThreads()
|
||||||
|
{
|
||||||
|
std::vector<std::thread> finished;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||||
|
finished.swap(mFinishedClientThreads);
|
||||||
|
}
|
||||||
|
for (std::thread& thread : finished)
|
||||||
|
{
|
||||||
|
if (thread.joinable())
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool HttpControlServer::SendResponse(SOCKET clientSocket, const HttpResponse& response) const
|
bool HttpControlServer::SendResponse(SOCKET clientSocket, const HttpResponse& response) const
|
||||||
{
|
{
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
@@ -360,6 +561,61 @@ std::string HttpControlServer::ActionResponse(bool ok, const std::string& error)
|
|||||||
return writer.StringValue();
|
return writer.StringValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool HttpControlServer::SendWebSocketText(SOCKET clientSocket, const std::string& text)
|
||||||
|
{
|
||||||
|
if (clientSocket == INVALID_SOCKET)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::vector<unsigned char> frame;
|
||||||
|
frame.reserve(text.size() + 16);
|
||||||
|
frame.push_back(0x81);
|
||||||
|
if (text.size() <= 125)
|
||||||
|
{
|
||||||
|
frame.push_back(static_cast<unsigned char>(text.size()));
|
||||||
|
}
|
||||||
|
else if (text.size() <= 0xffff)
|
||||||
|
{
|
||||||
|
frame.push_back(126);
|
||||||
|
frame.push_back(static_cast<unsigned char>((text.size() >> 8) & 0xff));
|
||||||
|
frame.push_back(static_cast<unsigned char>(text.size() & 0xff));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
frame.push_back(127);
|
||||||
|
const uint64_t length = static_cast<uint64_t>(text.size());
|
||||||
|
for (int shift = 56; shift >= 0; shift -= 8)
|
||||||
|
frame.push_back(static_cast<unsigned char>((length >> shift) & 0xff));
|
||||||
|
}
|
||||||
|
frame.insert(frame.end(), text.begin(), text.end());
|
||||||
|
|
||||||
|
const char* data = reinterpret_cast<const char*>(frame.data());
|
||||||
|
int remaining = static_cast<int>(frame.size());
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
const int sent = send(clientSocket, data, remaining, 0);
|
||||||
|
if (sent <= 0)
|
||||||
|
{
|
||||||
|
const int error = WSAGetLastError();
|
||||||
|
if (error == WSAEWOULDBLOCK)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
data += sent;
|
||||||
|
remaining -= sent;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpControlServer::WebSocketAcceptKey(const std::string& clientKey)
|
||||||
|
{
|
||||||
|
static constexpr const char* kWebSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
|
const std::array<uint8_t, 20> digest = Sha1(clientKey + kWebSocketGuid);
|
||||||
|
return Base64Encode(digest.data(), digest.size());
|
||||||
|
}
|
||||||
|
|
||||||
std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
|
std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
|
||||||
{
|
{
|
||||||
const std::string extension = ToLower(path.extension().string());
|
const std::string extension = ToLower(path.extension().string());
|
||||||
|
|||||||
@@ -9,8 +9,10 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace RenderCadenceCompositor
|
namespace RenderCadenceCompositor
|
||||||
{
|
{
|
||||||
@@ -89,11 +91,15 @@ public:
|
|||||||
HttpResponse RouteRequestForTest(const HttpRequest& request) const;
|
HttpResponse RouteRequestForTest(const HttpRequest& request) const;
|
||||||
|
|
||||||
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
|
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
|
||||||
|
static std::string WebSocketAcceptKey(const std::string& clientKey);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ThreadMain();
|
void ThreadMain();
|
||||||
bool TryAcceptClient();
|
bool TryAcceptClient();
|
||||||
bool HandleClient(UniqueSocket clientSocket);
|
bool HandleClient(UniqueSocket clientSocket);
|
||||||
|
bool HandleWebSocketClient(UniqueSocket clientSocket, const HttpRequest& request);
|
||||||
|
void WebSocketClientMain(UniqueSocket clientSocket);
|
||||||
|
void JoinFinishedClientThreads();
|
||||||
bool SendResponse(SOCKET clientSocket, const HttpResponse& response) const;
|
bool SendResponse(SOCKET clientSocket, const HttpResponse& response) const;
|
||||||
HttpResponse RouteRequest(const HttpRequest& request) const;
|
HttpResponse RouteRequest(const HttpRequest& request) const;
|
||||||
HttpResponse ServeGet(const HttpRequest& request) const;
|
HttpResponse ServeGet(const HttpRequest& request) const;
|
||||||
@@ -107,6 +113,7 @@ private:
|
|||||||
static HttpResponse TextResponse(const std::string& status, const std::string& body);
|
static HttpResponse TextResponse(const std::string& status, const std::string& body);
|
||||||
static HttpResponse HtmlResponse(const std::string& status, const std::string& body);
|
static HttpResponse HtmlResponse(const std::string& status, const std::string& body);
|
||||||
static std::string ActionResponse(bool ok, const std::string& error = std::string());
|
static std::string ActionResponse(bool ok, const std::string& error = std::string());
|
||||||
|
static bool SendWebSocketText(SOCKET clientSocket, const std::string& text);
|
||||||
static std::string GuessContentType(const std::filesystem::path& path);
|
static std::string GuessContentType(const std::filesystem::path& path);
|
||||||
static bool IsSafeRelativePath(const std::filesystem::path& path);
|
static bool IsSafeRelativePath(const std::filesystem::path& path);
|
||||||
static std::string ToLower(std::string text);
|
static std::string ToLower(std::string text);
|
||||||
@@ -117,6 +124,9 @@ private:
|
|||||||
HttpControlServerCallbacks mCallbacks;
|
HttpControlServerCallbacks mCallbacks;
|
||||||
UniqueSocket mListenSocket;
|
UniqueSocket mListenSocket;
|
||||||
std::thread mThread;
|
std::thread mThread;
|
||||||
|
std::mutex mClientThreadsMutex;
|
||||||
|
std::vector<std::thread> mClientThreads;
|
||||||
|
std::vector<std::thread> mFinishedClientThreads;
|
||||||
std::atomic<bool> mRunning{ false };
|
std::atomic<bool> mRunning{ false };
|
||||||
unsigned short mPort = 0;
|
unsigned short mPort = 0;
|
||||||
bool mWinsockStarted = false;
|
bool mWinsockStarted = false;
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ HiddenGlWindow::~HiddenGlWindow()
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error)
|
bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error)
|
||||||
|
{
|
||||||
|
return CreateShared(width, height, nullptr, nullptr, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HiddenGlWindow::CreateShared(unsigned width, unsigned height, HDC sharedDeviceContext, HGLRC sharedContext, std::string& error)
|
||||||
{
|
{
|
||||||
Destroy();
|
Destroy();
|
||||||
|
|
||||||
@@ -63,7 +68,11 @@ bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error)
|
|||||||
pfd.cDepthBits = 0;
|
pfd.cDepthBits = 0;
|
||||||
pfd.iLayerType = PFD_MAIN_PLANE;
|
pfd.iLayerType = PFD_MAIN_PLANE;
|
||||||
|
|
||||||
const int pixelFormat = ChoosePixelFormat(mDc, &pfd);
|
int pixelFormat = 0;
|
||||||
|
if (sharedDeviceContext != nullptr)
|
||||||
|
pixelFormat = GetPixelFormat(sharedDeviceContext);
|
||||||
|
if (pixelFormat == 0)
|
||||||
|
pixelFormat = ChoosePixelFormat(mDc, &pfd);
|
||||||
if (pixelFormat == 0 || !SetPixelFormat(mDc, pixelFormat, &pfd))
|
if (pixelFormat == 0 || !SetPixelFormat(mDc, pixelFormat, &pfd))
|
||||||
{
|
{
|
||||||
error = "Could not choose/set pixel format for hidden OpenGL window.";
|
error = "Could not choose/set pixel format for hidden OpenGL window.";
|
||||||
@@ -76,6 +85,11 @@ bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error)
|
|||||||
error = "wglCreateContext failed for hidden OpenGL window.";
|
error = "wglCreateContext failed for hidden OpenGL window.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (sharedContext != nullptr && wglShareLists(sharedContext, mGlrc) != TRUE)
|
||||||
|
{
|
||||||
|
error = "wglShareLists failed for hidden OpenGL shared context.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public:
|
|||||||
~HiddenGlWindow();
|
~HiddenGlWindow();
|
||||||
|
|
||||||
bool Create(unsigned width, unsigned height, std::string& error);
|
bool Create(unsigned width, unsigned height, std::string& error);
|
||||||
|
bool CreateShared(unsigned width, unsigned height, HDC sharedDeviceContext, HGLRC sharedContext, std::string& error);
|
||||||
bool MakeCurrent() const;
|
bool MakeCurrent() const;
|
||||||
void ClearCurrent() const;
|
void ClearCurrent() const;
|
||||||
void Destroy();
|
void Destroy();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "SimpleMotionRenderer.h"
|
#include "SimpleMotionRenderer.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <memory>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) :
|
RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) :
|
||||||
@@ -83,11 +84,22 @@ void RenderThread::ThreadMain()
|
|||||||
RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread starting.");
|
RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread starting.");
|
||||||
HiddenGlWindow window;
|
HiddenGlWindow window;
|
||||||
std::string error;
|
std::string error;
|
||||||
if (!window.Create(mConfig.width, mConfig.height, error) || !window.MakeCurrent())
|
if (!window.Create(mConfig.width, mConfig.height, error))
|
||||||
{
|
{
|
||||||
SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error);
|
SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
std::unique_ptr<HiddenGlWindow> prepareWindow = std::make_unique<HiddenGlWindow>();
|
||||||
|
if (!prepareWindow->CreateShared(mConfig.width, mConfig.height, window.DeviceContext(), window.Context(), error))
|
||||||
|
{
|
||||||
|
SignalStartupFailure(error.empty() ? "Runtime shader prepare shared context creation failed." : error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!window.MakeCurrent())
|
||||||
|
{
|
||||||
|
SignalStartupFailure("OpenGL context creation failed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!ResolveGLExtensions())
|
if (!ResolveGLExtensions())
|
||||||
{
|
{
|
||||||
SignalStartupFailure("OpenGL extension resolution failed.");
|
SignalStartupFailure("OpenGL extension resolution failed.");
|
||||||
@@ -97,6 +109,11 @@ void RenderThread::ThreadMain()
|
|||||||
SimpleMotionRenderer renderer;
|
SimpleMotionRenderer renderer;
|
||||||
RuntimeRenderScene runtimeRenderScene;
|
RuntimeRenderScene runtimeRenderScene;
|
||||||
Bgra8ReadbackPipeline readback;
|
Bgra8ReadbackPipeline readback;
|
||||||
|
if (!runtimeRenderScene.StartPrepareWorker(std::move(prepareWindow), error))
|
||||||
|
{
|
||||||
|
SignalStartupFailure(error.empty() ? "Runtime shader prepare worker initialization failed." : error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!renderer.InitializeGl(mConfig.width, mConfig.height) || !readback.Initialize(mConfig.width, mConfig.height, mConfig.pboDepth))
|
if (!renderer.InitializeGl(mConfig.width, mConfig.height) || !readback.Initialize(mConfig.width, mConfig.height, mConfig.pboDepth))
|
||||||
{
|
{
|
||||||
SignalStartupFailure("Render pipeline initialization failed.");
|
SignalStartupFailure("Render pipeline initialization failed.");
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "RuntimeRenderScene.h"
|
#include "RuntimeRenderScene.h"
|
||||||
|
|
||||||
|
#include "../platform/HiddenGlWindow.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -9,9 +11,17 @@ RuntimeRenderScene::~RuntimeRenderScene()
|
|||||||
ShutdownGl();
|
ShutdownGl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RuntimeRenderScene::StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error)
|
||||||
|
{
|
||||||
|
return mPrepareWorker.Start(std::move(sharedWindow), error);
|
||||||
|
}
|
||||||
|
|
||||||
bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error)
|
bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error)
|
||||||
{
|
{
|
||||||
|
ConsumePreparedPrograms();
|
||||||
|
|
||||||
std::vector<std::string> nextOrder;
|
std::vector<std::string> nextOrder;
|
||||||
|
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> layersToPrepare;
|
||||||
nextOrder.reserve(layers.size());
|
nextOrder.reserve(layers.size());
|
||||||
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||||
nextOrder.push_back(layer.id);
|
nextOrder.push_back(layer.id);
|
||||||
@@ -48,25 +58,38 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
|
|||||||
|
|
||||||
if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && program->renderer && program->renderer->HasProgram())
|
if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && program->renderer && program->renderer->HasProgram())
|
||||||
continue;
|
continue;
|
||||||
|
if (program->pendingFingerprint == fingerprint)
|
||||||
|
continue;
|
||||||
|
|
||||||
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
|
|
||||||
if (!nextRenderer->CommitShaderArtifact(layer.artifact, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (program->renderer)
|
|
||||||
program->renderer->ShutdownGl();
|
|
||||||
program->shaderId = layer.shaderId;
|
program->shaderId = layer.shaderId;
|
||||||
program->sourceFingerprint = fingerprint;
|
program->pendingFingerprint = fingerprint;
|
||||||
program->renderer = std::move(nextRenderer);
|
layersToPrepare.push_back(layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
mLayerOrder = std::move(nextOrder);
|
mLayerOrder = std::move(nextOrder);
|
||||||
|
if (!layersToPrepare.empty())
|
||||||
|
mPrepareWorker.Submit(layersToPrepare);
|
||||||
error.clear();
|
error.clear();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RuntimeRenderScene::HasLayers()
|
||||||
|
{
|
||||||
|
ConsumePreparedPrograms();
|
||||||
|
|
||||||
|
for (const std::string& layerId : mLayerOrder)
|
||||||
|
{
|
||||||
|
const LayerProgram* layer = FindLayer(layerId);
|
||||||
|
if (layer && layer->renderer && layer->renderer->HasProgram())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height)
|
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height)
|
||||||
{
|
{
|
||||||
|
ConsumePreparedPrograms();
|
||||||
|
|
||||||
for (const std::string& layerId : mLayerOrder)
|
for (const std::string& layerId : mLayerOrder)
|
||||||
{
|
{
|
||||||
LayerProgram* layer = FindLayer(layerId);
|
LayerProgram* layer = FindLayer(layerId);
|
||||||
@@ -78,6 +101,7 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
|
|||||||
|
|
||||||
void RuntimeRenderScene::ShutdownGl()
|
void RuntimeRenderScene::ShutdownGl()
|
||||||
{
|
{
|
||||||
|
mPrepareWorker.Stop();
|
||||||
for (LayerProgram& layer : mLayers)
|
for (LayerProgram& layer : mLayers)
|
||||||
{
|
{
|
||||||
if (layer.renderer)
|
if (layer.renderer)
|
||||||
@@ -87,6 +111,41 @@ void RuntimeRenderScene::ShutdownGl()
|
|||||||
mLayerOrder.clear();
|
mLayerOrder.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::ConsumePreparedPrograms()
|
||||||
|
{
|
||||||
|
RuntimePreparedShaderProgram preparedProgram;
|
||||||
|
while (mPrepareWorker.TryConsume(preparedProgram))
|
||||||
|
{
|
||||||
|
if (!preparedProgram.succeeded)
|
||||||
|
{
|
||||||
|
preparedProgram.ReleaseGl();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayerProgram* layer = FindLayer(preparedProgram.layerId);
|
||||||
|
if (!layer || layer->pendingFingerprint != preparedProgram.sourceFingerprint)
|
||||||
|
{
|
||||||
|
preparedProgram.ReleaseGl();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
|
||||||
|
std::string error;
|
||||||
|
if (!nextRenderer->CommitPreparedProgram(preparedProgram, error))
|
||||||
|
{
|
||||||
|
preparedProgram.ReleaseGl();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer->renderer)
|
||||||
|
layer->renderer->ShutdownGl();
|
||||||
|
layer->renderer = std::move(nextRenderer);
|
||||||
|
layer->shaderId = preparedProgram.shaderId;
|
||||||
|
layer->sourceFingerprint = preparedProgram.sourceFingerprint;
|
||||||
|
layer->pendingFingerprint.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
|
RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
|
||||||
{
|
{
|
||||||
for (LayerProgram& layer : mLayers)
|
for (LayerProgram& layer : mLayers)
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../runtime/RuntimeLayerModel.h"
|
#include "../runtime/RuntimeLayerModel.h"
|
||||||
|
#include "RuntimeShaderPrepareWorker.h"
|
||||||
#include "RuntimeShaderRenderer.h"
|
#include "RuntimeShaderRenderer.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -16,8 +19,9 @@ public:
|
|||||||
RuntimeRenderScene& operator=(const RuntimeRenderScene&) = delete;
|
RuntimeRenderScene& operator=(const RuntimeRenderScene&) = delete;
|
||||||
~RuntimeRenderScene();
|
~RuntimeRenderScene();
|
||||||
|
|
||||||
|
bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
|
||||||
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
|
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
|
||||||
bool HasLayers() const { return !mLayerOrder.empty(); }
|
bool HasLayers();
|
||||||
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
|
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
|
||||||
void ShutdownGl();
|
void ShutdownGl();
|
||||||
|
|
||||||
@@ -27,13 +31,16 @@ private:
|
|||||||
std::string layerId;
|
std::string layerId;
|
||||||
std::string shaderId;
|
std::string shaderId;
|
||||||
std::string sourceFingerprint;
|
std::string sourceFingerprint;
|
||||||
|
std::string pendingFingerprint;
|
||||||
std::unique_ptr<RuntimeShaderRenderer> renderer;
|
std::unique_ptr<RuntimeShaderRenderer> renderer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void ConsumePreparedPrograms();
|
||||||
LayerProgram* FindLayer(const std::string& layerId);
|
LayerProgram* FindLayer(const std::string& layerId);
|
||||||
const LayerProgram* FindLayer(const std::string& layerId) const;
|
const LayerProgram* FindLayer(const std::string& layerId) const;
|
||||||
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
|
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
|
||||||
|
|
||||||
|
RuntimeShaderPrepareWorker mPrepareWorker;
|
||||||
std::vector<LayerProgram> mLayers;
|
std::vector<LayerProgram> mLayers;
|
||||||
std::vector<std::string> mLayerOrder;
|
std::vector<std::string> mLayerOrder;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
#include "RuntimeShaderPrepareWorker.h"
|
||||||
|
|
||||||
|
#include "../platform/HiddenGlWindow.h"
|
||||||
|
#include "RuntimeShaderRenderer.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <functional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
RuntimeShaderPrepareWorker::~RuntimeShaderPrepareWorker()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderPrepareWorker::Start(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error)
|
||||||
|
{
|
||||||
|
if (mThread.joinable())
|
||||||
|
return true;
|
||||||
|
if (!sharedWindow || sharedWindow->DeviceContext() == nullptr || sharedWindow->Context() == nullptr)
|
||||||
|
{
|
||||||
|
error = "Runtime shader prepare worker needs an existing shared GL context.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mWindow = std::move(sharedWindow);
|
||||||
|
mStopping.store(false, std::memory_order_release);
|
||||||
|
mStarted.store(false, std::memory_order_release);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mStartupReady = false;
|
||||||
|
mStartupError.clear();
|
||||||
|
}
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
if (!mStartupCondition.wait_for(lock, std::chrono::seconds(3), [this]() {
|
||||||
|
return mStartupReady || !mStartupError.empty();
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
error = "Timed out starting runtime shader prepare worker.";
|
||||||
|
lock.unlock();
|
||||||
|
Stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mStartupError.empty())
|
||||||
|
{
|
||||||
|
error = mStartupError;
|
||||||
|
lock.unlock();
|
||||||
|
Stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderPrepareWorker::Stop()
|
||||||
|
{
|
||||||
|
mStopping.store(true, std::memory_order_release);
|
||||||
|
mCondition.notify_all();
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
|
||||||
|
std::deque<RuntimePreparedShaderProgram> completed;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mRequests.clear();
|
||||||
|
completed.swap(mCompleted);
|
||||||
|
}
|
||||||
|
for (RuntimePreparedShaderProgram& program : completed)
|
||||||
|
program.ReleaseGl();
|
||||||
|
mWindow.reset();
|
||||||
|
mStarted.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderPrepareWorker::Submit(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||||
|
{
|
||||||
|
if (layer.artifact.fragmentShaderSource.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
PrepareRequest request;
|
||||||
|
request.layerId = layer.id;
|
||||||
|
request.shaderId = layer.shaderId;
|
||||||
|
request.sourceFingerprint = Fingerprint(layer.artifact);
|
||||||
|
request.artifact = layer.artifact;
|
||||||
|
|
||||||
|
auto sameLayer = [&request](const PrepareRequest& existing) {
|
||||||
|
return existing.layerId == request.layerId;
|
||||||
|
};
|
||||||
|
mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end());
|
||||||
|
mRequests.push_back(std::move(request));
|
||||||
|
}
|
||||||
|
mCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderPrepareWorker::TryConsume(RuntimePreparedShaderProgram& preparedProgram)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mCompleted.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
preparedProgram = std::move(mCompleted.front());
|
||||||
|
mCompleted.pop_front();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderPrepareWorker::ThreadMain()
|
||||||
|
{
|
||||||
|
if (!mWindow || !mWindow->MakeCurrent())
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mStartupError = "Runtime shader prepare worker could not make shared GL context current.";
|
||||||
|
mStartupCondition.notify_all();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mStartupReady = true;
|
||||||
|
}
|
||||||
|
mStarted.store(true, std::memory_order_release);
|
||||||
|
mStartupCondition.notify_all();
|
||||||
|
|
||||||
|
while (!mStopping.load(std::memory_order_acquire))
|
||||||
|
{
|
||||||
|
PrepareRequest request;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
mCondition.wait(lock, [this]() {
|
||||||
|
return mStopping.load(std::memory_order_acquire) || !mRequests.empty();
|
||||||
|
});
|
||||||
|
if (mStopping.load(std::memory_order_acquire))
|
||||||
|
break;
|
||||||
|
request = std::move(mRequests.front());
|
||||||
|
mRequests.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimePreparedShaderProgram preparedProgram;
|
||||||
|
RuntimeShaderRenderer::BuildPreparedProgram(
|
||||||
|
request.layerId,
|
||||||
|
request.sourceFingerprint,
|
||||||
|
request.artifact,
|
||||||
|
preparedProgram);
|
||||||
|
glFlush();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mCompleted.push_back(std::move(preparedProgram));
|
||||||
|
}
|
||||||
|
|
||||||
|
mWindow->ClearCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact)
|
||||||
|
{
|
||||||
|
const std::hash<std::string> hasher;
|
||||||
|
return artifact.shaderId + ":" + std::to_string(artifact.fragmentShaderSource.size()) + ":" + std::to_string(hasher(artifact.fragmentShaderSource));
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RuntimeShaderProgram.h"
|
||||||
|
#include "../runtime/RuntimeLayerModel.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <deque>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class HiddenGlWindow;
|
||||||
|
|
||||||
|
class RuntimeShaderPrepareWorker
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RuntimeShaderPrepareWorker() = default;
|
||||||
|
RuntimeShaderPrepareWorker(const RuntimeShaderPrepareWorker&) = delete;
|
||||||
|
RuntimeShaderPrepareWorker& operator=(const RuntimeShaderPrepareWorker&) = delete;
|
||||||
|
~RuntimeShaderPrepareWorker();
|
||||||
|
|
||||||
|
bool Start(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
void Submit(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
|
||||||
|
bool TryConsume(RuntimePreparedShaderProgram& preparedProgram);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct PrepareRequest
|
||||||
|
{
|
||||||
|
std::string layerId;
|
||||||
|
std::string shaderId;
|
||||||
|
std::string sourceFingerprint;
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ThreadMain();
|
||||||
|
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
|
||||||
|
|
||||||
|
std::unique_ptr<HiddenGlWindow> mWindow;
|
||||||
|
std::mutex mMutex;
|
||||||
|
std::condition_variable mCondition;
|
||||||
|
std::deque<PrepareRequest> mRequests;
|
||||||
|
std::deque<RuntimePreparedShaderProgram> mCompleted;
|
||||||
|
std::condition_variable mStartupCondition;
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mStopping{ false };
|
||||||
|
std::atomic<bool> mStarted{ false };
|
||||||
|
bool mStartupReady = false;
|
||||||
|
std::string mStartupError;
|
||||||
|
};
|
||||||
32
apps/RenderCadenceCompositor/render/RuntimeShaderProgram.h
Normal file
32
apps/RenderCadenceCompositor/render/RuntimeShaderProgram.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "../runtime/RuntimeShaderArtifact.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct RuntimePreparedShaderProgram
|
||||||
|
{
|
||||||
|
std::string layerId;
|
||||||
|
std::string shaderId;
|
||||||
|
std::string sourceFingerprint;
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
GLuint program = 0;
|
||||||
|
GLuint vertexShader = 0;
|
||||||
|
GLuint fragmentShader = 0;
|
||||||
|
bool succeeded = false;
|
||||||
|
std::string error;
|
||||||
|
|
||||||
|
void ReleaseGl()
|
||||||
|
{
|
||||||
|
if (program != 0)
|
||||||
|
glDeleteProgram(program);
|
||||||
|
if (vertexShader != 0)
|
||||||
|
glDeleteShader(vertexShader);
|
||||||
|
if (fragmentShader != 0)
|
||||||
|
glDeleteShader(fragmentShader);
|
||||||
|
program = 0;
|
||||||
|
vertexShader = 0;
|
||||||
|
fragmentShader = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -71,6 +71,62 @@ bool RuntimeShaderRenderer::CommitShaderArtifact(const RuntimeShaderArtifact& ar
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error)
|
||||||
|
{
|
||||||
|
if (!preparedProgram.succeeded || preparedProgram.program == 0)
|
||||||
|
{
|
||||||
|
error = preparedProgram.error.empty() ? "Prepared runtime shader program is not valid." : preparedProgram.error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnsureStaticGlResources(error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
DestroyProgram();
|
||||||
|
mProgram = preparedProgram.program;
|
||||||
|
mVertexShader = preparedProgram.vertexShader;
|
||||||
|
mFragmentShader = preparedProgram.fragmentShader;
|
||||||
|
mArtifact = preparedProgram.artifact;
|
||||||
|
preparedProgram.program = 0;
|
||||||
|
preparedProgram.vertexShader = 0;
|
||||||
|
preparedProgram.fragmentShader = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderRenderer::BuildPreparedProgram(
|
||||||
|
const std::string& layerId,
|
||||||
|
const std::string& sourceFingerprint,
|
||||||
|
const RuntimeShaderArtifact& artifact,
|
||||||
|
RuntimePreparedShaderProgram& preparedProgram)
|
||||||
|
{
|
||||||
|
preparedProgram = RuntimePreparedShaderProgram();
|
||||||
|
preparedProgram.layerId = layerId;
|
||||||
|
preparedProgram.shaderId = artifact.shaderId;
|
||||||
|
preparedProgram.sourceFingerprint = sourceFingerprint;
|
||||||
|
preparedProgram.artifact = artifact;
|
||||||
|
|
||||||
|
if (artifact.fragmentShaderSource.empty())
|
||||||
|
{
|
||||||
|
preparedProgram.error = "Cannot prepare an empty fragment shader.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BuildProgram(
|
||||||
|
artifact.fragmentShaderSource,
|
||||||
|
preparedProgram.program,
|
||||||
|
preparedProgram.vertexShader,
|
||||||
|
preparedProgram.fragmentShader,
|
||||||
|
preparedProgram.error))
|
||||||
|
{
|
||||||
|
preparedProgram.ReleaseGl();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
preparedProgram.succeeded = true;
|
||||||
|
AssignSamplerUniforms(preparedProgram.program);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height)
|
void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height)
|
||||||
{
|
{
|
||||||
if (mProgram == 0)
|
if (mProgram == 0)
|
||||||
@@ -175,7 +231,7 @@ bool RuntimeShaderRenderer::BuildProgram(const std::string& fragmentShaderSource
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program) const
|
void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program)
|
||||||
{
|
{
|
||||||
glUseProgram(program);
|
glUseProgram(program);
|
||||||
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
|
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
|
||||||
@@ -219,7 +275,7 @@ void RuntimeShaderRenderer::BindRuntimeTextures()
|
|||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeShaderRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error) const
|
bool RuntimeShaderRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error)
|
||||||
{
|
{
|
||||||
shader = glCreateShader(shaderType);
|
shader = glCreateShader(shaderType);
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "GLExtensions.h"
|
#include "GLExtensions.h"
|
||||||
|
#include "RuntimeShaderProgram.h"
|
||||||
#include "../runtime/RuntimeShaderArtifact.h"
|
#include "../runtime/RuntimeShaderArtifact.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -17,15 +18,22 @@ public:
|
|||||||
|
|
||||||
bool CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error);
|
bool CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error);
|
||||||
bool CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error);
|
bool CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error);
|
||||||
|
bool CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error);
|
||||||
bool HasProgram() const { return mProgram != 0; }
|
bool HasProgram() const { return mProgram != 0; }
|
||||||
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
|
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
|
||||||
void ShutdownGl();
|
void ShutdownGl();
|
||||||
|
|
||||||
|
static bool BuildPreparedProgram(
|
||||||
|
const std::string& layerId,
|
||||||
|
const std::string& sourceFingerprint,
|
||||||
|
const RuntimeShaderArtifact& artifact,
|
||||||
|
RuntimePreparedShaderProgram& preparedProgram);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool EnsureStaticGlResources(std::string& error);
|
bool EnsureStaticGlResources(std::string& error);
|
||||||
bool CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error) const;
|
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error);
|
||||||
bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error);
|
static bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error);
|
||||||
void AssignSamplerUniforms(GLuint program) const;
|
static void AssignSamplerUniforms(GLuint program);
|
||||||
void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height);
|
void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height);
|
||||||
void BindRuntimeTextures();
|
void BindRuntimeTextures();
|
||||||
void DestroyProgram();
|
void DestroyProgram();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Only the render thread may bind and use its primary OpenGL context.
|
|||||||
Allowed on the render thread:
|
Allowed on the render thread:
|
||||||
|
|
||||||
- GL resource creation and destruction for resources it owns
|
- GL resource creation and destruction for resources it owns
|
||||||
- GL shader/program commit from an already-prepared artifact
|
- GL shader/program swap from an already-prepared GL program
|
||||||
- drawing the next frame
|
- drawing the next frame
|
||||||
- async readback queueing and completion polling
|
- async readback queueing and completion polling
|
||||||
- publishing completed system-memory frames
|
- publishing completed system-memory frames
|
||||||
@@ -28,7 +28,7 @@ Not allowed on the render thread:
|
|||||||
- blocking console logging
|
- blocking console logging
|
||||||
- config file discovery or parsing
|
- config file discovery or parsing
|
||||||
|
|
||||||
If future GL preparation needs to happen off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop.
|
If GL preparation happens off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop.
|
||||||
|
|
||||||
## 2. Render Cadence Does Not Chase Buffers
|
## 2. Render Cadence Does Not Chase Buffers
|
||||||
|
|
||||||
@@ -63,11 +63,12 @@ If no completed frame is available, record the miss and keep the ownership bound
|
|||||||
Runtime shader work is split into two phases:
|
Runtime shader work is split into two phases:
|
||||||
|
|
||||||
1. CPU/build phase outside the render thread
|
1. CPU/build phase outside the render thread
|
||||||
2. GL commit phase on the render thread
|
2. shared-context GL preparation outside the render thread where practical
|
||||||
|
3. GL program swap on the render thread
|
||||||
|
|
||||||
The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data.
|
The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data.
|
||||||
|
|
||||||
The render thread receives a completed artifact and either commits it at a frame boundary or rejects it. A failed artifact must not disturb the current renderer.
|
The render thread receives completed render-layer artifacts, asks the shared-context prepare worker to compile/link changed GL programs, and only swaps in prepared programs at a frame boundary. A failed artifact or failed GL preparation must not disturb the current renderer.
|
||||||
|
|
||||||
The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly.
|
The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly.
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ info:
|
|||||||
The API is intended for local control tools and the bundled React UI. All mutating
|
The API is intended for local control tools and the bundled React UI. All mutating
|
||||||
endpoints return a small action result object.
|
endpoints return a small action result object.
|
||||||
|
|
||||||
WebSocket state streaming is planned for the control UI but is not currently served
|
RenderCadenceCompositor serves `/api/state` for snapshots and `/ws` for local
|
||||||
by RenderCadenceCompositor. Clients should poll `/api/state` until `/ws` is implemented.
|
WebSocket state updates consumed by the bundled control UI.
|
||||||
servers:
|
servers:
|
||||||
- url: http://127.0.0.1:8080
|
- url: http://127.0.0.1:8080
|
||||||
description: Default local control server
|
description: Default local control server
|
||||||
@@ -179,6 +179,24 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/RuntimeState"
|
$ref: "#/components/schemas/RuntimeState"
|
||||||
|
/ws:
|
||||||
|
get:
|
||||||
|
tags: [State]
|
||||||
|
summary: Stream runtime state over WebSocket
|
||||||
|
description: |
|
||||||
|
Upgrades to a WebSocket connection. The server sends JSON runtime-state
|
||||||
|
snapshots using the same shape as `GET /api/state` whenever the serialized
|
||||||
|
state changes.
|
||||||
|
operationId: streamRuntimeState
|
||||||
|
responses:
|
||||||
|
"101":
|
||||||
|
description: WebSocket protocol upgrade accepted.
|
||||||
|
"400":
|
||||||
|
description: The request was not a valid WebSocket upgrade.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
/api/layers/add:
|
/api/layers/add:
|
||||||
post:
|
post:
|
||||||
tags: [Layers]
|
tags: [Layers]
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ void TestStateEndpointUsesCallback()
|
|||||||
ExpectEquals(response.body, "{\"ok\":true}", "state endpoint returns callback JSON");
|
ExpectEquals(response.body, "{\"ok\":true}", "state endpoint returns callback JSON");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestWebSocketAcceptKey()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
const std::string acceptKey = HttpControlServer::WebSocketAcceptKey("dGhlIHNhbXBsZSBub25jZQ==");
|
||||||
|
ExpectEquals(acceptKey, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", "WebSocket accept key matches RFC example");
|
||||||
|
}
|
||||||
|
|
||||||
void TestRootServesUiIndex()
|
void TestRootServesUiIndex()
|
||||||
{
|
{
|
||||||
using namespace RenderCadenceCompositor;
|
using namespace RenderCadenceCompositor;
|
||||||
@@ -157,6 +165,7 @@ int main()
|
|||||||
{
|
{
|
||||||
TestParsesHttpRequest();
|
TestParsesHttpRequest();
|
||||||
TestStateEndpointUsesCallback();
|
TestStateEndpointUsesCallback();
|
||||||
|
TestWebSocketAcceptKey();
|
||||||
TestRootServesUiIndex();
|
TestRootServesUiIndex();
|
||||||
TestKnownPostEndpointReturnsActionError();
|
TestKnownPostEndpointReturnsActionError();
|
||||||
TestLayerPostEndpointsUseCallbacks();
|
TestLayerPostEndpointsUseCallbacks();
|
||||||
|
|||||||
Reference in New Issue
Block a user