Font builder
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 2m10s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
2026-05-20 15:49:29 +10:00
parent f589b1e1fe
commit 081364e764
13 changed files with 476 additions and 33 deletions

View File

@@ -11,6 +11,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
set(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
set(SLANG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
set(MSDF_ATLAS_GEN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/msdf-atlas-gen" CACHE PATH "Path to msdf-atlas-gen binary release")
set(VIDEO_SHADER_INCLUDE_DIRS
"${SRC_DIR}"
@@ -61,6 +62,16 @@ set(SLANG_RUNTIME_FILES
"${SLANG_ROOT}/bin/slang-glslang.dll"
)
set(SLANG_LICENSE_FILE "${SLANG_ROOT}/LICENSE")
set(MSDF_ATLAS_GEN_EXE_FILE "${MSDF_ATLAS_GEN_ROOT}/msdf-atlas-gen.exe")
file(GLOB MSDF_ATLAS_GEN_DLL_FILES CONFIGURE_DEPENDS
"${MSDF_ATLAS_GEN_ROOT}/*.dll"
)
set(MSDF_ATLAS_GEN_RUNTIME_FILES
"${MSDF_ATLAS_GEN_EXE_FILE}"
${MSDF_ATLAS_GEN_DLL_FILES}
)
set(MSDF_ATLAS_GEN_LICENSE_FILE "${MSDF_ATLAS_GEN_ROOT}/LICENSE.txt")
set(MSDF_ATLAS_GEN_README_FILE "${MSDF_ATLAS_GEN_ROOT}/README.md")
set(RENDER_CADENCE_APP_REQUIRED_FILES
"${SRC_DIR}/RenderCadenceCompositor.cpp"
@@ -136,6 +147,34 @@ foreach(slang_runtime_file IN LISTS SLANG_RUNTIME_FILES)
endif()
endforeach()
foreach(msdf_runtime_file IN LISTS MSDF_ATLAS_GEN_RUNTIME_FILES)
if(EXISTS "${msdf_runtime_file}")
install(FILES "${msdf_runtime_file}"
DESTINATION "3rdParty/msdf-atlas-gen"
)
else()
message(STATUS "msdf-atlas-gen runtime file not found and will not be installed: ${msdf_runtime_file}")
endif()
endforeach()
if(EXISTS "${MSDF_ATLAS_GEN_LICENSE_FILE}")
install(FILES "${MSDF_ATLAS_GEN_LICENSE_FILE}"
DESTINATION "third_party_notices"
RENAME "MSDF_ATLAS_GEN_LICENSE.txt"
)
else()
message(STATUS "msdf-atlas-gen license file not found: ${MSDF_ATLAS_GEN_LICENSE_FILE}")
endif()
if(EXISTS "${MSDF_ATLAS_GEN_README_FILE}")
install(FILES "${MSDF_ATLAS_GEN_README_FILE}"
DESTINATION "third_party_notices"
RENAME "MSDF_ATLAS_GEN_README.md"
)
else()
message(STATUS "msdf-atlas-gen readme file not found: ${MSDF_ATLAS_GEN_README_FILE}")
endif()
if(EXISTS "${SLANG_LICENSE_FILE}")
install(FILES "${SLANG_LICENSE_FILE}"
DESTINATION "third_party_notices"

View File

@@ -6,7 +6,7 @@ The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime
## Repository Layout
- `apps/LoopThroughWithOpenGLCompositing/`: native C++ host app.
- `src/`: native C++ host app.
- `shaders/`: shader packages, each with `shader.json` and `shader.slang`.
- `ui/`: Vite/React control UI.
- `config/runtime-host.json`: runtime configuration.
@@ -30,6 +30,7 @@ Native app internals are grouped by boundary:
- Node.js and npm for the control UI.
- Blackmagic Desktop Video drivers and a DeckLink device for the current production video I/O backend.
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
- `msdf-atlas-gen` Windows binary release with `msdf-atlas-gen.exe`, `LICENSE.txt`, and any adjacent runtime DLLs for font atlas generation.
Default expected Slang path:
@@ -37,6 +38,12 @@ Default expected Slang path:
3rdParty/slang-2026.8-windows-x86_64
```
Default expected `msdf-atlas-gen` path:
```text
3rdParty/msdf-atlas-gen
```
Override example:
```powershell
@@ -88,10 +95,11 @@ The package folder will contain:
```text
dist/VideoShader/
LoopThroughWithOpenGLCompositing.exe
RenderCadenceCompositor.exe
config/
shaders/
3rdParty/slang/bin/
3rdParty/msdf-atlas-gen/
ui/dist/
docs/
SHADER_CONTRACT.md
@@ -99,9 +107,9 @@ dist/VideoShader/
third_party_notices/
```
You can run `LoopThroughWithOpenGLCompositing.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `3rdParty/slang/bin/slangc.exe`, `ui/dist/`, and `runtime/templates/` relative to the exe folder. In development mode, it still falls back to repo-root discovery.
You can run `RenderCadenceCompositor.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `3rdParty/slang/bin/slangc.exe`, `3rdParty/msdf-atlas-gen/msdf-atlas-gen.exe`, `ui/dist/`, and `runtime/templates/` relative to the exe folder. In development mode, it still falls back to repo-root discovery.
The install step copies only the Slang runtime files required by the shader compiler (`slangc.exe`, `slang-compiler.dll`, and `slang-glslang.dll`) plus `third_party_notices/SLANG_LICENSE.txt`. It does not copy the full Slang release folder.
The install step copies only the Slang runtime files required by the shader compiler (`slangc.exe`, `slang-compiler.dll`, and `slang-glslang.dll`) plus `third_party_notices/SLANG_LICENSE.txt`. It also copies `msdf-atlas-gen.exe`, any adjacent `msdf-atlas-gen` DLLs, and the `third_party_notices/MSDF_ATLAS_GEN_LICENSE.txt` and `third_party_notices/MSDF_ATLAS_GEN_README.md` notice files. It does not copy full third-party release folders.
Create a zip for distribution:
@@ -250,6 +258,7 @@ The Gitea workflow expects two act runners:
The Windows jobs validate native third-party dependencies before configuring CMake. Because `3rdParty/` is ignored, configure this path on the runner or in a Gitea repository variable:
- `SLANG_ROOT`: path to the Slang binary release folder containing `bin/slangc.exe`.
- `MSDF_ATLAS_GEN_ROOT`: path to the `msdf-atlas-gen` binary release folder containing `msdf-atlas-gen.exe`.
The Windows runner also needs the Visual Studio ATL component installed. In Visual Studio Build Tools 2022, add `C++ ATL for latest v143 build tools (x86 & x64)`, component ID `Microsoft.VisualStudio.Component.VC.ATL`.
@@ -257,9 +266,10 @@ Example runner paths:
```text
D:\SDKs\slang-2026.8-windows-x86_64
D:\SDKs\msdf-atlas-gen
```
If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default under `3rdParty/`.
If `SLANG_ROOT` or `MSDF_ATLAS_GEN_ROOT` is not set, the workflow falls back to the repo-local defaults under `3rdParty/`.
## Still Todo

View File

@@ -63,6 +63,11 @@ float grainScalar(float2 uv)
return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
}
float4 sampleVhsFrame(float2 uv)
{
return sampleVideo(saturate(uv));
}
float3 animatedChromaGrain(float2 uv, float time, float2 outputResolution, float grainSize)
{
float safeGrainSize = max(grainSize, 0.001);
@@ -142,15 +147,15 @@ float3 softBloom(float2 uv, float2 outputResolution, float radius)
float2 dx = float2(pixel.x * radius, 0.0);
float2 dy = float2(0.0, pixel.y * radius);
float3 sum = sampleVideo(frac(uv)).rgb * 0.28;
sum += sampleVideo(frac(uv + dx)).rgb * 0.14;
sum += sampleVideo(frac(uv - dx)).rgb * 0.14;
sum += sampleVideo(frac(uv + dy)).rgb * 0.14;
sum += sampleVideo(frac(uv - dy)).rgb * 0.14;
sum += sampleVideo(frac(uv + dx + dy)).rgb * 0.075;
sum += sampleVideo(frac(uv + dx - dy)).rgb * 0.075;
sum += sampleVideo(frac(uv - dx + dy)).rgb * 0.075;
sum += sampleVideo(frac(uv - dx - dy)).rgb * 0.075;
float3 sum = sampleVhsFrame(uv).rgb * 0.28;
sum += sampleVhsFrame(uv + dx).rgb * 0.14;
sum += sampleVhsFrame(uv - dx).rgb * 0.14;
sum += sampleVhsFrame(uv + dy).rgb * 0.14;
sum += sampleVhsFrame(uv - dy).rgb * 0.14;
sum += sampleVhsFrame(uv + dx + dy).rgb * 0.075;
sum += sampleVhsFrame(uv + dx - dy).rgb * 0.075;
sum += sampleVhsFrame(uv - dx + dy).rgb * 0.075;
sum += sampleVhsFrame(uv - dx - dy).rgb * 0.075;
return sum;
}
@@ -158,11 +163,11 @@ float3 softCrossBlur(float2 uv, float2 outputResolution, float radius)
{
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
float2 offset = pixel * radius;
float3 sum = sampleVideo(frac(uv)).rgb * 0.40;
sum += sampleVideo(frac(uv + float2(offset.x, 0.0))).rgb * 0.15;
sum += sampleVideo(frac(uv - float2(offset.x, 0.0))).rgb * 0.15;
sum += sampleVideo(frac(uv + float2(0.0, offset.y))).rgb * 0.15;
sum += sampleVideo(frac(uv - float2(0.0, offset.y))).rgb * 0.15;
float3 sum = sampleVhsFrame(uv).rgb * 0.40;
sum += sampleVhsFrame(uv + float2(offset.x, 0.0)).rgb * 0.15;
sum += sampleVhsFrame(uv - float2(offset.x, 0.0)).rgb * 0.15;
sum += sampleVhsFrame(uv + float2(0.0, offset.y)).rgb * 0.15;
sum += sampleVhsFrame(uv - float2(0.0, offset.y)).rgb * 0.15;
return sum;
}
@@ -174,14 +179,14 @@ float3 applyChromaCrawl(float3 color, float2 uv, float time, float2 outputResolu
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
float lumaCenter = dot(color, float3(0.299, 0.587, 0.114));
float lumaX = dot(sampleVideo(frac(uv + float2(pixel.x, 0.0))).rgb, float3(0.299, 0.587, 0.114));
float lumaY = dot(sampleVideo(frac(uv + float2(0.0, pixel.y))).rgb, float3(0.299, 0.587, 0.114));
float lumaX = dot(sampleVhsFrame(uv + float2(pixel.x, 0.0)).rgb, float3(0.299, 0.587, 0.114));
float lumaY = dot(sampleVhsFrame(uv + float2(0.0, pixel.y)).rgb, float3(0.299, 0.587, 0.114));
float edge = saturate((abs(lumaX - lumaCenter) + abs(lumaY - lumaCenter)) * 6.0);
float phase = sin(uv.y * outputResolution.y * 1.35 + time * 36.0) * cos(uv.x * outputResolution.x * 0.55 - time * 21.0);
float2 crawlOffset = float2(phase, -phase * 0.35) * pixel * (1.0 + amount * 8.0);
float3 shiftedA = sampleVideo(frac(uv + crawlOffset)).rgb;
float3 shiftedB = sampleVideo(frac(uv - crawlOffset * 0.75)).rgb;
float3 shiftedA = sampleVhsFrame(uv + crawlOffset).rgb;
float3 shiftedB = sampleVhsFrame(uv - crawlOffset * 0.75).rgb;
float3 crawled = color;
crawled.r = lerp(color.r, shiftedA.r, edge * amount);
crawled.b = lerp(color.b, shiftedB.b, edge * amount);
@@ -249,7 +254,7 @@ float3 blurVhs(float2 uv, float d, int sampleCount)
break;
float2 offset = circle(start, float(sampleCount), float(i)) * scale;
sum += sampleVideo(frac(uv + offset)).rgb * weight;
sum += sampleVhsFrame(uv + offset).rgb * weight;
}
return sum;
@@ -299,23 +304,23 @@ float4 buildTapeSmear(ShaderContext context)
float4 finishVhs(ShaderContext context)
{
float time = distortedTapeTime(context);
float3 color = sampleVideo(context.uv).rgb;
float3 color = sampleVhsFrame(context.uv).rgb;
// Radial red/blue offsets create lens and deck misregistration before the
// wider tape effects are layered in.
float2 centered = context.uv * 2.0 - 1.0;
centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0);
float2 aberrationOffset = centered * (aberrationAmount * 0.0015);
float redAberration = sampleVideo(frac(context.uv + aberrationOffset)).r;
float blueAberration = sampleVideo(frac(context.uv - aberrationOffset)).b;
float redAberration = sampleVhsFrame(context.uv + aberrationOffset).r;
float blueAberration = sampleVhsFrame(context.uv - aberrationOffset).b;
color.r = lerp(color.r, redAberration, 0.35);
color.b = lerp(color.b, blueAberration, 0.35);
float2 halationOffset = float2(0.0015, 0.0) * (1.0 + smear * 0.35);
float3 halationSource =
sampleVideo(frac(context.uv + halationOffset)).rgb * 0.4 +
sampleVideo(frac(context.uv - halationOffset)).rgb * 0.4 +
sampleVideo(frac(context.uv + halationOffset * 2.0)).rgb * 0.2;
sampleVhsFrame(context.uv + halationOffset).rgb * 0.4 +
sampleVhsFrame(context.uv - halationOffset).rgb * 0.4 +
sampleVhsFrame(context.uv + halationOffset * 2.0).rgb * 0.2;
float halationLuma = dot(halationSource, float3(0.299, 0.587, 0.114));
float halationMask = smoothstep(0.45, 1.0, halationLuma) * halationAmount;
color += halationSource * float3(1.0, 0.38, 0.24) * halationMask * 0.35;

View File

@@ -89,7 +89,7 @@ Intentionally not included yet:
- additional input format conversion/scaling
- temporal/history/feedback shader storage
- texture/LUT asset upload
- text-parameter rasterization
- text-parameter rasterization and font atlas GL binding
- runtime state
- OSC control
- persistent control/state writes
@@ -141,6 +141,7 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
- [ ] Feedback buffers
- [ ] Texture asset loading and upload
- [ ] LUT asset loading and upload
- [x] CPU-side MSDF/MTSDF font atlas generation cache
- [ ] Text parameter rasterization
- [ ] Trigger history/event buffers for overlapping repeated trigger effects
- [ ] Full runtime state store/read model
@@ -352,6 +353,7 @@ Current runtime shader support is deliberately limited to stateless full-frame p
- no temporal history
- no feedback storage
- no texture/LUT assets yet
- font atlas generation is CPU-side only; text parameter atlas upload/binding is not render-ready yet
- no text parameters yet
- manifest defaults initialize parameters
- HTTP controls can update runtime parameter values without rebuilding GL programs when the shader program is unchanged

View File

@@ -17,7 +17,14 @@ void RuntimeLayerController::LoadSupportedShaderCatalog(const std::string& shade
return;
}
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
std::size_t preparedFontAtlases = 0;
for (const auto& entry : mShaderCatalog.FontAtlases())
preparedFontAtlases += entry.second.size();
Log(
"runtime-shader",
"Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) +
" shader(s), prepared " + std::to_string(preparedFontAtlases) + " font atlas asset(s).");
}
void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)

View File

@@ -113,7 +113,11 @@ GLuint RuntimeRenderScene::RenderLayer(
{
sourceTexture = videoInputTexture;
}
else if (inputName != "layerInput")
else if (inputName == "layerInput")
{
sourceTexture = layerInputTexture;
}
else
{
// Named intermediate pass inputs currently use the gVideoInput binding slot as the
// selected pass source. Layer stack shaders should use gLayerInput for previous-layer

View File

@@ -0,0 +1,180 @@
#include "FontAtlasBuilder.h"
#include "NativeHandles.h"
#include <algorithm>
#include <cctype>
#include <fstream>
#include <sstream>
#include <system_error>
#include <windows.h>
namespace RenderCadenceCompositor
{
namespace
{
std::string QuotePath(const std::filesystem::path& path)
{
return "\"" + path.string() + "\"";
}
std::string NumberText(double value)
{
std::ostringstream stream;
stream << value;
return stream.str();
}
}
FontAtlasBuilder::FontAtlasBuilder(FontAtlasBuildConfig config) :
mConfig(std::move(config))
{
if (mConfig.cacheRoot.empty())
mConfig.cacheRoot = mConfig.repoRoot / "runtime" / "font_cache";
}
bool FontAtlasBuilder::BuildPackageFontAtlases(
const ShaderPackage& shaderPackage,
std::vector<FontAtlasBuildOutput>& outputs,
std::string& error) const
{
outputs.clear();
for (const ShaderFontAsset& fontAsset : shaderPackage.fontAssets)
{
FontAtlasBuildOutput output;
if (!BuildFontAtlas(shaderPackage, fontAsset, output, error))
return false;
outputs.push_back(std::move(output));
}
error.clear();
return true;
}
bool FontAtlasBuilder::BuildFontAtlas(
const ShaderPackage& shaderPackage,
const ShaderFontAsset& fontAsset,
FontAtlasBuildOutput& output,
std::string& error) const
{
output = FontAtlasBuildOutput();
if (fontAsset.id.empty())
{
error = "Font asset id is empty for shader package '" + shaderPackage.id + "'.";
return false;
}
if (fontAsset.path.empty() || !std::filesystem::exists(fontAsset.path))
{
error = "Font asset '" + fontAsset.id + "' does not exist: " + fontAsset.path.string();
return false;
}
std::filesystem::path executablePath;
if (!FindMsdfAtlasGenExecutable(mConfig.repoRoot, executablePath))
{
error = "Could not find msdf-atlas-gen.exe under 3rdParty/msdf-atlas-gen.";
return false;
}
const std::filesystem::path packageCache = PackageCacheDirectory(shaderPackage);
std::error_code fsError;
std::filesystem::create_directories(packageCache, fsError);
if (fsError)
{
error = "Could not create font atlas cache directory: " + packageCache.string();
return false;
}
const std::string outputStem = SanitizePathToken(shaderPackage.id + "-" + fontAsset.id);
output.fontId = fontAsset.id;
output.imagePath = packageCache / (outputStem + ".png");
output.jsonPath = packageCache / (outputStem + ".json");
const std::string commandLine =
QuotePath(executablePath) +
" -font " + QuotePath(fontAsset.path) +
" -fontname " + fontAsset.id +
" -type " + mConfig.atlasType +
" -format png" +
" -size " + NumberText(mConfig.sizePixelsPerEm) +
" -pxrange " + NumberText(mConfig.pixelRange) +
" -yorigin top" +
" -imageout " + QuotePath(output.imagePath) +
" -json " + QuotePath(output.jsonPath);
if (!RunProcess(commandLine, mConfig.repoRoot, error))
return false;
if (!std::filesystem::exists(output.imagePath) || !std::filesystem::exists(output.jsonPath) ||
std::filesystem::file_size(output.imagePath) == 0 || std::filesystem::file_size(output.jsonPath) == 0)
{
error = "msdf-atlas-gen did not produce expected atlas outputs for font '" + fontAsset.id + "'.";
return false;
}
error.clear();
return true;
}
bool FontAtlasBuilder::FindMsdfAtlasGenExecutable(const std::filesystem::path& repoRoot, std::filesystem::path& executablePath)
{
const std::filesystem::path expectedPath = repoRoot / "3rdParty" / "msdf-atlas-gen" / "msdf-atlas-gen.exe";
if (std::filesystem::exists(expectedPath))
{
executablePath = expectedPath;
return true;
}
return false;
}
std::filesystem::path FontAtlasBuilder::PackageCacheDirectory(const ShaderPackage& shaderPackage) const
{
return mConfig.cacheRoot / SanitizePathToken(shaderPackage.id);
}
std::string FontAtlasBuilder::SanitizePathToken(const std::string& value)
{
std::string sanitized;
sanitized.reserve(value.size());
for (unsigned char character : value)
{
if (std::isalnum(character) || character == '-' || character == '_')
sanitized.push_back(static_cast<char>(character));
else
sanitized.push_back('_');
}
return sanitized.empty() ? "font" : sanitized;
}
bool FontAtlasBuilder::RunProcess(const std::string& commandLine, const std::filesystem::path& workingDirectory, std::string& error)
{
STARTUPINFOA startupInfo = {};
PROCESS_INFORMATION processInfo = {};
startupInfo.cb = sizeof(startupInfo);
std::vector<char> mutableCommandLine(commandLine.begin(), commandLine.end());
mutableCommandLine.push_back('\0');
if (!CreateProcessA(nullptr, mutableCommandLine.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, workingDirectory.string().c_str(), &startupInfo, &processInfo))
{
error = "Failed to launch msdf-atlas-gen.exe.";
return false;
}
UniqueHandle processHandle(processInfo.hProcess);
UniqueHandle threadHandle(processInfo.hThread);
WaitForSingleObject(processHandle.get(), INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(processHandle.get(), &exitCode);
if (exitCode != 0)
{
error = "msdf-atlas-gen.exe returned a non-zero exit code.";
return false;
}
error.clear();
return true;
}
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include "ShaderTypes.h"
#include <filesystem>
#include <string>
#include <vector>
namespace RenderCadenceCompositor
{
struct FontAtlasBuildConfig
{
std::filesystem::path repoRoot;
std::filesystem::path cacheRoot;
double sizePixelsPerEm = 64.0;
double pixelRange = 4.0;
std::string atlasType = "mtsdf";
};
struct FontAtlasBuildOutput
{
std::string fontId;
std::filesystem::path imagePath;
std::filesystem::path jsonPath;
};
class FontAtlasBuilder
{
public:
explicit FontAtlasBuilder(FontAtlasBuildConfig config);
bool BuildPackageFontAtlases(
const ShaderPackage& shaderPackage,
std::vector<FontAtlasBuildOutput>& outputs,
std::string& error) const;
bool BuildFontAtlas(
const ShaderPackage& shaderPackage,
const ShaderFontAsset& fontAsset,
FontAtlasBuildOutput& output,
std::string& error) const;
static bool FindMsdfAtlasGenExecutable(const std::filesystem::path& repoRoot, std::filesystem::path& executablePath);
private:
std::filesystem::path PackageCacheDirectory(const ShaderPackage& shaderPackage) const;
static std::string SanitizePathToken(const std::string& value);
static bool RunProcess(const std::string& commandLine, const std::filesystem::path& workingDirectory, std::string& error);
FontAtlasBuildConfig mConfig;
};
}

View File

@@ -66,6 +66,7 @@ bool SupportedShaderCatalog::Load(const std::filesystem::path& shaderRoot, unsig
{
mShaders.clear();
mPackagesById.clear();
mFontAtlasesByShaderId.clear();
if (shaderRoot.empty())
{
@@ -80,6 +81,10 @@ bool SupportedShaderCatalog::Load(const std::filesystem::path& shaderRoot, unsig
if (!registry.Scan(shaderRoot, packagesById, packageOrder, packageStatuses, error))
return false;
FontAtlasBuildConfig fontConfig;
fontConfig.repoRoot = shaderRoot.parent_path();
FontAtlasBuilder fontAtlasBuilder(fontConfig);
for (const std::string& packageId : packageOrder)
{
const auto packageIt = packagesById.find(packageId);
@@ -87,6 +92,14 @@ bool SupportedShaderCatalog::Load(const std::filesystem::path& shaderRoot, unsig
continue;
const ShaderPackage& shaderPackage = packageIt->second;
if (!shaderPackage.fontAssets.empty())
{
std::vector<FontAtlasBuildOutput> fontAtlasOutputs;
std::string fontAtlasError;
if (fontAtlasBuilder.BuildPackageFontAtlases(shaderPackage, fontAtlasOutputs, fontAtlasError))
mFontAtlasesByShaderId[shaderPackage.id] = std::move(fontAtlasOutputs);
}
const ShaderSupportResult support = CheckStatelessSinglePassShaderSupport(shaderPackage);
if (!support.supported)
continue;

View File

@@ -1,5 +1,6 @@
#pragma once
#include "FontAtlasBuilder.h"
#include "ShaderTypes.h"
#include <filesystem>
@@ -30,10 +31,12 @@ class SupportedShaderCatalog
public:
bool Load(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error);
const std::vector<SupportedShaderSummary>& Shaders() const { return mShaders; }
const std::map<std::string, std::vector<FontAtlasBuildOutput>>& FontAtlases() const { return mFontAtlasesByShaderId; }
const ShaderPackage* FindPackage(const std::string& shaderId) const;
private:
std::vector<SupportedShaderSummary> mShaders;
std::map<std::string, ShaderPackage> mPackagesById;
std::map<std::string, std::vector<FontAtlasBuildOutput>> mFontAtlasesByShaderId;
};
}

View File

@@ -40,6 +40,7 @@ add_video_shader_test(RenderCadenceCompositorRuntimeShaderParamsTests
)
add_video_shader_test(RenderCadenceCompositorRuntimeLayerModelTests
"${SRC_DIR}/runtime/FontAtlasBuilder.cpp"
"${SRC_DIR}/runtime/RuntimeLayerModel.cpp"
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${SRC_DIR}/runtime/RuntimeParameterUtils.cpp"
@@ -48,7 +49,15 @@ add_video_shader_test(RenderCadenceCompositorRuntimeLayerModelTests
"${TEST_DIR}/RenderCadenceCompositorRuntimeLayerModelTests.cpp"
)
add_video_shader_test(FontAtlasBuilderTests
"${SRC_DIR}/runtime/FontAtlasBuilder.cpp"
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
"${TEST_DIR}/FontAtlasBuilderTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorSupportedShaderCatalogTests
"${SRC_DIR}/runtime/FontAtlasBuilder.cpp"
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${SRC_DIR}/runtime/SupportedShaderCatalog.cpp"
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
@@ -59,6 +68,7 @@ add_video_shader_test(RenderCadenceCompositorRuntimeStateJsonTests
"${SRC_DIR}/app/AppConfig.cpp"
"${SRC_DIR}/app/AppConfigProvider.cpp"
"${SRC_DIR}/json/JsonWriter.cpp"
"${SRC_DIR}/runtime/FontAtlasBuilder.cpp"
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${SRC_DIR}/runtime/RuntimeLayerModel.cpp"
"${SRC_DIR}/runtime/RuntimeParameterUtils.cpp"

View File

@@ -0,0 +1,83 @@
#include "FontAtlasBuilder.h"
#include "ShaderPackageRegistry.h"
#include <filesystem>
#include <iostream>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
std::filesystem::path RepoRoot()
{
std::filesystem::path path = std::filesystem::current_path();
while (!path.empty())
{
if (std::filesystem::exists(path / "3rdParty" / "msdf-atlas-gen" / "msdf-atlas-gen.exe"))
return path;
const std::filesystem::path parent = path.parent_path();
if (parent.empty() || parent == path)
break;
path = parent;
}
return std::filesystem::current_path();
}
void TestFindsBundledExecutable()
{
std::filesystem::path executablePath;
Expect(
RenderCadenceCompositor::FontAtlasBuilder::FindMsdfAtlasGenExecutable(RepoRoot(), executablePath),
"bundled msdf-atlas-gen executable is found");
Expect(std::filesystem::exists(executablePath), "bundled msdf-atlas-gen executable exists");
}
void TestBuildsTextOverlayFontAtlas()
{
const std::filesystem::path repoRoot = RepoRoot();
ShaderPackageRegistry registry(4);
ShaderPackage shaderPackage;
std::string error;
Expect(registry.ParseManifest(repoRoot / "shaders" / "text-overlay" / "shader.json", shaderPackage, error), "text overlay manifest parses");
Expect(!shaderPackage.fontAssets.empty(), "text overlay declares a font asset");
RenderCadenceCompositor::FontAtlasBuildConfig config;
config.repoRoot = repoRoot;
config.cacheRoot = repoRoot / "runtime" / "test_font_cache";
RenderCadenceCompositor::FontAtlasBuilder builder(config);
std::vector<RenderCadenceCompositor::FontAtlasBuildOutput> outputs;
Expect(builder.BuildPackageFontAtlases(shaderPackage, outputs, error), "text overlay font atlas builds");
Expect(outputs.size() == 1, "one font atlas output is produced");
if (!outputs.empty())
{
Expect(std::filesystem::exists(outputs[0].imagePath), "font atlas image exists");
Expect(std::filesystem::exists(outputs[0].jsonPath), "font atlas json exists");
Expect(std::filesystem::file_size(outputs[0].imagePath) > 0, "font atlas image is not empty");
Expect(std::filesystem::file_size(outputs[0].jsonPath) > 0, "font atlas json is not empty");
}
}
}
int main()
{
TestFindsBundledExecutable();
TestBuildsTextOverlayFontAtlas();
if (gFailures != 0)
{
std::cerr << gFailures << " font atlas builder test failure(s).\n";
return 1;
}
std::cout << "FontAtlasBuilder tests passed.\n";
return 0;
}

View File

@@ -1,5 +1,6 @@
#include "SupportedShaderCatalog.h"
#include <filesystem>
#include <iostream>
#include <string>
@@ -29,6 +30,21 @@ ShaderPackage MakeSinglePassPackage()
return shaderPackage;
}
std::filesystem::path RepoRoot()
{
std::filesystem::path path = std::filesystem::current_path();
while (!path.empty())
{
if (std::filesystem::exists(path / "shaders" / "text-overlay" / "shader.json"))
return path;
const std::filesystem::path parent = path.parent_path();
if (parent.empty() || parent == path)
break;
path = parent;
}
return std::filesystem::current_path();
}
void SupportsSinglePassStatelessPackage()
{
const ShaderPackage shaderPackage = MakeSinglePassPackage();
@@ -110,6 +126,24 @@ void RejectsTextParameters()
Expect(!result.supported, "text-parameter packages should be rejected for now");
Expect(result.reason.find("text") != std::string::npos, "text rejection should mention text parameters");
}
void BuildsDeclaredFontAtlasesDuringCatalogLoad()
{
RenderCadenceCompositor::SupportedShaderCatalog catalog;
std::string error;
Expect(catalog.Load(RepoRoot() / "shaders", 12, error), "shader catalog loads");
const auto& fontAtlases = catalog.FontAtlases();
const auto textOverlayIt = fontAtlases.find("text-overlay");
Expect(textOverlayIt != fontAtlases.end(), "text overlay font atlas is prepared during catalog load");
if (textOverlayIt != fontAtlases.end() && !textOverlayIt->second.empty())
{
Expect(std::filesystem::exists(textOverlayIt->second.front().imagePath), "catalog font atlas image exists");
Expect(std::filesystem::exists(textOverlayIt->second.front().jsonPath), "catalog font atlas json exists");
Expect(std::filesystem::file_size(textOverlayIt->second.front().imagePath) > 0, "catalog font atlas image is not empty");
Expect(std::filesystem::file_size(textOverlayIt->second.front().jsonPath) > 0, "catalog font atlas json is not empty");
}
}
}
int main()
@@ -120,6 +154,7 @@ int main()
RejectsTemporalPackage();
RejectsTextureAssets();
RejectsTextParameters();
BuildsDeclaredFontAtlasesDuringCatalogLoad();
if (gFailures != 0)
{