Golden rules change
This commit is contained in:
@@ -1,13 +1,7 @@
|
||||
#include "RuntimeTextTextureCache.h"
|
||||
|
||||
#include "../../runtime/RuntimeJson.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <wincodec.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -15,45 +9,6 @@ constexpr GLuint kFirstTextTextureUnit = 8;
|
||||
constexpr unsigned kTextTextureHeight = 256;
|
||||
constexpr unsigned kTextTexturePadding = 16;
|
||||
constexpr double kFontPixelsPerEm = 192.0;
|
||||
|
||||
std::string ReadTextFile(const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
return std::string();
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
const JsonValue* FindObjectValue(const JsonValue& object, const std::string& key)
|
||||
{
|
||||
return object.isObject() ? object.find(key) : nullptr;
|
||||
}
|
||||
|
||||
double NumberMember(const JsonValue& object, const std::string& key, double fallback = 0.0)
|
||||
{
|
||||
const JsonValue* value = FindObjectValue(object, key);
|
||||
return value && value->isNumber() ? value->asNumber(fallback) : fallback;
|
||||
}
|
||||
|
||||
struct ComThreadGuard
|
||||
{
|
||||
~ComThreadGuard()
|
||||
{
|
||||
if (initialized)
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
bool Initialize()
|
||||
{
|
||||
const HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
initialized = SUCCEEDED(result);
|
||||
return SUCCEEDED(result) || result == RPC_E_CHANGED_MODE;
|
||||
}
|
||||
|
||||
bool initialized = false;
|
||||
};
|
||||
}
|
||||
|
||||
RuntimeTextTextureCache::~RuntimeTextTextureCache()
|
||||
@@ -173,155 +128,32 @@ void RuntimeTextTextureCache::AssignSamplerUniforms(GLuint program, const Runtim
|
||||
bool RuntimeTextTextureCache::LoadAtlas(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const
|
||||
{
|
||||
atlas.fontId = output.fontId;
|
||||
if (!LoadAtlasJson(output, atlas, error))
|
||||
return false;
|
||||
if (!LoadAtlasImage(output, atlas, error))
|
||||
return false;
|
||||
if (atlas.width == 0 || atlas.height == 0 || atlas.rgbaPixels.empty())
|
||||
atlas.width = output.width;
|
||||
atlas.height = output.height;
|
||||
atlas.ascender = output.ascender;
|
||||
atlas.descender = output.descender;
|
||||
atlas.lineHeight = output.lineHeight;
|
||||
atlas.rgbaPixels = output.rgbaPixels;
|
||||
for (const auto& entry : output.glyphsByCodepoint)
|
||||
{
|
||||
error = "Font atlas image is empty for font '" + output.fontId + "'.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeTextTextureCache::LoadAtlasJson(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const
|
||||
{
|
||||
const std::string jsonText = ReadTextFile(output.jsonPath);
|
||||
if (jsonText.empty())
|
||||
{
|
||||
error = "Could not read font atlas json: " + output.jsonPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonValue root;
|
||||
if (!ParseJson(jsonText, root, error))
|
||||
return false;
|
||||
|
||||
const JsonValue* metrics = FindObjectValue(root, "metrics");
|
||||
if (metrics)
|
||||
{
|
||||
atlas.ascender = NumberMember(*metrics, "ascender", atlas.ascender);
|
||||
atlas.descender = NumberMember(*metrics, "descender", atlas.descender);
|
||||
atlas.lineHeight = NumberMember(*metrics, "lineHeight", atlas.lineHeight);
|
||||
}
|
||||
|
||||
const JsonValue* glyphs = FindObjectValue(root, "glyphs");
|
||||
if (!glyphs || !glyphs->isArray())
|
||||
{
|
||||
error = "Font atlas json has no glyph array: " + output.jsonPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const JsonValue& glyphJson : glyphs->asArray())
|
||||
{
|
||||
if (!glyphJson.isObject())
|
||||
continue;
|
||||
|
||||
const unsigned codepoint = static_cast<unsigned>(NumberMember(glyphJson, "unicode", 0.0));
|
||||
Glyph glyph;
|
||||
glyph.advance = NumberMember(glyphJson, "advance", 0.0);
|
||||
|
||||
const JsonValue* planeBounds = FindObjectValue(glyphJson, "planeBounds");
|
||||
const JsonValue* atlasBounds = FindObjectValue(glyphJson, "atlasBounds");
|
||||
if (planeBounds && atlasBounds)
|
||||
{
|
||||
glyph.planeLeft = NumberMember(*planeBounds, "left", 0.0);
|
||||
glyph.planeTop = NumberMember(*planeBounds, "top", 0.0);
|
||||
glyph.planeRight = NumberMember(*planeBounds, "right", 0.0);
|
||||
glyph.planeBottom = NumberMember(*planeBounds, "bottom", 0.0);
|
||||
glyph.atlasLeft = NumberMember(*atlasBounds, "left", 0.0);
|
||||
glyph.atlasTop = NumberMember(*atlasBounds, "top", 0.0);
|
||||
glyph.atlasRight = NumberMember(*atlasBounds, "right", 0.0);
|
||||
glyph.atlasBottom = NumberMember(*atlasBounds, "bottom", 0.0);
|
||||
glyph.hasBounds = true;
|
||||
glyph.advance = entry.second.advance;
|
||||
glyph.planeLeft = entry.second.planeLeft;
|
||||
glyph.planeTop = entry.second.planeTop;
|
||||
glyph.planeRight = entry.second.planeRight;
|
||||
glyph.planeBottom = entry.second.planeBottom;
|
||||
glyph.atlasLeft = entry.second.atlasLeft;
|
||||
glyph.atlasTop = entry.second.atlasTop;
|
||||
glyph.atlasRight = entry.second.atlasRight;
|
||||
glyph.atlasBottom = entry.second.atlasBottom;
|
||||
glyph.hasBounds = entry.second.hasBounds;
|
||||
atlas.glyphsByCodepoint[entry.first] = glyph;
|
||||
}
|
||||
|
||||
atlas.glyphsByCodepoint[codepoint] = glyph;
|
||||
}
|
||||
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeTextTextureCache::LoadAtlasImage(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const
|
||||
if (atlas.width == 0 || atlas.height == 0 || atlas.rgbaPixels.empty() || atlas.glyphsByCodepoint.empty())
|
||||
{
|
||||
ComThreadGuard comGuard;
|
||||
if (!comGuard.Initialize())
|
||||
{
|
||||
error = "Could not initialize COM for font atlas PNG loading.";
|
||||
error = "Prepared font atlas data is empty for font '" + output.fontId + "'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICImagingFactory> factory;
|
||||
HRESULT result = CoCreateInstance(
|
||||
CLSID_WICImagingFactory,
|
||||
nullptr,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(factory.GetAddressOf()));
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not create WIC imaging factory for font atlas PNG loading.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICBitmapDecoder> decoder;
|
||||
result = factory->CreateDecoderFromFilename(
|
||||
output.imagePath.wstring().c_str(),
|
||||
nullptr,
|
||||
GENERIC_READ,
|
||||
WICDecodeMetadataCacheOnLoad,
|
||||
decoder.GetAddressOf());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not decode font atlas PNG: " + output.imagePath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICBitmapFrameDecode> frame;
|
||||
result = decoder->GetFrame(0, frame.GetAddressOf());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not read font atlas PNG frame: " + output.imagePath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICFormatConverter> converter;
|
||||
result = factory->CreateFormatConverter(converter.GetAddressOf());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not create WIC format converter for font atlas PNG.";
|
||||
return false;
|
||||
}
|
||||
|
||||
result = converter->Initialize(
|
||||
frame.Get(),
|
||||
GUID_WICPixelFormat32bppRGBA,
|
||||
WICBitmapDitherTypeNone,
|
||||
nullptr,
|
||||
0.0,
|
||||
WICBitmapPaletteTypeCustom);
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not convert font atlas PNG to RGBA.";
|
||||
return false;
|
||||
}
|
||||
|
||||
UINT width = 0;
|
||||
UINT height = 0;
|
||||
converter->GetSize(&width, &height);
|
||||
atlas.width = static_cast<unsigned>(width);
|
||||
atlas.height = static_cast<unsigned>(height);
|
||||
atlas.rgbaPixels.assign(static_cast<std::size_t>(atlas.width) * atlas.height * 4u, 0);
|
||||
|
||||
const UINT stride = width * 4u;
|
||||
result = converter->CopyPixels(nullptr, stride, static_cast<UINT>(atlas.rgbaPixels.size()), atlas.rgbaPixels.data());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not copy font atlas PNG pixels.";
|
||||
return false;
|
||||
}
|
||||
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -63,8 +63,6 @@ private:
|
||||
};
|
||||
|
||||
bool LoadAtlas(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const;
|
||||
bool LoadAtlasJson(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const;
|
||||
bool LoadAtlasImage(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const;
|
||||
bool EnsureTextTexture(TextTexture& texture);
|
||||
std::vector<unsigned char> ComposeTextTexture(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) const;
|
||||
const Atlas* FindAtlas(const std::string& fontId) const;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#include "FontAtlasBuilder.h"
|
||||
|
||||
#include "NativeHandles.h"
|
||||
#include "RuntimeJson.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
#include <wincodec.h>
|
||||
#include <wrl/client.h>
|
||||
#include <windows.h>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
@@ -24,6 +27,45 @@ std::string NumberText(double value)
|
||||
stream << value;
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string ReadTextFile(const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
return std::string();
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
const JsonValue* FindObjectValue(const JsonValue& object, const std::string& key)
|
||||
{
|
||||
return object.isObject() ? object.find(key) : nullptr;
|
||||
}
|
||||
|
||||
double NumberMember(const JsonValue& object, const std::string& key, double fallback = 0.0)
|
||||
{
|
||||
const JsonValue* value = FindObjectValue(object, key);
|
||||
return value && value->isNumber() ? value->asNumber(fallback) : fallback;
|
||||
}
|
||||
|
||||
struct ComThreadGuard
|
||||
{
|
||||
~ComThreadGuard()
|
||||
{
|
||||
if (initialized)
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
bool Initialize()
|
||||
{
|
||||
const HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
initialized = SUCCEEDED(result);
|
||||
return SUCCEEDED(result) || result == RPC_E_CHANGED_MODE;
|
||||
}
|
||||
|
||||
bool initialized = false;
|
||||
};
|
||||
}
|
||||
|
||||
FontAtlasBuilder::FontAtlasBuilder(FontAtlasBuildConfig config) :
|
||||
@@ -111,6 +153,15 @@ bool FontAtlasBuilder::BuildFontAtlas(
|
||||
error = "msdf-atlas-gen did not produce expected atlas outputs for font '" + fontAsset.id + "'.";
|
||||
return false;
|
||||
}
|
||||
if (!LoadAtlasJson(output, error))
|
||||
return false;
|
||||
if (!LoadAtlasImage(output, error))
|
||||
return false;
|
||||
if (output.width == 0 || output.height == 0 || output.rgbaPixels.empty())
|
||||
{
|
||||
error = "Prepared font atlas image is empty for font '" + fontAsset.id + "'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
error.clear();
|
||||
return true;
|
||||
@@ -133,6 +184,148 @@ std::filesystem::path FontAtlasBuilder::PackageCacheDirectory(const ShaderPackag
|
||||
return mConfig.cacheRoot / SanitizePathToken(shaderPackage.id);
|
||||
}
|
||||
|
||||
bool FontAtlasBuilder::LoadAtlasJson(FontAtlasBuildOutput& output, std::string& error) const
|
||||
{
|
||||
const std::string jsonText = ReadTextFile(output.jsonPath);
|
||||
if (jsonText.empty())
|
||||
{
|
||||
error = "Could not read font atlas json: " + output.jsonPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonValue root;
|
||||
if (!ParseJson(jsonText, root, error))
|
||||
return false;
|
||||
|
||||
const JsonValue* metrics = FindObjectValue(root, "metrics");
|
||||
if (metrics)
|
||||
{
|
||||
output.ascender = NumberMember(*metrics, "ascender", output.ascender);
|
||||
output.descender = NumberMember(*metrics, "descender", output.descender);
|
||||
output.lineHeight = NumberMember(*metrics, "lineHeight", output.lineHeight);
|
||||
}
|
||||
|
||||
const JsonValue* glyphs = FindObjectValue(root, "glyphs");
|
||||
if (!glyphs || !glyphs->isArray())
|
||||
{
|
||||
error = "Font atlas json has no glyph array: " + output.jsonPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
output.glyphsByCodepoint.clear();
|
||||
for (const JsonValue& glyphJson : glyphs->asArray())
|
||||
{
|
||||
if (!glyphJson.isObject())
|
||||
continue;
|
||||
|
||||
const unsigned codepoint = static_cast<unsigned>(NumberMember(glyphJson, "unicode", 0.0));
|
||||
FontAtlasBuildOutput::Glyph glyph;
|
||||
glyph.advance = NumberMember(glyphJson, "advance", 0.0);
|
||||
|
||||
const JsonValue* planeBounds = FindObjectValue(glyphJson, "planeBounds");
|
||||
const JsonValue* atlasBounds = FindObjectValue(glyphJson, "atlasBounds");
|
||||
if (planeBounds && atlasBounds)
|
||||
{
|
||||
glyph.planeLeft = NumberMember(*planeBounds, "left", 0.0);
|
||||
glyph.planeTop = NumberMember(*planeBounds, "top", 0.0);
|
||||
glyph.planeRight = NumberMember(*planeBounds, "right", 0.0);
|
||||
glyph.planeBottom = NumberMember(*planeBounds, "bottom", 0.0);
|
||||
glyph.atlasLeft = NumberMember(*atlasBounds, "left", 0.0);
|
||||
glyph.atlasTop = NumberMember(*atlasBounds, "top", 0.0);
|
||||
glyph.atlasRight = NumberMember(*atlasBounds, "right", 0.0);
|
||||
glyph.atlasBottom = NumberMember(*atlasBounds, "bottom", 0.0);
|
||||
glyph.hasBounds = true;
|
||||
}
|
||||
|
||||
output.glyphsByCodepoint[codepoint] = glyph;
|
||||
}
|
||||
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FontAtlasBuilder::LoadAtlasImage(FontAtlasBuildOutput& output, std::string& error) const
|
||||
{
|
||||
ComThreadGuard comGuard;
|
||||
if (!comGuard.Initialize())
|
||||
{
|
||||
error = "Could not initialize COM for font atlas PNG loading.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICImagingFactory> factory;
|
||||
HRESULT result = CoCreateInstance(
|
||||
CLSID_WICImagingFactory,
|
||||
nullptr,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(factory.GetAddressOf()));
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not create WIC imaging factory for font atlas PNG loading.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICBitmapDecoder> decoder;
|
||||
result = factory->CreateDecoderFromFilename(
|
||||
output.imagePath.wstring().c_str(),
|
||||
nullptr,
|
||||
GENERIC_READ,
|
||||
WICDecodeMetadataCacheOnLoad,
|
||||
decoder.GetAddressOf());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not decode font atlas PNG: " + output.imagePath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICBitmapFrameDecode> frame;
|
||||
result = decoder->GetFrame(0, frame.GetAddressOf());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not read font atlas PNG frame: " + output.imagePath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICFormatConverter> converter;
|
||||
result = factory->CreateFormatConverter(converter.GetAddressOf());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not create WIC format converter for font atlas PNG.";
|
||||
return false;
|
||||
}
|
||||
|
||||
result = converter->Initialize(
|
||||
frame.Get(),
|
||||
GUID_WICPixelFormat32bppRGBA,
|
||||
WICBitmapDitherTypeNone,
|
||||
nullptr,
|
||||
0.0,
|
||||
WICBitmapPaletteTypeCustom);
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not convert font atlas PNG to RGBA.";
|
||||
return false;
|
||||
}
|
||||
|
||||
UINT width = 0;
|
||||
UINT height = 0;
|
||||
converter->GetSize(&width, &height);
|
||||
output.width = static_cast<unsigned>(width);
|
||||
output.height = static_cast<unsigned>(height);
|
||||
output.rgbaPixels.assign(static_cast<std::size_t>(output.width) * output.height * 4u, 0);
|
||||
|
||||
const UINT stride = width * 4u;
|
||||
result = converter->CopyPixels(nullptr, stride, static_cast<UINT>(output.rgbaPixels.size()), output.rgbaPixels.data());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not copy font atlas PNG pixels.";
|
||||
return false;
|
||||
}
|
||||
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string FontAtlasBuilder::SanitizePathToken(const std::string& value)
|
||||
{
|
||||
std::string sanitized;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -19,9 +20,30 @@ struct FontAtlasBuildConfig
|
||||
|
||||
struct FontAtlasBuildOutput
|
||||
{
|
||||
struct Glyph
|
||||
{
|
||||
double advance = 0.0;
|
||||
double planeLeft = 0.0;
|
||||
double planeTop = 0.0;
|
||||
double planeRight = 0.0;
|
||||
double planeBottom = 0.0;
|
||||
double atlasLeft = 0.0;
|
||||
double atlasTop = 0.0;
|
||||
double atlasRight = 0.0;
|
||||
double atlasBottom = 0.0;
|
||||
bool hasBounds = false;
|
||||
};
|
||||
|
||||
std::string fontId;
|
||||
std::filesystem::path imagePath;
|
||||
std::filesystem::path jsonPath;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
double ascender = -0.9;
|
||||
double descender = 0.25;
|
||||
double lineHeight = 1.2;
|
||||
std::vector<unsigned char> rgbaPixels;
|
||||
std::map<unsigned, Glyph> glyphsByCodepoint;
|
||||
};
|
||||
|
||||
class FontAtlasBuilder
|
||||
@@ -44,6 +66,8 @@ public:
|
||||
|
||||
private:
|
||||
std::filesystem::path PackageCacheDirectory(const ShaderPackage& shaderPackage) const;
|
||||
bool LoadAtlasJson(FontAtlasBuildOutput& output, std::string& error) const;
|
||||
bool LoadAtlasImage(FontAtlasBuildOutput& output, std::string& error) const;
|
||||
static std::string SanitizePathToken(const std::string& value);
|
||||
static bool RunProcess(const std::string& commandLine, const std::filesystem::path& workingDirectory, std::string& error);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user