diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e5f5535..570bc89 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -12,9 +12,9 @@ What happened? What did you expect? ## Steps to reproduce -1. … -2. … -3. … +1. ... +2. ... +3. ... ## Environment diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fdb05a1..4fdf9ba 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,7 @@ -**Release notes:** Use a [Conventional Commits](https://www.conventionalcommits.org/)–style PR title (e.g. `feat: add X`, `fix: resolve Y`, `docs: update Z`) so [release-please](https://github.com/googleapis/release-please) can include this change in the changelog. If you squash-merge, use the PR title as the commit message. +**Release notes:** Use a [Conventional Commits](https://www.conventionalcommits.org/)-style PR title (e.g. `feat: add X`, `fix: resolve Y`, `docs: update Z`) so [release-please](https://github.com/googleapis/release-please) can include this change in the changelog. If you squash-merge, use the PR title as the commit message. ## Checklist diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e7aad93..157c59e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,7 +4,7 @@ Use this for every PR review in this repository: optimize for correctness, safet ## Review output format -- Start with a brief summary (1–3 bullets): what changed + biggest risks. +- Start with a brief summary (1-3 bullets): what changed + biggest risks. - Then list issues grouped by severity: - **BLOCKER** (must fix before merge) - **MAJOR** (strongly recommended) @@ -32,7 +32,7 @@ Use this for every PR review in this repository: optimize for correctness, safet - Flag accidental O(N²) behavior, unnecessary allocations/copies, and hidden sync points. - Recommend `const&`, move semantics, reserve, and `std::span` when it helps. - Prefer algorithmic improvements over micro-optimizations. -- Avoid premature "clever" tricks—only optimize with a clear benefit. +- Avoid premature "clever" tricks - only optimize with a clear benefit. ### Concurrency @@ -96,11 +96,11 @@ Apply this section when the PR changes files under `tests/` or `*_test.cpp`. - If mocking: keep mocks minimal and verify behavior, not implementation details. - Ensure tests run fast and don't require special hardware unless explicitly marked. -## This repository (VneTemplate) +## This repository (vnesc) -### Project layout +### Project layout (vertexnova/sc) -- Library: **src/vertexnova/template/** — Public API: **include/vertexnova/template/** — Tests: **tests/** — Examples: **examples/** +- Library: **src/vertexnova/sc/** - Public API: **include/vertexnova/sc/** - Tests: **tests/** - Examples: **examples/** - Dependencies: **deps/internal/** (e.g. vnecommon, vnelogging), **deps/external/** (e.g. googletest). No top-level `external/` or `libs/`. ### Formatting and static analysis @@ -115,7 +115,7 @@ Keep comments concrete: cite the guideline or rule and suggest a specific fix wh ## Checklist -- [ ] Summary (1–3 bullets) and issues grouped by BLOCKER / MAJOR / MINOR with file, reason, and fix. +- [ ] Summary (1-3 bullets) and issues grouped by BLOCKER / MAJOR / MINOR with file, reason, and fix. - [ ] C++ focus areas considered (correctness, memory, perf, concurrency, errors, security, build, docs, CI). - [ ] Naming/style and test rules (if PR touches tests) applied; violations cited with suggested fix. - [ ] Repo layout and deps respected; no unnecessary deps edits. diff --git a/.github/workflows/build-reusable.yml b/.github/workflows/build-reusable.yml index 5b562bc..1fb1577 100644 --- a/.github/workflows/build-reusable.yml +++ b/.github/workflows/build-reusable.yml @@ -136,7 +136,7 @@ jobs: if: runner.os == 'Linux' && inputs.enable_coverage run: sudo apt-get install -y lcov - # Coverage builds must start clean — cached Release objects lack + # Coverage builds must start clean - cached Release objects lack # --coverage instrumentation and CMake will not recompile them. - name: Clean build directory (coverage only) if: inputs.enable_coverage @@ -144,7 +144,7 @@ jobs: run: rm -rf ${{ env.BUILD_DIR }} # ------------------------------------------------------------------ - # Configure — use EFFECTIVE_BUILD_TYPE from previous step (single source of truth). + # Configure - use EFFECTIVE_BUILD_TYPE from previous step (single source of truth). # ------------------------------------------------------------------ - name: Configure CMake shell: bash @@ -165,22 +165,14 @@ jobs: COMPILER_FLAGS="-DCMAKE_C_COMPILER=$C_COMPILER -DCMAKE_CXX_COMPILER=$CPP_COMPILER" fi - # Use validated CACHE_LIB_TYPE from previous step (empty/shared/static only). - LIB_TYPE_FLAGS="" - if [[ "$CACHE_LIB_TYPE" == "static" ]]; then - LIB_TYPE_FLAGS="-DVNE_TEMPLATE_LIB_TYPE=static" - else - LIB_TYPE_FLAGS="-DVNE_TEMPLATE_LIB_TYPE=shared" - fi - # shellcheck disable=SC2086 # intentional word-splitting for flag vars + # vnesc builds static archives; CACHE_LIB_TYPE only selects the build/ dir name. cmake -B "$BUILD_DIR" -S . \ -DCMAKE_BUILD_TYPE="$EFFECTIVE_BUILD_TYPE" \ - -DVNE_TEMPLATE_CI=ON \ + -DVNE_SC_CI=ON \ -DWARNINGS_AS_ERRORS="$WAE" \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ ${COVERAGE_FLAG} \ - ${LIB_TYPE_FLAGS} \ ${COMPILER_FLAGS} \ ${EXTRA_FLAGS} @@ -198,12 +190,12 @@ jobs: GCNO_COUNT=$(find "$BUILD_DIR" -name "*.gcno" 2>/dev/null | wc -l) echo "Total .gcno files: $GCNO_COUNT" if [[ "$GCNO_COUNT" -eq 0 ]]; then - echo "::error::No .gcno files found — binary was not compiled with --coverage. Check ENABLE_COVERAGE and build type." + echo "::error::No .gcno files found - binary was not compiled with --coverage. Check ENABLE_COVERAGE and build type." exit 1 fi # ------------------------------------------------------------------ - # Test — JUnit XML output for inline PR annotations + # Test - JUnit XML output for inline PR annotations # Note: --output-junit requires CMake 3.21+; ubuntu-latest ships 3.22+ # ------------------------------------------------------------------ - name: Test @@ -224,7 +216,7 @@ jobs: GCDA_COUNT=$(find "$BUILD_DIR" -name "*.gcda" 2>/dev/null | wc -l) echo "Total .gcda files: $GCDA_COUNT" if [[ "$GCDA_COUNT" -eq 0 ]]; then - echo "::error::No .gcda files found after running tests — tests may not have executed or binary not linked with --coverage." + echo "::error::No .gcda files found after running tests - tests may not have executed or binary not linked with --coverage." exit 1 fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 232b577..8273f04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,12 +5,12 @@ # Author: Ajeet Singh Yadav # Created: February 2026 # -# Description: CI pipeline — staged, gated, with concurrency cancellation. -# Stage 1 — Pre-checks : clang-format (fail-fast gate), commit-lint (warning only) -# Stage 2+3 — Build+Test: Linux GCC/Clang/ASan, macOS, Windows (static libs only) -# Stage 4 — Analysis : clang-tidy (reuses Clang build cache) -# Stage 5 — CI Gate : single required status check for branch protection -# Branch protection: require only "CI Gate" — not individual job names. +# Description: CI pipeline - staged, gated, with concurrency cancellation. +# Stage 1 - Pre-checks : clang-format (fail-fast gate), commit-lint (warning only) +# Stage 2+3 - Build+Test: Linux GCC/Clang/ASan, macOS, Windows (static libs only) +# Stage 4 - Analysis : clang-tidy (reuses Clang build cache) +# Stage 5 - CI Gate : single required status check for branch protection +# Branch protection: require only "CI Gate" - not individual job names. # # Autodoc: yes # ---------------------------------------------------------------------- @@ -43,7 +43,7 @@ jobs: # STAGE 0: DETECT RELEASE-PLEASE COMMITS # Release-please merges only touch: CHANGELOG.md, VERSION, # .release-please-manifest.json, release-please-config.json. - # Skip all builds for those commits — CI already ran on the source PR. + # Skip all builds for those commits - CI already ran on the source PR. # ==================================================================== detect-release: @@ -69,7 +69,7 @@ jobs: if [[ "$EVENT" == "pull_request" ]]; then if [[ "$HEAD_REF" == release-please--* ]]; then IS_RELEASE=true - echo "Release-please PR detected (branch: $HEAD_REF) — skipping builds." + echo "Release-please PR detected (branch: $HEAD_REF) - skipping builds." fi fi @@ -88,7 +88,7 @@ jobs: if [[ "$ACTOR_MATCH" == "true" && -z "$NON_RELEASE" && -n "$CHANGED" ]]; then IS_RELEASE=true - echo "Release-please merge detected (actor: $ACTOR, files: $CHANGED) — skipping builds." + echo "Release-please merge detected (actor: $ACTOR, files: $CHANGED) - skipping builds." fi fi @@ -96,7 +96,7 @@ jobs: # ==================================================================== # STAGE 1: PRE-CHECKS (fail-fast gate) - # These jobs do NOT clone submodules — they only need source files. + # These jobs do NOT clone submodules - they only need source files. # If any pre-check fails the gate blocks all downstream builds. # ==================================================================== @@ -108,7 +108,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - submodules: false # clang-format only reads src/ include/ — no deps needed + submodules: false # clang-format only reads src/ include/ - no deps needed - name: Install clang-format-17 # Pin to 17: the .clang-format file uses options removed in clang-format 18+ @@ -131,7 +131,7 @@ jobs: name: "Pre-checks / commit messages" runs-on: ubuntu-latest # Only validate on PRs. Push-to-main arrives as release-please merge - # commits (chore(main): release X.Y.Z) which are always valid — and + # commits (chore(main): release X.Y.Z) which are always valid - and # validating every commit in a merge-commit ancestry is fragile. if: github.event_name == 'pull_request' permissions: @@ -317,8 +317,7 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=clang \ -DCMAKE_CXX_COMPILER=clang++ \ - -DVNE_TEMPLATE_CI=ON \ - -DVNE_TEMPLATE_LIB_TYPE=static \ + -DVNE_SC_CI=ON \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON cmake --build build/static --config Release --parallel fi @@ -337,7 +336,7 @@ jobs: # ==================================================================== # STAGE 5: CI GATE # Single required status check. Branch protection should require only - # this job — not the individual build job names. This ensures the + # this job - not the individual build job names. This ensures the # required check name stays stable even as build jobs are renamed. # ==================================================================== diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4e1c161..86ee429 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -68,8 +68,7 @@ jobs: run: | cmake -B build/static -S . \ -DCMAKE_BUILD_TYPE=Release \ - -DVNE_TEMPLATE_CI=ON \ - -DVNE_TEMPLATE_LIB_TYPE=static \ + -DVNE_SC_CI=ON \ -DWARNINGS_AS_ERRORS=OFF cmake --build build/static --config Release --parallel diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index b4010c4..4ff2d05 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -9,10 +9,10 @@ # When the Release PR is merged, publishes pre-built Release binary artifacts # for all supported platforms to the GitHub Release. # Two-tier release model: -# Tier 1 — source consumers: pin the release tag as a git submodule. -# Tier 2 — binary consumers: download .tar.gz from GitHub Releases. +# Tier 1 - source consumers: pin the release tag as a git submodule. +# Tier 2 - binary consumers: download .tar.gz from GitHub Releases. # Repo Settings required (one-time): -# Settings → General → Pull Requests → "Allow auto-merge" — ON +# Settings -> General -> Pull Requests -> "Allow auto-merge" - ON # REPO_ACCESS_TOKEN (optional): use for higher limits; else github.token is used. # # Autodoc: yes @@ -53,7 +53,7 @@ jobs: token: ${{ secrets.REPO_ACCESS_TOKEN || github.token }} # Enable auto-merge so the Release PR merges once CI Gate passes. If the repo - # has not enabled "Allow auto-merge" (Settings → General, and Branches → main), + # has not enabled "Allow auto-merge" (Settings -> General, and Branches -> main), # we warn and leave the PR open for manual merge instead of failing the workflow. - name: Enable auto-merge on Release PR if: steps.rp.outputs.pr != '' @@ -64,7 +64,7 @@ jobs: PR_NUMBER="$(echo "$PR_RAW" | python3 -c 'import sys,json; print(json.load(sys.stdin)["number"])')" if ! gh pr merge --repo "${{ github.repository }}" --auto --squash "$PR_NUMBER" 2>&1 | tee /tmp/gh-out; then if grep -q "enablePullRequestAutoMerge\|Auto merge is not allowed" /tmp/gh-out; then - echo "::warning::Auto-merge is disabled for this repo. Enable it under Settings → General → Pull Requests → 'Allow auto-merge'. Release PR #$PR_NUMBER remains open for manual merge." + echo "::warning::Auto-merge is disabled for this repo. Enable it under Settings -> General -> Pull Requests -> 'Allow auto-merge'. Release PR #$PR_NUMBER remains open for manual merge." exit 0 fi exit 1 @@ -72,14 +72,14 @@ jobs: # -------------------------------------------------------------------- # Stage 2: Build and publish artifacts (only when a release was created this run). - # Skipped on most pushes — release_created is true only when the Release PR + # Skipped on most pushes - release_created is true only when the Release PR # was merged in this run (release-please then creates the tag + GitHub Release). # # Produces 3 tarballs per release (Release build only): - # vnetemplate-vX.Y.Z-{platform}.tar.gz + # vnesc-vX.Y.Z-{platform}.tar.gz # - # Contents: one library per build in lib/ (static or shared via VNE_TEMPLATE_LIB_TYPE; - # e.g. libvnetemplate.so or libvnetemplate.a on Unix), include/vertexnova/template/ + # Contents: static libs under lib/ (libvnesc.a, libvnesc_spirvcross.a, ...), + # include/vertexnova/sc/, and CLI tools under bin/ when built. # -------------------------------------------------------------------- publish: name: "Release / publish (${{ matrix.platform }})" @@ -128,10 +128,12 @@ jobs: if [[ -n "${{ matrix.c }}" ]]; then COMPILER_FLAGS="-DCMAKE_C_COMPILER=${{ matrix.c }} -DCMAKE_CXX_COMPILER=${{ matrix.cpp }}" fi + # shellcheck disable=SC2086 cmake -B build/static -S . \ -DCMAKE_BUILD_TYPE=Release \ - -DVNE_TEMPLATE_CI=ON \ - -DVNE_TEMPLATE_LIB_TYPE=static \ + -DVNE_SC_TESTS=OFF \ + -DVNE_SC_EXAMPLES=OFF \ + -DVNE_SC_TOOLS=ON \ -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/install" \ $COMPILER_FLAGS @@ -141,16 +143,37 @@ jobs: - name: Install to staging directory shell: bash - run: cmake --install build/static --config Release --component vnetemplate + run: | + # Install library + headers (vnesc) and CLI tools (vnesc_tools). + cmake --install build/static --config Release --component vnesc + cmake --install build/static --config Release --component vnesc_tools + + - name: Verify install layout + shell: bash + run: | + PREFIX="${{ github.workspace }}/install" + if [[ ! -d "$PREFIX/include/vertexnova/sc" ]]; then + echo "::error::Expected $PREFIX/include/vertexnova/sc missing after cmake --install." + ls -la "$PREFIX" || true + exit 1 + fi + if [[ ! -d "$PREFIX/lib" ]] && [[ ! -d "$PREFIX/lib64" ]]; then + echo "::error::Expected lib/ or lib64/ under $PREFIX missing." + ls -laR "$PREFIX" || true + exit 1 + fi + echo "Install layout OK:" + find "$PREFIX" -maxdepth 3 -type f | head -40 - name: Package artifact shell: bash run: | - ARTIFACT="vnetemplate-v${{ needs.release-please.outputs.version }}-${{ matrix.platform }}.tar.gz" + ARTIFACT="vnesc-v${{ needs.release-please.outputs.version }}-${{ matrix.platform }}.tar.gz" # -C changes into the install dir so tar sees only its contents (no path prefix). - # The output path is relative to CWD (repo root) — avoids Windows drive-letter issues. + # The output path is relative to CWD (repo root) - avoids Windows drive-letter issues. tar -czf "${ARTIFACT}" -C "${{ github.workspace }}/install" . echo "ARTIFACT=${ARTIFACT}" >> "$GITHUB_ENV" + echo "Published: ${ARTIFACT}" - name: Upload to GitHub Release uses: softprops/action-gh-release@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e755744..294f6f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# Pre-commit hooks for VneTemplate +# Pre-commit hooks for vnesc # Install: pip install pre-commit && pre-commit install # Run manually: pre-commit run --all-files diff --git a/CHANGELOG.md b/CHANGELOG.md index 945ddb3..5c2a5ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,7 +83,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug Fixes * fixing CI skipping issue while merging to main ([#15](https://github.com/vertexnova/vnetemplate/issues/15)) ([4f60e3e](https://github.com/vertexnova/vnetemplate/commit/4f60e3eec70bc6b2b9b926887c9ec3d447a6b394)) -* improve auto-merge handling for Release PRs to support special c… ([#17](https://github.com/vertexnova/vnetemplate/issues/17)) ([6c08f5d](https://github.com/vertexnova/vnetemplate/commit/6c08f5d92d205bc13beceb529342bec24fb4ac45)) +* improve auto-merge handling for Release PRs to support special c... ([#17](https://github.com/vertexnova/vnetemplate/issues/17)) ([6c08f5d](https://github.com/vertexnova/vnetemplate/commit/6c08f5d92d205bc13beceb529342bec24fb4ac45)) ## [0.1.1](https://github.com/vertexnova/vnetemplate/compare/v0.1.0...v0.1.1) (2026-02-20) diff --git a/CMakeLists.txt b/CMakeLists.txt index 50f4c31..ee0d26b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,8 +42,10 @@ include(ProjectSetup) include(CompilerOptions) #============================================================================== -# Internal Libraries (deps/internal) - use if already in build, else add +# Internal Libraries # #============================================================================== + +# Use deps/internal if already in the build, otherwise add_subdirectory via VneUseDep. include(VneUseDep) function(_vnesc_configure_vnecommon_dep) @@ -87,27 +89,30 @@ function(_vnesc_configure_vnelogging_dep) endfunction() _vnesc_configure_vnelogging_dep() -# Include dirs and config +#============================================================================== +# Include Dirs and config.h # +#============================================================================== + set(VNE_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include) set(VNE_SRC_DIR ${PROJECT_SOURCE_DIR}/src) configure_file(${PROJECT_SOURCE_DIR}/configs/config.h.in ${CMAKE_BINARY_DIR}/config.h) include_directories(${CMAKE_BINARY_DIR}) #============================================================================== -# Build Options +# Build Options # #============================================================================== + # Option naming and deps: -# - Use VNE_TEMPLATE_* for vnetemplate-only options so there is no conflict when -# this project is used as a submodule (parent can set VNE_TEMPLATE_TESTS=OFF -# without affecting BUILD_TESTS used by other deps). +# - Use VNE_SC_* for vnesc-only options so there is no conflict when this project +# is used as a submodule (parent can set VNE_SC_TESTS=OFF without affecting +# BUILD_TESTS used by other deps). # - BUILD_TESTS / BUILD_EXAMPLES are global; we force them (via VneUseDep # CACHE_VARS) before add_subdirectory for each dep so deps do not build their # own tests/examples when used as internal libs. We restore after each dep. # - ENABLE_COVERAGE, ENABLE_DOXYGEN, WARNINGS_AS_ERRORS are global: if multiple # libs in deps/internal define the same option name, the first to run wins the -# default; -D from the command line applies to all. No duplicate option() -# definition (CMake uses cache), but behavior is "one value for the whole tree." -#============================================================================== +# default; -D from the command line applies to all. + if(CMAKE_PROJECT_NAME STREQUAL "vnesc") set(VNE_SC_DEV_DEFAULT ON) else() @@ -124,12 +129,15 @@ option(VNE_SC_TESTS "Build vnesc test suite (turn OFF when used as submodule) option(VNE_SC_EXAMPLES "Build vnesc example programs (turn OFF when used as submodule)" OFF) option(VNE_SC_TOOLS "Build vnesc CLI tools (e.g. vnesc_shader_compiler)" ON) -# ── Shader compiler backend options ────────────────────────────────────────── -option(VNE_SC_GLSLANG "Enable glslang GLSL→SPIR-V front-end (Phase 1, primary)" ON) -option(VNE_SC_DXC "Enable DXC HLSL→SPIR-V front-end (Phase 2 — stub only)" OFF) -option(VNE_SC_SLANG "Enable Slang front-end (Phase 3 — stub only)" OFF) +#============================================================================== +# Shader Compiler Backends # +#============================================================================== + +option(VNE_SC_GLSLANG "Enable glslang GLSL->SPIR-V front-end (Phase 1, primary)" ON) +option(VNE_SC_DXC "Enable DXC HLSL->SPIR-V front-end (Phase 2 - stub only)" OFF) +option(VNE_SC_SLANG "Enable Slang front-end (Phase 3 - stub only)" OFF) option(VNE_SC_SPIRVTOOLS "Enable SPIRV-Tools SPIR-V validator (OFF = no-op validator)" OFF) -option(VNE_SC_TINT "Enable Dawn Tint SPIR-V→WGSL cross-compiler (slow first configure)" OFF) +option(VNE_SC_TINT "Enable Dawn Tint SPIR-V->WGSL cross-compiler (slow first configure)" OFF) option(VNE_SC_JSON "Enable nlohmann/json for manifests and bundle metadata" ON) option(ENABLE_DOXYGEN "Enable Doxygen documentation builds" OFF) @@ -137,10 +145,8 @@ option(ENABLE_COVERAGE "Enable code coverage reporting" OFF) option(ENABLE_ASAN "Enable AddressSanitizer + UndefinedBehaviorSanitizer (Clang/GCC, Linux/macOS only)" OFF) option(WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF) -#============================================================================== -# Incompatible options: coverage and ASan/UBSan must not both be enabled -# (--coverage + -fsanitize=... together produce noisy/incorrect results). -#============================================================================== +# Coverage and ASan/UBSan must not both be enabled (--coverage + -fsanitize +# together produce noisy/incorrect results). if(ENABLE_COVERAGE AND ENABLE_ASAN) message(FATAL_ERROR "ENABLE_COVERAGE and ENABLE_ASAN cannot both be ON. " @@ -161,7 +167,7 @@ elseif(VNE_SC_DEV) endif() #============================================================================== -# Compiler Warnings +# Compiler Warnings # #============================================================================== add_library(VnescWarnings INTERFACE) include(ProjectWarnings) @@ -169,7 +175,7 @@ set_project_warnings(VnescWarnings) add_library(vne::sc::Warnings ALIAS VnescWarnings) #============================================================================== -# Build Settings Interface Library +# Build Settings Interface Library # #============================================================================== add_library(VnescBuildSettings INTERFACE) target_compile_features(VnescBuildSettings INTERFACE cxx_std_20) @@ -201,16 +207,20 @@ elseif(VNE_TARGET_PLATFORM STREQUAL "Web") target_compile_definitions(VnescBuildSettings INTERFACE VNE_PLATFORM_WEB) endif() -# External shader compiler dependencies (FetchContent) +#============================================================================== +# External Dependencies (FetchContent) # +#============================================================================== + include(Dependencies) #============================================================================== -# Add Library +# Library # #============================================================================== + add_subdirectory(src) #============================================================================== -# Tests +# Tests # #============================================================================== if(VNE_SC_TESTS) enable_testing() @@ -218,21 +228,21 @@ if(VNE_SC_TESTS) endif() #============================================================================== -# Examples +# Examples # #============================================================================== if(VNE_SC_EXAMPLES) add_subdirectory(examples) endif() #============================================================================== -# Tools +# Tools # #============================================================================== if(VNE_SC_TOOLS) add_subdirectory(tools/shader_compiler_cli) endif() #============================================================================== -# Documentation +# Documentation # #============================================================================== if(ENABLE_DOXYGEN) find_package(Doxygen QUIET OPTIONAL_COMPONENTS dot) @@ -254,7 +264,7 @@ if(ENABLE_DOXYGEN) endif() #============================================================================== -# CPack (ZIP/TGZ release archives) +# CPack (ZIP/TGZ release archives) # #============================================================================== set(CPACK_PACKAGE_NAME "vnesc") set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") @@ -266,6 +276,6 @@ set(CPACK_COMPONENT_VNESC_TOOLS_DISPLAY_NAME "vnesc CLI tools") include(CPack) #============================================================================== -# Installation +# Installation # #============================================================================== include(InstallRules) diff --git a/README.md b/README.md index b3af4fc..d8b101d 100644 --- a/README.md +++ b/README.md @@ -21,20 +21,20 @@ ## About -**vnesc** is an offline shader compiler library that transforms GLSL source into a portable `.vneshader` bundle: SPIR-V bytecode, cross-compiled MSL/WGSL/HLSL output, and typed resource-binding reflection — all ready for direct consumption by [vnerhi](https://github.com/vertexnova/vnerhi). +**vnesc** is an offline shader compiler library that transforms GLSL source into a portable `.vneshader` bundle: SPIR-V bytecode, cross-compiled MSL/WGSL/HLSL output, and typed resource-binding reflection - all ready for direct consumption by [vnerhi](https://github.com/vertexnova/vnerhi). It is a core component of [VertexNova](https://github.com/vertexnova), a multi-backend graphics engine targeting Vulkan, Metal, WebGPU, and OpenGL/ES from a single source. ## Features -- **Full offline pipeline**: GLSL → SPIR-V → validate → reflect → cross-compile in one call +- **Full offline pipeline**: GLSL -> SPIR-V -> validate -> reflect -> cross-compile in one call - **Multi-target output**: MSL (Metal), WGSL (WebGPU), HLSL, GLSL ES from a single GLSL source - **Typed reflection**: per-binding resource metadata with typed optional backend slots (`MetalResourceSlot`, `WebGpuResourceSlot`) - **Artifact cache**: content-addressed file cache skips recompiling unchanged shaders -- **`.vneshader` bundle**: self-contained directory — SPIR-V + cross-compiled sources + binary reflection +- **`.vneshader` bundle**: self-contained directory - SPIR-V + cross-compiled sources + binary reflection - **JSON pipeline spec**: declare a full multi-stage pipeline in a `.pipeline.json` file; `include_paths` resolve relative to the spec file - **CLI tool**: `vnesc_shader_compiler` drives the full pipeline from the command line -- **Python batch tool**: `tools/compile_shaders.py` — parallel glob-aware wrapper around the CLI +- **Python batch tool**: `tools/compile_shaders.py` - parallel glob-aware wrapper around the CLI - **Extensible interfaces**: `IShaderFrontEnd`, `IShaderCrossCompiler`, `IShaderReflector`, `IShaderValidator` - **C++20, header-only public API**: single umbrella include `` @@ -73,11 +73,11 @@ target_link_libraries(your_target PRIVATE vnesc vnesc_glslang) ```cmake target_link_libraries(your_target PRIVATE vnesc # core: cache, factory, pipeline builder, bundle I/O - vnesc_glslang) # glslang GLSL → SPIR-V front-end (required for GLSL source) + vnesc_glslang) # glslang GLSL -> SPIR-V front-end (required for GLSL source) ``` `vnesc` carries the public headers, SPIRV-Cross cross-compiler, reflector, and bundle writer. -`vnesc_glslang` adds the GLSL → SPIR-V front-end; omit it only if you supply SPIR-V directly. +`vnesc_glslang` adds the GLSL -> SPIR-V front-end; omit it only if you supply SPIR-V directly. ### Single umbrella include @@ -136,11 +136,11 @@ Options: `-t` / `-BuildType` build type, `-a` / `-Action` action (`configure_and | Option | Default | Description | |--------|---------|-------------| -| `VNE_SC_GLSLANG` | `ON` | Enable glslang GLSL → SPIR-V front-end | +| `VNE_SC_GLSLANG` | `ON` | Enable glslang GLSL -> SPIR-V front-end | | `VNE_SC_JSON` | `ON` | Enable nlohmann/json for `.pipeline.json` specs and bundle manifests | | `VNE_SC_SPIRVTOOLS` | `OFF` | Enable SPIRV-Tools SPIR-V validator | -| `VNE_SC_TINT` | `OFF` | Enable Dawn Tint SPIR-V → WGSL cross-compiler | -| `VNE_SC_DXC` | `OFF` | Enable DXC HLSL → SPIR-V front-end (stub) | +| `VNE_SC_TINT` | `OFF` | Enable Dawn Tint SPIR-V -> WGSL cross-compiler | +| `VNE_SC_DXC` | `OFF` | Enable DXC HLSL -> SPIR-V front-end (stub) | | `VNE_SC_SLANG` | `OFF` | Enable Slang front-end (stub) | | `VNE_SC_TESTS` | `ON` | Build the test suite | | `VNE_SC_EXAMPLES` | `OFF` | Build example programs | @@ -168,7 +168,7 @@ auto result = factory->compile(req); if (!result.ok()) { // result.errors contains per-error messages } -// result.spirv — compiled SPIR-V words +// result.spirv - compiled SPIR-V words ``` ### Build a full pipeline (recommended) @@ -267,8 +267,8 @@ A compiled `.vneshader` is a directory: The binary reflection (`reflection.bin`) stores typed per-backend resource slots: ``` -metal: { buffer, texture, sampler } — present only when MSL was compiled -webgpu: { group, binding } — present only when WGSL was compiled +metal: { buffer, texture, sampler } - present only when MSL was compiled +webgpu: { group, binding } - present only when WGSL was compiled ``` ## Platform support @@ -281,7 +281,7 @@ webgpu: { group, binding } — present only when WGSL was compiled ## Documentation -- **Architecture doc**: [docs/vertexnova/sc/](docs/vertexnova/sc/) — pipeline design, reflection format, bundle layout +- **Architecture doc**: [docs/vertexnova/sc/](docs/vertexnova/sc/) - pipeline design, reflection format, bundle layout - **API docs**: Build with Doxygen: ```bash @@ -309,7 +309,7 @@ The **VERSION** file at the repo root is the single source of truth; CMake reads ## License -Apache License 2.0 — See [LICENSE](LICENSE) for details. +Apache License 2.0 - See [LICENSE](LICENSE) for details. --- diff --git a/cmake/CompilerOptions.cmake b/cmake/CompilerOptions.cmake index 8cb76ab..0cfecec 100644 --- a/cmake/CompilerOptions.cmake +++ b/cmake/CompilerOptions.cmake @@ -2,13 +2,20 @@ # Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License") # +# Author: Ajeet Singh Yadav +# Created: February 2026 +# +# Autodoc: yes +#============================================================================== + # cmake/CompilerOptions.cmake -# ──────────────────────────── # Platform detection, compile definitions, and optional sanitizer/coverage flags. # Included from the top-level CMakeLists.txt after project() and ProjectSetup. + +#============================================================================== +# Platform Detection # #============================================================================== -# ── Platform Detection ──────────────────────────────────────────────────────── if(DEFINED VNE_TARGET_PLATFORM) message(STATUS "[vnesc] Using manually specified target platform: ${VNE_TARGET_PLATFORM}") else() @@ -38,7 +45,10 @@ else() endif() message(STATUS "[vnesc] Platform: ${VNE_TARGET_PLATFORM}") -# ── Global Compile Definitions ──────────────────────────────────────────────── +#============================================================================== +# Global Compile Definitions # +#============================================================================== + if(VNE_TARGET_PLATFORM STREQUAL "macOS") add_compile_definitions(VNE_PLATFORM_MACOS) elseif(VNE_TARGET_PLATFORM STREQUAL "iOS") @@ -55,7 +65,10 @@ elseif(VNE_TARGET_PLATFORM STREQUAL "Web") add_compile_definitions(VNE_PLATFORM_WEB) endif() -# ── Code Coverage (gcov/lcov) ───────────────────────────────────────────────── +#============================================================================== +# Code Coverage (gcov/lcov) # +#============================================================================== + if(ENABLE_COVERAGE) include(FindCoverage OPTIONAL) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") @@ -64,12 +77,15 @@ if(ENABLE_COVERAGE) endif() endif() -# ── AddressSanitizer + UndefinedBehaviorSanitizer ──────────────────────────── +#============================================================================== +# AddressSanitizer + UndefinedBehaviorSanitizer # +#============================================================================== + if(ENABLE_ASAN) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT WIN32) add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) add_link_options(-fsanitize=address,undefined) else() - message(WARNING "ENABLE_ASAN is only supported with GCC/Clang on Linux/macOS — ignoring.") + message(WARNING "ENABLE_ASAN is only supported with GCC/Clang on Linux/macOS - ignoring.") endif() endif() diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index dd339c4..c4e8199 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -2,8 +2,13 @@ # Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License") # +# Author: Ajeet Singh Yadav +# Created: February 2026 +# +# Autodoc: yes +#============================================================================== + # cmake/Dependencies.cmake -# ───────────────────────── # External dependencies for vnesc via pure FetchContent. # # Each dep can be overridden with a local path: @@ -14,7 +19,6 @@ # -DVNE_SC_JSON_DIR=/path/to/nlohmann_json # # All KhronosGroup deps are pinned to vulkan-sdk-1.3.296.0. -#============================================================================== include(FetchContent) @@ -24,25 +28,36 @@ set(_vne_sc_sdk_tag "vulkan-sdk-1.3.296.0") set(_vne_sc_prev_pic "${CMAKE_POSITION_INDEPENDENT_CODE}") set(CMAKE_POSITION_INDEPENDENT_CODE ON) -# ── Helper: map VNE_SC__DIR to FETCHCONTENT_SOURCE_DIR_ ───────────── -# FetchContent skips the network entirely when FETCHCONTENT_SOURCE_DIR_ -# is set, making it equivalent to the old vendored-submodule path. +#============================================================================== +# FetchContent Path Overrides # +#============================================================================== + +# Map VNE_SC__DIR -> FETCHCONTENT_SOURCE_DIR_. +# FetchContent skips the network when FETCHCONTENT_SOURCE_DIR_ is set. +# Invalid or cleared overrides unset the FetchContent cache entry so a previous +# local path cannot stick across reconfigures. macro(_vne_sc_apply_override _fc_name _cache_var) - if(DEFINED ${_cache_var} AND NOT "${${_cache_var}}" STREQUAL "") - if(EXISTS "${${_cache_var}}/CMakeLists.txt") - string(TOUPPER "${_fc_name}" _fc_upper) - string(REPLACE "-" "_" _fc_upper "${_fc_upper}") - set("FETCHCONTENT_SOURCE_DIR_${_fc_upper}" "${${_cache_var}}" CACHE PATH "" FORCE) - message(STATUS "[vnesc] ${_fc_name}: local override → ${${_cache_var}}") - else() + string(TOUPPER "${_fc_name}" _fc_upper) + string(REPLACE "-" "_" _fc_upper "${_fc_upper}") + set(_fc_src_dir "FETCHCONTENT_SOURCE_DIR_${_fc_upper}") + if(DEFINED ${_cache_var} AND NOT "${${_cache_var}}" STREQUAL "" + AND EXISTS "${${_cache_var}}/CMakeLists.txt") + set("${_fc_src_dir}" "${${_cache_var}}" CACHE PATH "" FORCE) + message(STATUS "[vnesc] ${_fc_name}: local override -> ${${_cache_var}}") + else() + if(DEFINED ${_cache_var} AND NOT "${${_cache_var}}" STREQUAL "" + AND NOT EXISTS "${${_cache_var}}/CMakeLists.txt") message(WARNING "[vnesc] ${_cache_var}='${${_cache_var}}' has no CMakeLists.txt; ignored.") endif() + unset("${_fc_src_dir}" CACHE) endif() endmacro() -# ══════════════════════════════════════════════════════════════════════════════ -# SPIRV-Cross — always required (cross-compilation + reflection) -# ══════════════════════════════════════════════════════════════════════════════ +#============================================================================== +# SPIRV-Cross # +#============================================================================== + +# Always required (cross-compilation + reflection). set(SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS OFF CACHE BOOL "" FORCE) set(SPIRV_CROSS_ENABLE_TESTS OFF CACHE BOOL "" FORCE) set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "" FORCE) @@ -76,11 +91,12 @@ foreach(_t spirv-cross-core spirv-cross-glsl spirv-cross-msl) set_target_properties(${_t} PROPERTIES POSITION_INDEPENDENT_CODE TRUE) endif() endforeach() -message(STATUS "[vnesc] SPIRV-Cross → ${_vne_sc_spirvcross_src}") +message(STATUS "[vnesc] SPIRV-Cross -> ${_vne_sc_spirvcross_src}") + +#============================================================================== +# glslang + SPIRV-Headers # +#============================================================================== -# ══════════════════════════════════════════════════════════════════════════════ -# glslang + SPIRV-Headers (when VNE_SC_GLSLANG=ON) -# ══════════════════════════════════════════════════════════════════════════════ if(VNE_SC_GLSLANG) # SPIRV-Headers must be available before glslang configures. set(SPIRV_HEADERS_SKIP_INSTALL ON CACHE BOOL "" FORCE) @@ -93,7 +109,7 @@ if(VNE_SC_GLSLANG) FetchContent_GetProperties(SPIRV-Headers SOURCE_DIR _vne_sc_spirvhdr_src) # glslang locates SPIRV-Headers via this cache variable. set(SPIRV-Headers_SOURCE_DIR "${_vne_sc_spirvhdr_src}" CACHE PATH "" FORCE) - message(STATUS "[vnesc] SPIRV-Headers → ${_vne_sc_spirvhdr_src}") + message(STATUS "[vnesc] SPIRV-Headers -> ${_vne_sc_spirvhdr_src}") set(BUILD_EXTERNAL OFF CACHE BOOL "" FORCE) set(ENABLE_GLSLANG_BINARIES OFF CACHE BOOL "" FORCE) @@ -120,12 +136,13 @@ if(VNE_SC_GLSLANG) endif() endforeach() FetchContent_GetProperties(glslang SOURCE_DIR _vne_sc_glslang_src) - message(STATUS "[vnesc] glslang → ${_vne_sc_glslang_src}") + message(STATUS "[vnesc] glslang -> ${_vne_sc_glslang_src}") endif() -# ══════════════════════════════════════════════════════════════════════════════ -# SPIRV-Tools (when VNE_SC_SPIRVTOOLS=ON) -# ══════════════════════════════════════════════════════════════════════════════ +#============================================================================== +# SPIRV-Tools # +#============================================================================== + if(VNE_SC_SPIRVTOOLS) set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "" FORCE) set(SPIRV_SKIP_TESTS ON CACHE BOOL "" FORCE) @@ -146,12 +163,13 @@ if(VNE_SC_SPIRVTOOLS) endif() endforeach() FetchContent_GetProperties(SPIRV-Tools SOURCE_DIR _vne_sc_spirvtools_src) - message(STATUS "[vnesc] SPIRV-Tools → ${_vne_sc_spirvtools_src}") + message(STATUS "[vnesc] SPIRV-Tools -> ${_vne_sc_spirvtools_src}") endif() -# ══════════════════════════════════════════════════════════════════════════════ -# nlohmann/json (when VNE_SC_JSON=ON) -# ══════════════════════════════════════════════════════════════════════════════ +#============================================================================== +# nlohmann/json # +#============================================================================== + if(VNE_SC_JSON) set(JSON_BuildTests OFF CACHE BOOL "" FORCE) set(JSON_Install OFF CACHE BOOL "" FORCE) @@ -163,9 +181,13 @@ if(VNE_SC_JSON) GIT_SHALLOW TRUE) FetchContent_MakeAvailable(nlohmann_json) FetchContent_GetProperties(nlohmann_json SOURCE_DIR _vne_sc_json_src) - message(STATUS "[vnesc] nlohmann/json → ${_vne_sc_json_src}") + message(STATUS "[vnesc] nlohmann/json -> ${_vne_sc_json_src}") endif() +#============================================================================== +# Dawn / Tint Apple Workarounds # +#============================================================================== + # Fix abseil-cpp PR #1710 on Apple: CMAKE deduplicates -Xarch_* compile options, # so x86-only flags like -msse4.1 leak into arm64 builds. Patch targets only # (do not edit dawn-src; that dirties FetchContent's git checkout on reconfigure). @@ -203,9 +225,11 @@ function(_vne_sc_fix_dawn_abseil_randen_copts) endif() endfunction() -# ══════════════════════════════════════════════════════════════════════════════ -# Dawn / Tint (when VNE_SC_TINT=ON) — FetchContent only, no submodule -# ══════════════════════════════════════════════════════════════════════════════ +#============================================================================== +# Dawn / Tint (optional) # +#============================================================================== + +# FetchContent only - no submodule. When VNE_SC_TINT=ON. if(VNE_SC_TINT) set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) # Dawn can either bootstrap dependencies via Chromium's depot_tools/gclient @@ -239,7 +263,7 @@ if(VNE_SC_TINT) set(TINT_BUILD_FUZZERS OFF CACHE BOOL "" FORCE) set(TINT_BUILD_IR_FUZZER OFF CACHE BOOL "" FORCE) - # Build Dawn/Tint in Release even for Debug projects — it's a large compiler + # Build Dawn/Tint in Release even for Debug projects - it's a large compiler # library that has negligible benefit from debug symbols. set(_vne_sc_saved_build_type "${CMAKE_BUILD_TYPE}") set(CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE) @@ -252,12 +276,13 @@ if(VNE_SC_TINT) FetchContent_MakeAvailable(dawn) _vne_sc_fix_dawn_abseil_randen_copts() set(CMAKE_BUILD_TYPE "${_vne_sc_saved_build_type}" CACHE STRING "" FORCE) - message(STATUS "[vnesc] Dawn/Tint → FetchContent chromium/6723 (Release build)") + message(STATUS "[vnesc] Dawn/Tint -> FetchContent chromium/6723 (Release build)") endif() -# ══════════════════════════════════════════════════════════════════════════════ -# Helper functions consumed by src/CMakeLists.txt -# ══════════════════════════════════════════════════════════════════════════════ +#============================================================================== +# Link Helpers (for src/CMakeLists) # +#============================================================================== + function(vne_sc_link_spirvcross target) if(NOT VNE_SC_SPIRV_CROSS_INCLUDE_DIR OR NOT EXISTS "${VNE_SC_SPIRV_CROSS_INCLUDE_DIR}/spirv_cross.hpp") @@ -287,7 +312,10 @@ function(vne_sc_link_tint target) target_compile_definitions(${target} PRIVATE VNE_SC_TINT_ENABLED) endfunction() -# Restore PIC state. +#============================================================================== +# Restore PIC State # +#============================================================================== + if(DEFINED _vne_sc_prev_pic AND NOT _vne_sc_prev_pic STREQUAL "") set(CMAKE_POSITION_INDEPENDENT_CODE "${_vne_sc_prev_pic}") else() diff --git a/cmake/InstallRules.cmake b/cmake/InstallRules.cmake index 9e5dca5..707f89d 100644 --- a/cmake/InstallRules.cmake +++ b/cmake/InstallRules.cmake @@ -2,21 +2,29 @@ # Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License") # +# Author: Ajeet Singh Yadav +# Created: February 2026 +# +# Autodoc: yes +#============================================================================== + # cmake/InstallRules.cmake -# ───────────────────────── # Install rules for vnesc library targets and public headers. # Included from the bottom of the top-level CMakeLists.txt. -#============================================================================== include(GNUInstallDirs) +#============================================================================== +# Library Targets # +#============================================================================== + +# Install compiled archives only. INTERFACE targets (vnesc_interface, +# VnescWarnings, VnescBuildSettings) have no binary artifact and need an +# install(EXPORT) set; headers are installed via the DIRECTORY rule below. set(_vnesc_install_targets vnesc - vnesc_interface vnesc_spirvcross - vnesc_validator - VnescWarnings - VnescBuildSettings) + vnesc_validator) if(VNE_SC_GLSLANG AND TARGET vnesc_glslang) list(APPEND _vnesc_install_targets vnesc_glslang) @@ -32,19 +40,25 @@ install(TARGETS ${_vnesc_install_targets} INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} COMPONENT vnesc) +#============================================================================== +# Public Headers # +#============================================================================== + install(DIRECTORY include/vertexnova/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/vertexnova COMPONENT vnesc FILES_MATCHING PATTERN "*.h") -# ── CLI tool ────────────────────────────────────────────────────────────────── +#============================================================================== +# CLI Tools # +#============================================================================== + if(VNE_SC_TOOLS AND TARGET vnesc_shader_compiler) install(TARGETS vnesc_shader_compiler RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT vnesc_tools) endif() -# ── Python batch wrapper ────────────────────────────────────────────────────── if(VNE_SC_TOOLS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tools/compile_shaders.py") install(PROGRAMS "${CMAKE_CURRENT_SOURCE_DIR}/tools/compile_shaders.py" DESTINATION ${CMAKE_INSTALL_BINDIR} diff --git a/cmake/VnescShaders.cmake b/cmake/VnescShaders.cmake index 25c9b93..541847f 100644 --- a/cmake/VnescShaders.cmake +++ b/cmake/VnescShaders.cmake @@ -2,14 +2,20 @@ # Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License") # -# VnescShaders.cmake — CMake helper for build-time shader bundle compilation +# Author: Ajeet Singh Yadav +# Created: February 2026 +# +# Autodoc: yes +#============================================================================== + +# VnescShaders.cmake - CMake helper for build-time shader bundle compilation # # Usage: # include(VnescShaders) # # vne_compile_shaders( # OUTPUT_DIR # Where .vneshader directories are written -# MANIFESTS … # Paths to .manifest.json files +# MANIFESTS ... # Paths to .manifest.json files # [CACHE_DIR ] # Optional artifact cache directory # ) # @@ -17,6 +23,9 @@ # The bundle's bundle.header file is added as a source dependency so the # target rebuilds whenever a manifest or the vnesc_shader_compiler binary # changes. + +#============================================================================== +# vne_compile_shaders() # #============================================================================== function(vne_compile_shaders TARGET) @@ -56,7 +65,7 @@ function(vne_compile_shaders TARGET) VERBATIM ) - # Add the stamp as a source so CMake tracks the dependency + # Add the stamp as a source so CMake tracks the dependency. target_sources("${TARGET}" PRIVATE "${stamp_file}") endforeach() endfunction() diff --git a/docs/vertexnova/sc/offline_pipeline.md b/docs/vertexnova/sc/offline_pipeline.md index 8d2df38..431a38e 100644 --- a/docs/vertexnova/sc/offline_pipeline.md +++ b/docs/vertexnova/sc/offline_pipeline.md @@ -4,8 +4,8 @@ | CMake option | Default | Purpose | |--------------|---------|---------| -| `VNE_SC_GLSLANG` | ON | GLSL → SPIR-V (glslang) | -| `VNE_SC_TINT` | OFF | SPIR-V → WGSL (Dawn Tint; slow first configure) | +| `VNE_SC_GLSLANG` | ON | GLSL -> SPIR-V (glslang) | +| `VNE_SC_TINT` | OFF | SPIR-V -> WGSL (Dawn Tint; slow first configure) | | `VNE_SC_JSON` | ON | `manifest.json` + pipeline build JSON | | `VNE_SC_SPIRVTOOLS` | OFF | SPIR-V validation | diff --git a/docs/vertexnova/sc/separate_samplers.md b/docs/vertexnova/sc/separate_samplers.md index 60ae6f3..1fc5162 100644 --- a/docs/vertexnova/sc/separate_samplers.md +++ b/docs/vertexnova/sc/separate_samplers.md @@ -1,6 +1,6 @@ # GLSL separate samplers for WebGPU (WGSL) -vnesc produces WGSL via **Tint** (SPIR-V → WGSL). Tint does not support Vulkan-style **combined** `sampler2D` bindings in the WGSL output path. +vnesc produces WGSL via **Tint** (SPIR-V -> WGSL). Tint does not support Vulkan-style **combined** `sampler2D` bindings in the WGSL output path. ## Required GLSL style (all targets that include WGSL) @@ -16,7 +16,7 @@ void main() { ## Avoid for WGSL builds ```glsl -layout(set = 0, binding = 0) uniform sampler2D albedoTex; // combined — fails Tint → WGSL +layout(set = 0, binding = 0) uniform sampler2D albedoTex; // combined - fails Tint -> WGSL ``` SPIRV-Cross may still cross-compile combined samplers to MSL for Metal-only pipelines, but the engine should migrate to **separate texture + sampler** for one GLSL corpus across Vulkan, Metal, and WebGPU. @@ -28,4 +28,4 @@ When `CrossTarget::eWGSL` is in the build targets, vnesc emits: - `*.wgsl` per stage in the `.vneshader` bundle - `reflection.bin` with `wgpu_group` / `wgpu_binding` matching SPIR-V `(set, binding)` -vnerhi loads the bundle via `loadShaderBundle()` and uses baked reflection — no runtime WGSL parsing in production. +vnerhi loads the bundle via `loadShaderBundle()` and uses baked reflection - no runtime WGSL parsing in production. diff --git a/docs/vertexnova/template/diagrams/README.md b/docs/vertexnova/template/diagrams/README.md index 47589ce..a492d26 100644 --- a/docs/vertexnova/template/diagrams/README.md +++ b/docs/vertexnova/template/diagrams/README.md @@ -25,14 +25,14 @@ drawio -x -f png -o . . ### Option 2: draw.io Web 1. Open [app.diagrams.net](https://app.diagrams.net) -2. File → Open from → Device → select each `.drawio` file -3. File → Export as → PNG +2. File -> Open from -> Device -> select each `.drawio` file +3. File -> Export as -> PNG 4. Save to this `diagrams/` folder ### Files | Source | Output | Used in template.md | Contents | |--------|--------|---------------------|----------| -| context.drawio | context.png | Overview | C4 system context: Application → VneTemplate (get_version, hello) | +| context.drawio | context.png | Overview | C4 system context: Application -> VneTemplate (get_version, hello) | | api.drawio | api.png | API usage | API flow: get_version(), hello() | | architecture.drawio | architecture.png | Project layout | Folder structure and build flow | diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c9e0d3f..9b7932c 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,6 +1,14 @@ #============================================================================== # Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License") +# +# Author: Ajeet Singh Yadav +# Created: February 2026 +# +# Autodoc: yes #============================================================================== +# vnesc examples +# GLSL demos under examples/glsl/ (require VNE_SC_GLSLANG=ON). + add_subdirectory(glsl) diff --git a/examples/README.md b/examples/README.md index f173e35..4c08bec 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,11 +20,11 @@ Executables are under `build/bin/examples/glsl//`. | Example | Focus | |---------|--------| -| [glsl/01_hello_compiler](glsl/01_hello_compiler/) | Inline GLSL → MSL, `PipelineBuildDesc` | +| [glsl/01_hello_compiler](glsl/01_hello_compiler/) | Inline GLSL -> MSL, `PipelineBuildDesc` | | [glsl/02_triangle_bundle](glsl/02_triangle_bundle/) | File-based shaders, reflection dump, bundle I/O | | [glsl/03_textured_pipeline_spec](glsl/03_textured_pipeline_spec/) | `.pipeline.json` spec, `MetalBindingLayout` | ## Shared utilities -- `common/shader_loader.h` — load a shader file into a `std::string` -- `common/logging_guard.h` — configures `vne::log` console output; each example uses `LoggingGuard` and `VNE_LOG_*` for its own messages (library internals log through the same sink) +- `common/shader_loader.h` - load a shader file into a `std::string` +- `common/logging_guard.h` - configures `vne::log` console output; each example uses `LoggingGuard` and `VNE_LOG_*` for its own messages (library internals log through the same sink) diff --git a/examples/common/logging_guard.h b/examples/common/logging_guard.h index 7b3fe22..ec8de6c 100644 --- a/examples/common/logging_guard.h +++ b/examples/common/logging_guard.h @@ -12,7 +12,7 @@ CREATE_VNE_LOGGER_CATEGORY("vnesc.examples") namespace vne::sc::examples { /** - * @brief RAII guard — configures console logging for vnesc examples. + * @brief RAII guard - configures console logging for vnesc examples. * * Place at the start of `main()` so library log output (pipeline, glslang, etc.) * and example messages share the same sink. diff --git a/examples/glsl/01_hello_compiler/CMakeLists.txt b/examples/glsl/01_hello_compiler/CMakeLists.txt index 5c29794..7ce1a6e 100644 --- a/examples/glsl/01_hello_compiler/CMakeLists.txt +++ b/examples/glsl/01_hello_compiler/CMakeLists.txt @@ -1,5 +1,17 @@ #============================================================================== -# 01_hello_compiler — inline GLSL → MSL via PipelineBuildDesc +# Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License") +# +# Author: Ajeet Singh Yadav +# Created: February 2026 +# +# Autodoc: yes +#============================================================================== + +# 01_hello_compiler - inline GLSL -> MSL via PipelineBuildDesc + +#============================================================================== +# Build Executable # #============================================================================== add_executable(vnesc_example_01_hello_compiler main.cpp) @@ -8,6 +20,10 @@ target_include_directories(vnesc_example_01_hello_compiler PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/examples/common) +#============================================================================== +# Dependencies # +#============================================================================== + target_link_libraries(vnesc_example_01_hello_compiler PRIVATE vnesc vnesc_glslang) set_target_properties(vnesc_example_01_hello_compiler PROPERTIES diff --git a/examples/glsl/02_triangle_bundle/CMakeLists.txt b/examples/glsl/02_triangle_bundle/CMakeLists.txt index 54a7b23..b473b8e 100644 --- a/examples/glsl/02_triangle_bundle/CMakeLists.txt +++ b/examples/glsl/02_triangle_bundle/CMakeLists.txt @@ -1,5 +1,17 @@ #============================================================================== -# 02_triangle_bundle — file-based GLSL → MSL bundle + reflection dump +# Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License") +# +# Author: Ajeet Singh Yadav +# Created: February 2026 +# +# Autodoc: yes +#============================================================================== + +# 02_triangle_bundle - file-based GLSL -> MSL bundle + reflection dump + +#============================================================================== +# Build Executable # #============================================================================== add_executable(vnesc_example_02_triangle_bundle main.cpp) @@ -8,6 +20,10 @@ target_include_directories(vnesc_example_02_triangle_bundle PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/examples/common) +#============================================================================== +# Dependencies # +#============================================================================== + target_link_libraries(vnesc_example_02_triangle_bundle PRIVATE vnesc vnesc_glslang) set_target_properties(vnesc_example_02_triangle_bundle PROPERTIES diff --git a/examples/glsl/03_textured_pipeline_spec/CMakeLists.txt b/examples/glsl/03_textured_pipeline_spec/CMakeLists.txt index 472ad45..cec8f7f 100644 --- a/examples/glsl/03_textured_pipeline_spec/CMakeLists.txt +++ b/examples/glsl/03_textured_pipeline_spec/CMakeLists.txt @@ -1,18 +1,34 @@ #============================================================================== -# 03_textured_pipeline_spec — JSON pipeline spec → MSL bundle +# Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. +# Licensed under the Apache License, Version 2.0 (the "License") +# +# Author: Ajeet Singh Yadav +# Created: February 2026 +# +# Autodoc: yes #============================================================================== +# 03_textured_pipeline_spec - JSON pipeline spec -> MSL bundle + if(NOT VNE_SC_JSON) - message(STATUS "03_textured_pipeline_spec requires VNE_SC_JSON=ON — skipping") + message(STATUS "03_textured_pipeline_spec requires VNE_SC_JSON=ON - skipping") return() endif() +#============================================================================== +# Build Executable # +#============================================================================== + add_executable(vnesc_example_03_textured_pipeline_spec main.cpp) target_include_directories(vnesc_example_03_textured_pipeline_spec PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/examples/common) +#============================================================================== +# Dependencies # +#============================================================================== + target_link_libraries(vnesc_example_03_textured_pipeline_spec PRIVATE vnesc vnesc_glslang) set_target_properties(vnesc_example_03_textured_pipeline_spec PROPERTIES diff --git a/examples/glsl/CMakeLists.txt b/examples/glsl/CMakeLists.txt index 50887ef..c8ba943 100644 --- a/examples/glsl/CMakeLists.txt +++ b/examples/glsl/CMakeLists.txt @@ -1,13 +1,24 @@ #============================================================================== # Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License") +# +# Author: Ajeet Singh Yadav +# Created: February 2026 +# +# Autodoc: yes #============================================================================== +# GLSL examples (require VNE_SC_GLSLANG=ON) + if(NOT VNE_SC_GLSLANG) - message(STATUS "vnesc GLSL examples require VNE_SC_GLSLANG=ON — skipping") + message(STATUS "vnesc GLSL examples require VNE_SC_GLSLANG=ON - skipping") return() endif() +#============================================================================== +# Sample Programs # +#============================================================================== + add_subdirectory(01_hello_compiler) add_subdirectory(02_triangle_bundle) add_subdirectory(03_textured_pipeline_spec) diff --git a/include/vertexnova/sc/metal_binding_allocator.h b/include/vertexnova/sc/metal_binding_allocator.h new file mode 100644 index 0000000..27168bb --- /dev/null +++ b/include/vertexnova/sc/metal_binding_allocator.h @@ -0,0 +1,149 @@ +#pragma once +/* --------------------------------------------------------------------- + * Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License") + * + * Author: Ajeet Singh Yadav + * Created: July 2026 + * + * Autodoc: yes + * ---------------------------------------------------------------------- + */ + +/** + * @file metal_binding_allocator.h + * @brief Assigns MSL `[[buffer]]` / `[[texture]]` / `[[sampler]]` indices. + * @ingroup vne::sc + * + * @par Why this exists + * Metal's index spaces are small and fixed - **31 buffers**, **16 samplers** - while Vulkan's + * `(set, binding)` space is effectively unbounded. The previous mapping *flattened* the two: + * + * msl_buffer = buffer_base + set * flatten_stride + binding // base 16, stride 32 + * + * That overflows for set >= 1. This allocator packs indices **densely** instead: resources are + * enumerated in ascending `(set, binding)` order and handed consecutive Metal indices. + * + * @par Program-wide invariant + * Vertex and fragment (and any other stages in a program) must agree on Metal slots for the same + * logical `(set, binding)`. Build one allocator from the **union** of all stage SPIR-V modules and + * reuse it for every stage's reflect + MSL cross-compile. + * + * @note This header intentionally does **not** include SPIRV-Cross. Cross-compiler/reflector + * translation units that call @ref applyMslResourceBindings include SPIRV-Cross themselves. + */ + +#include "vertexnova/sc/sc_types.h" + +#include +#include +#include +#include +#include + +namespace spirv_cross { +class Compiler; +class CompilerMSL; +} // namespace spirv_cross + +namespace vne::sc { + +/** @brief Hard limits of Metal's argument tables. Exceeding one is an error, not a warning. */ +struct MetalIndexLimits { + //! vnerhi binds push constants at index 30, so 29 is the last usable descriptor buffer. + static constexpr std::uint32_t kMaxBufferIndex = 29; + static constexpr std::uint32_t kMaxSamplerIndex = 15; //!< Metal allows 16 samplers. + static constexpr std::uint32_t kMaxTextureIndex = 127; //!< Metal 2+ allows 128 textures. +}; + +/** + * @brief Dense, deterministic `(set, binding)` -> Metal index assignment. + * + * Prefer @ref fromProgram for multi-stage pipelines so all stages share one map. Stage-local + * construction from a single @c Compiler remains available for unit tests / legacy paths. + */ +class MetalBindingAllocator { + public: + using Key = std::pair; //!< (set, binding) + + MetalBindingAllocator(const spirv_cross::Compiler& compiler, const MetalBindingLayout& layout); + + /** + * @brief Program-wide map from the union of every stage's resources. + * + * Empty SPIR-V modules are skipped. Deterministic: sorted by `(set, binding)` across the union. + */ + [[nodiscard]] static MetalBindingAllocator fromProgram(const std::vector>& stage_spirv, + const MetalBindingLayout& layout); + + /** + * @brief Metal [[buffer(N)]] for @p set / @p binding. + * @returns The assigned index. Asserts if the key was not in this allocator's resource union. + */ + [[nodiscard]] std::uint32_t buffer(std::uint32_t set, std::uint32_t binding) const; + /** + * @brief Metal [[texture(N)]] for @p set / @p binding. + * @returns The assigned index. Asserts if the key was not in this allocator's resource union. + */ + [[nodiscard]] std::uint32_t texture(std::uint32_t set, std::uint32_t binding) const; + /** + * @brief Metal [[sampler(N)]] for @p set / @p binding. + * @returns The assigned index. Asserts if the key was not in this allocator's resource union. + */ + [[nodiscard]] std::uint32_t sampler(std::uint32_t set, std::uint32_t binding) const; + + /** + * @brief Empty when the allocation fits Metal's tables; otherwise names what overflowed. + */ + [[nodiscard]] std::string overflowError() const; + + /** + * @brief Stable fingerprint of the assigned dense maps (SPIR-V-derived). + * + * Two programs with the same logical resource union produce the same value. + * Artifact cache keys should use a source-level program fingerprint instead + * so lookup does not require compiling first. + */ + [[nodiscard]] std::uint64_t fingerprint() const noexcept; + + [[nodiscard]] const MetalBindingLayout& layout() const noexcept { return layout_; } + + private: + using IndexMap = std::map; + + MetalBindingAllocator() = default; + + void assignFromResourceLists(const std::vector& buffers, + const std::vector& textures, + const std::vector& combined_images, + const std::vector& separate_samplers); + + IndexMap buffer_index_; + IndexMap texture_index_; + IndexMap sampler_index_; + MetalBindingLayout layout_{}; +}; + +/** + * @brief Serializable view of a program-wide Metal binding map (alias for cache / request plumbing). + * + * Owned by @ref ShaderPipelineBuilder for the duration of a build; pointed to from + * @ref CrossCompileRequest and reflection calls. Vulkan/WebGPU ignore this entirely. + */ +using MetalProgramBindingSignature = MetalBindingAllocator; + +/** + * @brief Push @p alloc's indices into @p compiler as explicit @c MSLResourceBinding%s. + * + * Both the cross-compiler and the reflector call this before @c compile() so + * @c get_automatic_msl_resource_binding() returns the same indices in both. + * + * Defined in the SPIRV-Cross translation unit; callers must include SPIRV-Cross headers. + * @p stage is a @c spv::ExecutionModel value passed as @c std::uint32_t to keep this header + * free of SPIR-V headers. + */ +void applyMslResourceBindings(spirv_cross::CompilerMSL& compiler, + const MetalBindingAllocator& alloc, + std::uint32_t stage); + +} // namespace vne::sc diff --git a/include/vertexnova/sc/sc_result.h b/include/vertexnova/sc/sc_result.h index b9298a8..5cf096c 100644 --- a/include/vertexnova/sc/sc_result.h +++ b/include/vertexnova/sc/sc_result.h @@ -23,9 +23,7 @@ namespace vne::sc { -// ───────────────────────────────────────────────────────────────────────────── // Result code -// ───────────────────────────────────────────────────────────────────────────── /** * @brief Status code returned by every compiler pipeline operation. @@ -37,7 +35,7 @@ enum class ResultCode : int32_t { eSuccess = 0, ///< Operation completed without errors. eCompileWarnings = 1, ///< Compilation succeeded but generated warnings. eCompileFailed = -1, ///< Source code could not be compiled. - eCrossCompileFailed = -2, ///< SPIR-V → target cross-compilation failed. + eCrossCompileFailed = -2, ///< SPIR-V -> target cross-compilation failed. eReflectionFailed = -3, ///< SPIR-V reflection could not be completed. eValidationFailed = -4, ///< SPIR-V validation failed. eFileNotFound = -5, ///< Source file does not exist. @@ -51,9 +49,7 @@ inline bool succeeded(ResultCode code) noexcept { return static_cast(code) >= 0; } -// ───────────────────────────────────────────────────────────────────────────── // Result structs -// ───────────────────────────────────────────────────────────────────────────── /// Result of a source-to-SPIR-V compilation. struct CompileResult { @@ -73,10 +69,10 @@ struct WgpuBindingRemap { uint32_t binding = 0; ///< Actual `@binding(N)` in the emitted WGSL. }; -/// Result of a SPIR-V → shading-language cross-compilation. +/// Result of a SPIR-V -> shading-language cross-compilation. struct CrossCompileResult { ResultCode code = ResultCode::eCrossCompileFailed; - std::string source; ///< Cross-compiled source text (MSL, GLSL, …). + std::string source; ///< Cross-compiled source text (MSL, GLSL, ...). std::string entry_point; ///< Actual entry-point name in the output source. std::string error; /// For WGSL targets: actual @group/@binding in emitted WGSL (may differ from SPIR-V set/binding). diff --git a/include/vertexnova/sc/sc_types.h b/include/vertexnova/sc/sc_types.h index a6a0c47..21dfb95 100644 --- a/include/vertexnova/sc/sc_types.h +++ b/include/vertexnova/sc/sc_types.h @@ -29,9 +29,7 @@ inline constexpr uint32_t kDefaultMslVersion = 30000u; inline constexpr uint32_t kMetalBindingFlattenStrideDefault = 32u; inline constexpr uint32_t kMetalBindingBufferBaseDefault = 16u; -// ───────────────────────────────────────────────────────────────────────────── // Shader stage -// ───────────────────────────────────────────────────────────────────────────── /// Identifies a single programmable stage in the graphics or compute pipeline. enum class ShaderStage : uint8_t { @@ -66,20 +64,16 @@ inline bool operator&(ShaderStageFlags a, ShaderStageFlags b) noexcept { return (static_cast(a) & static_cast(b)) != 0; } -// ───────────────────────────────────────────────────────────────────────────── // Front-end selection -// ───────────────────────────────────────────────────────────────────────────── /// Selects the compiler front-end that translates source code to SPIR-V. enum class FrontEnd : uint8_t { - eGlslang = 0, ///< glslang — vulkan's GLSL (4.5) to SPIR-V (active). - eDxc = 1, ///< DXC — HLSL to SPIR-V (stub). - eSlang = 2, ///< Slang — Slang to SPIR-V (stub). + eGlslang = 0, ///< glslang - vulkan's GLSL (4.5) to SPIR-V (active). + eDxc = 1, ///< DXC - HLSL to SPIR-V (stub). + eSlang = 2, ///< Slang - Slang to SPIR-V (stub). }; -// ───────────────────────────────────────────────────────────────────────────── // Cross-compilation target -// ───────────────────────────────────────────────────────────────────────────── /// Shading-language target for SPIR-V cross-compilation. enum class CrossTarget : uint8_t { @@ -90,9 +84,7 @@ enum class CrossTarget : uint8_t { eHLSL = 4, ///< High-Level Shading Language. }; -// ───────────────────────────────────────────────────────────────────────────── // Source language -// ───────────────────────────────────────────────────────────────────────────── /// Source language of the shader being compiled. enum class SourceLang : uint8_t { @@ -103,20 +95,16 @@ enum class SourceLang : uint8_t { eSlang = 4, ///< Slang. }; -// ───────────────────────────────────────────────────────────────────────────── // Optimisation level -// ───────────────────────────────────────────────────────────────────────────── /// Controls SPIR-V optimisation during compilation. enum class OptLevel : uint8_t { - eNone = 0, ///< No optimisation — fastest compile, largest output. + eNone = 0, ///< No optimisation - fastest compile, largest output. eSize = 1, ///< Minimise SPIR-V binary size. ePerformance = 2, ///< Optimise for runtime performance (default). }; -// ───────────────────────────────────────────────────────────────────────────── // Compile request -// ───────────────────────────────────────────────────────────────────────────── /// A preprocessor macro injected into the shader source via a @c #define preamble. struct ShaderMacro { @@ -141,7 +129,7 @@ struct CompileRequest { std::vector include_dirs; ///< Directories searched for @c #include. bool debug_info = false; ///< Embed debug information in SPIR-V. bool validate = true; ///< Validate SPIR-V after generation. - uint32_t glsl_version = kDefaultGlslVersion; ///< GLSL version (e.g. 450 → @c #version 450). + uint32_t glsl_version = kDefaultGlslVersion; ///< GLSL version (e.g. 450 -> @c #version 450). }; /// Metal buffer-index layout configuration. @@ -154,21 +142,28 @@ struct MetalBindingLayout { }; /** - * @brief Describes a single SPIR-V → target cross-compilation job. + * @brief Describes a single SPIR-V -> target cross-compilation job. */ struct CrossCompileRequest { std::vector spirv; ///< Input SPIR-V binary. CrossTarget target = CrossTarget::eMSL; ShaderStage stage = ShaderStage::eVertex; - uint32_t msl_version = kDefaultMslVersion; ///< MSL version (e.g. 30000 → Metal 3.0). + uint32_t msl_version = kDefaultMslVersion; ///< MSL version (e.g. 30000 -> Metal 3.0). uint32_t glsl_version = kDefaultGlslVersion; bool fix_msl_fragment_signature = true; ///< Align fragment stage_in with vertex output. - MetalBindingLayout metal_layout; ///< MSL binding offsets (defaults match vnerhi). + MetalBindingLayout metal_layout; ///< Legacy flatten params (buffer_base still used by dense alloc). + /** + * @brief Optional program-wide dense Metal map (non-owning). + * + * When non-null and @c target is MSL, bindings are taken from this map instead of the + * flatten formula. Owned by @ref ShaderPipelineBuilder for the duration of @c build() + * only - do not retain, copy for later use, or process the request asynchronously while + * this pointer is set; it is dangling after @c build() returns. + */ + const class MetalBindingAllocator* metal_program_map = nullptr; }; -// ───────────────────────────────────────────────────────────────────────────── // Reflection types -// ───────────────────────────────────────────────────────────────────────────── /// Metal binding indices derived from SPIRV-Cross automatic slot assignment. struct MetalResourceSlot { diff --git a/include/vertexnova/sc/shader_artifact_cache.h b/include/vertexnova/sc/shader_artifact_cache.h index 81e4dfe..0f36744 100644 --- a/include/vertexnova/sc/shader_artifact_cache.h +++ b/include/vertexnova/sc/shader_artifact_cache.h @@ -43,16 +43,30 @@ class ShaderArtifactCache { explicit ShaderArtifactCache(std::string cache_dir); ~ShaderArtifactCache() = default; + /** + * @brief Program-wide fingerprint from compile inputs only (no SPIR-V). + * + * Hashes every stage's source/path/entry/stage/lang/macros plus + * @p metal_layout. Use this for MSL dense program maps so cache keys are + * available before front-end compile. Changing any stage invalidates all + * stage keys that embed this fingerprint. + */ + [[nodiscard]] static std::uint64_t makeProgramFingerprint(const std::vector& stages, + const MetalBindingLayout& metal_layout = {}); + /** * @brief Derives a deterministic cache key from a compile request. * * The key encodes source text, file path, entry point, stage, language, - * optimisation level, preprocessor macros, cross-compilation targets, and - * @ref MetalBindingLayout when @c CrossTarget::eMSL is among @p targets. + * optimisation level, preprocessor macros, cross-compilation targets, + * @ref MetalBindingLayout, and an optional program-wide fingerprint when + * @c CrossTarget::eMSL is among @p targets. Prefer + * @ref makeProgramFingerprint for that value so lookup can run before compile. */ static std::string makeKey(const CompileRequest& req, const std::vector& targets, - const MetalBindingLayout& metal_layout = {}); + const MetalBindingLayout& metal_layout = {}, + std::uint64_t metal_program_fingerprint = 0); /** * @brief Looks up a cached artifact by key. diff --git a/include/vertexnova/sc/shader_bundle.h b/include/vertexnova/sc/shader_bundle.h index 992f5f2..ec43749 100644 --- a/include/vertexnova/sc/shader_bundle.h +++ b/include/vertexnova/sc/shader_bundle.h @@ -32,7 +32,7 @@ struct BundleStageFiles { std::string entry_point; ///< SPIR-V / Vulkan entry point std::string spirv_file; std::string msl_file; - std::string msl_entry_point; ///< MSL entry (SPIRV-Cross may rename main→main0) + std::string msl_entry_point; ///< MSL entry (SPIRV-Cross may rename main->main0) std::string wgsl_file; std::string wgsl_entry_point; ///< WGSL entry point }; @@ -50,9 +50,9 @@ struct ShaderBundleHeader { * @brief Writes a @c .vneshader directory from a compiled @ref ShaderArtifact. * * Creates: - * - @c bundle.header — binary index for vnerhi - * - @c reflection.bin — @ref ProgramReflection - * - @c manifest.json — human-readable index (when JSON enabled) + * - @c bundle.header - binary index for vnerhi + * - @c reflection.bin - @ref ProgramReflection + * - @c manifest.json - human-readable index (when JSON enabled) * - Per-stage @c .spv / @c .msl / @c .wgsl files */ bool writeShaderBundle(const ShaderArtifact& artifact, const std::filesystem::path& bundle_dir); diff --git a/include/vertexnova/sc/shader_compiler_factory.h b/include/vertexnova/sc/shader_compiler_factory.h index bd7846e..a6144d7 100644 --- a/include/vertexnova/sc/shader_compiler_factory.h +++ b/include/vertexnova/sc/shader_compiler_factory.h @@ -44,7 +44,7 @@ class ShaderCompilerFactory { /// Creates a front-end for the given source language. static std::shared_ptr createFrontEnd(SourceLang lang); - /// Creates the SPIR-V → target cross-compiler (dispatches MSL/GLSL to SPIRV-Cross, WGSL to Tint). + /// Creates the SPIR-V -> target cross-compiler (dispatches MSL/GLSL to SPIRV-Cross, WGSL to Tint). static std::shared_ptr createCrossCompiler(); /// Creates the SPIRV-Cross cross-compiler only. diff --git a/include/vertexnova/sc/shader_cross_compiler.h b/include/vertexnova/sc/shader_cross_compiler.h index 8471240..0def07d 100644 --- a/include/vertexnova/sc/shader_cross_compiler.h +++ b/include/vertexnova/sc/shader_cross_compiler.h @@ -21,7 +21,7 @@ namespace vne::sc { /** - * @brief Translates a SPIR-V binary into a target shading language (MSL, GLSL, …). + * @brief Translates a SPIR-V binary into a target shading language (MSL, GLSL, ...). */ class IShaderCrossCompiler { public: diff --git a/include/vertexnova/sc/shader_pipeline_builder.h b/include/vertexnova/sc/shader_pipeline_builder.h index 12fda3f..f1d6b26 100644 --- a/include/vertexnova/sc/shader_pipeline_builder.h +++ b/include/vertexnova/sc/shader_pipeline_builder.h @@ -30,12 +30,10 @@ class IShaderCrossCompiler; class IShaderReflector; class IShaderValidator; -// ───────────────────────────────────────────────────────────────────────────── // Pipeline descriptor -// ───────────────────────────────────────────────────────────────────────────── /** - * @brief Describes a complete pipeline build job — all stages and targets. + * @brief Describes a complete pipeline build job - all stages and targets. */ struct PipelineBuildDesc { std::string name; @@ -45,6 +43,13 @@ struct PipelineBuildDesc { bool use_cache = true; ///< Enable the file-based artifact cache. std::string cache_dir; ///< Cache root directory; empty disables caching. MetalBindingLayout metal_layout; ///< Propagated to cross-compiler and reflector. + /** + * @brief Temporary rollback switch for Metal program-wide dense slot maps. + * + * When true (default), MSL builds compute one shared @ref MetalBindingAllocator from the + * union of all stages. Set false to restore stage-local dense allocation (emergency only). + */ + bool metal_dense_program_map = true; }; /** @@ -58,21 +63,20 @@ struct PipelineBuildResult { [[nodiscard]] bool ok() const noexcept { return succeeded(code); } }; -// ───────────────────────────────────────────────────────────────────────────── // Pipeline builder interface -// ───────────────────────────────────────────────────────────────────────────── /** * @brief Drives the full offline compilation sequence: - * source → SPIR-V → validate → reflect → cross-compile → @ref ShaderArtifact. + * source -> SPIR-V -> validate -> reflect -> cross-compile -> @ref ShaderArtifact. + * + * Per stage the pipeline is: + * -# Cache lookup (key includes a source-level program fingerprint for MSL dense maps). + * -# On miss: @ref IShaderFrontEnd::compile, optional validate, reflect, cross-compile, store. + * -# On hit: reuse the cached @ref StageArtifact (no front-end compile). * - * The pipeline is: - * -# Cache lookup — return early on hit. - * -# @ref IShaderFrontEnd::compile — source → SPIR-V. - * -# @ref IShaderValidator::validate — optional SPIR-V validation. - * -# @ref IShaderReflector::reflect — SPIR-V → typed @ref StageReflection. - * -# @ref IShaderCrossCompiler::crossCompile — SPIR-V → target source (one per target). - * -# Cache store. + * When any stage misses and MSL dense program maps are enabled, one shared + * @ref MetalBindingAllocator is built from the union of all stage SPIR-V + * (cached and freshly compiled) before reflect / cross-compile of the misses. */ class IShaderPipelineBuilder { public: @@ -84,9 +88,7 @@ class IShaderPipelineBuilder { virtual PipelineBuildResult build(const PipelineBuildDesc& desc) = 0; }; -// ───────────────────────────────────────────────────────────────────────────── // Concrete implementation -// ───────────────────────────────────────────────────────────────────────────── /** * @brief Default @ref IShaderPipelineBuilder returned by @ref ShaderCompilerFactory. diff --git a/include/vertexnova/sc/shader_pipeline_spec.h b/include/vertexnova/sc/shader_pipeline_spec.h index 211322b..2c82f13 100644 --- a/include/vertexnova/sc/shader_pipeline_spec.h +++ b/include/vertexnova/sc/shader_pipeline_spec.h @@ -35,12 +35,12 @@ struct ShaderStageSpec { }; /** - * @brief One compile variant of a @ref ShaderPipelineSpec — the same stage + * @brief One compile variant of a @ref ShaderPipelineSpec - the same stage * files recompiled with a different set of preprocessor @c #define macros, * producing a separate, sibling-named output bundle. * * An empty @c name denotes the base/default variant (no defines, no bundle - * suffix) — every manifest without an explicit "variants" array behaves as + * suffix) - every manifest without an explicit "variants" array behaves as * if it had exactly one variant with an empty name, preserving prior output * layout and naming for every existing manifest. */ diff --git a/include/vertexnova/sc/shader_reflector.h b/include/vertexnova/sc/shader_reflector.h index 48e9f1c..73b28ff 100644 --- a/include/vertexnova/sc/shader_reflector.h +++ b/include/vertexnova/sc/shader_reflector.h @@ -23,6 +23,8 @@ namespace vne::sc { +class MetalBindingAllocator; + /** * @brief Reflects a SPIR-V binary and returns typed binding metadata. * @@ -38,12 +40,14 @@ class IShaderReflector { * @brief Reflects @p spirv and returns typed stage binding metadata. * @param spirv SPIR-V binary words. * @param stage Pipeline stage the module belongs to. + * @param metal_program_map Optional program-wide dense Metal slots (MSL only). * @returns A @ref ReflectResult whose @c reflection is populated on success. */ virtual ReflectResult reflect(const std::vector& spirv, ShaderStage stage, const std::vector& targets = {}, - MetalBindingLayout metal_layout = {}) = 0; + MetalBindingLayout metal_layout = {}, + const MetalBindingAllocator* metal_program_map = nullptr) = 0; }; } // namespace vne::sc diff --git a/include/vertexnova/sc/shader_validator.h b/include/vertexnova/sc/shader_validator.h index ec03162..3ac88b7 100644 --- a/include/vertexnova/sc/shader_validator.h +++ b/include/vertexnova/sc/shader_validator.h @@ -27,7 +27,7 @@ namespace vne::sc { * @brief Validates a SPIR-V binary against the Vulkan rules. * * A no-op implementation is used when the backing validation library is not - * compiled in — it always returns @ref ResultCode::eSuccess. + * compiled in - it always returns @ref ResultCode::eSuccess. */ class IShaderValidator { public: diff --git a/include/vertexnova/sc/vnesc.h b/include/vertexnova/sc/vnesc.h index 959d4e1..97f5d23 100644 --- a/include/vertexnova/sc/vnesc.h +++ b/include/vertexnova/sc/vnesc.h @@ -12,12 +12,13 @@ /** * @file vnesc.h - * @brief Umbrella header — includes the complete vnesc public API. + * @brief Umbrella header - includes the complete vnesc public API. * For most use cases, including this single file is sufficient. */ #include "sc_types.h" #include "sc_result.h" +#include "metal_binding_allocator.h" #include "shader_artifact.h" #include "shader_frontend.h" #include "shader_cross_compiler.h" diff --git a/scripts/README.md b/scripts/README.md index 2713893..b6d0f0b 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -7,7 +7,7 @@ vnesc is a static shader-compiler library. Scripts pass `VNE_SC_*` CMake options ## Quick start ```bash -# Linux / macOS — dev build with tests (default) +# Linux / macOS - dev build with tests (default) ./scripts/build_linux.sh --dev -a test ./scripts/build_macos.sh --dev -a test @@ -79,4 +79,4 @@ Run all Windows scripts from a **Visual Studio Developer Command Prompt**. ## Other utilities -- `clang_formatter.py` — format `src/`, `include/`, `tests/` with project `.clang-format` +- `clang_formatter.py` - format `src/`, `include/`, `tests/` with project `.clang-format` diff --git a/scripts/build_linux.sh b/scripts/build_linux.sh index 71551f8..e3eba44 100755 --- a/scripts/build_linux.sh +++ b/scripts/build_linux.sh @@ -6,7 +6,7 @@ # Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License") # -# Builds vnesc on Linux (GCC or Clang). Desktop only — no mobile/web targets. +# Builds vnesc on Linux (GCC or Clang). Desktop only - no mobile/web targets. #============================================================================== set -e diff --git a/scripts/clang_formatter.py b/scripts/clang_formatter.py index e4280b4..89630f2 100755 --- a/scripts/clang_formatter.py +++ b/scripts/clang_formatter.py @@ -181,7 +181,7 @@ def main(): if p.is_dir(): source_files.extend(find_source_files(p)) source_files = sorted(set(source_files)) - print(f"Target: CI dirs — {len(source_files)} files") + print(f"Target: CI dirs - {len(source_files)} files") else: target_folder = (project_root / folder_arg).resolve() if not target_folder.is_dir(): diff --git a/scripts/generate-docs.sh b/scripts/generate-docs.sh index 4d72370..93b50df 100755 --- a/scripts/generate-docs.sh +++ b/scripts/generate-docs.sh @@ -40,7 +40,7 @@ check_prerequisites() { fi if ! command -v doxygen &> /dev/null; then - log_warning "Doxygen not found — API docs will be skipped." + log_warning "Doxygen not found - API docs will be skipped." DOXYGEN_AVAILABLE=false else DOXYGEN_AVAILABLE=true diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0e81db4..5b903af 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,16 +2,24 @@ # Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License") # -# vnesc library targets +# Author: Ajeet Singh Yadav +# Created: February 2026 # -# vnesc_interface — public headers; no compiled code -# vnesc_glslang — glslang GLSL → SPIR-V front-end (conditional) -# vnesc_spirvcross — SPIRV-Cross cross-compiler + reflector (always built) -# vnesc_validator — SPIR-V validator (SPIRV-Tools or no-op) -# vnesc — core: cache, factory, pipeline builder +# Autodoc: yes +#============================================================================== + +# vnesc library targets: +# vnesc_interface - public headers; no compiled code +# vnesc_glslang - glslang GLSL -> SPIR-V front-end (conditional) +# vnesc_spirvcross - SPIRV-Cross cross-compiler + reflector (always built) +# vnesc_validator - SPIR-V validator (SPIRV-Tools or no-op) +# vnesc_tint - Dawn Tint SPIR-V -> WGSL (optional) +# vnesc - core: cache, factory, pipeline builder, bundle + +#============================================================================== +# Interface Library # #============================================================================== -# ── Interface library (public headers only) ─────────────────────────────────── add_library(vnesc_interface INTERFACE) target_include_directories(vnesc_interface INTERFACE $ @@ -23,7 +31,10 @@ target_link_libraries(vnesc_interface INTERFACE target_compile_features(vnesc_interface INTERFACE cxx_std_20) add_library(vne::sc::interface ALIAS vnesc_interface) -# ── glslang front-end (GLSL → SPIR-V) ──────────────────────────────────────── +#============================================================================== +# glslang Front-End (optional) # +#============================================================================== + if(VNE_SC_GLSLANG) add_library(vnesc_glslang STATIC vertexnova/sc/frontend/glslang_frontend.cpp) @@ -46,13 +57,17 @@ if(VNE_SC_GLSLANG) add_library(vne::sc::glslang ALIAS vnesc_glslang) endif() -# ── SPIRV-Cross cross-compiler + reflector (SPIRV-Cross only; not SPIRV-Headers) ─ +#============================================================================== +# SPIRV-Cross Cross-Compiler + Reflector # +#============================================================================== + if(NOT TARGET spirv-cross-core) message(FATAL_ERROR "vnesc_spirvcross requires SPIRV-Cross (target spirv-cross-core). " "Set VNE_SC_SPIRV_CROSS_DIR or allow FetchContent in cmake/Dependencies.cmake.") endif() add_library(vnesc_spirvcross STATIC + vertexnova/sc/crosscompiler/metal_binding_allocator.cpp vertexnova/sc/crosscompiler/spirvcross_cross_compiler.cpp vertexnova/sc/reflector/spirvcross_reflector.cpp) target_include_directories(vnesc_spirvcross PRIVATE ${PROJECT_SOURCE_DIR}/src) @@ -61,7 +76,10 @@ target_link_libraries(vnesc_spirvcross PUBLIC vnesc_interface) set_target_properties(vnesc_spirvcross PROPERTIES POSITION_INDEPENDENT_CODE TRUE) add_library(vne::sc::spirvcross ALIAS vnesc_spirvcross) -# ── Validator (SPIRV-Tools when available, no-op otherwise) ─────────────────── +#============================================================================== +# Validator # +#============================================================================== + if(VNE_SC_SPIRVTOOLS) add_library(vnesc_validator STATIC vertexnova/sc/validator/spirvtools_validator.cpp) @@ -76,7 +94,10 @@ target_include_directories(vnesc_validator PRIVATE ${PROJECT_SOURCE_DIR}/src) set_target_properties(vnesc_validator PROPERTIES POSITION_INDEPENDENT_CODE TRUE) add_library(vne::sc::validator ALIAS vnesc_validator) -# ── Tint WGSL cross-compiler (optional) ─────────────────────────────────────── +#============================================================================== +# Tint WGSL Cross-Compiler (optional) # +#============================================================================== + if(VNE_SC_TINT) add_library(vnesc_tint STATIC vertexnova/sc/crosscompiler/tint_cross_compiler.cpp) @@ -87,7 +108,10 @@ if(VNE_SC_TINT) add_library(vne::sc::tint ALIAS vnesc_tint) endif() -# ── Core library (cache, factory, pipeline builder, bundle) ───────────────── +#============================================================================== +# vne::sc (core STATIC library) # +#============================================================================== + add_library(vnesc STATIC vertexnova/sc/shader_artifact_cache.cpp vertexnova/sc/shader_reflection_binary.cpp diff --git a/src/vertexnova/sc/crosscompiler/metal_binding_allocator.cpp b/src/vertexnova/sc/crosscompiler/metal_binding_allocator.cpp new file mode 100644 index 0000000..0970d06 --- /dev/null +++ b/src/vertexnova/sc/crosscompiler/metal_binding_allocator.cpp @@ -0,0 +1,290 @@ +/* --------------------------------------------------------------------- + * Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License") + * + * Author: Ajeet Singh Yadav + * Created: July 2026 + * + * Autodoc: yes + * ---------------------------------------------------------------------- + */ + +#include "vertexnova/sc/metal_binding_allocator.h" + +#include +#include + +#include +#include +#include + +namespace vne::sc { + +namespace { + +using Key = MetalBindingAllocator::Key; + +constexpr std::uint64_t kFnv1a64Offset = 14695981039346656037ULL; +constexpr std::uint64_t kFnv1a64Prime = 1099511628211ULL; + +void hashMix(std::uint64_t& h, std::uint64_t v) noexcept { + h ^= v; + h *= kFnv1a64Prime; +} + +void collect(const spirv_cross::Compiler& compiler, + const spirv_cross::SmallVector& resources, + std::vector& out) { + for (const auto& r : resources) { + const std::uint32_t set = compiler.has_decoration(r.id, spv::DecorationDescriptorSet) + ? compiler.get_decoration(r.id, spv::DecorationDescriptorSet) + : 0U; + const std::uint32_t binding = compiler.has_decoration(r.id, spv::DecorationBinding) + ? compiler.get_decoration(r.id, spv::DecorationBinding) + : 0U; + out.emplace_back(set, binding); + } +} + +void collectAll(const spirv_cross::Compiler& compiler, + std::vector& buffers, + std::vector& textures, + std::vector& combined, + std::vector& separate) { + const spirv_cross::ShaderResources res = compiler.get_shader_resources(); + collect(compiler, res.uniform_buffers, buffers); + collect(compiler, res.storage_buffers, buffers); + collect(compiler, res.sampled_images, textures); + collect(compiler, res.separate_images, textures); + collect(compiler, res.storage_images, textures); + collect(compiler, res.sampled_images, combined); + collect(compiler, res.separate_samplers, separate); +} + +void uniqueSorted(std::vector& keys) { + std::sort(keys.begin(), keys.end()); + keys.erase(std::unique(keys.begin(), keys.end()), keys.end()); +} + +void assign(std::vector& keys, const std::uint32_t base, std::map& out) { + uniqueSorted(keys); + std::uint32_t next = base; + for (const Key& k : keys) { + out.emplace(k, next++); + } +} + +[[nodiscard]] std::uint32_t lookup(const std::map& m, + const std::uint32_t set, + const std::uint32_t binding) { + const auto it = m.find(Key{set, binding}); + if (it == m.end()) { + assert(false && "MetalBindingAllocator: missing (set, binding) in allocation map"); + return 0U; + } + return it->second; +} + +[[nodiscard]] std::uint32_t highest(const std::map& m) { + if (m.empty()) { + return 0U; + } + std::uint32_t hi = 0U; + for (const auto& [_, idx] : m) { + hi = std::max(hi, idx); + } + return hi; +} + +void hashMap(std::uint64_t& h, const std::map& m) { + for (const auto& [key, idx] : m) { + hashMix(h, key.first); + hashMix(h, key.second); + hashMix(h, idx); + } +} + +} // namespace + +void MetalBindingAllocator::assignFromResourceLists(const std::vector& buffers_in, + const std::vector& textures_in, + const std::vector& combined_in, + const std::vector& separate_in) { + buffer_index_.clear(); + texture_index_.clear(); + sampler_index_.clear(); + + std::vector buffers = buffers_in; + assign(buffers, layout_.buffer_base, buffer_index_); + + std::vector textures = textures_in; + assign(textures, 0U, texture_index_); + + // Combined image-samplers: pin sampler index to the texture index so + // fixCombinedSamplerIndices cannot desync the pair. + std::vector combined = combined_in; + uniqueSorted(combined); + + std::vector reserved; + reserved.reserve(combined.size()); + for (const Key& k : combined) { + const std::uint32_t idx = lookup(texture_index_, k.first, k.second); + sampler_index_.emplace(k, idx); + reserved.push_back(idx); + } + std::sort(reserved.begin(), reserved.end()); + + std::vector separate = separate_in; + uniqueSorted(separate); + + std::uint32_t next = 0U; + for (const Key& k : separate) { + while (std::binary_search(reserved.begin(), reserved.end(), next)) { + ++next; + } + sampler_index_.emplace(k, next++); + } +} + +MetalBindingAllocator::MetalBindingAllocator(const spirv_cross::Compiler& compiler, const MetalBindingLayout& layout) + : layout_(layout) { + std::vector buffers; + std::vector textures; + std::vector combined; + std::vector separate; + collectAll(compiler, buffers, textures, combined, separate); + assignFromResourceLists(buffers, textures, combined, separate); +} + +MetalBindingAllocator MetalBindingAllocator::fromProgram(const std::vector>& stage_spirv, + const MetalBindingLayout& layout) { + MetalBindingAllocator alloc; + alloc.layout_ = layout; + + std::vector buffers; + std::vector textures; + std::vector combined; + std::vector separate; + + for (const auto& spirv : stage_spirv) { + if (spirv.empty()) { + continue; + } + spirv_cross::Compiler compiler(spirv.data(), spirv.size()); + collectAll(compiler, buffers, textures, combined, separate); + } + + alloc.assignFromResourceLists(buffers, textures, combined, separate); + return alloc; +} + +std::uint32_t MetalBindingAllocator::buffer(const std::uint32_t set, const std::uint32_t binding) const { + return lookup(buffer_index_, set, binding); +} + +std::uint32_t MetalBindingAllocator::texture(const std::uint32_t set, const std::uint32_t binding) const { + return lookup(texture_index_, set, binding); +} + +std::uint32_t MetalBindingAllocator::sampler(const std::uint32_t set, const std::uint32_t binding) const { + return lookup(sampler_index_, set, binding); +} + +std::string MetalBindingAllocator::overflowError() const { + if (const std::uint32_t hi = highest(buffer_index_); hi > MetalIndexLimits::kMaxBufferIndex) { + return "Metal buffer index " + std::to_string(hi) + " exceeds the maximum of " + + std::to_string(MetalIndexLimits::kMaxBufferIndex) + " (" + std::to_string(buffer_index_.size()) + + " buffers in this program). Metal has 31 buffer slots: index 30 is reserved for push " + "constants, and the slots below buffer_base for vertex buffers."; + } + if (const std::uint32_t hi = highest(sampler_index_); hi > MetalIndexLimits::kMaxSamplerIndex) { + return "Metal sampler index " + std::to_string(hi) + " exceeds the maximum of " + + std::to_string(MetalIndexLimits::kMaxSamplerIndex) + " (" + std::to_string(sampler_index_.size()) + + " samplers in this program; Metal allows 16)."; + } + if (const std::uint32_t hi = highest(texture_index_); hi > MetalIndexLimits::kMaxTextureIndex) { + return "Metal texture index " + std::to_string(hi) + " exceeds the maximum of " + + std::to_string(MetalIndexLimits::kMaxTextureIndex) + "."; + } + return {}; +} + +std::uint64_t MetalBindingAllocator::fingerprint() const noexcept { + std::uint64_t h = kFnv1a64Offset; + hashMix(h, layout_.buffer_base); + hashMix(h, layout_.flatten_stride); + hashMap(h, buffer_index_); + hashMap(h, texture_index_); + hashMap(h, sampler_index_); + return h; +} + +void applyMslResourceBindings(spirv_cross::CompilerMSL& compiler, + const MetalBindingAllocator& alloc, + const std::uint32_t stage) { + const auto exec_model = static_cast(stage); + const spirv_cross::ShaderResources res = compiler.get_shader_resources(); + + const auto decorations = [&compiler](const spirv_cross::Resource& r) { + const std::uint32_t set = compiler.has_decoration(r.id, spv::DecorationDescriptorSet) + ? compiler.get_decoration(r.id, spv::DecorationDescriptorSet) + : 0U; + const std::uint32_t binding = compiler.has_decoration(r.id, spv::DecorationBinding) + ? compiler.get_decoration(r.id, spv::DecorationBinding) + : 0U; + return std::pair{set, binding}; + }; + + const auto bindBuffers = [&](const spirv_cross::SmallVector& list) { + for (const auto& r : list) { + const auto [set, binding] = decorations(r); + spirv_cross::MSLResourceBinding mb{}; + mb.stage = exec_model; + mb.desc_set = set; + mb.binding = binding; + mb.msl_buffer = alloc.buffer(set, binding); + compiler.add_msl_resource_binding(mb); + } + }; + const auto bindTextures = [&](const spirv_cross::SmallVector& list) { + for (const auto& r : list) { + const auto [set, binding] = decorations(r); + spirv_cross::MSLResourceBinding mb{}; + mb.stage = exec_model; + mb.desc_set = set; + mb.binding = binding; + mb.msl_texture = alloc.texture(set, binding); + compiler.add_msl_resource_binding(mb); + } + }; + const auto bindSamplers = [&](const spirv_cross::SmallVector& list) { + for (const auto& r : list) { + const auto [set, binding] = decorations(r); + spirv_cross::MSLResourceBinding mb{}; + mb.stage = exec_model; + mb.desc_set = set; + mb.binding = binding; + mb.msl_sampler = alloc.sampler(set, binding); + compiler.add_msl_resource_binding(mb); + } + }; + + for (const auto& r : res.sampled_images) { + const auto [set, binding] = decorations(r); + spirv_cross::MSLResourceBinding mb{}; + mb.stage = exec_model; + mb.desc_set = set; + mb.binding = binding; + mb.msl_texture = alloc.texture(set, binding); + mb.msl_sampler = alloc.sampler(set, binding); + compiler.add_msl_resource_binding(mb); + } + + bindBuffers(res.uniform_buffers); + bindBuffers(res.storage_buffers); + bindTextures(res.separate_images); + bindTextures(res.storage_images); + bindSamplers(res.separate_samplers); +} + +} // namespace vne::sc diff --git a/src/vertexnova/sc/crosscompiler/spirvcross_cross_compiler.cpp b/src/vertexnova/sc/crosscompiler/spirvcross_cross_compiler.cpp index 459c647..b4f899d 100644 --- a/src/vertexnova/sc/crosscompiler/spirvcross_cross_compiler.cpp +++ b/src/vertexnova/sc/crosscompiler/spirvcross_cross_compiler.cpp @@ -11,12 +11,15 @@ #include "spirvcross_cross_compiler.h" +#include "vertexnova/sc/metal_binding_allocator.h" + #include "spirv_cross.hpp" #include "spirv_glsl.hpp" #include "spirv_msl.hpp" #include "vertexnova/logging/logging.h" +#include #include #include #include @@ -25,17 +28,7 @@ CREATE_VNE_LOGGER_CATEGORY("vne.sc.crosscompiler") namespace { -inline uint32_t metalBufferSlot(uint32_t set, uint32_t binding, const vne::sc::MetalBindingLayout& layout) noexcept { - return layout.buffer_base + (set * layout.flatten_stride + binding); -} -inline uint32_t metalTextureSlot(uint32_t set, uint32_t binding, const vne::sc::MetalBindingLayout& layout) noexcept { - return set * layout.flatten_stride + binding; -} -inline uint32_t metalSamplerSlot(uint32_t set, uint32_t binding, const vne::sc::MetalBindingLayout& layout) noexcept { - return set * layout.flatten_stride + binding; -} - -// ── Sampler index post-processing ───────────────────────────────────────────── +// Sampler index post-processing // SPIRV-Cross doesn't always correctly remap sampler indices when splitting combined // samplers. Fix by making each sampler's [[sampler(N)]] match its paired texture's // [[texture(N)]] index. @@ -61,12 +54,12 @@ void fixCombinedSamplerIndices(std::string& msl) { } } -// ── FixMSLFragmentSignature ──────────────────────────────────────────────────── +// FixMSLFragmentSignature // Ensure the fragment shader references a valid stage_in struct. // Ports the hard-won FixMSLFragmentSignature logic from the old spirv_cross_compiler.cpp. void fixMSLFragmentSignature(std::string& frag_msl, const std::string& vert_msl) { if (frag_msl.find("[[stage_in]]") != std::string::npos) { - // stage_in already present — verify the referenced struct exists + // stage_in already present - verify the referenced struct exists std::regex frag_fn_re(R"(fragment\s+(\w+)\s+(\w+)\s*\(([^)]*)\))"); std::smatch frag_fn_m; if (std::regex_search(frag_msl, frag_fn_m, frag_fn_re)) { @@ -76,7 +69,7 @@ void fixMSLFragmentSignature(std::string& frag_msl, const std::string& vert_msl) if (std::regex_search(params, si_m, si_re)) { const std::string si_type = si_m[1].str(); if (frag_msl.find("struct " + si_type) != std::string::npos) { - return; // struct exists — nothing to do + return; // struct exists - nothing to do } // Try to synthesize struct from vertex output std::regex vo_re(R"(struct\s+(\w+)\s*\{[^}]*\[\[position\]\])"); @@ -106,7 +99,7 @@ void fixMSLFragmentSignature(std::string& frag_msl, const std::string& vert_msl) return; } - // No stage_in — try to inject vertex output struct + stage_in parameter. + // No stage_in - try to inject vertex output struct + stage_in parameter. std::regex frag_fn_re(R"(fragment\s+(\w+)\s+(\w+)\s*\(([^)]*)\))"); std::smatch frag_fn_m; if (!std::regex_search(frag_msl, frag_fn_m, frag_fn_re)) @@ -190,7 +183,7 @@ CrossCompileResult SpirvCrossCrossCompiler::crossCompile(const CrossCompileReque } } -// ── MSL ─────────────────────────────────────────────────────────────────────── +// MSL CrossCompileResult SpirvCrossCrossCompiler::toMSL(const CrossCompileRequest& req) { CrossCompileResult result; try { @@ -201,7 +194,7 @@ CrossCompileResult SpirvCrossCrossCompiler::toMSL(const CrossCompileRequest& req opts.use_framebuffer_fetch_subpasses = false; compiler.set_msl_options(opts); - // ── Determine execution model ────────────────────────────────────────── + // Determine execution model std::string entry_name = "main"; spv::ExecutionModel exec_model = spv::ExecutionModelMax; { @@ -218,93 +211,22 @@ CrossCompileResult SpirvCrossCrossCompiler::toMSL(const CrossCompileRequest& req return result; } - // ── Resource binding remapping ───────────────────────────────────────── - // Note: do NOT call build_combined_image_samplers() for MSL. CompilerMSL - // handles OpSampledImage (from separate texture2D + sampler) natively. - // Calling it creates synthetic combined variables that conflict with the - // original separate resources, producing duplicate [[texture(N)]] slots. - auto resources = compiler.get_shader_resources(); - - // Combined image samplers (sampler2D uniforms in Vulkan GLSL → sampled_images in SPIR-V) - for (const auto& r : resources.sampled_images) { - uint32_t set = compiler.has_decoration(r.id, spv::DecorationDescriptorSet) - ? compiler.get_decoration(r.id, spv::DecorationDescriptorSet) - : 0; - uint32_t binding = compiler.has_decoration(r.id, spv::DecorationBinding) - ? compiler.get_decoration(r.id, spv::DecorationBinding) - : 0; - spirv_cross::MSLResourceBinding mb{}; - mb.stage = exec_model; - mb.desc_set = set; - mb.binding = binding; - mb.msl_texture = metalTextureSlot(set, binding, req.metal_layout); - mb.msl_sampler = metalSamplerSlot(set, binding, req.metal_layout); - compiler.add_msl_resource_binding(mb); - } - - // Uniform buffers - for (const auto& r : resources.uniform_buffers) { - uint32_t set = compiler.has_decoration(r.id, spv::DecorationDescriptorSet) - ? compiler.get_decoration(r.id, spv::DecorationDescriptorSet) - : 0; - uint32_t binding = compiler.has_decoration(r.id, spv::DecorationBinding) - ? compiler.get_decoration(r.id, spv::DecorationBinding) - : 0; - spirv_cross::MSLResourceBinding mb{}; - mb.stage = exec_model; - mb.desc_set = set; - mb.binding = binding; - mb.msl_buffer = metalBufferSlot(set, binding, req.metal_layout); - compiler.add_msl_resource_binding(mb); + // Resource binding remapping + // Dense (set,binding)->Metal index map. Prefer the program-wide signature from + // ShaderPipelineBuilder so all stages agree; otherwise allocate stage-locally. + // Note: do NOT call build_combined_image_samplers() for MSL. + std::unique_ptr stage_local_map; + const vne::sc::MetalBindingAllocator* metal_map = req.metal_program_map; + if (metal_map == nullptr) { + stage_local_map = std::make_unique(compiler, req.metal_layout); + metal_map = stage_local_map.get(); } - - // Storage buffers - for (const auto& r : resources.storage_buffers) { - uint32_t set = compiler.has_decoration(r.id, spv::DecorationDescriptorSet) - ? compiler.get_decoration(r.id, spv::DecorationDescriptorSet) - : 0; - uint32_t binding = compiler.has_decoration(r.id, spv::DecorationBinding) - ? compiler.get_decoration(r.id, spv::DecorationBinding) - : 0; - spirv_cross::MSLResourceBinding mb{}; - mb.stage = exec_model; - mb.desc_set = set; - mb.binding = binding; - mb.msl_buffer = metalBufferSlot(set, binding, req.metal_layout); - compiler.add_msl_resource_binding(mb); - } - - // Separate images - for (const auto& r : resources.separate_images) { - uint32_t set = compiler.has_decoration(r.id, spv::DecorationDescriptorSet) - ? compiler.get_decoration(r.id, spv::DecorationDescriptorSet) - : 0; - uint32_t binding = compiler.has_decoration(r.id, spv::DecorationBinding) - ? compiler.get_decoration(r.id, spv::DecorationBinding) - : 0; - spirv_cross::MSLResourceBinding mb{}; - mb.stage = exec_model; - mb.desc_set = set; - mb.binding = binding; - mb.msl_texture = metalTextureSlot(set, binding, req.metal_layout); - compiler.add_msl_resource_binding(mb); - } - - // Separate samplers - for (const auto& r : resources.separate_samplers) { - uint32_t set = compiler.has_decoration(r.id, spv::DecorationDescriptorSet) - ? compiler.get_decoration(r.id, spv::DecorationDescriptorSet) - : 0; - uint32_t binding = compiler.has_decoration(r.id, spv::DecorationBinding) - ? compiler.get_decoration(r.id, spv::DecorationBinding) - : 0; - spirv_cross::MSLResourceBinding mb{}; - mb.stage = exec_model; - mb.desc_set = set; - mb.binding = binding; - mb.msl_sampler = metalSamplerSlot(set, binding, req.metal_layout); - compiler.add_msl_resource_binding(mb); + if (const std::string overflow = metal_map->overflowError(); !overflow.empty()) { + result.code = ResultCode::eCrossCompileFailed; + result.error = "SpirvCrossCrossCompiler: " + overflow; + return result; } + vne::sc::applyMslResourceBindings(compiler, *metal_map, exec_model); // Disable unused fragment inputs to suppress spurious [[stage_in]] parameters if (exec_model == spv::ExecutionModelFragment) { @@ -342,7 +264,7 @@ CrossCompileResult SpirvCrossCrossCompiler::toMSL(const CrossCompileRequest& req fixCombinedSamplerIndices(result.source); } - // Extract final MSL entry point name (SPIRV-Cross may rename "main" → "main0") + // Extract final MSL entry point name (SPIRV-Cross may rename "main" -> "main0") try { result.entry_point = compiler.get_cleansed_entry_point_name(entry_name, exec_model); } catch (...) { @@ -362,7 +284,7 @@ CrossCompileResult SpirvCrossCrossCompiler::toMSL(const CrossCompileRequest& req } } -// ── GLSL / GLSL ES ──────────────────────────────────────────────────────────── +// GLSL / GLSL ES CrossCompileResult SpirvCrossCrossCompiler::toGLSL(const CrossCompileRequest& req, bool es) { CrossCompileResult result; try { @@ -379,7 +301,7 @@ CrossCompileResult SpirvCrossCrossCompiler::toGLSL(const CrossCompileRequest& re opts.vertex.flip_vert_y = false; compiler.set_common_options(opts); - // Build combined image samplers (required for GLSL — no separate images+samplers) + // Build combined image samplers (required for GLSL - no separate images+samplers) compiler.build_combined_image_samplers(); for (const auto& c : compiler.get_combined_image_samplers()) { const std::string img_name = compiler.get_name(c.image_id); @@ -406,7 +328,7 @@ CrossCompileResult SpirvCrossCrossCompiler::toGLSL(const CrossCompileRequest& re } } -// ── WGSL ───────────────────────────────────────────────────────────────────── +// WGSL CrossCompileResult SpirvCrossCrossCompiler::toWGSL(const CrossCompileRequest& req) { CrossCompileResult result; (void)req; @@ -417,7 +339,7 @@ CrossCompileResult SpirvCrossCrossCompiler::toWGSL(const CrossCompileRequest& re return result; } -// ── FixMSLFragmentSignature ──────────────────────────────────────────────────── +// FixMSLFragmentSignature void SpirvCrossCrossCompiler::fixMSLFragmentSignature(std::string& frag_msl, const std::string& vert_msl) { ::fixMSLFragmentSignature(frag_msl, vert_msl); } diff --git a/src/vertexnova/sc/crosscompiler/spirvcross_cross_compiler.h b/src/vertexnova/sc/crosscompiler/spirvcross_cross_compiler.h index 2cbd394..2d5dd22 100644 --- a/src/vertexnova/sc/crosscompiler/spirvcross_cross_compiler.h +++ b/src/vertexnova/sc/crosscompiler/spirvcross_cross_compiler.h @@ -12,7 +12,7 @@ /** * @file spirvcross_cross_compiler.h - * @brief SPIRV-Cross based cross-compiler: SPIR-V → MSL / GLSL / GLSL ES. + * @brief SPIRV-Cross based cross-compiler: SPIR-V -> MSL / GLSL / GLSL ES. */ #include "vertexnova/sc/shader_cross_compiler.h" diff --git a/src/vertexnova/sc/crosscompiler/tint_cross_compiler.h b/src/vertexnova/sc/crosscompiler/tint_cross_compiler.h index a716f18..e935c07 100644 --- a/src/vertexnova/sc/crosscompiler/tint_cross_compiler.h +++ b/src/vertexnova/sc/crosscompiler/tint_cross_compiler.h @@ -12,7 +12,7 @@ /** * @file tint_cross_compiler.h - * @brief SPIR-V → WGSL cross-compiler using Dawn Tint. + * @brief SPIR-V -> WGSL cross-compiler using Dawn Tint. */ #include "vertexnova/sc/shader_cross_compiler.h" diff --git a/src/vertexnova/sc/frontend/dxc_frontend.h b/src/vertexnova/sc/frontend/dxc_frontend.h index 5ecf0bd..e201e5c 100644 --- a/src/vertexnova/sc/frontend/dxc_frontend.h +++ b/src/vertexnova/sc/frontend/dxc_frontend.h @@ -13,7 +13,7 @@ /** * @file dxc_frontend.h * @brief DXC front-end: compiles HLSL source to SPIR-V. - * @note SFI — stub implementation. @ref isAvailable returns @c false and + * @note SFI - stub implementation. @ref isAvailable returns @c false and */ #include "vertexnova/sc/shader_frontend.h" @@ -21,9 +21,9 @@ namespace vne::sc { /** - * @brief HLSL → SPIR-V front-end using the DirectX Shader Compiler (DXC). + * @brief HLSL -> SPIR-V front-end using the DirectX Shader Compiler (DXC). * - * @note SFI — stub. All methods are no-ops until the DXC back-end is implemented. + * @note SFI - stub. All methods are no-ops until the DXC back-end is implemented. */ class DxcFrontEnd final : public IShaderFrontEnd { public: diff --git a/src/vertexnova/sc/frontend/glslang_frontend.cpp b/src/vertexnova/sc/frontend/glslang_frontend.cpp index 056df85..00333ba 100644 --- a/src/vertexnova/sc/frontend/glslang_frontend.cpp +++ b/src/vertexnova/sc/frontend/glslang_frontend.cpp @@ -89,7 +89,7 @@ bool readBinaryFile(std::ifstream& file, std::string& out) { return file.gcount() == size; } -// ── Stage mapping ───────────────────────────────────────────────────────────── +// Stage mapping EShLanguage toEshLang(vne::sc::ShaderStage stage) { switch (stage) { case vne::sc::ShaderStage::eVertex: @@ -108,7 +108,7 @@ EShLanguage toEshLang(vne::sc::ShaderStage stage) { return EShLangVertex; } -// ── #include resolver ───────────────────────────────────────────────────────── +// #include resolver class FileIncluder : public glslang::TShader::Includer { public: explicit FileIncluder(const std::vector& include_dirs) @@ -165,7 +165,7 @@ class FileIncluder : public glslang::TShader::Includer { } }; -// ── Macro preamble ──────────────────────────────────────────────────────────── +// Macro preamble std::string buildPreamble(const std::vector& macros) { std::string preamble; for (const auto& m : macros) { @@ -212,7 +212,7 @@ CompileResult GlslangFrontEnd::compile(const CompileRequest& req) { return result; } - // ── Resolve source text ──────────────────────────────────────────────────── + // Resolve source text std::string source; std::string source_path; @@ -238,7 +238,7 @@ CompileResult GlslangFrontEnd::compile(const CompileRequest& req) { return result; } - // ── Build TShader ────────────────────────────────────────────────────────── + // Build TShader EShLanguage esh_lang = toEshLang(req.stage); glslang::TShader shader(esh_lang); @@ -289,7 +289,7 @@ CompileResult GlslangFrontEnd::compile(const CompileRequest& req) { return result; } - // ── Link ─────────────────────────────────────────────────────────────────── + // Link glslang::TProgram program; program.addShader(&shader); if (!program.link(messages)) { @@ -300,7 +300,7 @@ CompileResult GlslangFrontEnd::compile(const CompileRequest& req) { return result; } - // ── SPIR-V generation ────────────────────────────────────────────────────── + // SPIR-V generation glslang::SpvOptions spv_opts; spv_opts.generateDebugInfo = req.debug_info; spv_opts.disableOptimizer = (req.opt_level == OptLevel::eNone); @@ -321,7 +321,7 @@ CompileResult GlslangFrontEnd::compile(const CompileRequest& req) { } result.code = result.warnings.empty() ? ResultCode::eSuccess : ResultCode::eCompileWarnings; - VNE_LOG_DEBUG << "GlslangFrontEnd: compiled " << source_path << " → " << result.spirv.size() << " SPIR-V words"; + VNE_LOG_DEBUG << "GlslangFrontEnd: compiled " << source_path << " -> " << result.spirv.size() << " SPIR-V words"; return result; } diff --git a/src/vertexnova/sc/frontend/glslang_frontend.h b/src/vertexnova/sc/frontend/glslang_frontend.h index 6b73eb6..d37c563 100644 --- a/src/vertexnova/sc/frontend/glslang_frontend.h +++ b/src/vertexnova/sc/frontend/glslang_frontend.h @@ -12,7 +12,7 @@ /** * @file glslang_frontend.h - * @brief glslang-based GLSL → SPIR-V compiler front-end. + * @brief glslang-based GLSL -> SPIR-V compiler front-end. */ #include "vertexnova/sc/shader_frontend.h" diff --git a/src/vertexnova/sc/frontend/slang_frontend.h b/src/vertexnova/sc/frontend/slang_frontend.h index 53ef310..ac156b7 100644 --- a/src/vertexnova/sc/frontend/slang_frontend.h +++ b/src/vertexnova/sc/frontend/slang_frontend.h @@ -13,7 +13,7 @@ /** * @file slang_frontend.h * @brief Slang front-end: compiles Slang source to SPIR-V. - * @note SFI — stub implementation. @ref isAvailable returns @c false and + * @note SFI - stub implementation. @ref isAvailable returns @c false and */ #include "vertexnova/sc/shader_frontend.h" @@ -21,9 +21,9 @@ namespace vne::sc { /** - * @brief Slang → SPIR-V front-end using the Slang shader compiler. + * @brief Slang -> SPIR-V front-end using the Slang shader compiler. * - * @note SFI — stub. All methods are no-ops until the Slang back-end is implemented. + * @note SFI - stub. All methods are no-ops until the Slang back-end is implemented. */ class SlangFrontEnd final : public IShaderFrontEnd { public: diff --git a/src/vertexnova/sc/gpu_layout_tools.cpp b/src/vertexnova/sc/gpu_layout_tools.cpp index df41a4b..2b4da88 100644 --- a/src/vertexnova/sc/gpu_layout_tools.cpp +++ b/src/vertexnova/sc/gpu_layout_tools.cpp @@ -379,7 +379,7 @@ bool validateGpuLayouts(const ShaderArtifact& artifact, const GpuLayoutRegistry& std::string buildBindingDeclsGlsl(const ShaderArtifact& artifact, const EmitBindingDeclOptions& options) { std::ostringstream out; - out << "// AUTO-GENERATED by vnesc_shader_compiler — DO NOT EDIT\n\n"; + out << "// AUTO-GENERATED by vnesc_shader_compiler - DO NOT EDIT\n\n"; for (const auto& include_path : options.compose_includes) { out << "#include \"" << include_path << "\"\n"; diff --git a/src/vertexnova/sc/reflector/spirvcross_reflector.cpp b/src/vertexnova/sc/reflector/spirvcross_reflector.cpp index d150e24..2745478 100644 --- a/src/vertexnova/sc/reflector/spirvcross_reflector.cpp +++ b/src/vertexnova/sc/reflector/spirvcross_reflector.cpp @@ -11,6 +11,8 @@ #include "spirvcross_reflector.h" +#include "vertexnova/sc/metal_binding_allocator.h" + #include "spirv_cross.hpp" #include "spirv_msl.hpp" @@ -27,17 +29,7 @@ CREATE_VNE_LOGGER_CATEGORY("vne.sc.reflector") namespace { -inline uint32_t metalBuf(uint32_t set, uint32_t binding, const vne::sc::MetalBindingLayout& layout) noexcept { - return layout.buffer_base + (set * layout.flatten_stride + binding); -} -inline uint32_t metalTex(uint32_t set, uint32_t binding, const vne::sc::MetalBindingLayout& layout) noexcept { - return set * layout.flatten_stride + binding; -} -inline uint32_t metalSmp(uint32_t set, uint32_t binding, const vne::sc::MetalBindingLayout& layout) noexcept { - return set * layout.flatten_stride + binding; -} - -// ── Reflect struct members from a SPIRV-Cross block type ───────────────────── +// Reflect struct members from a SPIRV-Cross block type std::vector reflectStructMembers(const spirv_cross::Compiler& compiler, const spirv_cross::SPIRType& block_type) { std::vector members; @@ -65,12 +57,12 @@ std::vector reflectStructMembers(const spirv_cro return members; } -// ── Populate one ReflectedBindingInfo from a SPIRV-Cross resource ──────────── +// Populate one ReflectedBindingInfo from a SPIRV-Cross resource template void appendBinding(const spirv_cross::Compiler& compiler, - const spirv_cross::CompilerMSL* msl, // nullptr → MSL not a target + const spirv_cross::CompilerMSL* msl, // nullptr -> MSL not a target bool has_webgpu, - const vne::sc::MetalBindingLayout& layout, + const vne::sc::MetalBindingAllocator* metal_map, const spirv_cross::Resource& res, std::vector& out) { using namespace vne::sc; @@ -81,7 +73,6 @@ void appendBinding(const spirv_cross::Compiler& compiler, const spirv_cross::SPIRType& ty = compiler.get_type(res.type_id); const uint32_t array_size = ty.array.empty() ? 1u : (ty.array[0] == 0u ? 0u : ty.array[0]); - // Classify cubemap images separately ReflectedResourceType actual_type = Type; if constexpr (Type == ReflectedResourceType::eSampledImage) { if (ty.image.dim == spv::DimCube) { @@ -89,11 +80,10 @@ void appendBinding(const spirv_cross::Compiler& compiler, } } - // ── Backend slot assignment ─────────────────────────────────────────────── ResourceBackendSlots slots; if (msl) { - MetalResourceSlot metal; + MetalResourceSlot metal{}; bool from_spirvcross = false; try { const uint32_t primary = msl->get_automatic_msl_resource_binding(res.id); @@ -117,14 +107,19 @@ void appendBinding(const spirv_cross::Compiler& compiler, } } catch (...) { } - if (!from_spirvcross) { - // Deterministic fallback formula when SPIRV-Cross auto-binding unavailable + if (!from_spirvcross && metal_map != nullptr) { if constexpr (Type == ReflectedResourceType::eUniformBuffer || Type == ReflectedResourceType::eStorageBuffer) { - metal.buffer = metalBuf(set, binding, layout); - } else { - metal.texture = metalTex(set, binding, layout); - metal.sampler = metalSmp(set, binding, layout); + metal.buffer = metal_map->buffer(set, binding); + } else if constexpr (Type == ReflectedResourceType::eSampler) { + metal.sampler = metal_map->sampler(set, binding); + } else if constexpr (Type == ReflectedResourceType::eCombinedImageSampler) { + metal.texture = metal_map->texture(set, binding); + metal.sampler = metal_map->sampler(set, binding); + } else if constexpr (Type == ReflectedResourceType::eSampledImage + || Type == ReflectedResourceType::eStorageImage + || Type == ReflectedResourceType::eSampledCubemap) { + metal.texture = metal_map->texture(set, binding); } } slots.metal = metal; @@ -134,17 +129,15 @@ void appendBinding(const spirv_cross::Compiler& compiler, slots.webgpu = WebGpuResourceSlot{set, binding}; } - // ── Assemble binding info ───────────────────────────────────────────────── ReflectedBindingInfo info; info.name = res.name; info.type = actual_type; info.set = set; info.binding = binding; info.array_size = array_size; - info.stages = ShaderStageFlags::eNone; // caller sets stage flags + info.stages = ShaderStageFlags::eNone; info.slots = slots; - // Struct members for buffer types if constexpr (Type == ReflectedResourceType::eUniformBuffer || Type == ReflectedResourceType::eStorageBuffer) { try { const spirv_cross::SPIRType& block = compiler.get_type(res.base_type_id); @@ -176,6 +169,24 @@ vne::sc::ShaderStageFlags stageToFlag(vne::sc::ShaderStage s) noexcept { return F::eNone; } +spv::ExecutionModel stageToExecModel(vne::sc::ShaderStage stage) noexcept { + switch (stage) { + case vne::sc::ShaderStage::eVertex: + return spv::ExecutionModelVertex; + case vne::sc::ShaderStage::eFragment: + return spv::ExecutionModelFragment; + case vne::sc::ShaderStage::eCompute: + return spv::ExecutionModelGLCompute; + case vne::sc::ShaderStage::eGeometry: + return spv::ExecutionModelGeometry; + case vne::sc::ShaderStage::eTessellationControl: + return spv::ExecutionModelTessellationControl; + case vne::sc::ShaderStage::eTessellationEvaluation: + return spv::ExecutionModelTessellationEvaluation; + } + return spv::ExecutionModelVertex; +} + } // namespace namespace vne::sc { @@ -183,7 +194,8 @@ namespace vne::sc { ReflectResult SpirvCrossReflector::reflect(const std::vector& spirv, ShaderStage stage, const std::vector& targets, - MetalBindingLayout metal_layout) { + MetalBindingLayout metal_layout, + const MetalBindingAllocator* metal_program_map) { ReflectResult result; if (spirv.empty()) { @@ -197,20 +209,32 @@ ReflectResult SpirvCrossReflector::reflect(const std::vector& spirv, const bool has_webgpu = std::find(targets.begin(), targets.end(), CrossTarget::eWGSL) != targets.end(); try { - // Base compiler for Vulkan reflection spirv_cross::Compiler compiler(spirv.data(), spirv.size()); - // MSL compiler — only instantiated when eMSL is a requested target + std::unique_ptr stage_local_map; + const MetalBindingAllocator* metal_map = metal_program_map; + if (has_msl && metal_map == nullptr) { + stage_local_map = std::make_unique(compiler, metal_layout); + metal_map = stage_local_map.get(); + } + std::unique_ptr msl_compiler; - if (has_msl) { + if (has_msl && metal_map != nullptr) { + if (const std::string overflow = metal_map->overflowError(); !overflow.empty()) { + result.code = ResultCode::eReflectionFailed; + result.error = "SpirvCrossReflector: " + overflow; + VNE_LOG_ERROR << result.error; + return result; + } msl_compiler = std::make_unique(spirv.data(), spirv.size()); spirv_cross::CompilerMSL::Options msl_opts; msl_opts.platform = spirv_cross::CompilerMSL::Options::macOS; msl_compiler->set_msl_options(msl_opts); + applyMslResourceBindings(*msl_compiler, *metal_map, stageToExecModel(stage)); try { msl_compiler->compile(); } catch (...) { - VNE_LOG_WARN << "SpirvCrossReflector: MSL compile pass failed; using fallback slot formula"; + VNE_LOG_WARN << "SpirvCrossReflector: MSL compile pass failed; using allocator slot map"; } } @@ -228,7 +252,7 @@ ReflectResult SpirvCrossReflector::reflect(const std::vector& spirv, appendBinding(compiler, \ msl_compiler.get(), \ has_webgpu, \ - metal_layout, \ + metal_map, \ res, \ bindings); \ } @@ -242,12 +266,10 @@ ReflectResult SpirvCrossReflector::reflect(const std::vector& spirv, #undef VNE_REFLECT_LIST - // Tag each binding with this stage for (auto& b : bindings) { b.stages = stage_flag; } - // Push-constant block size for (const auto& pc : resources.push_constant_buffers) { try { const spirv_cross::SPIRType& ty = compiler.get_type(pc.base_type_id); @@ -256,7 +278,6 @@ ReflectResult SpirvCrossReflector::reflect(const std::vector& spirv, } } - // Workgroup size for compute shaders if (stage == ShaderStage::eCompute) { const auto wg = compiler.get_execution_mode_argument(spv::ExecutionModeLocalSize, 0); const auto wg_y = compiler.get_execution_mode_argument(spv::ExecutionModeLocalSize, 1); diff --git a/src/vertexnova/sc/reflector/spirvcross_reflector.h b/src/vertexnova/sc/reflector/spirvcross_reflector.h index 7ddca5b..db76a82 100644 --- a/src/vertexnova/sc/reflector/spirvcross_reflector.h +++ b/src/vertexnova/sc/reflector/spirvcross_reflector.h @@ -31,7 +31,8 @@ class SpirvCrossReflector final : public IShaderReflector { // NOLINT(readabili ReflectResult reflect(const std::vector& spirv, ShaderStage stage, const std::vector& targets = {}, - MetalBindingLayout metal_layout = {}) override; + MetalBindingLayout metal_layout = {}, + const MetalBindingAllocator* metal_program_map = nullptr) override; }; } // namespace vne::sc diff --git a/src/vertexnova/sc/shader_artifact_cache.cpp b/src/vertexnova/sc/shader_artifact_cache.cpp index e7b0a56..32d5ddb 100644 --- a/src/vertexnova/sc/shader_artifact_cache.cpp +++ b/src/vertexnova/sc/shader_artifact_cache.cpp @@ -24,7 +24,7 @@ CREATE_VNE_LOGGER_CATEGORY("vne.sc.cache") namespace vne::sc { -// ── FNV-1a 64-bit hash ──────────────────────────────────────────────────────── +// FNV-1a 64-bit hash namespace { constexpr uint64_t kFnv1a64Prime = 0x100000001b3ULL; @@ -55,7 +55,7 @@ std::string toHex(uint64_t v) { return os.str(); } -// ── Binary I/O helpers ──────────────────────────────────────────────────────── +// Binary I/O helpers struct Writer { std::ostringstream os{std::ios::binary}; void u8(uint8_t v) { os.write(reinterpret_cast(&v), 1); } @@ -91,7 +91,7 @@ struct Reader { [[nodiscard]] bool ok() const { return is.good() || is.eof(); } }; -// ── StageArtifact binary format ─────────────────────────────────────────────── +// StageArtifact binary format // stage(u8) | entry_point(str) | spirv_count(u32) | spirv_data | // StageReflection | cross_count(u32) | { target(u8) | source(str) | ep(str) } * N @@ -147,7 +147,7 @@ bool deserializeArtifact(const std::string& data, StageArtifact& out) { } // namespace -// ── ShaderArtifactCache ─────────────────────────────────────────────────────── +// ShaderArtifactCache ShaderArtifactCache::ShaderArtifactCache(std::string cache_dir) : cache_dir_(std::move(cache_dir)) { @@ -157,10 +157,9 @@ ShaderArtifactCache::ShaderArtifactCache(std::string cache_dir) } } -std::string ShaderArtifactCache::makeKey(const CompileRequest& req, - const std::vector& targets, - const MetalBindingLayout& metal_layout) { - uint64_t h = kFnv1a64Offset; +namespace { + +void hashCompileRequest(uint64_t& h, const CompileRequest& req) noexcept { hashStr(h, req.source); hashStr(h, req.file_path); hashStr(h, req.entry_point); @@ -176,6 +175,34 @@ std::string ShaderArtifactCache::makeKey(const CompileRequest& req, hashStr(h, m.name); hashStr(h, m.value); } + for (const auto& dir : req.include_dirs) { + hashStr(h, dir); + } +} + +} // namespace + +std::uint64_t ShaderArtifactCache::makeProgramFingerprint(const std::vector& stages, + const MetalBindingLayout& metal_layout) { + uint64_t h = kFnv1a64Offset; + h ^= static_cast(stages.size()); + h *= kFnv1a64Prime; + for (const auto& req : stages) { + hashCompileRequest(h, req); + } + h ^= metal_layout.flatten_stride; + h *= kFnv1a64Prime; + h ^= metal_layout.buffer_base; + h *= kFnv1a64Prime; + return h; +} + +std::string ShaderArtifactCache::makeKey(const CompileRequest& req, + const std::vector& targets, + const MetalBindingLayout& metal_layout, + std::uint64_t metal_program_fingerprint) { + uint64_t h = kFnv1a64Offset; + hashCompileRequest(h, req); bool has_msl = false; for (auto t : targets) { h ^= static_cast(t); @@ -188,6 +215,8 @@ std::string ShaderArtifactCache::makeKey(const CompileRequest& req, h *= kFnv1a64Prime; h ^= metal_layout.buffer_base; h *= kFnv1a64Prime; + h ^= metal_program_fingerprint; + h *= kFnv1a64Prime; } return toHex(h); } @@ -211,7 +240,7 @@ std::optional ShaderArtifactCache::lookup(const std::string& key) return std::nullopt; StageArtifact artifact; if (!deserializeArtifact(data, artifact)) { - VNE_LOG_WARN << "ShaderArtifactCache: corrupt artifact at " << path << " — ignoring"; + VNE_LOG_WARN << "ShaderArtifactCache: corrupt artifact at " << path << " - ignoring"; return std::nullopt; } VNE_LOG_DEBUG << "ShaderArtifactCache: cache hit for key " << key; diff --git a/src/vertexnova/sc/shader_bundle.cpp b/src/vertexnova/sc/shader_bundle.cpp index d0eeef9..4a8180c 100644 --- a/src/vertexnova/sc/shader_bundle.cpp +++ b/src/vertexnova/sc/shader_bundle.cpp @@ -266,7 +266,7 @@ bool writeShaderBundle(const ShaderArtifact& artifact, const std::filesystem::pa // Canonical Vulkan set/binding jb["vulkan"] = {{"set", b.set}, {"binding", b.binding}}; - // Type-aware Metal slot — only emit fields meaningful for this resource type + // Type-aware Metal slot - only emit fields meaningful for this resource type if (b.slots.metal) { nlohmann::json jm; switch (b.type) { @@ -293,12 +293,12 @@ bool writeShaderBundle(const ShaderArtifact& artifact, const std::filesystem::pa jb["metal"] = std::move(jm); } - // WebGPU slot — only present when WGSL was a compilation target + // WebGPU slot - only present when WGSL was a compilation target if (b.slots.webgpu) { jb["webgpu"] = {{"group", b.slots.webgpu->group}, {"binding", b.slots.webgpu->binding}}; } - // Struct members for buffer types — omit noise fields when at defaults + // Struct members for buffer types - omit noise fields when at defaults if (!b.struct_members.empty()) { nlohmann::json jmembers = nlohmann::json::array(); for (const auto& m : b.struct_members) { diff --git a/src/vertexnova/sc/shader_pipeline_builder.cpp b/src/vertexnova/sc/shader_pipeline_builder.cpp index be3b741..1b74f15 100644 --- a/src/vertexnova/sc/shader_pipeline_builder.cpp +++ b/src/vertexnova/sc/shader_pipeline_builder.cpp @@ -10,6 +10,7 @@ */ #include "vertexnova/sc/shader_pipeline_builder.h" +#include "vertexnova/sc/metal_binding_allocator.h" #include "vertexnova/sc/shader_frontend.h" #include "vertexnova/sc/shader_cross_compiler.h" #include "vertexnova/sc/shader_reflector.h" @@ -18,8 +19,11 @@ #include "vertexnova/logging/logging.h" +#include #include +#include #include +#include CREATE_VNE_LOGGER_CATEGORY("vne.sc.pipeline") @@ -59,25 +63,47 @@ PipelineBuildResult ShaderPipelineBuilder::build(const PipelineBuildDesc& desc) cache = std::make_unique(desc.cache_dir); } - for (const auto& req : desc.stages) { - // ── Cache lookup ──────────────────────────────────────────────────── - const std::string key = - cache ? ShaderArtifactCache::makeKey(req, desc.targets, desc.metal_layout) : std::string{}; + const bool wants_msl = std::find(desc.targets.begin(), desc.targets.end(), CrossTarget::eMSL) != desc.targets.end(); + + // Source-level program fingerprint (no SPIR-V). Embed in MSL dense cache keys so + // lookup can run before compile; changing any stage invalidates every stage key. + const std::uint64_t metal_program_fingerprint = + (wants_msl && desc.metal_dense_program_map) + ? ShaderArtifactCache::makeProgramFingerprint(desc.stages, desc.metal_layout) + : 0ULL; + + if (wants_msl && !desc.metal_dense_program_map) { + VNE_LOG_WARN << "ShaderPipelineBuilder: metal_dense_program_map=false - using stage-local Metal maps"; + } + struct StageWork { + CompileRequest req; + StageArtifact artifact; + std::string cache_key; + bool needs_backend = true; // false when a full StageArtifact was loaded from cache + }; + std::vector work; + work.reserve(desc.stages.size()); + + // Phase A: cache lookup first; compile + validate only on miss. + for (const auto& req : desc.stages) { + StageWork sw; + sw.req = req; + sw.artifact.stage = req.stage; + sw.artifact.entry_point = req.entry_point; if (cache) { - auto cached = cache->lookup(key); + sw.cache_key = + ShaderArtifactCache::makeKey(sw.req, desc.targets, desc.metal_layout, metal_program_fingerprint); + auto cached = cache->lookup(sw.cache_key); if (cached.has_value()) { - VNE_LOG_DEBUG << "ShaderPipelineBuilder: cache hit for stage " << static_cast(req.stage); - result.artifact.stages.push_back(std::move(*cached)); + VNE_LOG_DEBUG << "ShaderPipelineBuilder: cache hit for stage " << static_cast(sw.req.stage); + sw.artifact = std::move(*cached); + sw.needs_backend = false; + work.push_back(std::move(sw)); continue; } } - StageArtifact stage_artifact; - stage_artifact.stage = req.stage; - stage_artifact.entry_point = req.entry_point; - - // ── 1. Compile → SPIR-V ─────────────────────────────────────────── VNE_LOG_DEBUG << "ShaderPipelineBuilder: compiling stage " << static_cast(req.stage); CompileResult cr = front_end_->compile(req); if (!cr.ok()) { @@ -91,9 +117,8 @@ PipelineBuildResult ShaderPipelineBuilder::build(const PipelineBuildDesc& desc) for (const auto& w : cr.warnings) { VNE_LOG_WARN << "ShaderPipelineBuilder: compile warning: " << w; } - stage_artifact.spirv = std::move(cr.spirv); + sw.artifact.spirv = std::move(cr.spirv); - // ── 2. Validate ─────────────────────────────────────────────────── if (desc.validate && (!validator_ || !validator_->isAvailable())) { result.code = ResultCode::eUnavailable; result.error = "ShaderPipelineBuilder: validator requested but not available"; @@ -101,7 +126,7 @@ PipelineBuildResult ShaderPipelineBuilder::build(const PipelineBuildDesc& desc) return result; } if (desc.validate) { - ValidationResult vr = validator_->validate(stage_artifact.spirv); + ValidationResult vr = validator_->validate(sw.artifact.spirv); if (!vr.ok()) { result.code = ResultCode::eValidationFailed; result.error = "ShaderPipelineBuilder: SPIR-V validation failed: " + vr.error; @@ -110,36 +135,77 @@ PipelineBuildResult ShaderPipelineBuilder::build(const PipelineBuildDesc& desc) } } - // ── 3. Reflect ──────────────────────────────────────────────────── + work.push_back(std::move(sw)); + } + + const bool any_miss = std::any_of(work.begin(), work.end(), [](const StageWork& sw) { return sw.needs_backend; }); + + // Program-link precompute (Metal only): needed when at least one stage must + // reflect / cross-compile with the shared dense map. + std::unique_ptr metal_program_map; + if (any_miss && wants_msl && desc.metal_dense_program_map) { + std::vector> stage_spirv; + stage_spirv.reserve(work.size()); + for (const auto& sw : work) { + stage_spirv.push_back(sw.artifact.spirv); + } + metal_program_map = + std::make_unique(MetalBindingAllocator::fromProgram(stage_spirv, desc.metal_layout)); + if (const std::string overflow = metal_program_map->overflowError(); !overflow.empty()) { + result.code = ResultCode::eCrossCompileFailed; + result.error = "ShaderPipelineBuilder: " + overflow; + VNE_LOG_ERROR << result.error; + return result; + } + { + std::ostringstream oss; + oss << "ShaderPipelineBuilder: Metal program signature fingerprint=0x" << std::hex + << metal_program_map->fingerprint(); + VNE_LOG_DEBUG << oss.str(); + } + } + + // Phase B: reflect / cross-compile / store for cache misses only. + if (any_miss && !desc.targets.empty() && !cross_compiler_) { + result.code = ResultCode::eUnavailable; + result.error = "ShaderPipelineBuilder: cross-compiler not available"; + VNE_LOG_ERROR << result.error; + return result; + } + + for (auto& sw : work) { + if (!sw.needs_backend) { + // Cache hit: move the stored StageArtifact wholesale into the result. + result.artifact.stages.push_back(std::move(sw.artifact)); + continue; + } + if (reflector_) { - ReflectResult rr = reflector_->reflect(stage_artifact.spirv, req.stage, desc.targets, desc.metal_layout); + ReflectResult rr = reflector_->reflect(sw.artifact.spirv, + sw.req.stage, + desc.targets, + desc.metal_layout, + metal_program_map.get()); if (rr.ok()) { - stage_artifact.reflection = std::move(rr.reflection); + sw.artifact.reflection = std::move(rr.reflection); } else { VNE_LOG_WARN << "ShaderPipelineBuilder: reflection failed (non-fatal): " << rr.error; } } - // ── 4. Cross-compile for each requested target ──────────────────── - if (!desc.targets.empty() && !cross_compiler_) { - result.code = ResultCode::eUnavailable; - result.error = "ShaderPipelineBuilder: cross-compiler not available"; - VNE_LOG_ERROR << result.error; - return result; - } if (cross_compiler_) { for (CrossTarget target : desc.targets) { CrossCompileRequest ccr; - ccr.spirv = stage_artifact.spirv; + ccr.spirv = sw.artifact.spirv; ccr.target = target; - ccr.stage = req.stage; + ccr.stage = sw.req.stage; ccr.metal_layout = desc.metal_layout; + ccr.metal_program_map = metal_program_map.get(); CrossCompileResult ccres = cross_compiler_->crossCompile(ccr); if (ccres.ok()) { - // Apply WGSL binding remap to reflection so WebGpuResourceSlot matches emitted WGSL. if (target == CrossTarget::eWGSL && !ccres.wgpu_binding_remap.empty()) { - for (auto& binding : stage_artifact.reflection.bindings) { + for (auto& binding : sw.artifact.reflection.bindings) { for (const auto& remap : ccres.wgpu_binding_remap) { if (binding.name == remap.name) { binding.slots.webgpu = WebGpuResourceSlot{remap.group, remap.binding}; @@ -152,7 +218,7 @@ PipelineBuildResult ShaderPipelineBuilder::build(const PipelineBuildDesc& desc) cc.target = target; cc.source = std::move(ccres.source); cc.entry_point = std::move(ccres.entry_point); - stage_artifact.cross_compiled.push_back(std::move(cc)); + sw.artifact.cross_compiled.push_back(std::move(cc)); } else { if (target == CrossTarget::eWGSL) { VNE_LOG_WARN << "ShaderPipelineBuilder: WGSL cross-compile failed (non-fatal): " << ccres.error; @@ -167,15 +233,14 @@ PipelineBuildResult ShaderPipelineBuilder::build(const PipelineBuildDesc& desc) } } - // ── 5. Cache store ───────────────────────────────────────────────── - if (cache && !key.empty()) { - cache->store(key, stage_artifact); + if (cache && !sw.cache_key.empty()) { + cache->store(sw.cache_key, sw.artifact); } - result.artifact.stages.push_back(std::move(stage_artifact)); + result.artifact.stages.push_back(std::move(sw.artifact)); } - VNE_LOG_INFO << "ShaderPipelineBuilder: build complete — " << result.artifact.stages.size() << " stage(s)"; + VNE_LOG_INFO << "ShaderPipelineBuilder: build complete - " << result.artifact.stages.size() << " stage(s)"; result.code = ResultCode::eSuccess; return result; } diff --git a/src/vertexnova/sc/shader_reflection_binary.cpp b/src/vertexnova/sc/shader_reflection_binary.cpp index 9c33a97..be5efd6 100644 --- a/src/vertexnova/sc/shader_reflection_binary.cpp +++ b/src/vertexnova/sc/shader_reflection_binary.cpp @@ -15,7 +15,7 @@ namespace { -constexpr uint32_t kReflectionBinaryVersion = 2u; // bumped: BackendSlot → ResourceBackendSlots +constexpr uint32_t kReflectionBinaryVersion = 2u; // bumped: BackendSlot -> ResourceBackendSlots constexpr uint32_t kMaxStringLen = 4096; constexpr uint32_t kMaxProgramStageCount = 16; constexpr uint32_t kMaxBindingCount = 256; diff --git a/src/vertexnova/sc/validator/noop_validator.cpp b/src/vertexnova/sc/validator/noop_validator.cpp index 48f02aa..26cc690 100644 --- a/src/vertexnova/sc/validator/noop_validator.cpp +++ b/src/vertexnova/sc/validator/noop_validator.cpp @@ -11,4 +11,4 @@ #include "noop_validator.h" -// Stub — implementation lives in the header. +// Stub - implementation lives in the header. diff --git a/src/vertexnova/sc/validator/noop_validator.h b/src/vertexnova/sc/validator/noop_validator.h index 971a7a6..66b35fe 100644 --- a/src/vertexnova/sc/validator/noop_validator.h +++ b/src/vertexnova/sc/validator/noop_validator.h @@ -21,7 +21,7 @@ namespace vne::sc { /** - * @brief Pass-through validator — accepts every SPIR-V binary without inspection. + * @brief Pass-through validator - accepts every SPIR-V binary without inspection. * * @ref isAvailable returns @c true so the pipeline builder does not skip the * validation step; it simply always succeeds. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a405a01..c41319a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,14 +2,23 @@ # Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License") # -# vnesc test suite — one translation unit per class under test (ClassNameTest). +# Author: Ajeet Singh Yadav +# Created: February 2026 +# +# Autodoc: yes #============================================================================== +# vnesc unit tests (GoogleTest) +# One translation unit per class under test (ClassNameTest fixtures). + if(NOT TARGET vnesc) return() endif() -# ── Google Test ─────────────────────────────────────────────────────────────── +#============================================================================== +# Google Test Setup # +#============================================================================== + # Always build from source via FetchContent to guarantee static linkage. message(STATUS "vnesc tests: fetching googletest v1.17.0") include(FetchContent) @@ -24,6 +33,12 @@ FetchContent_Declare(googletest FetchContent_MakeAvailable(googletest) set(GTEST_LIBS gtest gmock) +include(GoogleTest) + +#============================================================================== +# Build Executable # +#============================================================================== + set(VNESC_TEST_SOURCES main.cpp result_code_test.cpp @@ -40,6 +55,7 @@ if(VNE_SC_GLSLANG) spirvcross_reflector_test.cpp shader_cross_compiler_test.cpp shader_pipeline_builder_test.cpp + metal_binding_allocator_test.cpp shader_bundle_test.cpp shader_pipeline_spec_includes_test.cpp) endif() @@ -52,6 +68,11 @@ add_executable(vnesc_tests ${VNESC_TEST_SOURCES}) target_include_directories(vnesc_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/src) + +#============================================================================== +# Dependencies # +#============================================================================== + target_link_libraries(vnesc_tests PRIVATE ${GTEST_LIBS} vnesc) @@ -67,5 +88,8 @@ if(VNE_SC_JSON) target_compile_definitions(vnesc_tests PRIVATE VNE_SC_JSON_ENABLED) endif() -include(GoogleTest) +#============================================================================== +# Test Discovery # +#============================================================================== + gtest_discover_tests(vnesc_tests) diff --git a/tests/fixtures/glslang_frontend_test_fixture.h b/tests/fixtures/glslang_frontend_test_fixture.h index 126c6ec..e5d37cc 100644 --- a/tests/fixtures/glslang_frontend_test_fixture.h +++ b/tests/fixtures/glslang_frontend_test_fixture.h @@ -11,7 +11,7 @@ */ /** - * @file fixtures/glslang_frontend_test_fixture.h — base fixture for GlslangFrontEnd tests. + * @file fixtures/glslang_frontend_test_fixture.h - base fixture for GlslangFrontEnd tests. */ #include "fixtures/shader_sources_fixture.h" diff --git a/tests/fixtures/shader_pipeline_builder_test_fixture.h b/tests/fixtures/shader_pipeline_builder_test_fixture.h index 7884f89..c4289fd 100644 --- a/tests/fixtures/shader_pipeline_builder_test_fixture.h +++ b/tests/fixtures/shader_pipeline_builder_test_fixture.h @@ -11,7 +11,7 @@ */ /** - * @file fixtures/shader_pipeline_builder_test_fixture.h — helpers for ShaderPipelineBuilder tests. + * @file fixtures/shader_pipeline_builder_test_fixture.h - helpers for ShaderPipelineBuilder tests. */ #include diff --git a/tests/fixtures/shader_sources_fixture.h b/tests/fixtures/shader_sources_fixture.h index 032dd38..0a0f382 100644 --- a/tests/fixtures/shader_sources_fixture.h +++ b/tests/fixtures/shader_sources_fixture.h @@ -11,7 +11,7 @@ */ /** - * @file fixtures/shader_sources_fixture.h — shared GLSL sources for integration tests. + * @file fixtures/shader_sources_fixture.h - shared GLSL sources for integration tests. */ #include diff --git a/tests/metal_binding_allocator_test.cpp b/tests/metal_binding_allocator_test.cpp new file mode 100644 index 0000000..351aed1 --- /dev/null +++ b/tests/metal_binding_allocator_test.cpp @@ -0,0 +1,94 @@ +/* --------------------------------------------------------------------- + * Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License") + * + * Author: Ajeet Singh Yadav + * Created: July 2026 + * + * Autodoc: yes + * ---------------------------------------------------------------------- + */ + +#include + +#include "vertexnova/sc/metal_binding_allocator.h" +#include "vertexnova/sc/vnesc.h" + +#include +#include + +#if defined(VNE_SC_GLSLANG_ENABLED) + +namespace vne::sc { +namespace { + +std::vector compileGlsl(const char* source, ShaderStage stage) { + auto fe = ShaderCompilerFactory::createFrontEnd(SourceLang::eGLSL); + CompileRequest req; + req.source = source; + req.stage = stage; + auto cr = fe->compile(req); + EXPECT_TRUE(cr.ok()) << (cr.errors.empty() ? "" : cr.errors.front()); + return cr.spirv; +} + +} // namespace + +TEST(MetalBindingAllocatorTest, ProgramWideUnionIsDenseAndConsistent) { + const char* vert = R"glsl( + #version 450 + layout(set = 0, binding = 0) uniform A { float a; } ua; + layout(set = 2, binding = 1) uniform B { float b; } ub; + void main() { gl_Position = vec4(ua.a + ub.b, 0.0, 0.0, 1.0); } + )glsl"; + const char* frag = R"glsl( + #version 450 + layout(location = 0) out vec4 o; + layout(set = 0, binding = 0) uniform A { float a; } ua; + layout(set = 1, binding = 0) uniform C { float c; } uc; + void main() { o = vec4(ua.a + uc.c); } + )glsl"; + + auto vs = compileGlsl(vert, ShaderStage::eVertex); + auto fs = compileGlsl(frag, ShaderStage::eFragment); + ASSERT_FALSE(vs.empty()); + ASSERT_FALSE(fs.empty()); + + MetalBindingLayout layout; + auto program = MetalBindingAllocator::fromProgram({vs, fs}, layout); + EXPECT_TRUE(program.overflowError().empty()) << program.overflowError(); + + // Shared (0,0) must match stage-local union assignment. + EXPECT_EQ(program.buffer(0, 0), layout.buffer_base); + // (1,0) and (2,1) are packed densely after (0,0) - not flatten indices. + EXPECT_EQ(program.buffer(1, 0), layout.buffer_base + 1u); + EXPECT_EQ(program.buffer(2, 1), layout.buffer_base + 2u); + EXPECT_NE(program.buffer(2, 1), layout.buffer_base + 2u * layout.flatten_stride + 1u); + + auto again = MetalBindingAllocator::fromProgram({vs, fs}, layout); + EXPECT_EQ(program.fingerprint(), again.fingerprint()); +} + +TEST(MetalBindingAllocatorTest, OverflowReportsSamplerLimit) { + // Mix combined image-samplers and separate samplers so reserved texture-pinned + // sampler indices and gap-filled separate indices together exceed Metal's 16. + std::string src = "#version 450\nlayout(location=0) out vec4 o;\n"; + for (int i = 0; i < 8; ++i) { + src += "layout(set=0, binding=" + std::to_string(i) + ") uniform sampler2D t" + std::to_string(i) + ";\n"; + } + for (int i = 8; i < 18; ++i) { + src += "layout(set=0, binding=" + std::to_string(i) + ") uniform sampler s" + std::to_string(i) + ";\n"; + } + src += "void main(){ o = texture(t0, vec2(0.0)) + vec4(1.0); }\n"; + + auto spirv = compileGlsl(src.c_str(), ShaderStage::eFragment); + ASSERT_FALSE(spirv.empty()); + auto alloc = MetalBindingAllocator::fromProgram({spirv}, {}); + const std::string err = alloc.overflowError(); + EXPECT_FALSE(err.empty()); + EXPECT_NE(err.find("sampler"), std::string::npos); +} + +} // namespace vne::sc + +#endif // VNE_SC_GLSLANG_ENABLED diff --git a/tests/shader_artifact_cache_test.cpp b/tests/shader_artifact_cache_test.cpp index 1d50da7..68df53e 100644 --- a/tests/shader_artifact_cache_test.cpp +++ b/tests/shader_artifact_cache_test.cpp @@ -78,6 +78,31 @@ TEST_F(ShaderArtifactCacheTest, MetalLayoutIgnoredWhenMslNotAmongTargets) { EXPECT_EQ(ShaderArtifactCache::makeKey(req, targets, defaults), ShaderArtifactCache::makeKey(req, targets, custom)); } +TEST_F(ShaderArtifactCacheTest, DifferentMetalProgramFingerprintProducesDifferentKey) { + CompileRequest req; + req.source = "void main() {}"; + std::vector targets = {CrossTarget::eMSL}; + MetalBindingLayout layout; + EXPECT_NE(ShaderArtifactCache::makeKey(req, targets, layout, 0x1111ULL), + ShaderArtifactCache::makeKey(req, targets, layout, 0x2222ULL)); + EXPECT_EQ(ShaderArtifactCache::makeKey(req, targets, layout, 0xABCDULL), + ShaderArtifactCache::makeKey(req, targets, layout, 0xABCDULL)); +} + +TEST_F(ShaderArtifactCacheTest, MakeProgramFingerprintIsSourceLevelAndDeterministic) { + CompileRequest a; + a.source = "void main() { int x = 1; }"; + a.stage = ShaderStage::eVertex; + CompileRequest b = a; + b.source = "void main() { int x = 2; }"; + MetalBindingLayout layout; + + EXPECT_EQ(ShaderArtifactCache::makeProgramFingerprint({a}, layout), + ShaderArtifactCache::makeProgramFingerprint({a}, layout)); + EXPECT_NE(ShaderArtifactCache::makeProgramFingerprint({a}, layout), + ShaderArtifactCache::makeProgramFingerprint({b}, layout)); +} + TEST_F(ShaderArtifactCacheTest, StoreAndLookupRoundTrip) { namespace fs = std::filesystem; auto tmp = makeUniqueTempPath("vnesc_shader_artifact_cache_roundtrip"); diff --git a/tests/shader_cross_compiler_test.cpp b/tests/shader_cross_compiler_test.cpp index e3bd734..b938114 100644 --- a/tests/shader_cross_compiler_test.cpp +++ b/tests/shader_cross_compiler_test.cpp @@ -50,7 +50,7 @@ TEST_F(ShaderCrossCompilerTest, WgslReturnsUnavailableWithoutTint) { EXPECT_FALSE(ccres.ok()); EXPECT_EQ(ccres.code, ResultCode::eUnavailable); #else - GTEST_SKIP() << "Tint enabled — see TintCrossCompilerTest"; + GTEST_SKIP() << "Tint enabled - see TintCrossCompilerTest"; #endif } diff --git a/tests/shader_pipeline_builder_test.cpp b/tests/shader_pipeline_builder_test.cpp index 2b90af2..d3748d3 100644 --- a/tests/shader_pipeline_builder_test.cpp +++ b/tests/shader_pipeline_builder_test.cpp @@ -12,8 +12,38 @@ #include #include "fixtures/shader_pipeline_builder_test_fixture.h" +#include "vertexnova/sc/shader_artifact_cache.h" +#include "vertexnova/sc/shader_compiler_factory.h" +#include "vertexnova/sc/shader_frontend.h" +#include "vertexnova/sc/shader_pipeline_builder.h" + +#include +#include +#include namespace vne::sc::test { +namespace { + +class CountingFrontEnd final : public IShaderFrontEnd { + public: + explicit CountingFrontEnd(std::shared_ptr inner) + : inner_(std::move(inner)) {} + + [[nodiscard]] bool isAvailable() const noexcept override { return inner_ && inner_->isAvailable(); } + + CompileResult compile(const CompileRequest& req) override { + ++compile_count_; + return inner_->compile(req); + } + + [[nodiscard]] int compileCount() const noexcept { return compile_count_; } + + private: + std::shared_ptr inner_; + int compile_count_ = 0; +}; + +} // namespace class ShaderPipelineBuilderTest : public ShaderPipelineBuilderTestFixture {}; @@ -40,15 +70,20 @@ TEST_F(ShaderPipelineBuilderTest, FullBuildProducesValidArtifact) { TEST_F(ShaderPipelineBuilderTest, CacheHitSkipsRecompilation) { auto tmp = tempDir("vnesc_shader_pipeline_builder_cache"); - auto builder = makeGlslPipelineBuilder(); - ASSERT_NE(builder, nullptr); + auto frontend = std::make_shared(ShaderCompilerFactory::createFrontEnd(SourceLang::eGLSL)); + auto builder = std::make_shared(frontend, + ShaderCompilerFactory::createCrossCompiler(), + ShaderCompilerFactory::createReflector(), + ShaderCompilerFactory::createValidator()); auto desc = makeVertMslDesc("cached_pipeline", true, tmp.string()); auto r1 = builder->build(desc); ASSERT_TRUE(r1.ok()) << r1.error; + EXPECT_EQ(frontend->compileCount(), 1); auto r2 = builder->build(desc); ASSERT_TRUE(r2.ok()) << r2.error; + EXPECT_EQ(frontend->compileCount(), 1) << "cache hit must not re-invoke the front-end"; ASSERT_FALSE(r1.artifact.stages.empty()); ASSERT_FALSE(r2.artifact.stages.empty()); EXPECT_EQ(r1.artifact.stages[0].spirv, r2.artifact.stages[0].spirv); @@ -56,4 +91,144 @@ TEST_F(ShaderPipelineBuilderTest, CacheHitSkipsRecompilation) { removeTempDir(tmp); } +TEST_F(ShaderPipelineBuilderTest, SiblingStageChangeInvalidatesProgramCacheKeys) { + CompileRequest vert; + vert.stage = ShaderStage::eVertex; + vert.source = R"glsl( + #version 450 + layout(set = 0, binding = 0) uniform A { float a; } ua; + void main() { gl_Position = vec4(ua.a); } + )glsl"; + + CompileRequest frag_a; + frag_a.stage = ShaderStage::eFragment; + frag_a.source = R"glsl( + #version 450 + layout(location = 0) out vec4 o; + layout(set = 0, binding = 0) uniform A { float a; } ua; + void main() { o = vec4(ua.a); } + )glsl"; + + CompileRequest frag_b = frag_a; + frag_b.source = R"glsl( + #version 450 + layout(location = 0) out vec4 o; + layout(set = 0, binding = 0) uniform A { float a; } ua; + layout(set = 1, binding = 0) uniform B { float b; } ub; + void main() { o = vec4(ua.a + ub.b); } + )glsl"; + + MetalBindingLayout layout; + const auto fp_a = ShaderArtifactCache::makeProgramFingerprint({vert, frag_a}, layout); + const auto fp_b = ShaderArtifactCache::makeProgramFingerprint({vert, frag_b}, layout); + EXPECT_NE(fp_a, fp_b); + + const auto key_vert_a = ShaderArtifactCache::makeKey(vert, {CrossTarget::eMSL}, layout, fp_a); + const auto key_vert_b = ShaderArtifactCache::makeKey(vert, {CrossTarget::eMSL}, layout, fp_b); + EXPECT_NE(key_vert_a, key_vert_b); +} + +TEST_F(ShaderPipelineBuilderTest, MultiSetMetalSlotsAgreeAcrossStages) { + auto builder = makeGlslPipelineBuilder(); + ASSERT_NE(builder, nullptr); + + PipelineBuildDesc desc; + desc.name = "multi_set_program"; + desc.validate = false; + desc.use_cache = false; + desc.targets = {CrossTarget::eMSL}; + + CompileRequest vert; + vert.stage = ShaderStage::eVertex; + vert.source = R"glsl( + #version 450 + layout(location = 0) in vec3 aPos; + layout(set = 0, binding = 0) uniform Frame { mat4 mvp; } frame; + layout(set = 2, binding = 0) uniform Object { mat4 model; } object; + void main() { + gl_Position = frame.mvp * object.model * vec4(aPos, 1.0); + } + )glsl"; + + CompileRequest frag; + frag.stage = ShaderStage::eFragment; + frag.source = R"glsl( + #version 450 + layout(location = 0) out vec4 oColor; + layout(set = 0, binding = 0) uniform Frame { mat4 mvp; } frame; + layout(set = 1, binding = 0) uniform Material { vec4 color; } material; + void main() { + oColor = material.color * frame.mvp[0]; + } + )glsl"; + + desc.stages = {vert, frag}; + auto result = builder->build(desc); + ASSERT_TRUE(result.ok()) << result.error; + ASSERT_EQ(result.artifact.stages.size(), 2u); + + const auto* vs = result.artifact.findStage(ShaderStage::eVertex); + const auto* fs = result.artifact.findStage(ShaderStage::eFragment); + ASSERT_NE(vs, nullptr); + ASSERT_NE(fs, nullptr); + + auto findMetalBuffer = [](const StageReflection& sr, uint32_t set, uint32_t binding) -> std::optional { + for (const auto& b : sr.bindings) { + if (b.set == set && b.binding == binding && b.slots.metal.has_value()) { + return b.slots.metal->buffer; + } + } + return std::nullopt; + }; + + // set0 binding0 is shared - must get the same Metal buffer slot in both stages. + auto vs_frame = findMetalBuffer(vs->reflection, 0, 0); + auto fs_frame = findMetalBuffer(fs->reflection, 0, 0); + ASSERT_TRUE(vs_frame.has_value()); + ASSERT_TRUE(fs_frame.has_value()); + EXPECT_EQ(*vs_frame, *fs_frame); + + // Dense packing: set2 must not use the old flatten formula (16 + 2*32 + 0 = 80). + auto vs_object = findMetalBuffer(vs->reflection, 2, 0); + ASSERT_TRUE(vs_object.has_value()); + EXPECT_LE(*vs_object, MetalIndexLimits::kMaxBufferIndex); + EXPECT_NE(*vs_object, 80u); + + // Vulkan identity preserved on reflection metadata. + bool saw_set1 = false; + for (const auto& b : fs->reflection.bindings) { + if (b.set == 1 && b.binding == 0) { + saw_set1 = true; + EXPECT_EQ(b.set, 1u); + EXPECT_EQ(b.binding, 0u); + } + } + EXPECT_TRUE(saw_set1); + + // Emitted MSL must reference dense buffer indices (not out-of-range flatten values). + const auto* vs_msl = vs->findCrossCompiled(CrossTarget::eMSL); + ASSERT_NE(vs_msl, nullptr); + EXPECT_EQ(vs_msl->source.find("[[buffer(80)]]"), std::string::npos); +} + +TEST_F(ShaderPipelineBuilderTest, WgslLeavesVulkanSetBindingIdentity) { + auto builder = makeGlslPipelineBuilder(); + ASSERT_NE(builder, nullptr); + + PipelineBuildDesc desc; + desc.name = "wgsl_identity"; + desc.validate = false; + desc.use_cache = false; + desc.targets = {CrossTarget::eWGSL}; + desc.stages.push_back(makeVertexRequest()); + + auto result = builder->build(desc); + ASSERT_TRUE(result.ok()) << result.error; + const auto* vs = result.artifact.findStage(ShaderStage::eVertex); + ASSERT_NE(vs, nullptr); + ASSERT_FALSE(vs->reflection.bindings.empty()); + EXPECT_EQ(vs->reflection.bindings[0].set, 0u); + EXPECT_EQ(vs->reflection.bindings[0].binding, 0u); +} + } // namespace vne::sc::test diff --git a/tests/shader_pipeline_spec_includes_test.cpp b/tests/shader_pipeline_spec_includes_test.cpp index 224437d..799cfd3 100644 --- a/tests/shader_pipeline_spec_includes_test.cpp +++ b/tests/shader_pipeline_spec_includes_test.cpp @@ -21,7 +21,7 @@ namespace vne::sc::test { class ShaderPipelineSpecIncludesTest : public GlslangFrontEndTestFixture {}; -// ── include_paths → toBuildDesc wiring ─────────────────────────────────────── +// include_paths -> toBuildDesc wiring TEST_F(ShaderPipelineSpecIncludesTest, IncludePathsResolvedRelativeToSpecDir) { #ifndef VNE_SC_JSON_ENABLED @@ -72,7 +72,7 @@ TEST_F(ShaderPipelineSpecIncludesTest, EmptyIncludePathsProducesNoIncludeDirs) { #endif } -// ── end-to-end: glslang compiles a shader that uses #include ───────────────── +// end-to-end: glslang compiles a shader that uses #include TEST_F(ShaderPipelineSpecIncludesTest, GlslangCompilesShaderWithIncludedFile) { auto fe = requireGlslangFrontEnd(); diff --git a/tools/README.md b/tools/README.md index 905f793..e25ab7d 100644 --- a/tools/README.md +++ b/tools/README.md @@ -33,22 +33,22 @@ vnesc_shader_compiler \ Python batch wrapper around `vnesc_shader_compiler` with an automatic WGSL fallback via **naga**. Expands glob patterns, compiles manifests in parallel, and writes each bundle to `//`. -When `vnesc_shader_compiler` was built **without** Tint (`VNE_SC_TINT=OFF`) — which is the default and -avoids the large Dawn/Chromium dependency — the compiled bundle will have SPIR-V and MSL but no WGSL. -`compile_shaders.py` detects this automatically and runs `naga` (SPIR-V → WGSL) as a post-processing +When `vnesc_shader_compiler` was built **without** Tint (`VNE_SC_TINT=OFF`) - which is the default and +avoids the large Dawn/Chromium dependency - the compiled bundle will have SPIR-V and MSL but no WGSL. +`compile_shaders.py` detects this automatically and runs `naga` (SPIR-V -> WGSL) as a post-processing step, then patches `bundle.header` so `loadShaderBundle` finds the WGSL files at runtime. ### Quick-start (no Tint needed) ```bash -# 1. Get naga — either via cargo or the download helper +# 1. Get naga - either via cargo or the download helper cargo install naga-cli # option A python3 tools/download_naga.py # option B: downloads pre-built macOS binary to tools/macos/ # 2. Build the C++ compiler (one-time; no Tint required) cmake --build build/Debug/build-macos-clang-17.0.0 --target vnesc_shader_compiler -# 3. Compile shaders — WGSL is generated automatically via naga +# 3. Compile shaders - WGSL is generated automatically via naga python3 tools/compile_shaders.py \ --manifest "shaders/src/teapot.manifest.json" \ --output shaders/gen @@ -77,7 +77,7 @@ compile_shaders.py --manifest ... --output [options] `compile_shaders.py` reads `bundle.header` after each compile, detects stages whose `wgsl_file` field is empty, runs `naga ` for each such stage, then re-writes `bundle.header` with the new WGSL filenames. The existing `reflection.bin` -is unchanged — WGSL generation is purely additive. +is unchanged - WGSL generation is purely additive. naga is searched in order: 1. `--naga ` flag @@ -95,4 +95,4 @@ from the wgpu GitHub releases and places it in `tools/macos/`. python3 tools/download_naga.py [--version v24.0.0] [--output tools/macos/] ``` -After this, `compile_shaders.py` auto-detects the binary — no `--naga` flag needed. +After this, `compile_shaders.py` auto-detects the binary - no `--naga` flag needed. diff --git a/tools/compile_shaders.py b/tools/compile_shaders.py index cb255cd..ca418c0 100755 --- a/tools/compile_shaders.py +++ b/tools/compile_shaders.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""vnesc offline shader compiler — wrapper around vnesc_shader_compiler with naga WGSL fallback. +"""vnesc offline shader compiler - wrapper around vnesc_shader_compiler with naga WGSL fallback. Usage: compile_shaders.py --manifest --output @@ -157,7 +157,7 @@ def patch_bundle_with_wgsl(bundle_dir: Path, wgsl_map: dict[int, tuple[str, str] # --------------------------------------------------------------------------- -# Naga: SPIR-V → WGSL +# Naga: SPIR-V -> WGSL # --------------------------------------------------------------------------- _SCRIPT_DIR = Path(__file__).resolve().parent @@ -186,7 +186,7 @@ def find_naga(override: str | None) -> Path | None: def _naga_wgsl_entry(stage_type: int) -> str: - """naga preserves GLSL entry point names — all our GLSL shaders use 'main'.""" + """naga preserves GLSL entry point names - all our GLSL shaders use 'main'.""" return "main" @@ -209,7 +209,7 @@ def generate_wgsl_for_bundle(bundle_dir: Path, naga: Path, verbose: bool, dry_ru stage = stages[idx] spv_file = stage.get("spirv_file", "") if not spv_file: - sys.stderr.write(f"error: stage {idx} has no SPIR-V file — cannot generate WGSL\n") + sys.stderr.write(f"error: stage {idx} has no SPIR-V file - cannot generate WGSL\n") return False spv_path = bundle_dir / spv_file @@ -217,7 +217,7 @@ def generate_wgsl_for_bundle(bundle_dir: Path, naga: Path, verbose: bool, dry_ru sys.stderr.write(f"error: SPIR-V not found: {spv_path}\n") return False - # Derive WGSL filename from SPV filename: foo.vert.spv → foo.vert.wgsl + # Derive WGSL filename from SPV filename: foo.vert.spv -> foo.vert.wgsl stem = spv_path.stem # e.g. "teapot.vert" wgsl_name = stem + ".wgsl" wgsl_path = bundle_dir / wgsl_name @@ -228,7 +228,7 @@ def generate_wgsl_for_bundle(bundle_dir: Path, naga: Path, verbose: bool, dry_ru print(f"[dry-run] {' '.join(cmd)}") else: if verbose: - print(f" naga: {spv_file} → {wgsl_name}") + print(f" naga: {spv_file} -> {wgsl_name}") result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: sys.stderr.write(f"error: naga failed for {spv_path}:\n{result.stderr}\n") @@ -326,7 +326,7 @@ def compile_one( for bundle_dir in bundle_dirs: if bundle_dir.is_dir() and _bundle_missing_wgsl(bundle_dir): if verbose: - print(f" WGSL missing — running naga fallback for: {bundle_dir.name}") + print(f" WGSL missing - running naga fallback for: {bundle_dir.name}") if not generate_wgsl_for_bundle(bundle_dir, naga, verbose=verbose, dry_run=False): return False, str(manifest), f"naga WGSL generation failed for {bundle_dir}" @@ -358,11 +358,11 @@ def expand_manifests(patterns: list[str]) -> list[Path]: def main() -> int: parser = argparse.ArgumentParser( - description="vnesc offline shader compiler — wrapper with naga WGSL fallback", + description="vnesc offline shader compiler - wrapper with naga WGSL fallback", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "WGSL fallback:\n" - " When the bundle is missing WGSL files, naga is used to convert SPIR-V→WGSL\n" + " When the bundle is missing WGSL files, naga is used to convert SPIR-V->WGSL\n" " and bundle.header is patched automatically.\n" " Install naga: cargo install naga-cli\n" " Or download: python3 tools/download_naga.py\n" @@ -406,7 +406,7 @@ def main() -> int: print(f"naga: {naga}") elif not naga: print( - "info: naga not found — WGSL will be skipped " + "info: naga not found - WGSL will be skipped " "(install via: cargo install naga-cli or python3 tools/download_naga.py)" ) @@ -450,7 +450,7 @@ def main() -> int: return 1 if not args.dry_run: - print(f"compiled {len(manifests)} manifest(s) → {output_root}") + print(f"compiled {len(manifests)} manifest(s) -> {output_root}") return 0 diff --git a/tools/download_naga.py b/tools/download_naga.py index 1032b62..d19c54a 100644 --- a/tools/download_naga.py +++ b/tools/download_naga.py @@ -102,7 +102,7 @@ def download_naga(version: str, output_dir: Path, verbose: bool) -> int: with zf.open(name) as src, open(dest, "wb") as dst: dst.write(src.read()) if verbose: - print(f" extracted: {name} → naga") + print(f" extracted: {name} -> naga") break else: sys.stderr.write("error: naga binary not found in zip\n") diff --git a/tools/shader_compiler_cli/CMakeLists.txt b/tools/shader_compiler_cli/CMakeLists.txt index 7a37400..ec11f84 100644 --- a/tools/shader_compiler_cli/CMakeLists.txt +++ b/tools/shader_compiler_cli/CMakeLists.txt @@ -2,11 +2,24 @@ # Copyright (c) 2026 Ajeet Singh Yadav. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License") # -# vnesc_shader_compiler — offline shader compilation CLI tool +# Author: Ajeet Singh Yadav +# Created: February 2026 +# +# Autodoc: yes +#============================================================================== + +# vnesc_shader_compiler - offline shader compilation CLI tool + +#============================================================================== +# Build Executable # #============================================================================== add_executable(vnesc_shader_compiler main.cpp) +#============================================================================== +# Dependencies # +#============================================================================== + target_link_libraries(vnesc_shader_compiler PRIVATE vnesc) target_compile_features(vnesc_shader_compiler PRIVATE cxx_std_20) @@ -14,6 +27,10 @@ set_target_properties(vnesc_shader_compiler PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) +#============================================================================== +# Installation # +#============================================================================== + install(TARGETS vnesc_shader_compiler RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT vnesc_tools diff --git a/tools/shader_compiler_cli/main.cpp b/tools/shader_compiler_cli/main.cpp index 143474b..33460cf 100644 --- a/tools/shader_compiler_cli/main.cpp +++ b/tools/shader_compiler_cli/main.cpp @@ -77,7 +77,7 @@ int main(int argc, char** argv) { const std::filesystem::path bundle_path(output_dir); // A manifest with no "variants" behaves as exactly one implicit, unnamed - // variant — no defines, no bundle-path suffix — reproducing prior behavior + // variant - no defines, no bundle-path suffix - reproducing prior behavior // for every manifest that doesn't opt into variants. std::vector variants = manifest->variants; if (variants.empty()) { @@ -138,7 +138,7 @@ int main(int argc, char** argv) { return 1; } - // Only the first variant emits binding decls — it's the "schema owner" for + // Only the first variant emits binding decls - it's the "schema owner" for // the family; list a variant whose resource usage is a superset of the // others first, since later variants never contribute back into the file. if (variant_index == 0 && !manifest->emit_binding_decls.empty()) { @@ -161,7 +161,7 @@ int main(int argc, char** argv) { std::cout << "Bundle written to: " << this_output << "\n"; std::cout << " Stages: " << result.artifact.stages.size() << "\n"; for (const auto& stage : result.artifact.stages) { - std::cout << " - stage " << static_cast(stage.stage) << " | spirv=" << stage.spirv.size() << " words" + std::cout << " - stage " << static_cast(stage.stage) << " | spirv=" << stage.spirv.size() << " words" << " | cross=" << stage.cross_compiled.size() << " target(s)\n"; } }