Compare commits
4 Commits
081364e764
...
df0a77ef01
| Author | SHA1 | Date | |
|---|---|---|---|
| df0a77ef01 | |||
| 09efe2d6a0 | |||
| a9eeed30cf | |||
| e43ac21b2f |
@@ -122,6 +122,7 @@ else()
|
|||||||
target_link_libraries(RenderCadenceCompositor PRIVATE
|
target_link_libraries(RenderCadenceCompositor PRIVATE
|
||||||
opengl32
|
opengl32
|
||||||
Ole32
|
Ole32
|
||||||
|
Windowscodecs
|
||||||
Ws2_32
|
Ws2_32
|
||||||
)
|
)
|
||||||
source_group(TREE "${SRC_DIR}" FILES ${RENDER_CADENCE_APP_SOURCES})
|
source_group(TREE "${SRC_DIR}" FILES ${RENDER_CADENCE_APP_SOURCES})
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
"id": "scale",
|
"id": "scale",
|
||||||
"label": "Scale",
|
"label": "Scale",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"default": 0.42,
|
"default": 1,
|
||||||
"min": 0.1,
|
"min": 0.1,
|
||||||
"max": 3,
|
"max": 3,
|
||||||
"step": 0.01,
|
"step": 0.01,
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0.8
|
1
|
||||||
],
|
],
|
||||||
"description": "Text outline color and alpha."
|
"description": "Text outline color and alpha."
|
||||||
},
|
},
|
||||||
@@ -80,10 +80,10 @@
|
|||||||
"id": "outlineWidth",
|
"id": "outlineWidth",
|
||||||
"label": "Outline Width",
|
"label": "Outline Width",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"default": 0.12,
|
"default": 0.22,
|
||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 0.5,
|
"max": 1,
|
||||||
"step": 0.01,
|
"step": 0.02,
|
||||||
"description": "Width of the SDF outline around the text."
|
"description": "Width of the SDF outline around the text."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,11 +16,18 @@ float sdfCoverage(float2 uv, float edge, float aa)
|
|||||||
return smoothstep(edge - aa, edge + aa, distance);
|
return smoothstep(edge - aa, edge + aa, distance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float coverage(float distance, float edge, float aa)
|
||||||
|
{
|
||||||
|
return smoothstep(edge - aa, edge + aa, distance);
|
||||||
|
}
|
||||||
|
|
||||||
float4 shadeVideo(ShaderContext context)
|
float4 shadeVideo(ShaderContext context)
|
||||||
{
|
{
|
||||||
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||||
float aspect = resolution.x / resolution.y;
|
float aspect = resolution.x / resolution.y;
|
||||||
float2 textSize = float2(0.72 * scale, 0.09 * scale * aspect);
|
float textHeight = 0.09 * scale * aspect;
|
||||||
|
float textWidth = textHeight * max(titleTextTextureAspect, 0.01) / aspect;
|
||||||
|
float2 textSize = float2(textWidth, textHeight);
|
||||||
float2 safeTextSize = max(textSize, float2(0.0001, 0.0001));
|
float2 safeTextSize = max(textSize, float2(0.0001, 0.0001));
|
||||||
float2 textUv = (context.uv - position) / safeTextSize;
|
float2 textUv = (context.uv - position) / safeTextSize;
|
||||||
bool insideTextRect = textUv.x >= 0.0 && textUv.x <= 1.0 && textUv.y >= 0.0 && textUv.y <= 1.0;
|
bool insideTextRect = textUv.x >= 0.0 && textUv.x <= 1.0 && textUv.y >= 0.0 && textUv.y <= 1.0;
|
||||||
@@ -30,20 +37,15 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float aa = max(fwidth(distance) * (1.75 + softness * 5.0), 0.0025);
|
float aa = max(fwidth(distance) * (1.75 + softness * 5.0), 0.0025);
|
||||||
float2 pixelTextUv = (1.0 / resolution) / safeTextSize;
|
float2 pixelTextUv = (1.0 / resolution) / safeTextSize;
|
||||||
float2 sampleOffset = pixelTextUv * 0.38;
|
float2 sampleOffset = pixelTextUv * 0.38;
|
||||||
|
float msdfDistance = sampleTitleTextMsdf(textUv);
|
||||||
float fill = (
|
float fill = (
|
||||||
sdfCoverage(textUv, edge, aa) * 2.0 +
|
coverage(msdfDistance, edge, aa) * 2.0 +
|
||||||
sdfCoverage(textUv + float2(sampleOffset.x, sampleOffset.y), edge, aa) +
|
coverage(sampleTitleTextMsdf(textUv + float2(sampleOffset.x, sampleOffset.y)), edge, aa) +
|
||||||
sdfCoverage(textUv + float2(-sampleOffset.x, sampleOffset.y), edge, aa) +
|
coverage(sampleTitleTextMsdf(textUv + float2(-sampleOffset.x, sampleOffset.y)), edge, aa) +
|
||||||
sdfCoverage(textUv + float2(sampleOffset.x, -sampleOffset.y), edge, aa) +
|
coverage(sampleTitleTextMsdf(textUv + float2(sampleOffset.x, -sampleOffset.y)), edge, aa) +
|
||||||
sdfCoverage(textUv + float2(-sampleOffset.x, -sampleOffset.y), edge, aa)) / 6.0;
|
coverage(sampleTitleTextMsdf(textUv + float2(-sampleOffset.x, -sampleOffset.y)), edge, aa)) / 6.0;
|
||||||
float outlineDistance = outlineWidth * 0.16;
|
float outlineEdge = edge - min(outlineWidth * 0.7, 0.48);
|
||||||
float outlineEdge = edge - outlineDistance;
|
float outline = coverage(distance, outlineEdge, aa);
|
||||||
float outline = (
|
|
||||||
sdfCoverage(textUv, outlineEdge, aa) * 2.0 +
|
|
||||||
sdfCoverage(textUv + float2(sampleOffset.x, sampleOffset.y), outlineEdge, aa) +
|
|
||||||
sdfCoverage(textUv + float2(-sampleOffset.x, sampleOffset.y), outlineEdge, aa) +
|
|
||||||
sdfCoverage(textUv + float2(sampleOffset.x, -sampleOffset.y), outlineEdge, aa) +
|
|
||||||
sdfCoverage(textUv + float2(-sampleOffset.x, -sampleOffset.y), outlineEdge, aa)) / 6.0;
|
|
||||||
float outlineAlpha = saturate(outline - fill) * outlineColor.a;
|
float outlineAlpha = saturate(outline - fill) * outlineColor.a;
|
||||||
float fillAlpha = fill * fillColor.a;
|
float fillAlpha = fill * fillColor.a;
|
||||||
float textAlpha = max(fillAlpha, outlineAlpha);
|
float textAlpha = max(fillAlpha, outlineAlpha);
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ Intentionally not included yet:
|
|||||||
- additional input format conversion/scaling
|
- additional input format conversion/scaling
|
||||||
- temporal/history/feedback shader storage
|
- temporal/history/feedback shader storage
|
||||||
- texture/LUT asset upload
|
- texture/LUT asset upload
|
||||||
- text-parameter rasterization and font atlas GL binding
|
|
||||||
- runtime state
|
- runtime state
|
||||||
- OSC control
|
- OSC control
|
||||||
- persistent control/state writes
|
- persistent control/state writes
|
||||||
@@ -142,7 +141,7 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
|
|||||||
- [ ] Texture asset loading and upload
|
- [ ] Texture asset loading and upload
|
||||||
- [ ] LUT asset loading and upload
|
- [ ] LUT asset loading and upload
|
||||||
- [x] CPU-side MSDF/MTSDF font atlas generation cache
|
- [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
|
- [ ] Trigger history/event buffers for overlapping repeated trigger effects
|
||||||
- [ ] Full runtime state store/read model
|
- [ ] Full runtime state store/read model
|
||||||
- [ ] Persistent layer stack/config writes
|
- [ ] 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 temporal history
|
||||||
- no feedback storage
|
- no feedback storage
|
||||||
- no texture/LUT assets yet
|
- no texture/LUT assets yet
|
||||||
- font atlas generation is CPU-side only; text parameter atlas upload/binding is not render-ready yet
|
- text parameters are single-line ASCII masks backed by prepared MTSDF font atlases
|
||||||
- no text parameters yet
|
|
||||||
- manifest defaults initialize parameters
|
- manifest defaults initialize parameters
|
||||||
- HTTP controls can update runtime parameter values without rebuilding GL programs when the shader program is unchanged
|
- 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
|
- 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`.
|
- 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`.
|
- 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,8 @@
|
|||||||
#define GL_UNIFORM_BUFFER 0x8A11
|
#define GL_UNIFORM_BUFFER 0x8A11
|
||||||
#define GL_RGBA8 0x8058
|
#define GL_RGBA8 0x8058
|
||||||
#define GL_RGBA16F 0x881A
|
#define GL_RGBA16F 0x881A
|
||||||
|
#define GL_RED 0x1903
|
||||||
|
#define GL_R8 0x8229
|
||||||
#define GL_TEXTURE0 0x84C0
|
#define GL_TEXTURE0 0x84C0
|
||||||
#define GL_ACTIVE_TEXTURE 0x84E0
|
#define GL_ACTIVE_TEXTURE 0x84E0
|
||||||
#define GL_ARRAY_BUFFER 0x8892
|
#define GL_ARRAY_BUFFER 0x8892
|
||||||
|
|||||||
@@ -344,7 +344,8 @@ void RenderThread::TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRender
|
|||||||
std::string commitError;
|
std::string commitError;
|
||||||
if (TryTakePendingRenderLayers(layers))
|
if (TryTakePendingRenderLayers(layers))
|
||||||
{
|
{
|
||||||
if (!runtimeRenderScene.CommitRenderLayers(layers, commitError))
|
bool structuralChange = false;
|
||||||
|
if (!runtimeRenderScene.CommitRenderLayers(layers, commitError, &structuralChange))
|
||||||
{
|
{
|
||||||
RenderCadenceCompositor::TryLog(
|
RenderCadenceCompositor::TryLog(
|
||||||
RenderCadenceCompositor::LogLevel::Error,
|
RenderCadenceCompositor::LogLevel::Error,
|
||||||
@@ -354,11 +355,14 @@ void RenderThread::TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRender
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderCadenceCompositor::TryLog(
|
if (structuralChange)
|
||||||
RenderCadenceCompositor::LogLevel::Log,
|
{
|
||||||
"render-thread",
|
RenderCadenceCompositor::TryLog(
|
||||||
"Runtime render layer snapshot committed.");
|
RenderCadenceCompositor::LogLevel::Log,
|
||||||
mShaderBuildsCommitted.fetch_add(1, std::memory_order_relaxed);
|
"render-thread",
|
||||||
|
"Runtime render layer snapshot committed.");
|
||||||
|
mShaderBuildsCommitted.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ bool RuntimeRenderScene::StartPrepareWorker(std::unique_ptr<HiddenGlWindow> shar
|
|||||||
return mPrepareWorker.Start(std::move(sharedWindow), error);
|
return mPrepareWorker.Start(std::move(sharedWindow), error);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error)
|
bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error, bool* structuralChange)
|
||||||
{
|
{
|
||||||
ConsumePreparedPrograms();
|
ConsumePreparedPrograms();
|
||||||
|
bool changedStructure = false;
|
||||||
|
|
||||||
std::vector<std::string> nextOrder;
|
std::vector<std::string> nextOrder;
|
||||||
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> layersToPrepare;
|
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> layersToPrepare;
|
||||||
@@ -49,6 +50,7 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
|
|||||||
}
|
}
|
||||||
ReleasePendingPrograms(*layerIt);
|
ReleasePendingPrograms(*layerIt);
|
||||||
layerIt = mLayers.erase(layerIt);
|
layerIt = mLayers.erase(layerIt);
|
||||||
|
changedStructure = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||||
@@ -66,6 +68,7 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
|
|||||||
next.layerId = layer.id;
|
next.layerId = layer.id;
|
||||||
mLayers.push_back(std::move(next));
|
mLayers.push_back(std::move(next));
|
||||||
program = &mLayers.back();
|
program = &mLayers.back();
|
||||||
|
changedStructure = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasReadyPass = false;
|
bool hasReadyPass = false;
|
||||||
@@ -93,11 +96,16 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
|
|||||||
program->shaderId = layer.shaderId;
|
program->shaderId = layer.shaderId;
|
||||||
program->pendingFingerprint = fingerprint;
|
program->pendingFingerprint = fingerprint;
|
||||||
layersToPrepare.push_back(layer);
|
layersToPrepare.push_back(layer);
|
||||||
|
changedStructure = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mLayerOrder != nextOrder)
|
||||||
|
changedStructure = true;
|
||||||
mLayerOrder = std::move(nextOrder);
|
mLayerOrder = std::move(nextOrder);
|
||||||
if (!layersToPrepare.empty())
|
if (!layersToPrepare.empty())
|
||||||
mPrepareWorker.Submit(layersToPrepare);
|
mPrepareWorker.Submit(layersToPrepare);
|
||||||
|
if (structuralChange)
|
||||||
|
*structuralChange = changedStructure;
|
||||||
error.clear();
|
error.clear();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public:
|
|||||||
~RuntimeRenderScene();
|
~RuntimeRenderScene();
|
||||||
|
|
||||||
bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
|
bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
|
||||||
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
|
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error, bool* structuralChange = nullptr);
|
||||||
bool HasLayers();
|
bool HasLayers();
|
||||||
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture = 0);
|
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture = 0);
|
||||||
void ShutdownGl();
|
void ShutdownGl();
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ GLuint RuntimeRenderScene::RenderLayer(
|
|||||||
if (!pass.renderer || !pass.renderer->HasProgram())
|
if (!pass.renderer || !pass.renderer->HasProgram())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
GLuint sourceTexture = videoInputTexture;
|
GLuint sourceTexture = layerInputTexture;
|
||||||
if (!pass.inputNames.empty())
|
if (!pass.inputNames.empty())
|
||||||
{
|
{
|
||||||
const std::string& inputName = pass.inputNames.front();
|
const std::string& inputName = pass.inputNames.front();
|
||||||
|
|||||||
@@ -110,7 +110,15 @@ std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
|
|||||||
AppendStd140Int(buffer, EnumIndexForDefault(definition, value));
|
AppendStd140Int(buffer, EnumIndexForDefault(definition, value));
|
||||||
break;
|
break;
|
||||||
case ShaderParameterType::Text:
|
case ShaderParameterType::Text:
|
||||||
|
{
|
||||||
|
const auto metricsIt = artifact.textTextureMetrics.find(definition.id);
|
||||||
|
const RuntimeTextTextureMetrics metrics = metricsIt == artifact.textTextureMetrics.end()
|
||||||
|
? RuntimeTextTextureMetrics()
|
||||||
|
: metricsIt->second;
|
||||||
|
AppendStd140Float(buffer, metrics.activeWidthScale);
|
||||||
|
AppendStd140Float(buffer, metrics.aspect);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case ShaderParameterType::Trigger:
|
case ShaderParameterType::Trigger:
|
||||||
AppendStd140Int(buffer, value.numberValues.empty() ? 0 : static_cast<int>(value.numberValues[0]));
|
AppendStd140Int(buffer, value.numberValues.empty() ? 0 : static_cast<int>(value.numberValues[0]));
|
||||||
AppendStd140Float(buffer, value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : -1000000.0f);
|
AppendStd140Float(buffer, value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : -1000000.0f);
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram&
|
|||||||
preparedProgram.program = 0;
|
preparedProgram.program = 0;
|
||||||
preparedProgram.vertexShader = 0;
|
preparedProgram.vertexShader = 0;
|
||||||
preparedProgram.fragmentShader = 0;
|
preparedProgram.fragmentShader = 0;
|
||||||
|
if (!mTextTextures.Configure(mArtifact, error))
|
||||||
|
{
|
||||||
|
DestroyProgram();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mTextTextures.RefreshTextTextures(&mArtifact);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +70,8 @@ void RuntimeShaderRenderer::UpdateArtifactState(const RuntimeShaderArtifact& art
|
|||||||
mArtifact.parameterDefinitions = artifact.parameterDefinitions;
|
mArtifact.parameterDefinitions = artifact.parameterDefinitions;
|
||||||
mArtifact.parameterValues = artifact.parameterValues;
|
mArtifact.parameterValues = artifact.parameterValues;
|
||||||
mArtifact.message = artifact.message;
|
mArtifact.message = artifact.message;
|
||||||
|
mTextTextures.UpdateArtifactState(artifact);
|
||||||
|
mTextTextures.RefreshTextTextures(&mArtifact);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeShaderRenderer::BuildPreparedProgram(
|
bool RuntimeShaderRenderer::BuildPreparedProgram(
|
||||||
@@ -117,6 +125,7 @@ bool RuntimeShaderRenderer::BuildPreparedPassProgram(
|
|||||||
|
|
||||||
preparedProgram.succeeded = true;
|
preparedProgram.succeeded = true;
|
||||||
AssignSamplerUniforms(preparedProgram.program);
|
AssignSamplerUniforms(preparedProgram.program);
|
||||||
|
RuntimeTextTextureCache::AssignSamplerUniforms(preparedProgram.program, artifact);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +140,7 @@ void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, uns
|
|||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
UpdateGlobalParams(frameIndex, width, height);
|
UpdateGlobalParams(frameIndex, width, height);
|
||||||
BindRuntimeTextures(sourceTexture, layerInputTexture);
|
BindRuntimeTextures(sourceTexture, layerInputTexture);
|
||||||
|
mTextTextures.BindTextTextures(mProgram);
|
||||||
glBindVertexArray(mVertexArray);
|
glBindVertexArray(mVertexArray);
|
||||||
glUseProgram(mProgram);
|
glUseProgram(mProgram);
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
@@ -300,6 +310,7 @@ void RuntimeShaderRenderer::DestroyProgram()
|
|||||||
glDeleteShader(mVertexShader);
|
glDeleteShader(mVertexShader);
|
||||||
if (mFragmentShader != 0)
|
if (mFragmentShader != 0)
|
||||||
glDeleteShader(mFragmentShader);
|
glDeleteShader(mFragmentShader);
|
||||||
|
mTextTextures.ShutdownGl();
|
||||||
mProgram = 0;
|
mProgram = 0;
|
||||||
mVertexShader = 0;
|
mVertexShader = 0;
|
||||||
mFragmentShader = 0;
|
mFragmentShader = 0;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "GLExtensions.h"
|
#include "GLExtensions.h"
|
||||||
#include "RuntimeShaderProgram.h"
|
#include "RuntimeShaderProgram.h"
|
||||||
|
#include "RuntimeTextTextureCache.h"
|
||||||
#include "../../runtime/RuntimeShaderArtifact.h"
|
#include "../../runtime/RuntimeShaderArtifact.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -45,6 +46,7 @@ private:
|
|||||||
void DestroyStaticGlResources();
|
void DestroyStaticGlResources();
|
||||||
|
|
||||||
RuntimeShaderArtifact mArtifact;
|
RuntimeShaderArtifact mArtifact;
|
||||||
|
RuntimeTextTextureCache mTextTextures;
|
||||||
GLuint mProgram = 0;
|
GLuint mProgram = 0;
|
||||||
GLuint mVertexShader = 0;
|
GLuint mVertexShader = 0;
|
||||||
GLuint mFragmentShader = 0;
|
GLuint mFragmentShader = 0;
|
||||||
|
|||||||
505
src/render/runtime/RuntimeTextTextureCache.cpp
Normal file
505
src/render/runtime/RuntimeTextTextureCache.cpp
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
#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 = 256;
|
||||||
|
constexpr unsigned kTextTexturePadding = 16;
|
||||||
|
constexpr double kFontPixelsPerEm = 192.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;
|
||||||
|
texture.maxLength = definition.maxLength == 0 ? 64 : definition.maxLength;
|
||||||
|
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::RefreshTextTextures(RuntimeShaderArtifact* artifactState)
|
||||||
|
{
|
||||||
|
if (artifactState)
|
||||||
|
artifactState->textTextureMetrics.clear();
|
||||||
|
for (TextTexture& textTexture : mTextTextures)
|
||||||
|
{
|
||||||
|
EnsureTextTexture(textTexture);
|
||||||
|
if (artifactState)
|
||||||
|
{
|
||||||
|
RuntimeTextTextureMetrics metrics;
|
||||||
|
metrics.activeWidthScale = textTexture.width == 0
|
||||||
|
? 1.0f
|
||||||
|
: static_cast<float>(textTexture.liveWidth) / static_cast<float>(textTexture.width);
|
||||||
|
metrics.aspect = textTexture.height == 0
|
||||||
|
? 1.0f
|
||||||
|
: static_cast<float>(textTexture.liveWidth) / static_cast<float>(textTexture.height);
|
||||||
|
artifactState->textTextureMetrics[textTexture.parameterId] = metrics;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeTextTextureCache::BindTextTextures(GLuint program)
|
||||||
|
{
|
||||||
|
for (std::size_t index = 0; index < mTextTextures.size(); ++index)
|
||||||
|
{
|
||||||
|
const TextTexture& textTexture = mTextTextures[index];
|
||||||
|
if (textTexture.texture == 0)
|
||||||
|
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;
|
||||||
|
unsigned liveWidth = 1;
|
||||||
|
std::vector<unsigned char> pixels = ComposeTextTexture(*atlas, texture, text, width, height, liveWidth);
|
||||||
|
if (pixels.empty() || width == 0 || height == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (texture.texture == 0)
|
||||||
|
glGenTextures(1, &texture.texture);
|
||||||
|
|
||||||
|
GLint previousUnpackAlignment = 4;
|
||||||
|
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previousUnpackAlignment);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture.texture);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
if (texture.width != width || texture.height != height)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast<GLsizei>(width), static_cast<GLsizei>(height), 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height), GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
}
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, previousUnpackAlignment);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
|
||||||
|
texture.cachedText = text;
|
||||||
|
texture.width = width;
|
||||||
|
texture.height = height;
|
||||||
|
texture.liveWidth = liveWidth;
|
||||||
|
return texture.texture != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> RuntimeTextTextureCache::ComposeTextTexture(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned fixedWidth = static_cast<unsigned>(std::ceil(static_cast<double>(texture.maxLength) * kFontPixelsPerEm * 0.9)) + kTextTexturePadding * 2u;
|
||||||
|
liveWidth = (std::max)(1u, static_cast<unsigned>(std::ceil(advance * kFontPixelsPerEm)) + kTextTexturePadding * 2u);
|
||||||
|
width = (std::max)(fixedWidth, liveWidth);
|
||||||
|
height = kTextTextureHeight;
|
||||||
|
std::vector<unsigned char> texturePixels(static_cast<std::size_t>(width) * height * 4u, 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 sample[4] = {};
|
||||||
|
SampleAtlasPixel(atlas, atlasX, atlasY, sample);
|
||||||
|
unsigned char* destination = texturePixels.data() + (static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x)) * 4u;
|
||||||
|
for (unsigned channel = 0; channel < 4u; ++channel)
|
||||||
|
destination[channel] = (std::max)(destination[channel], sample[channel]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
penX += glyph.advance * kFontPixelsPerEm;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (unsigned y = 0; y < height / 2u; ++y)
|
||||||
|
{
|
||||||
|
unsigned char* topRow = texturePixels.data() + static_cast<std::size_t>(y) * width * 4u;
|
||||||
|
unsigned char* bottomRow = texturePixels.data() + static_cast<std::size_t>(height - 1u - y) * width * 4u;
|
||||||
|
for (unsigned x = 0; x < width * 4u; ++x)
|
||||||
|
std::swap(topRow[x], bottomRow[x]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return texturePixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeTextTextureCache::SampleAtlasPixel(const Atlas& 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 clampedY = (std::max)(0.0, (std::min)(static_cast<double>(atlas.height) - 1.0, y));
|
||||||
|
const int x0 = static_cast<int>(std::floor(clampedX));
|
||||||
|
const int y0 = static_cast<int>(std::floor(clampedY));
|
||||||
|
const int x1 = (std::min)(static_cast<int>(atlas.width) - 1, x0 + 1);
|
||||||
|
const int y1 = (std::min)(static_cast<int>(atlas.height) - 1, y0 + 1);
|
||||||
|
const double tx = clampedX - static_cast<double>(x0);
|
||||||
|
const double ty = clampedY - static_cast<double>(y0);
|
||||||
|
|
||||||
|
const auto channelAt = [&atlas](int sx, int sy, unsigned channel) {
|
||||||
|
const std::size_t pixelOffset = (static_cast<std::size_t>(sy) * atlas.width + static_cast<std::size_t>(sx)) * 4u;
|
||||||
|
return static_cast<double>(atlas.rgbaPixels[pixelOffset + channel]);
|
||||||
|
};
|
||||||
|
for (unsigned channel = 0; channel < 4u; ++channel)
|
||||||
|
{
|
||||||
|
const double top = channelAt(x0, y0, channel) * (1.0 - tx) + channelAt(x1, y0, channel) * tx;
|
||||||
|
const double bottom = channelAt(x0, y1, channel) * (1.0 - tx) + channelAt(x1, y1, channel) * tx;
|
||||||
|
const double value = top * (1.0 - ty) + bottom * ty;
|
||||||
|
rgba[channel] = static_cast<unsigned char>((std::max)(0.0, (std::min)(255.0, std::round(value))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
79
src/render/runtime/RuntimeTextTextureCache.h
Normal file
79
src/render/runtime/RuntimeTextTextureCache.h
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#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 RefreshTextTextures(RuntimeShaderArtifact* artifactState = nullptr);
|
||||||
|
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;
|
||||||
|
unsigned liveWidth = 1;
|
||||||
|
unsigned maxLength = 64;
|
||||||
|
};
|
||||||
|
|
||||||
|
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> ComposeTextTexture(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) 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 void SampleAtlasPixel(const Atlas& atlas, double x, double y, unsigned char* rgba);
|
||||||
|
static void DestroyTexture(TextTexture& texture);
|
||||||
|
|
||||||
|
RuntimeShaderArtifact mArtifact;
|
||||||
|
std::vector<Atlas> mAtlases;
|
||||||
|
std::vector<TextTexture> mTextTextures;
|
||||||
|
};
|
||||||
@@ -12,8 +12,8 @@ struct FontAtlasBuildConfig
|
|||||||
{
|
{
|
||||||
std::filesystem::path repoRoot;
|
std::filesystem::path repoRoot;
|
||||||
std::filesystem::path cacheRoot;
|
std::filesystem::path cacheRoot;
|
||||||
double sizePixelsPerEm = 64.0;
|
double sizePixelsPerEm = 128.0;
|
||||||
double pixelRange = 4.0;
|
double pixelRange = 8.0;
|
||||||
std::string atlasType = "mtsdf";
|
std::string atlasType = "mtsdf";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ShaderTypes.h"
|
#include "ShaderTypes.h"
|
||||||
|
#include "FontAtlasBuilder.h"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -14,6 +15,12 @@ struct RuntimeShaderPassArtifact
|
|||||||
std::string outputName;
|
std::string outputName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct RuntimeTextTextureMetrics
|
||||||
|
{
|
||||||
|
float activeWidthScale = 1.0f;
|
||||||
|
float aspect = 1.0f;
|
||||||
|
};
|
||||||
|
|
||||||
struct RuntimeShaderArtifact
|
struct RuntimeShaderArtifact
|
||||||
{
|
{
|
||||||
std::string layerId;
|
std::string layerId;
|
||||||
@@ -24,4 +31,6 @@ struct RuntimeShaderArtifact
|
|||||||
std::string message;
|
std::string message;
|
||||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
|
std::map<std::string, RuntimeTextTextureMetrics> textTextureMetrics;
|
||||||
|
std::vector<RenderCadenceCompositor::FontAtlasBuildOutput> fontAtlases;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -112,6 +112,17 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
|
|||||||
return build;
|
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(
|
ShaderCompiler compiler(
|
||||||
repoRoot,
|
repoRoot,
|
||||||
runtimeBuildDir / (shaderId + ".wrapper.slang"),
|
runtimeBuildDir / (shaderId + ".wrapper.slang"),
|
||||||
@@ -144,6 +155,7 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
|
|||||||
build.artifact.shaderId = shaderPackage.id;
|
build.artifact.shaderId = shaderPackage.id;
|
||||||
build.artifact.displayName = shaderPackage.displayName;
|
build.artifact.displayName = shaderPackage.displayName;
|
||||||
build.artifact.parameterDefinitions = shaderPackage.parameters;
|
build.artifact.parameterDefinitions = shaderPackage.parameters;
|
||||||
|
build.artifact.fontAtlases = std::move(fontAtlasOutputs);
|
||||||
if (!build.artifact.passes.empty())
|
if (!build.artifact.passes.empty())
|
||||||
build.artifact.fragmentShaderSource = build.artifact.passes.front().fragmentShaderSource;
|
build.artifact.fragmentShaderSource = build.artifact.passes.front().fragmentShaderSource;
|
||||||
build.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms.";
|
build.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms.";
|
||||||
|
|||||||
@@ -21,13 +21,25 @@ ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& s
|
|||||||
if (!shaderPackage.textureAssets.empty())
|
if (!shaderPackage.textureAssets.empty())
|
||||||
return { false, "RenderCadenceCompositor does not load shader texture assets yet; texture-backed shaders need a CPU-prepared asset handoff first." };
|
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)
|
for (const ShaderParameterDefinition& parameter : shaderPackage.parameters)
|
||||||
{
|
{
|
||||||
if (parameter.type == ShaderParameterType::Text)
|
if (parameter.type != ShaderParameterType::Text)
|
||||||
return { false, "RenderCadenceCompositor currently skips text parameters because they require per-shader text texture storage." };
|
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;
|
bool writesLayerOutput = false;
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ std::string BuildParameterUniforms(const std::vector<ShaderParameterDefinition>&
|
|||||||
for (const ShaderParameterDefinition& definition : parameters)
|
for (const ShaderParameterDefinition& definition : parameters)
|
||||||
{
|
{
|
||||||
if (definition.type == ShaderParameterType::Text)
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
source << "\tfloat " << definition.id << "TextureActiveWidthScale;\n";
|
||||||
|
source << "\tfloat " << definition.id << "TextureAspect;\n";
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
if (definition.type == ShaderParameterType::Trigger)
|
if (definition.type == ShaderParameterType::Trigger)
|
||||||
{
|
{
|
||||||
source << "\tint " << definition.id << ";\n";
|
source << "\tint " << definition.id << ";\n";
|
||||||
@@ -99,17 +103,36 @@ std::string BuildTextSamplerDeclarations(const std::vector<ShaderParameterDefini
|
|||||||
std::string BuildTextHelpers(const std::vector<ShaderParameterDefinition>& parameters)
|
std::string BuildTextHelpers(const std::vector<ShaderParameterDefinition>& parameters)
|
||||||
{
|
{
|
||||||
std::ostringstream source;
|
std::ostringstream source;
|
||||||
|
bool emittedMedian = false;
|
||||||
for (const ShaderParameterDefinition& definition : parameters)
|
for (const ShaderParameterDefinition& definition : parameters)
|
||||||
{
|
{
|
||||||
if (definition.type != ShaderParameterType::Text)
|
if (definition.type != ShaderParameterType::Text)
|
||||||
continue;
|
continue;
|
||||||
|
if (!emittedMedian)
|
||||||
|
{
|
||||||
|
source
|
||||||
|
<< "float median(float r, float g, float b)\n"
|
||||||
|
<< "{\n"
|
||||||
|
<< "\treturn max(min(r, g), min(max(r, g), b));\n"
|
||||||
|
<< "}\n\n";
|
||||||
|
emittedMedian = true;
|
||||||
|
}
|
||||||
const std::string suffix = CapitalizeIdentifier(definition.id);
|
const std::string suffix = CapitalizeIdentifier(definition.id);
|
||||||
source
|
source
|
||||||
<< "float sample" << suffix << "(float2 uv)\n"
|
<< "float4 sample" << suffix << "Mtsdf(float2 uv)\n"
|
||||||
<< "{\n"
|
<< "{\n"
|
||||||
<< "\tif (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)\n"
|
<< "\tif (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)\n"
|
||||||
<< "\t\treturn 0.0;\n"
|
<< "\t\treturn float4(0.0, 0.0, 0.0, 0.0);\n"
|
||||||
<< "\treturn " << definition.id << "Texture.Sample(uv).r;\n"
|
<< "\treturn " << definition.id << "Texture.Sample(float2(uv.x * " << definition.id << "TextureActiveWidthScale, uv.y));\n"
|
||||||
|
<< "}\n\n"
|
||||||
|
<< "float sample" << suffix << "Msdf(float2 uv)\n"
|
||||||
|
<< "{\n"
|
||||||
|
<< "\tfloat4 mtsdf = sample" << suffix << "Mtsdf(uv);\n"
|
||||||
|
<< "\treturn median(mtsdf.r, mtsdf.g, mtsdf.b);\n"
|
||||||
|
<< "}\n\n"
|
||||||
|
<< "float sample" << suffix << "(float2 uv)\n"
|
||||||
|
<< "{\n"
|
||||||
|
<< "\treturn sample" << suffix << "Mtsdf(uv).a;\n"
|
||||||
<< "}\n\n"
|
<< "}\n\n"
|
||||||
<< "float4 draw" << suffix << "(float2 uv, float4 fillColor)\n"
|
<< "float4 draw" << suffix << "(float2 uv, float4 fillColor)\n"
|
||||||
<< "{\n"
|
<< "{\n"
|
||||||
|
|||||||
@@ -112,19 +112,39 @@ void RejectsTextureAssets()
|
|||||||
Expect(result.reason.find("texture") != std::string::npos, "texture rejection should mention texture assets");
|
Expect(result.reason.find("texture") != std::string::npos, "texture rejection should mention texture assets");
|
||||||
}
|
}
|
||||||
|
|
||||||
void RejectsTextParameters()
|
void RejectsTextParametersWithoutDeclaredFont()
|
||||||
{
|
{
|
||||||
ShaderPackage shaderPackage = MakeSinglePassPackage();
|
ShaderPackage shaderPackage = MakeSinglePassPackage();
|
||||||
ShaderParameterDefinition parameter;
|
ShaderParameterDefinition parameter;
|
||||||
parameter.id = "caption";
|
parameter.id = "caption";
|
||||||
parameter.type = ShaderParameterType::Text;
|
parameter.type = ShaderParameterType::Text;
|
||||||
|
parameter.fontId = "missing";
|
||||||
shaderPackage.parameters.push_back(parameter);
|
shaderPackage.parameters.push_back(parameter);
|
||||||
|
|
||||||
const RenderCadenceCompositor::ShaderSupportResult result =
|
const RenderCadenceCompositor::ShaderSupportResult result =
|
||||||
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||||
|
|
||||||
Expect(!result.supported, "text-parameter packages should be rejected for now");
|
Expect(!result.supported, "text parameters without declared fonts should be rejected");
|
||||||
Expect(result.reason.find("text") != std::string::npos, "text rejection should mention text parameters");
|
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()
|
void BuildsDeclaredFontAtlasesDuringCatalogLoad()
|
||||||
@@ -133,6 +153,14 @@ void BuildsDeclaredFontAtlasesDuringCatalogLoad()
|
|||||||
std::string error;
|
std::string error;
|
||||||
Expect(catalog.Load(RepoRoot() / "shaders", 12, error), "shader catalog loads");
|
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& fontAtlases = catalog.FontAtlases();
|
||||||
const auto textOverlayIt = fontAtlases.find("text-overlay");
|
const auto textOverlayIt = fontAtlases.find("text-overlay");
|
||||||
Expect(textOverlayIt != fontAtlases.end(), "text overlay font atlas is prepared during catalog load");
|
Expect(textOverlayIt != fontAtlases.end(), "text overlay font atlas is prepared during catalog load");
|
||||||
@@ -153,7 +181,8 @@ int main()
|
|||||||
RejectsUnknownPassInput();
|
RejectsUnknownPassInput();
|
||||||
RejectsTemporalPackage();
|
RejectsTemporalPackage();
|
||||||
RejectsTextureAssets();
|
RejectsTextureAssets();
|
||||||
RejectsTextParameters();
|
RejectsTextParametersWithoutDeclaredFont();
|
||||||
|
SupportsTextParametersWithDeclaredFont();
|
||||||
BuildsDeclaredFontAtlasesDuringCatalogLoad();
|
BuildsDeclaredFontAtlasesDuringCatalogLoad();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
|||||||
placeholder={parameter.defaultValue ? `Default: ${parameter.defaultValue}` : ""}
|
placeholder={parameter.defaultValue ? `Default: ${parameter.defaultValue}` : ""}
|
||||||
value={draftValue ?? ""}
|
value={draftValue ?? ""}
|
||||||
onFocus={beginInteraction}
|
onFocus={beginInteraction}
|
||||||
onChange={(event) => sendValue(event.target.value)}
|
onChange={(event) => scheduleSendValue(event.target.value, false, 350)}
|
||||||
onBlur={endInteraction}
|
onBlur={endInteraction}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function useThrottledParameterValue(parameter, onParameterChange) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const scheduleSendValue = (value, immediate = false) => {
|
const scheduleSendValue = (value, immediate = false, throttleMs = 90) => {
|
||||||
setDraftValue(value);
|
setDraftValue(value);
|
||||||
latestDraftRef.current = value;
|
latestDraftRef.current = value;
|
||||||
isDirtyRef.current = true;
|
isDirtyRef.current = true;
|
||||||
@@ -93,7 +93,6 @@ export function useThrottledParameterValue(parameter, onParameterChange) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const throttleMs = 90;
|
|
||||||
const elapsed = now - lastSentAtRef.current;
|
const elapsed = now - lastSentAtRef.current;
|
||||||
|
|
||||||
if (immediate || elapsed >= throttleMs) {
|
if (immediate || elapsed >= throttleMs) {
|
||||||
|
|||||||
Reference in New Issue
Block a user