Texture composition for text no longer on the render thread
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 2m9s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
2026-05-21 17:25:28 +10:00
parent 3fc78d5bb8
commit 5cf1a09e75
8 changed files with 330 additions and 235 deletions

View File

@@ -1,6 +1,7 @@
#include "RuntimeLayerModel.h"
#include "RuntimeParameterUtils.h"
#include "RuntimeTextTextureComposer.h"
#include <algorithm>
#include <chrono>
@@ -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));
}
}

View File

@@ -4,6 +4,7 @@
#include "FontAtlasBuilder.h"
#include <map>
#include <memory>
#include <string>
#include <vector>
@@ -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<const std::vector<unsigned char>> rgbaPixels;
};
struct RuntimeShaderArtifact
{
std::string layerId;
@@ -33,5 +44,6 @@ struct RuntimeShaderArtifact
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
std::map<std::string, RuntimeTextTextureMetrics> textTextureMetrics;
std::vector<RuntimePreparedTextTexture> preparedTextTextures;
std::vector<RenderCadenceCompositor::FontAtlasBuildOutput> fontAtlases;
};

View File

@@ -0,0 +1,185 @@
#include "RuntimeTextTextureComposer.h"
#include <algorithm>
#include <cmath>
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<double>(atlas.width) - 1.0, x));
const double clampedY = (std::max)(0.0, (std::min)(static_cast<double>(atlas.height) - 1.0, y));
const int x0 = static_cast<int>(std::floor(clampedX));
const int y0 = static_cast<int>(std::floor(clampedY));
const int x1 = (std::min)(static_cast<int>(atlas.width) - 1, x0 + 1);
const int y1 = (std::min)(static_cast<int>(atlas.height) - 1, y0 + 1);
const double tx = clampedX - static_cast<double>(x0);
const double ty = clampedY - static_cast<double>(y0);
const auto channelAt = [&atlas](int sx, int sy, unsigned channel) {
const std::size_t pixelOffset = (static_cast<std::size_t>(sy) * atlas.width + static_cast<std::size_t>(sx)) * 4u;
return static_cast<double>(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<unsigned char>((std::max)(0.0, (std::min)(255.0, std::round(value))));
}
}
std::vector<unsigned char> 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<unsigned>(std::ceil(static_cast<double>(maxLength) * kFontPixelsPerEm * 0.9)) + kTextTexturePadding * 2u;
liveWidth = (std::max)(1u, static_cast<unsigned>(std::ceil(advance * kFontPixelsPerEm)) + kTextTexturePadding * 2u);
width = (std::max)(fixedWidth, liveWidth);
height = kTextTextureHeight;
std::vector<unsigned char> texturePixels(static_cast<std::size_t>(width) * height * 4u, 0);
const double baseline = kTextTexturePadding + (-atlas.ascender * kFontPixelsPerEm);
double penX = static_cast<double>(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<int>(std::floor(penX + glyph.planeLeft * kFontPixelsPerEm));
const int destTop = static_cast<int>(std::floor(baseline + glyph.planeTop * kFontPixelsPerEm));
const int destRight = static_cast<int>(std::ceil(penX + glyph.planeRight * kFontPixelsPerEm));
const int destBottom = static_cast<int>(std::ceil(baseline + glyph.planeBottom * kFontPixelsPerEm));
const double destWidth = (std::max)(1.0, static_cast<double>(destRight - destLeft));
const double destHeight = (std::max)(1.0, static_cast<double>(destBottom - destTop));
for (int y = destTop; y < destBottom; ++y)
{
if (y < 0 || y >= static_cast<int>(height))
continue;
const double v = (static_cast<double>(y) + 0.5 - destTop) / destHeight;
for (int x = destLeft; x < destRight; ++x)
{
if (x < 0 || x >= static_cast<int>(width))
continue;
const double u = (static_cast<double>(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<std::size_t>(y) * width + static_cast<std::size_t>(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<std::size_t>(y) * width * 4u;
unsigned char* bottomRow = texturePixels.data() + static_cast<std::size_t>(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<unsigned char> 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<const std::vector<unsigned char>>(std::move(pixels));
RuntimeTextTextureMetrics metrics;
metrics.activeWidthScale = prepared.width == 0
? 1.0f
: static_cast<float>(prepared.liveWidth) / static_cast<float>(prepared.width);
metrics.aspect = prepared.height == 0
? 1.0f
: static_cast<float>(prepared.liveWidth) / static_cast<float>(prepared.height);
artifact.textTextureMetrics[prepared.parameterId] = metrics;
artifact.preparedTextTextures.push_back(std::move(prepared));
}
error.clear();
return true;
}
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include "RuntimeShaderArtifact.h"
#include <string>
namespace RenderCadenceCompositor
{
bool PrepareRuntimeTextTextures(RuntimeShaderArtifact& artifact, std::string& error);
}