309 lines
11 KiB
C++
309 lines
11 KiB
C++
#include "AppConfigProvider.h"
|
|
#include "AppConfigJson.h"
|
|
#include "DeckLinkDisplayMode.h"
|
|
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
namespace
|
|
{
|
|
int gFailures = 0;
|
|
|
|
void Expect(bool condition, const std::string& message)
|
|
{
|
|
if (condition)
|
|
return;
|
|
|
|
++gFailures;
|
|
std::cerr << "FAILED: " << message << "\n";
|
|
}
|
|
|
|
std::filesystem::path WriteConfigFixture()
|
|
{
|
|
const std::filesystem::path path = std::filesystem::temp_directory_path() / "render-cadence-compositor-config-test.json";
|
|
std::ofstream output(path, std::ios::binary);
|
|
output
|
|
<< "{\n"
|
|
<< " \"shaderLibrary\": \"test-shaders\",\n"
|
|
<< " \"serverPort\": 8181,\n"
|
|
<< " \"oscBindAddress\": \"127.0.0.1\",\n"
|
|
<< " \"oscPort\": 9100,\n"
|
|
<< " \"oscSmoothing\": 0.25,\n"
|
|
<< " \"input\": {\n"
|
|
<< " \"backend\": \"none\",\n"
|
|
<< " \"device\": \"input-card-1\",\n"
|
|
<< " \"resolution\": \"720p\",\n"
|
|
<< " \"frameRate\": \"50\"\n"
|
|
<< " },\n"
|
|
<< " \"output\": {\n"
|
|
<< " \"backend\": \"decklink\",\n"
|
|
<< " \"device\": \"output-card-1\",\n"
|
|
<< " \"resolution\": \"2160p\",\n"
|
|
<< " \"frameRate\": \"60\",\n"
|
|
<< " \"pixelFormat\": \"uyvy8\",\n"
|
|
<< " \"keying\": {\n"
|
|
<< " \"external\": true,\n"
|
|
<< " \"alphaRequired\": true\n"
|
|
<< " }\n"
|
|
<< " },\n"
|
|
<< " \"windowOutput\": {\n"
|
|
<< " \"fullscreen\": false,\n"
|
|
<< " \"borderless\": true,\n"
|
|
<< " \"display\": \"Display 2\",\n"
|
|
<< " \"x\": 1920,\n"
|
|
<< " \"y\": 0,\n"
|
|
<< " \"width\": 1920,\n"
|
|
<< " \"height\": 1080,\n"
|
|
<< " \"vsync\": true,\n"
|
|
<< " \"allowTearing\": false\n"
|
|
<< " },\n"
|
|
<< " \"colorPipeline\": {\n"
|
|
<< " \"ocioEnabled\": true,\n"
|
|
<< " \"ocioConfig\": \"config/ocio/studio.ocio\",\n"
|
|
<< " \"inputColorSpace\": \"rec709\",\n"
|
|
<< " \"workingColorSpace\": \"acescg\",\n"
|
|
<< " \"outputColorSpace\": \"rec709_display\",\n"
|
|
<< " \"display\": \"sRGB\",\n"
|
|
<< " \"view\": \"Filmic\",\n"
|
|
<< " \"look\": \"none\",\n"
|
|
<< " \"exposure\": 0.5,\n"
|
|
<< " \"gamma\": 1.1,\n"
|
|
<< " \"workingFormat\": \"rgba16f\",\n"
|
|
<< " \"linearWorkingSpace\": true\n"
|
|
<< " },\n"
|
|
<< " \"autoReload\": false,\n"
|
|
<< " \"maxTemporalHistoryFrames\": 8,\n"
|
|
<< " \"previewEnabled\": true,\n"
|
|
<< " \"previewFps\": 24,\n"
|
|
<< " \"runtimeShaderId\": \"solid-color\"\n"
|
|
<< "}\n";
|
|
return path;
|
|
}
|
|
|
|
void TestLoadsRuntimeHostConfig()
|
|
{
|
|
using namespace RenderCadenceCompositor;
|
|
|
|
const std::filesystem::path path = WriteConfigFixture();
|
|
AppConfigProvider provider;
|
|
std::string error;
|
|
const bool loaded = provider.Load(path, error);
|
|
const AppConfig& config = provider.Config();
|
|
|
|
Expect(loaded, "config loads");
|
|
Expect(error.empty(), "config load has no error");
|
|
Expect(provider.LoadedFromFile(), "provider records file load");
|
|
Expect(config.shaderLibrary == "test-shaders", "shader library loads");
|
|
Expect(config.http.preferredPort == 8181, "server port loads");
|
|
Expect(config.oscBindAddress == "127.0.0.1", "OSC bind address loads");
|
|
Expect(config.oscPort == 9100, "OSC port loads");
|
|
Expect(config.oscSmoothing == 0.25, "OSC smoothing loads");
|
|
Expect(config.input.resolution == "720p", "input resolution loads");
|
|
Expect(config.input.frameRate == "50", "input frame rate loads");
|
|
Expect(config.input.device == "input-card-1", "input device loads");
|
|
Expect(config.output.resolution == "2160p", "output resolution loads");
|
|
Expect(config.output.frameRate == "60", "output frame rate loads");
|
|
Expect(config.output.pixelFormat == "uyvy8", "output pixel format loads");
|
|
Expect(config.output.device == "output-card-1", "output device loads");
|
|
Expect(config.input.backend == "none", "video input backend loads");
|
|
Expect(config.output.backend == "decklink", "video output backend loads");
|
|
Expect(!config.autoReload, "auto reload loads");
|
|
Expect(config.maxTemporalHistoryFrames == 8, "history length loads");
|
|
Expect(config.previewEnabled, "preview enabled toggle loads");
|
|
Expect(config.previewFps == 24.0, "preview fps loads");
|
|
Expect(config.runtimeShaderId == "solid-color", "runtime shader id loads");
|
|
Expect(config.output.externalKeyingEnabled, "external keying loads");
|
|
Expect(config.output.outputAlphaRequired, "output alpha requirement loads");
|
|
Expect(!config.windowOutput.fullscreen, "window output fullscreen loads");
|
|
Expect(config.windowOutput.borderless, "window output borderless loads");
|
|
Expect(config.windowOutput.display == "Display 2", "window output display loads");
|
|
Expect(config.windowOutput.x == 1920, "window output x loads");
|
|
Expect(config.windowOutput.y == 0, "window output y loads");
|
|
Expect(config.windowOutput.width == 1920, "window output width loads");
|
|
Expect(config.windowOutput.height == 1080, "window output height loads");
|
|
Expect(config.windowOutput.vsync, "window output vsync loads");
|
|
Expect(!config.windowOutput.allowTearing, "window output tearing flag loads");
|
|
Expect(config.colorPipeline.ocioEnabled, "OCIO enabled loads");
|
|
Expect(config.colorPipeline.ocioConfig == "config/ocio/studio.ocio", "OCIO config path loads");
|
|
Expect(config.colorPipeline.inputColorSpace == "rec709", "input color space loads");
|
|
Expect(config.colorPipeline.workingColorSpace == "acescg", "working color space loads");
|
|
Expect(config.colorPipeline.outputColorSpace == "rec709_display", "output color space loads");
|
|
Expect(config.colorPipeline.display == "sRGB", "OCIO display loads");
|
|
Expect(config.colorPipeline.view == "Filmic", "OCIO view loads");
|
|
Expect(config.colorPipeline.look == "none", "OCIO look loads");
|
|
Expect(config.colorPipeline.exposure == 0.5, "OCIO exposure loads");
|
|
Expect(config.colorPipeline.gamma == 1.1, "OCIO gamma loads");
|
|
Expect(config.colorPipeline.workingFormat == "rgba16f", "working format loads");
|
|
Expect(config.colorPipeline.linearWorkingSpace, "linear working space flag loads");
|
|
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
void TestCommandLineOverrides()
|
|
{
|
|
using namespace RenderCadenceCompositor;
|
|
|
|
AppConfigProvider provider;
|
|
const char* argv[] = {
|
|
"app.exe",
|
|
"--shader",
|
|
"solid-color",
|
|
"--port",
|
|
"8282"
|
|
};
|
|
provider.ApplyCommandLine(5, const_cast<char**>(argv));
|
|
|
|
const AppConfig& config = provider.Config();
|
|
Expect(config.runtimeShaderId == "solid-color", "shader CLI override applies");
|
|
Expect(config.http.preferredPort == 8282, "port CLI override applies");
|
|
}
|
|
|
|
void TestOscPortZeroIsAllowed()
|
|
{
|
|
using namespace RenderCadenceCompositor;
|
|
|
|
const std::filesystem::path path = std::filesystem::temp_directory_path() / "render-cadence-compositor-config-osc-disabled-test.json";
|
|
std::ofstream output(path, std::ios::binary);
|
|
output << "{ \"oscPort\": 0 }\n";
|
|
output.close();
|
|
|
|
std::string error;
|
|
AppConfigProvider provider;
|
|
Expect(provider.Load(path, error), "provider accepts oscPort zero");
|
|
Expect(provider.Config().oscPort == 0, "provider loads oscPort zero");
|
|
|
|
AppConfig parsed;
|
|
Expect(ParseAppConfigJson("{\"oscPort\":0}", parsed, error), "config JSON parser accepts oscPort zero");
|
|
Expect(parsed.oscPort == 0, "config JSON parser loads oscPort zero");
|
|
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
void TestPreviewDefaultsAreOptIn()
|
|
{
|
|
using namespace RenderCadenceCompositor;
|
|
|
|
const AppConfig config = DefaultAppConfig();
|
|
Expect(!config.previewEnabled, "preview is disabled by default");
|
|
Expect(config.previewFps == 60.0, "preview fps default is 60");
|
|
}
|
|
|
|
void TestConfigJsonRoundTrip()
|
|
{
|
|
using namespace RenderCadenceCompositor;
|
|
|
|
AppConfig config = DefaultAppConfig();
|
|
config.shaderLibrary = "custom-shaders";
|
|
config.output.backend = "ndi";
|
|
config.output.device = "Program";
|
|
config.output.pixelFormat = "uyvy8";
|
|
config.windowOutput.fullscreen = false;
|
|
config.windowOutput.display = "Display 3";
|
|
config.colorPipeline.ocioEnabled = true;
|
|
config.colorPipeline.workingColorSpace = "acescg";
|
|
config.previewEnabled = true;
|
|
config.runtimeShaderId = "solid-color";
|
|
|
|
const std::string json = AppConfigToJson(config);
|
|
AppConfig parsed;
|
|
std::string error;
|
|
Expect(ParseAppConfigJson(json, parsed, error), "serialized config parses");
|
|
Expect(error.empty(), "serialized config parse has no error");
|
|
Expect(parsed.shaderLibrary == "custom-shaders", "shader library round trips");
|
|
Expect(parsed.output.backend == "ndi", "output backend round trips");
|
|
Expect(parsed.output.device == "Program", "output device round trips");
|
|
Expect(parsed.output.pixelFormat == "uyvy8", "output pixel format round trips");
|
|
Expect(!parsed.windowOutput.fullscreen, "window output fullscreen round trips");
|
|
Expect(parsed.windowOutput.display == "Display 3", "window output display round trips");
|
|
Expect(parsed.colorPipeline.ocioEnabled, "OCIO enabled round trips");
|
|
Expect(parsed.colorPipeline.workingColorSpace == "acescg", "working color space round trips");
|
|
Expect(parsed.previewEnabled, "preview enabled round trips");
|
|
Expect(parsed.runtimeShaderId == "solid-color", "runtime shader id round trips");
|
|
}
|
|
|
|
void TestOutputAlphaNormalizesLegacyKeying()
|
|
{
|
|
using namespace RenderCadenceCompositor;
|
|
|
|
const std::string deckLinkJson =
|
|
"{"
|
|
"\"output\":{"
|
|
"\"backend\":\"decklink\","
|
|
"\"keying\":{\"external\":true,\"alphaRequired\":false}"
|
|
"}"
|
|
"}";
|
|
|
|
AppConfig parsed;
|
|
std::string error;
|
|
Expect(ParseAppConfigJson(deckLinkJson, parsed, error), "legacy DeckLink keying config parses");
|
|
Expect(parsed.output.externalKeyingEnabled, "DeckLink external keying remains enabled");
|
|
Expect(parsed.output.outputAlphaRequired, "DeckLink external keying implies output alpha");
|
|
|
|
const std::filesystem::path path = std::filesystem::temp_directory_path() / "render-cadence-compositor-config-alpha-test.json";
|
|
std::ofstream output(path, std::ios::binary);
|
|
output
|
|
<< "{\n"
|
|
<< " \"output\": {\n"
|
|
<< " \"backend\": \"ndi\",\n"
|
|
<< " \"keying\": {\n"
|
|
<< " \"external\": true,\n"
|
|
<< " \"alphaRequired\": false\n"
|
|
<< " }\n"
|
|
<< " }\n"
|
|
<< "}\n";
|
|
output.close();
|
|
|
|
AppConfigProvider provider;
|
|
Expect(provider.Load(path, error), "legacy NDI keying config loads");
|
|
Expect(provider.Config().output.outputAlphaRequired, "legacy external keying implies NDI output alpha");
|
|
Expect(!provider.Config().output.externalKeyingEnabled, "NDI ignores external keying backing field");
|
|
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
void TestHelpers()
|
|
{
|
|
using namespace RenderCadenceCompositor;
|
|
|
|
unsigned width = 0;
|
|
unsigned height = 0;
|
|
VideoFormatDimensions("720p", width, height);
|
|
Expect(width == 1280 && height == 720, "720p dimensions resolve");
|
|
|
|
VideoFormatDimensions("2160p", width, height);
|
|
Expect(width == 3840 && height == 2160, "2160p dimensions resolve");
|
|
|
|
const double duration = FrameDurationMillisecondsFromRateString("50");
|
|
Expect(duration > 19.9 && duration < 20.1, "frame duration parses numeric rate");
|
|
const double deckLinkDuration = FrameDurationMillisecondsFromDisplayMode(bmdModeHD1080p5994, 0.0);
|
|
Expect(deckLinkDuration > 16.6833 && deckLinkDuration < 16.6834, "DeckLink 59.94 display mode duration is exact");
|
|
|
|
const std::filesystem::path configPath = FindConfigFile();
|
|
Expect(!configPath.empty(), "default config is discoverable from test working directory");
|
|
Expect(configPath.filename() == "runtime-host.json", "default config discovery returns runtime-host.json");
|
|
}
|
|
}
|
|
|
|
int main()
|
|
{
|
|
TestLoadsRuntimeHostConfig();
|
|
TestCommandLineOverrides();
|
|
TestOscPortZeroIsAllowed();
|
|
TestPreviewDefaultsAreOptIn();
|
|
TestConfigJsonRoundTrip();
|
|
TestOutputAlphaNormalizesLegacyKeying();
|
|
TestHelpers();
|
|
|
|
if (gFailures != 0)
|
|
{
|
|
std::cerr << gFailures << " RenderCadenceCompositorAppConfigProvider test failure(s).\n";
|
|
return 1;
|
|
}
|
|
|
|
std::cout << "RenderCadenceCompositorAppConfigProvider tests passed.\n";
|
|
return 0;
|
|
}
|