diff --git a/.claude/skills/address-review/SKILL.md b/.claude/skills/address-review/SKILL.md index 782a3ffd..375a0475 100644 --- a/.claude/skills/address-review/SKILL.md +++ b/.claude/skills/address-review/SKILL.md @@ -1,7 +1,7 @@ --- name: address-review description: Fetch CodeRabbit (or other bot) review comments from the current PR and address each one with code changes. -argument-hint: "[PR number] — defaults to the PR for the current branch" +argument-hint: "[PR number] -- defaults to the PR for the current branch" --- Address review comments on a GitHub PR. Fetches comments, applies fixes, commits. @@ -35,7 +35,7 @@ Also fetch the PR review body (walkthrough / summary) but only act on ### 3. Triage comments **Default: fix everything.** Trivial fixes (add an attribute, remove an -unused import, rename a file) are still valid — they improve the codebase. +unused import, rename a file) are still valid -- they improve the codebase. Do not skip a comment because it's "just a nit." The whole point of this skill is to handle the tedious stuff. @@ -43,9 +43,9 @@ For each comment, classify as one of: - **FIX**: the comment requests a concrete change. This is the default. Includes: add attribute, rename, remove dead code, fix a race condition, add a test case, quote a path, catch an exception, improve an error - message — no matter how small. + message -- no matter how small. - **REJECT**: the suggestion is wrong or conflicts with project conventions - (CLAUDE.md). You must state *why* it's wrong — "not important" is not a + (CLAUDE.md). You must state *why* it's wrong -- "not important" is not a valid reason. Examples: suggesting a pattern the codebase explicitly avoids, proposing a change that breaks ABI, misunderstanding the code. - **STALE**: the file/line no longer exists in the current code (already @@ -53,11 +53,11 @@ For each comment, classify as one of: Present the triage to the user: ``` -PR #27 — 10 comments found - 1. [FIX] renderWorld.h:346 — add [[nodiscard]] - 2. [FIX] worker.h:114 — remove default-constructibility requirement - 3. [REJECT] editorApplication.cpp:500 — suggests X but CLAUDE.md says Y - 4. [STALE] oldFile.cpp:30 — file no longer exists +PR #27 -- 10 comments found + 1. [FIX] renderWorld.h:346 -- add [[nodiscard]] + 2. [FIX] worker.h:114 -- remove default-constructibility requirement + 3. [REJECT] editorApplication.cpp:500 -- suggests X but CLAUDE.md says Y + 4. [STALE] oldFile.cpp:30 -- file no longer exists ... Proceed with 8 fixes? (y/n) ``` @@ -68,7 +68,7 @@ Wait for user confirmation before making changes. For each actionable comment: 1. Read the file at the referenced path -2. Locate the relevant code (line number is a hint, not exact — find by context) +2. Locate the relevant code (line number is a hint, not exact -- find by context) 3. Apply the fix using the Edit tool 4. If the comment contains a diff suggestion (```suggestion block), apply it directly @@ -102,7 +102,7 @@ gh api repos/{owner}/{repo}/pulls/{number}/comments/{comment_id}/replies \ ## Notes -- Never blindly apply suggestions without reading the surrounding code — +- Never blindly apply suggestions without reading the surrounding code -- the suggestion may be based on stale context - If a suggestion conflicts with project conventions (CLAUDE.md), skip it and explain why diff --git a/.claude/skills/contribute-framework/SKILL.md b/.claude/skills/contribute-framework/SKILL.md index d3bdf32f..c4e79b98 100644 --- a/.claude/skills/contribute-framework/SKILL.md +++ b/.claude/skills/contribute-framework/SKILL.md @@ -16,7 +16,7 @@ cd tools/framework git stash git checkout main git pull --ff-only origin main -git stash pop # conflict → stop, ask user +git stash pop # conflict -> stop, ask user ``` ### 2. Bootstrap test driver & run tests @@ -31,8 +31,8 @@ bash test_driver/tools/framework/bootstrap.sh test_driver cd test_driver && ./repo test ``` -**Fail → fix the issue and re-run tests. Do NOT bump version or proceed until -tests pass.** Loop fix → test until green. +**Fail -> fix the issue and re-run tests. Do NOT bump version or proceed until +tests pass.** Loop fix -> test until green. ### 3. Clean up test driver @@ -48,7 +48,7 @@ rm -rf test_driver ### 4. Bump version & changelog -- Patch-increment `version` in `pyproject.toml` (e.g. `0.7.26` → `0.7.27`) +- Patch-increment `version` in `pyproject.toml` (e.g. `0.7.26` -> `0.7.27`) - Prepend to `CHANGELOG.md`: ```markdown @@ -68,7 +68,7 @@ git push origin main ### 6. Wait for CI tag -CI auto-tags `v` on green. Poll `git fetch origin --tags && git tag -l "v"` every ~30s, up to 3 min. Timeout → print manual finish instructions and stop. +CI auto-tags `v` on green. Poll `git fetch origin --tags && git tag -l "v"` every ~30s, up to 3 min. Timeout -> print manual finish instructions and stop. ### 7. Pin in parent repo @@ -77,4 +77,4 @@ cd tools/framework && git checkout v cd ../.. && git add tools/framework && git commit -m "Pin repokit submodule to v" ``` -Print: old version → new version, changelog entry, pin commit hash. +Print: old version -> new version, changelog entry, pin commit hash. diff --git a/.claude/skills/rotate-branch/SKILL.md b/.claude/skills/rotate-branch/SKILL.md index 4b539cbc..5aeab96a 100644 --- a/.claude/skills/rotate-branch/SKILL.md +++ b/.claude/skills/rotate-branch/SKILL.md @@ -1,7 +1,7 @@ --- name: rotate-branch description: After a PR is merged, prune the old dev branch and start a fresh one off develop. -argument-hint: "[branch name] — defaults to dev/rendering-next" +argument-hint: "[branch name] -- defaults to dev/rendering-next" --- Rotate a dev branch after its PR has been merged into develop. diff --git a/.claude/skills/triage-ci/SKILL.md b/.claude/skills/triage-ci/SKILL.md index aeedbdd0..61298dfc 100644 --- a/.claude/skills/triage-ci/SKILL.md +++ b/.claude/skills/triage-ci/SKILL.md @@ -1,7 +1,7 @@ --- name: triage-ci description: Wait for CI pipeline to finish, then triage and fix any failures. Loops until CI is green or the turn limit is reached. Use this after pushing code, opening a PR, or whenever the user says "check CI", "wait for CI", "triage CI", "fix CI", or asks about build/test failures on the current branch. -argument-hint: "[max turns] [PR number] — defaults to 3 turns, current branch's PR" +argument-hint: "[max turns] [PR number] -- defaults to 3 turns, current branch's PR" --- Wait for the CI pipeline to complete, then diagnose and fix failures. @@ -11,8 +11,8 @@ limit is reached. ## Arguments Parse the argument string for: -- A small integer (1-10) → max turns (default: 3) -- A larger integer or `#N` → PR number (default: current branch's PR) +- A small integer (1-10) -> max turns (default: 3) +- A larger integer or `#N` -> PR number (default: current branch's PR) Examples: `/triage-ci` (3 turns, auto PR), `/triage-ci 5` (5 turns), `/triage-ci 29` (PR #29, 3 turns), `/triage-ci 5 29` (5 turns, PR #29). @@ -21,8 +21,8 @@ Examples: `/triage-ci` (3 turns, auto PR), `/triage-ci 5` (5 turns), ``` for turn in 1..max_turns: - 1. Wait for CI (background — user can work while waiting) - 2. Check results — if green, report success and stop + 1. Wait for CI (background -- user can work while waiting) + 2. Check results -- if green, report success and stop 3. Triage failures 4. Apply fixes, build and test locally 5. Push and go to next turn @@ -55,8 +55,8 @@ gh run watch --exit-status ``` This lets the user continue working. When the background task completes, -a notification arrives — pick up from step 2 at that point. Tell the -user: "CI run is in progress. I'm watching in the background — +a notification arrives -- pick up from step 2 at that point. Tell the +user: "CI run is in progress. I'm watching in the background -- you'll be notified when it finishes. Feel free to keep working." ### 2. Check results @@ -75,18 +75,18 @@ gh run view --job --log-failed ``` Classify as: -- **COMPILE** — build error. Read the error, find the file/line, fix it. -- **TEST** — test failure. Check if it's a real regression or stale test. -- **INFRA** — CI infrastructure (missing artifacts, timeouts, network). +- **COMPILE** -- build error. Read the error, find the file/line, fix it. +- **TEST** -- test failure. Check if it's a real regression or stale test. +- **INFRA** -- CI infrastructure (missing artifacts, timeouts, network). Check if caused by a code change or transient. -- **FLAKY** — passes locally, fails in CI with no code cause. +- **FLAKY** -- passes locally, fails in CI with no code cause. Present the triage: ``` -Turn 1/3 — CI run — 2 checks failed +Turn 1/3 -- CI run -- 2 checks failed 1. [COMPILE] Build (windows-x64, Release) - error at file.cpp:42 — description + error at file.cpp:42 -- description Fix: ... 2. [INFRA] Build (emscripten, Release) @@ -115,12 +115,12 @@ Then loop back to step 1 for the next turn. - Cascade failures are common: one build failure causes downstream jobs to fail (e.g. missing artifacts). Identify the root cause first. -- `max-parallel: 1` in the CI matrix means jobs run sequentially — +- `max-parallel: 1` in the CI matrix means jobs run sequentially -- if the first matrix entry fails, later entries may fail for dependent reasons. - Always build and test locally before pushing a fix to avoid churn. - INFRA failures that are purely transient (network blip, runner OOM) can be retried without code changes: `gh run rerun --failed`. -- Do NOT use `sleep` for polling — the hook blocks it. Use +- Do NOT use `sleep` for polling -- the hook blocks it. Use `run_in_background: true` on `gh run watch` and wait for the notification instead. diff --git a/.claude/skills/usd/SKILL.md b/.claude/skills/usd/SKILL.md index e286a525..a0b05455 100644 --- a/.claude/skills/usd/SKILL.md +++ b/.claude/skills/usd/SKILL.md @@ -34,10 +34,10 @@ Organize prims under `/Root`: ``` /Root - /Materials ← Scope containing all materials + /Materials <- Scope containing all materials /MatName - /Shader ← UsdPreviewSurface shader node - /Geometry ← Xform grouping geometry (optional, flat layout OK for small scenes) + /Shader <- UsdPreviewSurface shader node + /Geometry <- Xform grouping geometry (optional, flat layout OK for small scenes) /MeshName /Lights /LightName @@ -47,26 +47,26 @@ For simple scenes with few prims, flat layout under `/Root` is fine (no `/Geomet ## Supported Prim Types -PTStudio adapters support these types. Only use these — anything else is silently ignored. +PTStudio adapters support these types. Only use these -- anything else is silently ignored. ### Geometry -- `Cube` — `double size` -- `Sphere` — `double radius` -- `Cylinder` — `token axis`, `double height`, `double radius` -- `Cone` — `token axis`, `double height`, `double radius` -- `Capsule` — `token axis`, `double height`, `double radius` -- `Mesh` — `point3f[] points`, `int[] faceVertexCounts`, `int[] faceVertexIndices`, `normal3f[] normals` (optional) +- `Cube` -- `double size` +- `Sphere` -- `double radius` +- `Cylinder` -- `token axis`, `double height`, `double radius` +- `Cone` -- `token axis`, `double height`, `double radius` +- `Capsule` -- `token axis`, `double height`, `double radius` +- `Mesh` -- `point3f[] points`, `int[] faceVertexCounts`, `int[] faceVertexIndices`, `normal3f[] normals` (optional) ### Lights All directional area lights (RectLight, DiskLight, DistantLight) are centered in the XY plane and **emit along -Z** by default. Rotate to aim them. -- `DistantLight` — `float inputs:intensity`, `color3f inputs:color`, `float inputs:angle`. Emits along -Z. -- `SphereLight` — `float inputs:intensity`, `color3f inputs:color`, `float inputs:radius`. Omnidirectional. -- `RectLight` — `float inputs:intensity`, `color3f inputs:color`, `float inputs:width`, `float inputs:height`. Emits along -Z. -- `DiskLight` — `float inputs:intensity`, `color3f inputs:color`, `float inputs:radius`. Emits along -Z. -- `DomeLight` — `float inputs:intensity`, `color3f inputs:color`, `asset inputs:texture:file`. Emits inward. +- `DistantLight` -- `float inputs:intensity`, `color3f inputs:color`, `float inputs:angle`. Emits along -Z. +- `SphereLight` -- `float inputs:intensity`, `color3f inputs:color`, `float inputs:radius`. Omnidirectional. +- `RectLight` -- `float inputs:intensity`, `color3f inputs:color`, `float inputs:width`, `float inputs:height`. Emits along -Z. +- `DiskLight` -- `float inputs:intensity`, `color3f inputs:color`, `float inputs:radius`. Emits along -Z. +- `DomeLight` -- `float inputs:intensity`, `color3f inputs:color`, `asset inputs:texture:file`. Emits inward. ### Materials (UsdPreviewSurface only) @@ -131,7 +131,7 @@ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] Always set `orientation = "rightHanded"` on custom meshes. In USD's `rightHanded` convention, vertices go **clockwise** when viewed from the front face. This matches how the renderer's projection and culling work together (glm right-handed -projection flips Z → CW world becomes CCW clip → `WGPUFrontFace_CCW` matches). +projection flips Z -> CW world becomes CCW clip -> `WGPUFrontFace_CCW` matches). For a +Y-facing ground plane, wind vertices **CW when viewed from above**: @@ -174,8 +174,8 @@ over "Root" - Always include at least one light so the scene is visible. - For general-purpose scenes: a `DistantLight` rotated ~(-45, 30, 0) at intensity 1.0 works well as a sun. -- For area-light testing: use `RectLight` or `DiskLight` with higher intensity (100–500+) since they are physically-sized emitters. -- `DomeLight` at low intensity (0.5–1.0) provides ambient fill. +- For area-light testing: use `RectLight` or `DiskLight` with higher intensity (100-500+) since they are physically-sized emitters. +- `DomeLight` at low intensity (0.5-1.0) provides ambient fill. ## Verification @@ -192,8 +192,8 @@ For specific debug output: ## What NOT to do -- Do not use schema types not listed above (e.g. UsdGeomBasisCurves, UsdSkelRoot) — they are not supported. -- Do not use texture file references — the editor has no texture loading pipeline yet. -- Do not use `class` prims or inherits composition — keep scenes self-contained. +- Do not use schema types not listed above (e.g. UsdGeomBasisCurves, UsdSkelRoot) -- they are not supported. +- Do not use texture file references -- the editor has no texture loading pipeline yet. +- Do not use `class` prims or inherits composition -- keep scenes self-contained. - Do not use animation / time samples unless explicitly asked. -- Do not omit `xformOpOrder` when using any xformOp — USD requires it. +- Do not omit `xformOpOrder` when using any xformOp -- USD requires it. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b6d0ab7..7df1d3d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,9 +44,6 @@ jobs: runs-on: ${{ matrix.os }} strategy: fail-fast: false - # Windows must finish before Emscripten starts (USDZ scene dependency). - # max-parallel: 1 ensures sequential execution in platform order. - max-parallel: 1 matrix: platform: [windows-x64, emscripten] build_type: [Release] @@ -108,30 +105,19 @@ jobs: restore-keys: | ${{ matrix.platform }}-conan-${{ hashFiles('conanfile.py') }} - # Windows prebuild generates USDZ scenes; upload them so the - # Emscripten build can embed them in WASM via --embed-file. - # usdz_pack is a native host tool that cannot cross-compile to WASM. - - name: Download USDZ scenes + # usdz_pack is a native host tool that can't cross-compile to WASM. + # Emscripten CI builds it natively first (via its own Conan package, + # isolated from the root project's Conan graph), then the main build + # picks up the generated .usdz scenes from assets/scenes/. + - name: Build host tools (native Linux) if: matrix.platform == 'emscripten' - uses: actions/download-artifact@v4 - with: - name: usdz-scenes - path: assets/scenes/ + run: ./repo build --platform linux-x64 --build-type ${{ matrix.build_type }} --host-tools-only - name: Build project run: ${{ matrix.repo }} build --platform ${{ matrix.platform }} --build-type ${{ matrix.build_type }} env: PTSTUDIO_GPU_BACKEND: ${{ matrix.platform == 'windows-x64' && 'D3D12' || '' }} - - name: Upload USDZ scenes - if: matrix.platform == 'windows-x64' - uses: actions/upload-artifact@v4 - with: - name: usdz-scenes - path: assets/scenes/*.usdz - if-no-files-found: error - retention-days: 3 - - name: Package build artifacts run: ${{ matrix.repo }} package --platform ${{ matrix.platform }} --build-type ${{ matrix.build_type }} diff --git a/CLAUDE.md b/CLAUDE.md index 742f9757..71ca2dda 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,12 +12,12 @@ Uses the [repokit](tools/framework/README.md) framework. See that README for CLI ### Conan `full_deploy` Invariant -`conan install --deployer=full_deploy` copies packages into `_build//deps/` and rewrites `buildenv_info` paths to the deploy folder. But `conf_info` (CMake toolchain, compiler paths) stays pointing to the original Conan package. On Windows CI (workspace on D:, Conan cache on C:) these end up on different drives. Never export env vars from Conan recipes that must resolve to the same root as the compiler — let the tool derive them from its own install location instead. +`conan install --deployer=full_deploy` copies packages into `_build//deps/` and rewrites `buildenv_info` paths to the deploy folder. But `conf_info` (CMake toolchain, compiler paths) stays pointing to the original Conan package. On Windows CI (workspace on D:, Conan cache on C:) these end up on different drives. Never export env vars from Conan recipes that must resolve to the same root as the compiler -- let the tool derive them from its own install location instead. ### Emscripten Build - emsdk is a Conan `tool_requires`, Dawn version pins the emdawnwebgpu port version -- The emsdk recipe does NOT export `EM_CACHE` or `EM_CONFIG` to consumers — emscripten defaults to `/cache/`, which is always on the same drive as `em++` +- The emsdk recipe does NOT export `EM_CACHE` or `EM_CONFIG` to consumers -- emscripten defaults to `/cache/`, which is always on the same drive as `em++` ### OpenUSD + TBB on Emscripten @@ -25,21 +25,40 @@ Static-linking OpenUSD via Conan on Emscripten has several non-obvious failure m - **Constructor dead-stripping**: OpenUSD's plugin discovery relies on `Plug_InitConfig`, an `__attribute__((constructor))` in `initConfig.cpp`. When USD libraries are separate static `.a` archives (Conan components), the linker drops `initConfig.o` because nothing references its symbols. Fix: `--whole-archive` on `libusd_plug.a` (see `CMakeLists.txt`). - **TBB static init crashes**: Setting `PXR_WORK_THREAD_LIMIT` to non-zero forces `tbb::global_control` creation during `__wasm_call_ctors`, before TBB's function table is ready. Leave it at default (0). -- **TBB + EMSCRIPTEN_WITHOUT_PTHREAD**: The Conan profile passes `-pthread` globally, so TBB source sees `__EMSCRIPTEN_PTHREADS__`. Don't override with `EMSCRIPTEN_WITHOUT_PTHREAD` — it creates contradictory state. +- **TBB + EMSCRIPTEN_WITHOUT_PTHREAD**: The Conan profile passes `-pthread` globally, so TBB source sees `__EMSCRIPTEN_PTHREADS__`. Don't override with `EMSCRIPTEN_WITHOUT_PTHREAD` -- it creates contradictory state. - **"Cannot create a log file"**: A misleading secondary error from USD's crash handler. The real error is whatever triggered the abort; this message means `ArchGetTmpDir()` failed to create a temp file on the WASM virtual filesystem. -- **Plugin resources**: Embed full `resources/` directories (not just `plugInfo.json`) — `generatedSchema.usda` is required for type registration. +- **Plugin resources**: Embed full `resources/` directories (not just `plugInfo.json`) -- `generatedSchema.usda` is required for type registration. ### Prebuild Tool Config -Tool configs (slangc, shader_codegen, embed) live at the top level of `config.yaml`. The `build.prebuild` section just lists the tools to run (as empty dicts `{}`). When invoked — whether standalone or as a prebuild step — `invoke_tool` reads the top-level config via `config.get(tool_name, {})`. +Tool configs (slangc, shader_codegen, embed) live at the top level of `config.yaml`. The `build.prebuild` section just lists the tools to run (as empty dicts `{}`). When invoked -- whether standalone or as a prebuild step -- `invoke_tool` reads the top-level config via `config.get(tool_name, {})`. ### Embed Tool Resource Keys The `embed` prebuild step generates C++ headers with `get_resource(key)` lookup. Resource keys are derived from input file paths by stripping the longest common prefix across all inputs in a group. Adding a new file to an embed group can change the common prefix and break existing lookups. When adding files to an embed resource group, always check that existing `get_resource()` callers still use the correct key. +### Build-time Tools: Python vs C++ + +Two distinct kinds of build-time tools, under different trees: + +- **Python tools** live in `tools/repo_tools/` and are invoked by the repo CLI framework (`./repo `). Examples: `format`, `slangc` (Python wrapper over libslang), `shader_codegen`, `embed`, `clean`, `test`, `build`, `package`, `publish`, `usdz` (driver that invokes the `usdz_pack` binary). These run in the repo's managed venv -- no compilation needed, just Python imports. +- **C++ tools** live in `tools/conan//` as standalone Conan packages. Examples: `usdz_pack` (wraps `UsdUtilsCreateNewUsdzPackage` from OpenUSD). Each has its own `conanfile.py` + `CMakeLists.txt` and builds into a native executable. These can't cross-compile to WASM, so Emscripten builds consume the scenes/outputs they produce rather than invoking them directly. + +Python tools run anywhere Python does. C++ tools need a native toolchain matching the host OS. + +### Linux Tool Builds (Docker) + +C++ build-time tools (currently `usdz_pack`) can be built on Linux via Docker for local CI-matching iteration: + + bash tools/docker/build-tools.sh + +First build takes ~30-40 min (OpenUSD + TBB + OpenSubdiv compiled from source). Subsequent builds reuse the `pts-conan-cache` Docker volume and finish in seconds on a cache hit. The `pts-managed` volume overlays `tools/framework/_managed/` so Windows Python/venv artifacts on the bind-mounted workspace don't collide with the Linux ones. Requires Docker Desktop or Docker Engine. + +For CI, `./repo build --host-tools-only` does the same on the Linux runner directly -- builds each C++ host tool via its own Conan package (isolated from the root project's Conan graph) and runs only the prebuild steps that depend on those tools (e.g. `usdz` packaging). The Emscripten job runs this before the cross-build so it has freshly-generated `.usdz` scenes to `--embed-file`. + ### Tracy Profiler (debug builds only) -Tracy 0.13.1's static `s_profiler` deadlocks at process exit on Windows if `` is included in widely-used headers — the changed static init ordering causes Tracy's destructor to run after WinSock cleanup, and its profiler thread hangs in `accept()`. **Never include `` (or headers that transitively include it, like `backgroundTask.h`) in `.h` files that are widely included.** Forward-declare and include in `.cpp` only. The proper fix is rebuilding Tracy with `TRACY_DELAYED_INIT=ON` + `TRACY_MANUAL_LIFETIME=ON`. +Tracy 0.13.1's static `s_profiler` deadlocks at process exit on Windows if `` is included in widely-used headers -- the changed static init ordering causes Tracy's destructor to run after WinSock cleanup, and its profiler thread hangs in `accept()`. **Never include `` (or headers that transitively include it, like `backgroundTask.h`) in `.h` files that are widely included.** Forward-declare and include in `.cpp` only. The proper fix is rebuilding Tracy with `TRACY_DELAYED_INIT=ON` + `TRACY_MANUAL_LIFETIME=ON`. ## Visual Verification @@ -59,32 +78,32 @@ Use `--capture-and-quit` to verify rendering changes without manual inspection: ## Verification -Never declare a feature "working" based on build/test passing alone. For runtime behavior (rendering, hot-reload, UI), always launch the application (`./repo launch editor`) and verify visually or via log output before concluding and committing. Add diagnostic logging when needed to confirm correctness — guessing at root causes from code alone leads to wasted cycles. `./repo launch editor` returns the editor's log output directly — use it. +Never declare a feature "working" based on build/test passing alone. For runtime behavior (rendering, hot-reload, UI), always launch the application (`./repo launch editor`) and verify visually or via log output before concluding and committing. Add diagnostic logging when needed to confirm correctness -- guessing at root causes from code alone leads to wasted cycles. `./repo launch editor` returns the editor's log output directly -- use it. ## Debug MRT Targets & Device Limits -Scene passes can declare debug MRT outputs (Normals, Base Color, etc.) via `debug_target_names()`. These are gated at runtime by `maxColorAttachmentBytesPerSample` — the WebGPU spec's `renderTargetPixelByteCost` for RGBA8Unorm is 8 bytes (not 4), so 5 attachments cost 40 bytes, exceeding the 32-byte limit on instrumented runtimes (RenderDoc, NSight). +Scene passes can declare debug MRT outputs (Normals, Base Color, etc.) via `debug_target_names()`. These are gated at runtime by `maxColorAttachmentBytesPerSample` -- the WebGPU spec's `renderTargetPixelByteCost` for RGBA8Unorm is 8 bytes (not 4), so 5 attachments cost 40 bytes, exceeding the 32-byte limit on instrumented runtimes (RenderDoc, NSight). **How it works:** - `IScenePass::setup()` queries device limits and computes an all-or-nothing `m_allowed_debug_count` (all debug targets fit, or none) - `effective_debug_target_names()` returns the gated count; the editor UI and frame graph use this -- `load_pass_shader(resource_key)` automatically selects the no-debug shader variant when targets are disabled — passes just call this instead of `ShaderLoader::load()` directly +- `load_pass_shader_module(fg, resource_key)` automatically selects the no-debug shader variant when targets are disabled -- passes route through FrameGraph (and hence the dep-tracked IShaderCompiler cache) instead of reading embedded WGSL directly - The no-debug variant is compiled at build time with `-DNO_DEBUG_TARGETS` (see `config.yaml` slangc entries with `defines:`) -- At runtime in hot-reload builds, `ShaderLoader::load_variant()` recompiles via Slang with the define; non-hot-reload builds fall back to the pre-compiled embedded WGSL +- On native, `SlangCompiler` recompiles via libslang with the define and caches the WGSL on disk (`/shader_cache/`); on WASM the `EmbeddedCompiler` serves the pre-compiled embedded variant. -**Shader convention:** guard debug MRT struct fields and writes with `#ifndef NO_DEBUG_TARGETS`. The variant key is derived automatically by inserting `_no_debug` before the extension (e.g. `forward.wgsl` → `forward_no_debug.wgsl`). Both the base and variant WGSL must be listed in `config.yaml` under `slangc.shaders` and `embed.resources`. +**Shader convention:** guard debug MRT struct fields and writes with `#ifndef NO_DEBUG_TARGETS`. The variant key is derived automatically by inserting `_no_debug` before the extension (e.g. `forward.wgsl` -> `forward_no_debug.wgsl`). Both the base and variant WGSL must be listed in `config.yaml` under `slangc.shaders` and `embed.resources`. ## Slang Shader Conventions -### GLSL→Slang porting: `mul` and matrix constructors +### GLSL->Slang porting: `mul` and matrix constructors -Slang `float3x3(A, B, C)` passes A, B, C directly to WGSL `mat3x3(A, B, C)`, which interprets them as **columns** (not rows). When porting GLSL code that constructs a matrix with `mat3(col0, col1, col2)`, use the same arguments in Slang — they'll arrive as columns in WGSL unchanged. +Slang `float3x3(A, B, C)` passes A, B, C directly to WGSL `mat3x3(A, B, C)`, which interprets them as **columns** (not rows). When porting GLSL code that constructs a matrix with `mat3(col0, col1, col2)`, use the same arguments in Slang -- they'll arrive as columns in WGSL unchanged. -For matrix-vector multiplication: `mul(M, v)` = `M * v`, `mul(v, M)` = `v * M`. When porting GLSL `M * v` where M was built with column arguments, use `mul(v, M)` in Slang — the column-as-column constructor plus row-vector multiply gives the correct result. +For matrix-vector multiplication: `mul(M, v)` = `M * v`, `mul(v, M)` = `v * M`. When porting GLSL `M * v` where M was built with column arguments, use `mul(v, M)` in Slang -- the column-as-column constructor plus row-vector multiply gives the correct result. ### Visibility modifiers -Default visibility is `public`, but once ANY declaration uses an explicit modifier (`internal`, `public`, `private`), all non-annotated declarations become `internal`. To use `internal` on helpers, explicitly mark the public API surface with `public` — including struct fields. +Default visibility is `public`, but once ANY declaration uses an explicit modifier (`internal`, `public`, `private`), all non-annotated declarations become `internal`. To use `internal` on helpers, explicitly mark the public API surface with `public` -- including struct fields. ## Code Conventions @@ -92,6 +111,7 @@ Default visibility is `public`, but once ANY declaration uses an explicit modifi - On Emscripten, use `IMGUI_IMPL_WEBGPU_BACKEND_DAWN` (emdawnwebgpu IS Dawn) - Dawn-only APIs (e.g. `wgpuDeviceGetAdapter`) must be guarded with `#ifndef __EMSCRIPTEN__` - emdawnwebgpu async APIs are JS Promises; synchronous busy-wait loops deadlock on Emscripten +- **ASCII-only source.** No Unicode in source files (`.cpp`, `.h`, `.slang`, `.py`, `.yaml`, etc.). Use `->`, `<-`, `--`, `...`, `|`, `-`, `+` instead of arrows, em dashes, ellipsis, box drawing. Applies to code and comments alike. Exception: test data / assets where the Unicode is the thing under test. ## Repo tooling @@ -112,6 +132,15 @@ This project uses [repokit](tools/framework/README.md) for general project tooli These paths are generated or managed by the framework: -- `tools/framework/` — contribute upstream instead -- `tools/framework/_managed/` — generated venv, lockfile, pyproject -- `repo`, `repo.cmd`, `repo.ps1` — generated CLI shims +- `tools/framework/` -- contribute upstream instead +- `tools/framework/_managed/` -- generated venv, lockfile, pyproject +- `repo`, `repo.cmd`, `repo.ps1` -- generated CLI shims + +### Agent Bash Hook: no subshells + +The agent allowlist hook denies any command that spawns a second shell -- `bash -c "..."`, `sh -c "..."`, heredocs (`$(cat <