Texture composition for text no longer on the render thread
This commit is contained in:
@@ -1,14 +1,8 @@
|
||||
#include "RuntimeTextTextureCache.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
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<float>(textTexture.liveWidth) / static_cast<float>(textTexture.width);
|
||||
metrics.aspect = textTexture.height == 0
|
||||
? 1.0f
|
||||
: static_cast<float>(textTexture.liveWidth) / static_cast<float>(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<unsigned char> 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<GLsizei>(width), static_cast<GLsizei>(height), 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast<GLsizei>(prepared->width), static_cast<GLsizei>(prepared->height), 0, GL_RGBA, GL_UNSIGNED_BYTE, prepared->rgbaPixels->data());
|
||||
}
|
||||
else
|
||||
{
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height), GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast<GLsizei>(prepared->width), static_cast<GLsizei>(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<unsigned char> 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<unsigned>(std::ceil(static_cast<double>(texture.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 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;
|
||||
}
|
||||
|
||||
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<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))));
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeTextTextureCache::DestroyTexture(TextTexture& texture)
|
||||
{
|
||||
if (texture.texture != 0)
|
||||
|
||||
@@ -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<unsigned char> rgbaPixels;
|
||||
std::map<unsigned, Glyph> 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<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;
|
||||
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<Atlas> mAtlases;
|
||||
std::vector<TextTexture> mTextTextures;
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
185
src/runtime/RuntimeTextTextureComposer.cpp
Normal file
185
src/runtime/RuntimeTextTextureComposer.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
10
src/runtime/RuntimeTextTextureComposer.h
Normal file
10
src/runtime/RuntimeTextTextureComposer.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeShaderArtifact.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
bool PrepareRuntimeTextTextures(RuntimeShaderArtifact& artifact, std::string& error);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user