Initial font work

This commit is contained in:
2026-05-05 23:18:50 +10:00
parent fd0ebb8d40
commit 3e8b472f74
20 changed files with 873 additions and 84 deletions

View File

@@ -47,7 +47,10 @@
#include <cstdint>
#include <cstring>
#include <cctype>
#include <gdiplus.h>
#include <wincodec.h>
#include <limits>
#include <memory>
#include <set>
#include <sstream>
#include <string>
@@ -64,6 +67,9 @@ constexpr GLuint kSourceHistoryTextureUnitBase = 2;
constexpr GLuint kPackedVideoTextureUnit = 2;
constexpr GLuint kGlobalParamsBindingPoint = 0;
constexpr unsigned kPrerollFrameCount = 8;
constexpr unsigned kTextTextureWidth = 1024;
constexpr unsigned kTextTextureHeight = 128;
constexpr int kTextSdfSpread = 10;
const char* kVertexShaderSource =
"#version 430 core\n"
"out vec2 vTexCoord;\n"
@@ -108,6 +114,156 @@ void CopyErrorMessage(const std::string& message, int errorMessageSize, char* er
strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
}
std::wstring Utf8ToWide(const std::string& text)
{
if (text.empty())
return std::wstring();
const int required = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, NULL, 0);
if (required <= 1)
return std::wstring();
std::wstring wide(static_cast<std::size_t>(required - 1), L'\0');
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, wide.data(), required);
return wide;
}
std::string TextValueForBinding(const RuntimeRenderState& state, const std::string& parameterId)
{
auto valueIt = state.parameterValues.find(parameterId);
return valueIt == state.parameterValues.end() ? std::string() : valueIt->second.textValue;
}
const ShaderFontAsset* FindFontAssetForParameter(const RuntimeRenderState& state, const ShaderParameterDefinition& definition)
{
if (!definition.fontId.empty())
{
for (const ShaderFontAsset& fontAsset : state.fontAssets)
{
if (fontAsset.id == definition.fontId)
return &fontAsset;
}
}
return state.fontAssets.empty() ? nullptr : &state.fontAssets.front();
}
std::vector<unsigned char> BuildLocalSdf(const std::vector<unsigned char>& alpha, unsigned width, unsigned height)
{
std::vector<unsigned char> sdf(static_cast<std::size_t>(width) * height * 4, 0);
for (unsigned y = 0; y < height; ++y)
{
for (unsigned x = 0; x < width; ++x)
{
const bool inside = alpha[static_cast<std::size_t>(y) * width + x] > 127;
int bestDistanceSq = kTextSdfSpread * kTextSdfSpread;
for (int oy = -kTextSdfSpread; oy <= kTextSdfSpread; ++oy)
{
const int sy = static_cast<int>(y) + oy;
if (sy < 0 || sy >= static_cast<int>(height))
continue;
for (int ox = -kTextSdfSpread; ox <= kTextSdfSpread; ++ox)
{
const int sx = static_cast<int>(x) + ox;
if (sx < 0 || sx >= static_cast<int>(width))
continue;
const bool sampleInside = alpha[static_cast<std::size_t>(sy) * width + sx] > 127;
if (sampleInside == inside)
continue;
const int distanceSq = ox * ox + oy * oy;
if (distanceSq < bestDistanceSq)
bestDistanceSq = distanceSq;
}
}
const float distance = std::sqrt(static_cast<float>(bestDistanceSq));
const float signedDistance = (inside ? 1.0f : -1.0f) * distance;
float normalized = 0.5f + signedDistance / static_cast<float>(kTextSdfSpread * 2);
if (normalized < 0.0f)
normalized = 0.0f;
if (normalized > 1.0f)
normalized = 1.0f;
const unsigned char value = static_cast<unsigned char>(normalized * 255.0f + 0.5f);
const std::size_t out = (static_cast<std::size_t>(y) * width + x) * 4;
sdf[out + 0] = value;
sdf[out + 1] = value;
sdf[out + 2] = value;
sdf[out + 3] = value;
}
}
return sdf;
}
bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector<unsigned char>& sdf, std::string& error)
{
ULONG_PTR gdiplusToken = 0;
Gdiplus::GdiplusStartupInput startupInput;
if (Gdiplus::GdiplusStartup(&gdiplusToken, &startupInput, NULL) != Gdiplus::Ok)
{
error = "Could not start GDI+ for text rendering.";
return false;
}
Gdiplus::PrivateFontCollection fontCollection;
Gdiplus::FontFamily fallbackFamily(L"Arial");
Gdiplus::FontFamily* fontFamily = &fallbackFamily;
std::unique_ptr<Gdiplus::FontFamily[]> families;
const std::wstring wideFontPath = fontPath.empty() ? std::wstring() : fontPath.wstring();
if (!wideFontPath.empty())
{
if (fontCollection.AddFontFile(wideFontPath.c_str()) != Gdiplus::Ok)
{
Gdiplus::GdiplusShutdown(gdiplusToken);
error = "Could not load packaged font file for text rendering: " + fontPath.string();
return false;
}
const INT familyCount = fontCollection.GetFamilyCount();
if (familyCount <= 0)
{
Gdiplus::GdiplusShutdown(gdiplusToken);
error = "Packaged font did not contain a usable font family: " + fontPath.string();
return false;
}
families.reset(new Gdiplus::FontFamily[familyCount]);
INT found = 0;
if (fontCollection.GetFamilies(familyCount, families.get(), &found) != Gdiplus::Ok || found <= 0)
{
Gdiplus::GdiplusShutdown(gdiplusToken);
error = "Could not read the packaged font family: " + fontPath.string();
return false;
}
fontFamily = &families[0];
}
Gdiplus::Bitmap bitmap(kTextTextureWidth, kTextTextureHeight, PixelFormat32bppARGB);
Gdiplus::Graphics graphics(&bitmap);
graphics.Clear(Gdiplus::Color(0, 0, 0, 0));
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAliasGridFit);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality);
Gdiplus::Font font(fontFamily, 72.0f, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel);
Gdiplus::SolidBrush brush(Gdiplus::Color(255, 255, 255, 255));
Gdiplus::StringFormat format;
format.SetAlignment(Gdiplus::StringAlignmentNear);
format.SetLineAlignment(Gdiplus::StringAlignmentCenter);
format.SetFormatFlags(Gdiplus::StringFormatFlagsNoWrap | Gdiplus::StringFormatFlagsMeasureTrailingSpaces);
const Gdiplus::RectF layout(24.0f, 0.0f, static_cast<Gdiplus::REAL>(kTextTextureWidth - 48), static_cast<Gdiplus::REAL>(kTextTextureHeight));
const std::wstring wideText = Utf8ToWide(text);
graphics.DrawString(wideText.c_str(), -1, &font, layout, &format, &brush);
std::vector<unsigned char> alpha(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight, 0);
for (unsigned y = 0; y < kTextTextureHeight; ++y)
{
for (unsigned x = 0; x < kTextTextureWidth; ++x)
{
Gdiplus::Color pixel;
bitmap.GetPixel(x, y, &pixel);
alpha[static_cast<std::size_t>(y) * kTextTextureWidth + x] = pixel.GetAlpha();
}
}
sdf = BuildLocalSdf(alpha, kTextTextureWidth, kTextTextureHeight);
Gdiplus::GdiplusShutdown(gdiplusToken);
return true;
}
std::string NormalizeModeToken(const std::string& value)
{
std::string normalized;
@@ -1488,6 +1644,26 @@ bool OpenGLComposite::compileSingleLayerProgram(const RuntimeRenderState& state,
}
textureBindings.push_back(textureBinding);
}
std::vector<LayerProgram::TextBinding> textBindings;
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{
if (definition.type != ShaderParameterType::Text)
continue;
LayerProgram::TextBinding textBinding;
textBinding.parameterId = definition.id;
textBinding.samplerName = definition.id + "Texture";
textBinding.fontId = definition.fontId;
glGenTextures(1, &textBinding.texture);
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
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);
std::vector<unsigned char> empty(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight * 4, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextTextureWidth, kTextTextureHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, empty.data());
glBindTexture(GL_TEXTURE_2D, 0);
textBindings.push_back(textBinding);
}
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
if (globalParamsIndex != GL_INVALID_INDEX)
@@ -1517,6 +1693,13 @@ bool OpenGLComposite::compileSingleLayerProgram(const RuntimeRenderState& state,
if (textureSamplerLocation >= 0)
glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(textureBindings.size());
for (std::size_t index = 0; index < textBindings.size(); ++index)
{
const GLint textSamplerLocation = glGetUniformLocation(newProgram.get(), textBindings[index].samplerName.c_str());
if (textSamplerLocation >= 0)
glUniform1i(textSamplerLocation, static_cast<GLint>(textTextureBase + static_cast<GLuint>(index)));
}
glUseProgram(0);
layerProgram.layerId = state.layerId;
@@ -1525,6 +1708,7 @@ bool OpenGLComposite::compileSingleLayerProgram(const RuntimeRenderState& state,
layerProgram.vertexShader = newVertexShader.release();
layerProgram.fragmentShader = newFragmentShader.release();
layerProgram.textureBindings.swap(textureBindings);
layerProgram.textBindings.swap(textBindings);
return true;
}
@@ -1626,6 +1810,15 @@ void OpenGLComposite::destroySingleLayerProgram(LayerProgram& layerProgram)
}
}
layerProgram.textureBindings.clear();
for (LayerProgram::TextBinding& textBinding : layerProgram.textBindings)
{
if (textBinding.texture != 0)
{
glDeleteTextures(1, &textBinding.texture);
textBinding.texture = 0;
}
}
layerProgram.textBindings.clear();
if (layerProgram.program != 0)
{
@@ -1759,6 +1952,35 @@ bool OpenGLComposite::loadTextureAsset(const ShaderTextureAsset& textureAsset, G
return true;
}
bool OpenGLComposite::renderTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
{
const std::string text = TextValueForBinding(state, textBinding.parameterId);
if (text == textBinding.renderedText && textBinding.renderedWidth == kTextTextureWidth && textBinding.renderedHeight == kTextTextureHeight)
return true;
auto definitionIt = std::find_if(state.parameterDefinitions.begin(), state.parameterDefinitions.end(),
[&textBinding](const ShaderParameterDefinition& definition) { return definition.id == textBinding.parameterId; });
if (definitionIt == state.parameterDefinitions.end())
return true;
const ShaderFontAsset* fontAsset = FindFontAssetForParameter(state, *definitionIt);
std::filesystem::path fontPath;
if (fontAsset)
fontPath = fontAsset->path;
std::vector<unsigned char> sdf;
if (!RasterizeTextSdf(text, fontPath, sdf, error))
return false;
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kTextTextureWidth, kTextTextureHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, sdf.data());
glBindTexture(GL_TEXTURE_2D, 0);
textBinding.renderedText = text;
textBinding.renderedWidth = kTextTextureWidth;
textBinding.renderedHeight = kTextTextureHeight;
return true;
}
void OpenGLComposite::bindLayerTextureAssets(const LayerProgram& layerProgram)
{
const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0;
@@ -1768,6 +1990,12 @@ void OpenGLComposite::bindLayerTextureAssets(const LayerProgram& layerProgram)
glActiveTexture(GL_TEXTURE0 + shaderTextureBase + static_cast<GLuint>(index));
glBindTexture(GL_TEXTURE_2D, layerProgram.textureBindings[index].texture);
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(layerProgram.textureBindings.size());
for (std::size_t index = 0; index < layerProgram.textBindings.size(); ++index)
{
glActiveTexture(GL_TEXTURE0 + textTextureBase + static_cast<GLuint>(index));
glBindTexture(GL_TEXTURE_2D, layerProgram.textBindings[index].texture);
}
glActiveTexture(GL_TEXTURE0);
}
@@ -1798,8 +2026,15 @@ bool OpenGLComposite::validateTemporalTextureUnitBudget(const std::vector<Runtim
unsigned maxAssetTextures = 0;
for (const RuntimeRenderState& state : layerStates)
{
if (state.textureAssets.size() > maxAssetTextures)
maxAssetTextures = static_cast<unsigned>(state.textureAssets.size());
unsigned textTextureCount = 0;
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{
if (definition.type == ShaderParameterType::Text)
++textTextureCount;
}
const unsigned totalShaderTextures = static_cast<unsigned>(state.textureAssets.size()) + textTextureCount;
if (totalShaderTextures > maxAssetTextures)
maxAssetTextures = totalShaderTextures;
}
GLint maxTextureUnits = 0;
glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
@@ -2062,8 +2297,15 @@ void OpenGLComposite::renderEffect()
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU);
}
void OpenGLComposite::renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, const LayerProgram& layerProgram, const RuntimeRenderState& state)
void OpenGLComposite::renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, LayerProgram& layerProgram, const RuntimeRenderState& state)
{
for (LayerProgram::TextBinding& textBinding : layerProgram.textBindings)
{
std::string textError;
if (!renderTextBindingTexture(state, textBinding, textError))
OutputDebugStringA((textError + "\n").c_str());
}
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
glViewport(0, 0, mInputFrameWidth, mInputFrameHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
@@ -2086,7 +2328,7 @@ void OpenGLComposite::renderShaderProgram(GLuint sourceTexture, GLuint destinati
glBindTexture(GL_TEXTURE_2D, 0);
}
const GLuint shaderTextureBase = kSourceHistoryTextureUnitBase + historyCap + historyCap;
for (std::size_t index = 0; index < layerProgram.textureBindings.size(); ++index)
for (std::size_t index = 0; index < layerProgram.textureBindings.size() + layerProgram.textBindings.size(); ++index)
{
glActiveTexture(GL_TEXTURE0 + shaderTextureBase + static_cast<GLuint>(index));
glBindTexture(GL_TEXTURE_2D, 0);
@@ -2223,6 +2465,8 @@ bool OpenGLComposite::updateGlobalParamsBuffer(const RuntimeRenderState& state,
AppendStd140Int(buffer, selectedIndex);
break;
}
case ShaderParameterType::Text:
break;
}
}

View File

@@ -167,12 +167,24 @@ private:
GLuint texture = 0;
};
struct TextBinding
{
std::string parameterId;
std::string samplerName;
std::string fontId;
GLuint texture = 0;
std::string renderedText;
unsigned renderedWidth = 0;
unsigned renderedHeight = 0;
};
std::string layerId;
std::string shaderId;
GLuint program = 0;
GLuint vertexShader = 0;
GLuint fragmentShader = 0;
std::vector<TextureBinding> textureBindings;
std::vector<TextBinding> textBindings;
};
std::vector<LayerProgram> mLayerPrograms;
@@ -203,8 +215,9 @@ private:
void destroySingleLayerProgram(LayerProgram& layerProgram);
void destroyDecodeShaderProgram();
void renderDecodePass();
void renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, const LayerProgram& layerProgram, const RuntimeRenderState& state);
void renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, LayerProgram& layerProgram, const RuntimeRenderState& state);
bool loadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
bool renderTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
void bindLayerTextureAssets(const LayerProgram& layerProgram);
void renderEffect();
bool PollRuntimeChanges();

