#include "RuntimeTextTextureCache.h" #include "../../runtime/RuntimeJson.h" #include #include #include #include #include #include namespace { constexpr GLuint kFirstTextTextureUnit = 8; constexpr unsigned kTextTextureHeight = 128; constexpr unsigned kTextTexturePadding = 8; constexpr double kFontPixelsPerEm = 96.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() { ShutdownGl(); } bool RuntimeTextTextureCache::Configure(const RuntimeShaderArtifact& artifact, std::string& error) { ShutdownGl(); mArtifact = artifact; for (const RenderCadenceCompositor::FontAtlasBuildOutput& output : artifact.fontAtlases) { Atlas atlas; if (!LoadAtlas(output, atlas, error)) return false; mAtlases.push_back(std::move(atlas)); } for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions) { if (definition.type != ShaderParameterType::Text) continue; if (FindAtlas(definition.fontId) == nullptr) { error = "No prepared font atlas is available for text parameter '" + definition.id + "'."; return false; } TextTexture texture; texture.parameterId = definition.id; texture.fontId = definition.fontId; texture.maxLength = definition.maxLength == 0 ? 64 : definition.maxLength; mTextTextures.push_back(std::move(texture)); } error.clear(); return true; } void RuntimeTextTextureCache::UpdateArtifactState(const RuntimeShaderArtifact& artifact) { mArtifact.parameterDefinitions = artifact.parameterDefinitions; mArtifact.parameterValues = artifact.parameterValues; } void RuntimeTextTextureCache::RefreshTextTextures(RuntimeShaderArtifact* artifactState) { if (artifactState) artifactState->textTextureWidthScales.clear(); for (TextTexture& textTexture : mTextTextures) { EnsureTextTexture(textTexture); if (artifactState) { const float scale = textTexture.width == 0 ? 1.0f : static_cast(textTexture.liveWidth) / static_cast(textTexture.width); artifactState->textTextureWidthScales[textTexture.parameterId] = scale; } } } void RuntimeTextTextureCache::BindTextTextures(GLuint program) { for (std::size_t index = 0; index < mTextTextures.size(); ++index) { const TextTexture& textTexture = mTextTextures[index]; if (textTexture.texture == 0) continue; glActiveTexture(GL_TEXTURE0 + kFirstTextTextureUnit + static_cast(index)); glBindTexture(GL_TEXTURE_2D, textTexture.texture); } glUseProgram(program); AssignSamplerUniforms(program, mArtifact); glUseProgram(0); glActiveTexture(GL_TEXTURE0); } void RuntimeTextTextureCache::ShutdownGl() { for (TextTexture& texture : mTextTextures) DestroyTexture(texture); mTextTextures.clear(); mAtlases.clear(); } void RuntimeTextTextureCache::AssignSamplerUniforms(GLuint program, const RuntimeShaderArtifact& artifact) { glUseProgram(program); GLuint nextUnit = kFirstTextTextureUnit; for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions) { if (definition.type != ShaderParameterType::Text) continue; const std::string samplerName = definition.id + "Texture"; const GLint location = glGetUniformLocation(program, samplerName.c_str()); if (location >= 0) glUniform1i(location, static_cast(nextUnit)); const std::string samplerArrayName = samplerName + "_0"; const GLint arrayLocation = glGetUniformLocation(program, samplerArrayName.c_str()); if (arrayLocation >= 0) glUniform1i(arrayLocation, static_cast(nextUnit)); ++nextUnit; } glUseProgram(0); } 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()) { 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(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; } atlas.glyphsByCodepoint[codepoint] = glyph; } error.clear(); return true; } bool RuntimeTextTextureCache::LoadAtlasImage(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const { ComThreadGuard comGuard; if (!comGuard.Initialize()) { error = "Could not initialize COM for font atlas PNG loading."; return false; } Microsoft::WRL::ComPtr 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 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 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 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(width); atlas.height = static_cast(height); atlas.rgbaPixels.assign(static_cast(atlas.width) * atlas.height * 4u, 0); const UINT stride = width * 4u; result = converter->CopyPixels(nullptr, stride, static_cast(atlas.rgbaPixels.size()), atlas.rgbaPixels.data()); if (FAILED(result)) { error = "Could not copy font atlas PNG pixels."; return false; } error.clear(); return true; } bool RuntimeTextTextureCache::EnsureTextTexture(TextTexture& texture) { const ShaderParameterValue* value = FindParameterValue(mArtifact, texture.parameterId); const std::string text = value ? value->textValue : DefaultTextValue(mArtifact, texture.parameterId); if (texture.texture != 0 && texture.cachedText == text) return true; const Atlas* atlas = FindAtlas(texture.fontId); if (!atlas) return false; unsigned width = 0; unsigned height = 0; unsigned liveWidth = 1; std::vector pixels = ComposeTextMask(*atlas, texture, text, width, height, liveWidth); if (pixels.empty() || width == 0 || height == 0) return false; if (texture.texture == 0) glGenTextures(1, &texture.texture); GLint previousUnpackAlignment = 4; glGetIntegerv(GL_UNPACK_ALIGNMENT, &previousUnpackAlignment); glBindTexture(GL_TEXTURE_2D, texture.texture); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); if (texture.width != width || texture.height != height) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, static_cast(width), static_cast(height), 0, GL_RED, GL_UNSIGNED_BYTE, pixels.data()); } else { glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast(width), static_cast(height), GL_RED, GL_UNSIGNED_BYTE, pixels.data()); } glBindTexture(GL_TEXTURE_2D, 0); glPixelStorei(GL_UNPACK_ALIGNMENT, previousUnpackAlignment); glActiveTexture(GL_TEXTURE0); texture.cachedText = text; texture.width = width; texture.height = height; texture.liveWidth = liveWidth; return texture.texture != 0; } std::vector RuntimeTextTextureCache::ComposeTextMask(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) const { double advance = 0.0; for (unsigned char character : text) { const auto glyphIt = atlas.glyphsByCodepoint.find(character); if (glyphIt != atlas.glyphsByCodepoint.end()) advance += glyphIt->second.advance; } const unsigned fixedWidth = static_cast(std::ceil(static_cast(texture.maxLength) * kFontPixelsPerEm * 0.9)) + kTextTexturePadding * 2u; liveWidth = (std::max)(1u, static_cast(std::ceil(advance * kFontPixelsPerEm)) + kTextTexturePadding * 2u); width = (std::max)(fixedWidth, liveWidth); height = kTextTextureHeight; std::vector mask(static_cast(width) * height, 0); const double baseline = kTextTexturePadding + (-atlas.ascender * kFontPixelsPerEm); double penX = static_cast(kTextTexturePadding); for (unsigned char character : text) { const auto glyphIt = atlas.glyphsByCodepoint.find(character); if (glyphIt == atlas.glyphsByCodepoint.end()) continue; const Glyph& glyph = glyphIt->second; if (glyph.hasBounds) { const int destLeft = static_cast(std::floor(penX + glyph.planeLeft * kFontPixelsPerEm)); const int destTop = static_cast(std::floor(baseline + glyph.planeTop * kFontPixelsPerEm)); const int destRight = static_cast(std::ceil(penX + glyph.planeRight * kFontPixelsPerEm)); const int destBottom = static_cast(std::ceil(baseline + glyph.planeBottom * kFontPixelsPerEm)); const double destWidth = (std::max)(1.0, static_cast(destRight - destLeft)); const double destHeight = (std::max)(1.0, static_cast(destBottom - destTop)); for (int y = destTop; y < destBottom; ++y) { if (y < 0 || y >= static_cast(height)) continue; const double v = (static_cast(y) + 0.5 - destTop) / destHeight; for (int x = destLeft; x < destRight; ++x) { if (x < 0 || x >= static_cast(width)) continue; const double u = (static_cast(x) + 0.5 - destLeft) / destWidth; const double atlasX = glyph.atlasLeft + u * (glyph.atlasRight - glyph.atlasLeft); const double atlasY = glyph.atlasTop + v * (glyph.atlasBottom - glyph.atlasTop); unsigned char& destination = mask[static_cast(y) * width + static_cast(x)]; destination = (std::max)(destination, SampleAtlasAlpha(atlas, atlasX, atlasY)); } } } penX += glyph.advance * kFontPixelsPerEm; } for (unsigned y = 0; y < height / 2u; ++y) { unsigned char* topRow = mask.data() + static_cast(y) * width; unsigned char* bottomRow = mask.data() + static_cast(height - 1u - y) * width; for (unsigned x = 0; x < width; ++x) std::swap(topRow[x], bottomRow[x]); } return mask; } const RuntimeTextTextureCache::Atlas* RuntimeTextTextureCache::FindAtlas(const std::string& fontId) const { for (const Atlas& atlas : mAtlases) { if (atlas.fontId == fontId) return &atlas; } return nullptr; } const ShaderParameterValue* RuntimeTextTextureCache::FindParameterValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId) { const auto valueIt = artifact.parameterValues.find(parameterId); return valueIt == artifact.parameterValues.end() ? nullptr : &valueIt->second; } std::string RuntimeTextTextureCache::DefaultTextValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId) { for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions) { if (definition.id == parameterId) return definition.defaultTextValue; } return std::string(); } unsigned char RuntimeTextTextureCache::SampleAtlasAlpha(const Atlas& atlas, double x, double y) { const int ix = (std::max)(0, (std::min)(static_cast(atlas.width) - 1, static_cast(std::floor(x)))); const int iy = (std::max)(0, (std::min)(static_cast(atlas.height) - 1, static_cast(std::floor(y)))); const std::size_t pixelOffset = (static_cast(iy) * atlas.width + static_cast(ix)) * 4u; return atlas.rgbaPixels[pixelOffset + 3u]; } void RuntimeTextTextureCache::DestroyTexture(TextTexture& texture) { if (texture.texture != 0) glDeleteTextures(1, &texture.texture); texture.texture = 0; texture.width = 0; texture.height = 0; texture.cachedText.clear(); }