INtial text again

This commit is contained in:
2026-05-20 16:26:36 +10:00
parent 081364e764
commit e43ac21b2f
12 changed files with 608 additions and 15 deletions

View File

@@ -122,6 +122,7 @@ else()
target_link_libraries(RenderCadenceCompositor PRIVATE
opengl32
Ole32
Windowscodecs
Ws2_32
)
source_group(TREE "${SRC_DIR}" FILES ${RENDER_CADENCE_APP_SOURCES})

View File

@@ -89,7 +89,6 @@ Intentionally not included yet:
- additional input format conversion/scaling
- temporal/history/feedback shader storage
- texture/LUT asset upload
- text-parameter rasterization and font atlas GL binding
- runtime state
- OSC control
- persistent control/state writes
@@ -142,7 +141,7 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
- [ ] Texture asset loading and upload
- [ ] LUT asset loading and upload
- [x] CPU-side MSDF/MTSDF font atlas generation cache
- [ ] Text parameter rasterization
- [x] Single-line text parameter rasterization and GL binding
- [ ] Trigger history/event buffers for overlapping repeated trigger effects
- [ ] Full runtime state store/read model
- [ ] Persistent layer stack/config writes
@@ -353,8 +352,7 @@ Current runtime shader support is deliberately limited to stateless full-frame p
- no temporal history
- no feedback storage
- no texture/LUT assets yet
- font atlas generation is CPU-side only; text parameter atlas upload/binding is not render-ready yet
- no text parameters yet
- text parameters are single-line ASCII masks backed by prepared MTSDF font atlases
- manifest defaults initialize parameters
- HTTP controls can update runtime parameter values without rebuilding GL programs when the shader program is unchanged
- trigger parameters are treated as latest-pulse controls: each press increments the trigger count and records the current runtime time
@@ -371,7 +369,7 @@ Shader source semantics:
- later layers receive `gVideoInput = original input` and `gLayerInput = previous layer`.
- named intermediate pass inputs inside a multipass layer are still routed through the selected pass-source slot; layer stacking should use `gLayerInput`.
The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now.
The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as temporal, feedback, and texture-backed shaders are hidden from the control UI for now.
Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter definitions, current parameter values, build status, and render-ready artifacts. POST controls mutate this app-owned model and may start background shader builds when the selected shader changes.

View File

@@ -61,6 +61,8 @@
#define GL_UNIFORM_BUFFER 0x8A11
#define GL_RGBA8 0x8058
#define GL_RGBA16F 0x881A
#define GL_RED 0x1903
#define GL_R8 0x8229
#define GL_TEXTURE0 0x84C0
#define GL_ACTIVE_TEXTURE 0x84E0
#define GL_ARRAY_BUFFER 0x8892

View File