View File

@@ -133,6 +133,7 @@ std::string ShaderParameterTypeToString(ShaderParameterType type)
case ShaderParameterType::Color: return "color";
case ShaderParameterType::Boolean: return "bool";
case ShaderParameterType::Enum: return "enum";
case ShaderParameterType::Text: return "text";
}
return "unknown";
}
@@ -179,6 +180,11 @@ bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType&
type = ShaderParameterType::Enum;
return true;
}
if (typeName == "text")
{
type = ShaderParameterType::Text;
return true;
}
return false;
}
@@ -200,6 +206,24 @@ bool TextureAssetsEqual(const std::vector<ShaderTextureAsset>& left, const std::
return true;
}
bool FontAssetsEqual(const std::vector<ShaderFontAsset>& left, const std::vector<ShaderFontAsset>& right)
{
if (left.size() != right.size())
return false;
for (std::size_t index = 0; index < left.size(); ++index)
{
if (left[index].id != right[index].id ||
left[index].path != right[index].path ||
left[index].writeTime != right[index].writeTime)
{
return false;
}
}
return true;
}
std::string ManifestPathMessage(const std::filesystem::path& manifestPath)
{
return manifestPath.string();
@@ -379,6 +403,49 @@ bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPack
return true;
}
bool ParseFontAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* fontsValue = nullptr;
if (!OptionalArrayField(manifestJson, "fonts", fontsValue, manifestPath, error))
return false;
if (!fontsValue)
return true;
for (const JsonValue& fontJson : fontsValue->asArray())
{
if (!fontJson.isObject())
{
error = "Shader font entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string fontId;
std::string fontPath;
if (!RequireNonEmptyStringField(fontJson, "id", fontId, manifestPath, error) ||
!RequireNonEmptyStringField(fontJson, "path", fontPath, manifestPath, error))
{
error = "Shader font is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(fontId, "fonts[].id", manifestPath, error))
return false;
ShaderFontAsset fontAsset;
fontAsset.id = fontId;
fontAsset.path = shaderPackage.directoryPath / fontPath;
if (!std::filesystem::exists(fontAsset.path))
{
error = "Shader font asset not found for package " + shaderPackage.id + ": " + fontAsset.path.string();
return false;
}
fontAsset.writeTime = std::filesystem::last_write_time(fontAsset.path);
shaderPackage.fontAssets.push_back(fontAsset);
}
return true;
}
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* temporalValue = nullptr;
@@ -461,6 +528,17 @@ bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefini
return true;
}
if (definition.type == ShaderParameterType::Text)
{
if (!defaultValue->isString())
{
error = "Text parameter default must be a string for: " + definition.id;
return false;
}
definition.defaultTextValue = defaultValue->asString();
return true;
}
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
}
@@ -543,6 +621,30 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
return false;
}
if (definition.type == ShaderParameterType::Text)
{
if (const JsonValue* fontValue = parameterJson.find("font"))
{
if (!fontValue->isString())
{
error = "Text parameter 'font' must be a string for: " + definition.id;
return false;
}
definition.fontId = fontValue->asString();
if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error))
return false;
}
if (const JsonValue* maxLengthValue = parameterJson.find("maxLength"))
{
if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0)
{
error = "Text parameter 'maxLength' must be a number from 1 to 256 for: " + definition.id;
return false;
}
definition.maxLength = static_cast<unsigned>(maxLengthValue->asNumber());
}
}
if (definition.type == ShaderParameterType::Enum)
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
@@ -693,7 +795,8 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
}
if (previous->second.shaderWriteTime != item.second.shaderWriteTime ||
previous->second.manifestWriteTime != item.second.manifestWriteTime ||
!TextureAssetsEqual(previous->second.textureAssets, item.second.textureAssets))
!TextureAssetsEqual(previous->second.textureAssets, item.second.textureAssets) ||
!FontAssetsEqual(previous->second.fontAssets, item.second.fontAssets))
{
registryChanged = true;
break;
@@ -714,7 +817,8 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
if (previous->second.first != active->second.shaderWriteTime ||
previous->second.second != active->second.manifestWriteTime ||
(previousPackage != previousPackages.end() &&
!TextureAssetsEqual(previousPackage->second.textureAssets, active->second.textureAssets)))
(!TextureAssetsEqual(previousPackage->second.textureAssets, active->second.textureAssets) ||
!FontAssetsEqual(previousPackage->second.fontAssets, active->second.fontAssets))))
{
mReloadRequested = true;
}
@@ -1143,6 +1247,7 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
state.outputHeight = outputHeight;
state.parameterDefinitions = shaderIt->second.parameters;
state.textureAssets = shaderIt->second.textureAssets;
state.fontAssets = shaderIt->second.fontAssets;
state.isTemporal = shaderIt->second.temporal.enabled;
state.temporalHistorySource = shaderIt->second.temporal.historySource;
state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength;
@@ -1447,6 +1552,7 @@ bool RuntimeHost::ParseShaderManifest(const std::filesystem::path& manifestPath,
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseTemporalSettings(manifestJson, shaderPackage, mConfig.maxTemporalHistoryFrames, manifestPath, error) &&
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
}
@@ -1692,6 +1798,12 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
}
parameter.set("options", options);
}
if (definition.type == ShaderParameterType::Text)
{
parameter.set("maxLength", JsonValue(static_cast<double>(definition.maxLength)));
if (!definition.fontId.empty())
parameter.set("font", JsonValue(definition.fontId));
}
ShaderParameterValue value = DefaultValueForDefinition(definition);
auto valueIt = layer.parameterValues.find(definition.id);
@@ -1830,6 +1942,8 @@ JsonValue RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition&
return JsonValue(value.booleanValue);
case ShaderParameterType::Enum:
return JsonValue(value.enumValue);
case ShaderParameterType::Text:
return JsonValue(value.textValue);
case ShaderParameterType::Float:
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
case ShaderParameterType::Vec2:

