4 Commits

Author SHA1 Message Date
c35ca8d61c Font fixes
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 1m50s
CI / Windows Release Package (push) Has been skipped
2026-05-22 17:38:28 +10:00
be9f3b4e8b font changing on the fly 2026-05-22 17:27:29 +10:00
af448c338c docs 2026-05-22 17:24:58 +10:00
283f38dddb Font selector 2026-05-22 17:22:57 +10:00
20 changed files with 341 additions and 40 deletions

View File

@@ -318,3 +318,6 @@ If `SLANG_ROOT` or `MSDF_ATLAS_GEN_ROOT` is not set, the workflow falls back to
- Anotate included shaders - Anotate included shaders
- allow 3 vector exposed controls - allow 3 vector exposed controls
- add nearest sampling to the extra shader pass - add nearest sampling to the extra shader pass
- add spout input/output (https://github.com/leadedge/Spout2)
- Add Aja input and output (Assuming i can get a hold of an aja card)
- Add bluefish input and output (Assuming again card acess)

View File

@@ -6,14 +6,14 @@
"oscPort": 9000, "oscPort": 9000,
"oscSmoothing": 0.18, "oscSmoothing": 0.18,
"input": { "input": {
"backend": "none", "backend": "ndi",
"device": "AIDENLAPTOP (NVIDIA GeForce RTX 4090 Laptop GPU 1)", "device": "AIDENLAPTOP (Test Pattern)",
"resolution": "1080p", "resolution": "1080p",
"frameRate": "59.94" "frameRate": "59.94"
}, },
"output": { "output": {
"backend": "ndi", "backend": "decklink",
"device": "shader toys", "device": "default",
"resolution": "1080p", "resolution": "1080p",
"frameRate": "59.94", "frameRate": "59.94",
"keying": { "keying": {

View File

@@ -1079,6 +1079,9 @@ components:
font: font:
type: string type: string
description: Font asset id used by text parameters, when declared. description: Font asset id used by text parameters, when declared.
fontParameter:
type: string
description: Enum parameter id used to select a text parameter font at runtime.
value: value:
description: Current parameter value. description: Current parameter value.
oneOf: oneOf:

View File

@@ -592,6 +592,40 @@ float4 premultipliedText = drawTitleText(textUv, float4(1.0, 1.0, 1.0, 1.0));
Text is currently limited to printable ASCII. `maxLength` defaults to `64` and is clamped to `1..256`. The optional `font` field references a packaged font declared in `fonts`; if no font is specified, the runtime uses its fallback sans-serif renderer. Text is currently limited to printable ASCII. `maxLength` defaults to `64` and is clamped to `1..256`. The optional `font` field references a packaged font declared in `fonts`; if no font is specified, the runtime uses its fallback sans-serif renderer.
Text parameters can also choose their font from an enum parameter by setting `fontParameter` to that enum parameter's `id`:
```json
{
"fonts": [
{ "id": "inter", "path": "fonts/Inter-Regular.ttf" },
{ "id": "mono", "path": "fonts/Mono-Regular.ttf" }
],
"parameters": [
{
"id": "font",
"label": "Font",
"type": "enum",
"default": "inter",
"options": [
{ "value": "inter", "label": "Inter" },
{ "value": "mono", "label": "Mono" }
]
},
{
"id": "titleText",
"label": "Title",
"type": "text",
"default": "LIVE",
"font": "inter",
"fontParameter": "font",
"maxLength": 64
}
]
}
```
Every option `value` in the font selector enum must match a declared font asset `id`. The `font` field remains useful as the default/fallback font for the text parameter, while `fontParameter` lets operators switch atlases at runtime without adding shader-specific code.
Trigger example: Trigger example:
```json ```json
@@ -619,6 +653,7 @@ Parameter validation:
- `color` must have exactly 4 numbers. - `color` must have exactly 4 numbers.
- Enum defaults must match one of the declared option values. - Enum defaults must match one of the declared option values.
- Text defaults must be strings. Non-printable characters are dropped and values are clamped to `maxLength`. - Text defaults must be strings. Non-printable characters are dropped and values are clamped to `maxLength`.
- Text `fontParameter` values must reference an enum parameter whose option values are declared font asset IDs.
- Trigger values are incremented by the host when triggered. The shader sees the trigger count and last trigger time. - Trigger values are incremented by the host when triggered. The shader sees the trigger count and last trigger time.
- Non-finite numeric values are rejected. - Non-finite numeric values are rejected.
@@ -800,6 +835,7 @@ For multipass shaders, these files reflect the most recently compiled pass. If a
- Remember enum globals are integer indexes, not strings. - Remember enum globals are integer indexes, not strings.
- Declare every texture in `shader.json`; undeclared texture samplers will not be bound. - Declare every texture in `shader.json`; undeclared texture samplers will not be bound.
- Declare packaged fonts in `shader.json` when text parameters should use a specific font. - Declare packaged fonts in `shader.json` when text parameters should use a specific font.
- For selectable fonts, use a text parameter `fontParameter` that points at an enum whose option values are font IDs.
- Keep temporal history requests modest. They consume texture units and memory and are capped by runtime config. - Keep temporal history requests modest. They consume texture units and memory and are capped by runtime config.
- If a parameter appears in the UI but not in Slang, the shader may still compile, but the control has no effect. - If a parameter appears in the UI but not in Slang, the shader may still compile, but the control has no effect.
- If a Slang name collides with a generated global, rename your parameter or local symbol. - If a Slang name collides with a generated global, rename your parameter or local symbol.
@@ -815,5 +851,6 @@ Before committing a new shader package:
- Texture files referenced by `textures` exist. - Texture files referenced by `textures` exist.
- Font files referenced by `fonts` exist. - Font files referenced by `fonts` exist.
- Enum defaults are present in their `options`. - Enum defaults are present in their `options`.
- Text `fontParameter` selectors reference valid font assets through their enum options.
- Temporal shaders handle short or empty history gracefully. - Temporal shaders handle short or empty history gracefully.
- The app can reload and compile the shader without errors. - The app can reload and compile the shader without errors.

Binary file not shown.

View File

@@ -0,0 +1,60 @@
Analog Mono Plus Pixel Font - License Agreement
Copyright © Andrew Gleeson, 2026
heygleeson@gmail.com
1. GRANT OF LICENSE
This Agreement is a license, not an agreement of sale. Licensee shall
not acquire any copyright ownership or equivalent rights to any of the
Licensed Content. Seller and the Licensed Content sources retain all
right, title and interest in and to all of the copyrights, trademarks,
and all other proprietary rights in the Licensed Content. All rights
in and to Licensed Content not expressly granted in this agreement are
retained by Seller or its suppliers.
Licensee is permitted to use the Licensed Content in unlimited
commercial projects. A commercial project is one defined as a Work for
Distribution launched with the capability to generate revenue, or
intention to generate revenue through the sale of, licensing of, or
otherwise intend to generate revenue directly from the Work for
Distribution.
2. RESTRICTION ON USE
Licensed Content may not be used contrary to any restriction on use
indicated herein.
Licensed Content may not be resold, sublicensed, assigned, transferred
or otherwise made available to third parties except as incorporated
into Works for Distribution.
Licensed Content may not be distributed to third parties as standalone
files or in a way that unreasonably permits the recipient to extract
the Licensed Content for use separately and apart from the Work for
Distribution.
Licensee may not distribute the Licensed Content in any library or
reusable template, including but not limited to game templates, website
templates intended to allow reproduction by third parties on electronic
or printed products.
Licensee may not distribute Licensed Content in a manner meant to enable
third parties to create derivative works incorporating Licensed Content.
Licensee may not superficially modify the Licensed Content and sell it
to others for consumption, reproduction or re-sale.
Licensee shall not use the Licensed Content in a manner that violates
the law of any applicable jurisdiction.
Licensee shall not claim copyright or attribution of Licensed Content.
3. TERM AND TERMINATION
The license contained in this Agreement terminates automatically without
notice from Seller if Licensee fails to comply with any provision of
this Agreement. Upon termination, Licensee must with immediate effect
stop using the Licensed Content, destroy, delete and remove the Licensed
Content from Licensees premises, computer systems and storage. Licensee
must also make all reasonable efforts to ensure that copies of the
licensed content are removed from any locations it has been distributed to.

View File

@@ -8,15 +8,37 @@
{ {
"id": "roboto", "id": "roboto",
"path": "fonts/Roboto-Regular.ttf" "path": "fonts/Roboto-Regular.ttf"
},
{
"id": "analogMono",
"path": "fonts/AnalogMono.ttf"
} }
], ],
"parameters": [ "parameters": [
{
"id": "font",
"label": "Font",
"type": "enum",
"default": "roboto",
"options": [
{
"value": "roboto",
"label": "Roboto"
},
{
"value": "analogMono",
"label": "Analog Mono"
}
],
"description": "Font atlas used by the text overlay."
},
{ {
"id": "titleText", "id": "titleText",
"label": "Text", "label": "Text",
"type": "text", "type": "text",
"default": "VIDEO SHADER", "default": "VIDEO SHADER",
"font": "roboto", "font": "roboto",
"fontParameter": "font",
"maxLength": 64, "maxLength": 64,
"description": "Text string rendered into the SDF text texture." "description": "Text string rendered into the SDF text texture."
}, },

View File

@@ -38,12 +38,14 @@ float4 shadeVideo(ShaderContext context)
float2 pixelTextUv = (1.0 / resolution) / safeTextSize; float2 pixelTextUv = (1.0 / resolution) / safeTextSize;
float2 sampleOffset = pixelTextUv * 0.38; float2 sampleOffset = pixelTextUv * 0.38;
float msdfDistance = sampleTitleTextMsdf(textUv); float msdfDistance = sampleTitleTextMsdf(textUv);
float fill = ( float msdfFill = (
coverage(msdfDistance, edge, aa) * 2.0 + coverage(msdfDistance, edge, aa) * 2.0 +
coverage(sampleTitleTextMsdf(textUv + float2(sampleOffset.x, sampleOffset.y)), edge, aa) + coverage(sampleTitleTextMsdf(textUv + float2(sampleOffset.x, sampleOffset.y)), edge, aa) +
coverage(sampleTitleTextMsdf(textUv + float2(-sampleOffset.x, sampleOffset.y)), edge, aa) + coverage(sampleTitleTextMsdf(textUv + float2(-sampleOffset.x, sampleOffset.y)), edge, aa) +
coverage(sampleTitleTextMsdf(textUv + float2(sampleOffset.x, -sampleOffset.y)), edge, aa) + coverage(sampleTitleTextMsdf(textUv + float2(sampleOffset.x, -sampleOffset.y)), edge, aa) +
coverage(sampleTitleTextMsdf(textUv + float2(-sampleOffset.x, -sampleOffset.y)), edge, aa)) / 6.0; coverage(sampleTitleTextMsdf(textUv + float2(-sampleOffset.x, -sampleOffset.y)), edge, aa)) / 6.0;
float sdfFill = coverage(distance, edge, aa);
float fill = min(msdfFill, sdfFill);
float outlineEdge = edge - min(outlineWidth * 0.7, 0.48); float outlineEdge = edge - min(outlineWidth * 0.7, 0.48);
float outline = coverage(distance, outlineEdge, aa); float outline = coverage(distance, outlineEdge, aa);
float outlineAlpha = saturate(outline - fill) * outlineColor.a; float outlineAlpha = saturate(outline - fill) * outlineColor.a;

View File

@@ -236,6 +236,8 @@ inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParamet
writer.KeyUInt("maxLength", parameter.maxLength); writer.KeyUInt("maxLength", parameter.maxLength);
if (!parameter.fontId.empty()) if (!parameter.fontId.empty())
writer.KeyString("font", parameter.fontId); writer.KeyString("font", parameter.fontId);
if (!parameter.fontParameterId.empty())
writer.KeyString("fontParameter", parameter.fontParameterId);
} }
writer.EndObject(); writer.EndObject();
} }

View File

@@ -103,8 +103,14 @@ bool RuntimeTextTextureCache::EnsureTextTexture(TextTexture& texture)
const RuntimePreparedTextTexture* prepared = FindPreparedTexture(texture.parameterId); const RuntimePreparedTextTexture* prepared = FindPreparedTexture(texture.parameterId);
if (!prepared || !prepared->rgbaPixels || prepared->rgbaPixels->empty() || prepared->width == 0 || prepared->height == 0) if (!prepared || !prepared->rgbaPixels || prepared->rgbaPixels->empty() || prepared->width == 0 || prepared->height == 0)
return false; return false;
if (texture.texture != 0 && texture.cachedText == prepared->textValue && texture.width == prepared->width && texture.height == prepared->height) if (texture.texture != 0 &&
texture.cachedText == prepared->textValue &&
texture.cachedPixels == prepared->rgbaPixels &&
texture.width == prepared->width &&
texture.height == prepared->height)
{
return true; return true;
}
if (texture.texture == 0) if (texture.texture == 0)
glGenTextures(1, &texture.texture); glGenTextures(1, &texture.texture);
@@ -130,6 +136,7 @@ bool RuntimeTextTextureCache::EnsureTextTexture(TextTexture& texture)
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
texture.cachedText = prepared->textValue; texture.cachedText = prepared->textValue;
texture.cachedPixels = prepared->rgbaPixels;
texture.width = prepared->width; texture.width = prepared->width;
texture.height = prepared->height; texture.height = prepared->height;
texture.liveWidth = prepared->liveWidth; texture.liveWidth = prepared->liveWidth;
@@ -154,4 +161,5 @@ void RuntimeTextTextureCache::DestroyTexture(TextTexture& texture)
texture.width = 0; texture.width = 0;
texture.height = 0; texture.height = 0;
texture.cachedText.clear(); texture.cachedText.clear();
texture.cachedPixels.reset();
} }

