diff --git a/.gitignore b/.gitignore index 65cf753..2c75e12 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ build.ninja *.log *.dmp *.tmp +/runtime/ diff --git a/CMakeLists.txt b/CMakeLists.txt index cc72ae1..e7abd9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,6 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/LoopThroughWithOpenGLCompositing") set(GPUDIRECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect") -set(SHADER_SLANG_SOURCE "${APP_DIR}/video_effect.slang") if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp") message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}") @@ -19,6 +18,8 @@ if(NOT EXISTS "${GPUDIRECT_DIR}/lib/x64/dvp.lib") endif() add_executable(LoopThroughWithOpenGLCompositing WIN32 + "${APP_DIR}/ControlServer.cpp" + "${APP_DIR}/ControlServer.h" "${APP_DIR}/DeckLinkAPI_i.c" "${APP_DIR}/GLExtensions.cpp" "${APP_DIR}/GLExtensions.h" @@ -28,12 +29,15 @@ add_executable(LoopThroughWithOpenGLCompositing WIN32 "${APP_DIR}/OpenGLComposite.cpp" "${APP_DIR}/OpenGLComposite.h" "${APP_DIR}/resource.h" + "${APP_DIR}/RuntimeHost.cpp" + "${APP_DIR}/RuntimeHost.h" + "${APP_DIR}/RuntimeJson.cpp" + "${APP_DIR}/RuntimeJson.h" "${APP_DIR}/stdafx.cpp" "${APP_DIR}/stdafx.h" "${APP_DIR}/targetver.h" "${APP_DIR}/VideoFrameTransfer.cpp" "${APP_DIR}/VideoFrameTransfer.h" - "${SHADER_SLANG_SOURCE}" ) target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE @@ -49,6 +53,9 @@ target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE dvp.lib opengl32 glu32 + Ws2_32 + Crypt32 + Advapi32 ) target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE @@ -56,11 +63,6 @@ target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE UNICODE ) -set_source_files_properties( - "${SHADER_SLANG_SOURCE}" - PROPERTIES HEADER_FILE_ONLY TRUE -) - if(MSVC) target_compile_options(LoopThroughWithOpenGLCompositing PRIVATE /W3) endif() @@ -72,6 +74,8 @@ add_custom_command(TARGET LoopThroughWithOpenGLCompositing POST_BUILD ) source_group(TREE "${APP_DIR}" FILES + "${APP_DIR}/ControlServer.cpp" + "${APP_DIR}/ControlServer.h" "${APP_DIR}/DeckLinkAPI_i.c" "${APP_DIR}/GLExtensions.cpp" "${APP_DIR}/GLExtensions.h" @@ -81,10 +85,13 @@ source_group(TREE "${APP_DIR}" FILES "${APP_DIR}/OpenGLComposite.cpp" "${APP_DIR}/OpenGLComposite.h" "${APP_DIR}/resource.h" + "${APP_DIR}/RuntimeHost.cpp" + "${APP_DIR}/RuntimeHost.h" + "${APP_DIR}/RuntimeJson.cpp" + "${APP_DIR}/RuntimeJson.h" "${APP_DIR}/stdafx.cpp" "${APP_DIR}/stdafx.h" "${APP_DIR}/targetver.h" "${APP_DIR}/VideoFrameTransfer.cpp" "${APP_DIR}/VideoFrameTransfer.h" - "${SHADER_SLANG_SOURCE}" ) diff --git a/SHADER_CONTRACT.md b/SHADER_CONTRACT.md new file mode 100644 index 0000000..fd071fb --- /dev/null +++ b/SHADER_CONTRACT.md @@ -0,0 +1,55 @@ +# Shader Package Contract + +Each shader package lives under `shaders//` and includes: + +- `shader.json` +- `shader.slang` + +## Manifest fields + +`shader.json` defines: + +- `id` +- `name` +- `description` +- `category` +- `entryPoint` +- `parameters` + +Supported parameter types: + +- `float` +- `vec2` +- `color` +- `bool` +- `enum` + +## Slang contract + +The runtime owns the fragment entry point, video decode, and final mix/bypass behavior. + +Your `shader.slang` file implements: + +```slang +float4 shadeVideo(ShaderContext context) +{ + return context.sourceColor; +} +``` + +Available built-ins through `ShaderContext`: + +- `uv` +- `sourceColor` +- `inputResolution` +- `outputResolution` +- `time` +- `frameCount` +- `mixAmount` +- `bypass` + +Manifest parameters are exposed to the shader as globals named by their `id`. + +Helper function: + +- `sampleVideo(float2 uv)` returns decoded RGBA video from the live DeckLink input. diff --git a/apps/LoopThroughWithOpenGLCompositing/ControlServer.cpp b/apps/LoopThroughWithOpenGLCompositing/ControlServer.cpp new file mode 100644 index 0000000..206a371 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/ControlServer.cpp @@ -0,0 +1,454 @@ +#include "stdafx.h" +#include "ControlServer.h" + +#include "RuntimeJson.h" + +#include +#include + +#include +#include +#include + +#pragma comment(lib, "Ws2_32.lib") +#pragma comment(lib, "Crypt32.lib") +#pragma comment(lib, "Advapi32.lib") + +namespace +{ +bool InitializeWinsock(std::string& error) +{ + WSADATA wsaData = {}; + int result = WSAStartup(MAKEWORD(2, 2), &wsaData); + if (result != 0) + { + error = "WSAStartup failed."; + return false; + } + return true; +} + +std::string ToLower(std::string text) +{ + std::transform(text.begin(), text.end(), text.begin(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + return text; +} +} + +ControlServer::ControlServer() + : mListenSocket(INVALID_SOCKET), mPort(0), mRunning(false) +{ +} + +ControlServer::~ControlServer() +{ + Stop(); +} + +bool ControlServer::Start(const std::filesystem::path& uiRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error) +{ + mUiRoot = uiRoot; + mCallbacks = callbacks; + + if (!InitializeWinsock(error)) + return false; + + mListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (mListenSocket == INVALID_SOCKET) + { + error = "Could not create listening socket."; + return false; + } + + u_long nonBlocking = 1; + ioctlsocket(mListenSocket, FIONBIO, &nonBlocking); + + sockaddr_in address = {}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + bool bound = false; + for (unsigned short offset = 0; offset < 20; ++offset) + { + address.sin_port = htons(static_cast(preferredPort + offset)); + if (bind(mListenSocket, reinterpret_cast(&address), sizeof(address)) == 0) + { + mPort = preferredPort + offset; + bound = true; + break; + } + } + + if (!bound) + { + error = "Could not bind the local control server to any port in the preferred range."; + closesocket(mListenSocket); + mListenSocket = INVALID_SOCKET; + return false; + } + + if (listen(mListenSocket, SOMAXCONN) != 0) + { + error = "Could not start listening on the local control server socket."; + closesocket(mListenSocket); + mListenSocket = INVALID_SOCKET; + return false; + } + + mRunning = true; + mThread = std::thread(&ControlServer::ServerLoop, this); + return true; +} + +void ControlServer::Stop() +{ + const bool wasActive = mRunning || mListenSocket != INVALID_SOCKET || mThread.joinable(); + mRunning = false; + + { + std::lock_guard lock(mMutex); + for (ClientConnection& client : mClients) + { + if (client.socket != INVALID_SOCKET) + { + closesocket(client.socket); + client.socket = INVALID_SOCKET; + } + } + mClients.clear(); + } + + if (mListenSocket != INVALID_SOCKET) + { + closesocket(mListenSocket); + mListenSocket = INVALID_SOCKET; + } + + if (mThread.joinable()) + mThread.join(); + + if (wasActive) + WSACleanup(); +} + +void ControlServer::BroadcastState() +{ + std::lock_guard lock(mMutex); + BroadcastStateLocked(); +} + +void ControlServer::ServerLoop() +{ + while (mRunning) + { + TryAcceptClient(); + Sleep(25); + } +} + +bool ControlServer::HandleHttpClient(SOCKET clientSocket) +{ + std::string request; + char buffer[8192]; + int received = recv(clientSocket, buffer, sizeof(buffer), 0); + if (received <= 0) + return false; + + request.assign(buffer, buffer + received); + return HandleHttpRequest(clientSocket, request); +} + +bool ControlServer::TryAcceptClient() +{ + sockaddr_in clientAddress = {}; + int addressSize = sizeof(clientAddress); + SOCKET clientSocket = accept(mListenSocket, reinterpret_cast(&clientAddress), &addressSize); + if (clientSocket == INVALID_SOCKET) + return false; + + bool handled = HandleHttpClient(clientSocket); + if (!handled) + closesocket(clientSocket); + return handled; +} + +bool ControlServer::SendHttpResponse(SOCKET clientSocket, const std::string& status, const std::string& contentType, const std::string& body) +{ + std::ostringstream response; + response << "HTTP/1.1 " << status << "\r\n"; + response << "Content-Type: " << contentType << "\r\n"; + response << "Content-Length: " << body.size() << "\r\n"; + response << "Connection: close\r\n\r\n"; + response << body; + + const std::string payload = response.str(); + return send(clientSocket, payload.c_str(), static_cast(payload.size()), 0) == static_cast(payload.size()); +} + +bool ControlServer::HandleHttpRequest(SOCKET clientSocket, const std::string& request) +{ + const std::string method = GetRequestMethod(request); + const std::string path = GetRequestPath(request); + + if (ToLower(GetHeaderValue(request, "Upgrade")) == "websocket") + return HandleWebSocketUpgrade(clientSocket, request); + + if (method == "GET") + { + if (path == "/" || path == "/index.html") + { + std::string contentType; + std::string body = LoadUiAsset("index.html", contentType); + SendHttpResponse(clientSocket, "200 OK", contentType, body); + closesocket(clientSocket); + return true; + } + if (path == "/app.js" || path == "/styles.css") + { + std::string contentType; + std::string body = LoadUiAsset(path.substr(1), contentType); + SendHttpResponse(clientSocket, "200 OK", contentType, body); + closesocket(clientSocket); + return true; + } + if (path == "/api/state") + { + SendHttpResponse(clientSocket, "200 OK", "application/json", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}"); + closesocket(clientSocket); + return true; + } + } + else if (method == "POST") + { + std::string body = GetRequestBody(request); + JsonValue root; + std::string parseError; + if (!ParseJson(body, root, parseError)) + { + SendHttpResponse(clientSocket, "400 Bad Request", "application/json", BuildJsonResponse(false, parseError)); + closesocket(clientSocket); + return true; + } + + bool success = false; + std::string actionError; + + if (path == "/api/select-shader") + { + const JsonValue* shaderId = root.find("shaderId"); + success = shaderId && mCallbacks.selectShader && mCallbacks.selectShader(shaderId->asString(), actionError); + } + else if (path == "/api/update-parameter") + { + const JsonValue* shaderId = root.find("shaderId"); + const JsonValue* parameterId = root.find("parameterId"); + const JsonValue* value = root.find("value"); + if (shaderId && parameterId && value && mCallbacks.updateParameter) + success = mCallbacks.updateParameter(shaderId->asString(), parameterId->asString(), SerializeJson(*value, false), actionError); + } + else if (path == "/api/set-bypass") + { + const JsonValue* bypass = root.find("bypass"); + if (bypass && mCallbacks.setBypass) + success = mCallbacks.setBypass(bypass->asBoolean(), actionError); + } + else if (path == "/api/set-mix") + { + const JsonValue* mixAmount = root.find("mixAmount"); + if (mixAmount && mCallbacks.setMixAmount) + success = mCallbacks.setMixAmount(mixAmount->asNumber(), actionError); + } + else if (path == "/api/reload") + { + if (mCallbacks.reloadShader) + success = mCallbacks.reloadShader(actionError); + } + + SendHttpResponse(clientSocket, success ? "200 OK" : "400 Bad Request", "application/json", BuildJsonResponse(success, actionError)); + closesocket(clientSocket); + if (success) + BroadcastState(); + return true; + } + + SendHttpResponse(clientSocket, "404 Not Found", "text/plain", "Not Found"); + closesocket(clientSocket); + return true; +} + +bool ControlServer::HandleWebSocketUpgrade(SOCKET clientSocket, const std::string& request) +{ + const std::string clientKey = GetHeaderValue(request, "Sec-WebSocket-Key"); + if (clientKey.empty()) + { + SendHttpResponse(clientSocket, "400 Bad Request", "text/plain", "Missing Sec-WebSocket-Key"); + closesocket(clientSocket); + return true; + } + + std::ostringstream response; + response << "HTTP/1.1 101 Switching Protocols\r\n"; + response << "Upgrade: websocket\r\n"; + response << "Connection: Upgrade\r\n"; + response << "Sec-WebSocket-Accept: " << ComputeWebSocketAcceptKey(clientKey) << "\r\n\r\n"; + + const std::string payload = response.str(); + send(clientSocket, payload.c_str(), static_cast(payload.size()), 0); + + { + std::lock_guard lock(mMutex); + ClientConnection client; + client.socket = clientSocket; + client.websocket = true; + mClients.push_back(client); + BroadcastStateLocked(); + } + return true; +} + +bool ControlServer::SendWebSocketText(SOCKET clientSocket, const std::string& payload) +{ + std::string frame; + frame.push_back(static_cast(0x81)); + if (payload.size() <= 125) + { + frame.push_back(static_cast(payload.size())); + } + else if (payload.size() <= 65535) + { + frame.push_back(126); + frame.push_back(static_cast((payload.size() >> 8) & 0xFF)); + frame.push_back(static_cast(payload.size() & 0xFF)); + } + else + { + frame.push_back(127); + for (int shift = 56; shift >= 0; shift -= 8) + frame.push_back(static_cast((payload.size() >> shift) & 0xFF)); + } + frame.append(payload); + + return send(clientSocket, frame.data(), static_cast(frame.size()), 0) == static_cast(frame.size()); +} + +void ControlServer::BroadcastStateLocked() +{ + const std::string stateMessage = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}"; + for (auto it = mClients.begin(); it != mClients.end();) + { + if (!SendWebSocketText(it->socket, stateMessage)) + { + closesocket(it->socket); + it = mClients.erase(it); + } + else + { + ++it; + } + } +} + +std::string ControlServer::LoadUiAsset(const std::string& relativePath, std::string& contentType) const +{ + const std::filesystem::path assetPath = mUiRoot / relativePath; + std::ifstream input(assetPath, std::ios::binary); + if (!input) + return "Missing UI asset

UI asset missing.

"; + + if (assetPath.extension() == ".js") + contentType = "text/javascript"; + else if (assetPath.extension() == ".css") + contentType = "text/css"; + else + contentType = "text/html"; + + std::ostringstream buffer; + buffer << input.rdbuf(); + return buffer.str(); +} + +std::string ControlServer::BuildJsonResponse(bool success, const std::string& error) const +{ + JsonValue response = JsonValue::MakeObject(); + response.set("ok", JsonValue(success)); + if (!error.empty()) + response.set("error", JsonValue(error)); + return SerializeJson(response, false); +} + +std::string ControlServer::Base64Encode(const unsigned char* data, DWORD dataLength) +{ + DWORD outputLength = 0; + CryptBinaryToStringA(data, dataLength, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, &outputLength); + std::string encoded(outputLength, '\0'); + CryptBinaryToStringA(data, dataLength, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, &encoded[0], &outputLength); + if (!encoded.empty() && encoded.back() == '\0') + encoded.pop_back(); + return encoded; +} + +std::string ControlServer::ComputeWebSocketAcceptKey(const std::string& clientKey) +{ + const std::string combined = clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + HCRYPTPROV provider = 0; + HCRYPTHASH hash = 0; + BYTE digest[20] = {}; + DWORD digestLength = sizeof(digest); + + CryptAcquireContext(&provider, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT); + CryptCreateHash(provider, CALG_SHA1, 0, 0, &hash); + CryptHashData(hash, reinterpret_cast(combined.data()), static_cast(combined.size()), 0); + CryptGetHashParam(hash, HP_HASHVAL, digest, &digestLength, 0); + + if (hash) + CryptDestroyHash(hash); + if (provider) + CryptReleaseContext(provider, 0); + + return Base64Encode(digest, digestLength); +} + +std::string ControlServer::GetHeaderValue(const std::string& request, const std::string& headerName) +{ + const std::string lowerRequest = ToLower(request); + const std::string lowerHeaderName = ToLower(headerName) + ":"; + const std::size_t start = lowerRequest.find(lowerHeaderName); + if (start == std::string::npos) + return std::string(); + + const std::size_t valueStart = start + lowerHeaderName.size(); + const std::size_t lineEnd = request.find("\r\n", valueStart); + if (lineEnd == std::string::npos) + return std::string(); + + std::string value = request.substr(valueStart, lineEnd - valueStart); + const std::size_t first = value.find_first_not_of(" \t"); + const std::size_t last = value.find_last_not_of(" \t"); + return first == std::string::npos ? std::string() : value.substr(first, last - first + 1); +} + +std::string ControlServer::GetRequestPath(const std::string& request) +{ + const std::size_t methodEnd = request.find(' '); + if (methodEnd == std::string::npos) + return "/"; + const std::size_t pathEnd = request.find(' ', methodEnd + 1); + if (pathEnd == std::string::npos) + return "/"; + return request.substr(methodEnd + 1, pathEnd - methodEnd - 1); +} + +std::string ControlServer::GetRequestMethod(const std::string& request) +{ + const std::size_t methodEnd = request.find(' '); + return methodEnd == std::string::npos ? std::string() : request.substr(0, methodEnd); +} + +std::string ControlServer::GetRequestBody(const std::string& request) +{ + const std::size_t separator = request.find("\r\n\r\n"); + if (separator == std::string::npos) + return std::string(); + return request.substr(separator + 4); +} diff --git a/apps/LoopThroughWithOpenGLCompositing/ControlServer.h b/apps/LoopThroughWithOpenGLCompositing/ControlServer.h new file mode 100644 index 0000000..2aacdfb --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/ControlServer.h @@ -0,0 +1,68 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +class ControlServer +{ +public: + struct Callbacks + { + std::function getStateJson; + std::function selectShader; + std::function updateParameter; + std::function setBypass; + std::function setMixAmount; + std::function reloadShader; + }; + + ControlServer(); + ~ControlServer(); + + bool Start(const std::filesystem::path& uiRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error); + void Stop(); + void BroadcastState(); + + unsigned short GetPort() const { return mPort; } + +private: + struct ClientConnection + { + SOCKET socket = INVALID_SOCKET; + bool websocket = false; + }; + + void ServerLoop(); + bool HandleHttpClient(SOCKET clientSocket); + bool TryAcceptClient(); + bool SendHttpResponse(SOCKET clientSocket, const std::string& status, const std::string& contentType, const std::string& body); + bool HandleHttpRequest(SOCKET clientSocket, const std::string& request); + bool HandleWebSocketUpgrade(SOCKET clientSocket, const std::string& request); + bool SendWebSocketText(SOCKET clientSocket, const std::string& payload); + void BroadcastStateLocked(); + std::string LoadUiAsset(const std::string& relativePath, std::string& contentType) const; + std::string BuildJsonResponse(bool success, const std::string& error = std::string()) const; + static std::string Base64Encode(const unsigned char* data, DWORD dataLength); + static std::string ComputeWebSocketAcceptKey(const std::string& clientKey); + static std::string GetHeaderValue(const std::string& request, const std::string& headerName); + static std::string GetRequestPath(const std::string& request); + static std::string GetRequestMethod(const std::string& request); + static std::string GetRequestBody(const std::string& request); + +private: + std::filesystem::path mUiRoot; + Callbacks mCallbacks; + SOCKET mListenSocket; + unsigned short mPort; + std::thread mThread; + std::atomic mRunning; + mutable std::mutex mMutex; + std::vector mClients; +}; diff --git a/apps/LoopThroughWithOpenGLCompositing/GLExtensions.cpp b/apps/LoopThroughWithOpenGLCompositing/GLExtensions.cpp index ff4c9e2..12e0c8d 100644 --- a/apps/LoopThroughWithOpenGLCompositing/GLExtensions.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/GLExtensions.cpp @@ -44,17 +44,17 @@ #include "GLExtensions.h" -PFNGLGENFRAMEBUFFERSEXTPROC glGenFramebuffersEXT; -PFNGLGENRENDERBUFFERSEXTPROC glGenRenderbuffersEXT; -PFNGLBINDRENDERBUFFEREXTPROC glBindRenderbufferEXT; -PFNGLRENDERBUFFERSTORAGEEXTPROC glRenderbufferStorageEXT; -PFNGLDELETEFRAMEBUFFERSEXTPROC glDeleteFramebuffersEXT; -PFNGLDELETERENDERBUFFERSEXTPROC glDeleteRenderbuffersEXT; -PFNGLBINDFRAMEBUFFEREXTPROC glBindFramebufferEXT; -PFNGLFRAMEBUFFERTEXTURE2DEXTPROC glFramebufferTexture2DEXT; -PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC glFramebufferRenderbufferEXT; -PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC glCheckFramebufferStatusEXT; -PFNGLBLITFRAMEBUFFEREXTPROC glBlitFramebufferEXT; +PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers; +PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers; +PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer; +PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage; +PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers; +PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers; +PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer; +PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D; +PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer; +PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus; +PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer; PFNGLFENCESYNCPROC glFenceSync; PFNGLCLIENTWAITSYNCPROC glClientWaitSync; PFNGLDELETESYNCPROC glDeleteSync; @@ -62,6 +62,12 @@ PFNGLGENBUFFERSPROC glGenBuffers; PFNGLDELETEBUFFERSPROC glDeleteBuffers; PFNGLBINDBUFFERPROC glBindBuffer; PFNGLBUFFERDATAPROC glBufferData; +PFNGLBUFFERSUBDATAPROC glBufferSubData; +PFNGLBINDBUFFERBASEPROC glBindBufferBase; +PFNGLACTIVETEXTUREPROC glActiveTexture; +PFNGLGENVERTEXARRAYSPROC glGenVertexArrays; +PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays; +PFNGLBINDVERTEXARRAYPROC glBindVertexArray; PFNGLCREATESHADERPROC glCreateShader; PFNGLDELETESHADERPROC glDeleteShader; PFNGLDELETEPROGRAMPROC glDeleteProgram; @@ -78,20 +84,44 @@ PFNGLUSEPROGRAMPROC glUseProgram; PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation; PFNGLUNIFORM1IPROC glUniform1i; PFNGLUNIFORM1FPROC glUniform1f; +PFNGLUNIFORM2FPROC glUniform2f; +PFNGLUNIFORM4FPROC glUniform4f; bool ResolveGLExtensions() { - glGenFramebuffersEXT = (PFNGLGENFRAMEBUFFERSEXTPROC) wglGetProcAddress("glGenFramebuffersEXT"); - glGenRenderbuffersEXT = (PFNGLGENRENDERBUFFERSEXTPROC) wglGetProcAddress("glGenRenderbuffersEXT"); - glBindRenderbufferEXT = (PFNGLBINDRENDERBUFFEREXTPROC) wglGetProcAddress("glBindRenderbufferEXT"); - glRenderbufferStorageEXT = (PFNGLRENDERBUFFERSTORAGEEXTPROC) wglGetProcAddress("glRenderbufferStorageEXT"); - glDeleteFramebuffersEXT = (PFNGLDELETEFRAMEBUFFERSEXTPROC) wglGetProcAddress("glDeleteFramebuffersEXT"); - glDeleteRenderbuffersEXT = (PFNGLDELETERENDERBUFFERSEXTPROC) wglGetProcAddress("glDeleteRenderbuffersEXT"); - glBindFramebufferEXT = (PFNGLBINDFRAMEBUFFEREXTPROC) wglGetProcAddress("glBindFramebufferEXT"); - glFramebufferTexture2DEXT = (PFNGLFRAMEBUFFERTEXTURE2DEXTPROC) wglGetProcAddress("glFramebufferTexture2DEXT"); - glFramebufferRenderbufferEXT = (PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC) wglGetProcAddress("glFramebufferRenderbufferEXT"); - glCheckFramebufferStatusEXT = (PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC) wglGetProcAddress("glCheckFramebufferStatusEXT"); - glBlitFramebufferEXT = (PFNGLBLITFRAMEBUFFEREXTPROC) wglGetProcAddress("glBlitFramebufferEXT"); + glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC) wglGetProcAddress("glGenFramebuffers"); + if (!glGenFramebuffers) + glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC) wglGetProcAddress("glGenFramebuffersEXT"); + glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC) wglGetProcAddress("glGenRenderbuffers"); + if (!glGenRenderbuffers) + glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC) wglGetProcAddress("glGenRenderbuffersEXT"); + glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC) wglGetProcAddress("glBindRenderbuffer"); + if (!glBindRenderbuffer) + glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC) wglGetProcAddress("glBindRenderbufferEXT"); + glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC) wglGetProcAddress("glRenderbufferStorage"); + if (!glRenderbufferStorage) + glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC) wglGetProcAddress("glRenderbufferStorageEXT"); + glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC) wglGetProcAddress("glDeleteFramebuffers"); + if (!glDeleteFramebuffers) + glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC) wglGetProcAddress("glDeleteFramebuffersEXT"); + glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC) wglGetProcAddress("glDeleteRenderbuffers"); + if (!glDeleteRenderbuffers) + glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC) wglGetProcAddress("glDeleteRenderbuffersEXT"); + glBindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC) wglGetProcAddress("glBindFramebuffer"); + if (!glBindFramebuffer) + glBindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC) wglGetProcAddress("glBindFramebufferEXT"); + glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC) wglGetProcAddress("glFramebufferTexture2D"); + if (!glFramebufferTexture2D) + glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC) wglGetProcAddress("glFramebufferTexture2DEXT"); + glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC) wglGetProcAddress("glFramebufferRenderbuffer"); + if (!glFramebufferRenderbuffer) + glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC) wglGetProcAddress("glFramebufferRenderbufferEXT"); + glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) wglGetProcAddress("glCheckFramebufferStatus"); + if (!glCheckFramebufferStatus) + glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) wglGetProcAddress("glCheckFramebufferStatusEXT"); + glBlitFramebuffer = (PFNGLBLITFRAMEBUFFERPROC) wglGetProcAddress("glBlitFramebuffer"); + if (!glBlitFramebuffer) + glBlitFramebuffer = (PFNGLBLITFRAMEBUFFERPROC) wglGetProcAddress("glBlitFramebufferEXT"); glFenceSync = (PFNGLFENCESYNCPROC) wglGetProcAddress("glFenceSync"); glClientWaitSync = (PFNGLCLIENTWAITSYNCPROC) wglGetProcAddress("glClientWaitSync"); glDeleteSync = (PFNGLDELETESYNCPROC) wglGetProcAddress("glDeleteSync"); @@ -99,6 +129,12 @@ bool ResolveGLExtensions() glDeleteBuffers = (PFNGLDELETEBUFFERSPROC) wglGetProcAddress("glDeleteBuffers"); glBindBuffer = (PFNGLBINDBUFFERPROC) wglGetProcAddress("glBindBuffer"); glBufferData = (PFNGLBUFFERDATAPROC) wglGetProcAddress("glBufferData"); + glBufferSubData = (PFNGLBUFFERSUBDATAPROC) wglGetProcAddress("glBufferSubData"); + glBindBufferBase = (PFNGLBINDBUFFERBASEPROC) wglGetProcAddress("glBindBufferBase"); + glActiveTexture = (PFNGLACTIVETEXTUREPROC) wglGetProcAddress("glActiveTexture"); + glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC) wglGetProcAddress("glGenVertexArrays"); + glDeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC) wglGetProcAddress("glDeleteVertexArrays"); + glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC) wglGetProcAddress("glBindVertexArray"); glCreateShader = (PFNGLCREATESHADERPROC) wglGetProcAddress("glCreateShader"); glDeleteShader = (PFNGLDELETESHADERPROC) wglGetProcAddress("glDeleteShader"); glDeleteProgram = (PFNGLDELETEPROGRAMPROC) wglGetProcAddress("glDeleteProgram"); @@ -115,18 +151,20 @@ bool ResolveGLExtensions() glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC) wglGetProcAddress("glGetUniformLocation"); glUniform1i = (PFNGLUNIFORM1IPROC) wglGetProcAddress("glUniform1i"); glUniform1f = (PFNGLUNIFORM1FPROC) wglGetProcAddress("glUniform1f"); + glUniform2f = (PFNGLUNIFORM2FPROC) wglGetProcAddress("glUniform2f"); + glUniform4f = (PFNGLUNIFORM4FPROC) wglGetProcAddress("glUniform4f"); - return glGenFramebuffersEXT - && glGenRenderbuffersEXT - && glBindRenderbufferEXT - && glRenderbufferStorageEXT - && glDeleteFramebuffersEXT - && glDeleteRenderbuffersEXT - && glBindFramebufferEXT - && glFramebufferTexture2DEXT - && glFramebufferRenderbufferEXT - && glCheckFramebufferStatusEXT - && glBlitFramebufferEXT + return glGenFramebuffers + && glGenRenderbuffers + && glBindRenderbuffer + && glRenderbufferStorage + && glDeleteFramebuffers + && glDeleteRenderbuffers + && glBindFramebuffer + && glFramebufferTexture2D + && glFramebufferRenderbuffer + && glCheckFramebufferStatus + && glBlitFramebuffer && glFenceSync && glClientWaitSync && glDeleteSync @@ -134,6 +172,12 @@ bool ResolveGLExtensions() && glDeleteBuffers && glBindBuffer && glBufferData + && glBufferSubData + && glBindBufferBase + && glActiveTexture + && glGenVertexArrays + && glDeleteVertexArrays + && glBindVertexArray && glCreateShader && glDeleteShader && glDeleteProgram @@ -150,5 +194,7 @@ bool ResolveGLExtensions() && glGetUniformLocation && glUniform1i && glUniform1f + && glUniform2f + && glUniform4f ; } diff --git a/apps/LoopThroughWithOpenGLCompositing/GLExtensions.h b/apps/LoopThroughWithOpenGLCompositing/GLExtensions.h index 3278bf3..48d3d0d 100644 --- a/apps/LoopThroughWithOpenGLCompositing/GLExtensions.h +++ b/apps/LoopThroughWithOpenGLCompositing/GLExtensions.h @@ -58,6 +58,8 @@ #define GL_STREAM_READ 0x88E1 #define GL_STREAM_COPY 0x88E2 #define GL_DYNAMIC_DRAW 0x88E8 +#define GL_UNIFORM_BUFFER 0x8A11 +#define GL_RGBA8 0x8058 #define GL_ARRAY_BUFFER 0x8892 #define GL_PIXEL_PACK_BUFFER 0x88EB #define GL_PIXEL_UNPACK_BUFFER 0x88EC @@ -71,6 +73,12 @@ #define GL_COLOR_ATTACHMENT0_EXT 0x8CE0 #define GL_READ_FRAMEBUFFER 0x8CA8 #define GL_DRAW_FRAMEBUFFER 0x8CA9 +#define GL_RENDERBUFFER 0x8D41 +#define GL_FRAMEBUFFER 0x8D40 +#define GL_FRAMEBUFFER_COMPLETE 0x8CD5 +#define GL_COLOR_ATTACHMENT0 0x8CE0 +#define GL_DEPTH_COMPONENT24 0x81A6 +#define GL_CLAMP_TO_EDGE 0x812F #define GL_DEPTH_ATTACHMENT_EXT 0x8D00 #define GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD 0x9160 #define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117 @@ -102,32 +110,40 @@ typedef void (APIENTRYP PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, co typedef void (APIENTRYP PFNGLUSEPROGRAMPROC) (GLuint program); typedef void (APIENTRYP PFNGLUNIFORM1FPROC) (GLint location, GLfloat v0); typedef void (APIENTRYP PFNGLUNIFORM1IPROC) (GLint location, GLint v0); +typedef void (APIENTRYP PFNGLUNIFORM2FPROC) (GLint location, GLfloat v0, GLfloat v1); +typedef void (APIENTRYP PFNGLUNIFORM4FPROC) (GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3); +typedef void (APIENTRYP PFNGLACTIVETEXTUREPROC) (GLenum texture); +typedef void (APIENTRYP PFNGLBINDBUFFERBASEPROC) (GLenum target, GLuint index, GLuint buffer); +typedef void (APIENTRYP PFNGLBUFFERSUBDATAPROC) (GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid* data); +typedef void (APIENTRYP PFNGLBINDVERTEXARRAYPROC) (GLuint array); +typedef void (APIENTRYP PFNGLDELETEVERTEXARRAYSPROC) (GLsizei n, const GLuint* arrays); +typedef void (APIENTRYP PFNGLGENVERTEXARRAYSPROC) (GLsizei n, GLuint* arrays); typedef GLsync (APIENTRYP PFNGLFENCESYNCPROC) (GLenum condition, GLbitfield flags); typedef void (APIENTRYP PFNGLDELETESYNCPROC) (GLsync sync); typedef GLenum (APIENTRYP PFNGLCLIENTWAITSYNCPROC) (GLsync sync, GLbitfield flags, GLuint64 timeout); -typedef void (APIENTRYP PFNGLBINDRENDERBUFFEREXTPROC) (GLenum target, GLuint renderbuffer); -typedef void (APIENTRYP PFNGLDELETERENDERBUFFERSEXTPROC) (GLsizei n, const GLuint *renderbuffers); -typedef void (APIENTRYP PFNGLGENRENDERBUFFERSEXTPROC) (GLsizei n, GLuint *renderbuffers); -typedef void (APIENTRYP PFNGLRENDERBUFFERSTORAGEEXTPROC) (GLenum target, GLenum internalformat, GLsizei width, GLsizei height); -typedef void (APIENTRYP PFNGLBINDFRAMEBUFFEREXTPROC) (GLenum target, GLuint framebuffer); -typedef void (APIENTRYP PFNGLDELETEFRAMEBUFFERSEXTPROC) (GLsizei n, const GLuint *framebuffers); -typedef void (APIENTRYP PFNGLGENFRAMEBUFFERSEXTPROC) (GLsizei n, GLuint *framebuffers); -typedef GLenum (APIENTRYP PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC) (GLenum target); -typedef void (APIENTRYP PFNGLFRAMEBUFFERTEXTURE2DEXTPROC) (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level); -typedef void (APIENTRYP PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC) (GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer); -typedef void (APIENTRYP PFNGLBLITFRAMEBUFFEREXTPROC) (GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter); +typedef void (APIENTRYP PFNGLBINDRENDERBUFFERPROC) (GLenum target, GLuint renderbuffer); +typedef void (APIENTRYP PFNGLDELETERENDERBUFFERSPROC) (GLsizei n, const GLuint *renderbuffers); +typedef void (APIENTRYP PFNGLGENRENDERBUFFERSPROC) (GLsizei n, GLuint *renderbuffers); +typedef void (APIENTRYP PFNGLRENDERBUFFERSTORAGEPROC) (GLenum target, GLenum internalformat, GLsizei width, GLsizei height); +typedef void (APIENTRYP PFNGLBINDFRAMEBUFFERPROC) (GLenum target, GLuint framebuffer); +typedef void (APIENTRYP PFNGLDELETEFRAMEBUFFERSPROC) (GLsizei n, const GLuint *framebuffers); +typedef void (APIENTRYP PFNGLGENFRAMEBUFFERSPROC) (GLsizei n, GLuint *framebuffers); +typedef GLenum (APIENTRYP PFNGLCHECKFRAMEBUFFERSTATUSPROC) (GLenum target); +typedef void (APIENTRYP PFNGLFRAMEBUFFERTEXTURE2DPROC) (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level); +typedef void (APIENTRYP PFNGLFRAMEBUFFERRENDERBUFFERPROC) (GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer); +typedef void (APIENTRYP PFNGLBLITFRAMEBUFFERPROC) (GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter); -extern PFNGLGENFRAMEBUFFERSEXTPROC glGenFramebuffersEXT; -extern PFNGLGENRENDERBUFFERSEXTPROC glGenRenderbuffersEXT; -extern PFNGLBINDRENDERBUFFEREXTPROC glBindRenderbufferEXT; -extern PFNGLRENDERBUFFERSTORAGEEXTPROC glRenderbufferStorageEXT; -extern PFNGLDELETEFRAMEBUFFERSEXTPROC glDeleteFramebuffersEXT; -extern PFNGLDELETERENDERBUFFERSEXTPROC glDeleteRenderbuffersEXT; -extern PFNGLBINDFRAMEBUFFEREXTPROC glBindFramebufferEXT; -extern PFNGLFRAMEBUFFERTEXTURE2DEXTPROC glFramebufferTexture2DEXT; -extern PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC glFramebufferRenderbufferEXT; -extern PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC glCheckFramebufferStatusEXT; -extern PFNGLBLITFRAMEBUFFEREXTPROC glBlitFramebufferEXT; +extern PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers; +extern PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers; +extern PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer; +extern PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage; +extern PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers; +extern PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers; +extern PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer; +extern PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D; +extern PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer; +extern PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus; +extern PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer; extern PFNGLFENCESYNCPROC glFenceSync; extern PFNGLCLIENTWAITSYNCPROC glClientWaitSync; extern PFNGLDELETESYNCPROC glDeleteSync; @@ -135,6 +151,12 @@ extern PFNGLGENBUFFERSPROC glGenBuffers; extern PFNGLDELETEBUFFERSPROC glDeleteBuffers; extern PFNGLBINDBUFFERPROC glBindBuffer; extern PFNGLBUFFERDATAPROC glBufferData; +extern PFNGLBUFFERSUBDATAPROC glBufferSubData; +extern PFNGLBINDBUFFERBASEPROC glBindBufferBase; +extern PFNGLACTIVETEXTUREPROC glActiveTexture; +extern PFNGLGENVERTEXARRAYSPROC glGenVertexArrays; +extern PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays; +extern PFNGLBINDVERTEXARRAYPROC glBindVertexArray; extern PFNGLCREATESHADERPROC glCreateShader; extern PFNGLDELETESHADERPROC glDeleteShader; extern PFNGLDELETEPROGRAMPROC glDeleteProgram; @@ -151,6 +173,8 @@ extern PFNGLUSEPROGRAMPROC glUseProgram; extern PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation; extern PFNGLUNIFORM1IPROC glUniform1i; extern PFNGLUNIFORM1FPROC glUniform1f; +extern PFNGLUNIFORM2FPROC glUniform2f; +extern PFNGLUNIFORM4FPROC glUniform4f; bool ResolveGLExtensions(); diff --git a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.cpp b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.cpp index c45ed97..b7ce753 100644 --- a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.cpp @@ -46,11 +46,41 @@ #include "resource.h" #include "OpenGLComposite.h" +#ifndef WGL_CONTEXT_MAJOR_VERSION_ARB +#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091 +#endif +#ifndef WGL_CONTEXT_MINOR_VERSION_ARB +#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092 +#endif +#ifndef WGL_CONTEXT_PROFILE_MASK_ARB +#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126 +#endif +#ifndef WGL_CONTEXT_CORE_PROFILE_BIT_ARB +#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001 +#endif #define MAX_LOADSTRING 100 // Declaration for Window procedure LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); +typedef HGLRC (WINAPI* PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hdc, HGLRC hShareContext, const int* attribList); + +void ShowUnhandledExceptionMessage(const char* prefix) +{ + try + { + throw; + } + catch (const std::exception& exception) + { + std::string message = std::string(prefix) + "\n\n" + exception.what(); + MessageBoxA(NULL, message.c_str(), "Unhandled exception", MB_OK | MB_ICONERROR); + } + catch (...) + { + MessageBoxA(NULL, prefix, "Unhandled exception", MB_OK | MB_ICONERROR); + } +} // Select the pixel format for a given device context void SetDCPixelFormat(HDC hDC) @@ -82,6 +112,38 @@ void SetDCPixelFormat(HDC hDC) SetPixelFormat(hDC, nPixelFormat, &pfd); } +HGLRC CreateModernOpenGLContext(HDC hDC) +{ + PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB = + reinterpret_cast(wglGetProcAddress("wglCreateContextAttribsARB")); + if (!wglCreateContextAttribsARB) + return NULL; + + const int versionCandidates[][2] = + { + { 4, 5 }, + { 4, 3 }, + { 3, 3 } + }; + + for (const auto& version : versionCandidates) + { + const int attribs[] = + { + WGL_CONTEXT_MAJOR_VERSION_ARB, version[0], + WGL_CONTEXT_MINOR_VERSION_ARB, version[1], + WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB, + 0 + }; + + HGLRC modernContext = wglCreateContextAttribsARB(hDC, 0, attribs); + if (modernContext != NULL) + return modernContext; + } + + return NULL; +} + int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { MSG msg; // Windows message structure @@ -145,6 +207,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { // Window creation, setup for OpenGL context case WM_CREATE: + { + try + { // Store the device context hDC = GetDC(hWnd); @@ -155,6 +220,19 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) hRC = wglCreateContext(hDC); wglMakeCurrent(hDC, hRC); + HGLRC modernRC = CreateModernOpenGLContext(hDC); + if (modernRC == NULL) + { + MessageBox(NULL, _T("This application requires an OpenGL 3.3+ core profile context."), _T("OpenGL initialization Error."), MB_OK); + PostMessage(hWnd, WM_CLOSE, 0, 0); + break; + } + + wglMakeCurrent(NULL, NULL); + wglDeleteContext(hRC); + hRC = modernRC; + wglMakeCurrent(hDC, hRC); + // Initialize COM HRESULT result; result = CoInitialize(NULL); @@ -180,12 +258,27 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) pOpenGLComposite = NULL; PostMessage(hWnd, WM_CLOSE, 0, 0); break; + } + catch (...) + { + ShowUnhandledExceptionMessage("Startup failed while creating the OpenGL/DeckLink runtime."); + PostMessage(hWnd, WM_CLOSE, 0, 0); + break; + } + } case WM_DESTROY: - if (pOpenGLComposite) + try { - pOpenGLComposite->Stop(); - delete pOpenGLComposite; + if (pOpenGLComposite) + { + pOpenGLComposite->Stop(); + delete pOpenGLComposite; + } + } + catch (...) + { + ShowUnhandledExceptionMessage("Shutdown failed while tearing down the OpenGL/DeckLink runtime."); } // Deselect the current rendering context and delete it @@ -197,24 +290,46 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) break; case WM_SIZE: - if (pOpenGLComposite) - pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam)); + try + { + if (pOpenGLComposite) + pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam)); + } + catch (...) + { + ShowUnhandledExceptionMessage("Resize failed inside the OpenGL runtime."); + } break; case WM_PAINT: - wglMakeCurrent(hDC, hRC); + try + { + wglMakeCurrent(hDC, hRC); - if (pOpenGLComposite) - pOpenGLComposite->paintGL(); + if (pOpenGLComposite) + pOpenGLComposite->paintGL(); - wglMakeCurrent( NULL, NULL ); + wglMakeCurrent( NULL, NULL ); + } + catch (...) + { + wglMakeCurrent( NULL, NULL ); + ShowUnhandledExceptionMessage("Paint failed inside the OpenGL runtime."); + } break; case WM_KEYDOWN: - if (pOpenGLComposite && (wParam == 'R' || wParam == 'r')) + try { - pOpenGLComposite->ReloadShader(); - InvalidateRect(hWnd, NULL, FALSE); + if (pOpenGLComposite && (wParam == 'R' || wParam == 'r')) + { + pOpenGLComposite->ReloadShader(); + InvalidateRect(hWnd, NULL, FALSE); + } + } + catch (...) + { + ShowUnhandledExceptionMessage("Shader reload failed inside the OpenGL runtime."); } break; diff --git a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp index b3fbed4..155a705 100644 --- a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp @@ -38,12 +38,13 @@ ** -LICENSE-END- */ +#include "ControlServer.h" #include "OpenGLComposite.h" #include "GLExtensions.h" -#include -#include -#include +#include +#include +#include #include #include @@ -53,12 +54,11 @@ DEFINE_GUID(IID_PinnedMemoryAllocator, namespace { -const char* kSlangShaderRelativePath = "apps/LoopThroughWithOpenGLCompositing/video_effect.slang"; -const char* kRuntimeShaderCacheDirectory = "shader_cache"; -const char* kRuntimeRawShaderFilename = "video_effect.raw.frag"; -const char* kRuntimePatchedShaderFilename = "video_effect.frag"; +constexpr GLuint kVideoTextureUnit = 1; +constexpr GLuint kGlobalParamsBindingPoint = 0; +const char* kDisplayModeName = "1080p59.94"; const char* kVertexShaderSource = - "#version 130\n" + "#version 430 core\n" "out vec2 vTexCoord;\n" "void main()\n" "{\n" @@ -68,34 +68,6 @@ const char* kVertexShaderSource = " vTexCoord = texCoords[gl_VertexID];\n" "}\n"; -std::string GetExecutableDirectory() -{ - char modulePath[MAX_PATH] = {}; - DWORD pathLength = GetModuleFileNameA(NULL, modulePath, MAX_PATH); - if (pathLength == 0 || pathLength == MAX_PATH) - return std::string(); - - std::string path(modulePath, pathLength); - std::string::size_type slashIndex = path.find_last_of("\\/"); - if (slashIndex == std::string::npos) - return std::string(); - - return path.substr(0, slashIndex); -} - -bool ReplaceAll(std::string& text, const std::string& from, const std::string& to) -{ - bool replaced = false; - std::string::size_type startPos = 0; - while ((startPos = text.find(from, startPos)) != std::string::npos) - { - text.replace(startPos, from.length(), to); - startPos += to.length(); - replaced = true; - } - return replaced; -} - void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage) { if (!errorMessage || errorMessageSize <= 0) @@ -104,178 +76,47 @@ void CopyErrorMessage(const std::string& message, int errorMessageSize, char* er strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE); } -bool LoadTextFile(const std::string& path, std::string& contents, std::string& error) +std::size_t AlignStd140(std::size_t offset, std::size_t alignment) { - std::ifstream input(path.c_str(), std::ios::binary); - if (!input) - { - error = "Could not open fragment shader file: " + path; - return false; - } - - std::ostringstream buffer; - buffer << input.rdbuf(); - contents = buffer.str(); - - if (contents.empty()) - { - error = "Fragment shader file is empty: " + path; - return false; - } - - return true; + const std::size_t mask = alignment - 1; + return (offset + mask) & ~mask; } -std::filesystem::path FindRepoRoot() +template +void AppendStd140Value(std::vector& buffer, std::size_t alignment, const TValue& value) { - std::vector rootsToTry; - - char currentDirBuffer[MAX_PATH] = {}; - if (GetCurrentDirectoryA(MAX_PATH, currentDirBuffer) > 0) - rootsToTry.push_back(std::filesystem::path(currentDirBuffer)); - - std::string executableDirectory = GetExecutableDirectory(); - if (!executableDirectory.empty()) - rootsToTry.push_back(std::filesystem::path(executableDirectory)); - - for (const std::filesystem::path& startPath : rootsToTry) - { - std::filesystem::path candidate = startPath; - for (int depth = 0; depth < 8 && !candidate.empty(); ++depth) - { - if (std::filesystem::exists(candidate / kSlangShaderRelativePath)) - return candidate; - - candidate = candidate.parent_path(); - } - } - - return std::filesystem::path(); + const std::size_t offset = AlignStd140(buffer.size(), alignment); + if (buffer.size() < offset + sizeof(TValue)) + buffer.resize(offset + sizeof(TValue), 0); + std::memcpy(buffer.data() + offset, &value, sizeof(TValue)); } -bool FindSlangCompiler(const std::filesystem::path& repoRoot, std::filesystem::path& slangCompilerPath, std::string& error) +void AppendStd140Float(std::vector& buffer, float value) { - std::filesystem::path thirdPartyPath = repoRoot / "3rdParty"; - if (!std::filesystem::exists(thirdPartyPath)) - { - error = "Could not locate the 3rdParty directory from the application runtime path."; - return false; - } - - for (const auto& entry : std::filesystem::directory_iterator(thirdPartyPath)) - { - if (!entry.is_directory()) - continue; - - std::filesystem::path candidate = entry.path() / "bin" / "slangc.exe"; - if (std::filesystem::exists(candidate)) - { - slangCompilerPath = candidate; - return true; - } - } - - error = "Could not find slangc.exe under 3rdParty."; - return false; + AppendStd140Value(buffer, 4, value); } -bool RunProcessAndWait(const std::string& commandLine, std::string& error) +void AppendStd140Int(std::vector& buffer, int value) { - STARTUPINFOA startupInfo = {}; - PROCESS_INFORMATION processInfo = {}; - startupInfo.cb = sizeof(startupInfo); - - std::vector mutableCommandLine(commandLine.begin(), commandLine.end()); - mutableCommandLine.push_back('\0'); - - if (!CreateProcessA(NULL, mutableCommandLine.data(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &startupInfo, &processInfo)) - { - error = "Failed to start slangc.exe."; - return false; - } - - WaitForSingleObject(processInfo.hProcess, INFINITE); - - DWORD exitCode = 0; - GetExitCodeProcess(processInfo.hProcess, &exitCode); - - CloseHandle(processInfo.hThread); - CloseHandle(processInfo.hProcess); - - if (exitCode != 0) - { - error = "slangc.exe returned a non-zero exit code while compiling the runtime shader."; - return false; - } - - return true; + AppendStd140Value(buffer, 4, value); } -bool PatchGeneratedSlangGLSL(std::string& shaderText, std::string& error) +void AppendStd140Vec2(std::vector& buffer, float x, float y) { - bool replacedVersion = ReplaceAll(shaderText, "#version 450", "#version 130"); - ReplaceAll(shaderText, "#extension GL_EXT_samplerless_texture_functions : require\n", ""); - ReplaceAll(shaderText, "layout(row_major) uniform;\n", ""); - ReplaceAll(shaderText, "layout(row_major) buffer;\n", ""); - ReplaceAll(shaderText, "layout(binding = 0)\nuniform texture2D UYVYtex_0;", "uniform sampler2D UYVYtex;"); - ReplaceAll(shaderText, "layout(location = 0)\nout vec4 entryPointParam_fragmentMain_0;\n", ""); - ReplaceAll(shaderText, "layout(location = 0)\nin vec2 input_texCoord_0;\n", "in vec2 vTexCoord;\n"); - ReplaceAll(shaderText, "UYVYtex_0", "UYVYtex"); - ReplaceAll(shaderText, "input_texCoord_0", "vTexCoord"); - ReplaceAll(shaderText, "entryPointParam_fragmentMain_0 =", "gl_FragColor ="); - - if (!replacedVersion) - { - error = "Generated Slang GLSL did not contain the expected version header."; - return false; - } - - return true; + const std::size_t offset = AlignStd140(buffer.size(), 8); + if (buffer.size() < offset + sizeof(float) * 2) + buffer.resize(offset + sizeof(float) * 2, 0); + float values[2] = { x, y }; + std::memcpy(buffer.data() + offset, values, sizeof(values)); } -bool BuildFragmentShaderSourceFromSlang(std::string& shaderSource, std::string& error) +void AppendStd140Vec4(std::vector& buffer, float x, float y, float z, float w) { - std::filesystem::path repoRoot = FindRepoRoot(); - if (repoRoot.empty()) - { - error = "Could not locate the repository root to load video_effect.slang."; - return false; - } - - std::filesystem::path slangSourcePath = repoRoot / kSlangShaderRelativePath; - if (!std::filesystem::exists(slangSourcePath)) - { - error = "Could not find video_effect.slang."; - return false; - } - - std::filesystem::path slangCompilerPath; - if (!FindSlangCompiler(repoRoot, slangCompilerPath, error)) - return false; - - std::filesystem::path shaderCachePath = std::filesystem::path(GetExecutableDirectory()) / kRuntimeShaderCacheDirectory; - std::filesystem::create_directories(shaderCachePath); - - std::filesystem::path rawShaderPath = shaderCachePath / kRuntimeRawShaderFilename; - std::filesystem::path patchedShaderPath = shaderCachePath / kRuntimePatchedShaderFilename; - - std::string commandLine = "\"" + slangCompilerPath.string() + "\" \"" + slangSourcePath.string() - + "\" -target glsl -profile glsl_430 -entry fragmentMain -stage fragment -o \"" + rawShaderPath.string() + "\""; - - if (!RunProcessAndWait(commandLine, error)) - return false; - - if (!LoadTextFile(rawShaderPath.string(), shaderSource, error)) - return false; - - if (!PatchGeneratedSlangGLSL(shaderSource, error)) - return false; - - std::ofstream patchedShaderOutput(patchedShaderPath.string().c_str(), std::ios::binary); - if (patchedShaderOutput) - patchedShaderOutput << shaderSource; - - return true; + const std::size_t offset = AlignStd140(buffer.size(), 16); + if (buffer.size() < offset + sizeof(float) * 4) + buffer.resize(offset + sizeof(float) * 4, 0); + float values[4] = { x, y, z, w }; + std::memcpy(buffer.data() + offset, values, sizeof(values)); } } @@ -289,14 +130,16 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : mFastTransferExtensionAvailable(false), mCaptureTexture(0), mFBOTexture(0), + mFullscreenVAO(0), + mGlobalParamsUBO(0), mProgram(0), mVertexShader(0), mFragmentShader(0), - mUYVYtexUniform(-1), - mRotateAngle(0.0f), - mRotateAngleRate(0.0f) + mGlobalParamsUBOSize(0) { InitializeCriticalSection(&pMutex); + mRuntimeHost = std::make_unique(); + mControlServer = std::make_unique(); } OpenGLComposite::~OpenGLComposite() @@ -348,7 +191,26 @@ OpenGLComposite::~OpenGLComposite() mPlayoutAllocator = NULL; } + if (mFullscreenVAO != 0) + glDeleteVertexArrays(1, &mFullscreenVAO); + if (mGlobalParamsUBO != 0) + glDeleteBuffers(1, &mGlobalParamsUBO); + if (mIdFrameBuf != 0) + glDeleteFramebuffers(1, &mIdFrameBuf); + if (mIdColorBuf != 0) + glDeleteRenderbuffers(1, &mIdColorBuf); + if (mIdDepthBuf != 0) + glDeleteRenderbuffers(1, &mIdDepthBuf); + if (mCaptureTexture != 0) + glDeleteTextures(1, &mCaptureTexture); + if (mFBOTexture != 0) + glDeleteTextures(1, &mFBOTexture); + if (mUnpinnedTextureBuffer != 0) + glDeleteBuffers(1, &mUnpinnedTextureBuffer); + destroyShaderProgram(); + if (mControlServer) + mControlServer->Stop(); DeleteCriticalSection(&pMutex); } @@ -564,10 +426,10 @@ void OpenGLComposite::paintGL() // we already have the rendered frame to be played out sitting in the GPU in the mIdFrameBuf frame buffer. // Simply copy the off-screen frame buffer to on-screen frame buffer, scaling to the viewing window size. - glBindFramebufferEXT(GL_READ_FRAMEBUFFER, mIdFrameBuf); - glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0); + glBindFramebuffer(GL_READ_FRAMEBUFFER, mIdFrameBuf); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glViewport(0, 0, mViewWidth, mViewHeight); - glBlitFramebufferEXT(0, 0, mFrameWidth, mFrameHeight, 0, 0, mViewWidth, mViewHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); + glBlitFramebuffer(0, 0, mFrameWidth, mFrameHeight, 0, 0, mViewWidth, mViewHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); SwapBuffers(hGLDC); ValidateRect(hGLWnd, NULL); @@ -595,7 +457,38 @@ bool OpenGLComposite::InitOpenGLState() if (! ResolveGLExtensions()) return false; - // Prepare the runtime shader program generated from the Slang source file. + std::string runtimeError; + if (!mRuntimeHost->Initialize(runtimeError)) + { + MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK); + return false; + } + + ControlServer::Callbacks callbacks; + callbacks.getStateJson = [this]() { return GetRuntimeStateJson(); }; + callbacks.selectShader = [this](const std::string& shaderId, std::string& error) { return SelectShader(shaderId, error); }; + callbacks.updateParameter = [this](const std::string& shaderId, const std::string& parameterId, const std::string& valueJson, std::string& error) { + return UpdateParameterJson(shaderId, parameterId, valueJson, error); + }; + callbacks.setBypass = [this](bool bypassEnabled, std::string& error) { return SetBypassEnabled(bypassEnabled, error); }; + callbacks.setMixAmount = [this](double mixAmount, std::string& error) { return SetMixAmount(mixAmount, error); }; + callbacks.reloadShader = [this](std::string& error) { + if (!ReloadShader()) + { + error = "Shader reload failed. See native app status for details."; + return false; + } + return true; + }; + + if (!mControlServer->Start(mRuntimeHost->GetUiRoot(), mRuntimeHost->GetServerPort(), callbacks, runtimeError)) + { + MessageBoxA(NULL, runtimeError.c_str(), "Local control server failed to start", MB_OK); + return false; + } + mRuntimeHost->SetServerPort(mControlServer->GetPort()); + + // Prepare the runtime shader program generated from the active shader package. char compilerErrorMessage[1024]; if (! compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage)) { @@ -612,54 +505,68 @@ bool OpenGLComposite::InitOpenGLState() } // Setup the texture which will hold the captured video frame pixels - glEnable(GL_TEXTURE_2D); glGenTextures(1, &mCaptureTexture); glBindTexture(GL_TEXTURE_2D, mCaptureTexture); // Parameters to control how texels are sampled from the texture glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Create texture with empty data, we will update it using glTexSubImage2D each frame. // The captured video is YCbCr 4:2:2 packed into a UYVY macropixel. OpenGL has no YCbCr format // so treat it as RGBA 4:4:4:4 by halving the width and using GL_RGBA internal format. - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, mFrameWidth/2, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth/2, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); glBindTexture(GL_TEXTURE_2D, 0); // Create Frame Buffer Object (FBO) to perform off-screen rendering of scene. // This allows the render to be done on a framebuffer with width and height exactly matching the video format. - glGenFramebuffersEXT(1, &mIdFrameBuf); - glGenRenderbuffersEXT(1, &mIdColorBuf); - glGenRenderbuffersEXT(1, &mIdDepthBuf); + glGenFramebuffers(1, &mIdFrameBuf); + glGenRenderbuffers(1, &mIdColorBuf); + glGenRenderbuffers(1, &mIdDepthBuf); + glGenVertexArrays(1, &mFullscreenVAO); + glGenBuffers(1, &mGlobalParamsUBO); - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, mIdFrameBuf); + glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf); // Texture for FBO glGenTextures(1, &mFBOTexture); glBindTexture(GL_TEXTURE_2D, mFBOTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, mFrameWidth, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + 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, mFrameWidth, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); // Attach a depth buffer - glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, mIdDepthBuf); - glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, mFrameWidth, mFrameHeight); + glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mFrameWidth, mFrameHeight); - glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, mIdDepthBuf); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf); // Attach the texture which stores the playback image - glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, mFBOTexture, 0); - glBindTexture(GL_TEXTURE_2D, 0); - glDisable(GL_TEXTURE_2D); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mFBOTexture, 0); - GLenum glStatus = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); - if (glStatus != GL_FRAMEBUFFER_COMPLETE_EXT) + GLenum glStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (glStatus != GL_FRAMEBUFFER_COMPLETE) { MessageBox(NULL, _T("Cannot initialize framebuffer."), _T("OpenGL initialization error."), MB_OK); return false; } + glBindTexture(GL_TEXTURE_2D, 0); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindVertexArray(mFullscreenVAO); + glBindVertexArray(0); + glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsUBO); + glBufferData(GL_UNIFORM_BUFFER, 1024, NULL, GL_DYNAMIC_DRAW); + glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsUBO); + glBindBuffer(GL_UNIFORM_BUFFER, 0); + + broadcastRuntimeState(); return true; } @@ -669,6 +576,9 @@ bool OpenGLComposite::InitOpenGLState() void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource) { mHasNoInputSource = hasNoInputSource; + if (mRuntimeHost) + mRuntimeHost->SetSignalStatus(!hasNoInputSource, mFrameWidth, mFrameHeight, kDisplayModeName); + if (mHasNoInputSource) return; // don't transfer texture when there's no input @@ -700,8 +610,6 @@ void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bo } else { - glEnable(GL_TEXTURE_2D); - // Use a straightforward texture buffer glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mUnpinnedTextureBuffer); glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, videoPixels, GL_DYNAMIC_DRAW); @@ -712,7 +620,6 @@ void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bo glBindTexture(GL_TEXTURE_2D, 0); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - glDisable(GL_TEXTURE_2D); } wglMakeCurrent( NULL, NULL ); @@ -738,8 +645,10 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, wglMakeCurrent( hGLDC, hGLRC ); // Draw the effect output to the off-screen framebuffer. - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, mIdFrameBuf); + glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf); renderEffect(); + if (mRuntimeHost) + mRuntimeHost->AdvanceFrame(); IDeckLinkVideoBuffer* outputVideoFrameBuffer; if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK) @@ -838,6 +747,9 @@ bool OpenGLComposite::Start() bool OpenGLComposite::Stop() { + if (mControlServer) + mControlServer->Stop(); + mDLInput->StopStreams(); mDLInput->DisableVideoInput(); @@ -855,12 +767,24 @@ bool OpenGLComposite::ReloadShader() wglMakeCurrent(hGLDC, hGLRC); bool success = compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage); + if (mRuntimeHost) + mRuntimeHost->ClearReloadRequest(); wglMakeCurrent(NULL, NULL); LeaveCriticalSection(&pMutex); if (!success) + { + if (mRuntimeHost) + mRuntimeHost->SetCompileStatus(false, compilerErrorMessage); MessageBoxA(NULL, compilerErrorMessage, "Slang shader reload failed", MB_OK); + } + else + { + if (mRuntimeHost) + mRuntimeHost->SetCompileStatus(true, "Shader compiled successfully."); + broadcastRuntimeState(); + } return success; } @@ -884,12 +808,12 @@ void OpenGLComposite::destroyShaderProgram() glDeleteShader(mVertexShader); mVertexShader = 0; } - - mUYVYtexUniform = -1; } void OpenGLComposite::renderEffect() { + PollRuntimeChanges(); + glViewport(0, 0, mFrameWidth, mFrameHeight); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); @@ -904,26 +828,30 @@ void OpenGLComposite::renderEffect() glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); - glEnable(GL_TEXTURE_2D); + glActiveTexture(GL_TEXTURE0 + kVideoTextureUnit); glBindTexture(GL_TEXTURE_2D, mCaptureTexture); + glBindVertexArray(mFullscreenVAO); glUseProgram(mProgram); - if (mUYVYtexUniform >= 0) - glUniform1i(mUYVYtexUniform, 0); + if (mRuntimeHost) + { + const RuntimeRenderState state = mRuntimeHost->GetRenderState(mFrameWidth, mFrameHeight); + updateGlobalParamsBuffer(state); + } glDrawArrays(GL_TRIANGLES, 0, 3); glUseProgram(0); + glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); - glDisable(GL_TEXTURE_2D); + glActiveTexture(GL_TEXTURE0); if (mFastTransferExtensionAvailable) VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU); } -// Compile a fullscreen shader pass from the runtime Slang source. The Slang compiler -// emits modern GLSL which we patch into a compatibility-profile shader that can run -// inside the sample's WGL context. +// Compile a fullscreen shader pass from the runtime Slang source into a core-profile +// GLSL program. The renderer owns the fullscreen pass and parameter UBO layout. bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMessage) { GLsizei errorBufferSize = 0; @@ -933,8 +861,9 @@ bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMes std::string loadError; const char* vertexSource = kVertexShaderSource; - if (!BuildFragmentShaderSourceFromSlang(fragmentShaderSource, loadError)) + if (!mRuntimeHost->BuildActiveFragmentShaderSource(fragmentShaderSource, loadError)) { + mRuntimeHost->SetCompileStatus(false, loadError); CopyErrorMessage(loadError, errorMessageSize, errorMessage); return false; } @@ -983,28 +912,182 @@ bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMes mProgram = newProgram; mVertexShader = newVertexShader; mFragmentShader = newFragmentShader; - mUYVYtexUniform = glGetUniformLocation(mProgram, "UYVYtex"); + const RuntimeRenderState state = mRuntimeHost->GetRenderState(mFrameWidth, mFrameHeight); + if (!updateGlobalParamsBuffer(state)) + { + CopyErrorMessage("Failed to allocate the runtime parameter UBO.", errorMessageSize, errorMessage); + destroyShaderProgram(); + return false; + } + mRuntimeHost->SetCompileStatus(true, "Shader compiled successfully."); + mRuntimeHost->ClearReloadRequest(); + + return true; +} + +bool OpenGLComposite::PollRuntimeChanges() +{ + if (!mRuntimeHost) + return true; + + bool registryChanged = false; + bool reloadRequested = false; + std::string runtimeError; + if (!mRuntimeHost->PollFileChanges(registryChanged, reloadRequested, runtimeError)) + { + mRuntimeHost->SetCompileStatus(false, runtimeError); + broadcastRuntimeState(); + return false; + } + + if (registryChanged) + broadcastRuntimeState(); + + if (!reloadRequested) + return true; + + char compilerErrorMessage[1024] = {}; + if (!compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage)) + { + mRuntimeHost->SetCompileStatus(false, compilerErrorMessage); + mRuntimeHost->ClearReloadRequest(); + broadcastRuntimeState(); + return false; + } + + broadcastRuntimeState(); + return true; +} + +void OpenGLComposite::broadcastRuntimeState() +{ + if (mControlServer) + mControlServer->BroadcastState(); +} + +bool OpenGLComposite::updateGlobalParamsBuffer(const RuntimeRenderState& state) +{ + std::vector buffer; + buffer.reserve(512); + + AppendStd140Float(buffer, static_cast(state.timeSeconds)); + AppendStd140Vec2(buffer, static_cast(state.inputWidth), static_cast(state.inputHeight)); + AppendStd140Vec2(buffer, static_cast(state.outputWidth), static_cast(state.outputHeight)); + AppendStd140Float(buffer, static_cast(state.frameCount)); + AppendStd140Float(buffer, static_cast(state.mixAmount)); + AppendStd140Float(buffer, static_cast(state.bypass)); + + for (const ShaderParameterDefinition& definition : state.parameterDefinitions) + { + auto valueIt = state.parameterValues.find(definition.id); + const ShaderParameterValue value = valueIt != state.parameterValues.end() + ? valueIt->second + : ShaderParameterValue(); + + switch (definition.type) + { + case ShaderParameterType::Float: + AppendStd140Float(buffer, value.numberValues.empty() ? 0.0f : static_cast(value.numberValues[0])); + break; + case ShaderParameterType::Vec2: + AppendStd140Vec2(buffer, + value.numberValues.size() > 0 ? static_cast(value.numberValues[0]) : 0.0f, + value.numberValues.size() > 1 ? static_cast(value.numberValues[1]) : 0.0f); + break; + case ShaderParameterType::Color: + AppendStd140Vec4(buffer, + value.numberValues.size() > 0 ? static_cast(value.numberValues[0]) : 1.0f, + value.numberValues.size() > 1 ? static_cast(value.numberValues[1]) : 1.0f, + value.numberValues.size() > 2 ? static_cast(value.numberValues[2]) : 1.0f, + value.numberValues.size() > 3 ? static_cast(value.numberValues[3]) : 1.0f); + break; + case ShaderParameterType::Boolean: + AppendStd140Int(buffer, value.booleanValue ? 1 : 0); + break; + case ShaderParameterType::Enum: + { + int selectedIndex = 0; + for (std::size_t optionIndex = 0; optionIndex < definition.enumOptions.size(); ++optionIndex) + { + if (definition.enumOptions[optionIndex].value == value.enumValue) + { + selectedIndex = static_cast(optionIndex); + break; + } + } + AppendStd140Int(buffer, selectedIndex); + break; + } + } + } + + buffer.resize(AlignStd140(buffer.size(), 16), 0); + + glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsUBO); + if (mGlobalParamsUBOSize != static_cast(buffer.size())) + { + glBufferData(GL_UNIFORM_BUFFER, static_cast(buffer.size()), buffer.data(), GL_DYNAMIC_DRAW); + mGlobalParamsUBOSize = static_cast(buffer.size()); + } + else + { + glBufferSubData(GL_UNIFORM_BUFFER, 0, static_cast(buffer.size()), buffer.data()); + } + glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsUBO); + glBindBuffer(GL_UNIFORM_BUFFER, 0); + + return true; +} + +std::string OpenGLComposite::GetRuntimeStateJson() const +{ + return mRuntimeHost ? mRuntimeHost->BuildStateJson() : "{}"; +} + +bool OpenGLComposite::SelectShader(const std::string& shaderId, std::string& error) +{ + if (!mRuntimeHost->SelectShader(shaderId, error)) + return false; + + ReloadShader(); + broadcastRuntimeState(); + return true; +} + +bool OpenGLComposite::UpdateParameterJson(const std::string& shaderId, const std::string& parameterId, const std::string& valueJson, std::string& error) +{ + JsonValue parsedValue; + if (!ParseJson(valueJson, parsedValue, error)) + return false; + + if (!mRuntimeHost->UpdateParameter(shaderId, parameterId, parsedValue, error)) + return false; + + broadcastRuntimeState(); + return true; +} + +bool OpenGLComposite::SetBypassEnabled(bool bypassEnabled, std::string& error) +{ + if (!mRuntimeHost->SetBypass(bypassEnabled, error)) + return false; + broadcastRuntimeState(); + return true; +} + +bool OpenGLComposite::SetMixAmount(double mixAmount, std::string& error) +{ + if (!mRuntimeHost->SetMixAmount(mixAmount, error)) + return false; + broadcastRuntimeState(); return true; } bool OpenGLComposite::CheckOpenGLExtensions() { - const GLubyte* strExt; - bool hasFBO; - - // The GL_EXT_framebuffer_object extension is required but GL_AMD_pinned_memory is optional - strExt = glGetString (GL_EXTENSIONS); - hasFBO = strstr((char*)strExt, "GL_EXT_framebuffer_object") != NULL; - mFastTransferExtensionAvailable = VideoFrameTransfer::checkFastMemoryTransferAvailable(); - if (!hasFBO) - { - MessageBox(NULL, _T("Required OpenGL extension \"GL_EXT_framebuffer_object\" is not available."), _T("OpenGL initialization error."), MB_OK); - return false; - } - if (!mFastTransferExtensionAvailable) OutputDebugStringA("Fast memory transfer extension not available, using regular OpenGL transfer fallback instead\n"); diff --git a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.h b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.h index 33a5f8e..aade1da 100644 --- a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.h +++ b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.h @@ -53,8 +53,10 @@ #include "DeckLinkAPI_h.h" #include "VideoFrameTransfer.h" +#include "RuntimeHost.h" #include +#include #include #include #include @@ -63,6 +65,7 @@ class PlayoutDelegate; class CaptureDelegate; class PinnedMemoryAllocator; +class ControlServer; class OpenGLComposite @@ -75,6 +78,11 @@ public: bool Start(); bool Stop(); bool ReloadShader(); + std::string GetRuntimeStateJson() const; + bool SelectShader(const std::string& shaderId, std::string& error); + bool UpdateParameterJson(const std::string& shaderId, const std::string& parameterId, const std::string& valueJson, std::string& error); + bool SetBypassEnabled(bool bypassEnabled, std::string& error); + bool SetMixAmount(double mixAmount, std::string& error); void resizeGL(WORD width, WORD height); void paintGL(); @@ -113,19 +121,24 @@ private: GLuint mIdFrameBuf; GLuint mIdColorBuf; GLuint mIdDepthBuf; + GLuint mFullscreenVAO; + GLuint mGlobalParamsUBO; GLuint mProgram; GLuint mVertexShader; GLuint mFragmentShader; - GLint mUYVYtexUniform; - GLfloat mRotateAngle; - GLfloat mRotateAngleRate; + GLsizeiptr mGlobalParamsUBOSize; int mViewWidth; int mViewHeight; + std::unique_ptr mRuntimeHost; + std::unique_ptr mControlServer; bool InitOpenGLState(); bool compileFragmentShader(int errorMessageSize, char* errorMessage); void destroyShaderProgram(); void renderEffect(); + bool PollRuntimeChanges(); + void broadcastRuntimeState(); + bool updateGlobalParamsBuffer(const RuntimeRenderState& state); }; //////////////////////////////////////////// diff --git a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp new file mode 100644 index 0000000..dee76bd --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp @@ -0,0 +1,1284 @@ +#include "stdafx.h" +#include "RuntimeHost.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +std::string Trim(const std::string& text) +{ + std::size_t start = 0; + while (start < text.size() && std::isspace(static_cast(text[start]))) + ++start; + + std::size_t end = text.size(); + while (end > start && std::isspace(static_cast(text[end - 1]))) + --end; + + return text.substr(start, end - start); +} + +std::string ReplaceAll(std::string text, const std::string& from, const std::string& to) +{ + std::size_t offset = 0; + while ((offset = text.find(from, offset)) != std::string::npos) + { + text.replace(offset, from.length(), to); + offset += to.length(); + } + return text; +} + +bool IsFiniteNumber(double value) +{ + return std::isfinite(value) != 0; +} + +std::vector JsonArrayToNumbers(const JsonValue& value) +{ + std::vector numbers; + for (const JsonValue& item : value.asArray()) + { + if (item.isNumber()) + numbers.push_back(item.asNumber()); + } + return numbers; +} + +std::filesystem::path FindRepoRootCandidate() +{ + std::vector rootsToTry; + + char currentDirectory[MAX_PATH] = {}; + if (GetCurrentDirectoryA(MAX_PATH, currentDirectory) > 0) + rootsToTry.push_back(std::filesystem::path(currentDirectory)); + + char modulePath[MAX_PATH] = {}; + DWORD moduleLength = GetModuleFileNameA(NULL, modulePath, MAX_PATH); + if (moduleLength > 0 && moduleLength < MAX_PATH) + rootsToTry.push_back(std::filesystem::path(modulePath).parent_path()); + + for (const std::filesystem::path& startPath : rootsToTry) + { + std::filesystem::path candidate = startPath; + for (int depth = 0; depth < 10 && !candidate.empty(); ++depth) + { + if (std::filesystem::exists(candidate / "CMakeLists.txt") && + std::filesystem::exists(candidate / "apps" / "LoopThroughWithOpenGLCompositing")) + { + return candidate; + } + + candidate = candidate.parent_path(); + } + } + + return std::filesystem::path(); +} + +std::string ShaderParameterTypeToString(ShaderParameterType type) +{ + switch (type) + { + case ShaderParameterType::Float: return "float"; + case ShaderParameterType::Vec2: return "vec2"; + case ShaderParameterType::Color: return "color"; + case ShaderParameterType::Boolean: return "bool"; + case ShaderParameterType::Enum: return "enum"; + } + return "unknown"; +} + +std::string SlangTypeForParameter(ShaderParameterType type) +{ + switch (type) + { + case ShaderParameterType::Float: return "uniform float"; + case ShaderParameterType::Vec2: return "uniform float2"; + case ShaderParameterType::Color: return "uniform float4"; + case ShaderParameterType::Boolean: return "uniform bool"; + case ShaderParameterType::Enum: return "uniform int"; + } + return "uniform float"; +} + +std::string GlslTypeForUniformDeclaration(const std::string& declaration) +{ + return Trim(declaration); +} + +bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type) +{ + if (typeName == "float") + { + type = ShaderParameterType::Float; + return true; + } + if (typeName == "vec2") + { + type = ShaderParameterType::Vec2; + return true; + } + if (typeName == "color") + { + type = ShaderParameterType::Color; + return true; + } + if (typeName == "bool") + { + type = ShaderParameterType::Boolean; + return true; + } + if (typeName == "enum") + { + type = ShaderParameterType::Enum; + return true; + } + return false; +} +} + +RuntimeHost::RuntimeHost() + : mReloadRequested(false), + mCompileSucceeded(false), + mHasSignal(false), + mSignalWidth(0), + mSignalHeight(0), + mServerPort(8080), + mAutoReloadEnabled(true), + mMixAmount(1.0), + mBypass(false), + mStartTime(std::chrono::steady_clock::now()), + mLastScanTime(std::chrono::steady_clock::time_point::min()), + mFrameCounter(0) +{ +} + +bool RuntimeHost::Initialize(std::string& error) +{ + try + { + std::lock_guard lock(mMutex); + + if (!ResolvePaths(error)) + return false; + if (!LoadConfig(error)) + return false; + mShaderRoot = mRepoRoot / mConfig.shaderLibrary; + if (!LoadPersistentState(error)) + return false; + if (!ScanShaderPackages(error)) + return false; + + if (mActiveShaderId.empty() && !mPackageOrder.empty()) + mActiveShaderId = mPackageOrder.front(); + + mServerPort = mConfig.serverPort; + mAutoReloadEnabled = mConfig.autoReload; + mReloadRequested = true; + mCompileMessage = "Waiting for shader compile."; + return true; + } + catch (const std::exception& exception) + { + error = std::string("RuntimeHost::Initialize exception: ") + exception.what(); + return false; + } + catch (...) + { + error = "RuntimeHost::Initialize threw a non-standard exception."; + return false; + } +} + +bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error) +{ + try + { + std::lock_guard lock(mMutex); + registryChanged = false; + reloadRequested = false; + + if (!mAutoReloadEnabled) + { + reloadRequested = mReloadRequested; + return true; + } + + const auto now = std::chrono::steady_clock::now(); + if (mLastScanTime != std::chrono::steady_clock::time_point::min() && + std::chrono::duration_cast(now - mLastScanTime).count() < 250) + { + reloadRequested = mReloadRequested; + return true; + } + + mLastScanTime = now; + + std::string scanError; + std::map previousPackages = mPackagesById; + std::vector previousOrder = mPackageOrder; + const std::string previousActive = mActiveShaderId; + + if (!ScanShaderPackages(scanError)) + { + error = scanError; + return false; + } + + registryChanged = previousOrder != mPackageOrder; + if (!registryChanged && previousPackages.size() == mPackagesById.size()) + { + for (const auto& item : mPackagesById) + { + auto previous = previousPackages.find(item.first); + if (previous == previousPackages.end()) + { + registryChanged = true; + break; + } + if (previous->second.shaderWriteTime != item.second.shaderWriteTime || + previous->second.manifestWriteTime != item.second.manifestWriteTime) + { + registryChanged = true; + break; + } + } + } + + auto previousActiveIt = previousPackages.find(previousActive); + auto activeIt = mPackagesById.find(mActiveShaderId); + if (previousActiveIt != previousPackages.end() && activeIt != mPackagesById.end()) + { + if (previousActiveIt->second.shaderWriteTime != activeIt->second.shaderWriteTime || + previousActiveIt->second.manifestWriteTime != activeIt->second.manifestWriteTime) + { + mReloadRequested = true; + } + } + + if (previousActive != mActiveShaderId) + mReloadRequested = true; + + reloadRequested = mReloadRequested; + return true; + } + catch (const std::exception& exception) + { + error = std::string("RuntimeHost::PollFileChanges exception: ") + exception.what(); + return false; + } + catch (...) + { + error = "RuntimeHost::PollFileChanges threw a non-standard exception."; + return false; + } +} + +bool RuntimeHost::ManualReloadRequested() +{ + std::lock_guard lock(mMutex); + return mReloadRequested; +} + +void RuntimeHost::ClearReloadRequest() +{ + std::lock_guard lock(mMutex); + mReloadRequested = false; +} + +bool RuntimeHost::SelectShader(const std::string& shaderId, std::string& error) +{ + std::lock_guard lock(mMutex); + if (mPackagesById.find(shaderId) == mPackagesById.end()) + { + error = "Unknown shader id: " + shaderId; + return false; + } + + mActiveShaderId = shaderId; + mPersistentState.activeShaderId = shaderId; + mReloadRequested = true; + return SavePersistentState(error); +} + +bool RuntimeHost::UpdateParameter(const std::string& shaderId, const std::string& parameterId, const JsonValue& newValue, std::string& error) +{ + std::lock_guard lock(mMutex); + + auto shaderIt = mPackagesById.find(shaderId); + if (shaderIt == mPackagesById.end()) + { + error = "Unknown shader id: " + shaderId; + return false; + } + + const ShaderPackage& shaderPackage = shaderIt->second; + auto parameterIt = std::find_if(shaderPackage.parameters.begin(), shaderPackage.parameters.end(), + [¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; }); + if (parameterIt == shaderPackage.parameters.end()) + { + error = "Unknown parameter id: " + parameterId; + return false; + } + + ShaderParameterValue normalized; + if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error)) + return false; + + mPersistentState.parameterValuesByShader[shaderId][parameterId] = normalized; + if (shaderId == mActiveShaderId) + mReloadRequested = true; + + return SavePersistentState(error); +} + +bool RuntimeHost::SetBypass(bool bypassEnabled, std::string& error) +{ + std::lock_guard lock(mMutex); + mBypass = bypassEnabled; + mPersistentState.bypass = bypassEnabled; + return SavePersistentState(error); +} + +bool RuntimeHost::SetMixAmount(double mixAmount, std::string& error) +{ + std::lock_guard lock(mMutex); + if (!IsFiniteNumber(mixAmount)) + { + error = "Mix amount must be a finite number."; + return false; + } + + mMixAmount = std::clamp(mixAmount, 0.0, 1.0); + mPersistentState.mixAmount = mMixAmount; + return SavePersistentState(error); +} + +void RuntimeHost::SetCompileStatus(bool succeeded, const std::string& message) +{ + std::lock_guard lock(mMutex); + mCompileSucceeded = succeeded; + mCompileMessage = message; +} + +void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName) +{ + std::lock_guard lock(mMutex); + mHasSignal = hasSignal; + mSignalWidth = width; + mSignalHeight = height; + mSignalModeName = modeName; +} + +void RuntimeHost::AdvanceFrame() +{ + std::lock_guard lock(mMutex); + ++mFrameCounter; +} + +bool RuntimeHost::BuildActiveFragmentShaderSource(std::string& fragmentShaderSource, std::string& error) +{ + try + { + ShaderPackage shaderPackage; + { + std::lock_guard lock(mMutex); + auto it = mPackagesById.find(mActiveShaderId); + if (it == mPackagesById.end()) + { + error = "No active shader is selected."; + return false; + } + shaderPackage = it->second; + } + + const std::string wrapperSource = BuildWrapperSlangSource(shaderPackage); + if (!WriteTextFile(mWrapperPath, wrapperSource, error)) + return false; + + if (!RunSlangCompiler(mWrapperPath, mGeneratedGlslPath, error)) + return false; + + fragmentShaderSource = ReadTextFile(mGeneratedGlslPath, error); + if (fragmentShaderSource.empty()) + return false; + + if (!PatchGeneratedGlsl(fragmentShaderSource, error)) + return false; + + if (!WriteTextFile(mPatchedGlslPath, fragmentShaderSource, error)) + return false; + + return true; + } + catch (const std::exception& exception) + { + error = std::string("RuntimeHost::BuildActiveFragmentShaderSource exception: ") + exception.what(); + return false; + } + catch (...) + { + error = "RuntimeHost::BuildActiveFragmentShaderSource threw a non-standard exception."; + return false; + } +} + +RuntimeRenderState RuntimeHost::GetRenderState(unsigned outputWidth, unsigned outputHeight) const +{ + std::lock_guard lock(mMutex); + RuntimeRenderState state; + state.activeShaderId = mActiveShaderId; + state.timeSeconds = std::chrono::duration_cast>(std::chrono::steady_clock::now() - mStartTime).count(); + state.frameCount = static_cast(mFrameCounter); + state.mixAmount = mMixAmount; + state.bypass = mBypass ? 1.0 : 0.0; + state.inputWidth = mSignalWidth; + state.inputHeight = mSignalHeight; + state.outputWidth = outputWidth; + state.outputHeight = outputHeight; + + auto shaderIt = mPackagesById.find(mActiveShaderId); + if (shaderIt != mPackagesById.end()) + { + state.parameterDefinitions = shaderIt->second.parameters; + auto persistedIt = mPersistentState.parameterValuesByShader.find(mActiveShaderId); + for (const ShaderParameterDefinition& definition : shaderIt->second.parameters) + { + ShaderParameterValue value = DefaultValueForDefinition(definition); + if (persistedIt != mPersistentState.parameterValuesByShader.end()) + { + auto valueIt = persistedIt->second.find(definition.id); + if (valueIt != persistedIt->second.end()) + value = valueIt->second; + } + state.parameterValues[definition.id] = value; + } + } + + return state; +} + +std::string RuntimeHost::BuildStateJson() const +{ + return SerializeJson(BuildStateValue(), true); +} + +void RuntimeHost::SetServerPort(unsigned short port) +{ + std::lock_guard lock(mMutex); + mServerPort = port; +} + +bool RuntimeHost::LoadConfig(std::string& error) +{ + if (!std::filesystem::exists(mConfigPath)) + return true; + + std::string configText = ReadTextFile(mConfigPath, error); + if (configText.empty()) + return false; + + JsonValue configJson; + if (!ParseJson(configText, configJson, error)) + return false; + + if (const JsonValue* shaderLibraryValue = configJson.find("shaderLibrary")) + mConfig.shaderLibrary = shaderLibraryValue->asString(); + if (const JsonValue* serverPortValue = configJson.find("serverPort")) + mConfig.serverPort = static_cast(serverPortValue->asNumber(mConfig.serverPort)); + if (const JsonValue* autoReloadValue = configJson.find("autoReload")) + mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload); + + mAutoReloadEnabled = mConfig.autoReload; + return true; +} + +bool RuntimeHost::LoadPersistentState(std::string& error) +{ + if (!std::filesystem::exists(mRuntimeStatePath)) + return true; + + std::string stateText = ReadTextFile(mRuntimeStatePath, error); + if (stateText.empty()) + return false; + + JsonValue root; + if (!ParseJson(stateText, root, error)) + return false; + + if (const JsonValue* activeShaderValue = root.find("activeShaderId")) + mPersistentState.activeShaderId = activeShaderValue->asString(); + if (const JsonValue* mixAmountValue = root.find("mixAmount")) + mPersistentState.mixAmount = mixAmountValue->asNumber(1.0); + if (const JsonValue* bypassValue = root.find("bypass")) + mPersistentState.bypass = bypassValue->asBoolean(false); + + if (const JsonValue* valuesByShader = root.find("parameterValuesByShader")) + { + for (const auto& shaderItem : valuesByShader->asObject()) + { + std::map& shaderValues = mPersistentState.parameterValuesByShader[shaderItem.first]; + for (const auto& parameterItem : shaderItem.second.asObject()) + { + ShaderParameterValue value; + const JsonValue& jsonValue = parameterItem.second; + if (jsonValue.isBoolean()) + { + value.booleanValue = jsonValue.asBoolean(); + } + else if (jsonValue.isString()) + { + value.enumValue = jsonValue.asString(); + } + else if (jsonValue.isNumber()) + { + value.numberValues.push_back(jsonValue.asNumber()); + } + else if (jsonValue.isArray()) + { + value.numberValues = JsonArrayToNumbers(jsonValue); + } + shaderValues[parameterItem.first] = value; + } + } + } + + mActiveShaderId = mPersistentState.activeShaderId; + mMixAmount = std::clamp(mPersistentState.mixAmount, 0.0, 1.0); + mBypass = mPersistentState.bypass; + return true; +} + +bool RuntimeHost::SavePersistentState(std::string& error) const +{ + JsonValue root = JsonValue::MakeObject(); + root.set("activeShaderId", JsonValue(mActiveShaderId)); + root.set("mixAmount", JsonValue(mMixAmount)); + root.set("bypass", JsonValue(mBypass)); + + JsonValue valuesByShader = JsonValue::MakeObject(); + for (const auto& shaderItem : mPersistentState.parameterValuesByShader) + { + JsonValue shaderValues = JsonValue::MakeObject(); + auto packageIt = mPackagesById.find(shaderItem.first); + for (const auto& parameterItem : shaderItem.second) + { + const ShaderParameterDefinition* definition = nullptr; + if (packageIt != mPackagesById.end()) + { + for (const ShaderParameterDefinition& candidate : packageIt->second.parameters) + { + if (candidate.id == parameterItem.first) + { + definition = &candidate; + break; + } + } + } + + if (definition) + shaderValues.set(parameterItem.first, SerializeParameterValue(*definition, parameterItem.second)); + } + valuesByShader.set(shaderItem.first, shaderValues); + } + root.set("parameterValuesByShader", valuesByShader); + + return WriteTextFile(mRuntimeStatePath, SerializeJson(root, true), error); +} + +bool RuntimeHost::ScanShaderPackages(std::string& error) +{ + std::map packagesById; + std::vector packageOrder; + + if (!std::filesystem::exists(mShaderRoot)) + { + error = "Shader library directory does not exist: " + mShaderRoot.string(); + return false; + } + + for (const auto& entry : std::filesystem::directory_iterator(mShaderRoot)) + { + if (!entry.is_directory()) + continue; + + std::filesystem::path manifestPath = entry.path() / "shader.json"; + if (!std::filesystem::exists(manifestPath)) + continue; + + ShaderPackage shaderPackage; + if (!ParseShaderManifest(manifestPath, shaderPackage, error)) + return false; + + if (packagesById.find(shaderPackage.id) != packagesById.end()) + { + error = "Duplicate shader id found: " + shaderPackage.id; + return false; + } + + EnsureParameterDefaultsLocked(shaderPackage); + packageOrder.push_back(shaderPackage.id); + packagesById[shaderPackage.id] = shaderPackage; + } + + std::sort(packageOrder.begin(), packageOrder.end()); + mPackagesById.swap(packagesById); + mPackageOrder.swap(packageOrder); + + if (!mActiveShaderId.empty() && mPackagesById.find(mActiveShaderId) == mPackagesById.end()) + { + mActiveShaderId.clear(); + mPersistentState.activeShaderId.clear(); + } + + if (mActiveShaderId.empty() && !mPackageOrder.empty()) + { + mActiveShaderId = mPackageOrder.front(); + mPersistentState.activeShaderId = mActiveShaderId; + } + + return true; +} + +bool RuntimeHost::ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const +{ + const std::string manifestText = ReadTextFile(manifestPath, error); + if (manifestText.empty()) + return false; + + JsonValue manifestJson; + if (!ParseJson(manifestText, manifestJson, error)) + return false; + + const JsonValue* idValue = manifestJson.find("id"); + const JsonValue* nameValue = manifestJson.find("name"); + if (!idValue || !nameValue) + { + error = "Shader manifest is missing required 'id' or 'name' field: " + manifestPath.string(); + return false; + } + + shaderPackage.id = idValue->asString(); + shaderPackage.displayName = nameValue->asString(); + shaderPackage.description = manifestJson.find("description") ? manifestJson.find("description")->asString() : ""; + shaderPackage.category = manifestJson.find("category") ? manifestJson.find("category")->asString() : ""; + shaderPackage.entryPoint = manifestJson.find("entryPoint") ? manifestJson.find("entryPoint")->asString() : "shadeVideo"; + shaderPackage.directoryPath = manifestPath.parent_path(); + shaderPackage.shaderPath = shaderPackage.directoryPath / "shader.slang"; + shaderPackage.manifestPath = manifestPath; + + if (!std::filesystem::exists(shaderPackage.shaderPath)) + { + error = "Shader source not found for package " + shaderPackage.id + ": " + shaderPackage.shaderPath.string(); + return false; + } + + shaderPackage.shaderWriteTime = std::filesystem::last_write_time(shaderPackage.shaderPath); + shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath); + + const JsonValue* parametersValue = manifestJson.find("parameters"); + if (parametersValue && parametersValue->isArray()) + { + for (const JsonValue& parameterJson : parametersValue->asArray()) + { + const JsonValue* parameterIdValue = parameterJson.find("id"); + const JsonValue* parameterLabelValue = parameterJson.find("label"); + const JsonValue* parameterTypeValue = parameterJson.find("type"); + if (!parameterIdValue || !parameterLabelValue || !parameterTypeValue) + { + error = "Shader parameter is missing required fields in: " + manifestPath.string(); + return false; + } + + ShaderParameterDefinition definition; + definition.id = parameterIdValue->asString(); + definition.label = parameterLabelValue->asString(); + if (!ParseShaderParameterType(parameterTypeValue->asString(), definition.type)) + { + error = "Unsupported parameter type '" + parameterTypeValue->asString() + "' in: " + manifestPath.string(); + return false; + } + + if (const JsonValue* defaultValue = parameterJson.find("default")) + { + if (definition.type == ShaderParameterType::Boolean) + { + definition.defaultBoolean = defaultValue->asBoolean(false); + } + else if (definition.type == ShaderParameterType::Enum) + { + definition.defaultEnumValue = defaultValue->asString(); + } + else if (defaultValue->isNumber()) + { + definition.defaultNumbers.push_back(defaultValue->asNumber()); + } + else if (defaultValue->isArray()) + { + definition.defaultNumbers = JsonArrayToNumbers(*defaultValue); + } + } + + if (const JsonValue* minValue = parameterJson.find("min")) + { + if (minValue->isNumber()) + definition.minNumbers.push_back(minValue->asNumber()); + else if (minValue->isArray()) + definition.minNumbers = JsonArrayToNumbers(*minValue); + } + if (const JsonValue* maxValue = parameterJson.find("max")) + { + if (maxValue->isNumber()) + definition.maxNumbers.push_back(maxValue->asNumber()); + else if (maxValue->isArray()) + definition.maxNumbers = JsonArrayToNumbers(*maxValue); + } + if (const JsonValue* stepValue = parameterJson.find("step")) + { + if (stepValue->isNumber()) + definition.stepNumbers.push_back(stepValue->asNumber()); + else if (stepValue->isArray()) + definition.stepNumbers = JsonArrayToNumbers(*stepValue); + } + + if (definition.type == ShaderParameterType::Enum) + { + const JsonValue* optionsValue = parameterJson.find("options"); + if (!optionsValue || !optionsValue->isArray()) + { + error = "Enum parameter is missing 'options' in: " + manifestPath.string(); + return false; + } + + for (const JsonValue& optionJson : optionsValue->asArray()) + { + const JsonValue* value = optionJson.find("value"); + const JsonValue* label = optionJson.find("label"); + if (!value || !label) + { + error = "Enum parameter option is missing 'value' or 'label' in: " + manifestPath.string(); + return false; + } + + ShaderParameterOption option; + option.value = value->asString(); + option.label = label->asString(); + definition.enumOptions.push_back(option); + } + + bool defaultFound = definition.defaultEnumValue.empty(); + for (const ShaderParameterOption& option : definition.enumOptions) + { + if (option.value == definition.defaultEnumValue) + { + defaultFound = true; + break; + } + } + + if (!defaultFound) + { + error = "Enum parameter default is not present in its option list for: " + definition.id; + return false; + } + } + + shaderPackage.parameters.push_back(definition); + } + } + + return true; +} + +bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const +{ + normalizedValue = DefaultValueForDefinition(definition); + + switch (definition.type) + { + case ShaderParameterType::Float: + { + if (!value.isNumber()) + { + error = "Expected numeric value for float parameter '" + definition.id + "'."; + return false; + } + double number = value.asNumber(); + if (!IsFiniteNumber(number)) + { + error = "Float parameter '" + definition.id + "' must be finite."; + return false; + } + if (!definition.minNumbers.empty()) + number = std::max(number, definition.minNumbers.front()); + if (!definition.maxNumbers.empty()) + number = std::min(number, definition.maxNumbers.front()); + normalizedValue.numberValues = { number }; + return true; + } + case ShaderParameterType::Vec2: + case ShaderParameterType::Color: + { + std::vector numbers = JsonArrayToNumbers(value); + const std::size_t expectedSize = definition.type == ShaderParameterType::Vec2 ? 2 : 4; + if (numbers.size() != expectedSize) + { + error = "Expected array value of size " + std::to_string(expectedSize) + " for parameter '" + definition.id + "'."; + return false; + } + for (std::size_t index = 0; index < numbers.size(); ++index) + { + if (!IsFiniteNumber(numbers[index])) + { + error = "Parameter '" + definition.id + "' contains a non-finite value."; + return false; + } + if (index < definition.minNumbers.size()) + numbers[index] = std::max(numbers[index], definition.minNumbers[index]); + if (index < definition.maxNumbers.size()) + numbers[index] = std::min(numbers[index], definition.maxNumbers[index]); + } + normalizedValue.numberValues = numbers; + return true; + } + case ShaderParameterType::Boolean: + if (!value.isBoolean()) + { + error = "Expected boolean value for parameter '" + definition.id + "'."; + return false; + } + normalizedValue.booleanValue = value.asBoolean(); + return true; + case ShaderParameterType::Enum: + { + if (!value.isString()) + { + error = "Expected string value for enum parameter '" + definition.id + "'."; + return false; + } + const std::string selectedValue = value.asString(); + for (const ShaderParameterOption& option : definition.enumOptions) + { + if (option.value == selectedValue) + { + normalizedValue.enumValue = selectedValue; + return true; + } + } + error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'."; + return false; + } + } + + return false; +} + +ShaderParameterValue RuntimeHost::DefaultValueForDefinition(const ShaderParameterDefinition& definition) const +{ + ShaderParameterValue value; + switch (definition.type) + { + case ShaderParameterType::Float: + value.numberValues = definition.defaultNumbers.empty() ? std::vector{ 0.0 } : definition.defaultNumbers; + break; + case ShaderParameterType::Vec2: + value.numberValues = definition.defaultNumbers.size() == 2 ? definition.defaultNumbers : std::vector{ 0.0, 0.0 }; + break; + case ShaderParameterType::Color: + value.numberValues = definition.defaultNumbers.size() == 4 ? definition.defaultNumbers : std::vector{ 1.0, 1.0, 1.0, 1.0 }; + break; + case ShaderParameterType::Boolean: + value.booleanValue = definition.defaultBoolean; + break; + case ShaderParameterType::Enum: + value.enumValue = definition.defaultEnumValue; + break; + } + return value; +} + +void RuntimeHost::EnsureParameterDefaultsLocked(ShaderPackage& shaderPackage) +{ + for (const ShaderParameterDefinition& definition : shaderPackage.parameters) + { + auto& shaderValues = mPersistentState.parameterValuesByShader[shaderPackage.id]; + if (shaderValues.find(definition.id) == shaderValues.end()) + shaderValues[definition.id] = DefaultValueForDefinition(definition); + } +} + +std::string RuntimeHost::BuildWrapperSlangSource(const ShaderPackage& shaderPackage) const +{ + std::ostringstream source; + source << "struct FragmentInput\n"; + source << "{\n"; + source << "\tfloat4 position : SV_Position;\n"; + source << "\tfloat2 texCoord : TEXCOORD0;\n"; + source << "};\n\n"; + source << "struct ShaderContext\n"; + source << "{\n"; + source << "\tfloat2 uv;\n"; + source << "\tfloat4 sourceColor;\n"; + source << "\tfloat2 inputResolution;\n"; + source << "\tfloat2 outputResolution;\n"; + source << "\tfloat time;\n"; + source << "\tfloat frameCount;\n"; + source << "\tfloat mixAmount;\n"; + source << "\tfloat bypass;\n"; + source << "};\n\n"; + source << "cbuffer GlobalParams\n"; + source << "{\n"; + source << "\tfloat gTime;\n"; + source << "\tfloat2 gInputResolution;\n"; + source << "\tfloat2 gOutputResolution;\n"; + source << "\tfloat gFrameCount;\n"; + source << "\tfloat gMixAmount;\n"; + source << "\tfloat gBypass;\n"; + for (const ShaderParameterDefinition& definition : shaderPackage.parameters) + source << "\t" << SlangTypeForParameter(definition.type).substr(strlen("uniform ")) << " " << definition.id << ";\n"; + source << "};\n\n"; + source << "Sampler2D gVideoInput;\n\n"; + source << "float4 rec709YCbCr2rgba(float Y, float Cb, float Cr, float a)\n"; + source << "{\n"; + source << "\tY = (Y * 256.0 - 16.0) / 219.0;\n"; + source << "\tCb = (Cb * 256.0 - 16.0) / 224.0 - 0.5;\n"; + source << "\tCr = (Cr * 256.0 - 16.0) / 224.0 - 0.5;\n"; + source << "\treturn float4(Y + 1.5748 * Cr, Y - 0.1873 * Cb - 0.4681 * Cr, Y + 1.8556 * Cb, a);\n"; + source << "}\n\n"; + source << "float4 bilinear(float4 W, float4 X, float4 Y, float4 Z, float2 weight)\n"; + source << "{\n"; + source << "\tfloat4 m0 = lerp(W, Z, weight.x);\n"; + source << "\tfloat4 m1 = lerp(X, Y, weight.x);\n"; + source << "\treturn lerp(m0, m1, weight.y);\n"; + source << "}\n\n"; + source << "void textureGatherYUV(Sampler2D textureSampler, float2 tc, out float4 W, out float4 X, out float4 Y, out float4 Z)\n"; + source << "{\n"; + source << "\tuint width = 0;\n"; + source << "\tuint height = 0;\n"; + source << "\ttextureSampler.GetDimensions(width, height);\n"; + source << "\tint2 tx = int2(tc * float2(width, height));\n"; + source << "\tint2 tmin = int2(0, 0);\n"; + source << "\tint2 tmax = int2(int(width), int(height)) - int2(1, 1);\n"; + source << "\tW = textureSampler.Load(int3(tx, 0));\n"; + source << "\tX = textureSampler.Load(int3(clamp(tx + int2(0, 1), tmin, tmax), 0));\n"; + source << "\tY = textureSampler.Load(int3(clamp(tx + int2(1, 1), tmin, tmax), 0));\n"; + source << "\tZ = textureSampler.Load(int3(clamp(tx + int2(1, 0), tmin, tmax), 0));\n"; + source << "}\n\n"; + source << "float4 sampleVideo(float2 tc)\n"; + source << "{\n"; + source << "\tfloat4 macro, macroU, macroR, macroUR;\n"; + source << "\ttextureGatherYUV(gVideoInput, tc, macro, macroU, macroUR, macroR);\n"; + source << "\tuint width = 0;\n"; + source << "\tuint height = 0;\n"; + source << "\tgVideoInput.GetDimensions(width, height);\n"; + source << "\tfloat2 off = frac(tc * float2(width, height));\n"; + source << "\tfloat4 pixel, pixelR, pixelU, pixelUR;\n"; + source << "\tif (off.x > 0.5)\n"; + source << "\t{\n"; + source << "\t\tpixel = rec709YCbCr2rgba(macro.a, macro.b, macro.r, 1.0);\n"; + source << "\t\tpixelR = rec709YCbCr2rgba(macroR.g, macroR.b, macroR.r, 1.0);\n"; + source << "\t\tpixelU = rec709YCbCr2rgba(macroU.a, macroU.b, macroU.r, 1.0);\n"; + source << "\t\tpixelUR = rec709YCbCr2rgba(macroUR.g, macroUR.b, macroUR.r, 1.0);\n"; + source << "\t}\n"; + source << "\telse\n"; + source << "\t{\n"; + source << "\t\tpixel = rec709YCbCr2rgba(macro.g, macro.b, macro.r, 1.0);\n"; + source << "\t\tpixelR = rec709YCbCr2rgba(macro.a, macro.b, macro.r, 1.0);\n"; + source << "\t\tpixelU = rec709YCbCr2rgba(macroU.g, macroU.b, macroU.r, 1.0);\n"; + source << "\t\tpixelUR = rec709YCbCr2rgba(macroU.a, macroU.b, macroU.r, 1.0);\n"; + source << "\t}\n"; + source << "\treturn bilinear(pixel, pixelU, pixelUR, pixelR, off);\n"; + source << "}\n\n"; + source << "#include \"" << shaderPackage.shaderPath.generic_string() << "\"\n\n"; + source << "[shader(\"fragment\")]\n"; + source << "float4 fragmentMain(FragmentInput input) : SV_Target\n"; + source << "{\n"; + source << "\tShaderContext context;\n"; + source << "\tfloat2 correctedUv = float2(input.texCoord.x, 1.0 - input.texCoord.y);\n"; + source << "\tcontext.uv = correctedUv;\n"; + source << "\tcontext.sourceColor = sampleVideo(correctedUv);\n"; + source << "\tcontext.inputResolution = gInputResolution;\n"; + source << "\tcontext.outputResolution = gOutputResolution;\n"; + source << "\tcontext.time = gTime;\n"; + source << "\tcontext.frameCount = gFrameCount;\n"; + source << "\tcontext.mixAmount = gMixAmount;\n"; + source << "\tcontext.bypass = gBypass;\n"; + source << "\tfloat4 effectedColor = " << shaderPackage.entryPoint << "(context);\n"; + source << "\tfloat mixValue = clamp(gBypass > 0.5 ? 0.0 : gMixAmount, 0.0, 1.0);\n"; + source << "\treturn lerp(context.sourceColor, effectedColor, mixValue);\n"; + source << "}\n"; + return source.str(); +} + +bool RuntimeHost::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const +{ + std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty"; + if (!std::filesystem::exists(thirdPartyRoot)) + { + error = "3rdParty directory was not found under the repository root."; + return false; + } + + for (const auto& entry : std::filesystem::directory_iterator(thirdPartyRoot)) + { + if (!entry.is_directory()) + continue; + std::filesystem::path candidate = entry.path() / "bin" / "slangc.exe"; + if (std::filesystem::exists(candidate)) + { + compilerPath = candidate; + return true; + } + } + + error = "Could not find slangc.exe under 3rdParty."; + return false; +} + +bool RuntimeHost::RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const +{ + std::filesystem::path compilerPath; + if (!FindSlangCompiler(compilerPath, error)) + return false; + + std::string commandLine = "\"" + compilerPath.string() + "\" \"" + wrapperPath.string() + + "\" -target glsl -profile glsl_430 -entry fragmentMain -stage fragment -o \"" + outputPath.string() + "\""; + + STARTUPINFOA startupInfo = {}; + PROCESS_INFORMATION processInfo = {}; + startupInfo.cb = sizeof(startupInfo); + std::vector mutableCommandLine(commandLine.begin(), commandLine.end()); + mutableCommandLine.push_back('\0'); + + if (!CreateProcessA(NULL, mutableCommandLine.data(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, mRepoRoot.string().c_str(), &startupInfo, &processInfo)) + { + error = "Failed to launch slangc.exe."; + return false; + } + + WaitForSingleObject(processInfo.hProcess, INFINITE); + + DWORD exitCode = 0; + GetExitCodeProcess(processInfo.hProcess, &exitCode); + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); + + if (exitCode != 0) + { + error = "slangc.exe returned a non-zero exit code while compiling the active shader package."; + return false; + } + + return true; +} + +bool RuntimeHost::PatchGeneratedGlsl(std::string& shaderText, std::string& error) const +{ + if (shaderText.find("#version 450") == std::string::npos) + { + error = "Generated GLSL did not include the expected version header."; + return false; + } + + shaderText = ReplaceAll(shaderText, "#version 450", "#version 430 core"); + shaderText = std::regex_replace(shaderText, std::regex(R"(#extension GL_EXT_samplerless_texture_functions : require\r?\n)"), ""); + shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) uniform;\r?\n)"), ""); + shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) buffer;\r?\n)"), ""); + shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(location = 0\)\s*in vec2 ([A-Za-z0-9_]+);)"), "in vec2 vTexCoord;"); + shaderText = ReplaceAll(shaderText, "input_texCoord_0", "vTexCoord"); + + std::smatch match; + std::regex outRegex(R"(layout\(location = 0\)\s*out vec4 ([A-Za-z0-9_]+);)"); + if (std::regex_search(shaderText, match, outRegex)) + { + const std::string outputName = match[1].str(); + shaderText = std::regex_replace(shaderText, outRegex, "layout(location = 0) out vec4 fragColor;"); + shaderText = ReplaceAll(shaderText, outputName + " =", "fragColor ="); + } + + return true; +} + +std::string RuntimeHost::ReadTextFile(const std::filesystem::path& path, std::string& error) const +{ + std::ifstream input(path, std::ios::binary); + if (!input) + { + error = "Could not open file: " + path.string(); + return std::string(); + } + + std::ostringstream buffer; + buffer << input.rdbuf(); + return buffer.str(); +} + +bool RuntimeHost::WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const +{ + std::error_code fsError; + std::filesystem::create_directories(path.parent_path(), fsError); + + std::ofstream output(path, std::ios::binary); + if (!output) + { + error = "Could not write file: " + path.string(); + return false; + } + + output << contents; + return output.good(); +} + +bool RuntimeHost::ResolvePaths(std::string& error) +{ + mRepoRoot = FindRepoRootCandidate(); + if (mRepoRoot.empty()) + { + error = "Could not locate the repository root from the current runtime path."; + return false; + } + + mUiRoot = mRepoRoot / "ui"; + mConfigPath = mRepoRoot / "config" / "runtime-host.json"; + mShaderRoot = mRepoRoot / mConfig.shaderLibrary; + mRuntimeRoot = mRepoRoot / "runtime"; + mRuntimeStatePath = mRuntimeRoot / "runtime_state.json"; + mWrapperPath = mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang"; + mGeneratedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.raw.frag"; + mPatchedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.frag"; + + std::error_code fsError; + std::filesystem::create_directories(mRuntimeRoot / "shader_cache", fsError); + return true; +} + +JsonValue RuntimeHost::BuildStateValue() const +{ + std::lock_guard lock(mMutex); + + JsonValue root = JsonValue::MakeObject(); + + JsonValue app = JsonValue::MakeObject(); + app.set("serverPort", JsonValue(static_cast(mServerPort))); + app.set("autoReload", JsonValue(mAutoReloadEnabled)); + root.set("app", app); + + JsonValue runtime = JsonValue::MakeObject(); + runtime.set("activeShaderId", JsonValue(mActiveShaderId)); + runtime.set("compileSucceeded", JsonValue(mCompileSucceeded)); + runtime.set("compileMessage", JsonValue(mCompileMessage)); + runtime.set("mixAmount", JsonValue(mMixAmount)); + runtime.set("bypass", JsonValue(mBypass)); + root.set("runtime", runtime); + + JsonValue video = JsonValue::MakeObject(); + video.set("hasSignal", JsonValue(mHasSignal)); + video.set("width", JsonValue(static_cast(mSignalWidth))); + video.set("height", JsonValue(static_cast(mSignalHeight))); + video.set("modeName", JsonValue(mSignalModeName)); + root.set("video", video); + + JsonValue shaders = JsonValue::MakeArray(); + for (const std::string& shaderId : mPackageOrder) + { + auto shaderIt = mPackagesById.find(shaderId); + if (shaderIt == mPackagesById.end()) + continue; + + const ShaderPackage& shaderPackage = shaderIt->second; + JsonValue shader = JsonValue::MakeObject(); + shader.set("id", JsonValue(shaderPackage.id)); + shader.set("name", JsonValue(shaderPackage.displayName)); + shader.set("description", JsonValue(shaderPackage.description)); + shader.set("category", JsonValue(shaderPackage.category)); + + JsonValue parameters = JsonValue::MakeArray(); + auto persistedIt = mPersistentState.parameterValuesByShader.find(shaderPackage.id); + for (const ShaderParameterDefinition& definition : shaderPackage.parameters) + { + JsonValue parameter = JsonValue::MakeObject(); + parameter.set("id", JsonValue(definition.id)); + parameter.set("label", JsonValue(definition.label)); + parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type))); + + if (!definition.minNumbers.empty()) + { + JsonValue minValue = JsonValue::MakeArray(); + for (double number : definition.minNumbers) + minValue.pushBack(JsonValue(number)); + parameter.set("min", minValue); + } + if (!definition.maxNumbers.empty()) + { + JsonValue maxValue = JsonValue::MakeArray(); + for (double number : definition.maxNumbers) + maxValue.pushBack(JsonValue(number)); + parameter.set("max", maxValue); + } + if (!definition.stepNumbers.empty()) + { + JsonValue stepValue = JsonValue::MakeArray(); + for (double number : definition.stepNumbers) + stepValue.pushBack(JsonValue(number)); + parameter.set("step", stepValue); + } + + if (definition.type == ShaderParameterType::Enum) + { + JsonValue options = JsonValue::MakeArray(); + for (const ShaderParameterOption& option : definition.enumOptions) + { + JsonValue optionValue = JsonValue::MakeObject(); + optionValue.set("value", JsonValue(option.value)); + optionValue.set("label", JsonValue(option.label)); + options.pushBack(optionValue); + } + parameter.set("options", options); + } + + ShaderParameterValue value = DefaultValueForDefinition(definition); + if (persistedIt != mPersistentState.parameterValuesByShader.end()) + { + auto valueIt = persistedIt->second.find(definition.id); + if (valueIt != persistedIt->second.end()) + value = valueIt->second; + } + parameter.set("value", SerializeParameterValue(definition, value)); + parameters.pushBack(parameter); + } + + shader.set("parameters", parameters); + shaders.pushBack(shader); + } + root.set("shaders", shaders); + + return root; +} + +JsonValue RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const +{ + switch (definition.type) + { + case ShaderParameterType::Boolean: + return JsonValue(value.booleanValue); + case ShaderParameterType::Enum: + return JsonValue(value.enumValue); + case ShaderParameterType::Float: + return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front()); + case ShaderParameterType::Vec2: + case ShaderParameterType::Color: + { + JsonValue array = JsonValue::MakeArray(); + for (double number : value.numberValues) + array.pushBack(JsonValue(number)); + return array; + } + } + return JsonValue(); +} diff --git a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h new file mode 100644 index 0000000..f15a66c --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h @@ -0,0 +1,173 @@ +#pragma once + +#include "RuntimeJson.h" + +#include +#include +#include +#include +#include +#include + +enum class ShaderParameterType +{ + Float, + Vec2, + Color, + Boolean, + Enum +}; + +struct ShaderParameterOption +{ + std::string value; + std::string label; +}; + +struct ShaderParameterDefinition +{ + std::string id; + std::string label; + ShaderParameterType type = ShaderParameterType::Float; + std::vector defaultNumbers; + std::vector minNumbers; + std::vector maxNumbers; + std::vector stepNumbers; + bool defaultBoolean = false; + std::string defaultEnumValue; + std::vector enumOptions; +}; + +struct ShaderParameterValue +{ + std::vector numberValues; + bool booleanValue = false; + std::string enumValue; +}; + +struct ShaderPackage +{ + std::string id; + std::string displayName; + std::string description; + std::string category; + std::string entryPoint; + std::filesystem::path directoryPath; + std::filesystem::path shaderPath; + std::filesystem::path manifestPath; + std::vector parameters; + std::filesystem::file_time_type shaderWriteTime; + std::filesystem::file_time_type manifestWriteTime; +}; + +struct RuntimeRenderState +{ + std::string activeShaderId; + std::vector parameterDefinitions; + std::map parameterValues; + double timeSeconds = 0.0; + double frameCount = 0.0; + double mixAmount = 1.0; + double bypass = 0.0; + unsigned inputWidth = 0; + unsigned inputHeight = 0; + unsigned outputWidth = 0; + unsigned outputHeight = 0; +}; + +class RuntimeHost +{ +public: + RuntimeHost(); + + bool Initialize(std::string& error); + + bool PollFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error); + bool ManualReloadRequested(); + void ClearReloadRequest(); + + bool SelectShader(const std::string& shaderId, std::string& error); + bool UpdateParameter(const std::string& shaderId, const std::string& parameterId, const JsonValue& newValue, std::string& error); + bool SetBypass(bool bypassEnabled, std::string& error); + bool SetMixAmount(double mixAmount, std::string& error); + + void SetCompileStatus(bool succeeded, const std::string& message); + void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName); + void AdvanceFrame(); + + bool BuildActiveFragmentShaderSource(std::string& fragmentShaderSource, std::string& error); + RuntimeRenderState GetRenderState(unsigned outputWidth, unsigned outputHeight) const; + std::string BuildStateJson() const; + + const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; } + const std::filesystem::path& GetUiRoot() const { return mUiRoot; } + const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; } + unsigned short GetServerPort() const { return mServerPort; } + void SetServerPort(unsigned short port); + bool AutoReloadEnabled() const { return mAutoReloadEnabled; } + +private: + struct AppConfig + { + std::string shaderLibrary = "shaders"; + unsigned short serverPort = 8080; + bool autoReload = true; + }; + + struct PersistentState + { + std::string activeShaderId; + double mixAmount = 1.0; + bool bypass = false; + std::map> parameterValuesByShader; + }; + + bool LoadConfig(std::string& error); + bool LoadPersistentState(std::string& error); + bool SavePersistentState(std::string& error) const; + bool ScanShaderPackages(std::string& error); + bool ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const; + bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const; + ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const; + void EnsureParameterDefaultsLocked(ShaderPackage& shaderPackage); + std::string BuildWrapperSlangSource(const ShaderPackage& shaderPackage) const; + bool FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const; + bool RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const; + bool PatchGeneratedGlsl(std::string& shaderText, std::string& error) const; + std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const; + bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const; + bool ResolvePaths(std::string& error); + JsonValue BuildStateValue() const; + JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const; + +private: + mutable std::mutex mMutex; + AppConfig mConfig; + PersistentState mPersistentState; + std::filesystem::path mRepoRoot; + std::filesystem::path mUiRoot; + std::filesystem::path mShaderRoot; + std::filesystem::path mRuntimeRoot; + std::filesystem::path mRuntimeStatePath; + std::filesystem::path mConfigPath; + std::filesystem::path mWrapperPath; + std::filesystem::path mGeneratedGlslPath; + std::filesystem::path mPatchedGlslPath; + std::map mPackagesById; + std::vector mPackageOrder; + std::string mActiveShaderId; + bool mReloadRequested; + bool mCompileSucceeded; + std::string mCompileMessage; + bool mHasSignal; + unsigned mSignalWidth; + unsigned mSignalHeight; + std::string mSignalModeName; + unsigned short mServerPort; + bool mAutoReloadEnabled; + double mMixAmount; + bool mBypass; + std::chrono::steady_clock::time_point mStartTime; + std::chrono::steady_clock::time_point mLastScanTime; + uint64_t mFrameCounter; +}; diff --git a/apps/LoopThroughWithOpenGLCompositing/RuntimeJson.cpp b/apps/LoopThroughWithOpenGLCompositing/RuntimeJson.cpp new file mode 100644 index 0000000..5a74cde --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/RuntimeJson.cpp @@ -0,0 +1,500 @@ +#include "stdafx.h" +#include "RuntimeJson.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +class JsonParser +{ +public: + JsonParser(const std::string& text, std::string& error) + : mText(text), mError(error), mPosition(0) + { + } + + bool parse(JsonValue& value) + { + skipWhitespace(); + if (!parseValue(value)) + return false; + + skipWhitespace(); + if (mPosition != mText.size()) + { + setError("Unexpected trailing characters in JSON input."); + return false; + } + + return true; + } + +private: + bool parseValue(JsonValue& value) + { + if (mPosition >= mText.size()) + { + setError("Unexpected end of JSON input."); + return false; + } + + char ch = mText[mPosition]; + if (ch == '{') + return parseObject(value); + if (ch == '[') + return parseArray(value); + if (ch == '"') + { + std::string stringValue; + if (!parseString(stringValue)) + return false; + value = JsonValue(stringValue); + return true; + } + if (ch == 't') + return parseLiteral("true", JsonValue(true), value); + if (ch == 'f') + return parseLiteral("false", JsonValue(false), value); + if (ch == 'n') + return parseLiteral("null", JsonValue(), value); + if (ch == '-' || std::isdigit(static_cast(ch))) + return parseNumber(value); + + setError("Unexpected token while parsing JSON."); + return false; + } + + bool parseObject(JsonValue& value) + { + value = JsonValue::MakeObject(); + ++mPosition; + skipWhitespace(); + if (consume('}')) + return true; + + while (mPosition < mText.size()) + { + std::string key; + if (!parseString(key)) + return false; + + skipWhitespace(); + if (!consume(':')) + { + setError("Expected ':' after JSON object key."); + return false; + } + + skipWhitespace(); + JsonValue item; + if (!parseValue(item)) + return false; + + value.set(key, item); + + skipWhitespace(); + if (consume('}')) + return true; + if (!consume(',')) + { + setError("Expected ',' or '}' in JSON object."); + return false; + } + skipWhitespace(); + } + + setError("Unexpected end of JSON object."); + return false; + } + + bool parseArray(JsonValue& value) + { + value = JsonValue::MakeArray(); + ++mPosition; + skipWhitespace(); + if (consume(']')) + return true; + + while (mPosition < mText.size()) + { + JsonValue item; + if (!parseValue(item)) + return false; + + value.pushBack(item); + + skipWhitespace(); + if (consume(']')) + return true; + if (!consume(',')) + { + setError("Expected ',' or ']' in JSON array."); + return false; + } + skipWhitespace(); + } + + setError("Unexpected end of JSON array."); + return false; + } + + bool parseString(std::string& value) + { + if (!consume('"')) + { + setError("Expected string literal."); + return false; + } + + std::ostringstream result; + while (mPosition < mText.size()) + { + char ch = mText[mPosition++]; + if (ch == '"') + { + value = result.str(); + return true; + } + + if (ch == '\\') + { + if (mPosition >= mText.size()) + { + setError("Unexpected end of escaped JSON string."); + return false; + } + + char escaped = mText[mPosition++]; + switch (escaped) + { + case '"': result << '"'; break; + case '\\': result << '\\'; break; + case '/': result << '/'; break; + case 'b': result << '\b'; break; + case 'f': result << '\f'; break; + case 'n': result << '\n'; break; + case 'r': result << '\r'; break; + case 't': result << '\t'; break; + case 'u': + setError("Unicode escape sequences are not supported in this JSON parser."); + return false; + default: + setError("Invalid escape sequence in JSON string."); + return false; + } + } + else + { + result << ch; + } + } + + setError("Unexpected end of JSON string."); + return false; + } + + bool parseNumber(JsonValue& value) + { + std::size_t start = mPosition; + + if (mText[mPosition] == '-') + ++mPosition; + + while (mPosition < mText.size() && std::isdigit(static_cast(mText[mPosition]))) + ++mPosition; + + if (mPosition < mText.size() && mText[mPosition] == '.') + { + ++mPosition; + while (mPosition < mText.size() && std::isdigit(static_cast(mText[mPosition]))) + ++mPosition; + } + + if (mPosition < mText.size() && (mText[mPosition] == 'e' || mText[mPosition] == 'E')) + { + ++mPosition; + if (mPosition < mText.size() && (mText[mPosition] == '+' || mText[mPosition] == '-')) + ++mPosition; + while (mPosition < mText.size() && std::isdigit(static_cast(mText[mPosition]))) + ++mPosition; + } + + std::string token = mText.substr(start, mPosition - start); + char* endPtr = nullptr; + double parsed = strtod(token.c_str(), &endPtr); + if (endPtr == token.c_str() || *endPtr != '\0') + { + setError("Invalid JSON number."); + return false; + } + + value = JsonValue(parsed); + return true; + } + + bool parseLiteral(const char* literal, const JsonValue& literalValue, JsonValue& value) + { + std::size_t length = strlen(literal); + if (mText.compare(mPosition, length, literal) != 0) + { + setError("Invalid JSON literal."); + return false; + } + + mPosition += length; + value = literalValue; + return true; + } + + void skipWhitespace() + { + while (mPosition < mText.size() && std::isspace(static_cast(mText[mPosition]))) + ++mPosition; + } + + bool consume(char expected) + { + if (mPosition < mText.size() && mText[mPosition] == expected) + { + ++mPosition; + return true; + } + return false; + } + + void setError(const std::string& error) + { + if (mError.empty()) + mError = error; + } + + const std::string& mText; + std::string& mError; + std::size_t mPosition; +}; + +void SerializeJsonImpl(const JsonValue& value, std::ostringstream& output, bool pretty, int indentLevel) +{ + auto indent = [&](int level) { + if (!pretty) + return; + for (int i = 0; i < level; ++i) + output << " "; + }; + + switch (value.type()) + { + case JsonValue::Type::Null: + output << "null"; + break; + case JsonValue::Type::Boolean: + output << (value.asBoolean() ? "true" : "false"); + break; + case JsonValue::Type::Number: + { + double number = value.asNumber(); + if (std::isfinite(number)) + { + output << std::setprecision(15) << number; + } + else + { + output << "0"; + } + break; + } + case JsonValue::Type::String: + { + output << '"'; + for (char ch : value.asString()) + { + switch (ch) + { + case '"': output << "\\\""; break; + case '\\': output << "\\\\"; break; + case '\b': output << "\\b"; break; + case '\f': output << "\\f"; break; + case '\n': output << "\\n"; break; + case '\r': output << "\\r"; break; + case '\t': output << "\\t"; break; + default: output << ch; break; + } + } + output << '"'; + break; + } + case JsonValue::Type::Array: + { + output << "["; + const std::vector& array = value.asArray(); + if (!array.empty()) + { + if (pretty) + output << "\n"; + for (std::size_t i = 0; i < array.size(); ++i) + { + indent(indentLevel + 1); + SerializeJsonImpl(array[i], output, pretty, indentLevel + 1); + if (i + 1 != array.size()) + output << ","; + if (pretty) + output << "\n"; + } + indent(indentLevel); + } + output << "]"; + break; + } + case JsonValue::Type::Object: + { + output << "{"; + const std::map& object = value.asObject(); + if (!object.empty()) + { + if (pretty) + output << "\n"; + std::size_t index = 0; + for (const auto& item : object) + { + indent(indentLevel + 1); + SerializeJsonImpl(JsonValue(item.first), output, pretty, indentLevel + 1); + output << (pretty ? ": " : ":"); + SerializeJsonImpl(item.second, output, pretty, indentLevel + 1); + if (++index != object.size()) + output << ","; + if (pretty) + output << "\n"; + } + indent(indentLevel); + } + output << "}"; + break; + } + } +} +} + +JsonValue::JsonValue() + : mType(Type::Null), mBooleanValue(false), mNumberValue(0.0) +{ +} + +JsonValue::JsonValue(bool value) + : mType(Type::Boolean), mBooleanValue(value), mNumberValue(0.0) +{ +} + +JsonValue::JsonValue(double value) + : mType(Type::Number), mBooleanValue(false), mNumberValue(value) +{ +} + +JsonValue::JsonValue(const char* value) + : mType(Type::String), mBooleanValue(false), mNumberValue(0.0), mStringValue(value ? value : "") +{ +} + +JsonValue::JsonValue(const std::string& value) + : mType(Type::String), mBooleanValue(false), mNumberValue(0.0), mStringValue(value) +{ +} + +JsonValue JsonValue::MakeArray() +{ + JsonValue value; + value.mType = Type::Array; + return value; +} + +JsonValue JsonValue::MakeObject() +{ + JsonValue value; + value.mType = Type::Object; + return value; +} + +bool JsonValue::asBoolean(bool fallback) const +{ + return mType == Type::Boolean ? mBooleanValue : fallback; +} + +double JsonValue::asNumber(double fallback) const +{ + return mType == Type::Number ? mNumberValue : fallback; +} + +const std::string& JsonValue::asString() const +{ + static const std::string emptyString; + return mType == Type::String ? mStringValue : emptyString; +} + +const std::vector& JsonValue::asArray() const +{ + static const std::vector emptyArray; + return mType == Type::Array ? mArrayValue : emptyArray; +} + +const std::map& JsonValue::asObject() const +{ + static const std::map emptyObject; + return mType == Type::Object ? mObjectValue : emptyObject; +} + +std::vector& JsonValue::array() +{ + if (mType != Type::Array) + { + mType = Type::Array; + mArrayValue.clear(); + } + return mArrayValue; +} + +std::map& JsonValue::object() +{ + if (mType != Type::Object) + { + mType = Type::Object; + mObjectValue.clear(); + } + return mObjectValue; +} + +void JsonValue::pushBack(const JsonValue& value) +{ + array().push_back(value); +} + +void JsonValue::set(const std::string& key, const JsonValue& value) +{ + object()[key] = value; +} + +const JsonValue* JsonValue::find(const std::string& key) const +{ + if (mType != Type::Object) + return nullptr; + + auto iterator = mObjectValue.find(key); + return iterator != mObjectValue.end() ? &iterator->second : nullptr; +} + +bool ParseJson(const std::string& text, JsonValue& value, std::string& error) +{ + error.clear(); + JsonParser parser(text, error); + return parser.parse(value); +} + +std::string SerializeJson(const JsonValue& value, bool pretty) +{ + std::ostringstream output; + SerializeJsonImpl(value, output, pretty, 0); + return output.str(); +} diff --git a/apps/LoopThroughWithOpenGLCompositing/RuntimeJson.h b/apps/LoopThroughWithOpenGLCompositing/RuntimeJson.h new file mode 100644 index 0000000..4adfb97 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/RuntimeJson.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +class JsonValue +{ +public: + enum class Type + { + Null, + Boolean, + Number, + String, + Array, + Object + }; + + JsonValue(); + explicit JsonValue(bool value); + explicit JsonValue(double value); + explicit JsonValue(const char* value); + explicit JsonValue(const std::string& value); + + static JsonValue MakeArray(); + static JsonValue MakeObject(); + + Type type() const { return mType; } + + bool isNull() const { return mType == Type::Null; } + bool isBoolean() const { return mType == Type::Boolean; } + bool isNumber() const { return mType == Type::Number; } + bool isString() const { return mType == Type::String; } + bool isArray() const { return mType == Type::Array; } + bool isObject() const { return mType == Type::Object; } + + bool asBoolean(bool fallback = false) const; + double asNumber(double fallback = 0.0) const; + const std::string& asString() const; + const std::vector& asArray() const; + const std::map& asObject() const; + + std::vector& array(); + std::map& object(); + + void pushBack(const JsonValue& value); + void set(const std::string& key, const JsonValue& value); + + const JsonValue* find(const std::string& key) const; + +private: + Type mType; + bool mBooleanValue; + double mNumberValue; + std::string mStringValue; + std::vector mArrayValue; + std::map mObjectValue; +}; + +bool ParseJson(const std::string& text, JsonValue& value, std::string& error); +std::string SerializeJson(const JsonValue& value, bool pretty = false); diff --git a/apps/LoopThroughWithOpenGLCompositing/VideoFrameTransfer.cpp b/apps/LoopThroughWithOpenGLCompositing/VideoFrameTransfer.cpp index 848d8fb..94f79c1 100644 --- a/apps/LoopThroughWithOpenGLCompositing/VideoFrameTransfer.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/VideoFrameTransfer.cpp @@ -72,6 +72,9 @@ bool VideoFrameTransfer::isNvidiaDvpAvailable() { // Look for supported graphics boards const GLubyte* renderer = glGetString(GL_RENDERER); + if (renderer == NULL) + return false; + bool hasDvp = (strstr((char*)renderer, "Quadro") != NULL); return hasDvp; } @@ -80,6 +83,13 @@ bool VideoFrameTransfer::isAMDPinnedMemoryAvailable() { // GL_AMD_pinned_memory presence indicates GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD buffer target is supported const GLubyte* strExt = glGetString(GL_EXTENSIONS); + if (strExt == NULL) + { + // In a core profile context GL_EXTENSIONS is no longer queryable via glGetString(). + // Treat this as "extension unavailable" for now; the fast-transfer path is optional. + return false; + } + bool hasAMDPinned = (strstr((char*)strExt, "GL_AMD_pinned_memory") != NULL); return hasAMDPinned; } diff --git a/apps/LoopThroughWithOpenGLCompositing/stdafx.h b/apps/LoopThroughWithOpenGLCompositing/stdafx.h index fd91902..bc9dfea 100644 --- a/apps/LoopThroughWithOpenGLCompositing/stdafx.h +++ b/apps/LoopThroughWithOpenGLCompositing/stdafx.h @@ -47,8 +47,14 @@ #include "targetver.h" +#ifndef NOMINMAX +#define NOMINMAX +#endif + #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers // Windows Header Files: +#include +#include #include // C RunTime Header Files diff --git a/apps/LoopThroughWithOpenGLCompositing/video_effect.slang b/apps/LoopThroughWithOpenGLCompositing/video_effect.slang deleted file mode 100644 index a1aed89..0000000 --- a/apps/LoopThroughWithOpenGLCompositing/video_effect.slang +++ /dev/null @@ -1,79 +0,0 @@ -// Source-of-truth shader in Slang. -// The current OpenGL sample still runs the checked-in GLSL fallback because it uses -// legacy fixed-function texture coordinates in its fragment stage. - -struct FragmentInput -{ - float4 position : SV_Position; - float2 texCoord : TEXCOORD0; -}; - -Texture2D UYVYtex; - -float4 rec709YCbCr2rgba(float Y, float Cb, float Cr, float a) -{ - Y = (Y * 256.0 - 16.0) / 219.0; - Cb = (Cb * 256.0 - 16.0) / 224.0 - 0.5; - Cr = (Cr * 256.0 - 16.0) / 224.0 - 0.5; - - float r = Y + 1.5748 * Cr; - float g = Y - 0.1873 * Cb - 0.4681 * Cr; - float b = Y + 1.8556 * Cb; - return float4(r, g, b, a); -} - -float4 bilinear(float4 W, float4 X, float4 Y, float4 Z, float2 weight) -{ - float4 m0 = lerp(W, Z, weight.x); - float4 m1 = lerp(X, Y, weight.x); - return lerp(m0, m1, weight.y); -} - -void textureGatherYUV(Texture2D textureSampler, float2 tc, out float4 W, out float4 X, out float4 Y, out float4 Z) -{ - uint width = 0; - uint height = 0; - textureSampler.GetDimensions(width, height); - - int2 tx = int2(tc * float2(width, height)); - int2 tmin = int2(0, 0); - int2 tmax = int2(int(width), int(height)) - int2(1, 1); - - W = textureSampler.Load(int3(tx, 0)); - X = textureSampler.Load(int3(clamp(tx + int2(0, 1), tmin, tmax), 0)); - Y = textureSampler.Load(int3(clamp(tx + int2(1, 1), tmin, tmax), 0)); - Z = textureSampler.Load(int3(clamp(tx + int2(1, 0), tmin, tmax), 0)); -} - -[shader("fragment")] -float4 fragmentMain(FragmentInput input) : SV_Target -{ - float2 tc = input.texCoord; - float alpha = 0.7; - - float4 macro, macroU, macroR, macroUR; - float4 pixel, pixelR, pixelU, pixelUR; - textureGatherYUV(UYVYtex, tc, macro, macroU, macroUR, macroR); - - uint width = 0; - uint height = 0; - UYVYtex.GetDimensions(width, height); - - float2 off = frac(tc * float2(width, height)); - if (off.x > 0.5) - { - pixel = rec709YCbCr2rgba(macro.a, macro.b, macro.r, alpha); - pixelR = rec709YCbCr2rgba(macroR.g, macroR.b, macroR.r, alpha); - pixelU = rec709YCbCr2rgba(macroU.a, macroU.b, macroU.r, alpha); - pixelUR = rec709YCbCr2rgba(macroUR.g, macroUR.b, macroUR.r, alpha); - } - else - { - pixel = rec709YCbCr2rgba(macro.g, macro.b, macro.r, alpha); - pixelR = rec709YCbCr2rgba(macro.a, macro.b, macro.r, alpha); - pixelU = rec709YCbCr2rgba(macroU.g, macroU.b, macroU.r, alpha); - pixelUR = rec709YCbCr2rgba(macroU.a, macroU.b, macroU.r, alpha); - } - - return bilinear(pixel, pixelU, pixelUR, pixelR, off); -} diff --git a/config/runtime-host.json b/config/runtime-host.json new file mode 100644 index 0000000..e3a315f --- /dev/null +++ b/config/runtime-host.json @@ -0,0 +1,5 @@ +{ + "shaderLibrary": "shaders", + "serverPort": 8080, + "autoReload": true +} diff --git a/shaders/studio-color/shader.json b/shaders/studio-color/shader.json new file mode 100644 index 0000000..ee61b6e --- /dev/null +++ b/shaders/studio-color/shader.json @@ -0,0 +1,50 @@ +{ + "id": "studio-color", + "name": "Studio Color", + "description": "A built-in sample shader package that demonstrates the runtime parameter contract.", + "category": "Built-in", + "entryPoint": "shadeVideo", + "parameters": [ + { + "id": "brightness", + "label": "Brightness", + "type": "float", + "default": 1.0, + "min": 0.0, + "max": 2.0, + "step": 0.01 + }, + { + "id": "offset", + "label": "Offset", + "type": "vec2", + "default": [0.0, 0.0], + "min": [-0.2, -0.2], + "max": [0.2, 0.2], + "step": [0.001, 0.001] + }, + { + "id": "tint", + "label": "Tint", + "type": "color", + "default": [1.0, 1.0, 1.0, 1.0] + }, + { + "id": "invert", + "label": "Invert", + "type": "bool", + "default": false + }, + { + "id": "mode", + "label": "Mode", + "type": "enum", + "default": "normal", + "options": [ + { "value": "normal", "label": "Normal" }, + { "value": "luma", "label": "Luma" }, + { "value": "posterize", "label": "Posterize" } + ] + } + ] +} diff --git a/shaders/studio-color/shader.slang b/shaders/studio-color/shader.slang new file mode 100644 index 0000000..bb8b084 --- /dev/null +++ b/shaders/studio-color/shader.slang @@ -0,0 +1,23 @@ +float4 shadeVideo(ShaderContext context) +{ + float2 uv = clamp(context.uv + offset, float2(0.0, 0.0), float2(1.0, 1.0)); + float4 color = sampleVideo(uv); + + color.rgb *= brightness; + color *= tint; + + if (invert) + color.rgb = 1.0 - color.rgb; + + if (mode == 1) + { + float luma = dot(color.rgb, float3(0.2126, 0.7152, 0.0722)); + color.rgb = float3(luma, luma, luma); + } + else if (mode == 2) + { + color.rgb = floor(color.rgb * 4.0) / 4.0; + } + + return saturate(color); +} diff --git a/ui/app.js b/ui/app.js new file mode 100644 index 0000000..323ccc0 --- /dev/null +++ b/ui/app.js @@ -0,0 +1,212 @@ +const shaderSelect = document.getElementById("shader-select"); +const mixSlider = document.getElementById("mix-slider"); +const bypassToggle = document.getElementById("bypass-toggle"); +const reloadButton = document.getElementById("reload-button"); +const runtimeStatus = document.getElementById("runtime-status"); +const videoStatus = document.getElementById("video-status"); +const compileStatus = document.getElementById("compile-status"); +const parameterForm = document.getElementById("parameter-form"); + +let appState = null; +let websocket = null; + +function createKv(target, values) { + target.innerHTML = ""; + values.forEach(([key, value]) => { + const dt = document.createElement("dt"); + dt.textContent = key; + const dd = document.createElement("dd"); + dd.textContent = value; + target.append(dt, dd); + }); +} + +function postJson(path, payload) { + return fetch(path, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); +} + +function renderParameters(shader) { + parameterForm.innerHTML = ""; + if (!shader) { + return; + } + + shader.parameters.forEach((parameter) => { + const section = document.createElement("section"); + section.className = "parameter"; + + const label = document.createElement("label"); + label.textContent = parameter.label; + section.appendChild(label); + + const valueLabel = document.createElement("div"); + valueLabel.className = "parameter__value"; + + const sendValue = (value) => { + postJson("/api/update-parameter", { + shaderId: shader.id, + parameterId: parameter.id, + value, + }); + }; + + if (parameter.type === "float") { + const range = document.createElement("input"); + range.type = "range"; + range.min = parameter.min?.[0] ?? 0; + range.max = parameter.max?.[0] ?? 1; + range.step = parameter.step?.[0] ?? 0.01; + range.value = parameter.value; + const number = document.createElement("input"); + number.type = "number"; + number.min = range.min; + number.max = range.max; + number.step = range.step; + number.value = parameter.value; + const pair = document.createElement("div"); + pair.className = "parameter__pair"; + pair.append(range, number); + section.append(pair, valueLabel); + const update = (value) => { + valueLabel.textContent = Number(value).toFixed(3); + range.value = value; + number.value = value; + }; + update(parameter.value); + range.addEventListener("input", () => update(range.value)); + range.addEventListener("change", () => sendValue(Number(range.value))); + number.addEventListener("change", () => { + update(number.value); + sendValue(Number(number.value)); + }); + } else if (parameter.type === "vec2" || parameter.type === "color") { + const pair = document.createElement("div"); + pair.className = "parameter__pair"; + const values = parameter.value.slice(); + values.forEach((component, index) => { + const input = document.createElement("input"); + input.type = "number"; + input.step = parameter.step?.[index] ?? 0.01; + input.min = parameter.min?.[index] ?? ""; + input.max = parameter.max?.[index] ?? ""; + input.value = component; + input.addEventListener("change", () => { + values[index] = Number(input.value); + valueLabel.textContent = values.map((value) => Number(value).toFixed(3)).join(", "); + sendValue(values); + }); + pair.appendChild(input); + }); + valueLabel.textContent = values.map((value) => Number(value).toFixed(3)).join(", "); + section.append(pair, valueLabel); + } else if (parameter.type === "bool") { + const toggle = document.createElement("input"); + toggle.type = "checkbox"; + toggle.checked = parameter.value; + valueLabel.textContent = parameter.value ? "Enabled" : "Disabled"; + toggle.addEventListener("change", () => { + valueLabel.textContent = toggle.checked ? "Enabled" : "Disabled"; + sendValue(toggle.checked); + }); + section.append(toggle, valueLabel); + } else if (parameter.type === "enum") { + const select = document.createElement("select"); + parameter.options.forEach((option) => { + const item = document.createElement("option"); + item.value = option.value; + item.textContent = option.label; + if (option.value === parameter.value) { + item.selected = true; + } + select.appendChild(item); + }); + valueLabel.textContent = parameter.value; + select.addEventListener("change", () => { + valueLabel.textContent = select.value; + sendValue(select.value); + }); + section.append(select, valueLabel); + } + + parameterForm.appendChild(section); + }); +} + +function renderState(state) { + appState = state; + const shaders = state.shaders || []; + const activeShaderId = state.runtime.activeShaderId; + const activeShader = shaders.find((shader) => shader.id === activeShaderId) || shaders[0]; + + shaderSelect.innerHTML = ""; + shaders.forEach((shader) => { + const option = document.createElement("option"); + option.value = shader.id; + option.textContent = shader.name; + if (shader.id === activeShaderId) { + option.selected = true; + } + shaderSelect.appendChild(option); + }); + + mixSlider.value = state.runtime.mixAmount ?? 1; + bypassToggle.checked = Boolean(state.runtime.bypass); + compileStatus.textContent = state.runtime.compileMessage || "No compiler output."; + + createKv(runtimeStatus, [ + ["Active Shader", activeShader?.name || "None"], + ["Auto Reload", state.app.autoReload ? "On" : "Off"], + ["Control URL", `http://127.0.0.1:${state.app.serverPort}`], + ["Compile Status", state.runtime.compileSucceeded ? "Ready" : "Error"], + ]); + + createKv(videoStatus, [ + ["Signal", state.video.hasSignal ? "Present" : "Missing"], + ["Mode", state.video.modeName || "Unknown"], + ["Resolution", `${state.video.width || 0} x ${state.video.height || 0}`], + ]); + + renderParameters(activeShader); +} + +async function loadInitialState() { + const response = await fetch("/api/state"); + renderState(await response.json()); +} + +function connectWebSocket() { + const protocol = location.protocol === "https:" ? "wss" : "ws"; + websocket = new WebSocket(`${protocol}://${location.host}/ws`); + websocket.onmessage = (event) => { + try { + renderState(JSON.parse(event.data)); + } catch (error) { + console.error("Failed to parse state update", error); + } + }; + websocket.onclose = () => { + setTimeout(connectWebSocket, 1000); + }; +} + +shaderSelect.addEventListener("change", () => { + postJson("/api/select-shader", { shaderId: shaderSelect.value }); +}); + +mixSlider.addEventListener("change", () => { + postJson("/api/set-mix", { mixAmount: Number(mixSlider.value) }); +}); + +bypassToggle.addEventListener("change", () => { + postJson("/api/set-bypass", { bypass: bypassToggle.checked }); +}); + +reloadButton.addEventListener("click", () => { + postJson("/api/reload", {}); +}); + +loadInitialState().then(connectWebSocket); diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..48d7bcc --- /dev/null +++ b/ui/index.html @@ -0,0 +1,52 @@ + + + + + + Video Shader Host + + + +
+
+
+ + +
+
+ + +
+ + +
+ +
+
+

Runtime

+
+
+
+

Video

+
+
+
+

Compiler

+

+      
+
+ +
+
+

Parameters

+
+
+
+
+ + + + diff --git a/ui/styles.css b/ui/styles.css new file mode 100644 index 0000000..74e612b --- /dev/null +++ b/ui/styles.css @@ -0,0 +1,160 @@ +:root { + color-scheme: dark; + font-family: "Segoe UI", sans-serif; + background: #111318; + color: #edf1f7; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: #111318; +} + +.layout { + max-width: 1200px; + margin: 0 auto; + padding: 24px; + display: grid; + gap: 20px; +} + +.toolbar, +.status-grid, +.parameter-grid { + display: grid; + gap: 16px; +} + +.toolbar { + grid-template-columns: minmax(220px, 2fr) minmax(220px, 3fr) auto auto; + align-items: end; +} + +.toolbar__group { + display: grid; + gap: 8px; +} + +.toolbar__group--wide { + min-width: 260px; +} + +.panel { + background: #181c24; + border: 1px solid #2a3140; + border-radius: 8px; + padding: 16px; +} + +.panel--full { + grid-column: 1 / -1; +} + +.panel__header { + margin-bottom: 12px; +} + +.status-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.kv { + display: grid; + grid-template-columns: 160px 1fr; + gap: 8px 12px; + margin: 0; +} + +.kv dt { + color: #94a4c2; +} + +.kv dd { + margin: 0; +} + +.parameter-grid { + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} + +.parameter { + display: grid; + gap: 8px; + padding: 12px; + border: 1px solid #2a3140; + border-radius: 8px; + background: #131720; +} + +.parameter__value { + color: #94a4c2; + font-size: 12px; +} + +.parameter__pair { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +label, +h2 { + margin: 0; + font-size: 14px; + font-weight: 600; +} + +input, +select, +button, +pre { + font: inherit; +} + +input[type="range"] { + width: 100%; +} + +input[type="number"], +select, +button { + width: 100%; + min-height: 36px; + border-radius: 6px; + border: 1px solid #38445b; + background: #0f131a; + color: inherit; + padding: 8px 10px; +} + +button { + cursor: pointer; + background: #22314a; +} + +.toggle { + display: inline-flex; + align-items: center; + gap: 10px; + min-height: 36px; +} + +pre { + margin: 0; + white-space: pre-wrap; + color: #c9d5ea; +} + +@media (max-width: 900px) { + .toolbar { + grid-template-columns: 1fr; + } + + .status-grid { + grid-template-columns: 1fr; + } +}