View File

@@ -36,6 +36,21 @@ std::vector<double> JsonArrayToNumbers(const JsonValue& value)
}
return numbers;
}
std::string NormalizeTextValue(const std::string& text, unsigned maxLength)
{
std::string normalized;
normalized.reserve(std::min<std::size_t>(text.size(), maxLength));
for (unsigned char ch : text)
{
if (ch < 32 || ch > 126)
continue;
if (normalized.size() >= maxLength)
break;
normalized.push_back(static_cast<char>(ch));
}
return normalized;
}
}
std::string MakeSafePresetFileStem(const std::string& presetName)
@@ -82,6 +97,9 @@ ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition&
case ShaderParameterType::Enum:
value.enumValue = definition.defaultEnumValue;
break;
case ShaderParameterType::Text:
value.textValue = NormalizeTextValue(definition.defaultTextValue, definition.maxLength);
break;
}
return value;
}
@@ -164,6 +182,14 @@ bool NormalizeAndValidateParameterValue(const ShaderParameterDefinition& definit
error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'.";
return false;
}
case ShaderParameterType::Text:
if (!value.isString())
{
error = "Expected string value for text parameter '" + definition.id + "'.";
return false;
}
normalizedValue.textValue = NormalizeTextValue(value.asString(), definition.maxLength);
return true;
}
return false;

