Font fix and forkable docs
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m52s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
Aiden
2026-05-30 15:01:36 +10:00
parent cfb796756d
commit d0b1f63524
4 changed files with 158 additions and 7 deletions

View File

@@ -13,6 +13,7 @@ The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime
- `runtime/templates/`: tracked shader wrapper templates.
- `runtime/`: ignored generated runtime cache/state output. See `runtime/README.md`.
- `tests/`: focused native tests for pure runtime logic.
- `docs/FORKING_RENDER_CADENCE_BASE.md`: notes for forking the cadence/video I/O base while replacing the GPU-rendered content.
- `.gitea/workflows/ci.yml`: Gitea Actions CI for Windows native tests and Ubuntu UI build.
Native app internals are grouped by boundary:

View File

@@ -0,0 +1,93 @@
# Forking The Render Cadence Base
This note captures the fork-readiness review for using this repository as a base where the render cadence and video I/O work are kept, but the GPU content being rendered is replaced in a separate repository.
## Verdict
The repository is clean enough for an internal fork, but it needs a small hygiene pass before it becomes a comfortable long-lived base repo.
The important architecture is already in place: render cadence, video input/output, frame exchange, readback, preview, control, and shader build work are mostly separated by role. The main replacement point is the render-thread draw path in `src/render/thread/RenderThread.cpp`, where cadence, input upload, readback, and frame publication wrap the actual GPU rendering call.
For a new repo, keep the cadence and frame handoff machinery, then replace or narrow the runtime shader rendering layer.
## Keep
These parts are the useful base for the fork:
- `src/frames`: CPU frame handoff, input mailbox, and completed-frame exchange.
- `src/video/core`: backend-neutral input/output contracts.
- `src/video/decklink`, `src/video/ndi`, and `src/video/playout`: concrete video I/O edges and scheduling support.
- `src/render/thread`: render cadence ownership, readback pumping, runtime render-layer commit point, and metrics.
- `src/render/readback`: BGRA8 PBO readback and completed-frame publication.
- `src/platform`: hidden GL window/context support.
- `src/app`: startup, config, video backend factory, runtime layer orchestration, preview, telemetry, and HTTP server hookup.
- `src/control`, `src/telemetry`, `src/logging`, and `ui`: useful if the new repo still wants a local control surface.
## Replace Or Rework
These are most likely to change when the fork renders something other than shader packages:
- `src/render/runtime`: current runtime shader scene, renderer, text texture cache, and shared-context shader preparation.
- `src/runtime/shader`: background Slang package build bridge.
- `src/shader`: shader package manifest parsing and Slang wrapper generation, unless the new renderer keeps the same shader package contract.
- `shaders/`: bundled shader package library.
- `runtime/templates/shader_wrapper.slang.in`: only needed for the current Slang package pipeline.
- Shader-specific UI affordances in `ui`, if the new renderer has a different control model.
The cleanest first fork step is to preserve `RenderThread`'s cadence/readback shell and introduce a narrow render-content interface behind the draw call. Then the new repo can swap the implementation without touching video I/O scheduling.
## Current Swap Point
The current draw decision happens inside the readback queue call in `src/render/thread/RenderThread.cpp`:
```cpp
if (runtimeRenderScene.HasLayers())
runtimeRenderScene.RenderFrame(index, mConfig.width, mConfig.height, videoInputTexture);
else if (videoInputTexture != 0)
renderer.RenderTexture(videoInputTexture);
else
renderer.RenderFrame(index);
```
That is the practical boundary for a fork:
- keep the tick clock, input upload, readback queueing, and `SystemFrameExchange` publication around it
- replace what draws into the current GL framebuffer
- keep video output consuming already completed system-memory frames
Do not move DeckLink, NDI, file I/O, shader compilation, or control handling into the cadence path while doing the replacement.
## Fork Hygiene Checklist
Before cutting a long-lived fork, fix or decide these items:
- Remove hardcoded `happy-accident` assumptions in `src/app/AppConfig.h` and `src/runtime/shader/RuntimeSlangShaderCompiler.cpp`.
- Align remaining runtime third-party discovery with CMake. Font atlas generation now checks `MSDF_ATLAS_GEN_ROOT`, `THIRD_PARTY_ROOT`, `3rdParty`, and `video-io-3rdParty`; shader compiler lookup still needs the same treatment for Slang.
- Make `config/runtime-host.json` portable. Current checked-in defaults include a local NDI source name and DeckLink output.
- Decide whether the fork keeps the Slang shader package contract. If not, retire or clearly isolate `shaders/SHADER_CONTRACT.md`, shader package UI, and shader manifest tests.
- Mark older docs that reference `apps/LoopThroughWithOpenGLCompositing` as historical, or update them to point at the current `src/` implementation.
- Keep `runtime/` generated output ignored, and keep only `runtime/templates/` plus `runtime/README.md` tracked.
- Keep the private SDK bundle as a submodule only if the new repo is intended for the same org/build environment. External forks should use ignored `3rdParty/` or explicit CMake SDK paths.
## Verification Snapshot
Last checked locally on 2026-05-30 after the font-builder lookup fix:
- Worktree was clean before documentation changes.
- UI production build passed with `npm.cmd run build`.
- Native debug build passed with `cmake --build --preset build-debug --parallel`.
- Native tests passed 24 of 24 with `ctest --test-dir build\vs2022-x64-debug -C Debug --output-on-failure`.
- `FontAtlasBuilderTests` now passes with `msdf-atlas-gen.exe` supplied by the private `video-io-3rdParty/msdf-atlas-gen` bundle.
The generated Visual Studio `RUN_TESTS` target did not build missing test executables by itself during the first local run; building the debug preset first produced the test binaries.
## Recommended Fork Sequence
1. Make the hygiene fixes above in this repo or immediately after the fork.
2. Add a small render-content abstraction behind the `RenderThread` draw call.
3. Port the existing runtime shader renderer behind that abstraction as the baseline implementation.
4. Add the new renderer beside it.
5. Verify that video output still consumes completed frames and never requests rendering directly.
6. Only then remove shader package pieces that the new repo no longer needs.
That sequence preserves the hard-won cadence/video I/O behavior while giving the fork a clean place to become its own renderer.

