Font selector

This commit is contained in:
2026-05-22 17:22:57 +10:00
parent c5f0a9df0e
commit 283f38dddb
15 changed files with 276 additions and 34 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,8 +6,8 @@
"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"
}, },

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

@@ -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

@@ -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));
@@ -145,7 +171,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 +180,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");