View File

@@ -4,6 +4,7 @@
#include "NativeHandles.h"
#include <fstream>
#include <cctype>
#include <regex>
#include <sstream>
#include <vector>
@@ -30,15 +31,29 @@ std::string SlangCBufferTypeForParameter(ShaderParameterType type)
case ShaderParameterType::Color: return "float4";
case ShaderParameterType::Boolean: return "bool";
case ShaderParameterType::Enum: return "int";
case ShaderParameterType::Text: return "";
}
return "float";
}
std::string CapitalizeIdentifier(const std::string& identifier)
{
if (identifier.empty())
return identifier;
std::string text = identifier;
text[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(text[0])));
return text;
}
std::string BuildParameterUniforms(const std::vector<ShaderParameterDefinition>& parameters)
{
std::ostringstream source;
for (const ShaderParameterDefinition& definition : parameters)
{
if (definition.type == ShaderParameterType::Text)
continue;
source << "\t" << SlangCBufferTypeForParameter(definition.type) << " " << definition.id << ";\n";
}
return source.str();
}
@@ -60,6 +75,42 @@ std::string BuildTextureSamplerDeclarations(const std::vector<ShaderTextureAsset
return source.str();
}
std::string BuildTextSamplerDeclarations(const std::vector<ShaderParameterDefinition>& parameters)
{
std::ostringstream source;
for (const ShaderParameterDefinition& definition : parameters)
{
if (definition.type != ShaderParameterType::Text)
continue;
source << "Sampler2D<float4> " << definition.id << "Texture;\n";
}
if (source.tellp() > 0)
source << "\n";
return source.str();
}
std::string BuildTextHelpers(const std::vector<ShaderParameterDefinition>& parameters)
{
std::ostringstream source;
for (const ShaderParameterDefinition& definition : parameters)
{
if (definition.type != ShaderParameterType::Text)
continue;
const std::string suffix = CapitalizeIdentifier(definition.id);
source
<< "float sample" << suffix << "(float2 uv)\n"
<< "{\n"
<< "\treturn " << definition.id << "Texture.Sample(uv).r;\n"
<< "}\n\n"
<< "float4 draw" << suffix << "(float2 uv, float4 fillColor)\n"
<< "{\n"
<< "\tfloat alpha = sample" << suffix << "(uv) * fillColor.a;\n"
<< "\treturn float4(fillColor.rgb * alpha, alpha);\n"
<< "}\n\n";
}
return source.str();
}
std::string BuildHistorySwitchCases(const std::string& samplerPrefix, unsigned historyLength)
{
std::ostringstream source;
@@ -118,6 +169,8 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", mMaxTemporalHistoryFrames));
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", mMaxTemporalHistoryFrames));
wrapperSource = ReplaceAll(wrapperSource, "{{TEXTURE_SAMPLERS}}", BuildTextureSamplerDeclarations(shaderPackage.textureAssets));
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_SAMPLERS}}", BuildTextSamplerDeclarations(shaderPackage.parameters));
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", mMaxTemporalHistoryFrames));
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", mMaxTemporalHistoryFrames));
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", shaderPackage.shaderPath.generic_string());