View File

@@ -4,6 +4,7 @@
#include "RuntimeShaderArtifact.h" #include "RuntimeShaderArtifact.h"
#include <map> #include <map>
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -28,6 +29,7 @@ private:
{ {
std::string parameterId; std::string parameterId;
std::string cachedText; std::string cachedText;
std::shared_ptr<const std::vector<unsigned char>> cachedPixels;
GLuint texture = 0; GLuint texture = 0;
unsigned width = 0; unsigned width = 0;
unsigned height = 0; unsigned height = 0;

View File

@@ -8,6 +8,29 @@
namespace RenderCadenceCompositor namespace RenderCadenceCompositor
{ {
namespace
{
const ShaderParameterDefinition* FindParameterDefinition(const ShaderPackage& shaderPackage, const std::string& parameterId)
{
for (const ShaderParameterDefinition& parameter : shaderPackage.parameters)
{
if (parameter.id == parameterId)
return &parameter;
}
return nullptr;
}
bool HasFontAsset(const ShaderPackage& shaderPackage, const std::string& fontId)
{
for (const ShaderFontAsset& fontAsset : shaderPackage.fontAssets)
{
if (fontAsset.id == fontId)
return true;
}
return false;
}
}
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage) ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage)
{ {
if (shaderPackage.passes.empty()) if (shaderPackage.passes.empty())
@@ -27,20 +50,24 @@ ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& s
if (parameter.type != ShaderParameterType::Text) if (parameter.type != ShaderParameterType::Text)
continue; continue;
if (parameter.fontId.empty()) if (parameter.fontId.empty() && parameter.fontParameterId.empty())
return { false, "Text parameter '" + parameter.id + "' must reference a declared font asset." }; return { false, "Text parameter '" + parameter.id + "' must reference a declared font asset." };
bool hasFontAsset = false; if (!parameter.fontId.empty() && !HasFontAsset(shaderPackage, parameter.fontId))
for (const ShaderFontAsset& fontAsset : shaderPackage.fontAssets) return { false, "Text parameter '" + parameter.id + "' references unknown font asset '" + parameter.fontId + "'." };
if (!parameter.fontParameterId.empty())
{ {
if (fontAsset.id == parameter.fontId) const ShaderParameterDefinition* fontParameter = FindParameterDefinition(shaderPackage, parameter.fontParameterId);
if (fontParameter == nullptr || fontParameter->type != ShaderParameterType::Enum)
return { false, "Text parameter '" + parameter.id + "' references unknown font enum parameter '" + parameter.fontParameterId + "'." };
for (const ShaderParameterOption& option : fontParameter->enumOptions)
{ {
hasFontAsset = true; if (!HasFontAsset(shaderPackage, option.value))
break; return { false, "Font enum parameter '" + fontParameter->id + "' references unknown font asset '" + option.value + "'." };
} }
} }
if (!hasFontAsset)
return { false, "Text parameter '" + parameter.id + "' references unknown font asset '" + parameter.fontId + "'." };
} }
bool writesLayerOutput = false; bool writesLayerOutput = false;
@@ -102,6 +129,7 @@ std::string ShaderPackageFingerprint(const ShaderPackage& shaderPackage)
{ {
source << "param:" << parameter.id << ":" << static_cast<int>(parameter.type) << ":" source << "param:" << parameter.id << ":" << static_cast<int>(parameter.type) << ":"
<< parameter.label << ":" << parameter.description << ":" << parameter.fontId << ":" << parameter.label << ":" << parameter.description << ":" << parameter.fontId << ":"
<< parameter.fontParameterId << ":"
<< parameter.defaultTextValue << ":" << parameter.defaultBoolean << ":" << parameter.defaultTextValue << ":" << parameter.defaultBoolean << ":"
<< parameter.defaultEnumValue << ":" << parameter.maxLength << "\n"; << parameter.defaultEnumValue << ":" << parameter.maxLength << "\n";
for (double value : parameter.defaultNumbers) for (double value : parameter.defaultNumbers)

View File

@@ -178,7 +178,17 @@ bool RuntimeLayerModel::UpdateParameter(const std::string& layerId, const std::s
if (layer->renderReady) if (layer->renderReady)
{ {
layer->artifact.parameterValues = layer->parameterValues; layer->artifact.parameterValues = layer->parameterValues;
if (definition->type == ShaderParameterType::Text && !PrepareRuntimeTextTextures(layer->artifact, error)) bool textTexturesDependOnParameter = false;
for (const ShaderParameterDefinition& textDefinition : layer->parameterDefinitions)
{
if (textDefinition.type == ShaderParameterType::Text &&
(textDefinition.id == parameterId || textDefinition.fontParameterId == parameterId))
{
textTexturesDependOnParameter = true;
break;
}
}
if (textTexturesDependOnParameter && !PrepareRuntimeTextTextures(layer->artifact, error))
return false; return false;
} }
error.clear(); error.clear();

View File

@@ -27,12 +27,38 @@ const ShaderParameterValue* FindParameterValue(const RuntimeShaderArtifact& arti
return valueIt == artifact.parameterValues.end() ? nullptr : &valueIt->second; return valueIt == artifact.parameterValues.end() ? nullptr : &valueIt->second;
} }
const ShaderParameterDefinition* FindParameterDefinition(const RuntimeShaderArtifact& artifact, const std::string& parameterId)
{
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
{
if (definition.id == parameterId)
return &definition;
}
return nullptr;
}
std::string TextValueForDefinition(const RuntimeShaderArtifact& artifact, const ShaderParameterDefinition& definition) std::string TextValueForDefinition(const RuntimeShaderArtifact& artifact, const ShaderParameterDefinition& definition)
{ {
const ShaderParameterValue* value = FindParameterValue(artifact, definition.id); const ShaderParameterValue* value = FindParameterValue(artifact, definition.id);
return value ? value->textValue : definition.defaultTextValue; return value ? value->textValue : definition.defaultTextValue;
} }
std::string FontIdForTextDefinition(const RuntimeShaderArtifact& artifact, const ShaderParameterDefinition& definition)
{
if (definition.fontParameterId.empty())
return definition.fontId;
const ShaderParameterValue* fontValue = FindParameterValue(artifact, definition.fontParameterId);
if (fontValue != nullptr && !fontValue->enumValue.empty())
return fontValue->enumValue;
const ShaderParameterDefinition* fontDefinition = FindParameterDefinition(artifact, definition.fontParameterId);
if (fontDefinition != nullptr && !fontDefinition->defaultEnumValue.empty())
return fontDefinition->defaultEnumValue;
return definition.fontId;
}
void SampleAtlasPixel(const FontAtlasBuildOutput& atlas, double x, double y, unsigned char* rgba) 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 clampedX = (std::max)(0.0, (std::min)(static_cast<double>(atlas.width) - 1.0, x));
@@ -57,6 +83,13 @@ void SampleAtlasPixel(const FontAtlasBuildOutput& atlas, double x, double y, uns
} }
} }
double GlyphAtlasCoordinate(double minBound, double maxBound, double uv)
{
if (maxBound - minBound <= 1.0)
return (minBound + maxBound) * 0.5;
return minBound + 0.5 + uv * ((maxBound - 0.5) - (minBound + 0.5));
}
std::vector<unsigned char> ComposeTextTexture( std::vector<unsigned char> ComposeTextTexture(
const FontAtlasBuildOutput& atlas, const FontAtlasBuildOutput& atlas,
const ShaderParameterDefinition& definition, const ShaderParameterDefinition& definition,
@@ -109,8 +142,8 @@ std::vector<unsigned char> ComposeTextTexture(
if (x < 0 || x >= static_cast<int>(width)) if (x < 0 || x >= static_cast<int>(width))
continue; continue;
const double u = (static_cast<double>(x) + 0.5 - destLeft) / destWidth; const double u = (static_cast<double>(x) + 0.5 - destLeft) / destWidth;
const double atlasX = glyph.atlasLeft + u * (glyph.atlasRight - glyph.atlasLeft); const double atlasX = GlyphAtlasCoordinate(glyph.atlasLeft, glyph.atlasRight, u);
const double atlasY = glyph.atlasTop + v * (glyph.atlasBottom - glyph.atlasTop); const double atlasY = GlyphAtlasCoordinate(glyph.atlasTop, glyph.atlasBottom, v);
unsigned char sample[4] = {}; unsigned char sample[4] = {};
SampleAtlasPixel(atlas, atlasX, atlasY, sample); SampleAtlasPixel(atlas, atlasX, atlasY, sample);
unsigned char* destination = texturePixels.data() + (static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x)) * 4u; unsigned char* destination = texturePixels.data() + (static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x)) * 4u;
@@ -145,7 +178,8 @@ bool PrepareRuntimeTextTextures(RuntimeShaderArtifact& artifact, std::string& er
if (definition.type != ShaderParameterType::Text) if (definition.type != ShaderParameterType::Text)
continue; continue;
const FontAtlasBuildOutput* atlas = FindAtlas(artifact, definition.fontId); const std::string fontId = FontIdForTextDefinition(artifact, definition);
const FontAtlasBuildOutput* atlas = FindAtlas(artifact, fontId);
if (atlas == nullptr) if (atlas == nullptr)
{ {
error = "No prepared font atlas is available for text parameter '" + definition.id + "'."; error = "No prepared font atlas is available for text parameter '" + definition.id + "'.";
@@ -153,7 +187,7 @@ bool PrepareRuntimeTextTextures(RuntimeShaderArtifact& artifact, std::string& er
} }
if (atlas->width == 0 || atlas->height == 0 || atlas->rgbaPixels.empty() || atlas->glyphsByCodepoint.empty()) if (atlas->width == 0 || atlas->height == 0 || atlas->rgbaPixels.empty() || atlas->glyphsByCodepoint.empty())
{ {
error = "Prepared font atlas data is empty for font '" + definition.fontId + "'."; error = "Prepared font atlas data is empty for font '" + fontId + "'.";
return false; return false;
} }

View File

@@ -189,6 +189,17 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error)) if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error))
return false; return false;
} }
if (const JsonValue* fontParameterValue = parameterJson.find("fontParameter"))
{
if (!fontParameterValue->isString())
{
error = "Text parameter 'fontParameter' must be a string for: " + definition.id;
return false;
}
definition.fontParameterId = fontParameterValue->asString();
if (!definition.fontParameterId.empty() && !ValidateShaderIdentifier(definition.fontParameterId, "parameters[].fontParameter", manifestPath, error))
return false;
}
if (const JsonValue* maxLengthValue = parameterJson.find("maxLength")) if (const JsonValue* maxLengthValue = parameterJson.find("maxLength"))
{ {
if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0) if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0)

