From 283f38dddb9ad379e14641000a62f24e9d7f55ce Mon Sep 17 00:00:00 2001 From: Aiden Date: Fri, 22 May 2026 17:22:57 +1000 Subject: [PATCH] Font selector --- README.md | 3 + config/runtime-host.json | 4 +- shaders/text-overlay/fonts/AnalogMono.ttf | Bin 0 -> 21792 bytes shaders/text-overlay/fonts/LICENSE.txt | 60 +++++++++++++++++ shaders/text-overlay/shader.json | 22 +++++++ src/control/RuntimeStateJson.h | 2 + .../catalog/SupportedShaderCatalog.cpp | 44 ++++++++++--- src/runtime/layers/RuntimeLayerModel.cpp | 12 +++- .../text/RuntimeTextTextureComposer.cpp | 31 ++++++++- src/shader/ShaderManifestParameters.cpp | 11 ++++ src/shader/ShaderTypes.h | 1 + tests/FontAtlasBuilderTests.cpp | 12 ++-- ...adenceCompositorRuntimeLayerModelTests.cpp | 30 ++++++--- ...eCompositorSupportedShaderCatalogTests.cpp | 61 ++++++++++++++++++ tests/ShaderPackageRegistryTests.cpp | 17 +++-- 15 files changed, 276 insertions(+), 34 deletions(-) create mode 100644 shaders/text-overlay/fonts/AnalogMono.ttf create mode 100644 shaders/text-overlay/fonts/LICENSE.txt 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 0000000000000000000000000000000000000000..d1025f2161e61a25ff8070e9fe7f654616497dce GIT binary patch literal 21792 zcmc&+3zS`Db>8RRbLUPTlgVUelF9qtOcH`Z2pI-ZP>IHX$|FKV)QAkp5E6$;%mfTb z>rwHAND*6W5hQ|NoqfBqGu!nN(%_wrx8uJ~H}_BO)(PadpK7TVJ$I*5OEYJdVx4 zrI(+v`SqWD*C#}hD{+4HuIon*Z2Qp9aC{EhH^xV~!U@2|*KZxWsi_eClllH4gZNo&}MOr39g9B0;;5c>keishACF4`#POILG^ z@i0e{tdS&B-$m1lFjJLQXSwi+Qy~;an7qmh{=&k*2UM-%U zp^up#^*a^oBMS``8Jy+)Q7ZC@m2DDWXI%4a)K5> z3b8^`=u%|9T2~5B`(=4hrkZQuC-l39d{`dh+0;*_emM1&sV{&3-a>e^z(Q3AJQ4z= zAn7gJ;ASJpF*v+4s2`}`Q~zH5@y=zP+d6OU{CMY&=bSd@z?@s>JUHjb-1&1anS1lx zhvt5D?$74UpLfo@ZS!`|yKCOJ=XcKEI{(J`ch5i4m3FP|+TQi{uKT*a*7ewe^$V_9 zaPNXIcUQXCcVF0jbN7AS|Fm$y!sjg9xA5@7k1hPp!XNcKqi1)|gFR0yN*C=|v~SS^ ziyrMA=-tqp_1@9@Nbix}<9!2toBDS29qM~a-v|0W-S=q!;{L7uhx|D(kl7Vlg9 znZ?JKoWJCqOTI9W4xBcyW8l_-hXx)Um|VJX>6J_GT>9N*tCsCs_Q>)D%XconWBCtO zEM9TZirZFvb;ab$^((JldH2dkS1nt0)v9||9a+6<^_8pNzxun4fyR}MTN?K_zTNm~ z^Q`7r^K)w|Yj&*p*qSE?7Y%M7%mzO__}JQIYj>=@W$nXjk4S}HSY-^-P15YqmEJk2 zzq%aDrZjTGO0!hb3~lSuEz;ZU8SKZ3XLOI|SSq%w%cNvkRkzf#TDZo#<+2*q?{th> zd7I#ru9nD@{-&_!YUkm4U9U}I3qJLifC%npY&%JU@ZPBmG&32JbLE9bwSTC8sHZtR zI6TEN)^fM`?rCHV1(tDSKluIt^pHss|=+~ALb zSQFyKpFWgiC-gSdQ6pfA)jZ%lB+n=Bz2Fu&gR`EYCH+IgrzVCvHQ;MgStOgjCN0k> zHS(Zp52UceoiXpETSXXxr9>qiF2nBE8K> z>OM5XUg_Yu8q216ic{{}?5_e}G=J>KkC5JgKg&u)pXvBB^e8?wE0kMmU5pVmIfgT) zSw7Fp*uwG{O^S#GqG*E(P=elIXYmS|%WH~9Kv5SHnTMU|rFEqS;+53h?w8By@Ko?p z(xTglc_uAgP2L@JaJV;jUbTAIJ?kN5Bx3+=vyfHVyKCI73HHxxSi+tT0CsZz`pKI| z+LCpdmX1MF!K&)=6eLf8wOdr&&pP&#+i4R5u~%A=xh<}kRV4r?!*YTw9>ZY;C*Y0< ztHWXsP16i;pc=^S$uT{npaqR7*TGgjNPxBmi)mCfLa|qh#Z=Zpn>LASIbF1jSO6G7 zuS=v!E=WI@)cq13@A^Z=(#NOdI6DnX1=f8WP%<5bNaXRiq#I;J(@cSDtDH4-wnO%tO%^ZVCm_wim`GQg4AwQZ4SIT8sjohh4`Z=V8*B4w(*P(&u43QDKO0Y{F zDg4A478L5W8j-QcsXkpqznV-!X&mXYEuBl-7Li$X5Za^H@Bkw&;AM168OyCoGmTkZ znkCp$&y9JNRqj0XytdJj~@d zsL|3-sJ$Dy=8&u#i0}#DqahZJ33)sS=rXzmR8^}X>bUJX!WmI>Q*Oh_Gb3NW#JL9oFVi`vRc${h6L zGHEU`|LAgpD$6#c>m!G}kXlFbE6Y>A$VCSr?r8hX`} zU}7e@-M4G;mM$LAhm?i`<2LPswOe#wtrOmd_~TokptX!{$%p+&!+us&5JIj;(Jx~& zkB^sSSN}$G%U9(@?Ij>w$Z0Z=1_oFrT#C*;ZlUKQMlCODmcpmJM@8BzX~s!Hu6{N{ z5r;Ju-gcSTs#UH*d1hIzptX>Fa*dUwweAbu zl_e+1a4$k3w!b_^D;=#q`(03?OR6fk*YSD|m0Ipa-xR;7Kp`1ps6`Xvuhey8IH|gI zgLW|=2pi@njHebsY3ENtcO`#zf5}a&C1$Ye@H5q5h78wONQFpI>d$>7^1m<^H9y2z z=ug>pJQ@3f*D?NCeH46B!%q=|{zpE}jLS$n1^W5;rs3gHOq&fCsPQzjtJXG~-NvZFt;`ll5dUMXx5VD+M*H6OwmM|!24WZ{ZT zqqATiCIvj4*PV(cB?F7MDUM@>1dqt=<6g!~^R__9e4&(AkUh{ZII^!~P7vAnj2Qz>>R2N!UVv%j5BhWrkn@=9SEv z6xVYk)K!d$w0qSeMOO+ry?|%1FZ7L$XoI@aj)5C^t^g_&*cos`m|`cG)`4NF`9Uw> zm2a4#m(^iL0+z-2L9{pqC}~5pJgma)0-QVV1m<|gm}EeK92dijLOf+>c7fbPOJN_7 zXQ}#9VU-pY%}!WpDh4J9g$b5t#9i|!y z)V9Ub)LtOQV2@KI4+I|Hl>71u)S`^`73tD?GW(7?QZ>>xJr4fg*U(BVn5w%KWio}u ze&KO4pn(`}*%fHQuG++oV58^@1j=#JWaM?dTf;V{I*l$PCH#@rG2IVa<1<}@2I<|C z1Qs(J5Apo@u=rW^W$gQyHt4H$mKN5>)#6IzL+G0;V_{q)Tm42OQ1-#E zRg9L@jA9HNg@Twgm%_(1?*h4!?QcljSk1Tmud6xe(9rnOM=Mb`=HY1JwYnYtTY;UYDsmLQZsO z(waod5icq=ZaakA_^v(W9O~0x28-4$i0hOvlz%S2V*qY^1*e{^TF0`S)a1`fQo|$Y zf(V#*fS(ZNoPViAegLBg^_+PFlU^sH?<9UD+j$ngl|frWjCsr~MZE2V6oIF4mjBzQA zn|6(rQJ%viL`eO3HUAc5sjS2|5ogk#${L_?8RO6a0{@xeg36)ut@z#w00FGQVIr-F zgJWGd;^P5$!;gkUn5y_iy1^Zss|In)&e*(yAAN=oXBiIhjN6r36{vAihZ7P|JoE^| zf)+JXX|~9tInl=0F1MyIXKSW_&IvDcncD^~kt!~0-19; zWw!SNS@CVSqNTZnS(SR=yAQP0=fP)Pj!K6-Ma5^(B3J<5m8_bj8LsK`)Yhb=P!8*b zN!&v_$h1yv5uCw987YYXdekkLC~!0RAqQjuYT8y+&(XD-CGjd^0rl+jT0o7Gp9>pr zi35P46a&a3=L*r&H2>Ea?6nJ{7ja+`qb1~)8edmAf;_?d!+9+TNln;4zhx(&&*Tp6 zLkE<_M)@I>4l$n9ICH6%T%f_i88dw7-Q3jXOVKBzJz+Y58?Ak-x8;syeY<^|oWokv zL3!vTP-wAt-g7dpJk>`o10!>jwg9a7!ks$twJeKz66vS}l!DcESti`Jr7V@|%pIh&{Y(1#Q%_Neln;p^kWEbB6Tk zX0QlG2wfQ=Xf4VF$b&S(D9EsH22}Rh`rf}&?ZIdt19OvEYRwcH90FRkkro~-DnV+s z%Gr2eBG(9K61QUk|w4k@}UiS3Z3>Lc5tkM<{^%0AkvF)n;E1j4Ls*U8C1aVVo2n%~}%P(}QP(3}tzTS3OamE57e5JvXu13m)i2WnpF@hkDKARMR<|&)j zp^OVYcQS0Tfn!rz4D&NqtMk0xAWXmjN*(5y7CjdI{k)jAGFxC;^iCv=-;>9* z+;4$t(QDzKOAs$~oKOGJh@rKP&Lj;w#G2&jG8mESqO*;F*?g9lSfB%SY&e&|pHC}g z3>!Gb;<4LiXyFy=jjXj%nW(SEu}VGE{S>t=eFpYuZllTsDJ*$p#5JfJTfNA3I6%cYb5El$GGNg+R z(ostTn>IWJi}aYr(E%e@6t&v%R73)Lm(gjOxff`7}%) z2$2)-{)FhXH#{^8t}zRIXany^vzq?j9Q6U9r=F?VQ!}N`>4Z^H#wYY22t$7RhvOPK zDP6HocF3-lu+~KV_20@&f7LRY|dx z^M0L%W3x}dA^%d1)juKBA&%K0`+yM@@riLEj|v`_jI`Gf!4KI@$m66LeijFU=N|*B zpl_3~ID#!qqZPcps8@hU$>$5t<}hzMEl5FDrV}e@(LX4cg)z<<0M6OuD|;(vrKZz` zKwR2UF5*Y9dr%KnKeME zn1UfOuTh;rpQuCmt+vkQ<++sfX}KU~ZyEU{C1B*Yk0Mx2vJ%7|4vqdej>kFr!@GLZ z82!aP20dzbVV;5`A3e4{(Je@@Cu1K>gME;)FhSk{YW^>KD(fIokE5W;QH-Kzc`=Wl z$UbKI788mufPxoRvbFOQaKYvCJ^a@|DWV^vv<~>YVWObGfViW z)dqH9uDC2VK!>!2{ZW2Pf+VfBPo9pU9X;4>U*DB;^sKY&%tP?2P^DUS!a2NRsD>Nx z>K=L-Y@DGHoqF-BMga?jKXV2j6gZ8^e{75E{73SvkJZ)P-$6f*(B@-0G&jrioE&mb zK5|8m=s83tcCDU(%Nc2tYI&=XA(vVGz3FjArL{U(y`JYS6(=u1SkTb|n}!cyOW%#f zK2Jwi7KpnpG_^R^IPKW)XQN$szIk0c;vi_Y-i~@fyBkuU8RDyYoMuGxxmkID)+m;F zzj|FeHe6t;rVS%ypUY8T&a!!<**GY9-3$ywZr6sqcwM0S*Eazc?MdI(Ld$$5RD7>S zeMVobP3tFUGW1hujQ<_6=*P7befk#h37x)GKJCh#LsOURKb%z`z`d4lurYBb~n{py=3GDQ*=N%UhnM3SU%vFq4F%~V=6?r~CS^lIP zX{aJBfd@SaW}_##7dP;z8<|fOdkY>*7Nl{9_H&XvmTfD<`59P@jVv^o&SW_@{$j?< zxuZw;PUnm|h7IC$f76sPToV z#U8$}g!e*9#fdUFPMVRz$YC}qX22_!BL{5;IpBplMy}hKTSviz>lkk_uK~O;zSr-~ zfrgIHG=!0%T*dYMew^YR$4C6QhUg#~E|XE* zy#cL8>n4|4qAtCdkjsl)h35< zv=?1W$N}ux&2C(|MsC7(q=elexe?ch-|1+*OWTg(tbr%?>hr{p&xM{RAdhU{dw6uL zaqa%`Lyf)T69@Jl9Npcx=BCDoDd5nJjs4@NPwYB4Iy&Ciy?5fk*vL)Vcmj`Z0iWdb zGW5Sku9HKcV#rQ_&x7EH+{%`*vBqV4_gr^q0?CWy z%V6&-kfT;%9;yL?YcLWNpyJK&(X;Tkvd+faoOAFd z_&JziK2M%2&x5a=53yf>zxuURUWC7mc>x4@vAjfHDi^_cFM&UAmzM#M%VmeWT&{pW zT`8}WSIMhor@Th4lGn=BP{K##R(YHJp}b!nlefzq@^1N%d_q1dx5+=qTjhO8HA&?? z^6T!lxd=vhPVkU2v$K?t6g4|xeVSMkVO`Epb{so(TKjeG7Htype != 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");