View File

@@ -67,6 +67,11 @@ bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType&
type = ShaderParameterType::Enum;
return true;
}
if (typeName == "text")
{
type = ShaderParameterType::Text;
return true;
}
return false;
}
@@ -283,6 +288,49 @@ bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPack
return true;
}
bool ParseFontAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* fontsValue = nullptr;
if (!OptionalArrayField(manifestJson, "fonts", fontsValue, manifestPath, error))
return false;
if (!fontsValue)
return true;
for (const JsonValue& fontJson : fontsValue->asArray())
{
if (!fontJson.isObject())
{
error = "Shader font entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string fontId;
std::string fontPath;
if (!RequireNonEmptyStringField(fontJson, "id", fontId, manifestPath, error) ||
!RequireNonEmptyStringField(fontJson, "path", fontPath, manifestPath, error))
{
error = "Shader font is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(fontId, "fonts[].id", manifestPath, error))
return false;
ShaderFontAsset fontAsset;
fontAsset.id = fontId;
fontAsset.path = shaderPackage.directoryPath / fontPath;
if (!std::filesystem::exists(fontAsset.path))
{
error = "Shader font asset not found for package " + shaderPackage.id + ": " + fontAsset.path.string();
return false;
}
fontAsset.writeTime = std::filesystem::last_write_time(fontAsset.path);
shaderPackage.fontAssets.push_back(fontAsset);
}
return true;
}
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* temporalValue = nullptr;
@@ -365,6 +413,17 @@ bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefini
return true;
}
if (definition.type == ShaderParameterType::Text)
{
if (!defaultValue->isString())
{
error = "Text parameter default must be a string for: " + definition.id;
return false;
}
definition.defaultTextValue = defaultValue->asString();
return true;
}
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
}
@@ -447,6 +506,30 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
return false;
}
if (definition.type == ShaderParameterType::Text)
{
if (const JsonValue* fontValue = parameterJson.find("font"))
{
if (!fontValue->isString())
{
error = "Text parameter 'font' must be a string for: " + definition.id;
return false;
}
definition.fontId = fontValue->asString();
if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error))
return false;
}
if (const JsonValue* maxLengthValue = parameterJson.find("maxLength"))
{
if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0)
{
error = "Text parameter 'maxLength' must be a number from 1 to 256 for: " + definition.id;
return false;
}
definition.maxLength = static_cast<unsigned>(maxLengthValue->asNumber());
}
}
if (definition.type == ShaderParameterType::Enum)
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
@@ -544,6 +627,7 @@ bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestP
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
}