View File

@@ -36,6 +36,7 @@ struct ShaderParameterDefinition
std::string defaultEnumValue; std::string defaultEnumValue;
std::string defaultTextValue; std::string defaultTextValue;
std::string fontId; std::string fontId;
std::string fontParameterId;
unsigned maxLength = 64; unsigned maxLength = 64;
std::vector<ShaderParameterOption> enumOptions; std::vector<ShaderParameterOption> enumOptions;
}; };

View File

@@ -82,13 +82,13 @@ void TestBuildsTextOverlayFontAtlas()
Expect(false, ("text overlay font atlas builds: " + error).c_str()); Expect(false, ("text overlay font atlas builds: " + error).c_str());
return; return;
} }
Expect(outputs.size() == 1, "one font atlas output is produced"); Expect(outputs.size() == shaderPackage.fontAssets.size(), "one font atlas output is produced for each declared font");
if (!outputs.empty()) for (const RenderCadenceCompositor::FontAtlasBuildOutput& output : outputs)
{ {
Expect(std::filesystem::exists(outputs[0].imagePath), "font atlas image exists"); Expect(std::filesystem::exists(output.imagePath), "font atlas image exists");
Expect(std::filesystem::exists(outputs[0].jsonPath), "font atlas json exists"); Expect(std::filesystem::exists(output.jsonPath), "font atlas json exists");
Expect(std::filesystem::file_size(outputs[0].imagePath) > 0, "font atlas image is not empty"); Expect(std::filesystem::file_size(output.imagePath) > 0, "font atlas image is not empty");
Expect(std::filesystem::file_size(outputs[0].jsonPath) > 0, "font atlas json is not empty"); Expect(std::filesystem::file_size(output.jsonPath) > 0, "font atlas json is not empty");
} }
} }
} }

