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

@@ -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;
};
}