diff --git a/README.md b/README.md index f015212..cb353c8 100644 --- a/README.md +++ b/README.md @@ -318,3 +318,6 @@ If `SLANG_ROOT` or `MSDF_ATLAS_GEN_ROOT` is not set, the workflow falls back to - Anotate included shaders - allow 3 vector exposed controls - 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) diff --git a/config/runtime-host.json b/config/runtime-host.json index ca26f3d..eac6e33 100644 --- a/config/runtime-host.json +++ b/config/runtime-host.json @@ -6,8 +6,8 @@ "oscPort": 9000, "oscSmoothing": 0.18, "input": { - "backend": "none", - "device": "AIDENLAPTOP (NVIDIA GeForce RTX 4090 Laptop GPU 1)", + "backend": "ndi", + "device": "AIDENLAPTOP (Test Pattern)", "resolution": "1080p", "frameRate": "59.94" }, diff --git a/shaders/text-overlay/fonts/AnalogMono.ttf b/shaders/text-overlay/fonts/AnalogMono.ttf new file mode 100644 index 0000000..d1025f2 Binary files /dev/null and b/shaders/text-overlay/fonts/AnalogMono.ttf differ diff --git a/shaders/text-overlay/fonts/LICENSE.txt b/shaders/text-overlay/fonts/LICENSE.txt new file mode 100644 index 0000000..3b06d88 --- /dev/null +++ b/shaders/text-overlay/fonts/LICENSE.txt @@ -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 Licensee’s 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. \ No newline at end of file diff --git a/shaders/text-overlay/shader.json b/shaders/text-overlay/shader.json index d78d8af..d52733d 100644 --- a/shaders/text-overlay/shader.json +++ b/shaders/text-overlay/shader.json @@ -8,15 +8,37 @@ { "id": "roboto", "path": "fonts/Roboto-Regular.ttf" + }, + { + "id": "analogMono", + "path": "fonts/AnalogMono.ttf" } ], "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", "label": "Text", "type": "text", "default": "VIDEO SHADER", "font": "roboto", + "fontParameter": "font", "maxLength": 64, "description": "Text string rendered into the SDF text texture." }, diff --git a/src/control/RuntimeStateJson.h b/src/control/RuntimeStateJson.h index ed52e9b..2fd62f5 100644 --- a/src/control/RuntimeStateJson.h +++ b/src/control/RuntimeStateJson.h @@ -236,6 +236,8 @@ inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParamet writer.KeyUInt("maxLength", parameter.maxLength); if (!parameter.fontId.empty()) writer.KeyString("font", parameter.fontId); + if (!parameter.fontParameterId.empty()) + writer.KeyString("fontParameter", parameter.fontParameterId); } writer.EndObject(); } diff --git a/src/runtime/catalog/SupportedShaderCatalog.cpp b/src/runtime/catalog/SupportedShaderCatalog.cpp index c5ffe52..b33a763 100644 --- a/src/runtime/catalog/SupportedShaderCatalog.cpp +++ b/src/runtime/catalog/SupportedShaderCatalog.cpp @@ -8,6 +8,29 @@ namespace RenderCadenceCompositor { +namespace +{ +const ShaderParameterDefinition* FindParameterDefinition(const ShaderPackage& shaderPackage, const std::string& parameterId) +{ + for (const ShaderParameterDefinition& parameter : shaderPackage.parameters) + { + if (parameter.id == parameterId) + return ¶meter; + } + 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) { if (shaderPackage.passes.empty()) @@ -27,20 +50,24 @@ ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& s if (parameter.type != ShaderParameterType::Text) continue; - if (parameter.fontId.empty()) + if (parameter.fontId.empty() && parameter.fontParameterId.empty()) return { false, "Text parameter '" + parameter.id + "' must reference a declared font asset." }; - bool hasFontAsset = false; - for (const ShaderFontAsset& fontAsset : shaderPackage.fontAssets) + if (!parameter.fontId.empty() && !HasFontAsset(shaderPackage, parameter.fontId)) + 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; - break; + if (!HasFontAsset(shaderPackage, option.value)) + 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; @@ -102,6 +129,7 @@ std::string ShaderPackageFingerprint(const ShaderPackage& shaderPackage) { source << "param:" << parameter.id << ":" << static_cast(parameter.type) << ":" << parameter.label << ":" << parameter.description << ":" << parameter.fontId << ":" + << parameter.fontParameterId << ":" << parameter.defaultTextValue << ":" << parameter.defaultBoolean << ":" << parameter.defaultEnumValue << ":" << parameter.maxLength << "\n"; for (double value : parameter.defaultNumbers) diff --git a/src/runtime/layers/RuntimeLayerModel.cpp b/src/runtime/layers/RuntimeLayerModel.cpp index 3711dff..ca7830c 100644 --- a/src/runtime/layers/RuntimeLayerModel.cpp +++ b/src/runtime/layers/RuntimeLayerModel.cpp @@ -178,7 +178,17 @@ bool RuntimeLayerModel::UpdateParameter(const std::string& layerId, const std::s if (layer->renderReady) { 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; } error.clear(); diff --git a/src/runtime/text/RuntimeTextTextureComposer.cpp b/src/runtime/text/RuntimeTextTextureComposer.cpp index 591086d..5ed814d 100644 --- a/src/runtime/text/RuntimeTextTextureComposer.cpp +++ b/src/runtime/text/RuntimeTextTextureComposer.cpp @@ -27,12 +27,38 @@ const ShaderParameterValue* FindParameterValue(const RuntimeShaderArtifact& arti 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) { const ShaderParameterValue* value = FindParameterValue(artifact, definition.id); 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) { const double clampedX = (std::max)(0.0, (std::min)(static_cast(atlas.width) - 1.0, x)); @@ -145,7 +171,8 @@ bool PrepareRuntimeTextTextures(RuntimeShaderArtifact& artifact, std::string& er if (definition.type != ShaderParameterType::Text) continue; - const FontAtlasBuildOutput* atlas = FindAtlas(artifact, definition.fontId); + const std::string fontId = FontIdForTextDefinition(artifact, definition); + const FontAtlasBuildOutput* atlas = FindAtlas(artifact, fontId); if (atlas == nullptr) { 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()) { - error = "Prepared font atlas data is empty for font '" + definition.fontId + "'."; + error = "Prepared font atlas data is empty for font '" + fontId + "'."; return false; } diff --git a/src/shader/ShaderManifestParameters.cpp b/src/shader/ShaderManifestParameters.cpp index 4c7b1c7..9493e5d 100644 --- a/src/shader/ShaderManifestParameters.cpp +++ b/src/shader/ShaderManifestParameters.cpp @@ -189,6 +189,17 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error)) 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 (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0) diff --git a/src/shader/ShaderTypes.h b/src/shader/ShaderTypes.h index f392e3f..d7d9d93 100644 --- a/src/shader/ShaderTypes.h +++ b/src/shader/ShaderTypes.h @@ -36,6 +36,7 @@ struct ShaderParameterDefinition std::string defaultEnumValue; std::string defaultTextValue; std::string fontId; + std::string fontParameterId; unsigned maxLength = 64; std::vector enumOptions; }; diff --git a/tests/FontAtlasBuilderTests.cpp b/tests/FontAtlasBuilderTests.cpp index 23012ff..0691608 100644 --- a/tests/FontAtlasBuilderTests.cpp +++ b/tests/FontAtlasBuilderTests.cpp @@ -82,13 +82,13 @@ void TestBuildsTextOverlayFontAtlas() Expect(false, ("text overlay font atlas builds: " + error).c_str()); return; } - Expect(outputs.size() == 1, "one font atlas output is produced"); - if (!outputs.empty()) + Expect(outputs.size() == shaderPackage.fontAssets.size(), "one font atlas output is produced for each declared font"); + for (const RenderCadenceCompositor::FontAtlasBuildOutput& output : outputs) { - Expect(std::filesystem::exists(outputs[0].imagePath), "font atlas image exists"); - Expect(std::filesystem::exists(outputs[0].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(outputs[0].jsonPath) > 0, "font atlas json is not empty"); + Expect(std::filesystem::exists(output.imagePath), "font atlas image exists"); + Expect(std::filesystem::exists(output.jsonPath), "font atlas json exists"); + Expect(std::filesystem::file_size(output.imagePath) > 0, "font atlas image is not empty"); + Expect(std::filesystem::file_size(output.jsonPath) > 0, "font atlas json is not empty"); } } } diff --git a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp index 1c67c7c..44aef40 100644 --- a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp +++ b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp @@ -62,17 +62,20 @@ std::string AllParametersShaderManifest() "description": "All parameter restore test shader", "category": "Tests", "entryPoint": "shadeVideo", - "fonts": [{ "id": "inter", "path": "Inter.ttf" }], + "fonts": [ + { "id": "inter", "path": "Inter.ttf" }, + { "id": "mono", "path": "Mono.ttf" } + ], "parameters": [ { "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": "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": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [ - { "value": "soft", "label": "Soft" }, - { "value": "hard", "label": "Hard" } + { "id": "mode", "label": "Mode", "type": "enum", "default": "inter", "options": [ + { "value": "inter", "label": "Inter" }, + { "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" } ] })"; @@ -94,10 +97,10 @@ RenderCadenceCompositor::SupportedShaderCatalog MakeCatalog(std::filesystem::pat return LoadCatalog(root); } -RenderCadenceCompositor::FontAtlasBuildOutput MakeFakeFontAtlas() +RenderCadenceCompositor::FontAtlasBuildOutput MakeFakeFontAtlas(const std::string& fontId = "inter") { RenderCadenceCompositor::FontAtlasBuildOutput atlas; - atlas.fontId = "inter"; + atlas.fontId = fontId; atlas.width = 2; atlas.height = 2; atlas.ascender = -0.8; @@ -226,6 +229,7 @@ void TestInitializeFromRuntimeStateRestoresLayerStack() 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" / "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()); RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root); @@ -242,7 +246,7 @@ void TestInitializeFromRuntimeStateRestoresLayerStack() "offset": [0.25, -0.5], "tint": [0.1, 0.2, 0.3, 0.4], "enabled": false, - "mode": "hard", + "mode": "mono", "titleText": "RESTORED-TEXT", "drop": 4 } @@ -275,7 +279,7 @@ void TestInitializeFromRuntimeStateRestoresLayerStack() Expect(snapshot.displayLayers[0].parameterValues.at("offset").numberValues == std::vector({ 0.25, -0.5 }), "restore preserves vec2 parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("tint").numberValues == std::vector({ 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("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("drop").numberValues.front() == 4.0, "restore preserves trigger counts"); Expect(snapshot.displayLayers[1].id == "layer-33", "restore preserves later supported layer order"); @@ -488,6 +492,7 @@ void TestTextTexturesArePreparedInRuntimeModel() std::filesystem::path root = MakeTestRoot(); WriteFile(root / "all-params" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n"); WriteFile(root / "all-params" / "Inter.ttf", "not a real font, but enough for catalog support checks"); + WriteFile(root / "all-params" / "Mono.ttf", "not a real font, but enough for catalog support checks"); WriteFile(root / "all-params" / "shader.json", AllParametersShaderManifest()); RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root); @@ -503,6 +508,7 @@ void TestTextTexturesArePreparedInRuntimeModel() artifact.fragmentShaderSource = "void main(){}"; artifact.parameterDefinitions = snapshot.displayLayers[0].parameterDefinitions; artifact.fontAtlases.push_back(MakeFakeFontAtlas()); + artifact.fontAtlases.push_back(MakeFakeFontAtlas("mono")); artifact.message = "build ready"; 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.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); } } diff --git a/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp b/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp index 2bed6e8..2b58383 100644 --- a/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp +++ b/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp @@ -174,6 +174,65 @@ void SupportsTextParametersWithDeclaredFont() 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() { std::string msdfReason; @@ -217,6 +276,8 @@ int main() RejectsTextureAssets(); RejectsTextParametersWithoutDeclaredFont(); SupportsTextParametersWithDeclaredFont(); + SupportsTextParametersWithFontSelector(); + RejectsTextParametersWithFontSelectorUnknownOption(); BuildsDeclaredFontAtlasesDuringCatalogLoad(); if (gFailures != 0) diff --git a/tests/ShaderPackageRegistryTests.cpp b/tests/ShaderPackageRegistryTests.cpp index 8c0feb8..c7520a6 100644 --- a/tests/ShaderPackageRegistryTests.cpp +++ b/tests/ShaderPackageRegistryTests.cpp @@ -49,6 +49,7 @@ void TestValidManifest() const std::filesystem::path root = MakeTestRoot(); 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" / "Mono.ttf", "not a real font, but enough for existence checks"); WriteShaderPackage(root, "look", R"({ "id": "look-01", "name": "Look 01", @@ -56,15 +57,18 @@ void TestValidManifest() "category": "Tests", "entryPoint": "shadeVideo", "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 }, "feedback": { "enabled": true }, "parameters": [ { "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": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [ - { "value": "soft", "label": "Soft" }, - { "value": "hard", "label": "Hard" } + { "id": "titleText", "label": "Title", "type": "text", "default": "LIVE", "font": "inter", "fontParameter": "mode", "maxLength": 32 }, + { "id": "mode", "label": "Mode", "type": "enum", "default": "inter", "options": [ + { "value": "inter", "label": "Inter" }, + { "value": "mono", "label": "Mono" } ] }, { "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(package.id == "look-01", "manifest id is preserved"); 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.feedback.enabled && package.feedback.writePassId == "main", "feedback defaults to the implicit main pass"); Expect(package.parameters.size() == 4, "parameters 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].fontParameterId == "mode", "text font selector 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");