View File

@@ -62,17 +62,20 @@ std::string AllParametersShaderManifest()
"description": "All parameter restore test shader", "description": "All parameter restore test shader",
"category": "Tests", "category": "Tests",
"entryPoint": "shadeVideo", "entryPoint": "shadeVideo",
"fonts": [{ "id": "inter", "path": "Inter.ttf" }], "fonts": [
{ "id": "inter", "path": "Inter.ttf" },
{ "id": "mono", "path": "Mono.ttf" }
],
"parameters": [ "parameters": [
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0.0, "max": 1.0 }, { "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0.0, "max": 1.0 },
{ "id": "offset", "label": "Offset", "type": "vec2", "default": [0.0, 0.0], "min": [-1.0, -1.0], "max": [1.0, 1.0] }, { "id": "offset", "label": "Offset", "type": "vec2", "default": [0.0, 0.0], "min": [-1.0, -1.0], "max": [1.0, 1.0] },
{ "id": "tint", "label": "Tint", "type": "color", "default": [1.0, 1.0, 1.0, 1.0], "min": [0.0, 0.0, 0.0, 0.0], "max": [1.0, 1.0, 1.0, 1.0] }, { "id": "tint", "label": "Tint", "type": "color", "default": [1.0, 1.0, 1.0, 1.0], "min": [0.0, 0.0, 0.0, 0.0], "max": [1.0, 1.0, 1.0, 1.0] },
{ "id": "enabled", "label": "Enabled", "type": "bool", "default": true }, { "id": "enabled", "label": "Enabled", "type": "bool", "default": true },
{ "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [ { "id": "mode", "label": "Mode", "type": "enum", "default": "inter", "options": [
{ "value": "soft", "label": "Soft" }, { "value": "inter", "label": "Inter" },
{ "value": "hard", "label": "Hard" } { "value": "mono", "label": "Mono" }
] }, ] },
{ "id": "titleText", "label": "Title", "type": "text", "default": "DEFAULT", "font": "inter", "maxLength": 8 }, { "id": "titleText", "label": "Title", "type": "text", "default": "DEFAULT", "font": "inter", "fontParameter": "mode", "maxLength": 8 },
{ "id": "drop", "label": "Drop", "type": "trigger" } { "id": "drop", "label": "Drop", "type": "trigger" }
] ]
})"; })";
@@ -94,10 +97,10 @@ RenderCadenceCompositor::SupportedShaderCatalog MakeCatalog(std::filesystem::pat
return LoadCatalog(root); return LoadCatalog(root);
} }
RenderCadenceCompositor::FontAtlasBuildOutput MakeFakeFontAtlas() RenderCadenceCompositor::FontAtlasBuildOutput MakeFakeFontAtlas(const std::string& fontId = "inter")
{ {
RenderCadenceCompositor::FontAtlasBuildOutput atlas; RenderCadenceCompositor::FontAtlasBuildOutput atlas;
atlas.fontId = "inter"; atlas.fontId = fontId;
atlas.width = 2; atlas.width = 2;
atlas.height = 2; atlas.height = 2;
atlas.ascender = -0.8; atlas.ascender = -0.8;
@@ -226,6 +229,7 @@ void TestInitializeFromRuntimeStateRestoresLayerStack()
WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.5, false)); WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.5, false));
WriteFile(root / "all-params" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n"); 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 restore catalog support checks"); WriteFile(root / "all-params" / "Inter.ttf", "not a real font, but enough for restore catalog support checks");
WriteFile(root / "all-params" / "Mono.ttf", "not a real font, but enough for restore catalog support checks");
WriteFile(root / "all-params" / "shader.json", AllParametersShaderManifest()); WriteFile(root / "all-params" / "shader.json", AllParametersShaderManifest());
RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root); RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root);
@@ -242,7 +246,7 @@ void TestInitializeFromRuntimeStateRestoresLayerStack()
"offset": [0.25, -0.5], "offset": [0.25, -0.5],
"tint": [0.1, 0.2, 0.3, 0.4], "tint": [0.1, 0.2, 0.3, 0.4],
"enabled": false, "enabled": false,
"mode": "hard", "mode": "mono",
"titleText": "RESTORED-TEXT", "titleText": "RESTORED-TEXT",
"drop": 4 "drop": 4
} }
@@ -275,7 +279,7 @@ void TestInitializeFromRuntimeStateRestoresLayerStack()
Expect(snapshot.displayLayers[0].parameterValues.at("offset").numberValues == std::vector<double>({ 0.25, -0.5 }), "restore preserves vec2 parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("offset").numberValues == std::vector<double>({ 0.25, -0.5 }), "restore preserves vec2 parameter values");
Expect(snapshot.displayLayers[0].parameterValues.at("tint").numberValues == std::vector<double>({ 0.1, 0.2, 0.3, 0.4 }), "restore preserves color parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("tint").numberValues == std::vector<double>({ 0.1, 0.2, 0.3, 0.4 }), "restore preserves color parameter values");
Expect(!snapshot.displayLayers[0].parameterValues.at("enabled").booleanValue, "restore preserves boolean parameter values"); Expect(!snapshot.displayLayers[0].parameterValues.at("enabled").booleanValue, "restore preserves boolean parameter values");
Expect(snapshot.displayLayers[0].parameterValues.at("mode").enumValue == "hard", "restore preserves enum parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("mode").enumValue == "mono", "restore preserves enum parameter values");
Expect(snapshot.displayLayers[0].parameterValues.at("titleText").textValue == "RESTORED", "restore normalizes and preserves text parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("titleText").textValue == "RESTORED", "restore normalizes and preserves text parameter values");
Expect(snapshot.displayLayers[0].parameterValues.at("drop").numberValues.front() == 4.0, "restore preserves trigger counts"); Expect(snapshot.displayLayers[0].parameterValues.at("drop").numberValues.front() == 4.0, "restore preserves trigger counts");
Expect(snapshot.displayLayers[1].id == "layer-33", "restore preserves later supported layer order"); Expect(snapshot.displayLayers[1].id == "layer-33", "restore preserves later supported layer order");
@@ -488,6 +492,7 @@ void TestTextTexturesArePreparedInRuntimeModel()
std::filesystem::path root = MakeTestRoot(); 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" / "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" / "Inter.ttf", "not a real font, but enough for catalog support checks");
WriteFile(root / "all-params" / "Mono.ttf", "not a real font, but enough for catalog support checks");
WriteFile(root / "all-params" / "shader.json", AllParametersShaderManifest()); WriteFile(root / "all-params" / "shader.json", AllParametersShaderManifest());
RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root); RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root);
@@ -503,6 +508,7 @@ void TestTextTexturesArePreparedInRuntimeModel()
artifact.fragmentShaderSource = "void main(){}"; artifact.fragmentShaderSource = "void main(){}";
artifact.parameterDefinitions = snapshot.displayLayers[0].parameterDefinitions; artifact.parameterDefinitions = snapshot.displayLayers[0].parameterDefinitions;
artifact.fontAtlases.push_back(MakeFakeFontAtlas()); artifact.fontAtlases.push_back(MakeFakeFontAtlas());
artifact.fontAtlases.push_back(MakeFakeFontAtlas("mono"));
artifact.message = "build ready"; artifact.message = "build ready";
Expect(model.MarkBuildReady(artifact, error), error.empty() ? "ready text artifact prepares textures" : error); Expect(model.MarkBuildReady(artifact, error), error.empty() ? "ready text artifact prepares textures" : error);
@@ -520,6 +526,12 @@ void TestTextTexturesArePreparedInRuntimeModel()
Expect(preparedUpdated.textValue == "AB", "updated text is prepared before render snapshot"); 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"); Expect(preparedUpdated.rgbaPixels && preparedUpdated.rgbaPixels != preparedDefault.rgbaPixels, "updated text receives a new prepared pixel payload");
Expect(model.UpdateParameter(model.FirstLayerId(), "mode", JsonValue("mono"), error), error.empty() ? "font selector update prepares texture" : error);
snapshot = model.Snapshot();
const RuntimePreparedTextTexture preparedWithNewFont = snapshot.renderLayers[0].artifact.preparedTextTextures[0];
Expect(preparedWithNewFont.textValue == "AB", "font selector update preserves current text");
Expect(preparedWithNewFont.rgbaPixels && preparedWithNewFont.rgbaPixels != preparedUpdated.rgbaPixels, "font selector update receives a new prepared pixel payload");
std::filesystem::remove_all(root); std::filesystem::remove_all(root);
} }
} }