@@ -105,7 +105,7 @@ GLuint RuntimeRenderScene::RenderLayer(
if (!pass.renderer || !pass.renderer->HasProgram())
continue;
GLuint sourceTexture = videoInputTexture;
GLuint sourceTexture = layerInputTexture;
if (!pass.inputNames.empty())
{
const std::string& inputName = pass.inputNames.front();

View File

@@ -56,6 +56,11 @@ bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram&
preparedProgram.program = 0;
preparedProgram.vertexShader = 0;
preparedProgram.fragmentShader = 0;
if (!mTextTextures.Configure(mArtifact, error))
{
DestroyProgram();
return false;
}
return true;
}
@@ -64,6 +69,7 @@ void RuntimeShaderRenderer::UpdateArtifactState(const RuntimeShaderArtifact& art
mArtifact.parameterDefinitions = artifact.parameterDefinitions;
mArtifact.parameterValues = artifact.parameterValues;
mArtifact.message = artifact.message;
mTextTextures.UpdateArtifactState(artifact);
}
bool RuntimeShaderRenderer::BuildPreparedProgram(
@@ -117,6 +123,7 @@ bool RuntimeShaderRenderer::BuildPreparedPassProgram(
preparedProgram.succeeded = true;
AssignSamplerUniforms(preparedProgram.program);
RuntimeTextTextureCache::AssignSamplerUniforms(preparedProgram.program, artifact);
return true;
}
@@ -131,6 +138,7 @@ void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, uns
glDisable(GL_BLEND);
UpdateGlobalParams(frameIndex, width, height);
BindRuntimeTextures(sourceTexture, layerInputTexture);
mTextTextures.BindTextTextures(mProgram);
glBindVertexArray(mVertexArray);
glUseProgram(mProgram);
glDrawArrays(GL_TRIANGLES, 0, 3);
@@ -300,6 +308,7 @@ void RuntimeShaderRenderer::DestroyProgram()
glDeleteShader(mVertexShader);
if (mFragmentShader != 0)
glDeleteShader(mFragmentShader);
mTextTextures.ShutdownGl();
mProgram = 0;
mVertexShader = 0;
mFragmentShader = 0;

View File

@@ -2,6 +2,7 @@
#include "GLExtensions.h"
#include "RuntimeShaderProgram.h"
#include "RuntimeTextTextureCache.h"
#include "../../runtime/RuntimeShaderArtifact.h"
#include <cstdint>
@@ -45,6 +46,7 @@ private:
void DestroyStaticGlResources();
RuntimeShaderArtifact mArtifact;
RuntimeTextTextureCache mTextTextures;
GLuint mProgram = 0;
GLuint mVertexShader = 0;
GLuint mFragmentShader = 0;

View File

@@ -0,0 +1,450 @@
#include "RuntimeTextTextureCache.h"
#include "../../runtime/RuntimeJson.h"
#include <algorithm>
#include <cmath>
#include <fstream>
#include <sstream>
#include <wincodec.h>
#include <wrl/client.h>
namespace
{
constexpr GLuint kFirstTextTextureUnit = 8;
constexpr unsigned kTextTextureHeight = 128;
constexpr unsigned kTextTexturePadding = 8;
constexpr double kFontPixelsPerEm = 96.0;
std::string ReadTextFile(const std::filesystem::path& path)
{
std::ifstream input(path, std::ios::binary);
if (!input)
return std::string();
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
const JsonValue* FindObjectValue(const JsonValue& object, const std::string& key)
{
return object.isObject() ? object.find(key) : nullptr;
}
double NumberMember(const JsonValue& object, const std::string& key, double fallback = 0.0)
{
const JsonValue* value = FindObjectValue(object, key);
return value && value->isNumber() ? value->asNumber(fallback) : fallback;
}
struct ComThreadGuard
{
~ComThreadGuard()
{
if (initialized)
CoUninitialize();
}
bool Initialize()
{
const HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
initialized = SUCCEEDED(result);
return SUCCEEDED(result) || result == RPC_E_CHANGED_MODE;
}
bool initialized = false;
};
}
RuntimeTextTextureCache::~RuntimeTextTextureCache()
{
ShutdownGl();
}
bool RuntimeTextTextureCache::Configure(const RuntimeShaderArtifact& artifact, std::string& error)
{
ShutdownGl();
mArtifact = artifact;
for (const RenderCadenceCompositor::FontAtlasBuildOutput& output : artifact.fontAtlases)
{
Atlas atlas;
if (!LoadAtlas(output, atlas, error))
return false;
mAtlases.push_back(std::move(atlas));
}
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
{
if (definition.type != ShaderParameterType::Text)
continue;
if (FindAtlas(definition.fontId) == nullptr)
{
error = "No prepared font atlas is available for text parameter '" + definition.id + "'.";
return false;
}
TextTexture texture;
texture.parameterId = definition.id;
texture.fontId = definition.fontId;
mTextTextures.push_back(std::move(texture));
}
error.clear();
return true;
}
void RuntimeTextTextureCache::UpdateArtifactState(const RuntimeShaderArtifact& artifact)
{
mArtifact.parameterDefinitions = artifact.parameterDefinitions;
mArtifact.parameterValues = artifact.parameterValues;
}
void RuntimeTextTextureCache::BindTextTextures(GLuint program)
{
for (std::size_t index = 0; index < mTextTextures.size(); ++index)
{
TextTexture& textTexture = mTextTextures[index];
if (!EnsureTextTexture(textTexture))
continue;
glActiveTexture(GL_TEXTURE0 + kFirstTextTextureUnit + static_cast<GLuint>(index));
glBindTexture(GL_TEXTURE_2D, textTexture.texture);
}
glUseProgram(program);
AssignSamplerUniforms(program, mArtifact);
glUseProgram(0);
glActiveTexture(GL_TEXTURE0);
}
void RuntimeTextTextureCache::ShutdownGl()
{
for (TextTexture& texture : mTextTextures)
DestroyTexture(texture);
mTextTextures.clear();
mAtlases.clear();
}
void RuntimeTextTextureCache::AssignSamplerUniforms(GLuint program, const RuntimeShaderArtifact& artifact)
{
glUseProgram(program);
GLuint nextUnit = kFirstTextTextureUnit;
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
{
if (definition.type != ShaderParameterType::Text)
continue;
const std::string samplerName = definition.id + "Texture";
const GLint location = glGetUniformLocation(program, samplerName.c_str());
if (location >= 0)
glUniform1i(location, static_cast<GLint>(nextUnit));
const std::string samplerArrayName = samplerName + "_0";
const GLint arrayLocation = glGetUniformLocation(program, samplerArrayName.c_str());
if (arrayLocation >= 0)
glUniform1i(arrayLocation, static_cast<GLint>(nextUnit));
++nextUnit;
}
glUseProgram(0);
}
bool RuntimeTextTextureCache::LoadAtlas(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const
{
atlas.fontId = output.fontId;
if (!LoadAtlasJson(output, atlas, error))
return false;
if (!LoadAtlasImage(output, atlas, error))
return false;
if (atlas.width == 0 || atlas.height == 0 || atlas.rgbaPixels.empty())
{
error = "Font atlas image is empty for font '" + output.fontId + "'.";
return false;
}
return true;
}
bool RuntimeTextTextureCache::LoadAtlasJson(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const
{
const std::string jsonText = ReadTextFile(output.jsonPath);
if (jsonText.empty())
{
error = "Could not read font atlas json: " + output.jsonPath.string();
return false;
}
JsonValue root;
if (!ParseJson(jsonText, root, error))
return false;
const JsonValue* metrics = FindObjectValue(root, "metrics");
if (metrics)
{
atlas.ascender = NumberMember(*metrics, "ascender", atlas.ascender);
atlas.descender = NumberMember(*metrics, "descender", atlas.descender);
atlas.lineHeight = NumberMember(*metrics, "lineHeight", atlas.lineHeight);
}
const JsonValue* glyphs = FindObjectValue(root, "glyphs");
if (!glyphs || !glyphs->isArray())
{
error = "Font atlas json has no glyph array: " + output.jsonPath.string();
return false;
}
for (const JsonValue& glyphJson : glyphs->asArray())
{
if (!glyphJson.isObject())
continue;
const unsigned codepoint = static_cast<unsigned>(NumberMember(glyphJson, "unicode", 0.0));
Glyph glyph;
glyph.advance = NumberMember(glyphJson, "advance", 0.0);
const JsonValue* planeBounds = FindObjectValue(glyphJson, "planeBounds");
const JsonValue* atlasBounds = FindObjectValue(glyphJson, "atlasBounds");
if (planeBounds && atlasBounds)
{
glyph.planeLeft = NumberMember(*planeBounds, "left", 0.0);
glyph.planeTop = NumberMember(*planeBounds, "top", 0.0);
glyph.planeRight = NumberMember(*planeBounds, "right", 0.0);
glyph.planeBottom = NumberMember(*planeBounds, "bottom", 0.0);
glyph.atlasLeft = NumberMember(*atlasBounds, "left", 0.0);
glyph.atlasTop = NumberMember(*atlasBounds, "top", 0.0);
glyph.atlasRight = NumberMember(*atlasBounds, "right", 0.0);
glyph.atlasBottom = NumberMember(*atlasBounds, "bottom", 0.0);
glyph.hasBounds = true;
}
atlas.glyphsByCodepoint[codepoint] = glyph;
}
error.clear();
return true;
}
bool RuntimeTextTextureCache::LoadAtlasImage(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const
{
ComThreadGuard comGuard;
if (!comGuard.Initialize())
{
error = "Could not initialize COM for font atlas PNG loading.";
return false;
}
Microsoft::WRL::ComPtr<IWICImagingFactory> factory;
HRESULT result = CoCreateInstance(
CLSID_WICImagingFactory,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(factory.GetAddressOf()));
if (FAILED(result))
{
error = "Could not create WIC imaging factory for font atlas PNG loading.";
return false;
}
Microsoft::WRL::ComPtr<IWICBitmapDecoder> decoder;
result = factory->CreateDecoderFromFilename(
output.imagePath.wstring().c_str(),
nullptr,
GENERIC_READ,
WICDecodeMetadataCacheOnLoad,
decoder.GetAddressOf());
if (FAILED(result))
{
error = "Could not decode font atlas PNG: " + output.imagePath.string();
return false;
}
Microsoft::WRL::ComPtr<IWICBitmapFrameDecode> frame;
result = decoder->GetFrame(0, frame.GetAddressOf());
if (FAILED(result))
{
error = "Could not read font atlas PNG frame: " + output.imagePath.string();
return false;
}
Microsoft::WRL::ComPtr<IWICFormatConverter> converter;
result = factory->CreateFormatConverter(converter.GetAddressOf());
if (FAILED(result))
{
error = "Could not create WIC format converter for font atlas PNG.";
return false;
}
result = converter->Initialize(
frame.Get(),
GUID_WICPixelFormat32bppRGBA,
WICBitmapDitherTypeNone,
nullptr,
0.0,
WICBitmapPaletteTypeCustom);
if (FAILED(result))
{
error = "Could not convert font atlas PNG to RGBA.";
return false;
}
UINT width = 0;
UINT height = 0;
converter->GetSize(&width, &height);
atlas.width = static_cast<unsigned>(width);
atlas.height = static_cast<unsigned>(height);
atlas.rgbaPixels.assign(static_cast<std::size_t>(atlas.width) * atlas.height * 4u, 0);
const UINT stride = width * 4u;
result = converter->CopyPixels(nullptr, stride, static_cast<UINT>(atlas.rgbaPixels.size()), atlas.rgbaPixels.data());
if (FAILED(result))
{
error = "Could not copy font atlas PNG pixels.";
return false;
}
error.clear();
return true;
}
bool RuntimeTextTextureCache::EnsureTextTexture(TextTexture& texture)
{
const ShaderParameterValue* value = FindParameterValue(mArtifact, texture.parameterId);
const std::string text = value ? value->textValue : DefaultTextValue(mArtifact, texture.parameterId);
if (texture.texture != 0 && texture.cachedText == text)
return true;
const Atlas* atlas = FindAtlas(texture.fontId);
if (!atlas)
return false;
unsigned width = 0;
unsigned height = 0;
std::vector<unsigned char> pixels = ComposeTextMask(*atlas, text, width, height);
if (pixels.empty() || width == 0 || height == 0)
return false;
if (texture.texture == 0)
glGenTextures(1, &texture.texture);
glBindTexture(GL_TEXTURE_2D, texture.texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, static_cast<GLsizei>(width), static_cast<GLsizei>(height), 0, GL_RED, GL_UNSIGNED_BYTE, pixels.data());
glBindTexture(GL_TEXTURE_2D, 0);
texture.cachedText = text;
texture.width = width;
texture.height = height;
return texture.texture != 0;
}
std::vector<unsigned char> RuntimeTextTextureCache::ComposeTextMask(const Atlas& atlas, const std::string& text, unsigned& width, unsigned& height) const
{
double advance = 0.0;
for (unsigned char character : text)
{
const auto glyphIt = atlas.glyphsByCodepoint.find(character);
if (glyphIt != atlas.glyphsByCodepoint.end())
advance += glyphIt->second.advance;
}
width = (std::max)(1u, static_cast<unsigned>(std::ceil(advance * kFontPixelsPerEm)) + kTextTexturePadding * 2u);
height = kTextTextureHeight;
std::vector<unsigned char> mask(static_cast<std::size_t>(width) * height, 0);
const double baseline = kTextTexturePadding + (-atlas.ascender * kFontPixelsPerEm);
double penX = static_cast<double>(kTextTexturePadding);
for (unsigned char character : text)
{
const auto glyphIt = atlas.glyphsByCodepoint.find(character);
if (glyphIt == atlas.glyphsByCodepoint.end())
continue;
const Glyph& glyph = glyphIt->second;
if (glyph.hasBounds)
{
const int destLeft = static_cast<int>(std::floor(penX + glyph.planeLeft * kFontPixelsPerEm));
const int destTop = static_cast<int>(std::floor(baseline + glyph.planeTop * kFontPixelsPerEm));
const int destRight = static_cast<int>(std::ceil(penX + glyph.planeRight * kFontPixelsPerEm));
const int destBottom = static_cast<int>(std::ceil(baseline + glyph.planeBottom * kFontPixelsPerEm));
const double destWidth = (std::max)(1.0, static_cast<double>(destRight - destLeft));
const double destHeight = (std::max)(1.0, static_cast<double>(destBottom - destTop));
for (int y = destTop; y < destBottom; ++y)
{
if (y < 0 || y >= static_cast<int>(height))
continue;
const double v = (static_cast<double>(y) + 0.5 - destTop) / destHeight;
for (int x = destLeft; x < destRight; ++x)
{
if (x < 0 || x >= static_cast<int>(width))
continue;
const double u = (static_cast<double>(x) + 0.5 - destLeft) / destWidth;
const double atlasX = glyph.atlasLeft + u * (glyph.atlasRight - glyph.atlasLeft);
const double atlasY = glyph.atlasTop + v * (glyph.atlasBottom - glyph.atlasTop);
unsigned char& destination = mask[static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x)];
destination = (std::max)(destination, SampleAtlasAlpha(atlas, atlasX, atlasY));
}
}
}
penX += glyph.advance * kFontPixelsPerEm;
}
for (unsigned y = 0; y < height / 2u; ++y)
{
unsigned char* topRow = mask.data() + static_cast<std::size_t>(y) * width;
unsigned char* bottomRow = mask.data() + static_cast<std::size_t>(height - 1u - y) * width;
for (unsigned x = 0; x < width; ++x)
std::swap(topRow[x], bottomRow[x]);
}
return mask;
}
const RuntimeTextTextureCache::Atlas* RuntimeTextTextureCache::FindAtlas(const std::string& fontId) const
{
for (const Atlas& atlas : mAtlases)
{
if (atlas.fontId == fontId)
return &atlas;
}
return nullptr;
}
const ShaderParameterValue* RuntimeTextTextureCache::FindParameterValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId)
{
const auto valueIt = artifact.parameterValues.find(parameterId);
return valueIt == artifact.parameterValues.end() ? nullptr : &valueIt->second;
}
std::string RuntimeTextTextureCache::DefaultTextValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId)
{
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
{
if (definition.id == parameterId)
return definition.defaultTextValue;
}
return std::string();
}
unsigned char RuntimeTextTextureCache::SampleAtlasAlpha(const Atlas& atlas, double x, double y)
{
const int ix = (std::max)(0, (std::min)(static_cast<int>(atlas.width) - 1, static_cast<int>(std::floor(x))));
const int iy = (std::max)(0, (std::min)(static_cast<int>(atlas.height) - 1, static_cast<int>(std::floor(y))));
const std::size_t pixelOffset = (static_cast<std::size_t>(iy) * atlas.width + static_cast<std::size_t>(ix)) * 4u;
return atlas.rgbaPixels[pixelOffset + 3u];
}
void RuntimeTextTextureCache::DestroyTexture(TextTexture& texture)
{
if (texture.texture != 0)
glDeleteTextures(1, &texture.texture);
texture.texture = 0;
texture.width = 0;
texture.height = 0;
texture.cachedText.clear();
}

View File

@@ -0,0 +1,76 @@
#pragma once
#include "GLExtensions.h"
#include "../../runtime/RuntimeShaderArtifact.h"
#include <map>
#include <string>
#include <vector>
class RuntimeTextTextureCache
{
public:
RuntimeTextTextureCache() = default;
RuntimeTextTextureCache(const RuntimeTextTextureCache&) = delete;
RuntimeTextTextureCache& operator=(const RuntimeTextTextureCache&) = delete;
~RuntimeTextTextureCache();
bool Configure(const RuntimeShaderArtifact& artifact, std::string& error);
void UpdateArtifactState(const RuntimeShaderArtifact& artifact);
void BindTextTextures(GLuint program);
void ShutdownGl();
static void AssignSamplerUniforms(GLuint program, const RuntimeShaderArtifact& artifact);
private:
struct Glyph
{
double advance = 0.0;
double planeLeft = 0.0;
double planeTop = 0.0;
double planeRight = 0.0;
double planeBottom = 0.0;
double atlasLeft = 0.0;
double atlasTop = 0.0;
double atlasRight = 0.0;
double atlasBottom = 0.0;
bool hasBounds = false;
};
struct Atlas
{
std::string fontId;
unsigned width = 0;
unsigned height = 0;
double ascender = -0.9;
double descender = 0.25;
double lineHeight = 1.2;
std::vector<unsigned char> rgbaPixels;
std::map<unsigned, Glyph> glyphsByCodepoint;
};
struct TextTexture
{
std::string parameterId;
std::string fontId;
std::string cachedText;
GLuint texture = 0;
unsigned width = 0;
unsigned height = 0;
};
bool LoadAtlas(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const;
bool LoadAtlasJson(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const;
bool LoadAtlasImage(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const;
bool EnsureTextTexture(TextTexture& texture);
std::vector<unsigned char> ComposeTextMask(const Atlas& atlas, const std::string& text, unsigned& width, unsigned& height) const;
const Atlas* FindAtlas(const std::string& fontId) const;
static const ShaderParameterValue* FindParameterValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId);
static std::string DefaultTextValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId);
static unsigned char SampleAtlasAlpha(const Atlas& atlas, double x, double y);
static void DestroyTexture(TextTexture& texture);
RuntimeShaderArtifact mArtifact;
std::vector<Atlas> mAtlases;
std::vector<TextTexture> mTextTextures;
};

View File

@@ -1,6 +1,7 @@
#pragma once
#include "ShaderTypes.h"
#include "FontAtlasBuilder.h"
#include <map>
#include <string>
@@ -24,4 +25,5 @@ struct RuntimeShaderArtifact
std::string message;
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
std::vector<RenderCadenceCompositor::FontAtlasBuildOutput> fontAtlases;
};

View File

@@ -112,6 +112,17 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
return build;
}
RenderCadenceCompositor::FontAtlasBuildConfig fontConfig;
fontConfig.repoRoot = repoRoot;
RenderCadenceCompositor::FontAtlasBuilder fontAtlasBuilder(fontConfig);
std::vector<RenderCadenceCompositor::FontAtlasBuildOutput> fontAtlasOutputs;
if (!fontAtlasBuilder.BuildPackageFontAtlases(shaderPackage, fontAtlasOutputs, error))
{
build.succeeded = false;
build.message = error.empty() ? "Font atlas build failed." : error;
return build;
}
ShaderCompiler compiler(
repoRoot,
runtimeBuildDir / (shaderId + ".wrapper.slang"),
@@ -144,6 +155,7 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
build.artifact.shaderId = shaderPackage.id;
build.artifact.displayName = shaderPackage.displayName;
build.artifact.parameterDefinitions = shaderPackage.parameters;
build.artifact.fontAtlases = std::move(fontAtlasOutputs);
if (!build.artifact.passes.empty())
build.artifact.fragmentShaderSource = build.artifact.passes.front().fragmentShaderSource;
build.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms.";

View File

@@ -21,13 +21,25 @@ ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& s
if (!shaderPackage.textureAssets.empty())
return { false, "RenderCadenceCompositor does not load shader texture assets yet; texture-backed shaders need a CPU-prepared asset handoff first." };
if (!shaderPackage.fontAssets.empty())
return { false, "RenderCadenceCompositor does not load shader font assets yet; text shaders need a CPU-prepared asset handoff first." };
for (const ShaderParameterDefinition& parameter : shaderPackage.parameters)
{
if (parameter.type == ShaderParameterType::Text)
return { false, "RenderCadenceCompositor currently skips text parameters because they require per-shader text texture storage." };
if (parameter.type != ShaderParameterType::Text)
continue;
if (parameter.fontId.empty())
return { false, "Text parameter '" + parameter.id + "' must reference a declared font asset." };
bool hasFontAsset = false;
for (const ShaderFontAsset& fontAsset : shaderPackage.fontAssets)
{
if (fontAsset.id == parameter.fontId)
{
hasFontAsset = true;
break;
}
}
if (!hasFontAsset)
return { false, "Text parameter '" + parameter.id + "' references unknown font asset '" + parameter.fontId + "'." };
}
bool writesLayerOutput = false;

View File

@@ -112,19 +112,39 @@ void RejectsTextureAssets()
Expect(result.reason.find("texture") != std::string::npos, "texture rejection should mention texture assets");
}
void RejectsTextParameters()
void RejectsTextParametersWithoutDeclaredFont()
{
ShaderPackage shaderPackage = MakeSinglePassPackage();
ShaderParameterDefinition parameter;
parameter.id = "caption";
parameter.type = ShaderParameterType::Text;
parameter.fontId = "missing";
shaderPackage.parameters.push_back(parameter);
const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(!result.supported, "text-parameter packages should be rejected for now");
Expect(result.reason.find("text") != std::string::npos, "text rejection should mention text parameters");
Expect(!result.supported, "text parameters without declared fonts should be rejected");
Expect(result.reason.find("unknown font") != std::string::npos, "text rejection should mention the missing font");
}
void SupportsTextParametersWithDeclaredFont()
{
ShaderPackage shaderPackage = MakeSinglePassPackage();
ShaderFontAsset fontAsset;
fontAsset.id = "roboto";
shaderPackage.fontAssets.push_back(fontAsset);
ShaderParameterDefinition parameter;
parameter.id = "caption";
parameter.type = ShaderParameterType::Text;
parameter.fontId = "roboto";
shaderPackage.parameters.push_back(parameter);
const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(result.supported, "text parameters with declared fonts should be supported");
Expect(result.reason.empty(), "supported text parameters should not report a rejection reason");
}
void BuildsDeclaredFontAtlasesDuringCatalogLoad()
@@ -133,6 +153,14 @@ void BuildsDeclaredFontAtlasesDuringCatalogLoad()
std::string error;
Expect(catalog.Load(RepoRoot() / "shaders", 12, error), "shader catalog loads");
bool textOverlaySupported = false;
for (const RenderCadenceCompositor::SupportedShaderSummary& shader : catalog.Shaders())
{
if (shader.id == "text-overlay")
textOverlaySupported = true;
}
Expect(textOverlaySupported, "text overlay is listed as a supported shader after font atlas preparation");
const auto& fontAtlases = catalog.FontAtlases();
const auto textOverlayIt = fontAtlases.find("text-overlay");
Expect(textOverlayIt != fontAtlases.end(), "text overlay font atlas is prepared during catalog load");
@@ -153,7 +181,8 @@ int main()
RejectsUnknownPassInput();
RejectsTemporalPackage();
RejectsTextureAssets();
RejectsTextParameters();
RejectsTextParametersWithoutDeclaredFont();
SupportsTextParametersWithDeclaredFont();
BuildsDeclaredFontAtlasesDuringCatalogLoad();
if (gFailures != 0)