From 5cf1a09e7514485a783784193887749e590ce897 Mon Sep 17 00:00:00 2001 From: Aiden Date: Thu, 21 May 2026 17:25:28 +1000 Subject: [PATCH] Texture composition for text no longer on the render thread --- .../runtime/RuntimeTextTextureCache.cpp | 222 ++---------------- src/render/runtime/RuntimeTextTextureCache.h | 35 +-- src/runtime/RuntimeLayerModel.cpp | 22 ++ src/runtime/RuntimeShaderArtifact.h | 12 + src/runtime/RuntimeTextTextureComposer.cpp | 185 +++++++++++++++ src/runtime/RuntimeTextTextureComposer.h | 10 + tests/CMakeLists.txt | 2 + ...adenceCompositorRuntimeLayerModelTests.cpp | 77 ++++++ 8 files changed, 330 insertions(+), 235 deletions(-) create mode 100644 src/runtime/RuntimeTextTextureComposer.cpp create mode 100644 src/runtime/RuntimeTextTextureComposer.h diff --git a/src/render/runtime/RuntimeTextTextureCache.cpp b/src/render/runtime/RuntimeTextTextureCache.cpp index b9029a1..97eb552 100644 --- a/src/render/runtime/RuntimeTextTextureCache.cpp +++ b/src/render/runtime/RuntimeTextTextureCache.cpp @@ -1,14 +1,8 @@ #include "RuntimeTextTextureCache.h" -#include -#include - namespace { constexpr GLuint kFirstTextTextureUnit = 8; -constexpr unsigned kTextTextureHeight = 256; -constexpr unsigned kTextTexturePadding = 16; -constexpr double kFontPixelsPerEm = 192.0; } RuntimeTextTextureCache::~RuntimeTextTextureCache() @@ -21,28 +15,19 @@ bool RuntimeTextTextureCache::Configure(const RuntimeShaderArtifact& artifact, s 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) + if (FindPreparedTexture(definition.id) == nullptr) { - error = "No prepared font atlas is available for text parameter '" + definition.id + "'."; + error = "No prepared text texture 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)); } @@ -55,27 +40,16 @@ void RuntimeTextTextureCache::UpdateArtifactState(const RuntimeShaderArtifact& a { mArtifact.parameterDefinitions = artifact.parameterDefinitions; mArtifact.parameterValues = artifact.parameterValues; + mArtifact.textTextureMetrics = artifact.textTextureMetrics; + mArtifact.preparedTextTextures = artifact.preparedTextTextures; } void RuntimeTextTextureCache::RefreshTextTextures(RuntimeShaderArtifact* artifactState) { - if (artifactState) - artifactState->textTextureMetrics.clear(); for (TextTexture& textTexture : mTextTextures) - { EnsureTextTexture(textTexture); - if (artifactState) - { - RuntimeTextTextureMetrics metrics; - metrics.activeWidthScale = textTexture.width == 0 - ? 1.0f - : static_cast(textTexture.liveWidth) / static_cast(textTexture.width); - metrics.aspect = textTexture.height == 0 - ? 1.0f - : static_cast(textTexture.liveWidth) / static_cast(textTexture.height); - artifactState->textTextureMetrics[textTexture.parameterId] = metrics; - } - } + if (artifactState) + artifactState->textTextureMetrics = mArtifact.textTextureMetrics; } void RuntimeTextTextureCache::BindTextTextures(GLuint program) @@ -100,7 +74,6 @@ void RuntimeTextTextureCache::ShutdownGl() for (TextTexture& texture : mTextTextures) DestroyTexture(texture); mTextTextures.clear(); - mAtlases.clear(); } void RuntimeTextTextureCache::AssignSamplerUniforms(GLuint program, const RuntimeShaderArtifact& artifact) @@ -125,57 +98,14 @@ void RuntimeTextTextureCache::AssignSamplerUniforms(GLuint program, const Runtim glUseProgram(0); } -bool RuntimeTextTextureCache::LoadAtlas(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const -{ - atlas.fontId = output.fontId; - 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) - { - Glyph glyph; - 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; - } - if (atlas.width == 0 || atlas.height == 0 || atlas.rgbaPixels.empty() || atlas.glyphsByCodepoint.empty()) - { - error = "Prepared font atlas data is empty for font '" + output.fontId + "'."; - 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) + const RuntimePreparedTextTexture* prepared = FindPreparedTexture(texture.parameterId); + if (!prepared || !prepared->rgbaPixels || prepared->rgbaPixels->empty() || prepared->width == 0 || prepared->height == 0) + return false; + if (texture.texture != 0 && texture.cachedText == prepared->textValue && texture.width == prepared->width && texture.height == prepared->height) return true; - const Atlas* atlas = FindAtlas(texture.fontId); - if (!atlas) - return false; - - unsigned width = 0; - unsigned height = 0; - unsigned liveWidth = 1; - std::vector pixels = ComposeTextTexture(*atlas, texture, text, width, height, liveWidth); - if (pixels.empty() || width == 0 || height == 0) - return false; - if (texture.texture == 0) glGenTextures(1, &texture.texture); @@ -183,149 +113,39 @@ bool RuntimeTextTextureCache::EnsureTextTexture(TextTexture& texture) glGetIntegerv(GL_UNPACK_ALIGNMENT, &previousUnpackAlignment); glBindTexture(GL_TEXTURE_2D, texture.texture); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - if (texture.width != width || texture.height != height) + if (texture.width != prepared->width || texture.height != prepared->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_RGBA8, static_cast(width), static_cast(height), 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast(prepared->width), static_cast(prepared->height), 0, GL_RGBA, GL_UNSIGNED_BYTE, prepared->rgbaPixels->data()); } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast(width), static_cast(height), GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast(prepared->width), static_cast(prepared->height), GL_RGBA, GL_UNSIGNED_BYTE, prepared->rgbaPixels->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; + texture.cachedText = prepared->textValue; + texture.width = prepared->width; + texture.height = prepared->height; + texture.liveWidth = prepared->liveWidth; return texture.texture != 0; } -std::vector RuntimeTextTextureCache::ComposeTextTexture(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) const +const RuntimePreparedTextTexture* RuntimeTextTextureCache::FindPreparedTexture(const std::string& parameterId) const { - double advance = 0.0; - for (unsigned char character : text) + for (const RuntimePreparedTextTexture& prepared : mArtifact.preparedTextTextures) { - 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 texturePixels(static_cast(width) * height * 4u, 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 sample[4] = {}; - SampleAtlasPixel(atlas, atlasX, atlasY, sample); - unsigned char* destination = texturePixels.data() + (static_cast(y) * width + static_cast(x)) * 4u; - for (unsigned channel = 0; channel < 4u; ++channel) - destination[channel] = (std::max)(destination[channel], sample[channel]); - } - } - } - - penX += glyph.advance * kFontPixelsPerEm; - } - - for (unsigned y = 0; y < height / 2u; ++y) - { - unsigned char* topRow = texturePixels.data() + static_cast(y) * width * 4u; - unsigned char* bottomRow = texturePixels.data() + static_cast(height - 1u - y) * width * 4u; - for (unsigned x = 0; x < width * 4u; ++x) - std::swap(topRow[x], bottomRow[x]); - } - - return texturePixels; -} - -const RuntimeTextTextureCache::Atlas* RuntimeTextTextureCache::FindAtlas(const std::string& fontId) const -{ - for (const Atlas& atlas : mAtlases) - { - if (atlas.fontId == fontId) - return &atlas; + if (prepared.parameterId == parameterId) + return &prepared; } 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(); -} - -void RuntimeTextTextureCache::SampleAtlasPixel(const Atlas& atlas, double x, double y, unsigned char* rgba) -{ - const double clampedX = (std::max)(0.0, (std::min)(static_cast(atlas.width) - 1.0, x)); - const double clampedY = (std::max)(0.0, (std::min)(static_cast(atlas.height) - 1.0, y)); - const int x0 = static_cast(std::floor(clampedX)); - const int y0 = static_cast(std::floor(clampedY)); - const int x1 = (std::min)(static_cast(atlas.width) - 1, x0 + 1); - const int y1 = (std::min)(static_cast(atlas.height) - 1, y0 + 1); - const double tx = clampedX - static_cast(x0); - const double ty = clampedY - static_cast(y0); - - const auto channelAt = [&atlas](int sx, int sy, unsigned channel) { - const std::size_t pixelOffset = (static_cast(sy) * atlas.width + static_cast(sx)) * 4u; - return static_cast(atlas.rgbaPixels[pixelOffset + channel]); - }; - for (unsigned channel = 0; channel < 4u; ++channel) - { - const double top = channelAt(x0, y0, channel) * (1.0 - tx) + channelAt(x1, y0, channel) * tx; - const double bottom = channelAt(x0, y1, channel) * (1.0 - tx) + channelAt(x1, y1, channel) * tx; - const double value = top * (1.0 - ty) + bottom * ty; - rgba[channel] = static_cast((std::max)(0.0, (std::min)(255.0, std::round(value)))); - } -} - void RuntimeTextTextureCache::DestroyTexture(TextTexture& texture) { if (texture.texture != 0) diff --git a/src/render/runtime/RuntimeTextTextureCache.h b/src/render/runtime/RuntimeTextTextureCache.h index e1db6ec..76d6e7d 100644 --- a/src/render/runtime/RuntimeTextTextureCache.h +++ b/src/render/runtime/RuntimeTextTextureCache.h @@ -24,36 +24,9 @@ public: static void AssignSamplerUniforms(GLuint program, const RuntimeShaderArtifact& artifact); private: - 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; - }; - - struct Atlas - { - std::string fontId; - unsigned width = 0; - unsigned height = 0; - double ascender = -0.9; - double descender = 0.25; - double lineHeight = 1.2; - std::vector rgbaPixels; - std::map glyphsByCodepoint; - }; - struct TextTexture { std::string parameterId; - std::string fontId; std::string cachedText; GLuint texture = 0; unsigned width = 0; @@ -62,16 +35,10 @@ private: unsigned maxLength = 64; }; - bool LoadAtlas(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const; bool EnsureTextTexture(TextTexture& texture); - std::vector 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; - static const ShaderParameterValue* FindParameterValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId); - static std::string DefaultTextValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId); - static void SampleAtlasPixel(const Atlas& atlas, double x, double y, unsigned char* rgba); + const RuntimePreparedTextTexture* FindPreparedTexture(const std::string& parameterId) const; static void DestroyTexture(TextTexture& texture); RuntimeShaderArtifact mArtifact; - std::vector mAtlases; std::vector mTextTextures; }; diff --git a/src/runtime/RuntimeLayerModel.cpp b/src/runtime/RuntimeLayerModel.cpp index 9c198de..96c44ea 100644 --- a/src/runtime/RuntimeLayerModel.cpp +++ b/src/runtime/RuntimeLayerModel.cpp @@ -1,6 +1,7 @@ #include "RuntimeLayerModel.h" #include "RuntimeParameterUtils.h" +#include "RuntimeTextTextureComposer.h" #include #include @@ -238,7 +239,11 @@ bool RuntimeLayerModel::UpdateParameter(const std::string& layerId, const std::s layer->parameterValues[parameterId] = normalizedValue; if (layer->renderReady) + { layer->artifact.parameterValues = layer->parameterValues; + if (definition->type == ShaderParameterType::Text && !PrepareRuntimeTextTextures(layer->artifact, error)) + return false; + } error.clear(); return true; } @@ -256,7 +261,11 @@ bool RuntimeLayerModel::ResetParameters(const std::string& layerId, std::string& for (const ShaderParameterDefinition& definition : layer->parameterDefinitions) layer->parameterValues[definition.id] = DefaultValueForDefinition(definition); if (layer->renderReady) + { layer->artifact.parameterValues = layer->parameterValues; + if (!PrepareRuntimeTextTextures(layer->artifact, error)) + return false; + } error.clear(); return true; } @@ -394,7 +403,11 @@ bool RuntimeLayerModel::ReloadFromCatalog(const SupportedShaderCatalog& shaderCa layer.parameterDefinitions = shaderPackage->parameters; layer.parameterValues = std::move(nextValues); if (layer.renderReady) + { layer.artifact.parameterValues = layer.parameterValues; + std::string prepareError; + PrepareRuntimeTextTextures(layer.artifact, prepareError); + } buildsToStart.push_back({ layer.id, layer.shaderId }); } @@ -442,6 +455,14 @@ bool RuntimeLayerModel::MarkBuildReady(const RuntimeShaderArtifact& artifact, st layer->renderReady = true; layer->artifact = artifact; layer->artifact.parameterValues = layer->parameterValues; + if (!PrepareRuntimeTextTextures(layer->artifact, error)) + { + layer->buildState = RuntimeLayerBuildState::Failed; + layer->message = error; + layer->renderReady = false; + layer->artifact = RuntimeShaderArtifact(); + return false; + } error.clear(); return true; } @@ -498,6 +519,7 @@ RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const renderLayer.bypass = layer.bypass; renderLayer.artifact = layer.artifact; renderLayer.artifact.parameterValues = layer.parameterValues; + renderLayer.artifact.fontAtlases.clear(); snapshot.renderLayers.push_back(std::move(renderLayer)); } } diff --git a/src/runtime/RuntimeShaderArtifact.h b/src/runtime/RuntimeShaderArtifact.h index fbfd472..2a3bd9a 100644 --- a/src/runtime/RuntimeShaderArtifact.h +++ b/src/runtime/RuntimeShaderArtifact.h @@ -4,6 +4,7 @@ #include "FontAtlasBuilder.h" #include +#include #include #include @@ -21,6 +22,16 @@ struct RuntimeTextTextureMetrics float aspect = 1.0f; }; +struct RuntimePreparedTextTexture +{ + std::string parameterId; + std::string textValue; + unsigned width = 0; + unsigned height = 0; + unsigned liveWidth = 1; + std::shared_ptr> rgbaPixels; +}; + struct RuntimeShaderArtifact { std::string layerId; @@ -33,5 +44,6 @@ struct RuntimeShaderArtifact std::vector parameterDefinitions; std::map parameterValues; std::map textTextureMetrics; + std::vector preparedTextTextures; std::vector fontAtlases; }; diff --git a/src/runtime/RuntimeTextTextureComposer.cpp b/src/runtime/RuntimeTextTextureComposer.cpp new file mode 100644 index 0000000..591086d --- /dev/null +++ b/src/runtime/RuntimeTextTextureComposer.cpp @@ -0,0 +1,185 @@ +#include "RuntimeTextTextureComposer.h" + +#include +#include + +namespace RenderCadenceCompositor +{ +namespace +{ +constexpr unsigned kTextTextureHeight = 256; +constexpr unsigned kTextTexturePadding = 16; +constexpr double kFontPixelsPerEm = 192.0; + +const FontAtlasBuildOutput* FindAtlas(const RuntimeShaderArtifact& artifact, const std::string& fontId) +{ + for (const FontAtlasBuildOutput& atlas : artifact.fontAtlases) + { + if (atlas.fontId == fontId) + return &atlas; + } + return nullptr; +} + +const ShaderParameterValue* 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 TextValueForDefinition(const RuntimeShaderArtifact& artifact, const ShaderParameterDefinition& definition) +{ + const ShaderParameterValue* value = FindParameterValue(artifact, definition.id); + return value ? value->textValue : definition.defaultTextValue; +} + +void SampleAtlasPixel(const FontAtlasBuildOutput& atlas, double x, double y, unsigned char* rgba) +{ + const double clampedX = (std::max)(0.0, (std::min)(static_cast(atlas.width) - 1.0, x)); + const double clampedY = (std::max)(0.0, (std::min)(static_cast(atlas.height) - 1.0, y)); + const int x0 = static_cast(std::floor(clampedX)); + const int y0 = static_cast(std::floor(clampedY)); + const int x1 = (std::min)(static_cast(atlas.width) - 1, x0 + 1); + const int y1 = (std::min)(static_cast(atlas.height) - 1, y0 + 1); + const double tx = clampedX - static_cast(x0); + const double ty = clampedY - static_cast(y0); + + const auto channelAt = [&atlas](int sx, int sy, unsigned channel) { + const std::size_t pixelOffset = (static_cast(sy) * atlas.width + static_cast(sx)) * 4u; + return static_cast(atlas.rgbaPixels[pixelOffset + channel]); + }; + for (unsigned channel = 0; channel < 4u; ++channel) + { + const double top = channelAt(x0, y0, channel) * (1.0 - tx) + channelAt(x1, y0, channel) * tx; + const double bottom = channelAt(x0, y1, channel) * (1.0 - tx) + channelAt(x1, y1, channel) * tx; + const double value = top * (1.0 - ty) + bottom * ty; + rgba[channel] = static_cast((std::max)(0.0, (std::min)(255.0, std::round(value)))); + } +} + +std::vector ComposeTextTexture( + const FontAtlasBuildOutput& atlas, + const ShaderParameterDefinition& definition, + const std::string& text, + unsigned& width, + unsigned& height, + unsigned& liveWidth) +{ + 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 maxLength = definition.maxLength == 0 ? 64 : definition.maxLength; + const unsigned fixedWidth = static_cast(std::ceil(static_cast(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 texturePixels(static_cast(width) * height * 4u, 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 FontAtlasBuildOutput::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 sample[4] = {}; + SampleAtlasPixel(atlas, atlasX, atlasY, sample); + unsigned char* destination = texturePixels.data() + (static_cast(y) * width + static_cast(x)) * 4u; + for (unsigned channel = 0; channel < 4u; ++channel) + destination[channel] = (std::max)(destination[channel], sample[channel]); + } + } + } + + penX += glyph.advance * kFontPixelsPerEm; + } + + for (unsigned y = 0; y < height / 2u; ++y) + { + unsigned char* topRow = texturePixels.data() + static_cast(y) * width * 4u; + unsigned char* bottomRow = texturePixels.data() + static_cast(height - 1u - y) * width * 4u; + for (unsigned x = 0; x < width * 4u; ++x) + std::swap(topRow[x], bottomRow[x]); + } + + return texturePixels; +} +} + +bool PrepareRuntimeTextTextures(RuntimeShaderArtifact& artifact, std::string& error) +{ + artifact.preparedTextTextures.clear(); + artifact.textTextureMetrics.clear(); + + for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions) + { + if (definition.type != ShaderParameterType::Text) + continue; + + const FontAtlasBuildOutput* atlas = FindAtlas(artifact, definition.fontId); + if (atlas == nullptr) + { + error = "No prepared font atlas is available for text parameter '" + definition.id + "'."; + return false; + } + if (atlas->width == 0 || atlas->height == 0 || atlas->rgbaPixels.empty() || atlas->glyphsByCodepoint.empty()) + { + error = "Prepared font atlas data is empty for font '" + definition.fontId + "'."; + return false; + } + + RuntimePreparedTextTexture prepared; + prepared.parameterId = definition.id; + prepared.textValue = TextValueForDefinition(artifact, definition); + std::vector pixels = ComposeTextTexture(*atlas, definition, prepared.textValue, prepared.width, prepared.height, prepared.liveWidth); + if (pixels.empty() || prepared.width == 0 || prepared.height == 0) + { + error = "Could not prepare text texture for parameter '" + definition.id + "'."; + return false; + } + prepared.rgbaPixels = std::make_shared>(std::move(pixels)); + + RuntimeTextTextureMetrics metrics; + metrics.activeWidthScale = prepared.width == 0 + ? 1.0f + : static_cast(prepared.liveWidth) / static_cast(prepared.width); + metrics.aspect = prepared.height == 0 + ? 1.0f + : static_cast(prepared.liveWidth) / static_cast(prepared.height); + artifact.textTextureMetrics[prepared.parameterId] = metrics; + artifact.preparedTextTextures.push_back(std::move(prepared)); + } + + error.clear(); + return true; +} +} diff --git a/src/runtime/RuntimeTextTextureComposer.h b/src/runtime/RuntimeTextTextureComposer.h new file mode 100644 index 0000000..c83bdf7 --- /dev/null +++ b/src/runtime/RuntimeTextTextureComposer.h @@ -0,0 +1,10 @@ +#pragma once + +#include "RuntimeShaderArtifact.h" + +#include + +namespace RenderCadenceCompositor +{ +bool PrepareRuntimeTextTextures(RuntimeShaderArtifact& artifact, std::string& error); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 69ff1a3..f7cb756 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -44,6 +44,7 @@ add_video_shader_test(RenderCadenceCompositorRuntimeLayerModelTests "${SRC_DIR}/runtime/RuntimeLayerModel.cpp" "${SRC_DIR}/runtime/RuntimeJson.cpp" "${SRC_DIR}/runtime/RuntimeParameterUtils.cpp" + "${SRC_DIR}/runtime/RuntimeTextTextureComposer.cpp" "${SRC_DIR}/runtime/SupportedShaderCatalog.cpp" "${SRC_DIR}/shader/ShaderPackageRegistry.cpp" "${TEST_DIR}/RenderCadenceCompositorRuntimeLayerModelTests.cpp" @@ -79,6 +80,7 @@ add_video_shader_test(RenderCadenceCompositorRuntimeStateJsonTests "${SRC_DIR}/runtime/RuntimeJson.cpp" "${SRC_DIR}/runtime/RuntimeLayerModel.cpp" "${SRC_DIR}/runtime/RuntimeParameterUtils.cpp" + "${SRC_DIR}/runtime/RuntimeTextTextureComposer.cpp" "${SRC_DIR}/runtime/SupportedShaderCatalog.cpp" "${SRC_DIR}/shader/ShaderPackageRegistry.cpp" "${TEST_DIR}/RenderCadenceCompositorRuntimeStateJsonTests.cpp" diff --git a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp index 5c50775..b9c0ed3 100644 --- a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp +++ b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp @@ -94,6 +94,42 @@ RenderCadenceCompositor::SupportedShaderCatalog MakeCatalog(std::filesystem::pat return LoadCatalog(root); } +RenderCadenceCompositor::FontAtlasBuildOutput MakeFakeFontAtlas() +{ + RenderCadenceCompositor::FontAtlasBuildOutput atlas; + atlas.fontId = "inter"; + atlas.width = 2; + atlas.height = 2; + atlas.ascender = -0.8; + atlas.rgbaPixels = { + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255, + 255, 255, 255, 255 + }; + + RenderCadenceCompositor::FontAtlasBuildOutput::Glyph glyph; + glyph.advance = 0.5; + glyph.planeLeft = 0.0; + glyph.planeTop = 0.0; + glyph.planeRight = 0.4; + glyph.planeBottom = 0.6; + glyph.atlasLeft = 0.0; + glyph.atlasTop = 0.0; + glyph.atlasRight = 1.0; + glyph.atlasBottom = 1.0; + glyph.hasBounds = true; + atlas.glyphsByCodepoint['A'] = glyph; + atlas.glyphsByCodepoint['B'] = glyph; + atlas.glyphsByCodepoint['D'] = glyph; + atlas.glyphsByCodepoint['E'] = glyph; + atlas.glyphsByCodepoint['F'] = glyph; + atlas.glyphsByCodepoint['L'] = glyph; + atlas.glyphsByCodepoint['T'] = glyph; + atlas.glyphsByCodepoint['U'] = glyph; + return atlas; +} + void TestSingleLayerLifecycle() { std::filesystem::path root; @@ -368,6 +404,46 @@ void TestReloadRefreshesChangedShaderMetadataAndPreservesValues() std::filesystem::remove_all(root); } + +void TestTextTexturesArePreparedInRuntimeModel() +{ + std::filesystem::path root = MakeTestRoot(); + WriteFile(root / "all-params" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n"); + WriteFile(root / "all-params" / "Inter.ttf", "not a real font, but enough for catalog support checks"); + WriteFile(root / "all-params" / "shader.json", AllParametersShaderManifest()); + RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root); + + RenderCadenceCompositor::RuntimeLayerModel model; + std::string error; + Expect(model.InitializeSingleLayer(catalog, "all-params", error), "text layer can initialize"); + + RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); + RuntimeShaderArtifact artifact; + artifact.layerId = model.FirstLayerId(); + artifact.shaderId = "all-params"; + artifact.displayName = "All Params"; + artifact.fragmentShaderSource = "void main(){}"; + artifact.parameterDefinitions = snapshot.displayLayers[0].parameterDefinitions; + artifact.fontAtlases.push_back(MakeFakeFontAtlas()); + artifact.message = "build ready"; + Expect(model.MarkBuildReady(artifact, error), error.empty() ? "ready text artifact prepares textures" : error); + + snapshot = model.Snapshot(); + Expect(snapshot.renderLayers.size() == 1, "text artifact is render-ready"); + Expect(snapshot.renderLayers[0].artifact.preparedTextTextures.size() == 1, "render snapshot carries prepared text texture"); + Expect(snapshot.renderLayers[0].artifact.fontAtlases.empty(), "render snapshot does not carry font atlas pixels"); + const RuntimePreparedTextTexture preparedDefault = snapshot.renderLayers[0].artifact.preparedTextTextures[0]; + Expect(preparedDefault.textValue == "DEFAULT", "default text is prepared"); + Expect(preparedDefault.rgbaPixels && !preparedDefault.rgbaPixels->empty(), "prepared text has pixels"); + + Expect(model.UpdateParameter(model.FirstLayerId(), "titleText", JsonValue("AB"), error), error.empty() ? "text parameter update prepares texture" : error); + snapshot = model.Snapshot(); + const RuntimePreparedTextTexture preparedUpdated = snapshot.renderLayers[0].artifact.preparedTextTextures[0]; + Expect(preparedUpdated.textValue == "AB", "updated text is prepared before render snapshot"); + Expect(preparedUpdated.rgbaPixels && preparedUpdated.rgbaPixels != preparedDefault.rgbaPixels, "updated text receives a new prepared pixel payload"); + + std::filesystem::remove_all(root); +} } int main() @@ -380,6 +456,7 @@ int main() TestInvalidRuntimeStateCanFallBackToConfiguredShader(); TestLayerControlsUpdateDisplayAndRenderModels(); TestReloadRefreshesChangedShaderMetadataAndPreservesValues(); + TestTextTexturesArePreparedInRuntimeModel(); if (gFailures != 0) {