View File

@@ -11,7 +11,8 @@ enum class ShaderParameterType
Vec2,
Color,
Boolean,
Enum
Enum,
Text
};
struct ShaderParameterOption
@@ -31,6 +32,9 @@ struct ShaderParameterDefinition
std::vector<double> stepNumbers;
bool defaultBoolean = false;
std::string defaultEnumValue;
std::string defaultTextValue;
std::string fontId;
unsigned maxLength = 64;
std::vector<ShaderParameterOption> enumOptions;
};
@@ -39,6 +43,7 @@ struct ShaderParameterValue
std::vector<double> numberValues;
bool booleanValue = false;
std::string enumValue;
std::string textValue;
};
enum class TemporalHistorySource
@@ -63,6 +68,13 @@ struct ShaderTextureAsset
std::filesystem::file_time_type writeTime;
};
struct ShaderFontAsset
{
std::string id;
std::filesystem::path path;
std::filesystem::file_time_type writeTime;
};
struct ShaderPackage
{
std::string id;
@@ -75,6 +87,7 @@ struct ShaderPackage
std::filesystem::path manifestPath;
std::vector<ShaderParameterDefinition> parameters;
std::vector<ShaderTextureAsset> textureAssets;
std::vector<ShaderFontAsset> fontAssets;
TemporalSettings temporal;
std::filesystem::file_time_type shaderWriteTime;
std::filesystem::file_time_type manifestWriteTime;
@@ -87,6 +100,7 @@ struct RuntimeRenderState
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
std::vector<ShaderTextureAsset> textureAssets;
std::vector<ShaderFontAsset> fontAssets;
double timeSeconds = 0.0;
double frameCount = 0.0;
double mixAmount = 1.0;