View File

@@ -174,6 +174,65 @@ void SupportsTextParametersWithDeclaredFont()
Expect(result.reason.empty(), "supported text parameters should not report a rejection reason"); Expect(result.reason.empty(), "supported text parameters should not report a rejection reason");
} }
void SupportsTextParametersWithFontSelector()
{
ShaderPackage shaderPackage = MakeSinglePassPackage();
ShaderFontAsset roboto;
roboto.id = "roboto";
shaderPackage.fontAssets.push_back(roboto);
ShaderFontAsset mono;
mono.id = "mono";
shaderPackage.fontAssets.push_back(mono);
ShaderParameterDefinition fontSelector;
fontSelector.id = "font";
fontSelector.type = ShaderParameterType::Enum;
fontSelector.defaultEnumValue = "roboto";
fontSelector.enumOptions = { { "roboto", "Roboto" }, { "mono", "Mono" } };
shaderPackage.parameters.push_back(fontSelector);
ShaderParameterDefinition parameter;
parameter.id = "caption";
parameter.type = ShaderParameterType::Text;
parameter.fontId = "roboto";
parameter.fontParameterId = "font";
shaderPackage.parameters.push_back(parameter);
const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(result.supported, "text parameters with font selector enum should be supported");
Expect(result.reason.empty(), "supported font selector text parameters should not report a rejection reason");
}
void RejectsTextParametersWithFontSelectorUnknownOption()
{
ShaderPackage shaderPackage = MakeSinglePassPackage();
ShaderFontAsset roboto;
roboto.id = "roboto";
shaderPackage.fontAssets.push_back(roboto);
ShaderParameterDefinition fontSelector;
fontSelector.id = "font";
fontSelector.type = ShaderParameterType::Enum;
fontSelector.defaultEnumValue = "roboto";
fontSelector.enumOptions = { { "roboto", "Roboto" }, { "missing", "Missing" } };
shaderPackage.parameters.push_back(fontSelector);
ShaderParameterDefinition parameter;
parameter.id = "caption";
parameter.type = ShaderParameterType::Text;
parameter.fontId = "roboto";
parameter.fontParameterId = "font";
shaderPackage.parameters.push_back(parameter);
const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(!result.supported, "font selector enum options must reference declared font assets");
Expect(result.reason.find("unknown font asset") != std::string::npos, "font selector rejection mentions unknown font asset");
}
void BuildsDeclaredFontAtlasesDuringCatalogLoad() void BuildsDeclaredFontAtlasesDuringCatalogLoad()
{ {
std::string msdfReason; std::string msdfReason;
@@ -217,6 +276,8 @@ int main()
RejectsTextureAssets(); RejectsTextureAssets();
RejectsTextParametersWithoutDeclaredFont(); RejectsTextParametersWithoutDeclaredFont();
SupportsTextParametersWithDeclaredFont(); SupportsTextParametersWithDeclaredFont();
SupportsTextParametersWithFontSelector();
RejectsTextParametersWithFontSelectorUnknownOption();
BuildsDeclaredFontAtlasesDuringCatalogLoad(); BuildsDeclaredFontAtlasesDuringCatalogLoad();
if (gFailures != 0) if (gFailures != 0)

View File

@@ -49,6 +49,7 @@ void TestValidManifest()
const std::filesystem::path root = MakeTestRoot(); const std::filesystem::path root = MakeTestRoot();
WriteFile(root / "look" / "mask.png", "not a real png, but enough for existence checks"); WriteFile(root / "look" / "mask.png", "not a real png, but enough for existence checks");
WriteFile(root / "look" / "Inter.ttf", "not a real font, but enough for existence checks"); WriteFile(root / "look" / "Inter.ttf", "not a real font, but enough for existence checks");
WriteFile(root / "look" / "Mono.ttf", "not a real font, but enough for existence checks");
WriteShaderPackage(root, "look", R"({ WriteShaderPackage(root, "look", R"({
"id": "look-01", "id": "look-01",
"name": "Look 01", "name": "Look 01",
@@ -56,15 +57,18 @@ void TestValidManifest()
"category": "Tests", "category": "Tests",
"entryPoint": "shadeVideo", "entryPoint": "shadeVideo",
"textures": [{ "id": "maskTex", "path": "mask.png" }], "textures": [{ "id": "maskTex", "path": "mask.png" }],
"fonts": [{ "id": "inter", "path": "Inter.ttf" }], "fonts": [
{ "id": "inter", "path": "Inter.ttf" },
{ "id": "mono", "path": "Mono.ttf" }
],
"temporal": { "enabled": true, "historySource": "source", "historyLength": 8 }, "temporal": { "enabled": true, "historySource": "source", "historyLength": 8 },
"feedback": { "enabled": true }, "feedback": { "enabled": true },
"parameters": [ "parameters": [
{ "id": "gain", "label": "Gain", "description": "Scales the output intensity.", "type": "float", "default": 0.5, "min": 0, "max": 1 }, { "id": "gain", "label": "Gain", "description": "Scales the output intensity.", "type": "float", "default": 0.5, "min": 0, "max": 1 },
{ "id": "titleText", "label": "Title", "type": "text", "default": "LIVE", "font": "inter", "maxLength": 32 }, { "id": "titleText", "label": "Title", "type": "text", "default": "LIVE", "font": "inter", "fontParameter": "mode", "maxLength": 32 },
{ "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [ { "id": "mode", "label": "Mode", "type": "enum", "default": "inter", "options": [
{ "value": "soft", "label": "Soft" }, { "value": "inter", "label": "Inter" },
{ "value": "hard", "label": "Hard" } { "value": "mono", "label": "Mono" }
] }, ] },
{ "id": "flash", "label": "Flash", "type": "trigger" } { "id": "flash", "label": "Flash", "type": "trigger" }
] ]
@@ -76,12 +80,13 @@ void TestValidManifest()
Expect(registry.ParseManifest(root / "look" / "shader.json", package, error), "valid manifest parses"); Expect(registry.ParseManifest(root / "look" / "shader.json", package, error), "valid manifest parses");
Expect(package.id == "look-01", "manifest id is preserved"); Expect(package.id == "look-01", "manifest id is preserved");
Expect(package.textureAssets.size() == 1 && package.textureAssets[0].id == "maskTex", "texture assets parse"); Expect(package.textureAssets.size() == 1 && package.textureAssets[0].id == "maskTex", "texture assets parse");
Expect(package.fontAssets.size() == 1 && package.fontAssets[0].id == "inter", "font assets parse"); Expect(package.fontAssets.size() == 2 && package.fontAssets[0].id == "inter", "font assets parse");
Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped"); Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped");
Expect(package.feedback.enabled && package.feedback.writePassId == "main", "feedback defaults to the implicit main pass"); Expect(package.feedback.enabled && package.feedback.writePassId == "main", "feedback defaults to the implicit main pass");
Expect(package.parameters.size() == 4, "parameters parse"); Expect(package.parameters.size() == 4, "parameters parse");
Expect(package.parameters[0].description == "Scales the output intensity.", "parameter descriptions parse"); Expect(package.parameters[0].description == "Scales the output intensity.", "parameter descriptions parse");
Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses"); Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses");
Expect(package.parameters[1].fontParameterId == "mode", "text font selector parameter parses");
Expect(package.parameters[3].type == ShaderParameterType::Trigger, "trigger parameter parses"); Expect(package.parameters[3].type == ShaderParameterType::Trigger, "trigger parameter parses");
Expect(package.passes.size() == 1 && package.passes[0].id == "main", "legacy manifests get an implicit main pass"); Expect(package.passes.size() == 1 && package.passes[0].id == "main", "legacy manifests get an implicit main pass");