View File

@@ -2,6 +2,7 @@
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <sstream>
#include <system_error>
@@ -20,6 +21,55 @@ std::string NumberText(double value)
stream << value;
return stream.str();
}
bool UseExecutableIfPresent(const std::filesystem::path& candidate, std::filesystem::path& executablePath)
{
if (!std::filesystem::exists(candidate) || std::filesystem::is_directory(candidate))
return false;
executablePath = candidate;
return true;
}
bool UseRootIfPresent(const std::filesystem::path& root, std::filesystem::path& executablePath)
{
if (root.empty())
return false;
if (UseExecutableIfPresent(root, executablePath))
return true;
return UseExecutableIfPresent(root / "msdf-atlas-gen.exe", executablePath);
}
std::filesystem::path EnvironmentPath(const char* variableName)
{
#if defined(_MSC_VER)
char* value = nullptr;
std::size_t size = 0;
if (_dupenv_s(&value, &size, variableName) != 0 || value == nullptr)
return std::filesystem::path();
std::string text(value);
std::free(value);
if (text.empty())
return std::filesystem::path();
return std::filesystem::path(text);
#else
const char* value = std::getenv(variableName);
if (value == nullptr || *value == '\0')
return std::filesystem::path();
return std::filesystem::path(value);
#endif
}
bool UseEnvironmentRoot(const char* variableName, std::filesystem::path& executablePath)
{
const std::filesystem::path value = EnvironmentPath(variableName);
if (value.empty())
return false;
return UseRootIfPresent(value, executablePath);
}
}
FontAtlasBuilder::FontAtlasBuilder(FontAtlasBuildConfig config) :
@@ -68,7 +118,7 @@ bool FontAtlasBuilder::BuildFontAtlas(
std::filesystem::path executablePath;
if (!FindMsdfAtlasGenExecutable(mConfig.repoRoot, executablePath))
{
error = "Could not find msdf-atlas-gen.exe under 3rdParty/msdf-atlas-gen.";
error = "Could not find msdf-atlas-gen.exe. Set MSDF_ATLAS_GEN_ROOT or place it under 3rdParty/msdf-atlas-gen or video-io-3rdParty/msdf-atlas-gen.";
return false;
}
@@ -123,12 +173,18 @@ bool FontAtlasBuilder::BuildFontAtlas(
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;
if (UseEnvironmentRoot("MSDF_ATLAS_GEN_ROOT", executablePath))
return true;
const std::filesystem::path thirdPartyRoot = EnvironmentPath("THIRD_PARTY_ROOT");
if (!thirdPartyRoot.empty() && UseRootIfPresent(thirdPartyRoot / "msdf-atlas-gen", executablePath))
return true;
if (UseRootIfPresent(repoRoot / "3rdParty" / "msdf-atlas-gen", executablePath))
return true;
if (UseRootIfPresent(repoRoot / "video-io-3rdParty" / "msdf-atlas-gen", executablePath))
return true;
}
return false;
}

View File

@@ -22,7 +22,8 @@ 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"))
if (std::filesystem::exists(path / "CMakeLists.txt") &&
std::filesystem::exists(path / "shaders" / "text-overlay" / "shader.json"))
return path;
const std::filesystem::path parent = path.parent_path();
if (parent.empty() || parent == path)