Font builder
This commit is contained in:
@@ -11,6 +11,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
|||||||
set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
set(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
set(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||||
set(SLANG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
|
set(SLANG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
|
||||||
|
set(MSDF_ATLAS_GEN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/msdf-atlas-gen" CACHE PATH "Path to msdf-atlas-gen binary release")
|
||||||
|
|
||||||
set(VIDEO_SHADER_INCLUDE_DIRS
|
set(VIDEO_SHADER_INCLUDE_DIRS
|
||||||
"${SRC_DIR}"
|
"${SRC_DIR}"
|
||||||
@@ -61,6 +62,16 @@ set(SLANG_RUNTIME_FILES
|
|||||||
"${SLANG_ROOT}/bin/slang-glslang.dll"
|
"${SLANG_ROOT}/bin/slang-glslang.dll"
|
||||||
)
|
)
|
||||||
set(SLANG_LICENSE_FILE "${SLANG_ROOT}/LICENSE")
|
set(SLANG_LICENSE_FILE "${SLANG_ROOT}/LICENSE")
|
||||||
|
set(MSDF_ATLAS_GEN_EXE_FILE "${MSDF_ATLAS_GEN_ROOT}/msdf-atlas-gen.exe")
|
||||||
|
file(GLOB MSDF_ATLAS_GEN_DLL_FILES CONFIGURE_DEPENDS
|
||||||
|
"${MSDF_ATLAS_GEN_ROOT}/*.dll"
|
||||||
|
)
|
||||||
|
set(MSDF_ATLAS_GEN_RUNTIME_FILES
|
||||||
|
"${MSDF_ATLAS_GEN_EXE_FILE}"
|
||||||
|
${MSDF_ATLAS_GEN_DLL_FILES}
|
||||||
|
)
|
||||||
|
set(MSDF_ATLAS_GEN_LICENSE_FILE "${MSDF_ATLAS_GEN_ROOT}/LICENSE.txt")
|
||||||
|
set(MSDF_ATLAS_GEN_README_FILE "${MSDF_ATLAS_GEN_ROOT}/README.md")
|
||||||
|
|
||||||
set(RENDER_CADENCE_APP_REQUIRED_FILES
|
set(RENDER_CADENCE_APP_REQUIRED_FILES
|
||||||
"${SRC_DIR}/RenderCadenceCompositor.cpp"
|
"${SRC_DIR}/RenderCadenceCompositor.cpp"
|
||||||
@@ -136,6 +147,34 @@ foreach(slang_runtime_file IN LISTS SLANG_RUNTIME_FILES)
|
|||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
foreach(msdf_runtime_file IN LISTS MSDF_ATLAS_GEN_RUNTIME_FILES)
|
||||||
|
if(EXISTS "${msdf_runtime_file}")
|
||||||
|
install(FILES "${msdf_runtime_file}"
|
||||||
|
DESTINATION "3rdParty/msdf-atlas-gen"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "msdf-atlas-gen runtime file not found and will not be installed: ${msdf_runtime_file}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if(EXISTS "${MSDF_ATLAS_GEN_LICENSE_FILE}")
|
||||||
|
install(FILES "${MSDF_ATLAS_GEN_LICENSE_FILE}"
|
||||||
|
DESTINATION "third_party_notices"
|
||||||
|
RENAME "MSDF_ATLAS_GEN_LICENSE.txt"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "msdf-atlas-gen license file not found: ${MSDF_ATLAS_GEN_LICENSE_FILE}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(EXISTS "${MSDF_ATLAS_GEN_README_FILE}")
|
||||||
|
install(FILES "${MSDF_ATLAS_GEN_README_FILE}"
|
||||||
|
DESTINATION "third_party_notices"
|
||||||
|
RENAME "MSDF_ATLAS_GEN_README.md"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "msdf-atlas-gen readme file not found: ${MSDF_ATLAS_GEN_README_FILE}")
|
||||||
|
endif()
|
||||||
|
|
||||||
if(EXISTS "${SLANG_LICENSE_FILE}")
|
if(EXISTS "${SLANG_LICENSE_FILE}")
|
||||||
install(FILES "${SLANG_LICENSE_FILE}"
|
install(FILES "${SLANG_LICENSE_FILE}"
|
||||||
DESTINATION "third_party_notices"
|
DESTINATION "third_party_notices"
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -6,7 +6,7 @@ The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime
|
|||||||
|
|
||||||
## Repository Layout
|
## Repository Layout
|
||||||
|
|
||||||
- `apps/LoopThroughWithOpenGLCompositing/`: native C++ host app.
|
- `src/`: native C++ host app.
|
||||||
- `shaders/`: shader packages, each with `shader.json` and `shader.slang`.
|
- `shaders/`: shader packages, each with `shader.json` and `shader.slang`.
|
||||||
- `ui/`: Vite/React control UI.
|
- `ui/`: Vite/React control UI.
|
||||||
- `config/runtime-host.json`: runtime configuration.
|
- `config/runtime-host.json`: runtime configuration.
|
||||||
@@ -30,6 +30,7 @@ Native app internals are grouped by boundary:
|
|||||||
- Node.js and npm for the control UI.
|
- Node.js and npm for the control UI.
|
||||||
- Blackmagic Desktop Video drivers and a DeckLink device for the current production video I/O backend.
|
- Blackmagic Desktop Video drivers and a DeckLink device for the current production video I/O backend.
|
||||||
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
|
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
|
||||||
|
- `msdf-atlas-gen` Windows binary release with `msdf-atlas-gen.exe`, `LICENSE.txt`, and any adjacent runtime DLLs for font atlas generation.
|
||||||
|
|
||||||
Default expected Slang path:
|
Default expected Slang path:
|
||||||
|
|
||||||
@@ -37,6 +38,12 @@ Default expected Slang path:
|
|||||||
3rdParty/slang-2026.8-windows-x86_64
|
3rdParty/slang-2026.8-windows-x86_64
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Default expected `msdf-atlas-gen` path:
|
||||||
|
|
||||||
|
```text
|
||||||
|
3rdParty/msdf-atlas-gen
|
||||||
|
```
|
||||||
|
|
||||||
Override example:
|
Override example:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -88,10 +95,11 @@ The package folder will contain:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
dist/VideoShader/
|
dist/VideoShader/
|
||||||
LoopThroughWithOpenGLCompositing.exe
|
RenderCadenceCompositor.exe
|
||||||
config/
|
config/
|
||||||
shaders/
|
shaders/
|
||||||
3rdParty/slang/bin/
|
3rdParty/slang/bin/
|
||||||
|
3rdParty/msdf-atlas-gen/
|
||||||
ui/dist/
|
ui/dist/
|
||||||
docs/
|
docs/
|
||||||
SHADER_CONTRACT.md
|
SHADER_CONTRACT.md
|
||||||
@@ -99,9 +107,9 @@ dist/VideoShader/
|
|||||||
third_party_notices/
|
third_party_notices/
|
||||||
```
|
```
|
||||||
|
|
||||||
You can run `LoopThroughWithOpenGLCompositing.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `3rdParty/slang/bin/slangc.exe`, `ui/dist/`, and `runtime/templates/` relative to the exe folder. In development mode, it still falls back to repo-root discovery.
|
You can run `RenderCadenceCompositor.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `3rdParty/slang/bin/slangc.exe`, `3rdParty/msdf-atlas-gen/msdf-atlas-gen.exe`, `ui/dist/`, and `runtime/templates/` relative to the exe folder. In development mode, it still falls back to repo-root discovery.
|
||||||
|
|
||||||
The install step copies only the Slang runtime files required by the shader compiler (`slangc.exe`, `slang-compiler.dll`, and `slang-glslang.dll`) plus `third_party_notices/SLANG_LICENSE.txt`. It does not copy the full Slang release folder.
|
The install step copies only the Slang runtime files required by the shader compiler (`slangc.exe`, `slang-compiler.dll`, and `slang-glslang.dll`) plus `third_party_notices/SLANG_LICENSE.txt`. It also copies `msdf-atlas-gen.exe`, any adjacent `msdf-atlas-gen` DLLs, and the `third_party_notices/MSDF_ATLAS_GEN_LICENSE.txt` and `third_party_notices/MSDF_ATLAS_GEN_README.md` notice files. It does not copy full third-party release folders.
|
||||||
|
|
||||||
Create a zip for distribution:
|
Create a zip for distribution:
|
||||||
|
|
||||||
@@ -250,6 +258,7 @@ The Gitea workflow expects two act runners:
|
|||||||
The Windows jobs validate native third-party dependencies before configuring CMake. Because `3rdParty/` is ignored, configure this path on the runner or in a Gitea repository variable:
|
The Windows jobs validate native third-party dependencies before configuring CMake. Because `3rdParty/` is ignored, configure this path on the runner or in a Gitea repository variable:
|
||||||
|
|
||||||
- `SLANG_ROOT`: path to the Slang binary release folder containing `bin/slangc.exe`.
|
- `SLANG_ROOT`: path to the Slang binary release folder containing `bin/slangc.exe`.
|
||||||
|
- `MSDF_ATLAS_GEN_ROOT`: path to the `msdf-atlas-gen` binary release folder containing `msdf-atlas-gen.exe`.
|
||||||
|
|
||||||
The Windows runner also needs the Visual Studio ATL component installed. In Visual Studio Build Tools 2022, add `C++ ATL for latest v143 build tools (x86 & x64)`, component ID `Microsoft.VisualStudio.Component.VC.ATL`.
|
The Windows runner also needs the Visual Studio ATL component installed. In Visual Studio Build Tools 2022, add `C++ ATL for latest v143 build tools (x86 & x64)`, component ID `Microsoft.VisualStudio.Component.VC.ATL`.
|
||||||
|
|
||||||
@@ -257,9 +266,10 @@ Example runner paths:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
D:\SDKs\slang-2026.8-windows-x86_64
|
D:\SDKs\slang-2026.8-windows-x86_64
|
||||||
|
D:\SDKs\msdf-atlas-gen
|
||||||
```
|
```
|
||||||
|
|
||||||
If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default under `3rdParty/`.
|
If `SLANG_ROOT` or `MSDF_ATLAS_GEN_ROOT` is not set, the workflow falls back to the repo-local defaults under `3rdParty/`.
|
||||||
|
|
||||||
## Still Todo
|
## Still Todo
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ float grainScalar(float2 uv)
|
|||||||
return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
|
return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float4 sampleVhsFrame(float2 uv)
|
||||||
|
{
|
||||||
|
return sampleVideo(saturate(uv));
|
||||||
|
}
|
||||||
|
|
||||||
float3 animatedChromaGrain(float2 uv, float time, float2 outputResolution, float grainSize)
|
float3 animatedChromaGrain(float2 uv, float time, float2 outputResolution, float grainSize)
|
||||||
{
|
{
|
||||||
float safeGrainSize = max(grainSize, 0.001);
|
float safeGrainSize = max(grainSize, 0.001);
|
||||||
@@ -142,15 +147,15 @@ float3 softBloom(float2 uv, float2 outputResolution, float radius)
|
|||||||
float2 dx = float2(pixel.x * radius, 0.0);
|
float2 dx = float2(pixel.x * radius, 0.0);
|
||||||
float2 dy = float2(0.0, pixel.y * radius);
|
float2 dy = float2(0.0, pixel.y * radius);
|
||||||
|
|
||||||
float3 sum = sampleVideo(frac(uv)).rgb * 0.28;
|
float3 sum = sampleVhsFrame(uv).rgb * 0.28;
|
||||||
sum += sampleVideo(frac(uv + dx)).rgb * 0.14;
|
sum += sampleVhsFrame(uv + dx).rgb * 0.14;
|
||||||
sum += sampleVideo(frac(uv - dx)).rgb * 0.14;
|
sum += sampleVhsFrame(uv - dx).rgb * 0.14;
|
||||||
sum += sampleVideo(frac(uv + dy)).rgb * 0.14;
|
sum += sampleVhsFrame(uv + dy).rgb * 0.14;
|
||||||
sum += sampleVideo(frac(uv - dy)).rgb * 0.14;
|
sum += sampleVhsFrame(uv - dy).rgb * 0.14;
|
||||||
sum += sampleVideo(frac(uv + dx + dy)).rgb * 0.075;
|
sum += sampleVhsFrame(uv + dx + dy).rgb * 0.075;
|
||||||
sum += sampleVideo(frac(uv + dx - dy)).rgb * 0.075;
|
sum += sampleVhsFrame(uv + dx - dy).rgb * 0.075;
|
||||||
sum += sampleVideo(frac(uv - dx + dy)).rgb * 0.075;
|
sum += sampleVhsFrame(uv - dx + dy).rgb * 0.075;
|
||||||
sum += sampleVideo(frac(uv - dx - dy)).rgb * 0.075;
|
sum += sampleVhsFrame(uv - dx - dy).rgb * 0.075;
|
||||||
return sum;
|
return sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,11 +163,11 @@ float3 softCrossBlur(float2 uv, float2 outputResolution, float radius)
|
|||||||
{
|
{
|
||||||
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
|
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
|
||||||
float2 offset = pixel * radius;
|
float2 offset = pixel * radius;
|
||||||
float3 sum = sampleVideo(frac(uv)).rgb * 0.40;
|
float3 sum = sampleVhsFrame(uv).rgb * 0.40;
|
||||||
sum += sampleVideo(frac(uv + float2(offset.x, 0.0))).rgb * 0.15;
|
sum += sampleVhsFrame(uv + float2(offset.x, 0.0)).rgb * 0.15;
|
||||||
sum += sampleVideo(frac(uv - float2(offset.x, 0.0))).rgb * 0.15;
|
sum += sampleVhsFrame(uv - float2(offset.x, 0.0)).rgb * 0.15;
|
||||||
sum += sampleVideo(frac(uv + float2(0.0, offset.y))).rgb * 0.15;
|
sum += sampleVhsFrame(uv + float2(0.0, offset.y)).rgb * 0.15;
|
||||||
sum += sampleVideo(frac(uv - float2(0.0, offset.y))).rgb * 0.15;
|
sum += sampleVhsFrame(uv - float2(0.0, offset.y)).rgb * 0.15;
|
||||||
return sum;
|
return sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,14 +179,14 @@ float3 applyChromaCrawl(float3 color, float2 uv, float time, float2 outputResolu
|
|||||||
|
|
||||||
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
|
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
|
||||||
float lumaCenter = dot(color, float3(0.299, 0.587, 0.114));
|
float lumaCenter = dot(color, float3(0.299, 0.587, 0.114));
|
||||||
float lumaX = dot(sampleVideo(frac(uv + float2(pixel.x, 0.0))).rgb, float3(0.299, 0.587, 0.114));
|
float lumaX = dot(sampleVhsFrame(uv + float2(pixel.x, 0.0)).rgb, float3(0.299, 0.587, 0.114));
|
||||||
float lumaY = dot(sampleVideo(frac(uv + float2(0.0, pixel.y))).rgb, float3(0.299, 0.587, 0.114));
|
float lumaY = dot(sampleVhsFrame(uv + float2(0.0, pixel.y)).rgb, float3(0.299, 0.587, 0.114));
|
||||||
float edge = saturate((abs(lumaX - lumaCenter) + abs(lumaY - lumaCenter)) * 6.0);
|
float edge = saturate((abs(lumaX - lumaCenter) + abs(lumaY - lumaCenter)) * 6.0);
|
||||||
float phase = sin(uv.y * outputResolution.y * 1.35 + time * 36.0) * cos(uv.x * outputResolution.x * 0.55 - time * 21.0);
|
float phase = sin(uv.y * outputResolution.y * 1.35 + time * 36.0) * cos(uv.x * outputResolution.x * 0.55 - time * 21.0);
|
||||||
float2 crawlOffset = float2(phase, -phase * 0.35) * pixel * (1.0 + amount * 8.0);
|
float2 crawlOffset = float2(phase, -phase * 0.35) * pixel * (1.0 + amount * 8.0);
|
||||||
|
|
||||||
float3 shiftedA = sampleVideo(frac(uv + crawlOffset)).rgb;
|
float3 shiftedA = sampleVhsFrame(uv + crawlOffset).rgb;
|
||||||
float3 shiftedB = sampleVideo(frac(uv - crawlOffset * 0.75)).rgb;
|
float3 shiftedB = sampleVhsFrame(uv - crawlOffset * 0.75).rgb;
|
||||||
float3 crawled = color;
|
float3 crawled = color;
|
||||||
crawled.r = lerp(color.r, shiftedA.r, edge * amount);
|
crawled.r = lerp(color.r, shiftedA.r, edge * amount);
|
||||||
crawled.b = lerp(color.b, shiftedB.b, edge * amount);
|
crawled.b = lerp(color.b, shiftedB.b, edge * amount);
|
||||||
@@ -249,7 +254,7 @@ float3 blurVhs(float2 uv, float d, int sampleCount)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
float2 offset = circle(start, float(sampleCount), float(i)) * scale;
|
float2 offset = circle(start, float(sampleCount), float(i)) * scale;
|
||||||
sum += sampleVideo(frac(uv + offset)).rgb * weight;
|
sum += sampleVhsFrame(uv + offset).rgb * weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sum;
|
return sum;
|
||||||
@@ -299,23 +304,23 @@ float4 buildTapeSmear(ShaderContext context)
|
|||||||
float4 finishVhs(ShaderContext context)
|
float4 finishVhs(ShaderContext context)
|
||||||
{
|
{
|
||||||
float time = distortedTapeTime(context);
|
float time = distortedTapeTime(context);
|
||||||
float3 color = sampleVideo(context.uv).rgb;
|
float3 color = sampleVhsFrame(context.uv).rgb;
|
||||||
|
|
||||||
// Radial red/blue offsets create lens and deck misregistration before the
|
// Radial red/blue offsets create lens and deck misregistration before the
|
||||||
// wider tape effects are layered in.
|
// wider tape effects are layered in.
|
||||||
float2 centered = context.uv * 2.0 - 1.0;
|
float2 centered = context.uv * 2.0 - 1.0;
|
||||||
centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0);
|
centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0);
|
||||||
float2 aberrationOffset = centered * (aberrationAmount * 0.0015);
|
float2 aberrationOffset = centered * (aberrationAmount * 0.0015);
|
||||||
float redAberration = sampleVideo(frac(context.uv + aberrationOffset)).r;
|
float redAberration = sampleVhsFrame(context.uv + aberrationOffset).r;
|
||||||
float blueAberration = sampleVideo(frac(context.uv - aberrationOffset)).b;
|
float blueAberration = sampleVhsFrame(context.uv - aberrationOffset).b;
|
||||||
color.r = lerp(color.r, redAberration, 0.35);
|
color.r = lerp(color.r, redAberration, 0.35);
|
||||||
color.b = lerp(color.b, blueAberration, 0.35);
|
color.b = lerp(color.b, blueAberration, 0.35);
|
||||||
|
|
||||||
float2 halationOffset = float2(0.0015, 0.0) * (1.0 + smear * 0.35);
|
float2 halationOffset = float2(0.0015, 0.0) * (1.0 + smear * 0.35);
|
||||||
float3 halationSource =
|
float3 halationSource =
|
||||||
sampleVideo(frac(context.uv + halationOffset)).rgb * 0.4 +
|
sampleVhsFrame(context.uv + halationOffset).rgb * 0.4 +
|
||||||
sampleVideo(frac(context.uv - halationOffset)).rgb * 0.4 +
|
sampleVhsFrame(context.uv - halationOffset).rgb * 0.4 +
|
||||||
sampleVideo(frac(context.uv + halationOffset * 2.0)).rgb * 0.2;
|
sampleVhsFrame(context.uv + halationOffset * 2.0).rgb * 0.2;
|
||||||
float halationLuma = dot(halationSource, float3(0.299, 0.587, 0.114));
|
float halationLuma = dot(halationSource, float3(0.299, 0.587, 0.114));
|
||||||
float halationMask = smoothstep(0.45, 1.0, halationLuma) * halationAmount;
|
float halationMask = smoothstep(0.45, 1.0, halationLuma) * halationAmount;
|
||||||
color += halationSource * float3(1.0, 0.38, 0.24) * halationMask * 0.35;
|
color += halationSource * float3(1.0, 0.38, 0.24) * halationMask * 0.35;
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ 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
|
- text-parameter rasterization and font atlas GL binding
|
||||||
- runtime state
|
- runtime state
|
||||||
- OSC control
|
- OSC control
|
||||||
- persistent control/state writes
|
- persistent control/state writes
|
||||||
@@ -141,6 +141,7 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
|
|||||||
- [ ] Feedback buffers
|
- [ ] Feedback buffers
|
||||||
- [ ] 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
|
||||||
- [ ] Text parameter rasterization
|
- [ ] Text parameter rasterization
|
||||||
- [ ] 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
|
||||||
@@ -352,6 +353,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
|
||||||
- no text parameters yet
|
- 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
|
||||||
|
|||||||
@@ -17,7 +17,14 @@ void RuntimeLayerController::LoadSupportedShaderCatalog(const std::string& shade
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
|
std::size_t preparedFontAtlases = 0;
|
||||||
|
for (const auto& entry : mShaderCatalog.FontAtlases())
|
||||||
|
preparedFontAtlases += entry.second.size();
|
||||||
|
|
||||||
|
Log(
|
||||||
|
"runtime-shader",
|
||||||
|
"Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) +
|
||||||
|
" shader(s), prepared " + std::to_string(preparedFontAtlases) + " font atlas asset(s).");
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)
|
void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)
|
||||||
|
|||||||
@@ -113,7 +113,11 @@ GLuint RuntimeRenderScene::RenderLayer(
|
|||||||
{
|
{
|
||||||
sourceTexture = videoInputTexture;
|
sourceTexture = videoInputTexture;
|
||||||
}
|
}
|
||||||
else if (inputName != "layerInput")
|
else if (inputName == "layerInput")
|
||||||
|
{
|
||||||
|
sourceTexture = layerInputTexture;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
// Named intermediate pass inputs currently use the gVideoInput binding slot as the
|
// Named intermediate pass inputs currently use the gVideoInput binding slot as the
|
||||||
// selected pass source. Layer stack shaders should use gLayerInput for previous-layer
|
// selected pass source. Layer stack shaders should use gLayerInput for previous-layer
|
||||||
|
|||||||
180
src/runtime/FontAtlasBuilder.cpp
Normal file
180
src/runtime/FontAtlasBuilder.cpp
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#include "FontAtlasBuilder.h"
|
||||||
|
|
||||||
|
#include "NativeHandles.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <system_error>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::string QuotePath(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
return "\"" + path.string() + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string NumberText(double value)
|
||||||
|
{
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << value;
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FontAtlasBuilder::FontAtlasBuilder(FontAtlasBuildConfig config) :
|
||||||
|
mConfig(std::move(config))
|
||||||
|
{
|
||||||
|
if (mConfig.cacheRoot.empty())
|
||||||
|
mConfig.cacheRoot = mConfig.repoRoot / "runtime" / "font_cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontAtlasBuilder::BuildPackageFontAtlases(
|
||||||
|
const ShaderPackage& shaderPackage,
|
||||||
|
std::vector<FontAtlasBuildOutput>& outputs,
|
||||||
|
std::string& error) const
|
||||||
|
{
|
||||||
|
outputs.clear();
|
||||||
|
for (const ShaderFontAsset& fontAsset : shaderPackage.fontAssets)
|
||||||
|
{
|
||||||
|
FontAtlasBuildOutput output;
|
||||||
|
if (!BuildFontAtlas(shaderPackage, fontAsset, output, error))
|
||||||
|
return false;
|
||||||
|
outputs.push_back(std::move(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontAtlasBuilder::BuildFontAtlas(
|
||||||
|
const ShaderPackage& shaderPackage,
|
||||||
|
const ShaderFontAsset& fontAsset,
|
||||||
|
FontAtlasBuildOutput& output,
|
||||||
|
std::string& error) const
|
||||||
|
{
|
||||||
|
output = FontAtlasBuildOutput();
|
||||||
|
if (fontAsset.id.empty())
|
||||||
|
{
|
||||||
|
error = "Font asset id is empty for shader package '" + shaderPackage.id + "'.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (fontAsset.path.empty() || !std::filesystem::exists(fontAsset.path))
|
||||||
|
{
|
||||||
|
error = "Font asset '" + fontAsset.id + "' does not exist: " + fontAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path executablePath;
|
||||||
|
if (!FindMsdfAtlasGenExecutable(mConfig.repoRoot, executablePath))
|
||||||
|
{
|
||||||
|
error = "Could not find msdf-atlas-gen.exe under 3rdParty/msdf-atlas-gen.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::filesystem::path packageCache = PackageCacheDirectory(shaderPackage);
|
||||||
|
std::error_code fsError;
|
||||||
|
std::filesystem::create_directories(packageCache, fsError);
|
||||||
|
if (fsError)
|
||||||
|
{
|
||||||
|
error = "Could not create font atlas cache directory: " + packageCache.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string outputStem = SanitizePathToken(shaderPackage.id + "-" + fontAsset.id);
|
||||||
|
output.fontId = fontAsset.id;
|
||||||
|
output.imagePath = packageCache / (outputStem + ".png");
|
||||||
|
output.jsonPath = packageCache / (outputStem + ".json");
|
||||||
|
|
||||||
|
const std::string commandLine =
|
||||||
|
QuotePath(executablePath) +
|
||||||
|
" -font " + QuotePath(fontAsset.path) +
|
||||||
|
" -fontname " + fontAsset.id +
|
||||||
|
" -type " + mConfig.atlasType +
|
||||||
|
" -format png" +
|
||||||
|
" -size " + NumberText(mConfig.sizePixelsPerEm) +
|
||||||
|
" -pxrange " + NumberText(mConfig.pixelRange) +
|
||||||
|
" -yorigin top" +
|
||||||
|
" -imageout " + QuotePath(output.imagePath) +
|
||||||
|
" -json " + QuotePath(output.jsonPath);
|
||||||
|
|
||||||
|
if (!RunProcess(commandLine, mConfig.repoRoot, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(output.imagePath) || !std::filesystem::exists(output.jsonPath) ||
|
||||||
|
std::filesystem::file_size(output.imagePath) == 0 || std::filesystem::file_size(output.jsonPath) == 0)
|
||||||
|
{
|
||||||
|
error = "msdf-atlas-gen did not produce expected atlas outputs for font '" + fontAsset.id + "'.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontAtlasBuilder::FindMsdfAtlasGenExecutable(const std::filesystem::path& repoRoot, std::filesystem::path& executablePath)
|
||||||
|
{
|
||||||
|
const std::filesystem::path expectedPath = repoRoot / "3rdParty" / "msdf-atlas-gen" / "msdf-atlas-gen.exe";
|
||||||
|
if (std::filesystem::exists(expectedPath))
|
||||||
|
{
|
||||||
|
executablePath = expectedPath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path FontAtlasBuilder::PackageCacheDirectory(const ShaderPackage& shaderPackage) const
|
||||||
|
{
|
||||||
|
return mConfig.cacheRoot / SanitizePathToken(shaderPackage.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FontAtlasBuilder::SanitizePathToken(const std::string& value)
|
||||||
|
{
|
||||||
|
std::string sanitized;
|
||||||
|
sanitized.reserve(value.size());
|
||||||
|
for (unsigned char character : value)
|
||||||
|
{
|
||||||
|
if (std::isalnum(character) || character == '-' || character == '_')
|
||||||
|
sanitized.push_back(static_cast<char>(character));
|
||||||
|
else
|
||||||
|
sanitized.push_back('_');
|
||||||
|
}
|
||||||
|
return sanitized.empty() ? "font" : sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FontAtlasBuilder::RunProcess(const std::string& commandLine, const std::filesystem::path& workingDirectory, std::string& error)
|
||||||
|
{
|
||||||
|
STARTUPINFOA startupInfo = {};
|
||||||
|
PROCESS_INFORMATION processInfo = {};
|
||||||
|
startupInfo.cb = sizeof(startupInfo);
|
||||||
|
|
||||||
|
std::vector<char> mutableCommandLine(commandLine.begin(), commandLine.end());
|
||||||
|
mutableCommandLine.push_back('\0');
|
||||||
|
|
||||||
|
if (!CreateProcessA(nullptr, mutableCommandLine.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, workingDirectory.string().c_str(), &startupInfo, &processInfo))
|
||||||
|
{
|
||||||
|
error = "Failed to launch msdf-atlas-gen.exe.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UniqueHandle processHandle(processInfo.hProcess);
|
||||||
|
UniqueHandle threadHandle(processInfo.hThread);
|
||||||
|
WaitForSingleObject(processHandle.get(), INFINITE);
|
||||||
|
|
||||||
|
DWORD exitCode = 0;
|
||||||
|
GetExitCodeProcess(processHandle.get(), &exitCode);
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
error = "msdf-atlas-gen.exe returned a non-zero exit code.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/runtime/FontAtlasBuilder.h
Normal file
52
src/runtime/FontAtlasBuilder.h
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct FontAtlasBuildConfig
|
||||||
|
{
|
||||||
|
std::filesystem::path repoRoot;
|
||||||
|
std::filesystem::path cacheRoot;
|
||||||
|
double sizePixelsPerEm = 64.0;
|
||||||
|
double pixelRange = 4.0;
|
||||||
|
std::string atlasType = "mtsdf";
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FontAtlasBuildOutput
|
||||||
|
{
|
||||||
|
std::string fontId;
|
||||||
|
std::filesystem::path imagePath;
|
||||||
|
std::filesystem::path jsonPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FontAtlasBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit FontAtlasBuilder(FontAtlasBuildConfig config);
|
||||||
|
|
||||||
|
bool BuildPackageFontAtlases(
|
||||||
|
const ShaderPackage& shaderPackage,
|
||||||
|
std::vector<FontAtlasBuildOutput>& outputs,
|
||||||
|
std::string& error) const;
|
||||||
|
|
||||||
|
bool BuildFontAtlas(
|
||||||
|
const ShaderPackage& shaderPackage,
|
||||||
|
const ShaderFontAsset& fontAsset,
|
||||||
|
FontAtlasBuildOutput& output,
|
||||||
|
std::string& error) const;
|
||||||
|
|
||||||
|
static bool FindMsdfAtlasGenExecutable(const std::filesystem::path& repoRoot, std::filesystem::path& executablePath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path PackageCacheDirectory(const ShaderPackage& shaderPackage) const;
|
||||||
|
static std::string SanitizePathToken(const std::string& value);
|
||||||
|
static bool RunProcess(const std::string& commandLine, const std::filesystem::path& workingDirectory, std::string& error);
|
||||||
|
|
||||||
|
FontAtlasBuildConfig mConfig;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -66,6 +66,7 @@ bool SupportedShaderCatalog::Load(const std::filesystem::path& shaderRoot, unsig
|
|||||||
{
|
{
|
||||||
mShaders.clear();
|
mShaders.clear();
|
||||||
mPackagesById.clear();
|
mPackagesById.clear();
|
||||||
|
mFontAtlasesByShaderId.clear();
|
||||||
|
|
||||||
if (shaderRoot.empty())
|
if (shaderRoot.empty())
|
||||||
{
|
{
|
||||||
@@ -80,6 +81,10 @@ bool SupportedShaderCatalog::Load(const std::filesystem::path& shaderRoot, unsig
|
|||||||
if (!registry.Scan(shaderRoot, packagesById, packageOrder, packageStatuses, error))
|
if (!registry.Scan(shaderRoot, packagesById, packageOrder, packageStatuses, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
FontAtlasBuildConfig fontConfig;
|
||||||
|
fontConfig.repoRoot = shaderRoot.parent_path();
|
||||||
|
FontAtlasBuilder fontAtlasBuilder(fontConfig);
|
||||||
|
|
||||||
for (const std::string& packageId : packageOrder)
|
for (const std::string& packageId : packageOrder)
|
||||||
{
|
{
|
||||||
const auto packageIt = packagesById.find(packageId);
|
const auto packageIt = packagesById.find(packageId);
|
||||||
@@ -87,6 +92,14 @@ bool SupportedShaderCatalog::Load(const std::filesystem::path& shaderRoot, unsig
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
const ShaderPackage& shaderPackage = packageIt->second;
|
const ShaderPackage& shaderPackage = packageIt->second;
|
||||||
|
if (!shaderPackage.fontAssets.empty())
|
||||||
|
{
|
||||||
|
std::vector<FontAtlasBuildOutput> fontAtlasOutputs;
|
||||||
|
std::string fontAtlasError;
|
||||||
|
if (fontAtlasBuilder.BuildPackageFontAtlases(shaderPackage, fontAtlasOutputs, fontAtlasError))
|
||||||
|
mFontAtlasesByShaderId[shaderPackage.id] = std::move(fontAtlasOutputs);
|
||||||
|
}
|
||||||
|
|
||||||
const ShaderSupportResult support = CheckStatelessSinglePassShaderSupport(shaderPackage);
|
const ShaderSupportResult support = CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||||
if (!support.supported)
|
if (!support.supported)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "FontAtlasBuilder.h"
|
||||||
#include "ShaderTypes.h"
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
@@ -30,10 +31,12 @@ class SupportedShaderCatalog
|
|||||||
public:
|
public:
|
||||||
bool Load(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error);
|
bool Load(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error);
|
||||||
const std::vector<SupportedShaderSummary>& Shaders() const { return mShaders; }
|
const std::vector<SupportedShaderSummary>& Shaders() const { return mShaders; }
|
||||||
|
const std::map<std::string, std::vector<FontAtlasBuildOutput>>& FontAtlases() const { return mFontAtlasesByShaderId; }
|
||||||
const ShaderPackage* FindPackage(const std::string& shaderId) const;
|
const ShaderPackage* FindPackage(const std::string& shaderId) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<SupportedShaderSummary> mShaders;
|
std::vector<SupportedShaderSummary> mShaders;
|
||||||
std::map<std::string, ShaderPackage> mPackagesById;
|
std::map<std::string, ShaderPackage> mPackagesById;
|
||||||
|
std::map<std::string, std::vector<FontAtlasBuildOutput>> mFontAtlasesByShaderId;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ add_video_shader_test(RenderCadenceCompositorRuntimeShaderParamsTests
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_video_shader_test(RenderCadenceCompositorRuntimeLayerModelTests
|
add_video_shader_test(RenderCadenceCompositorRuntimeLayerModelTests
|
||||||
|
"${SRC_DIR}/runtime/FontAtlasBuilder.cpp"
|
||||||
"${SRC_DIR}/runtime/RuntimeLayerModel.cpp"
|
"${SRC_DIR}/runtime/RuntimeLayerModel.cpp"
|
||||||
"${SRC_DIR}/runtime/RuntimeJson.cpp"
|
"${SRC_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${SRC_DIR}/runtime/RuntimeParameterUtils.cpp"
|
"${SRC_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||||
@@ -48,7 +49,15 @@ add_video_shader_test(RenderCadenceCompositorRuntimeLayerModelTests
|
|||||||
"${TEST_DIR}/RenderCadenceCompositorRuntimeLayerModelTests.cpp"
|
"${TEST_DIR}/RenderCadenceCompositorRuntimeLayerModelTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_video_shader_test(FontAtlasBuilderTests
|
||||||
|
"${SRC_DIR}/runtime/FontAtlasBuilder.cpp"
|
||||||
|
"${SRC_DIR}/runtime/RuntimeJson.cpp"
|
||||||
|
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
|
"${TEST_DIR}/FontAtlasBuilderTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
add_video_shader_test(RenderCadenceCompositorSupportedShaderCatalogTests
|
add_video_shader_test(RenderCadenceCompositorSupportedShaderCatalogTests
|
||||||
|
"${SRC_DIR}/runtime/FontAtlasBuilder.cpp"
|
||||||
"${SRC_DIR}/runtime/RuntimeJson.cpp"
|
"${SRC_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${SRC_DIR}/runtime/SupportedShaderCatalog.cpp"
|
"${SRC_DIR}/runtime/SupportedShaderCatalog.cpp"
|
||||||
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
|
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
@@ -59,6 +68,7 @@ add_video_shader_test(RenderCadenceCompositorRuntimeStateJsonTests
|
|||||||
"${SRC_DIR}/app/AppConfig.cpp"
|
"${SRC_DIR}/app/AppConfig.cpp"
|
||||||
"${SRC_DIR}/app/AppConfigProvider.cpp"
|
"${SRC_DIR}/app/AppConfigProvider.cpp"
|
||||||
"${SRC_DIR}/json/JsonWriter.cpp"
|
"${SRC_DIR}/json/JsonWriter.cpp"
|
||||||
|
"${SRC_DIR}/runtime/FontAtlasBuilder.cpp"
|
||||||
"${SRC_DIR}/runtime/RuntimeJson.cpp"
|
"${SRC_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${SRC_DIR}/runtime/RuntimeLayerModel.cpp"
|
"${SRC_DIR}/runtime/RuntimeLayerModel.cpp"
|
||||||
"${SRC_DIR}/runtime/RuntimeParameterUtils.cpp"
|
"${SRC_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||||
|
|||||||
83
tests/FontAtlasBuilderTests.cpp
Normal file
83
tests/FontAtlasBuilderTests.cpp
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#include "FontAtlasBuilder.h"
|
||||||
|
#include "ShaderPackageRegistry.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const char* message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
++gFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path RepoRoot()
|
||||||
|
{
|
||||||
|
std::filesystem::path path = std::filesystem::current_path();
|
||||||
|
while (!path.empty())
|
||||||
|
{
|
||||||
|
if (std::filesystem::exists(path / "3rdParty" / "msdf-atlas-gen" / "msdf-atlas-gen.exe"))
|
||||||
|
return path;
|
||||||
|
const std::filesystem::path parent = path.parent_path();
|
||||||
|
if (parent.empty() || parent == path)
|
||||||
|
break;
|
||||||
|
path = parent;
|
||||||
|
}
|
||||||
|
return std::filesystem::current_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestFindsBundledExecutable()
|
||||||
|
{
|
||||||
|
std::filesystem::path executablePath;
|
||||||
|
Expect(
|
||||||
|
RenderCadenceCompositor::FontAtlasBuilder::FindMsdfAtlasGenExecutable(RepoRoot(), executablePath),
|
||||||
|
"bundled msdf-atlas-gen executable is found");
|
||||||
|
Expect(std::filesystem::exists(executablePath), "bundled msdf-atlas-gen executable exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestBuildsTextOverlayFontAtlas()
|
||||||
|
{
|
||||||
|
const std::filesystem::path repoRoot = RepoRoot();
|
||||||
|
ShaderPackageRegistry registry(4);
|
||||||
|
ShaderPackage shaderPackage;
|
||||||
|
std::string error;
|
||||||
|
Expect(registry.ParseManifest(repoRoot / "shaders" / "text-overlay" / "shader.json", shaderPackage, error), "text overlay manifest parses");
|
||||||
|
Expect(!shaderPackage.fontAssets.empty(), "text overlay declares a font asset");
|
||||||
|
|
||||||
|
RenderCadenceCompositor::FontAtlasBuildConfig config;
|
||||||
|
config.repoRoot = repoRoot;
|
||||||
|
config.cacheRoot = repoRoot / "runtime" / "test_font_cache";
|
||||||
|
RenderCadenceCompositor::FontAtlasBuilder builder(config);
|
||||||
|
std::vector<RenderCadenceCompositor::FontAtlasBuildOutput> outputs;
|
||||||
|
Expect(builder.BuildPackageFontAtlases(shaderPackage, outputs, error), "text overlay font atlas builds");
|
||||||
|
Expect(outputs.size() == 1, "one font atlas output is produced");
|
||||||
|
if (!outputs.empty())
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestFindsBundledExecutable();
|
||||||
|
TestBuildsTextOverlayFontAtlas();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " font atlas builder test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "FontAtlasBuilder tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "SupportedShaderCatalog.h"
|
#include "SupportedShaderCatalog.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -29,6 +30,21 @@ ShaderPackage MakeSinglePassPackage()
|
|||||||
return shaderPackage;
|
return shaderPackage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::filesystem::path RepoRoot()
|
||||||
|
{
|
||||||
|
std::filesystem::path path = std::filesystem::current_path();
|
||||||
|
while (!path.empty())
|
||||||
|
{
|
||||||
|
if (std::filesystem::exists(path / "shaders" / "text-overlay" / "shader.json"))
|
||||||
|
return path;
|
||||||
|
const std::filesystem::path parent = path.parent_path();
|
||||||
|
if (parent.empty() || parent == path)
|
||||||
|
break;
|
||||||
|
path = parent;
|
||||||
|
}
|
||||||
|
return std::filesystem::current_path();
|
||||||
|
}
|
||||||
|
|
||||||
void SupportsSinglePassStatelessPackage()
|
void SupportsSinglePassStatelessPackage()
|
||||||
{
|
{
|
||||||
const ShaderPackage shaderPackage = MakeSinglePassPackage();
|
const ShaderPackage shaderPackage = MakeSinglePassPackage();
|
||||||
@@ -110,6 +126,24 @@ void RejectsTextParameters()
|
|||||||
Expect(!result.supported, "text-parameter packages should be rejected for now");
|
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.reason.find("text") != std::string::npos, "text rejection should mention text parameters");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BuildsDeclaredFontAtlasesDuringCatalogLoad()
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::SupportedShaderCatalog catalog;
|
||||||
|
std::string error;
|
||||||
|
Expect(catalog.Load(RepoRoot() / "shaders", 12, error), "shader catalog loads");
|
||||||
|
|
||||||
|
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");
|
||||||
|
if (textOverlayIt != fontAtlases.end() && !textOverlayIt->second.empty())
|
||||||
|
{
|
||||||
|
Expect(std::filesystem::exists(textOverlayIt->second.front().imagePath), "catalog font atlas image exists");
|
||||||
|
Expect(std::filesystem::exists(textOverlayIt->second.front().jsonPath), "catalog font atlas json exists");
|
||||||
|
Expect(std::filesystem::file_size(textOverlayIt->second.front().imagePath) > 0, "catalog font atlas image is not empty");
|
||||||
|
Expect(std::filesystem::file_size(textOverlayIt->second.front().jsonPath) > 0, "catalog font atlas json is not empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
@@ -120,6 +154,7 @@ int main()
|
|||||||
RejectsTemporalPackage();
|
RejectsTemporalPackage();
|
||||||
RejectsTextureAssets();
|
RejectsTextureAssets();
|
||||||
RejectsTextParameters();
|
RejectsTextParameters();
|
||||||
|
BuildsDeclaredFontAtlasesDuringCatalogLoad();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user