Working
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,3 +41,4 @@ build.ninja
|
|||||||
*.log
|
*.log
|
||||||
*.dmp
|
*.dmp
|
||||||
*.tmp
|
*.tmp
|
||||||
|
/runtime/
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
|||||||
|
|
||||||
set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/LoopThroughWithOpenGLCompositing")
|
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(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")
|
if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp")
|
||||||
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
|
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()
|
endif()
|
||||||
|
|
||||||
add_executable(LoopThroughWithOpenGLCompositing WIN32
|
add_executable(LoopThroughWithOpenGLCompositing WIN32
|
||||||
|
"${APP_DIR}/ControlServer.cpp"
|
||||||
|
"${APP_DIR}/ControlServer.h"
|
||||||
"${APP_DIR}/DeckLinkAPI_i.c"
|
"${APP_DIR}/DeckLinkAPI_i.c"
|
||||||
"${APP_DIR}/GLExtensions.cpp"
|
"${APP_DIR}/GLExtensions.cpp"
|
||||||
"${APP_DIR}/GLExtensions.h"
|
"${APP_DIR}/GLExtensions.h"
|
||||||
@@ -28,12 +29,15 @@ add_executable(LoopThroughWithOpenGLCompositing WIN32
|
|||||||
"${APP_DIR}/OpenGLComposite.cpp"
|
"${APP_DIR}/OpenGLComposite.cpp"
|
||||||
"${APP_DIR}/OpenGLComposite.h"
|
"${APP_DIR}/OpenGLComposite.h"
|
||||||
"${APP_DIR}/resource.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.cpp"
|
||||||
"${APP_DIR}/stdafx.h"
|
"${APP_DIR}/stdafx.h"
|
||||||
"${APP_DIR}/targetver.h"
|
"${APP_DIR}/targetver.h"
|
||||||
"${APP_DIR}/VideoFrameTransfer.cpp"
|
"${APP_DIR}/VideoFrameTransfer.cpp"
|
||||||
"${APP_DIR}/VideoFrameTransfer.h"
|
"${APP_DIR}/VideoFrameTransfer.h"
|
||||||
"${SHADER_SLANG_SOURCE}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
||||||
@@ -49,6 +53,9 @@ target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
|
|||||||
dvp.lib
|
dvp.lib
|
||||||
opengl32
|
opengl32
|
||||||
glu32
|
glu32
|
||||||
|
Ws2_32
|
||||||
|
Crypt32
|
||||||
|
Advapi32
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
|
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
|
||||||
@@ -56,11 +63,6 @@ target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
|
|||||||
UNICODE
|
UNICODE
|
||||||
)
|
)
|
||||||
|
|
||||||
set_source_files_properties(
|
|
||||||
"${SHADER_SLANG_SOURCE}"
|
|
||||||
PROPERTIES HEADER_FILE_ONLY TRUE
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
target_compile_options(LoopThroughWithOpenGLCompositing PRIVATE /W3)
|
target_compile_options(LoopThroughWithOpenGLCompositing PRIVATE /W3)
|
||||||
endif()
|
endif()
|
||||||
@@ -72,6 +74,8 @@ add_custom_command(TARGET LoopThroughWithOpenGLCompositing POST_BUILD
|
|||||||
)
|
)
|
||||||
|
|
||||||
source_group(TREE "${APP_DIR}" FILES
|
source_group(TREE "${APP_DIR}" FILES
|
||||||
|
"${APP_DIR}/ControlServer.cpp"
|
||||||
|
"${APP_DIR}/ControlServer.h"
|
||||||
"${APP_DIR}/DeckLinkAPI_i.c"
|
"${APP_DIR}/DeckLinkAPI_i.c"
|
||||||
"${APP_DIR}/GLExtensions.cpp"
|
"${APP_DIR}/GLExtensions.cpp"
|
||||||
"${APP_DIR}/GLExtensions.h"
|
"${APP_DIR}/GLExtensions.h"
|
||||||
@@ -81,10 +85,13 @@ source_group(TREE "${APP_DIR}" FILES
|
|||||||
"${APP_DIR}/OpenGLComposite.cpp"
|
"${APP_DIR}/OpenGLComposite.cpp"
|
||||||
"${APP_DIR}/OpenGLComposite.h"
|
"${APP_DIR}/OpenGLComposite.h"
|
||||||
"${APP_DIR}/resource.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.cpp"
|
||||||
"${APP_DIR}/stdafx.h"
|
"${APP_DIR}/stdafx.h"
|
||||||
"${APP_DIR}/targetver.h"
|
"${APP_DIR}/targetver.h"
|
||||||
"${APP_DIR}/VideoFrameTransfer.cpp"
|
"${APP_DIR}/VideoFrameTransfer.cpp"
|
||||||
"${APP_DIR}/VideoFrameTransfer.h"
|
"${APP_DIR}/VideoFrameTransfer.h"
|
||||||
"${SHADER_SLANG_SOURCE}"
|
|
||||||
)
|
)
|
||||||
|
|||||||
55
SHADER_CONTRACT.md
Normal file
55
SHADER_CONTRACT.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Shader Package Contract
|
||||||
|
|
||||||
|
Each shader package lives under `shaders/<id>/` 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.
|
||||||
454
apps/LoopThroughWithOpenGLCompositing/ControlServer.cpp
Normal file
454
apps/LoopThroughWithOpenGLCompositing/ControlServer.cpp
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#include "ControlServer.h"
|
||||||
|
|
||||||
|
#include "RuntimeJson.h"
|
||||||
|
|
||||||
|
#include <Wincrypt.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#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<char>(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<u_short>(preferredPort + offset));
|
||||||
|
if (bind(mListenSocket, reinterpret_cast<sockaddr*>(&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<std::mutex> 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<std::mutex> 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<sockaddr*>(&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<int>(payload.size()), 0) == static_cast<int>(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<int>(payload.size()), 0);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> 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<char>(0x81));
|
||||||
|
if (payload.size() <= 125)
|
||||||
|
{
|
||||||
|
frame.push_back(static_cast<char>(payload.size()));
|
||||||
|
}
|
||||||
|
else if (payload.size() <= 65535)
|
||||||
|
{
|
||||||
|
frame.push_back(126);
|
||||||
|
frame.push_back(static_cast<char>((payload.size() >> 8) & 0xFF));
|
||||||
|
frame.push_back(static_cast<char>(payload.size() & 0xFF));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
frame.push_back(127);
|
||||||
|
for (int shift = 56; shift >= 0; shift -= 8)
|
||||||
|
frame.push_back(static_cast<char>((payload.size() >> shift) & 0xFF));
|
||||||
|
}
|
||||||
|
frame.append(payload);
|
||||||
|
|
||||||
|
return send(clientSocket, frame.data(), static_cast<int>(frame.size()), 0) == static_cast<int>(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 "<!doctype html><title>Missing UI asset</title><p>UI asset missing.</p>";
|
||||||
|
|
||||||
|
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<const BYTE*>(combined.data()), static_cast<DWORD>(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);
|
||||||
|
}
|
||||||
68
apps/LoopThroughWithOpenGLCompositing/ControlServer.h
Normal file
68
apps/LoopThroughWithOpenGLCompositing/ControlServer.h
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <winsock2.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class ControlServer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Callbacks
|
||||||
|
{
|
||||||
|
std::function<std::string()> getStateJson;
|
||||||
|
std::function<bool(const std::string&, std::string&)> selectShader;
|
||||||
|
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateParameter;
|
||||||
|
std::function<bool(bool, std::string&)> setBypass;
|
||||||
|
std::function<bool(double, std::string&)> setMixAmount;
|
||||||
|
std::function<bool(std::string&)> 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<bool> mRunning;
|
||||||
|
mutable std::mutex mMutex;
|
||||||
|
std::vector<ClientConnection> mClients;
|
||||||
|
};
|
||||||
@@ -44,17 +44,17 @@
|
|||||||
|
|
||||||
#include "GLExtensions.h"
|
#include "GLExtensions.h"
|
||||||
|
|
||||||
PFNGLGENFRAMEBUFFERSEXTPROC glGenFramebuffersEXT;
|
PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers;
|
||||||
PFNGLGENRENDERBUFFERSEXTPROC glGenRenderbuffersEXT;
|
PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers;
|
||||||
PFNGLBINDRENDERBUFFEREXTPROC glBindRenderbufferEXT;
|
PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer;
|
||||||
PFNGLRENDERBUFFERSTORAGEEXTPROC glRenderbufferStorageEXT;
|
PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage;
|
||||||
PFNGLDELETEFRAMEBUFFERSEXTPROC glDeleteFramebuffersEXT;
|
PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers;
|
||||||
PFNGLDELETERENDERBUFFERSEXTPROC glDeleteRenderbuffersEXT;
|
PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers;
|
||||||
PFNGLBINDFRAMEBUFFEREXTPROC glBindFramebufferEXT;
|
PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer;
|
||||||
PFNGLFRAMEBUFFERTEXTURE2DEXTPROC glFramebufferTexture2DEXT;
|
PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D;
|
||||||
PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC glFramebufferRenderbufferEXT;
|
PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer;
|
||||||
PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC glCheckFramebufferStatusEXT;
|
PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus;
|
||||||
PFNGLBLITFRAMEBUFFEREXTPROC glBlitFramebufferEXT;
|
PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer;
|
||||||
PFNGLFENCESYNCPROC glFenceSync;
|
PFNGLFENCESYNCPROC glFenceSync;
|
||||||
PFNGLCLIENTWAITSYNCPROC glClientWaitSync;
|
PFNGLCLIENTWAITSYNCPROC glClientWaitSync;
|
||||||
PFNGLDELETESYNCPROC glDeleteSync;
|
PFNGLDELETESYNCPROC glDeleteSync;
|
||||||
@@ -62,6 +62,12 @@ PFNGLGENBUFFERSPROC glGenBuffers;
|
|||||||
PFNGLDELETEBUFFERSPROC glDeleteBuffers;
|
PFNGLDELETEBUFFERSPROC glDeleteBuffers;
|
||||||
PFNGLBINDBUFFERPROC glBindBuffer;
|
PFNGLBINDBUFFERPROC glBindBuffer;
|
||||||
PFNGLBUFFERDATAPROC glBufferData;
|
PFNGLBUFFERDATAPROC glBufferData;
|
||||||
|
PFNGLBUFFERSUBDATAPROC glBufferSubData;
|
||||||
|
PFNGLBINDBUFFERBASEPROC glBindBufferBase;
|
||||||
|
PFNGLACTIVETEXTUREPROC glActiveTexture;
|
||||||
|
PFNGLGENVERTEXARRAYSPROC glGenVertexArrays;
|
||||||
|
PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays;
|
||||||
|
PFNGLBINDVERTEXARRAYPROC glBindVertexArray;
|
||||||
PFNGLCREATESHADERPROC glCreateShader;
|
PFNGLCREATESHADERPROC glCreateShader;
|
||||||
PFNGLDELETESHADERPROC glDeleteShader;
|
PFNGLDELETESHADERPROC glDeleteShader;
|
||||||
PFNGLDELETEPROGRAMPROC glDeleteProgram;
|
PFNGLDELETEPROGRAMPROC glDeleteProgram;
|
||||||
@@ -78,20 +84,44 @@ PFNGLUSEPROGRAMPROC glUseProgram;
|
|||||||
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;
|
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;
|
||||||
PFNGLUNIFORM1IPROC glUniform1i;
|
PFNGLUNIFORM1IPROC glUniform1i;
|
||||||
PFNGLUNIFORM1FPROC glUniform1f;
|
PFNGLUNIFORM1FPROC glUniform1f;
|
||||||
|
PFNGLUNIFORM2FPROC glUniform2f;
|
||||||
|
PFNGLUNIFORM4FPROC glUniform4f;
|
||||||
|
|
||||||
bool ResolveGLExtensions()
|
bool ResolveGLExtensions()
|
||||||
{
|
{
|
||||||
glGenFramebuffersEXT = (PFNGLGENFRAMEBUFFERSEXTPROC) wglGetProcAddress("glGenFramebuffersEXT");
|
glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC) wglGetProcAddress("glGenFramebuffers");
|
||||||
glGenRenderbuffersEXT = (PFNGLGENRENDERBUFFERSEXTPROC) wglGetProcAddress("glGenRenderbuffersEXT");
|
if (!glGenFramebuffers)
|
||||||
glBindRenderbufferEXT = (PFNGLBINDRENDERBUFFEREXTPROC) wglGetProcAddress("glBindRenderbufferEXT");
|
glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC) wglGetProcAddress("glGenFramebuffersEXT");
|
||||||
glRenderbufferStorageEXT = (PFNGLRENDERBUFFERSTORAGEEXTPROC) wglGetProcAddress("glRenderbufferStorageEXT");
|
glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC) wglGetProcAddress("glGenRenderbuffers");
|
||||||
glDeleteFramebuffersEXT = (PFNGLDELETEFRAMEBUFFERSEXTPROC) wglGetProcAddress("glDeleteFramebuffersEXT");
|
if (!glGenRenderbuffers)
|
||||||
glDeleteRenderbuffersEXT = (PFNGLDELETERENDERBUFFERSEXTPROC) wglGetProcAddress("glDeleteRenderbuffersEXT");
|
glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC) wglGetProcAddress("glGenRenderbuffersEXT");
|
||||||
glBindFramebufferEXT = (PFNGLBINDFRAMEBUFFEREXTPROC) wglGetProcAddress("glBindFramebufferEXT");
|
glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC) wglGetProcAddress("glBindRenderbuffer");
|
||||||
glFramebufferTexture2DEXT = (PFNGLFRAMEBUFFERTEXTURE2DEXTPROC) wglGetProcAddress("glFramebufferTexture2DEXT");
|
if (!glBindRenderbuffer)
|
||||||
glFramebufferRenderbufferEXT = (PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC) wglGetProcAddress("glFramebufferRenderbufferEXT");
|
glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC) wglGetProcAddress("glBindRenderbufferEXT");
|
||||||
glCheckFramebufferStatusEXT = (PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC) wglGetProcAddress("glCheckFramebufferStatusEXT");
|
glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC) wglGetProcAddress("glRenderbufferStorage");
|
||||||
glBlitFramebufferEXT = (PFNGLBLITFRAMEBUFFEREXTPROC) wglGetProcAddress("glBlitFramebufferEXT");
|
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");
|
glFenceSync = (PFNGLFENCESYNCPROC) wglGetProcAddress("glFenceSync");
|
||||||
glClientWaitSync = (PFNGLCLIENTWAITSYNCPROC) wglGetProcAddress("glClientWaitSync");
|
glClientWaitSync = (PFNGLCLIENTWAITSYNCPROC) wglGetProcAddress("glClientWaitSync");
|
||||||
glDeleteSync = (PFNGLDELETESYNCPROC) wglGetProcAddress("glDeleteSync");
|
glDeleteSync = (PFNGLDELETESYNCPROC) wglGetProcAddress("glDeleteSync");
|
||||||
@@ -99,6 +129,12 @@ bool ResolveGLExtensions()
|
|||||||
glDeleteBuffers = (PFNGLDELETEBUFFERSPROC) wglGetProcAddress("glDeleteBuffers");
|
glDeleteBuffers = (PFNGLDELETEBUFFERSPROC) wglGetProcAddress("glDeleteBuffers");
|
||||||
glBindBuffer = (PFNGLBINDBUFFERPROC) wglGetProcAddress("glBindBuffer");
|
glBindBuffer = (PFNGLBINDBUFFERPROC) wglGetProcAddress("glBindBuffer");
|
||||||
glBufferData = (PFNGLBUFFERDATAPROC) wglGetProcAddress("glBufferData");
|
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");
|
glCreateShader = (PFNGLCREATESHADERPROC) wglGetProcAddress("glCreateShader");
|
||||||
glDeleteShader = (PFNGLDELETESHADERPROC) wglGetProcAddress("glDeleteShader");
|
glDeleteShader = (PFNGLDELETESHADERPROC) wglGetProcAddress("glDeleteShader");
|
||||||
glDeleteProgram = (PFNGLDELETEPROGRAMPROC) wglGetProcAddress("glDeleteProgram");
|
glDeleteProgram = (PFNGLDELETEPROGRAMPROC) wglGetProcAddress("glDeleteProgram");
|
||||||
@@ -115,18 +151,20 @@ bool ResolveGLExtensions()
|
|||||||
glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC) wglGetProcAddress("glGetUniformLocation");
|
glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC) wglGetProcAddress("glGetUniformLocation");
|
||||||
glUniform1i = (PFNGLUNIFORM1IPROC) wglGetProcAddress("glUniform1i");
|
glUniform1i = (PFNGLUNIFORM1IPROC) wglGetProcAddress("glUniform1i");
|
||||||
glUniform1f = (PFNGLUNIFORM1FPROC) wglGetProcAddress("glUniform1f");
|
glUniform1f = (PFNGLUNIFORM1FPROC) wglGetProcAddress("glUniform1f");
|
||||||
|
glUniform2f = (PFNGLUNIFORM2FPROC) wglGetProcAddress("glUniform2f");
|
||||||
|
glUniform4f = (PFNGLUNIFORM4FPROC) wglGetProcAddress("glUniform4f");
|
||||||
|
|
||||||
return glGenFramebuffersEXT
|
return glGenFramebuffers
|
||||||
&& glGenRenderbuffersEXT
|
&& glGenRenderbuffers
|
||||||
&& glBindRenderbufferEXT
|
&& glBindRenderbuffer
|
||||||
&& glRenderbufferStorageEXT
|
&& glRenderbufferStorage
|
||||||
&& glDeleteFramebuffersEXT
|
&& glDeleteFramebuffers
|
||||||
&& glDeleteRenderbuffersEXT
|
&& glDeleteRenderbuffers
|
||||||
&& glBindFramebufferEXT
|
&& glBindFramebuffer
|
||||||
&& glFramebufferTexture2DEXT
|
&& glFramebufferTexture2D
|
||||||
&& glFramebufferRenderbufferEXT
|
&& glFramebufferRenderbuffer
|
||||||
&& glCheckFramebufferStatusEXT
|
&& glCheckFramebufferStatus
|
||||||
&& glBlitFramebufferEXT
|
&& glBlitFramebuffer
|
||||||
&& glFenceSync
|
&& glFenceSync
|
||||||
&& glClientWaitSync
|
&& glClientWaitSync
|
||||||
&& glDeleteSync
|
&& glDeleteSync
|
||||||
@@ -134,6 +172,12 @@ bool ResolveGLExtensions()
|
|||||||
&& glDeleteBuffers
|
&& glDeleteBuffers
|
||||||
&& glBindBuffer
|
&& glBindBuffer
|
||||||
&& glBufferData
|
&& glBufferData
|
||||||
|
&& glBufferSubData
|
||||||
|
&& glBindBufferBase
|
||||||
|
&& glActiveTexture
|
||||||
|
&& glGenVertexArrays
|
||||||
|
&& glDeleteVertexArrays
|
||||||
|
&& glBindVertexArray
|
||||||
&& glCreateShader
|
&& glCreateShader
|
||||||
&& glDeleteShader
|
&& glDeleteShader
|
||||||
&& glDeleteProgram
|
&& glDeleteProgram
|
||||||
@@ -150,5 +194,7 @@ bool ResolveGLExtensions()
|
|||||||
&& glGetUniformLocation
|
&& glGetUniformLocation
|
||||||
&& glUniform1i
|
&& glUniform1i
|
||||||
&& glUniform1f
|
&& glUniform1f
|
||||||
|
&& glUniform2f
|
||||||
|
&& glUniform4f
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,8 @@
|
|||||||
#define GL_STREAM_READ 0x88E1
|
#define GL_STREAM_READ 0x88E1
|
||||||
#define GL_STREAM_COPY 0x88E2
|
#define GL_STREAM_COPY 0x88E2
|
||||||
#define GL_DYNAMIC_DRAW 0x88E8
|
#define GL_DYNAMIC_DRAW 0x88E8
|
||||||
|
#define GL_UNIFORM_BUFFER 0x8A11
|
||||||
|
#define GL_RGBA8 0x8058
|
||||||
#define GL_ARRAY_BUFFER 0x8892
|
#define GL_ARRAY_BUFFER 0x8892
|
||||||
#define GL_PIXEL_PACK_BUFFER 0x88EB
|
#define GL_PIXEL_PACK_BUFFER 0x88EB
|
||||||
#define GL_PIXEL_UNPACK_BUFFER 0x88EC
|
#define GL_PIXEL_UNPACK_BUFFER 0x88EC
|
||||||
@@ -71,6 +73,12 @@
|
|||||||
#define GL_COLOR_ATTACHMENT0_EXT 0x8CE0
|
#define GL_COLOR_ATTACHMENT0_EXT 0x8CE0
|
||||||
#define GL_READ_FRAMEBUFFER 0x8CA8
|
#define GL_READ_FRAMEBUFFER 0x8CA8
|
||||||
#define GL_DRAW_FRAMEBUFFER 0x8CA9
|
#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_DEPTH_ATTACHMENT_EXT 0x8D00
|
||||||
#define GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD 0x9160
|
#define GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD 0x9160
|
||||||
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
|
#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 PFNGLUSEPROGRAMPROC) (GLuint program);
|
||||||
typedef void (APIENTRYP PFNGLUNIFORM1FPROC) (GLint location, GLfloat v0);
|
typedef void (APIENTRYP PFNGLUNIFORM1FPROC) (GLint location, GLfloat v0);
|
||||||
typedef void (APIENTRYP PFNGLUNIFORM1IPROC) (GLint location, GLint 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 GLsync (APIENTRYP PFNGLFENCESYNCPROC) (GLenum condition, GLbitfield flags);
|
||||||
typedef void (APIENTRYP PFNGLDELETESYNCPROC) (GLsync sync);
|
typedef void (APIENTRYP PFNGLDELETESYNCPROC) (GLsync sync);
|
||||||
typedef GLenum (APIENTRYP PFNGLCLIENTWAITSYNCPROC) (GLsync sync, GLbitfield flags, GLuint64 timeout);
|
typedef GLenum (APIENTRYP PFNGLCLIENTWAITSYNCPROC) (GLsync sync, GLbitfield flags, GLuint64 timeout);
|
||||||
typedef void (APIENTRYP PFNGLBINDRENDERBUFFEREXTPROC) (GLenum target, GLuint renderbuffer);
|
typedef void (APIENTRYP PFNGLBINDRENDERBUFFERPROC) (GLenum target, GLuint renderbuffer);
|
||||||
typedef void (APIENTRYP PFNGLDELETERENDERBUFFERSEXTPROC) (GLsizei n, const GLuint *renderbuffers);
|
typedef void (APIENTRYP PFNGLDELETERENDERBUFFERSPROC) (GLsizei n, const GLuint *renderbuffers);
|
||||||
typedef void (APIENTRYP PFNGLGENRENDERBUFFERSEXTPROC) (GLsizei n, GLuint *renderbuffers);
|
typedef void (APIENTRYP PFNGLGENRENDERBUFFERSPROC) (GLsizei n, GLuint *renderbuffers);
|
||||||
typedef void (APIENTRYP PFNGLRENDERBUFFERSTORAGEEXTPROC) (GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
|
typedef void (APIENTRYP PFNGLRENDERBUFFERSTORAGEPROC) (GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
|
||||||
typedef void (APIENTRYP PFNGLBINDFRAMEBUFFEREXTPROC) (GLenum target, GLuint framebuffer);
|
typedef void (APIENTRYP PFNGLBINDFRAMEBUFFERPROC) (GLenum target, GLuint framebuffer);
|
||||||
typedef void (APIENTRYP PFNGLDELETEFRAMEBUFFERSEXTPROC) (GLsizei n, const GLuint *framebuffers);
|
typedef void (APIENTRYP PFNGLDELETEFRAMEBUFFERSPROC) (GLsizei n, const GLuint *framebuffers);
|
||||||
typedef void (APIENTRYP PFNGLGENFRAMEBUFFERSEXTPROC) (GLsizei n, GLuint *framebuffers);
|
typedef void (APIENTRYP PFNGLGENFRAMEBUFFERSPROC) (GLsizei n, GLuint *framebuffers);
|
||||||
typedef GLenum (APIENTRYP PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC) (GLenum target);
|
typedef GLenum (APIENTRYP PFNGLCHECKFRAMEBUFFERSTATUSPROC) (GLenum target);
|
||||||
typedef void (APIENTRYP PFNGLFRAMEBUFFERTEXTURE2DEXTPROC) (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
|
typedef void (APIENTRYP PFNGLFRAMEBUFFERTEXTURE2DPROC) (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 PFNGLFRAMEBUFFERRENDERBUFFERPROC) (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 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 PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers;
|
||||||
extern PFNGLGENRENDERBUFFERSEXTPROC glGenRenderbuffersEXT;
|
extern PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers;
|
||||||
extern PFNGLBINDRENDERBUFFEREXTPROC glBindRenderbufferEXT;
|
extern PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer;
|
||||||
extern PFNGLRENDERBUFFERSTORAGEEXTPROC glRenderbufferStorageEXT;
|
extern PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage;
|
||||||
extern PFNGLDELETEFRAMEBUFFERSEXTPROC glDeleteFramebuffersEXT;
|
extern PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers;
|
||||||
extern PFNGLDELETERENDERBUFFERSEXTPROC glDeleteRenderbuffersEXT;
|
extern PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers;
|
||||||
extern PFNGLBINDFRAMEBUFFEREXTPROC glBindFramebufferEXT;
|
extern PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer;
|
||||||
extern PFNGLFRAMEBUFFERTEXTURE2DEXTPROC glFramebufferTexture2DEXT;
|
extern PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D;
|
||||||
extern PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC glFramebufferRenderbufferEXT;
|
extern PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer;
|
||||||
extern PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC glCheckFramebufferStatusEXT;
|
extern PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus;
|
||||||
extern PFNGLBLITFRAMEBUFFEREXTPROC glBlitFramebufferEXT;
|
extern PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer;
|
||||||
extern PFNGLFENCESYNCPROC glFenceSync;
|
extern PFNGLFENCESYNCPROC glFenceSync;
|
||||||
extern PFNGLCLIENTWAITSYNCPROC glClientWaitSync;
|
extern PFNGLCLIENTWAITSYNCPROC glClientWaitSync;
|
||||||
extern PFNGLDELETESYNCPROC glDeleteSync;
|
extern PFNGLDELETESYNCPROC glDeleteSync;
|
||||||
@@ -135,6 +151,12 @@ extern PFNGLGENBUFFERSPROC glGenBuffers;
|
|||||||
extern PFNGLDELETEBUFFERSPROC glDeleteBuffers;
|
extern PFNGLDELETEBUFFERSPROC glDeleteBuffers;
|
||||||
extern PFNGLBINDBUFFERPROC glBindBuffer;
|
extern PFNGLBINDBUFFERPROC glBindBuffer;
|
||||||
extern PFNGLBUFFERDATAPROC glBufferData;
|
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 PFNGLCREATESHADERPROC glCreateShader;
|
||||||
extern PFNGLDELETESHADERPROC glDeleteShader;
|
extern PFNGLDELETESHADERPROC glDeleteShader;
|
||||||
extern PFNGLDELETEPROGRAMPROC glDeleteProgram;
|
extern PFNGLDELETEPROGRAMPROC glDeleteProgram;
|
||||||
@@ -151,6 +173,8 @@ extern PFNGLUSEPROGRAMPROC glUseProgram;
|
|||||||
extern PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;
|
extern PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;
|
||||||
extern PFNGLUNIFORM1IPROC glUniform1i;
|
extern PFNGLUNIFORM1IPROC glUniform1i;
|
||||||
extern PFNGLUNIFORM1FPROC glUniform1f;
|
extern PFNGLUNIFORM1FPROC glUniform1f;
|
||||||
|
extern PFNGLUNIFORM2FPROC glUniform2f;
|
||||||
|
extern PFNGLUNIFORM4FPROC glUniform4f;
|
||||||
|
|
||||||
bool ResolveGLExtensions();
|
bool ResolveGLExtensions();
|
||||||
|
|
||||||
|
|||||||
@@ -46,11 +46,41 @@
|
|||||||
#include "resource.h"
|
#include "resource.h"
|
||||||
#include "OpenGLComposite.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
|
#define MAX_LOADSTRING 100
|
||||||
|
|
||||||
// Declaration for Window procedure
|
// Declaration for Window procedure
|
||||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
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
|
// Select the pixel format for a given device context
|
||||||
void SetDCPixelFormat(HDC hDC)
|
void SetDCPixelFormat(HDC hDC)
|
||||||
@@ -82,6 +112,38 @@ void SetDCPixelFormat(HDC hDC)
|
|||||||
SetPixelFormat(hDC, nPixelFormat, &pfd);
|
SetPixelFormat(hDC, nPixelFormat, &pfd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HGLRC CreateModernOpenGLContext(HDC hDC)
|
||||||
|
{
|
||||||
|
PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB =
|
||||||
|
reinterpret_cast<PFNWGLCREATECONTEXTATTRIBSARBPROC>(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)
|
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
|
||||||
{
|
{
|
||||||
MSG msg; // Windows message structure
|
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
|
// Window creation, setup for OpenGL context
|
||||||
case WM_CREATE:
|
case WM_CREATE:
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
// Store the device context
|
// Store the device context
|
||||||
hDC = GetDC(hWnd);
|
hDC = GetDC(hWnd);
|
||||||
|
|
||||||
@@ -155,6 +220,19 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
hRC = wglCreateContext(hDC);
|
hRC = wglCreateContext(hDC);
|
||||||
wglMakeCurrent(hDC, hRC);
|
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
|
// Initialize COM
|
||||||
HRESULT result;
|
HRESULT result;
|
||||||
result = CoInitialize(NULL);
|
result = CoInitialize(NULL);
|
||||||
@@ -180,12 +258,27 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
pOpenGLComposite = NULL;
|
pOpenGLComposite = NULL;
|
||||||
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
ShowUnhandledExceptionMessage("Startup failed while creating the OpenGL/DeckLink runtime.");
|
||||||
|
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case WM_DESTROY:
|
case WM_DESTROY:
|
||||||
if (pOpenGLComposite)
|
try
|
||||||
{
|
{
|
||||||
pOpenGLComposite->Stop();
|
if (pOpenGLComposite)
|
||||||
delete pOpenGLComposite;
|
{
|
||||||
|
pOpenGLComposite->Stop();
|
||||||
|
delete pOpenGLComposite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
ShowUnhandledExceptionMessage("Shutdown failed while tearing down the OpenGL/DeckLink runtime.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deselect the current rendering context and delete it
|
// Deselect the current rendering context and delete it
|
||||||
@@ -197,24 +290,46 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case WM_SIZE:
|
case WM_SIZE:
|
||||||
if (pOpenGLComposite)
|
try
|
||||||
pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam));
|
{
|
||||||
|
if (pOpenGLComposite)
|
||||||
|
pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam));
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
ShowUnhandledExceptionMessage("Resize failed inside the OpenGL runtime.");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WM_PAINT:
|
case WM_PAINT:
|
||||||
wglMakeCurrent(hDC, hRC);
|
try
|
||||||
|
{
|
||||||
|
wglMakeCurrent(hDC, hRC);
|
||||||
|
|
||||||
if (pOpenGLComposite)
|
if (pOpenGLComposite)
|
||||||
pOpenGLComposite->paintGL();
|
pOpenGLComposite->paintGL();
|
||||||
|
|
||||||
wglMakeCurrent( NULL, NULL );
|
wglMakeCurrent( NULL, NULL );
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
wglMakeCurrent( NULL, NULL );
|
||||||
|
ShowUnhandledExceptionMessage("Paint failed inside the OpenGL runtime.");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WM_KEYDOWN:
|
case WM_KEYDOWN:
|
||||||
if (pOpenGLComposite && (wParam == 'R' || wParam == 'r'))
|
try
|
||||||
{
|
{
|
||||||
pOpenGLComposite->ReloadShader();
|
if (pOpenGLComposite && (wParam == 'R' || wParam == 'r'))
|
||||||
InvalidateRect(hWnd, NULL, FALSE);
|
{
|
||||||
|
pOpenGLComposite->ReloadShader();
|
||||||
|
InvalidateRect(hWnd, NULL, FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
ShowUnhandledExceptionMessage("Shader reload failed inside the OpenGL runtime.");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -38,12 +38,13 @@
|
|||||||
** -LICENSE-END-
|
** -LICENSE-END-
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include "ControlServer.h"
|
||||||
#include "OpenGLComposite.h"
|
#include "OpenGLComposite.h"
|
||||||
#include "GLExtensions.h"
|
#include "GLExtensions.h"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <algorithm>
|
||||||
#include <fstream>
|
#include <cstdint>
|
||||||
#include <sstream>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -53,12 +54,11 @@ DEFINE_GUID(IID_PinnedMemoryAllocator,
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
const char* kSlangShaderRelativePath = "apps/LoopThroughWithOpenGLCompositing/video_effect.slang";
|
constexpr GLuint kVideoTextureUnit = 1;
|
||||||
const char* kRuntimeShaderCacheDirectory = "shader_cache";
|
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||||
const char* kRuntimeRawShaderFilename = "video_effect.raw.frag";
|
const char* kDisplayModeName = "1080p59.94";
|
||||||
const char* kRuntimePatchedShaderFilename = "video_effect.frag";
|
|
||||||
const char* kVertexShaderSource =
|
const char* kVertexShaderSource =
|
||||||
"#version 130\n"
|
"#version 430 core\n"
|
||||||
"out vec2 vTexCoord;\n"
|
"out vec2 vTexCoord;\n"
|
||||||
"void main()\n"
|
"void main()\n"
|
||||||
"{\n"
|
"{\n"
|
||||||
@@ -68,34 +68,6 @@ const char* kVertexShaderSource =
|
|||||||
" vTexCoord = texCoords[gl_VertexID];\n"
|
" vTexCoord = texCoords[gl_VertexID];\n"
|
||||||
"}\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)
|
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
|
||||||
{
|
{
|
||||||
if (!errorMessage || errorMessageSize <= 0)
|
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);
|
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);
|
const std::size_t mask = alignment - 1;
|
||||||
if (!input)
|
return (offset + mask) & ~mask;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path FindRepoRoot()
|
template <typename TValue>
|
||||||
|
void AppendStd140Value(std::vector<unsigned char>& buffer, std::size_t alignment, const TValue& value)
|
||||||
{
|
{
|
||||||
std::vector<std::filesystem::path> rootsToTry;
|
const std::size_t offset = AlignStd140(buffer.size(), alignment);
|
||||||
|
if (buffer.size() < offset + sizeof(TValue))
|
||||||
char currentDirBuffer[MAX_PATH] = {};
|
buffer.resize(offset + sizeof(TValue), 0);
|
||||||
if (GetCurrentDirectoryA(MAX_PATH, currentDirBuffer) > 0)
|
std::memcpy(buffer.data() + offset, &value, sizeof(TValue));
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FindSlangCompiler(const std::filesystem::path& repoRoot, std::filesystem::path& slangCompilerPath, std::string& error)
|
void AppendStd140Float(std::vector<unsigned char>& buffer, float value)
|
||||||
{
|
{
|
||||||
std::filesystem::path thirdPartyPath = repoRoot / "3rdParty";
|
AppendStd140Value(buffer, 4, value);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RunProcessAndWait(const std::string& commandLine, std::string& error)
|
void AppendStd140Int(std::vector<unsigned char>& buffer, int value)
|
||||||
{
|
{
|
||||||
STARTUPINFOA startupInfo = {};
|
AppendStd140Value(buffer, 4, value);
|
||||||
PROCESS_INFORMATION processInfo = {};
|
|
||||||
startupInfo.cb = sizeof(startupInfo);
|
|
||||||
|
|
||||||
std::vector<char> 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PatchGeneratedSlangGLSL(std::string& shaderText, std::string& error)
|
void AppendStd140Vec2(std::vector<unsigned char>& buffer, float x, float y)
|
||||||
{
|
{
|
||||||
bool replacedVersion = ReplaceAll(shaderText, "#version 450", "#version 130");
|
const std::size_t offset = AlignStd140(buffer.size(), 8);
|
||||||
ReplaceAll(shaderText, "#extension GL_EXT_samplerless_texture_functions : require\n", "");
|
if (buffer.size() < offset + sizeof(float) * 2)
|
||||||
ReplaceAll(shaderText, "layout(row_major) uniform;\n", "");
|
buffer.resize(offset + sizeof(float) * 2, 0);
|
||||||
ReplaceAll(shaderText, "layout(row_major) buffer;\n", "");
|
float values[2] = { x, y };
|
||||||
ReplaceAll(shaderText, "layout(binding = 0)\nuniform texture2D UYVYtex_0;", "uniform sampler2D UYVYtex;");
|
std::memcpy(buffer.data() + offset, values, sizeof(values));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BuildFragmentShaderSourceFromSlang(std::string& shaderSource, std::string& error)
|
void AppendStd140Vec4(std::vector<unsigned char>& buffer, float x, float y, float z, float w)
|
||||||
{
|
{
|
||||||
std::filesystem::path repoRoot = FindRepoRoot();
|
const std::size_t offset = AlignStd140(buffer.size(), 16);
|
||||||
if (repoRoot.empty())
|
if (buffer.size() < offset + sizeof(float) * 4)
|
||||||
{
|
buffer.resize(offset + sizeof(float) * 4, 0);
|
||||||
error = "Could not locate the repository root to load video_effect.slang.";
|
float values[4] = { x, y, z, w };
|
||||||
return false;
|
std::memcpy(buffer.data() + offset, values, sizeof(values));
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,14 +130,16 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
|||||||
mFastTransferExtensionAvailable(false),
|
mFastTransferExtensionAvailable(false),
|
||||||
mCaptureTexture(0),
|
mCaptureTexture(0),
|
||||||
mFBOTexture(0),
|
mFBOTexture(0),
|
||||||
|
mFullscreenVAO(0),
|
||||||
|
mGlobalParamsUBO(0),
|
||||||
mProgram(0),
|
mProgram(0),
|
||||||
mVertexShader(0),
|
mVertexShader(0),
|
||||||
mFragmentShader(0),
|
mFragmentShader(0),
|
||||||
mUYVYtexUniform(-1),
|
mGlobalParamsUBOSize(0)
|
||||||
mRotateAngle(0.0f),
|
|
||||||
mRotateAngleRate(0.0f)
|
|
||||||
{
|
{
|
||||||
InitializeCriticalSection(&pMutex);
|
InitializeCriticalSection(&pMutex);
|
||||||
|
mRuntimeHost = std::make_unique<RuntimeHost>();
|
||||||
|
mControlServer = std::make_unique<ControlServer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
OpenGLComposite::~OpenGLComposite()
|
OpenGLComposite::~OpenGLComposite()
|
||||||
@@ -348,7 +191,26 @@ OpenGLComposite::~OpenGLComposite()
|
|||||||
mPlayoutAllocator = NULL;
|
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();
|
destroyShaderProgram();
|
||||||
|
if (mControlServer)
|
||||||
|
mControlServer->Stop();
|
||||||
|
|
||||||
DeleteCriticalSection(&pMutex);
|
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.
|
// 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.
|
// Simply copy the off-screen frame buffer to on-screen frame buffer, scaling to the viewing window size.
|
||||||
glBindFramebufferEXT(GL_READ_FRAMEBUFFER, mIdFrameBuf);
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mIdFrameBuf);
|
||||||
glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0);
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||||
glViewport(0, 0, mViewWidth, mViewHeight);
|
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);
|
SwapBuffers(hGLDC);
|
||||||
ValidateRect(hGLWnd, NULL);
|
ValidateRect(hGLWnd, NULL);
|
||||||
@@ -595,7 +457,38 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
if (! ResolveGLExtensions())
|
if (! ResolveGLExtensions())
|
||||||
return false;
|
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];
|
char compilerErrorMessage[1024];
|
||||||
if (! compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
if (! compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||||
{
|
{
|
||||||
@@ -612,54 +505,68 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup the texture which will hold the captured video frame pixels
|
// Setup the texture which will hold the captured video frame pixels
|
||||||
glEnable(GL_TEXTURE_2D);
|
|
||||||
glGenTextures(1, &mCaptureTexture);
|
glGenTextures(1, &mCaptureTexture);
|
||||||
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
||||||
|
|
||||||
// Parameters to control how texels are sampled from the texture
|
// 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_MIN_FILTER, GL_LINEAR);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_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_S, GL_CLAMP_TO_EDGE);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
|
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.
|
// 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
|
// 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.
|
// 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);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
|
||||||
// Create Frame Buffer Object (FBO) to perform off-screen rendering of scene.
|
// 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.
|
// This allows the render to be done on a framebuffer with width and height exactly matching the video format.
|
||||||
glGenFramebuffersEXT(1, &mIdFrameBuf);
|
glGenFramebuffers(1, &mIdFrameBuf);
|
||||||
glGenRenderbuffersEXT(1, &mIdColorBuf);
|
glGenRenderbuffers(1, &mIdColorBuf);
|
||||||
glGenRenderbuffersEXT(1, &mIdDepthBuf);
|
glGenRenderbuffers(1, &mIdDepthBuf);
|
||||||
|
glGenVertexArrays(1, &mFullscreenVAO);
|
||||||
|
glGenBuffers(1, &mGlobalParamsUBO);
|
||||||
|
|
||||||
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, mIdFrameBuf);
|
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
||||||
|
|
||||||
// Texture for FBO
|
// Texture for FBO
|
||||||
glGenTextures(1, &mFBOTexture);
|
glGenTextures(1, &mFBOTexture);
|
||||||
glBindTexture(GL_TEXTURE_2D, 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
|
// Attach a depth buffer
|
||||||
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, mIdDepthBuf);
|
glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf);
|
||||||
glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, mFrameWidth, mFrameHeight);
|
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
|
// Attach the texture which stores the playback image
|
||||||
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, mFBOTexture, 0);
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mFBOTexture, 0);
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
glDisable(GL_TEXTURE_2D);
|
|
||||||
|
|
||||||
GLenum glStatus = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
|
GLenum glStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
if (glStatus != GL_FRAMEBUFFER_COMPLETE_EXT)
|
if (glStatus != GL_FRAMEBUFFER_COMPLETE)
|
||||||
{
|
{
|
||||||
MessageBox(NULL, _T("Cannot initialize framebuffer."), _T("OpenGL initialization error."), MB_OK);
|
MessageBox(NULL, _T("Cannot initialize framebuffer."), _T("OpenGL initialization error."), MB_OK);
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,6 +576,9 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
|
void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
|
||||||
{
|
{
|
||||||
mHasNoInputSource = hasNoInputSource;
|
mHasNoInputSource = hasNoInputSource;
|
||||||
|
if (mRuntimeHost)
|
||||||
|
mRuntimeHost->SetSignalStatus(!hasNoInputSource, mFrameWidth, mFrameHeight, kDisplayModeName);
|
||||||
|
|
||||||
if (mHasNoInputSource)
|
if (mHasNoInputSource)
|
||||||
return; // don't transfer texture when there's no input
|
return; // don't transfer texture when there's no input
|
||||||
|
|
||||||
@@ -700,8 +610,6 @@ void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bo
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
glEnable(GL_TEXTURE_2D);
|
|
||||||
|
|
||||||
// Use a straightforward texture buffer
|
// Use a straightforward texture buffer
|
||||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mUnpinnedTextureBuffer);
|
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mUnpinnedTextureBuffer);
|
||||||
glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, videoPixels, GL_DYNAMIC_DRAW);
|
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);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||||
glDisable(GL_TEXTURE_2D);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wglMakeCurrent( NULL, NULL );
|
wglMakeCurrent( NULL, NULL );
|
||||||
@@ -738,8 +645,10 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame,
|
|||||||
wglMakeCurrent( hGLDC, hGLRC );
|
wglMakeCurrent( hGLDC, hGLRC );
|
||||||
|
|
||||||
// Draw the effect output to the off-screen framebuffer.
|
// Draw the effect output to the off-screen framebuffer.
|
||||||
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, mIdFrameBuf);
|
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
||||||
renderEffect();
|
renderEffect();
|
||||||
|
if (mRuntimeHost)
|
||||||
|
mRuntimeHost->AdvanceFrame();
|
||||||
|
|
||||||
IDeckLinkVideoBuffer* outputVideoFrameBuffer;
|
IDeckLinkVideoBuffer* outputVideoFrameBuffer;
|
||||||
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
|
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
|
||||||
@@ -838,6 +747,9 @@ bool OpenGLComposite::Start()
|
|||||||
|
|
||||||
bool OpenGLComposite::Stop()
|
bool OpenGLComposite::Stop()
|
||||||
{
|
{
|
||||||
|
if (mControlServer)
|
||||||
|
mControlServer->Stop();
|
||||||
|
|
||||||
mDLInput->StopStreams();
|
mDLInput->StopStreams();
|
||||||
mDLInput->DisableVideoInput();
|
mDLInput->DisableVideoInput();
|
||||||
|
|
||||||
@@ -855,12 +767,24 @@ bool OpenGLComposite::ReloadShader()
|
|||||||
wglMakeCurrent(hGLDC, hGLRC);
|
wglMakeCurrent(hGLDC, hGLRC);
|
||||||
|
|
||||||
bool success = compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage);
|
bool success = compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage);
|
||||||
|
if (mRuntimeHost)
|
||||||
|
mRuntimeHost->ClearReloadRequest();
|
||||||
|
|
||||||
wglMakeCurrent(NULL, NULL);
|
wglMakeCurrent(NULL, NULL);
|
||||||
LeaveCriticalSection(&pMutex);
|
LeaveCriticalSection(&pMutex);
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
|
{
|
||||||
|
if (mRuntimeHost)
|
||||||
|
mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
|
||||||
MessageBoxA(NULL, compilerErrorMessage, "Slang shader reload failed", MB_OK);
|
MessageBoxA(NULL, compilerErrorMessage, "Slang shader reload failed", MB_OK);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (mRuntimeHost)
|
||||||
|
mRuntimeHost->SetCompileStatus(true, "Shader compiled successfully.");
|
||||||
|
broadcastRuntimeState();
|
||||||
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
@@ -884,12 +808,12 @@ void OpenGLComposite::destroyShaderProgram()
|
|||||||
glDeleteShader(mVertexShader);
|
glDeleteShader(mVertexShader);
|
||||||
mVertexShader = 0;
|
mVertexShader = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
mUYVYtexUniform = -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLComposite::renderEffect()
|
void OpenGLComposite::renderEffect()
|
||||||
{
|
{
|
||||||
|
PollRuntimeChanges();
|
||||||
|
|
||||||
glViewport(0, 0, mFrameWidth, mFrameHeight);
|
glViewport(0, 0, mFrameWidth, mFrameHeight);
|
||||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
@@ -904,26 +828,30 @@ void OpenGLComposite::renderEffect()
|
|||||||
|
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(GL_DEPTH_TEST);
|
||||||
glEnable(GL_TEXTURE_2D);
|
glActiveTexture(GL_TEXTURE0 + kVideoTextureUnit);
|
||||||
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
||||||
|
glBindVertexArray(mFullscreenVAO);
|
||||||
glUseProgram(mProgram);
|
glUseProgram(mProgram);
|
||||||
|
|
||||||
if (mUYVYtexUniform >= 0)
|
if (mRuntimeHost)
|
||||||
glUniform1i(mUYVYtexUniform, 0);
|
{
|
||||||
|
const RuntimeRenderState state = mRuntimeHost->GetRenderState(mFrameWidth, mFrameHeight);
|
||||||
|
updateGlobalParamsBuffer(state);
|
||||||
|
}
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
|
||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
glDisable(GL_TEXTURE_2D);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
|
||||||
if (mFastTransferExtensionAvailable)
|
if (mFastTransferExtensionAvailable)
|
||||||
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU);
|
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile a fullscreen shader pass from the runtime Slang source. The Slang compiler
|
// Compile a fullscreen shader pass from the runtime Slang source into a core-profile
|
||||||
// emits modern GLSL which we patch into a compatibility-profile shader that can run
|
// GLSL program. The renderer owns the fullscreen pass and parameter UBO layout.
|
||||||
// inside the sample's WGL context.
|
|
||||||
bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMessage)
|
bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMessage)
|
||||||
{
|
{
|
||||||
GLsizei errorBufferSize = 0;
|
GLsizei errorBufferSize = 0;
|
||||||
@@ -933,8 +861,9 @@ bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMes
|
|||||||
std::string loadError;
|
std::string loadError;
|
||||||
const char* vertexSource = kVertexShaderSource;
|
const char* vertexSource = kVertexShaderSource;
|
||||||
|
|
||||||
if (!BuildFragmentShaderSourceFromSlang(fragmentShaderSource, loadError))
|
if (!mRuntimeHost->BuildActiveFragmentShaderSource(fragmentShaderSource, loadError))
|
||||||
{
|
{
|
||||||
|
mRuntimeHost->SetCompileStatus(false, loadError);
|
||||||
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
|
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -983,28 +912,182 @@ bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMes
|
|||||||
mProgram = newProgram;
|
mProgram = newProgram;
|
||||||
mVertexShader = newVertexShader;
|
mVertexShader = newVertexShader;
|
||||||
mFragmentShader = newFragmentShader;
|
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<unsigned char> buffer;
|
||||||
|
buffer.reserve(512);
|
||||||
|
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.timeSeconds));
|
||||||
|
AppendStd140Vec2(buffer, static_cast<float>(state.inputWidth), static_cast<float>(state.inputHeight));
|
||||||
|
AppendStd140Vec2(buffer, static_cast<float>(state.outputWidth), static_cast<float>(state.outputHeight));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.frameCount));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.mixAmount));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(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<float>(value.numberValues[0]));
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Vec2:
|
||||||
|
AppendStd140Vec2(buffer,
|
||||||
|
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 0.0f,
|
||||||
|
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 0.0f);
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Color:
|
||||||
|
AppendStd140Vec4(buffer,
|
||||||
|
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 1.0f,
|
||||||
|
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 1.0f,
|
||||||
|
value.numberValues.size() > 2 ? static_cast<float>(value.numberValues[2]) : 1.0f,
|
||||||
|
value.numberValues.size() > 3 ? static_cast<float>(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<int>(optionIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppendStd140Int(buffer, selectedIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.resize(AlignStd140(buffer.size(), 16), 0);
|
||||||
|
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsUBO);
|
||||||
|
if (mGlobalParamsUBOSize != static_cast<GLsizeiptr>(buffer.size()))
|
||||||
|
{
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, static_cast<GLsizeiptr>(buffer.size()), buffer.data(), GL_DYNAMIC_DRAW);
|
||||||
|
mGlobalParamsUBOSize = static_cast<GLsizeiptr>(buffer.size());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
glBufferSubData(GL_UNIFORM_BUFFER, 0, static_cast<GLsizeiptr>(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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::CheckOpenGLExtensions()
|
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();
|
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)
|
if (!mFastTransferExtensionAvailable)
|
||||||
OutputDebugStringA("Fast memory transfer extension not available, using regular OpenGL transfer fallback instead\n");
|
OutputDebugStringA("Fast memory transfer extension not available, using regular OpenGL transfer fallback instead\n");
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,10 @@
|
|||||||
#include "DeckLinkAPI_h.h"
|
#include "DeckLinkAPI_h.h"
|
||||||
|
|
||||||
#include "VideoFrameTransfer.h"
|
#include "VideoFrameTransfer.h"
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
class PlayoutDelegate;
|
class PlayoutDelegate;
|
||||||
class CaptureDelegate;
|
class CaptureDelegate;
|
||||||
class PinnedMemoryAllocator;
|
class PinnedMemoryAllocator;
|
||||||
|
class ControlServer;
|
||||||
|
|
||||||
|
|
||||||
class OpenGLComposite
|
class OpenGLComposite
|
||||||
@@ -75,6 +78,11 @@ public:
|
|||||||
bool Start();
|
bool Start();
|
||||||
bool Stop();
|
bool Stop();
|
||||||
bool ReloadShader();
|
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 resizeGL(WORD width, WORD height);
|
||||||
void paintGL();
|
void paintGL();
|
||||||
@@ -113,19 +121,24 @@ private:
|
|||||||
GLuint mIdFrameBuf;
|
GLuint mIdFrameBuf;
|
||||||
GLuint mIdColorBuf;
|
GLuint mIdColorBuf;
|
||||||
GLuint mIdDepthBuf;
|
GLuint mIdDepthBuf;
|
||||||
|
GLuint mFullscreenVAO;
|
||||||
|
GLuint mGlobalParamsUBO;
|
||||||
GLuint mProgram;
|
GLuint mProgram;
|
||||||
GLuint mVertexShader;
|
GLuint mVertexShader;
|
||||||
GLuint mFragmentShader;
|
GLuint mFragmentShader;
|
||||||
GLint mUYVYtexUniform;
|
GLsizeiptr mGlobalParamsUBOSize;
|
||||||
GLfloat mRotateAngle;
|
|
||||||
GLfloat mRotateAngleRate;
|
|
||||||
int mViewWidth;
|
int mViewWidth;
|
||||||
int mViewHeight;
|
int mViewHeight;
|
||||||
|
std::unique_ptr<RuntimeHost> mRuntimeHost;
|
||||||
|
std::unique_ptr<ControlServer> mControlServer;
|
||||||
|
|
||||||
bool InitOpenGLState();
|
bool InitOpenGLState();
|
||||||
bool compileFragmentShader(int errorMessageSize, char* errorMessage);
|
bool compileFragmentShader(int errorMessageSize, char* errorMessage);
|
||||||
void destroyShaderProgram();
|
void destroyShaderProgram();
|
||||||
void renderEffect();
|
void renderEffect();
|
||||||
|
bool PollRuntimeChanges();
|
||||||
|
void broadcastRuntimeState();
|
||||||
|
bool updateGlobalParamsBuffer(const RuntimeRenderState& state);
|
||||||
};
|
};
|
||||||
|
|
||||||
////////////////////////////////////////////
|
////////////////////////////////////////////
|
||||||
|
|||||||
1284
apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp
Normal file
1284
apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp
Normal file
File diff suppressed because it is too large
Load Diff
173
apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h
Normal file
173
apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RuntimeJson.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<double> defaultNumbers;
|
||||||
|
std::vector<double> minNumbers;
|
||||||
|
std::vector<double> maxNumbers;
|
||||||
|
std::vector<double> stepNumbers;
|
||||||
|
bool defaultBoolean = false;
|
||||||
|
std::string defaultEnumValue;
|
||||||
|
std::vector<ShaderParameterOption> enumOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ShaderParameterValue
|
||||||
|
{
|
||||||
|
std::vector<double> 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<ShaderParameterDefinition> parameters;
|
||||||
|
std::filesystem::file_time_type shaderWriteTime;
|
||||||
|
std::filesystem::file_time_type manifestWriteTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RuntimeRenderState
|
||||||
|
{
|
||||||
|
std::string activeShaderId;
|
||||||
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
|
std::map<std::string, ShaderParameterValue> 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<std::string, std::map<std::string, ShaderParameterValue>> 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<std::string, ShaderPackage> mPackagesById;
|
||||||
|
std::vector<std::string> 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;
|
||||||
|
};
|
||||||
500
apps/LoopThroughWithOpenGLCompositing/RuntimeJson.cpp
Normal file
500
apps/LoopThroughWithOpenGLCompositing/RuntimeJson.cpp
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#include "RuntimeJson.h"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <limits>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
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<unsigned char>(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<unsigned char>(mText[mPosition])))
|
||||||
|
++mPosition;
|
||||||
|
|
||||||
|
if (mPosition < mText.size() && mText[mPosition] == '.')
|
||||||
|
{
|
||||||
|
++mPosition;
|
||||||
|
while (mPosition < mText.size() && std::isdigit(static_cast<unsigned char>(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<unsigned char>(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<unsigned char>(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<JsonValue>& 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<std::string, JsonValue>& 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>& JsonValue::asArray() const
|
||||||
|
{
|
||||||
|
static const std::vector<JsonValue> emptyArray;
|
||||||
|
return mType == Type::Array ? mArrayValue : emptyArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::map<std::string, JsonValue>& JsonValue::asObject() const
|
||||||
|
{
|
||||||
|
static const std::map<std::string, JsonValue> emptyObject;
|
||||||
|
return mType == Type::Object ? mObjectValue : emptyObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<JsonValue>& JsonValue::array()
|
||||||
|
{
|
||||||
|
if (mType != Type::Array)
|
||||||
|
{
|
||||||
|
mType = Type::Array;
|
||||||
|
mArrayValue.clear();
|
||||||
|
}
|
||||||
|
return mArrayValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, JsonValue>& 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();
|
||||||
|
}
|
||||||
62
apps/LoopThroughWithOpenGLCompositing/RuntimeJson.h
Normal file
62
apps/LoopThroughWithOpenGLCompositing/RuntimeJson.h
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<JsonValue>& asArray() const;
|
||||||
|
const std::map<std::string, JsonValue>& asObject() const;
|
||||||
|
|
||||||
|
std::vector<JsonValue>& array();
|
||||||
|
std::map<std::string, JsonValue>& 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<JsonValue> mArrayValue;
|
||||||
|
std::map<std::string, JsonValue> mObjectValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool ParseJson(const std::string& text, JsonValue& value, std::string& error);
|
||||||
|
std::string SerializeJson(const JsonValue& value, bool pretty = false);
|
||||||
@@ -72,6 +72,9 @@ bool VideoFrameTransfer::isNvidiaDvpAvailable()
|
|||||||
{
|
{
|
||||||
// Look for supported graphics boards
|
// Look for supported graphics boards
|
||||||
const GLubyte* renderer = glGetString(GL_RENDERER);
|
const GLubyte* renderer = glGetString(GL_RENDERER);
|
||||||
|
if (renderer == NULL)
|
||||||
|
return false;
|
||||||
|
|
||||||
bool hasDvp = (strstr((char*)renderer, "Quadro") != NULL);
|
bool hasDvp = (strstr((char*)renderer, "Quadro") != NULL);
|
||||||
return hasDvp;
|
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
|
// GL_AMD_pinned_memory presence indicates GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD buffer target is supported
|
||||||
const GLubyte* strExt = glGetString(GL_EXTENSIONS);
|
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);
|
bool hasAMDPinned = (strstr((char*)strExt, "GL_AMD_pinned_memory") != NULL);
|
||||||
return hasAMDPinned;
|
return hasAMDPinned;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,14 @@
|
|||||||
|
|
||||||
#include "targetver.h"
|
#include "targetver.h"
|
||||||
|
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
|
||||||
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
|
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
|
||||||
// Windows Header Files:
|
// Windows Header Files:
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
// C RunTime Header Files
|
// C RunTime Header Files
|
||||||
|
|||||||
@@ -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<float4> 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<float4> 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);
|
|
||||||
}
|
|
||||||
5
config/runtime-host.json
Normal file
5
config/runtime-host.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"shaderLibrary": "shaders",
|
||||||
|
"serverPort": 8080,
|
||||||
|
"autoReload": true
|
||||||
|
}
|
||||||
50
shaders/studio-color/shader.json
Normal file
50
shaders/studio-color/shader.json
Normal file
@@ -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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
23
shaders/studio-color/shader.slang
Normal file
23
shaders/studio-color/shader.slang
Normal file
@@ -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);
|
||||||
|
}
|
||||||
212
ui/app.js
Normal file
212
ui/app.js
Normal file
@@ -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);
|
||||||
52
ui/index.html
Normal file
52
ui/index.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Video Shader Host</title>
|
||||||
|
<link rel="stylesheet" href="/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="layout">
|
||||||
|
<section class="toolbar">
|
||||||
|
<div class="toolbar__group">
|
||||||
|
<label for="shader-select">Shader</label>
|
||||||
|
<select id="shader-select"></select>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar__group toolbar__group--wide">
|
||||||
|
<label for="mix-slider">Mix</label>
|
||||||
|
<input id="mix-slider" type="range" min="0" max="1" step="0.01">
|
||||||
|
</div>
|
||||||
|
<label class="toggle">
|
||||||
|
<input id="bypass-toggle" type="checkbox">
|
||||||
|
<span>Bypass</span>
|
||||||
|
</label>
|
||||||
|
<button id="reload-button" type="button">Reload Shader</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="status-grid">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>Runtime</h2>
|
||||||
|
<dl id="runtime-status" class="kv"></dl>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<h2>Video</h2>
|
||||||
|
<dl id="video-status" class="kv"></dl>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel--full">
|
||||||
|
<h2>Compiler</h2>
|
||||||
|
<pre id="compile-status"></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel__header">
|
||||||
|
<h2>Parameters</h2>
|
||||||
|
</div>
|
||||||
|
<form id="parameter-form" class="parameter-grid"></form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
160
ui/styles.css
Normal file
160
ui/styles.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user