diff --git a/README.md b/README.md index 41fb040..2bb33ab 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/docs/FORKING_RENDER_CADENCE_BASE.md b/docs/FORKING_RENDER_CADENCE_BASE.md new file mode 100644 index 0000000..b3bc6f6 --- /dev/null +++ b/docs/FORKING_RENDER_CADENCE_BASE.md @@ -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. diff --git a/src/runtime/text/FontAtlasBuilder.cpp b/src/runtime/text/FontAtlasBuilder.cpp index c963dfa..d3ac5bb 100644 --- a/src/runtime/text/FontAtlasBuilder.cpp +++ b/src/runtime/text/FontAtlasBuilder.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -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; } diff --git a/tests/FontAtlasBuilderTests.cpp b/tests/FontAtlasBuilderTests.cpp index 0691608..a079a1e 100644 --- a/tests/FontAtlasBuilderTests.cpp +++ b/tests/FontAtlasBuilderTests.cpp @@ -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)