Font builder
This commit is contained in:
180
src/runtime/FontAtlasBuilder.cpp
Normal file
180
src/runtime/FontAtlasBuilder.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
52
src/runtime/FontAtlasBuilder.h
Normal file
52
src/runtime/FontAtlasBuilder.h
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user