[
  {
    "path": ".cargo/config.toml",
    "content": "[alias]\nxtask = \"run --package xtask --\"\nshaders = \"xtask compile-shaders\"\nlinkage = \"xtask generate-linkage\"\ntest-wasm = \"xtask test-wasm\"\n\n[build]\nrustflags = [\"--cfg=web_sys_unstable_apis\"]\nrustdocflags = [\"--cfg=web_sys_unstable_apis\"]\n\n[env]\nCARGO_WORKSPACE_DIR = { value = \"\", relative = true }\n"
  },
  {
    "path": ".gitattributes",
    "content": "*.txt text\n*.rs text\n*.md text\n*.yaml text\n*.spv binary\n*.wgsl linguist-generated=true binary\n* text=auto eol=lf\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [schell] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/workflows/push.yaml",
    "content": "# Happens on push to main, and all PRs\nname: push\n\non: \n  push:\n    branches: \n      - main\n  pull_request:\n\nenv:\n  # For setup-rust, see https://github.com/moonrepo/setup-rust/issues/22\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  CARGO_GPU_COMMITSH: 31153a8edd3bc626d4b9fb0cd7bdb7a8b30797d3\n\njobs:\n  # Installs cargo deps and sets the cache directory for subsequent jobs\n  install-cargo-gpu:\n    strategy:\n      matrix: \n        os: [ubuntu-24.04, macos-latest]\n    runs-on: ${{ matrix.os }}\n    defaults:\n      run: \n        shell: bash\n    env:\n      RUST_LOG: debug  \n      RUNNER_OS: ${{ matrix.os }}\n    outputs:\n      cachepath-macOS: ${{ steps.cachepathstep.outputs.cachepath-macOS }}\n      cachepath-Linux: ${{ steps.cachepathstep.outputs.cachepath-Linux }}\n      cachepath-Windows: ${{ steps.cachepathstep.outputs.cachepath-Windows }}\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/cache@v4\n        with:\n          path: ~/.cargo\n          # THIS KEY MUST MATCH BELOW\n          key: cargo-cache-2-${{ env.CARGO_GPU_COMMITSH }}-${{ matrix.os }}\n      - uses: moonrepo/setup-rust@v1\n        with:\n          cache: false\n      - run: rustup default stable \n      - run: rustup update\n      - run: | \n          cargo install --git https://github.com/rust-gpu/cargo-gpu --rev $CARGO_GPU_COMMITSH cargo-gpu \n      - run: cargo gpu show commitsh\n      - id: cachepathstep\n        run: |\n          CACHE_PATH=`cargo gpu show cache-directory`\n          echo $CACHE_PATH\n          echo \"cachepath-$RUNNER_OS=$CACHE_PATH\" >> \"$GITHUB_OUTPUT\"\n\n  # Builds the shaders and ensures there is no git diff\n  renderling-build-shaders:\n    needs: install-cargo-gpu\n    strategy:\n      fail-fast: false\n      matrix: \n        # temporarily skip windows, revisit after a fix for this error is found:\n        # https://github.com/rust-lang/cc-rs/issues/1331\n        os: [ubuntu-24.04, macos-latest] #, windows-latest]\n    runs-on: ${{ matrix.os }}\n    defaults:\n      run: \n        shell: bash\n    env:\n      RUST_LOG: debug\n    steps:\n      - uses: actions/checkout@v2\n      # Disable moonrepo's built-in cache so it doesn't overwrite ~/.cargo\n      # (which contains the cargo-gpu binary from the install-cargo-gpu job).\n      - uses: moonrepo/setup-rust@v1\n        with:\n          cache: false\n      - uses: actions/cache@v4\n        with:\n          path: ~/.cargo\n          # THIS KEY MUST MATCH ABOVE\n          key: cargo-cache-2-${{ env.CARGO_GPU_COMMITSH }}-${{ matrix.os }}\n      - uses: actions/cache@v4\n        with:\n          path: | \n              ${{ needs.install-cargo-gpu.outputs.cachepath-macOS }}\n              ${{ needs.install-cargo-gpu.outputs.cachepath-Linux }}\n              ${{ needs.install-cargo-gpu.outputs.cachepath-Windows }}\n          key: rust-gpu-cache-1-${{ matrix.os }}\n      - run: rustup install nightly\n      - run: rustup component add --toolchain nightly rustfmt\n      - run: cargo gpu show commitsh\n      - run: rm -rf crates/renderling/src/linkage/* crates/renderling/shaders\n      - run: cargo shaders\n      - run: cargo linkage\n      - run: cargo build -p renderling\n      - run: git diff --exit-code --no-ext-diff\n\n  # Ensures code is properly formatted with nightly rustfmt\n  renderling-fmt:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/cache@v4\n        with:\n          path: ~/.cargo\n          # THIS KEY MUST MATCH ABOVE\n          key: renderling-fmt-${{ env.CARGO_GPU_COMMITSH }}-ubuntu-24.04\n      - run: mkdir -p $HOME/.cargo/bin\n      - uses: moonrepo/setup-rust@v1\n      - run: rustup install nightly\n      - run: rustup component add --toolchain nightly rustfmt\n      - run: cargo +nightly fmt -- --check\n\n  # BAU clippy lints\n  renderling-clippy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: moonrepo/setup-rust@v1\n      - run: cargo clippy\n\n  # Ensures the example glTF viewer compiles\n  renderling-build-example:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2 \n      - uses: moonrepo/setup-rust@v1\n      - run: cargo build -p example\n      \n  # BAU tests\n  renderling-test:\n    strategy:\n      matrix: \n        os: [ubuntu-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v2\n      - uses: moonrepo/setup-rust@v1\n      - uses: actions/cache@v4\n        with:\n          path: ~/.cargo\n          key: ${{ matrix.os }}-test-cargo-${{ hashFiles('**/Cargo.lock') }}\n          restore-keys: ${{ matrix.os }}-cargo-\n\n      - name: Install linux deps \n        if: runner.os == 'Linux'\n        run: |\n            sudo apt-get -y update\n            sudo apt-get -y install mesa-vulkan-drivers libvulkan1 vulkan-tools vulkan-validationlayers\n\n      - name: Install cargo-nextest\n        run: cargo install --locked cargo-nextest || true\n\n      - name: Test \n        run: cargo nextest run -j 1\n        env: \n          RUST_BACKTRACE: 1\n\n      - uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: test-output-${{ matrix.os }}\n          path: test_output  \n\n  ## WASM tests, commented out until we can get a proper headless browser on CI\n  # renderling-wasm-test:\n  #   # strategy:\n  #   #   matrix: \n  #   #     # empty string means ff, --chrome is chrome\n  #   #     browser: [\"\", \"--chrome\"]\n  #   runs-on: ubuntu-latest\n  #   steps:\n  #     - uses: actions/checkout@v2\n  #     - uses: moonrepo/setup-rust@v1\n  #     - uses: actions/cache@v4\n  #       with:\n  #         path: ~/.cargo\n  #         key: ${{ runner.os }}-test-cargo-${{ hashFiles('**/Cargo.lock') }}\n  #         restore-keys: ${{ runner.os }}-cargo-        \n  #     - name: Install linux deps \n  #       if: runner.os == 'Linux'\n  #       run: |\n  #           sudo apt-get -y update\n  #           sudo apt-get -y install mesa-vulkan-drivers libvulkan1 vulkan-tools vulkan-validationlayers\n  #     - name: Install wasm-pack\n  #       run: cargo install --locked wasm-pack || true\n  #     - name: Test WASM\n  #       env:\n  #         RUST_LOG: info\n  #       run: cargo test-wasm --chrome #${{ matrix.browser }}\n  #     - uses: actions/upload-artifact@v4\n  #       if: always()\n  #       with:\n  #         name: test-output-${{ runner.os }}-wasm\n  #         path: test_output\n"
  },
  {
    "path": ".gitignore",
    "content": "# macOS\n*.DS_Store\n# will have compiled files and executables\n/target/\nshaders/target\nshaders/shader-crate/target\n**/spirv-manifest.json\n\n# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries\n# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html\n# Cargo.lock\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\n*~undo-tree~\n\ntest_output\ncmy_triangle_renderer.svg\n.aider*\nflamegraph.svg\n**/*.blend1\n\n# WGSL is generated by `cargo xtask generate-linkage --wgsl`\n# or by `cargo build -p renderling`, but we don't want to track\n# the changes in them.\ncrates/renderling/shaders/*.wgsl\n"
  },
  {
    "path": ".helix/snippets/rust.json",
    "content": "{\n  \"anchor-snippet\": {\n    \"prefix\": \"anchor\",\n    \"body\": [\n      \"// ANCHOR: ${1:anchor}\",\n  \t  \"$2\",\n  \t  \"// ANCHOR_END: $1\"\n    ],\n    \"description\": \"insert a video tag\"\n  }\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS.md\n\n## Build & Test\n- Build: `cargo build -p renderling`\n- Test all: `cargo nextest run -j 1` or `cargo test`\n- Single test: `cargo test <test_name>` or `cargo nextest run <test_name>`\n- Lint: `cargo clippy`\n- Shaders: `cargo shaders` (compile), `cargo linkage` (generate WGSL)\n\n## Code Style\n- Max line width: 100 chars\n- Imports: group by crate (`imports_granularity = \"crate\"`), std → external → internal\n- Error handling: use `snafu` crate with `#[derive(Debug, Snafu)]`\n- Naming: `snake_case` functions/modules, `PascalCase` types, `with_*` builder methods\n- Tests: inline `#[cfg(test)] mod test { ... }` within modules\n- CPU-only code: wrap with `#[cfg(cpu)]`\n\nAlways format with `cargo +nightly fmt`.\n\n## Disallowed Methods (clippy.toml)\nAvoid: `Vec{2,3,4}::normalize_or_zero`, `Mat4::to_scale_rotation_translation`, `f32::signum`\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Renderling\n\nThank you for your interest in contributing to Renderling!\n\n## Code of Conduct\n\nThis project follows the [Zcash Code of Conduct](https://github.com/zcash/zcash/blob/master/code_of_conduct.md).\nWe are committed to providing a welcoming and harassment-free experience for everyone.\n\n## NLnet Generative AI Policy\n\nThis is an NLnet-funded project. We adhere to the [NLnet Generative AI Policy](https://nlnet.nl/foundation/policies/generativeAI/).\nIf you use generative AI tools like LLMs, code assistants, etc. in your contributions, you must:\n- Disclose any substantive use\n- Maintain a prompt provenance log for material contributions\n- Ensure outputs can be legally published under FLOS licenses\n- Not present AI-generated content as your own human-authored work\n\n### When it comes to AI - use your best judgment\n\nI use AI to help plan a strategy and then make changes by hand.\n\nWhat I want to avoid is a situation where copyrighted material from an LLM's training corpus\nmakes it into the codebase.\n\nSo please disclose if the _outputs_ of generative AI is what is being committed.\n\n## Getting Started\n\n1. Fork and clone the repository\n2. Install Rust via [rustup](https://rustup.rs)\n3. Install `cargo-gpu`: `cargo install --git https://github.com/rust-gpu/cargo-gpu cargo-gpu`\n4. Optionally install `cargo-nextest`: `cargo install cargo-nextest`\n\n## Development Workflow\n\n1. Create a branch for your changes\n2. Follow the code style guidelines in [AGENTS.md](AGENTS.md)\n3. Run tests: `cargo nextest run -j 1` or `cargo test`\n4. Run lints: `cargo clippy`\n5. If modifying shaders: `cargo shaders && cargo linkage`\n6. Ensure there are no unexpected diffs: `git diff`\n7. Submit a pull request\n\n## Discussions\n\nQuestions, ideas, and general discussion should happen on\n[GitHub Discussions](https://github.com/schell/renderling/discussions).\n\n## Testing\n\nTests render images in headless mode and compare against reference images in `test_img/`.\nNew visual features should include image comparison tests where applicable.\n\n## License\n\nBy contributing, you agree that your contributions will be dual-licensed under\nthe MIT and Apache 2.0 licenses. See [LICENSE](LICENSE) for details.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nmembers = [ \n    \"crates/example\", \n    \"crates/examples\",\n    \"crates/example-culling\",\n    #\"crates/example-wasm\",\n    \"crates/loading-bytes\",\n    \"crates/renderling\", \n    \"crates/renderling-build\",\n    \"crates/renderling-ui\",\n    \"crates/wire-types\",\n    # \"crates/sandbox\",\n    \"crates/xtask\"\n]\n\nexclude = [\"./shaders\"]\n\nresolver = \"2\"\n\n[workspace.dependencies]\nassert_approx_eq = \"1.1.0\"\nasync-channel = \"1.8\"\naxum = \"0.8.4\"\nbytemuck = { version = \"1.19.0\", features = [\"derive\"] }\ncfg_aliases = \"0.2\"\nclap = { version = \"4.5.23\", features = [\"derive\"] }\nconsole_log = \"1.0.0\"\ncraballoc = { version = \"0.3.1\" } \ncrabslab = { version = \"0.6.6\", default-features = false }\nplotters = \"0.3.7\"\nctor = \"0.2.2\"\ndagga = \"0.2.1\"\nenv_logger = \"0.10.0\"\nfutures-lite = \"1.13\"\nfutures-util = \"0.3.31\"\nglam = { version = \"0.30\", default-features = false }\ngltf = { version = \"1.4,1\", features = [\"KHR_lights_punctual\", \"KHR_materials_unlit\", \"KHR_materials_emissive_strength\", \"extras\", \"extensions\"] }\nglyph_brush = \"0.7.8\"\nimage = \"0.25\"\nlog = \"0.4\"\nloading-bytes = { path = \"crates/loading-bytes\", version = \"0.1.1\" }\nlyon = \"1.0.1\"\nnaga = { version = \"26.0\", features = [\"spv-in\", \"wgsl-out\", \"wgsl-in\", \"msl-out\"] }\nnew_mime_guess = \"4.0.4\"\npretty_assertions = \"1.4.0\"\nproc-macro2 = { version = \"1.0\", features = [\"span-locations\"] }\nquote = \"1.0\"\nreqwest = \"0.12.23\"\nrustc-hash = \"1.1\"\nserde = {version = \"1.0\", features = [\"derive\"]}\nserde_json = \"1.0.117\"\nsend_wrapper = \"0.6.0\"\nsimilarity = \"0.2.0\"\nsnafu = \"0.8\"\nspirv-std = { git = \"https://github.com/rust-gpu/rust-gpu.git\", rev = \"05b34493ce661dccd6694cf58afc13e3c8f7a7e0\" }\nspirv-std-macros = { git = \"https://github.com/rust-gpu/rust-gpu.git\", rev = \"05b34493ce661dccd6694cf58afc13e3c8f7a7e0\" }\nsyn = { version = \"2.0.49\", features = [\"full\", \"extra-traits\", \"parsing\"] }\ntokio = \"1.47.1\"\ntracing = \"0.1.41\"\nwasm-bindgen = \"0.2\"\nwasm-bindgen-futures = \"0.4\"\nwasm-bindgen-test = \"0.3\"\nweb-sys = \"0.3\"\nwinit = { version = \"0.30.12\" }\nwgpu = { version = \"26.0\" }\nwgpu-core = { version = \"26.0\" }\nmetal = \"0.32\"\n\n[profile.dev]\nopt-level = 1\n\n[profile.dev.package.image]\nopt-level = 3\n\n[profile.dev.package.gltf]\nopt-level = 3\n\n[patch.crates-io]\nspirv-std = { git = \"https://github.com/rust-gpu/rust-gpu.git\", rev = \"05b34493ce661dccd6694cf58afc13e3c8f7a7e0\" }\n\n[workspace.lints.rust]\nunexpected_cfgs = { level = \"allow\", check-cfg = [ 'cfg(spirv)' ] }\n"
  },
  {
    "path": "LICENSE",
    "content": "Rendeling is dual-licensed under either\n\n* MIT License (docs/LICENSE-MIT or http://opensource.org/licenses/MIT)\n* Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)\n\nat your option."
  },
  {
    "path": "NOTES.md",
    "content": "# Notes\n\nJust pro-cons on tech choices and little things I don't want to forget whil implementing `renderling`.\n\n# gltf\n\n* why are there repeats of nodes in document.nodes?\n\n# rust-gpu\n\n## pros\n\n* sharing code on CPU and GPU\n  - sanity testing GPU code on CPU using regular tests\n  - ability to run shaders on either CPU or GPU and profile\n* it's Rust\n  - using cargo and Rust module system\n  - expressions!\n  - type checking!\n  - traits!\n  - editor tooling!\n\n## cons / limititions / gotchas\n\n* Can't use an array as a slice, it causes this error:\n  ```\n  error: cannot cast between pointer types\n         from `*[u32; 3]`\n           to `*[u32]`  \n  ```\n  See the ticket I opened <https://github.com/Rust-GPU/rust-gpu/issues/465>\n* ~~can't use enums (but you can't in glsl or hlsl or msl or wgsl either)~~ you _can_ but they must be simple (like `#[repr(u32)]`)\n* ~~struct layout size/alignment errors can be really tricky~~ solved by using a slab\n* rust code must be no-std\n* don't use `while let` or `while` loops\n* for loops are hit or miss, sometimes they work and sometimes they don't\n  - see [this rust-gpu issue](https://github.com/EmbarkStudios/rust-gpu/issues/739)\n  - see [conversation with eddyb on discord](https://discord.com/channels/750717012564770887/750717499737243679/threads/1092283362217046066)\n* can't use `.max` or `.min` on integers\n* meh, but no support for dynamically sized arrays (how would that work in no-std?)\n  - see [conversation on discord](https://discord.com/channels/750717012564770887/750717499737243679/1091813590400516106)\n* can't use bitwise rotate_left or rotate_right\n  - see [the issue on github](https://github.com/EmbarkStudios/rust-gpu/issues/1062)\n* sometimes things like indexing are just funky-joe-monkey:\n  - see [this comment on discord](https://discord.com/channels/750717012564770887/750717499737243679/1131395331368693770)\n  - see [this comment on matrix](https://matrix.to/#/!XFRnMvAfptAHthwBCx:matrix.org/$f4RmQGzq4Ulmmd4bEFOvP0LzLZei8lrHCF--s71Zcxs?via=matrix.org&via=mozilla.org&via=kyju.org)\n* cannot use shader entry point functions nested within each other\n  - see [the discussion on `rust-gpu` discord](https://discord.com/channels/750717012564770887/750717499737243679/1198813817975603251)\n* if your shader crate is just a library and has no entry points it **cannot** have the\n  `crate-type = [\"rlib\", \"dylib\"]` Cargo.toml annotation or you will get \"Undefined symbols\" errors\n* no recursion! you must convert your recursive algos into ones with manually managed stacks\n* `usize` is `u32` on `target_arch = \"spirv\"`! Watch out for silent shader panics caused by wrapping\n  arithmetic operations.\n\n# wgpu\n\n## pros\n\n* works on all platforms with the same API\n* much more configurable than OpenGL\n* much better error messages than OpenGL\n* less verbose than Vulkan\n* the team is very responsive\n\n## cons\n\n* no support for arrays of textures on web, yet\n* atomics are not supported in the Naga SPIRV frontend, which limits the capabilities of compute\n  - see [the related Naga issue](https://github.com/gfx-rs/naga/issues/2301)\n\n# glam\n\n## pros\n\n* lots of other graphics libs use it\n* speed\n\n## cons\n\n* different types on SIMD, with different structures - like Vec4 is a struct on my macos\n  but it's a tuple on SIMD linux.\n\n# more things to figure out\n\n* bindless - wth exactly is it\n\n# tips, gotchas, links and further reading\n\n* `location[...] is provided by the previous stage output but is not consumed as input by this stage.`\n  - rust-gpu has optimized away the shader input, you must use the input parameter in your downstream shader\n  - sometimes the optimization is pretty agressive, so you really gotta _use_ the input\n* [Forward+ shading (as opposed to deferred)](https://takahiroharada.files.wordpress.com/2015/04/forward_plus.pdf)\n  **tl;dr**\n  In a compute shader before the vertex pass:\n  * break up the frame into tiles\n  * for each tile compute which lights' contribution to the pixels in the tile\n  * during shading, iterate over _only_ the lights for each pixel according to its tile\n* [**Help inspecting buffers in Xcode** ](https://developer.apple.com/documentation/xcode/inspecting-buffers?changes=__9)\n* command that includes some vulkan debugging stuff\n  - VK_LOADER_LAYERS_ENABLE='*validation' VK_LAYER_ENABLES=VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT DEBUG_PRINTF_TO_STDOUT=1\n* When generating mipmaps I ran into a problem where sampling the original texture was always coming up [0.0, 0.0 0.0, 0.0]. It turns out that the sampler was trying to read from the mipmap at level 1, and of course it didn't exist yet as that was the one I was trying to generate. The fix was to sample a different texture - one without slots for the mipmaps, then throw away that texture.\n\n## PBR reference implementations\n* [khronos sample viewer](https://github.khronos.org/glTF-Sample-Viewer-Release/)\n  - [vertex shader code](https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/main/source/Renderer/shaders/primitive.vert)\n  - [fragment shader code](https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/main/source/Renderer/shaders/pbr.frag)\n* [babylonjs](https://sandbox.babylonjs.com/)\n  - [vertex shader code](https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/core/src/Shaders/pbr.vertex.fx)\n  - [fragment shader code](https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/core/src/Shaders/pbr.fragment.fx)\n\n# contributions made during the course of this project\n* wrote an NLNet grant proposal to add atomics to `naga`'s spv frontend\n  - roughly to complete this PR https://github.com/gfx-rs/naga/pull/2304\n* fixing `wgpu`'s vulkan backend selection on macOS\n  - https://github.com/gfx-rs/wgpu/pull/3958\n  - https://github.com/gfx-rs/wgpu/pull/3962\n"
  },
  {
    "path": "README.md",
    "content": "# <img style=\"image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges;\" src=\"https://github.com/user-attachments/assets/83eafc47-287c-4b5b-8fd7-2063e56b2338\" /> renderling\n\nRenderling is an innovative, GPU-driven renderer designed for efficient scene rendering with a focus on leveraging \nGPU capabilities for nearly all rendering operations. \nUtilizing Rust for shader development, it ensures memory safety and cross-platform compatibility, including web platforms. \nThe project, currently in the alpha stage, aims for rapid loading of GLTF files, handling large scenes, and supporting numerous lights. \nDevelopment emphasizes performance, ergonomics, observability and the use of modern rendering techniques like forward+ rendering and \nphysically based shading.\n\nRead the [manual](https://renderling.xyz/manual/index.html) to get started.\n\nRead the [docs](https://renderling.xyz/docs/renderling/index.html) for more info.\n\nVisit <https://renderling.xyz> to read the development blog.\n\n<img width=\"912\" alt=\"ibl_environment_test\" src=\"https://github.com/schell/renderling/assets/24942/297d6150-64b2-45b8-9760-12b27dc8cc3e\">\n\nThis project is funded through [NGI Zero Commons](https://nlnet.nl/commonsfund/), a fund established by [NLnet](https://nlnet.nl) \nwith financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. \nLearn more at the [2025 NLnet project page](https://nlnet.nl/project/Renderling-Ecosystem/) and the \n[2024 NLnet project page](https://nlnet.nl/project/Renderling).\n\n[<img src=\"https://nlnet.nl/logo/banner.png\" alt=\"NLnet foundation logo\" width=\"20%\" />](https://nlnet.nl)\n\n[<img src=\"https://nlnet.nl/image/logos/NGI0_tag.svg\" alt=\"NGI Zero Logo\" width=\"20%\" />](https://nlnet.nl/core)\n\n\n## Warning\n\nThis is very much a work in progress.\n\n## What\n\n`renderling` holds entire scenes of geometry, textures, materials, lighting, even the scene graph itself - in GPU buffers.\nAll but a few of the rendering operations happen on the GPU.\nThe CPU is used to interact with the filesystem to marshall data to the GPU and to update transforms.\n\nShaders are written in Rust, via `rust-gpu`.\n\n## Why should I use `renderling`\n\n* Data is easily staged on the GPU using an automatically reference counted slab allocator that \n  provides access from the CPU.\n\n  Your scene geometry, materials, animations - all of it - live on the GPU, while the CPU has easy access\n  to read and modify that data, without borrowing - allowing you to send your data through threads to anything \n  that needs it. \n\n* Having everything on the GPU makes `renderling` very effective at rendering certain types of scenes.\n  \n  Specifically `renderling` aims to be good at rendering scenes with a moderate level of unique geometry,\n  (possibly a large amount of repeated geometry), with a small number of large textures (or large number of small textures),\n  and lots of lighting effects.\n\n* Tight integration with GLTF:\n  - Loading scenes, nodes, animations etc\n  - Includes tools for controlling animations\n  - Supported extensions:\n    * KHR_lights_punctual\n    * KHR_materials_unlit\n    * KHR_materials_emissive_strength\n* Image based lighting + analytical lighting\n\n* Good documentation\n\n## API Features\n\n* simple structs represent nodes, meshes, materials and lights\n* seamless GPU / CPU syncronization\n* headless rendering support\n  - rendering to texture and saving via `image` crate\n* text and user interface rendering support\n* nested nodes with local transforms\n* tight integration with glTF (cargo feature `gltf` - on by default)\n\nShaders are written in Rust via `rust-gpu` where possible, falling back to `wgsl` where needed.\n\n## Rendering Features / Roadmap\n\nRenderling takes a [forward+](https://takahiroharada.files.wordpress.com/2015/04/forward_plus.pdf) approach to rendering.\n\nBy default it uses a single uber-shader for rendering.\n\n- [x] texture atlas\n  - [x] automatic resource management (Arc/drop based)\n  - [ ] [BCn compression](https://www.reedbeta.com/blog/understanding-bcn-texture-compression-formats/)\n\n- [x] GPU slab allocator\n  - [x] automatic resource management (Arc/drop based)\n\n- [x] frustum culling\n- [ ] occlusion culling - in progress\n\n- [x] Built-in support for common lighting/material workflows\n  - [x] physically based shading\n  - [x] unlit\n- [x] forward+ style light tiling/light culling\n- [x] shadow mapping\n  - [ ] shadow mapping from image-based lighting\n- [ ] ssao\n\n- [x] msaa\n- [x] bloom \"physically based\" up+downsampling blur\n- [ ] depth of field\n- [x] high dynamic range\n- [x] skybox\n\n- [x] image based lighting\n  - [x] diffuse\n  - [x] specular\n     \n- [ ] order independent transparency\n\n- [ ] entirely too much raymarching \n\n- gltf support\n  - [x] scenes\n  - [x] nodes\n  - [x] cameras\n  - [x] meshes\n  - materials\n    - [x] pbr metallic roughness (factors + textures)\n    - [x] normal mapping\n    - [x] occlusion textures\n    - [ ] pbr specular glossiness\n    - [ ] parallax mapping\n  - [x] textures, images, samplers\n  - animation\n    - [x] interpolation\n    - [x] skinning\n    - [x] morph targets \n\n- 2d (renderling-ui)\n  - [x] text\n  - [x] stroked and filled paths\n    - [x] circles\n    - [x] rectangles\n    - [x] cubic beziers\n    - [x] quadratic beziers\n    - [x] arbitrary polygons\n    - [x] fill w/ image\n\n## Definition\n**renderling** noun\n\nA small beast that looks cute up close, ready to do your graphics bidding.\n\n## Haiku\n\n> Ghost in the machine,\n> lighting your scene with magic.\n> Cute technology.\n\n## Project Organization\n\n* crates/renderling\n\n  Main library crate.\n  Contains CPU Rust code for creating pipelines and managing resources, making render passes, etc.\n  Contains GPU Rust code of the shader operations themselves.\n  Contains tests, some using image comparison of actual frame renders for consistency and backwards compatibility.\n\n* crates/renderling/shaders\n\n  Contains **.spv** and **.wgsl** files generated by [`cargo-gpu`](https://github.com/rust-gpu/cargo-gpu).\n\n* crates/renderling/src/linkage*\n\n  Contains autogenerated `wgpu` linkage for the generated shaders.\n\n* img\n\n  Image assets for tests (textures, etc.)\n\n* test_img\n\n  Reference images to use for testing.\n\n* crates/example\n\n  Contains an example of using the `renderling` crate to make an application.\n\n## Tests\n\nTests use `renderling` in headless mode and generate images that are compared to expected output.\n\n### Running tests\n\n```\ncargo test\n```\n\n## Building the shaders\n\nThe `crates/renderling/shaders/` folder contains the generated SPIR-V files.\n\nTo regenerate the shaders, run:\n\n```\ncargo shaders\n```\n\nAnd to explicitly re-generate `wgpu` linkage, you can run: \n\n```\ncargo linkage\n```\n\n...but the `build.rs` script will do this for you, so it's not strictly necessary.\n\n## Building on WASM\n\n```\nRUSTFLAGS=--cfg=web_sys_unstable_apis trunk build crates/example-wasm/index.html && basic-http-server -a 127.0.0.1:8080 crates/example-wasm/dist\n```\n\n## 🫶 Sponsor this!\n\nThis work will always be free and open source. \nIf you use it (outright or for inspiration), please consider donating.\n\n[💰 Sponsor 💝](https://github.com/sponsors/schell)\n\n### Special thanks \n\n- James Harton ([@jimsynz](https://github.com/jimsynz/)) for donating multiple linux CI runners with \n  physical GPUs!\n\n### Related work & spin-off projects \n\nMany projects were born from first solving a need within `renderling`. \nSome of these solutions were then spun off into their own projects.\n\n- [`wgsl-rs`](https://github.com/schell/wgsl-rs)\n  Write WGSL shaders using a subset of Rust and run them on CPU _and_ GPU\n- [`crabslab` and `craballoc`](https://github.com/schell/crabslab)\n  A slab allocator for working across CPU/GPU boundaries.\n- [`loading-bytes`](crates/loading-bytes)\n  A cross-platform (including the web) and comedically tiny way of loading files to bytes.\n- [`moongraph`](https://github.com/schell/moongraph)\n  A DAG and resource graph runner.\n- Contributions to [`naga`](https://github.com/gfx-rs/wgpu/issues/4489)\n  * Adding atomics support to the SPIR-V frontend\n- Contributions to [`gltf`](https://github.com/gltf-rs/gltf/pull/419)\n- [`cargo-gpu`](https://githu.com/rust-gpu/cargo-gpu)\n  A shader compilation cli tool.\n\nSponsoring this project contributes to the ecosystem. \n\n## License\nRenderling is free and open source. All code in this repository is dual-licensed under either:\n\n    MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)\n    Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)\n\nat your option. This means you can select the license you prefer! This dual-licensing approach\nis the de-facto standard in the Rust ecosystem and there are very good reasons to include both.\n\nUnless you explicitly state otherwise, any contribution intentionally submitted for inclusion\nin the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above,\nwithout any additional terms or conditions.\n\n## Notes & Devlog\nI keep a list of (un)organized notes about this project [here](NOTES.md).\nI keep a devlog at the official website <https://renderling.xyz/devlog/index.html>.\n"
  },
  {
    "path": "clippy.toml",
    "content": "disallowed-methods = [\n    \"glam::Vec2::normalize_or_zero\",\n    \"glam::Vec3::normalize_or_zero\",\n    \"glam::Vec4::normalize_or_zero\",\n    \"glam::Mat4::to_scale_rotation_translation\",\n    \"f32::signum\"\n]\n"
  },
  {
    "path": "crates/example/Cargo.toml",
    "content": "[package]\nname = \"example\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[lib]\n\n[[bin]]\nname = \"example\"\n\n[dependencies]\nclap = { version = \"^4.3\", features = [\"derive\"] }\ncraballoc.workspace = true\n#console-subscriber = \"0.4.0\"\nenv_logger = {workspace=true}\nfutures-lite = {workspace=true}\ngltf = { workspace = true }\nicosahedron = \"0.1\"\nlazy_static = \"1.4.0\"\nloading-bytes = { path = \"../loading-bytes\" }\nlog = { workspace = true }\nrenderling = { path = \"../renderling\" }\nrenderling-ui = { path = \"../renderling-ui\", features = [\"text\", \"path\"] }\nwasm-bindgen = { workspace = true }\nwasm-bindgen-futures = \"^0.4\"\nweb-sys = { workspace = true, features = [\"Performance\", \"Window\"] }\nwinit = { workspace = true }\nwgpu = { workspace = true }\n\n[dev-dependencies]\nimage = { workspace = true }\nimg-diff = { path = \"../img-diff\" }\n"
  },
  {
    "path": "crates/example/src/camera.rs",
    "content": "//! Camera control.\nuse std::str::FromStr;\n\nuse renderling::{\n    bvol::Aabb,\n    camera::Camera,\n    glam::{Mat4, Quat, UVec2, Vec2, Vec3},\n};\nuse winit::{event::KeyEvent, keyboard::Key};\n\nconst RADIUS_SCROLL_DAMPENING: f32 = 0.001;\nconst DX_DY_DRAG_DAMPENING: f32 = 0.01;\n\n#[derive(Clone, Copy, Debug, Default)]\npub enum CameraControl {\n    #[default]\n    Turntable,\n    WasdMouse,\n}\n\nimpl FromStr for CameraControl {\n    type Err = String;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        match s {\n            \"turntable\" => Ok(CameraControl::Turntable),\n            \"wasdmouse\" => Ok(CameraControl::WasdMouse),\n            _ => Err(\"must be 'turntable' or 'wasdmouse'\".to_owned()),\n        }\n    }\n}\n\npub struct TurntableCameraController {\n    /// look at\n    pub center: Vec3,\n    /// distance from the origin\n    pub radius: f32,\n    /// Determines the distance between the camera's near and far planes\n    depth: f32,\n    /// anglular position on a circle `radius` away from the origin on x,z\n    pub phi: f32,\n    /// angular distance from y axis\n    pub theta: f32,\n    /// is the left mouse button down\n    left_mb_down: bool,\n}\n\nimpl Default for TurntableCameraController {\n    fn default() -> Self {\n        Self {\n            center: Vec3::ZERO,\n            radius: 6.0,\n            depth: 12.0,\n            phi: 0.0,\n            theta: std::f32::consts::FRAC_PI_4,\n            left_mb_down: false,\n        }\n    }\n}\n\nimpl CameraController for TurntableCameraController {\n    fn tick(&mut self) {}\n\n    fn reset(&mut self, bounds: Aabb) {\n        log::debug!(\"resetting turntable bounds to {bounds:?}\");\n        let diagonal_length = bounds.diagonal_length();\n        self.radius = diagonal_length * 1.25;\n        self.depth = 2.0 * diagonal_length;\n        self.center = bounds.center();\n        self.left_mb_down = false;\n    }\n\n    fn update_camera(&self, UVec2 { x: w, y: h }: UVec2, current_camera: &Camera) {\n        let camera_position = Self::camera_position(self.radius, self.phi, self.theta);\n        let znear = self.depth / 1000.0;\n        current_camera.set_projection_and_view(\n            Mat4::perspective_rh(\n                std::f32::consts::FRAC_PI_4,\n                w as f32 / h as f32,\n                znear,\n                self.depth,\n            ),\n            Mat4::look_at_rh(camera_position, self.center, Vec3::Y),\n        );\n    }\n\n    fn mouse_scroll(&mut self, delta: f32) {\n        self.zoom(delta);\n    }\n\n    fn mouse_moved(&mut self, _position: Vec2) {}\n\n    fn mouse_motion(&mut self, delta: Vec2) {\n        self.pan(delta);\n    }\n\n    fn mouse_button(&mut self, is_pressed: bool, is_left_button: bool) {\n        self.left_mb_down = is_left_button && is_pressed;\n    }\n\n    fn key(&mut self, _event: KeyEvent) {}\n}\n\nimpl TurntableCameraController {\n    fn camera_position(radius: f32, phi: f32, theta: f32) -> Vec3 {\n        // convert from spherical to cartesian\n        let x = radius * theta.sin() * phi.cos();\n        let y = radius * theta.sin() * phi.sin();\n        let z = radius * theta.cos();\n        // in renderling Y is up so switch the y and z axis\n        Vec3::new(x, z, y)\n    }\n\n    fn zoom(&mut self, delta: f32) {\n        self.radius = (self.radius - (delta * RADIUS_SCROLL_DAMPENING)).max(0.0);\n    }\n\n    fn pan(&mut self, delta: Vec2) {\n        if self.left_mb_down {\n            self.phi += delta.x * DX_DY_DRAG_DAMPENING;\n\n            let next_theta = self.theta - delta.y * DX_DY_DRAG_DAMPENING;\n            self.theta = next_theta.clamp(0.0001, std::f32::consts::PI);\n        }\n    }\n}\n\n#[derive(Default)]\npub struct WasdMouseCameraController {\n    position: Vec3,\n    theta: f32,\n    phi: f32,\n    forward_is_down: bool,\n    backward_is_down: bool,\n    left_is_down: bool,\n    right_is_down: bool,\n    up_is_down: bool,\n    down_is_down: bool,\n    speed: f32,\n    last_tick: Option<f64>,\n}\n\nimpl CameraController for WasdMouseCameraController {\n    fn tick(&mut self) {\n        let now = super::now();\n        if let Some(prev) = self.last_tick.replace(now) {\n            let dt = now - prev;\n\n            // We want the direction to be based solely on self.theta, because we\n            // don't want the camera to move in Y.\n            let forward_direction = Vec3::NEG_Z;\n            let left_direction = Vec3::NEG_X;\n            let up_direction = Vec3::Y;\n            let rotation = Quat::from_rotation_y(-self.theta);\n\n            if self.forward_is_down {\n                let direction = rotation * forward_direction;\n                let velocity = dt as f32 * self.speed * direction;\n                self.position += velocity;\n            }\n            if self.backward_is_down {\n                let direction = rotation * forward_direction;\n                let velocity = dt as f32 * self.speed * direction;\n                self.position -= velocity;\n            }\n            if self.left_is_down {\n                let direction = rotation * left_direction;\n                let velocity = dt as f32 * self.speed * direction;\n                self.position += velocity;\n            }\n            if self.right_is_down {\n                let direction = rotation * left_direction;\n                let velocity = dt as f32 * self.speed * direction;\n                self.position -= velocity;\n            }\n            if self.up_is_down {\n                let velocity = dt as f32 * self.speed * up_direction;\n                self.position += velocity;\n            }\n            if self.down_is_down {\n                let velocity = dt as f32 * self.speed * up_direction;\n                self.position -= velocity;\n            }\n        }\n    }\n\n    fn update_camera(&self, UVec2 { x: w, y: h }: UVec2, camera: &Camera) {\n        let camera_rotation =\n            Quat::from_euler(renderling::glam::EulerRot::XYZ, self.phi, self.theta, 0.0);\n        let projection =\n            Mat4::perspective_infinite_rh(std::f32::consts::FRAC_PI_4, w as f32 / h as f32, 0.01);\n        let view = Mat4::from_quat(camera_rotation) * Mat4::from_translation(-self.position);\n        camera.set_projection_and_view(projection, view);\n    }\n\n    fn reset(&mut self, _bounds: Aabb) {\n        self.position = Vec3::ZERO; //center_max_z + (center_max_z - center_min_z) * 0.5;\n        self.theta = 0.0;\n        self.phi = 0.0;\n        self.speed = 2.0;\n        log::info!(\"resetting to position: {}\", self.position);\n    }\n\n    fn mouse_scroll(&mut self, _delta: f32) {}\n\n    fn mouse_moved(&mut self, _position: Vec2) {}\n\n    fn mouse_motion(&mut self, delta: Vec2) {\n        const DAMPER: f32 = 0.005;\n        self.theta = (self.theta + DAMPER * delta.x).rem_euclid(2.0 * std::f32::consts::PI);\n        self.phi = (self.phi + DAMPER * delta.y).clamp(-1.2, 1.2);\n    }\n\n    fn mouse_button(&mut self, _is_pressed: bool, _is_left_button: bool) {}\n\n    fn key(\n        &mut self,\n        KeyEvent {\n            logical_key, state, ..\n        }: KeyEvent,\n    ) {\n        match logical_key {\n            Key::Character(c) => match c.as_str() {\n                \"p\" => self.forward_is_down = state.is_pressed(),\n                \"k\" => self.backward_is_down = state.is_pressed(),\n                \"i\" => self.right_is_down = state.is_pressed(),\n                \"y\" => self.left_is_down = state.is_pressed(),\n                \"u\" => {\n                    self.down_is_down = false;\n                    self.up_is_down = state.is_pressed();\n                }\n                \"U\" => {\n                    self.up_is_down = false;\n                    self.down_is_down = state.is_pressed();\n                }\n                s => log::info!(\"unused key char '{s}'\"),\n            },\n\n            k => log::info!(\"key: {k:#?}\"),\n        }\n    }\n}\n\npub trait CameraController {\n    fn reset(&mut self, bounds: Aabb);\n    fn tick(&mut self);\n    fn update_camera(&self, size: UVec2, camera: &Camera);\n    fn mouse_scroll(&mut self, delta: f32);\n    fn mouse_moved(&mut self, position: Vec2);\n    fn mouse_motion(&mut self, delta: Vec2);\n    fn mouse_button(&mut self, is_pressed: bool, is_left_button: bool);\n    fn key(&mut self, event: KeyEvent);\n\n    fn window_event(&mut self, event: winit::event::WindowEvent) {\n        match event {\n            winit::event::WindowEvent::MouseWheel { delta, .. } => {\n                let delta = match delta {\n                    winit::event::MouseScrollDelta::LineDelta(_, up) => up,\n                    winit::event::MouseScrollDelta::PixelDelta(pos) => pos.y as f32,\n                };\n\n                self.mouse_scroll(delta);\n            }\n            winit::event::WindowEvent::CursorMoved { position, .. } => {\n                self.mouse_moved(Vec2::new(position.x as f32, position.y as f32));\n            }\n            winit::event::WindowEvent::MouseInput { state, button, .. } => {\n                let is_pressed = matches!(state, winit::event::ElementState::Pressed);\n                let is_left_button = matches!(button, winit::event::MouseButton::Left);\n                self.mouse_button(is_pressed, is_left_button);\n            }\n            winit::event::WindowEvent::KeyboardInput {\n                device_id: _,\n                event,\n                is_synthetic: _,\n            } => {\n                self.key(event);\n            }\n\n            _ => {}\n        }\n    }\n\n    fn device_event(&mut self, event: winit::event::DeviceEvent) {\n        if let winit::event::DeviceEvent::MouseMotion { delta } = event {\n            self.mouse_motion(Vec2::new(delta.0 as f32, delta.1 as f32))\n        }\n    }\n}\n"
  },
  {
    "path": "crates/example/src/lib.rs",
    "content": "//! Runs through all the gltf sample models to test and show-off renderling's\n//! gltf capabilities.\nuse std::{\n    collections::{HashMap, HashSet},\n    sync::{Arc, Mutex},\n};\n\nuse glam::{Mat4, UVec2, Vec2, Vec3, Vec4};\nuse renderling::{\n    atlas::AtlasImage,\n    bvol::{Aabb, BoundingSphere},\n    camera::Camera,\n    context::Context,\n    geometry::Vertex,\n    glam,\n    gltf::{Animator, GltfDocument},\n    light::{AnalyticalLight, Lux},\n    primitive::Primitive,\n    skybox::Skybox,\n    stage::Stage,\n};\nuse renderling_ui::{FontArc, Section, Text, UiRect, UiRenderer, UiText};\n\npub mod camera;\nuse camera::{\n    CameraControl, CameraController, TurntableCameraController, WasdMouseCameraController,\n};\n\npub mod time;\nuse time::FPSCounter;\n\npub mod utils;\n\nconst FONT_BYTES: &[u8] =\n    include_bytes!(\"../../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf\");\n\nconst DARK_BLUE_BG_COLOR: Vec4 = Vec4::new(\n    0x30 as f32 / 255.0,\n    0x35 as f32 / 255.0,\n    0x42 as f32 / 255.0,\n    1.0,\n);\n\npub enum SupportedFileType {\n    Gltf,\n    Hdr,\n}\n\npub fn is_file_supported(file: impl AsRef<std::path::Path>) -> Option<SupportedFileType> {\n    let ext = file.as_ref().extension()?;\n    Some(match ext.to_str()? {\n        \"hdr\" => SupportedFileType::Hdr,\n        _ => SupportedFileType::Gltf,\n    })\n}\n\n#[cfg(not(target_arch = \"wasm32\"))]\nlazy_static::lazy_static! {\n    static ref START: std::time::Instant = std::time::Instant::now();\n}\n\nfn now() -> f64 {\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        let doc = web_sys::window().unwrap();\n        let perf = doc.performance().unwrap();\n        perf.now()\n    }\n    #[cfg(not(target_arch = \"wasm32\"))]\n    {\n        let now = std::time::Instant::now();\n        let duration = now.duration_since(*START);\n        duration.as_secs_f64()\n    }\n}\n\nstruct AppUi {\n    ui: UiRenderer,\n    fps_text: UiText,\n    fps_counter: FPSCounter,\n    fps_background: UiRect,\n    last_fps_display: f64,\n}\n\nimpl AppUi {\n    fn make_fps_widget(fps_counter: &FPSCounter, ui: &mut UiRenderer) -> (UiText, UiRect) {\n        let offset = Vec2::new(2.0, 2.0);\n        let text = format!(\"{}fps\", fps_counter.current_fps_string());\n        let fps_text = ui.add_text(\n            Section::default()\n                .add_text(\n                    Text::new(&text)\n                        .with_scale(32.0)\n                        .with_color([0.0, 0.0, 0.0, 1.0]),\n                )\n                .with_screen_position((offset.x, offset.y)),\n        );\n        let (min, max) = fps_text.bounds();\n        let size = max - min;\n        let background = ui\n            .add_rect()\n            .with_position(min)\n            .with_size(size)\n            .with_fill_color(Vec4::ONE)\n            .with_z(-0.9);\n        (fps_text, background)\n    }\n\n    fn tick(&mut self) {\n        self.fps_counter.next_frame();\n        let now = now();\n        if now - self.last_fps_display >= 1.0 {\n            // Remove old text and background before recreating.\n            self.ui.remove_text(&self.fps_text);\n            self.ui.remove_rect(&self.fps_background);\n            let (fps_text, background) = Self::make_fps_widget(&self.fps_counter, &mut self.ui);\n            self.fps_text = fps_text;\n            self.fps_background = background;\n            self.last_fps_display = now;\n        }\n    }\n}\n\npub enum Model {\n    Gltf(Box<GltfDocument>),\n    Default(Primitive),\n    None,\n}\n\npub struct App {\n    last_frame_instant: f64,\n    skybox_image_bytes: Option<Vec<u8>>,\n    loads: Arc<Mutex<HashMap<std::path::PathBuf, Vec<u8>>>>,\n    pub stage: Stage,\n    camera: Camera,\n    _lighting: AnalyticalLight,\n    model: Model,\n    animators: Option<Vec<Animator>>,\n    animations_conflict: bool,\n    pub camera_controller: Box<dyn CameraController + 'static>,\n    ui: AppUi,\n}\n\nimpl App {\n    pub fn new(ctx: &Context, camera_control: CameraControl) -> Self {\n        let stage = ctx\n            .new_stage()\n            .with_background_color(DARK_BLUE_BG_COLOR)\n            .with_bloom_mix_strength(0.5)\n            .with_bloom_filter_radius(4.0)\n            .with_msaa_sample_count(4);\n        let size = ctx.get_size();\n        let (proj, view) = renderling::camera::default_perspective(size.x as f32, size.y as f32);\n        let camera = stage.new_camera().with_projection_and_view(proj, view);\n\n        let sunlight = stage\n            .new_directional_light()\n            .with_direction(Vec3::NEG_Y)\n            .with_color(renderling::math::hex_to_vec4(0xFDFBD3FF))\n            .with_intensity(Lux::OUTDOOR_SUNSET);\n\n        stage\n            .set_atlas_size(wgpu::Extent3d {\n                width: 2048,\n                height: 2048,\n                depth_or_array_layers: 32,\n            })\n            .unwrap();\n\n        let mut ui = UiRenderer::new(ctx);\n        let _ = ui.add_font(FontArc::try_from_slice(FONT_BYTES).unwrap());\n        let fps_counter = FPSCounter::default();\n        let (fps_text, fps_background) = AppUi::make_fps_widget(&fps_counter, &mut ui);\n\n        Self {\n            ui: AppUi {\n                ui,\n                fps_text,\n                fps_counter,\n                fps_background,\n                last_fps_display: now(),\n            },\n            stage,\n            camera,\n            _lighting: sunlight.into_generic(),\n            model: Model::None,\n            animators: None,\n            animations_conflict: false,\n\n            skybox_image_bytes: None,\n            loads: Arc::new(Mutex::new(HashMap::default())),\n            last_frame_instant: now(),\n\n            camera_controller: match camera_control {\n                CameraControl::Turntable => Box::new(TurntableCameraController::default()),\n                CameraControl::WasdMouse => Box::new(WasdMouseCameraController::default()),\n            },\n        }\n    }\n\n    pub fn tick(&mut self) {\n        self.camera_controller.tick();\n        self.tick_loads();\n        self.update_view();\n        self.animate();\n        self.ui.tick();\n    }\n\n    pub fn render(&mut self, ctx: &Context) {\n        log::info!(\"render\");\n        let frame = ctx.get_next_frame().unwrap();\n        self.stage.render(&frame.view());\n        self.ui.ui.render(&frame.view());\n        frame.present();\n        log::info!(\"render done\");\n    }\n\n    pub fn update_view(&mut self) {\n        self.camera_controller\n            .update_camera(self.stage.get_size(), &self.camera);\n    }\n\n    pub fn load_hdr_skybox(&mut self, bytes: Vec<u8>) {\n        log::info!(\"loading skybox\");\n        let img = AtlasImage::from_hdr_bytes(&bytes).unwrap();\n        let skybox = Skybox::new(self.stage.runtime(), img);\n        self.skybox_image_bytes = Some(bytes);\n        self.stage.use_skybox(&skybox);\n        let ibl = self.stage.new_ibl(&skybox);\n        self.stage.use_ibl(&ibl);\n    }\n\n    fn set_model(&mut self, model: Model) {\n        match std::mem::replace(&mut self.model, model) {\n            Model::Gltf(gltf_document) => {\n                // Remove all the things that was loaded by the document\n                for prim in gltf_document.primitives.values().flatten() {\n                    self.stage.remove_primitive(prim);\n                }\n                for light in gltf_document.lights.iter() {\n                    self.stage.remove_light(light);\n                }\n            }\n            Model::Default(primitive) => {\n                self.stage.remove_primitive(&primitive);\n            }\n            Model::None => {}\n        }\n    }\n\n    pub fn load_default_model(&mut self) {\n        log::info!(\"loading default model\");\n        let mut min = Vec3::splat(f32::INFINITY);\n        let mut max = Vec3::splat(f32::NEG_INFINITY);\n\n        self.last_frame_instant = now();\n        let vertices = self\n            .stage\n            .new_vertices(renderling::math::unit_cube().into_iter().map(|(p, n)| {\n                let p = p * 2.0;\n                min = min.min(p);\n                max = max.max(p);\n                Vertex::default()\n                    .with_position(p)\n                    .with_normal(n)\n                    .with_color(Vec4::new(1.0, 0.0, 0.0, 1.0))\n            }));\n        let primitive = self\n            .stage\n            .new_primitive()\n            .with_vertices(vertices)\n            .with_bounds({\n                log::info!(\"default model bounds: {min} {max}\");\n                BoundingSphere::from((min, max))\n            });\n\n        self.set_model(Model::Default(primitive));\n\n        self.camera_controller.reset(Aabb::new(min, max));\n        self.camera_controller\n            .update_camera(self.stage.get_size(), &self.camera);\n    }\n\n    fn load_gltf_model(&mut self, path: impl AsRef<std::path::Path>, bytes: &[u8]) {\n        log::info!(\"loading gltf\");\n        self.camera_controller\n            .reset(Aabb::new(Vec3::NEG_ONE, Vec3::ONE));\n        self.stage.clear_images().unwrap();\n        let doc = match self.stage.load_gltf_document_from_bytes(bytes) {\n            Err(e) => {\n                log::error!(\"gltf loading error: {e}\");\n                if cfg!(not(target_arch = \"wasm32\")) {\n                    log::info!(\"attempting to load by filesystem\");\n                    match self.stage.load_gltf_document_from_path(path) {\n                        Ok(doc) => doc,\n                        Err(e) => {\n                            log::error!(\"gltf loading error: {e}\");\n                            return;\n                        }\n                    }\n                } else {\n                    return;\n                }\n            }\n            Ok(doc) => doc,\n        };\n\n        // find the bounding box of the model so we can display it correctly\n        let mut min = Vec3::splat(f32::INFINITY);\n        let mut max = Vec3::splat(f32::NEG_INFINITY);\n\n        let scene = doc.default_scene.unwrap_or(0);\n        log::info!(\"Displaying scene {scene}\");\n\n        let nodes = doc.recursive_nodes_in_scene(scene);\n        log::trace!(\"  nodes:\");\n        for node in nodes {\n            let tfrm = Mat4::from(node.global_transform());\n            if let Some(mesh_index) = node.mesh {\n                // UNWRAP: safe because we know the node exists\n                for primitive in doc.meshes.get(mesh_index).unwrap().primitives.iter() {\n                    let bbmin = tfrm.transform_point3(primitive.bounding_box.0);\n                    let bbmax = tfrm.transform_point3(primitive.bounding_box.1);\n                    min = min.min(bbmin);\n                    max = max.max(bbmax);\n                }\n            }\n        }\n\n        log::trace!(\"Bounding box: {min} {max}\");\n        let bounding_box = Aabb::new(min, max);\n        self.camera_controller.reset(bounding_box);\n        self.camera_controller\n            .update_camera(self.stage.get_size(), &self.camera);\n\n        self.last_frame_instant = now();\n\n        if doc.animations.is_empty() {\n            log::trace!(\"  animations: none\");\n        } else {\n            log::trace!(\"  animations:\");\n        }\n        let mut animated_nodes = HashSet::default();\n        let mut has_conflicting_animations = false;\n        self.animators = Some(\n            doc.animations\n                .iter()\n                .enumerate()\n                .map(|(i, a)| {\n                    let target_nodes = a.target_node_indices().collect::<HashSet<_>>();\n                    has_conflicting_animations =\n                        has_conflicting_animations || !animated_nodes.is_disjoint(&target_nodes);\n                    animated_nodes.extend(target_nodes);\n\n                    log::trace!(\"    {i} {:?} {}s\", a.name, a.length_in_seconds());\n                    // for (t, tween) in a.tweens.iter().enumerate() {\n                    //     log::trace!(\n                    //         \"      tween {t} targets node {} {}\",\n                    //         tween.target_node_index,\n                    //         tween.properties.description()\n                    //     );\n                    // }\n                    Animator::new(doc.nodes.iter(), a.clone())\n                })\n                .collect(),\n        );\n        if has_conflicting_animations {\n            log::trace!(\"  and some animations conflict\");\n        }\n        self.animations_conflict = has_conflicting_animations;\n\n        // // Update lights and shadows\n        // for light in doc.lights.iter() {\n        //     if let Some(dir) = light.details.as_directional() {\n        //         log::info!(\"found a directional light to use for shadows\");\n        //         {\n        //             let (p, j) = dir.get().shadow_mapping_projection_and_view(\n        //                 &light.node_transform.get_global_transform().into(),\n        //                 &self.camera.get(),\n        //             );\n        //             let mut guard = self.lighting.shadow_map.descriptor_lock();\n        //             guard.light_space_transform = p * j;\n        //         }\n\n        //         self.lighting\n        //             .shadow_map\n        //             .update(&self.lighting.lighting,\n        // doc.primitives.values().flatten());         self.lighting.light =\n        // light.light.clone();         self.lighting.light_details =\n        // dir.clone();     }\n        // }\n\n        self.set_model(Model::Gltf(Box::new(doc)));\n    }\n\n    pub fn tick_loads(&mut self) {\n        let loaded = std::mem::take(&mut *self.loads.lock().unwrap());\n        for (path, bytes) in loaded.into_iter() {\n            log::info!(\"loaded {}bytes from {}\", bytes.len(), path.display());\n            match is_file_supported(&path) {\n                Some(SupportedFileType::Gltf) => self.load_gltf_model(path, &bytes),\n                Some(SupportedFileType::Hdr) => self.load_hdr_skybox(bytes),\n                None => {}\n            }\n        }\n    }\n\n    /// Queues a load operation.\n    pub fn load(&mut self, path: &str) {\n        let path = std::path::PathBuf::from(path);\n        let loads = self.loads.clone();\n\n        #[cfg(target_arch = \"wasm32\")]\n        {\n            wasm_bindgen_futures::spawn_local(async move {\n                let path_str = format!(\"{}\", path.display());\n                let bytes = loading_bytes::load(&path_str).await.unwrap();\n                let mut loads = loads.lock().unwrap();\n                loads.insert(path, bytes);\n                log::debug!(\"loaded {path_str}\");\n            });\n        }\n        #[cfg(not(target_arch = \"wasm32\"))]\n        {\n            let _ = std::thread::spawn(move || {\n                let bytes = std::fs::read(&path)\n                    .unwrap_or_else(|e| panic!(\"could not load '{}': {e}\", path.display()));\n                let mut loads = loads.lock().unwrap();\n                loads.insert(path, bytes);\n            });\n        }\n    }\n\n    pub fn set_size(&mut self, size: UVec2) {\n        self.stage.set_size(size);\n    }\n\n    pub fn animate(&mut self) {\n        let now = now();\n        let dt_seconds = now - self.last_frame_instant;\n        self.last_frame_instant = now;\n        self.camera_controller.tick();\n        if let Some(animators) = self.animators.as_mut() {\n            if self.animations_conflict {\n                if let Some(animator) = animators.first_mut() {\n                    animator.progress(dt_seconds as f32).unwrap();\n                }\n            } else {\n                for animator in animators.iter_mut() {\n                    animator.progress(dt_seconds as f32).unwrap();\n                }\n            }\n        }\n    }\n}\n\n// /// Sets up the demo for a given model\n// pub async fn demo(\n//     ctx: &Context,\n//     model: Option<impl AsRef<str>>,\n//     skybox: Option<impl AsRef<str>>,\n// ) -> impl FnMut(&mut Context, Option<&winit::event::WindowEvent>) {\n//     let mut app = App::new(ctx).await;\n//     if let Some(file) = model {\n//         app.load(file.as_ref());\n//     }\n//     if let Some(file) = skybox {\n//         app.load(file.as_ref());\n//     }\n//     let mut event_state = renderling_gpui::EventState::default();\n//     move |r, ev: Option<&winit::event::WindowEvent>| {\n//         if let Some(ev) = ev {\n//             match ev {\n//                 winit::event::WindowEvent::MouseWheel { delta, .. } => {\n//                     let delta = match delta {\n//                         winit::event::MouseScrollDelta::LineDelta(_, up) =>\n// *up,                         winit::event::MouseScrollDelta::PixelDelta(pos)\n// => pos.y as f32,                     };\n\n//                     app.zoom(r, delta);\n//                 }\n//                 winit::event::WindowEvent::CursorMoved { position, .. } => {\n//                     app.pan(r, *position);\n//                 }\n//                 winit::event::WindowEvent::MouseInput { state, button, .. }\n// => {                     app.mouse_button(*state, *button);\n//                 }\n//                 winit::event::WindowEvent::KeyboardInput { input, .. } => {\n//                     app.key_input(r, *input);\n//                 }\n//                 winit::event::WindowEvent::Resized(size) => {\n//                     log::trace!(\"resizing to {size:?}\");\n//                     app.resize(r, size.width, size.height);\n//                     let _ = app\n//                         .gpui\n//                         .0\n//                         .graph\n//                         .get_resource::<ScreenSize>()\n//                         .unwrap()\n//                         .unwrap();\n//                 }\n//                 winit::event::WindowEvent::DroppedFile(path) => {\n//                     log::trace!(\"got dropped file event: {}\",\n// path.display());                     let path = format!(\"{}\",\n// path.display());                     app.load(&path);\n//                 }\n//                 _ => {}\n//             }\n\n//             if let Some(ev) = event_state.event_from_winit(ev) {\n//                 let scene =\n// r.graph.get_resource_mut::<Scene>().unwrap().unwrap();                 let\n// channel = scene.get_debug_channel();                 let mut\n// set_debug_channel = |mode| {                     log::debug!(\"setting debug\n// mode to {mode:?}\");                     if channel != mode {\n//                         scene.set_debug_channel(mode);\n//                     } else {\n//                         scene.set_debug_channel(DebugChannel::None);\n//                     }\n//                 };\n\n//                 match app.ui.event(ev) {\n//                     None => {}\n//                     Some(ev) => match ev {\n//                         UiEvent::ToggleDebugChannel(channel) =>\n// set_debug_channel(channel),                     },\n//                 }\n//             }\n//         } else {\n//             app.tick_loads(r);\n//             app.update_camera_view(r);\n//             app.animate(r);\n//             app.gpui.layout(&mut app.ui);\n//             app.gpui.render(&mut app.ui);\n//             r.render().unwrap();\n//         }\n//     }\n// }\n"
  },
  {
    "path": "crates/example/src/main.rs",
    "content": "//! Main entry point for the gltf viewer.\nuse std::sync::Arc;\n\nuse clap::Parser;\nuse example::{camera::CameraControl, App};\nuse renderling::{\n    context::Context,\n    glam::{UVec2, Vec2},\n};\nuse winit::{application::ApplicationHandler, event::WindowEvent, window::WindowAttributes};\n\n#[derive(Debug, Parser)]\n#[command(author, version, about)]\nstruct Cli {\n    /// Optional gltf model to load at startup\n    #[arg(short, long)]\n    model: Option<String>,\n\n    /// Optional HDR image to use as skybox at startup\n    #[arg(short, long)]\n    skybox: Option<String>,\n\n    /// Camera control scheme\n    #[arg(short, long, default_value = \"turntable\")]\n    camera_control: CameraControl,\n    // /// Optional number of repeat instances of the same model\n    // #[arg(short, long)]\n    // repeat_n: Option<u32>,\n}\n\nstruct InnerApp {\n    ctx: Context,\n    app: App,\n}\n\nimpl InnerApp {\n    fn tick(&mut self) {\n        self.app.tick();\n    }\n\n    fn event(&mut self, event: WindowEvent) -> bool {\n        match event {\n            winit::event::WindowEvent::DroppedFile(path) => {\n                log::trace!(\"got dropped file event: {}\", path.display());\n                let path = format!(\"{}\", path.display());\n                self.app.load(&path);\n            }\n\n            winit::event::WindowEvent::CloseRequested\n            | winit::event::WindowEvent::KeyboardInput {\n                event:\n                    winit::event::KeyEvent {\n                        logical_key: winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape),\n                        ..\n                    },\n                ..\n            } => return true,\n            winit::event::WindowEvent::Resized(size) => {\n                let size = UVec2::new(size.width, size.height);\n                self.ctx.set_size(size);\n                self.app.set_size(size);\n            }\n            winit::event::WindowEvent::RedrawRequested => {\n                self.ctx.get_device().poll(wgpu::PollType::Wait).unwrap();\n            }\n            e => self.app.camera_controller.window_event(e),\n        }\n        false\n    }\n}\n\nstruct OuterApp {\n    cli: Cli,\n    inner: Option<InnerApp>,\n}\n\nimpl ApplicationHandler for OuterApp {\n    fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {\n        let window_size = winit::dpi::LogicalSize {\n            width: 800,\n            height: 600,\n        };\n        let attributes = WindowAttributes::default()\n            .with_inner_size(window_size)\n            .with_title(\"renderling gltf viewer\");\n        let window = Arc::new(event_loop.create_window(attributes).unwrap());\n\n        // Set up a new renderling context\n        let ctx = futures_lite::future::block_on(Context::from_winit_window(None, window.clone()));\n        let mut app = App::new(&ctx, self.cli.camera_control);\n        if let Some(file) = self.cli.model.as_ref() {\n            log::info!(\"loading model '{file}'\");\n            app.load(file.as_ref());\n        } else {\n            log::info!(\"loading default model\");\n            app.load_default_model();\n        }\n        if let Some(file) = self.cli.skybox.as_ref() {\n            log::info!(\"loading skybox '{file}'\");\n            app.load(file.as_ref());\n        }\n        self.inner = Some(InnerApp { ctx, app });\n    }\n\n    fn about_to_wait(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {\n        if let Some(inner) = self.inner.as_mut() {\n            inner.tick();\n            inner.app.render(&inner.ctx);\n        }\n    }\n\n    fn window_event(\n        &mut self,\n        event_loop: &winit::event_loop::ActiveEventLoop,\n        _window_id: winit::window::WindowId,\n        event: winit::event::WindowEvent,\n    ) {\n        if let Some(inner) = self.inner.as_mut() {\n            if inner.event(event) {\n                event_loop.exit();\n            }\n        }\n    }\n\n    fn device_event(\n        &mut self,\n        _event_loop: &winit::event_loop::ActiveEventLoop,\n        _device_id: winit::event::DeviceId,\n        event: winit::event::DeviceEvent,\n    ) {\n        if let Some(inner) = self.inner.as_mut() {\n            if let winit::event::DeviceEvent::MouseMotion { delta } = event {\n                inner\n                    .app\n                    .camera_controller\n                    .mouse_motion(Vec2::new(delta.0 as f32, delta.1 as f32))\n            }\n        }\n    }\n}\n\nfn main() {\n    let cli = Cli::parse();\n    env_logger::builder().init();\n    log::info!(\"starting up with options: {cli:#?}\");\n\n    let event_loop = winit::event_loop::EventLoop::new().unwrap();\n    event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);\n    let mut outer_app = OuterApp { cli, inner: None };\n    event_loop.run_app(&mut outer_app).unwrap();\n}\n"
  },
  {
    "path": "crates/example/src/time.rs",
    "content": "//! Time constructs.\n//!\n//! Because Instant::now() doesn't work on arch = wasm32.\npub use std::time::Duration;\n#[cfg(not(target_arch = \"wasm32\"))]\npub use std::time::Instant;\n#[cfg(target_arch = \"wasm32\")]\nuse web_sys::window;\n\n#[derive(Clone, Copy, Debug)]\npub struct Time {\n    #[cfg(target_arch = \"wasm32\")]\n    time: u32,\n\n    #[cfg(not(target_arch = \"wasm32\"))]\n    time: Instant,\n}\n\nimpl Time {\n    #[cfg(not(target_arch = \"wasm32\"))]\n    pub fn now() -> Self {\n        Time {\n            time: Instant::now(),\n        }\n    }\n\n    #[cfg(target_arch = \"wasm32\")]\n    pub fn now() -> Self {\n        Time {\n            time: window().unwrap().performance().unwrap().now() as u32,\n        }\n    }\n\n    #[cfg(not(target_arch = \"wasm32\"))]\n    pub fn millis_since(&self, then: Time) -> u32 {\n        self.time.duration_since(then.time).as_millis() as u32\n    }\n\n    #[cfg(target_arch = \"wasm32\")]\n    pub fn millis_since(&self, then: Time) -> u32 {\n        self.time - then.time\n    }\n}\n\npub const FPS_COUNTER_BUFFER_SIZE: usize = 60;\n\npub struct CounterBuffer<T> {\n    buffer: [T; FPS_COUNTER_BUFFER_SIZE],\n    index: usize,\n}\n\nimpl CounterBuffer<f32> {\n    pub fn new(init: f32) -> Self {\n        CounterBuffer {\n            buffer: [init; FPS_COUNTER_BUFFER_SIZE],\n            index: 0,\n        }\n    }\n\n    pub fn write(&mut self, val: f32) {\n        self.buffer[self.index] = val;\n        self.index = (self.index + 1) % self.buffer.len();\n    }\n\n    pub fn average(&self) -> f32 {\n        self.buffer.iter().fold(0.0, |sum, dt| sum + dt) / self.buffer.len() as f32\n    }\n\n    pub fn current(&self) -> f32 {\n        let last_index = if self.index == 0 {\n            self.buffer.len() - 1\n        } else {\n            self.index - 1\n        };\n        self.buffer[last_index]\n    }\n\n    pub fn frames(&self) -> &[f32; FPS_COUNTER_BUFFER_SIZE] {\n        &self.buffer\n    }\n}\n\npub struct FPSCounter {\n    counter: CounterBuffer<f32>,\n    last_instant: Time,\n    last_dt: f32,\n    averages: CounterBuffer<f32>,\n}\n\nimpl Default for FPSCounter {\n    fn default() -> Self {\n        FPSCounter::new()\n    }\n}\n\nimpl FPSCounter {\n    pub fn new() -> FPSCounter {\n        FPSCounter {\n            counter: CounterBuffer::new(0.0),\n            last_instant: Time::now(),\n            last_dt: 0.0,\n            averages: CounterBuffer::new(0.0),\n        }\n    }\n\n    pub fn restart(&mut self) {\n        self.last_instant = Time::now();\n    }\n\n    pub fn next_frame(&mut self) -> f32 {\n        let this_instant = Time::now();\n        let delta = this_instant.millis_since(self.last_instant);\n        let dt_seconds = delta as f32 / 1000.0;\n        self.last_dt = dt_seconds;\n        self.last_instant = this_instant;\n        self.counter.write(dt_seconds);\n        if self.counter.index + 1 == FPS_COUNTER_BUFFER_SIZE {\n            let avg = self.counter.average();\n            self.averages.write(avg);\n        }\n        dt_seconds\n    }\n\n    pub fn avg_frame_delta(&self) -> f32 {\n        self.counter.average()\n    }\n\n    pub fn current_fps(&self) -> f32 {\n        1.0 / self.avg_frame_delta()\n    }\n\n    pub fn current_fps_string(&self) -> String {\n        let avg = self.averages.current();\n        format!(\"{:.1}\", 1.0 / avg)\n    }\n\n    /// Return the last frame's delta in seconds.\n    pub fn last_delta(&self) -> f32 {\n        self.last_dt\n    }\n\n    pub fn second_averages(&self) -> &[f32; FPS_COUNTER_BUFFER_SIZE] {\n        self.averages.frames()\n    }\n}\n"
  },
  {
    "path": "crates/example/src/utils.rs",
    "content": "//! Example app utilities.\n\nuse std::sync::Arc;\n\nuse renderling::context::Context;\nuse winit::monitor::MonitorHandle;\n\npub trait TestAppHandler: winit::application::ApplicationHandler {\n    fn new(\n        event_loop: &winit::event_loop::ActiveEventLoop,\n        window: Arc<winit::window::Window>,\n        ctx: &Context,\n    ) -> Self;\n    fn render(&mut self, ctx: &Context);\n}\n\npub(crate) struct InnerTestApp<T> {\n    ctx: Context,\n    app: T,\n}\n\npub struct TestApp<T> {\n    size: winit::dpi::Size,\n    inner: Option<InnerTestApp<T>>,\n}\n\nimpl<T: TestAppHandler> winit::application::ApplicationHandler for TestApp<T> {\n    fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {\n        // I have my editor on the high monitor, and I read on the low one,\n        // so set the position of the opened window to the lowest monitor so\n        // it doesn't constantly switch away from my editor...\n        let maybe_lowest_monitor =\n            event_loop\n                .available_monitors()\n                .fold(None::<MonitorHandle>, |mut acc, monitor| {\n                    let position = monitor.position();\n                    let name = monitor.name().unwrap_or(\"unknown\".to_owned());\n                    log::info!(\"found monitor: {name} {position:?}\");\n                    if let Some(current_monitor) = acc.as_mut() {\n                        // greater than here because y increases downward\n                        if monitor.position().y > current_monitor.position().y {\n                            acc = Some(monitor);\n                        }\n                    } else {\n                        acc = Some(monitor);\n                    }\n                    acc\n                });\n\n        let window = Arc::new(\n            event_loop\n                .create_window(\n                    winit::window::WindowAttributes::default()\n                        .with_inner_size(self.size)\n                        .with_title(\"test app\")\n                        .with_fullscreen(\n                            maybe_lowest_monitor\n                                .map(|m| winit::window::Fullscreen::Borderless(Some(m))),\n                        ),\n                )\n                .unwrap(),\n        );\n        let ctx = futures_lite::future::block_on(Context::from_winit_window(None, window.clone()));\n        let mut app = T::new(event_loop, window, &ctx);\n        app.resumed(event_loop);\n        self.inner = Some(InnerTestApp { app, ctx });\n    }\n\n    fn window_event(\n        &mut self,\n        event_loop: &winit::event_loop::ActiveEventLoop,\n        window_id: winit::window::WindowId,\n        event: winit::event::WindowEvent,\n    ) {\n        if let Some(inner) = self.inner.as_mut() {\n            match event {\n                // winit::event::WindowEvent::RedrawRequested => {\n                //     inner.ctx.get_device().poll(wgpu::Maintain::Wait);\n                // }\n                winit::event::WindowEvent::CloseRequested\n                | winit::event::WindowEvent::KeyboardInput {\n                    event:\n                        winit::event::KeyEvent {\n                            logical_key:\n                                winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape),\n                            ..\n                        },\n                    ..\n                } => {\n                    event_loop.exit();\n                }\n                _ => {\n                    inner.app.window_event(event_loop, window_id, event);\n                }\n            }\n        }\n    }\n\n    fn device_event(\n        &mut self,\n        event_loop: &winit::event_loop::ActiveEventLoop,\n        device_id: winit::event::DeviceId,\n        event: winit::event::DeviceEvent,\n    ) {\n        if let Some(inner) = self.inner.as_mut() {\n            inner.app.device_event(event_loop, device_id, event);\n        }\n    }\n\n    fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {\n        if let Some(inner) = self.inner.as_mut() {\n            inner.app.about_to_wait(event_loop);\n            inner.app.render(&inner.ctx);\n        }\n    }\n}\n\nimpl<T: TestAppHandler> TestApp<T> {\n    pub fn new(size: impl Into<winit::dpi::Size>) -> Self {\n        TestApp {\n            size: size.into(),\n            inner: None,\n        }\n    }\n\n    pub fn run(&mut self) {\n        let event_loop = winit::event_loop::EventLoop::new().unwrap();\n        event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);\n        event_loop.run_app(self).unwrap();\n    }\n}\n"
  },
  {
    "path": "crates/example-culling/Cargo.toml",
    "content": "[package]\nname = \"example-culling\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nenv_logger.workspace = true\nexample = { path = \"../example\" }\nfastrand = \"2.1.1\"\nlog.workspace = true\nrenderling = { path = \"../renderling\" }\nwinit.workspace = true\n"
  },
  {
    "path": "crates/example-culling/src/main.rs",
    "content": "//! An example app showing (and verifying) how frustum culling works in\n//! `renderling`.\nuse std::sync::Arc;\n\nuse example::{camera::CameraController, utils::*};\nuse renderling::{\n    bvol::{Aabb, BoundingSphere},\n    camera::{shader::CameraDescriptor, Camera},\n    context::Context,\n    geometry::Vertex,\n    glam::{EulerRot, Mat4, Quat, UVec2, Vec3, Vec4},\n    light::{AnalyticalLight, DirectionalLight, Lux},\n    material::Material,\n    math::hex_to_vec4,\n    primitive::Primitive,\n    stage::Stage,\n    tonemapping::srgba_to_linear,\n};\nuse winit::{\n    application::ApplicationHandler,\n    event::{ElementState, KeyEvent},\n    event_loop::ActiveEventLoop,\n    keyboard::Key,\n};\n\nconst MIN_SIZE: f32 = 0.5;\nconst MAX_SIZE: f32 = 4.0;\nconst MAX_DIST: f32 = 40.0;\nconst BOUNDS: Aabb = Aabb {\n    min: Vec3::new(-MAX_DIST, -MAX_DIST, -MAX_DIST),\n    max: Vec3::new(MAX_DIST, MAX_DIST, MAX_DIST),\n};\n\nstruct AppCamera(Camera);\nstruct FrustumCamera(CameraDescriptor);\n\ntype Type = Primitive;\n\n#[allow(dead_code)]\nstruct CullingExample {\n    app_camera: AppCamera,\n    controller: example::camera::TurntableCameraController,\n    stage: Stage,\n    dlights: [AnalyticalLight<DirectionalLight>; 2],\n    frustum_camera: FrustumCamera,\n    frustum_primitive: Primitive,\n    material_aabb_outside: Material,\n    material_aabb_overlapping: Material,\n    primitives: Vec<Type>,\n    next_k: u64,\n}\n\nimpl CullingExample {\n    fn make_aabb(center: Vec3, half_size: Vec3) -> Aabb {\n        let min = center - half_size;\n        let max = center + half_size;\n        Aabb::new(min, max)\n    }\n\n    fn make_aabbs(\n        seed: u64,\n        stage: &Stage,\n        frustum_camera: &FrustumCamera,\n        material_outside: &Material,\n        material_overlapping: &Material,\n    ) -> Vec<Primitive> {\n        log::info!(\"generating aabbs with seed {seed}\");\n        fastrand::seed(seed);\n        (0..25u32)\n            .map(|i| {\n                log::info!(\"aabb {i}\");\n                let x = fastrand::f32() * MAX_DIST - MAX_DIST / 2.0;\n                let y = fastrand::f32() * MAX_DIST - MAX_DIST / 2.0;\n                let z = fastrand::f32() * MAX_DIST - MAX_DIST / 2.0;\n                let w = fastrand::f32() * (MAX_SIZE - MIN_SIZE) + MIN_SIZE;\n                let h = fastrand::f32() * (MAX_SIZE - MIN_SIZE) + MIN_SIZE;\n                let l = fastrand::f32() * (MAX_SIZE - MIN_SIZE) + MIN_SIZE;\n\n                let rx = std::f32::consts::PI * fastrand::f32();\n                let ry = std::f32::consts::PI * fastrand::f32();\n                let rz = std::f32::consts::PI * fastrand::f32();\n\n                let rotation = Quat::from_euler(EulerRot::XYZ, rx, ry, rz);\n\n                let center = Vec3::new(x, y, z);\n                let half_size = Vec3::new(w, h, l);\n                let aabb = Self::make_aabb(Vec3::ZERO, half_size);\n\n                let transform = stage\n                    .new_transform()\n                    .with_translation(center)\n                    .with_rotation(rotation);\n                stage\n                    .new_primitive()\n                    .with_vertices(stage.new_vertices(aabb.get_mesh().into_iter().map(\n                        |(position, normal)| Vertex {\n                            position,\n                            normal,\n                            ..Default::default()\n                        },\n                    )))\n                    .with_material(\n                        if BoundingSphere::from(aabb)\n                            .is_inside_camera_view(&frustum_camera.0, transform.descriptor())\n                            .0\n                        {\n                            material_overlapping\n                        } else {\n                            material_outside\n                        },\n                    )\n                    .with_transform(transform)\n            })\n            .collect::<Vec<_>>()\n    }\n}\n\nimpl ApplicationHandler for CullingExample {\n    fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {\n        log::info!(\"culling-example resumed\");\n    }\n\n    fn window_event(\n        &mut self,\n        _event_loop: &winit::event_loop::ActiveEventLoop,\n        _window_id: winit::window::WindowId,\n        event: winit::event::WindowEvent,\n    ) {\n        match event {\n            winit::event::WindowEvent::KeyboardInput {\n                event:\n                    KeyEvent {\n                        logical_key: Key::Character(c),\n                        state: ElementState::Pressed,\n                        ..\n                    },\n                ..\n            } => {\n                if c.as_str() == \"r\" {\n                    // remove all the primitives, dropping their resources\n                    for primitive in self.primitives.drain(..) {\n                        self.stage.remove_primitive(&primitive);\n                    }\n\n                    let _ = self.stage.commit();\n                    self.primitives.extend(Self::make_aabbs(\n                        self.next_k,\n                        &self.stage,\n                        &self.frustum_camera,\n                        &self.material_aabb_outside,\n                        &self.material_aabb_overlapping,\n                    ));\n                    self.next_k += 1;\n                }\n            }\n            winit::event::WindowEvent::Resized(physical_size) => {\n                log::info!(\"window resized to {physical_size:?}\");\n                let size = UVec2 {\n                    x: physical_size.width,\n                    y: physical_size.height,\n                };\n                self.stage.set_size(size);\n                self.controller.update_camera(size, &self.app_camera.0);\n            }\n            event => self.controller.window_event(event),\n        }\n    }\n\n    fn device_event(\n        &mut self,\n        _event_loop: &winit::event_loop::ActiveEventLoop,\n        _device_id: winit::event::DeviceId,\n        event: winit::event::DeviceEvent,\n    ) {\n        self.controller.device_event(event);\n    }\n}\n\nimpl TestAppHandler for CullingExample {\n    fn new(\n        _event_loop: &ActiveEventLoop,\n        _window: Arc<winit::window::Window>,\n        ctx: &Context,\n    ) -> Self {\n        let mut seed = 46;\n        let mut prims = vec![];\n        let stage = ctx.new_stage().with_lighting(true);\n        let sunlight_a = stage\n            .new_directional_light()\n            .with_direction(Vec3::new(-0.8, -1.0, 0.5).normalize())\n            .with_color(Vec4::ONE)\n            .with_intensity(Lux::OUTDOOR_SUNSET);\n        let sunlight_b = stage\n            .new_directional_light()\n            .with_direction(Vec3::new(1.0, 1.0, -0.1).normalize())\n            .with_color(Vec4::ONE)\n            .with_intensity(Lux::OUTDOOR_TWILIGHT);\n\n        let dlights = [sunlight_a, sunlight_b];\n\n        let frustum_camera = FrustumCamera({\n            let aspect = 1.0;\n            let fovy = core::f32::consts::FRAC_PI_4;\n            let znear = 4.0;\n            let zfar = 1000.0;\n            let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);\n            let eye = Vec3::new(0.0, 0.0, 10.0);\n            let target = Vec3::ZERO;\n            let up = Vec3::Y;\n            let view = Mat4::look_at_rh(eye, target, up);\n            // let projection = Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, -10.0,\n            // 10.0); let view = Mat4::IDENTITY;\n            CameraDescriptor::new(projection, view)\n        });\n\n        let frustum = frustum_camera.0.frustum();\n        log::info!(\"frustum.planes: {:#?}\", frustum.planes);\n\n        let blue_color = srgba_to_linear(hex_to_vec4(0x7EACB5FF));\n        let red_color = srgba_to_linear(hex_to_vec4(0xC96868FF));\n        let yellow_color = srgba_to_linear(hex_to_vec4(0xFADFA1FF));\n\n        let material_aabb_overlapping = stage.new_material().with_albedo_factor(blue_color);\n        let material_aabb_outside = stage.new_material().with_albedo_factor(red_color);\n        let material_frustum = stage.new_material().with_albedo_factor(yellow_color);\n        let app_camera = AppCamera(stage.new_camera());\n        prims.extend(Self::make_aabbs(\n            seed,\n            &stage,\n            &frustum_camera,\n            &material_aabb_outside,\n            &material_aabb_overlapping,\n        ));\n        seed += 1;\n\n        let frustum_vertices =\n            stage.new_vertices(frustum_camera.0.frustum().get_mesh().into_iter().map(\n                |(position, normal)| Vertex {\n                    position,\n                    normal,\n                    ..Default::default()\n                },\n            ));\n        let frustum_prim = stage\n            .new_primitive()\n            .with_vertices(&frustum_vertices)\n            .with_material(&material_frustum);\n        stage.add_primitive(&frustum_prim);\n\n        Self {\n            next_k: seed,\n            app_camera,\n            frustum_camera,\n            dlights,\n            controller: {\n                let mut c = example::camera::TurntableCameraController::default();\n                c.reset(BOUNDS);\n                c.phi = 0.5;\n                c\n            },\n            stage,\n            material_aabb_overlapping,\n            material_aabb_outside,\n            frustum_primitive: frustum_prim,\n            primitives: prims,\n        }\n    }\n\n    fn render(&mut self, ctx: &Context) {\n        let size = self.stage.get_size();\n        self.controller.update_camera(size, &self.app_camera.0);\n\n        let frame = ctx.get_next_frame().unwrap();\n        self.stage.render(&frame.view());\n        frame.present();\n    }\n}\n\nfn main() {\n    env_logger::builder().init();\n    log::info!(\"starting example-culling\");\n    TestApp::<CullingExample>::new(winit::dpi::LogicalSize::new(800, 600)).run();\n}\n"
  },
  {
    "path": "crates/example-wasm/.gitignore",
    "content": "dist\n"
  },
  {
    "path": "crates/example-wasm/Cargo.toml",
    "content": "[package]\nname = \"example-wasm\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n[dependencies]\nconsole_log = \"^0.2\"\nconsole_error_panic_hook = \"^0.1\"\nexample = { path = \"../example\" }\nfern = \"0.6\"\nfutures-lite.workspace = true\ngltf.workspace = true\nlog.workspace = true\nrenderling = { path = \"../renderling\", features = [\"wasm\", \"winit\"] }\nwasm-bindgen.workspace = true\nwasm-bindgen-futures.workspace = true\nwasm-bindgen-test = \"^0.3\"\nweb-sys = { workspace = true, features = [\"Document\", \"Element\", \"Event\", \"HtmlCanvasElement\", \"HtmlElement\", \"Window\"] }\nwgpu.workspace = true\nwinit.workspace = true\n"
  },
  {
    "path": "crates/example-wasm/Trunk.toml",
    "content": "target = \"index.html\"\n"
  },
  {
    "path": "crates/example-wasm/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <style>\n      /* A sane CSS reset from \n\t\t\t * https://www.digitalocean.com/community/tutorials/css-minimal-css-reset\n\t\t\t*/\n      html {\n        box-sizing: border-box;\n        font-size: 16px;\n      }\n\n      *,\n      *:before,\n      *:after {\n        box-sizing: inherit;\n      }\n\n      body,\n      h1,\n      h2,\n      h3,\n      h4,\n      h5,\n      h6,\n      p,\n      ol,\n      ul {\n        margin: 0;\n        padding: 0;\n        font-weight: normal;\n      }\n\n      ol,\n      ul {\n        list-style: none;\n      }\n\n      img {\n        max-width: 100%;\n        height: auto;\n      }\n\n      main {\n        margin: 0;\n        width: 100%;\n        height: 100%;\n        background-color: #7f7f7f;\n      }\n\n      main canvas {\n        margin: 0;\n        padding: 0;\n        position: absolute;\n        left: 0;\n        right: 0;\n        top: 0;\n        bottom: 0;\n        background-color: #ffffff;\n      }\n\n      section fieldset {\n        margin: 0 0.25em 0.5em 0.25em;\n      }\n    </style>\n\n    <link data-trunk rel=\"copy-file\" href=\"../../gltf/Fox.glb\" />\n    <link data-trunk rel=\"copy-file\" href=\"../../img/hdr/helipad.hdr\" />\n    <link data-trunk rel=\"copy-file\" href=\"../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf\" />\n  </head>\n  <body>\n    <main>\n      <canvas></canvas>\n    </main>\n  </body>\n</html>\n"
  },
  {
    "path": "crates/example-wasm/src/event.rs",
    "content": "//! A light abstraction over UI event callbacks.\n//!\n//! This uses [`futures-lite::Stream`] to send events to downstream listeners.\nuse std::{\n    pin::Pin,\n    sync::{Arc, Mutex},\n    task::Waker,\n};\n\nuse futures_lite::Stream;\nuse wasm_bindgen::{prelude::Closure, JsCast, JsValue};\nuse web_sys::EventTarget;\n\ntype MaybeCallback = Option<Arc<Closure<dyn FnMut(JsValue)>>>;\n\nstruct WebCallback {\n    target: EventTarget,\n    name: String,\n    closure: MaybeCallback,\n    waker: Arc<Mutex<Option<Waker>>>,\n    event: Arc<Mutex<Option<web_sys::Event>>>,\n}\n\nimpl Drop for WebCallback {\n    fn drop(&mut self) {\n        if let Some(arc) = self.closure.take() {\n            if let Ok(closure) = Arc::try_unwrap(arc) {\n                self.target\n                    .remove_event_listener_with_callback(\n                        self.name.as_str(),\n                        closure.as_ref().unchecked_ref(),\n                    )\n                    .unwrap();\n            }\n        }\n    }\n}\n\nimpl Stream for WebCallback {\n    type Item = web_sys::Event;\n\n    fn poll_next(\n        self: Pin<&mut Self>,\n        cx: &mut std::task::Context<'_>,\n    ) -> std::task::Poll<Option<Self::Item>> {\n        let data = self.get_mut();\n        *data.waker.lock().unwrap() = Some(cx.waker().clone());\n\n        if let Some(event) = data.event.lock().unwrap().take() {\n            std::task::Poll::Ready(Some(event))\n        } else {\n            std::task::Poll::Pending\n        }\n    }\n}\n\n/// Listen for events of the given name on the given target.\n/// All events will be sent downstream until the stream is\n/// dropped.\npub fn event_stream(\n    ev_name: &str,\n    target: &web_sys::EventTarget,\n) -> impl Stream<Item = web_sys::Event> {\n    let waker: Arc<Mutex<Option<Waker>>> = Default::default();\n    let waker_here = waker.clone();\n\n    let event: Arc<Mutex<Option<web_sys::Event>>> = Default::default();\n    let event_here = event.clone();\n\n    let closure = Closure::wrap(Box::new(move |val: JsValue| {\n        let ev = val.unchecked_into();\n        *event.lock().unwrap() = Some(ev);\n        if let Some(waker) = waker.lock().unwrap().take() {\n            waker.wake()\n        }\n    }) as Box<dyn FnMut(JsValue)>);\n\n    target\n        .add_event_listener_with_callback(ev_name, closure.as_ref().unchecked_ref())\n        .unwrap();\n\n    WebCallback {\n        target: target.clone(),\n        name: ev_name.to_string(),\n        closure: Some(closure.into()),\n        event: event_here,\n        waker: waker_here,\n    }\n}\n"
  },
  {
    "path": "crates/example-wasm/src/lib.rs",
    "content": "#![allow(dead_code)]\nuse glam::{Vec2, Vec4};\nuse renderling::{camera::Camera, gltf::GltfDocument, stage::Stage, ui::prelude::*};\nuse wasm_bindgen::prelude::*;\nuse web_sys::HtmlCanvasElement;\n\nmod event;\nmod req_animation_frame;\n\nconst HDR_IMAGE_BYTES: &[u8] = include_bytes!(\"../../../img/hdr/helipad.hdr\");\nconst GLTF_FOX_BYTES: &[u8] = include_bytes!(\"../../../gltf/Fox.glb\");\n\nfn surface_from_canvas(_canvas: HtmlCanvasElement) -> Option<wgpu::SurfaceTarget<'static>> {\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        Some(wgpu::SurfaceTarget::Canvas(_canvas))\n    }\n    #[cfg(not(target_arch = \"wasm32\"))]\n    {\n        None\n    }\n}\n\npub struct App {\n    ctx: Context,\n    ui: Ui,\n    path: UiPath,\n    stage: Stage,\n    doc: GltfDocument,\n    camera: Camera,\n    text: UiText,\n}\n\nimpl App {\n    fn tick(&self) {\n        let frame = self.ctx.get_next_frame().unwrap();\n        self.ui.render(&frame.view());\n        self.stage.render(&frame.view());\n        frame.present();\n    }\n}\n\n#[wasm_bindgen(start)]\npub async fn main() {\n    std::panic::set_hook(Box::new(console_error_panic_hook::hook));\n    fern::Dispatch::new()\n        .level(log::LevelFilter::Info)\n        .level_for(\"wgpu\", log::LevelFilter::Warn)\n        .level_for(\"naga\", log::LevelFilter::Trace)\n        .level_for(\"renderling::draw\", log::LevelFilter::Trace)\n        .chain(fern::Output::call(console_log::log))\n        .apply()\n        .unwrap();\n\n    log::info!(\"Starting example-wasm\");\n\n    let dom_window = web_sys::window().unwrap();\n    let dom_doc = dom_window.document().unwrap();\n\n    let canvas = dom_doc\n        .query_selector(\"main canvas\")\n        .unwrap()\n        .unwrap()\n        .dyn_into::<HtmlCanvasElement>()\n        .unwrap();\n    canvas.set_width(800);\n    canvas.set_height(600);\n\n    let surface = surface_from_canvas(canvas.clone()).unwrap();\n    let ctx = Context::try_new_with_surface(800, 600, None, surface)\n        .await\n        .unwrap();\n\n    let ui = ctx.new_ui();\n    let path = ui\n        .path_builder()\n        .with_circle(Vec2::splat(100.0), 20.0)\n        .with_fill_color(Vec4::new(1.0, 1.0, 0.0, 1.0))\n        .fill();\n    let _ = ui\n        .load_font(\"Recursive Mn Lnr St Med Nerd Font Complete.ttf\")\n        .await\n        .expect_throw(\"Could not load font\");\n    let text = ui\n        .text_builder()\n        .with_color(\n            // white\n            Vec4::ONE,\n        )\n        .with_section(Section::default().add_text(Text::new(\"WASM example\").with_scale(24.0)))\n        .build();\n\n    let stage = ctx\n        .new_stage()\n        .with_background_color(\n            // black\n            // Vec3::ZERO.extend(1.0),\n            Vec4::new(1.0, 0.0, 0.0, 1.0),\n        )\n        .with_lighting(false);\n\n    let skybox = stage.new_skybox_from_bytes(HDR_IMAGE_BYTES).unwrap();\n    stage.set_skybox(skybox);\n\n    let fox = stage.load_gltf_document_from_bytes(GLTF_FOX_BYTES).unwrap();\n    log::info!(\"fox aabb: {:?}\", fox.bounding_volume());\n\n    let (p, v) = renderling::camera::default_perspective(800.0, 600.0);\n    let camera = stage.new_camera().with_projection_and_view(p, v);\n\n    let app = App {\n        ctx,\n        ui,\n        path,\n        stage,\n        doc: fox,\n        camera,\n        text,\n    };\n    app.tick();\n\n    loop {\n        let _ = req_animation_frame::next_animation_frame().await;\n        app.tick();\n    }\n}\n"
  },
  {
    "path": "crates/example-wasm/src/req_animation_frame.rs",
    "content": "//! Request animation frame helpers, taken from [mogwai](https://crates.io/crates/mogwai).\nuse std::{cell::RefCell, rc::Rc};\n\nuse wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};\n\nfn req_animation_frame(f: &Closure<dyn FnMut(JsValue)>) {\n    web_sys::window()\n        .expect_throw(\"could not get window\")\n        .request_animation_frame(f.as_ref().unchecked_ref())\n        .expect_throw(\"should register `requestAnimationFrame` OK\");\n}\n\n/// Sets a static rust closure to be called with `window.requestAnimationFrame`.\n///\n/// The static rust closure takes one parameter which is\n/// a timestamp representing the number of milliseconds since the application's\n/// load. See <https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp>\n/// for more info.\nfn request_animation_frame(mut f: impl FnMut(JsValue) + 'static) {\n    let wrapper = Rc::new(RefCell::new(None));\n    let callback = Box::new({\n        let wrapper = wrapper.clone();\n        move |jsval| {\n            f(jsval);\n            wrapper.borrow_mut().take();\n        }\n    }) as Box<dyn FnMut(JsValue)>;\n    let closure: Closure<dyn FnMut(JsValue)> = Closure::wrap(callback);\n    *wrapper.borrow_mut() = Some(closure);\n    req_animation_frame(wrapper.borrow().as_ref().unwrap_throw());\n}\n\n#[derive(Clone, Default)]\n#[expect(clippy::type_complexity, reason = \"not too complex\")]\nstruct NextFrame {\n    closure: Rc<RefCell<Option<Closure<dyn FnMut(JsValue)>>>>,\n    ts: Rc<RefCell<Option<f64>>>,\n    waker: Rc<RefCell<Option<std::task::Waker>>>,\n}\n\nimpl std::future::Future for NextFrame {\n    type Output = f64;\n\n    fn poll(\n        self: std::pin::Pin<&mut Self>,\n        cx: &mut std::task::Context<'_>,\n    ) -> std::task::Poll<Self::Output> {\n        if let Some(ts) = self.ts.borrow_mut().take() {\n            std::task::Poll::Ready(ts)\n        } else {\n            *self.waker.borrow_mut() = Some(cx.waker().clone());\n            std::task::Poll::Pending\n        }\n    }\n}\n\n/// Creates a future that will resolve on the next animation frame.\n///\n/// The future's output is a timestamp representing the number of\n/// milliseconds since the application's load.\n/// See <https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp>\n/// for more info.\npub fn next_animation_frame() -> impl std::future::Future<Output = f64> {\n    // https://rustwasm.github.io/wasm-bindgen/examples/request-animation-frame.html#srclibrs\n    let frame = NextFrame::default();\n\n    *frame.closure.borrow_mut() = Some(Closure::wrap(Box::new({\n        let frame = frame.clone();\n        move |ts_val: JsValue| {\n            *frame.ts.borrow_mut() = Some(ts_val.as_f64().unwrap_or(0.0));\n            if let Some(waker) = frame.waker.borrow_mut().take() {\n                waker.wake();\n            }\n        }\n    }) as Box<dyn FnMut(JsValue)>));\n\n    req_animation_frame(frame.closure.borrow().as_ref().unwrap_throw());\n\n    frame\n}\n"
  },
  {
    "path": "crates/examples/Cargo.toml",
    "content": "[package]\nname = \"examples\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\ndoc-comment = \"0.3\"\nenv_logger.workspace = true\nfutures-lite.workspace = true\nrenderling = { path = \"../renderling\", features = [\"test-utils\"] }\nrenderling_build = { path = \"../renderling-build\" }\ntokio = { workspace = true, features = [\"full\"] }\nwgpu.workspace = true\n"
  },
  {
    "path": "crates/examples/src/context.rs",
    "content": "//! Context manual page.\n\n#[tokio::test]\nasync fn context_page() {\n    // ANCHOR: create\n    use renderling::context::Context;\n\n    let ctx = Context::headless(256, 256).await;\n    // ANCHOR_END: create\n\n    // ANCHOR: frame\n    let frame = ctx.get_next_frame().unwrap();\n    // ...do some rendering\n    //\n    // Then capture the frame into an image, if you like\n    let _image_capture = frame.read_image().await.unwrap();\n    frame.present();\n    // ANCHOR_END: frame\n}\n"
  },
  {
    "path": "crates/examples/src/gltf.rs",
    "content": "//! GLTF manual page.\n\nuse crate::workspace_dir;\n\n#[tokio::test]\nasync fn manual_gltf() {\n    // ANCHOR: setup\n    use renderling::{\n        camera::Camera,\n        context::Context,\n        glam::{Mat4, Vec3, Vec4},\n        stage::Stage,\n    };\n\n    let ctx = Context::headless(256, 256).await;\n    let stage: Stage = ctx\n        .new_stage()\n        .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0));\n\n    let _camera: Camera = {\n        let aspect = 1.0;\n        let fovy = core::f32::consts::PI / 4.0;\n        let znear = 0.1;\n        let zfar = 10.0;\n        let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);\n        let eye = Vec3::new(0.5, 0.5, 0.8);\n        let target = Vec3::new(0.0, 0.3, 0.0);\n        let up = Vec3::Y;\n        let view = Mat4::look_at_rh(eye, target, up);\n\n        stage\n            .new_camera()\n            .with_projection_and_view(projection, view)\n    };\n    // ANCHOR_END: setup\n\n    // ANCHOR: load\n    use renderling::{gltf::GltfDocument, types::GpuOnlyArray};\n    let model: GltfDocument<GpuOnlyArray> = stage\n        .load_gltf_document_from_path(workspace_dir().join(\"gltf/marble_bust_1k.glb\"))\n        .unwrap()\n        .into_gpu_only();\n    println!(\"bounds: {:?}\", model.bounding_volume());\n    // ANCHOR_END: load\n\n    super::cwd_to_manual_assets_dir();\n\n    // ANCHOR: render_1\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame.read_image().await.unwrap();\n    img.save(\"gltf-example-shadow.png\").unwrap();\n    frame.present();\n    // ANCHOR_END: render_1\n\n    // ANCHOR: no_lights\n    stage.set_has_lighting(false);\n    // ANCHOR_END: no_lights\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame.read_image().await.unwrap();\n    img.save(\"gltf-example-unlit.png\").unwrap();\n    frame.present();\n}\n"
  },
  {
    "path": "crates/examples/src/lib.rs",
    "content": "//! # Examples for the manual\n//!\n//! This crate contains examples and snippets that get pulled into the manual\n//! via mdbook links. It also contains tests.\n\n#[cfg(test)]\nmod context;\n\n#[cfg(test)]\nmod stage;\n\n#[cfg(test)]\nmod gltf;\n\n#[cfg(test)]\nmod skybox;\n\n#[cfg(test)]\nmod lighting;\n\npub fn cwd_to_manual_assets_dir() -> std::path::PathBuf {\n    let current_dir =\n        std::path::PathBuf::from(std::env!(\"CARGO_WORKSPACE_DIR\")).join(\"manual/src/assets\");\n    let current_dir = current_dir.canonicalize().unwrap();\n    std::env::set_current_dir(&current_dir).unwrap();\n    println!(\"current dir: {:?}\", std::env::current_dir());\n    current_dir\n}\n\npub fn workspace_dir() -> std::path::PathBuf {\n    renderling_build::workspace_dir().canonicalize().unwrap()\n}\n\npub fn test_output_dir() -> std::path::PathBuf {\n    let dir = renderling_build::test_output_dir();\n    std::fs::create_dir_all(&dir).unwrap();\n    dir.canonicalize().unwrap()\n}\n\npub fn cwd_to_cargo_workspace() -> std::path::PathBuf {\n    let current_dir = workspace_dir();\n    std::env::set_current_dir(&current_dir).unwrap();\n    println!(\"current dir: {:?}\", std::env::current_dir());\n    current_dir\n}\n\ndoc_comment::doctest!(\"../../../manual/src/stage.md\", stage_md);\n\n#[test]\nfn can_test() {\n    assert_eq!(1, 1);\n}\n"
  },
  {
    "path": "crates/examples/src/lighting.rs",
    "content": "//! Lighting examples.\n\nuse crate::{cwd_to_manual_assets_dir, workspace_dir};\n\n#[tokio::test]\nasync fn manual_lighting() {\n    // ANCHOR: setup\n    use renderling::{\n        camera::Camera,\n        context::Context,\n        glam::{Mat4, Vec3, Vec4},\n        gltf::GltfDocument,\n        stage::Stage,\n        types::GpuOnlyArray,\n    };\n\n    let ctx = Context::headless(256, 256).await;\n    let stage: Stage = ctx\n        .new_stage()\n        .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0))\n        .with_lighting(false);\n\n    let _camera: Camera = {\n        let aspect = 1.0;\n        let fovy = core::f32::consts::PI / 4.0;\n        let znear = 0.1;\n        let zfar = 10.0;\n        let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);\n        let eye = Vec3::new(0.5, 0.5, 0.8);\n        let target = Vec3::new(0.0, 0.3, 0.0);\n        let up = Vec3::Y;\n        let view = Mat4::look_at_rh(eye, target, up);\n\n        stage\n            .new_camera()\n            .with_projection_and_view(projection, view)\n    };\n\n    let model: GltfDocument<GpuOnlyArray> = stage\n        .load_gltf_document_from_path(workspace_dir().join(\"gltf/marble_bust_1k.glb\"))\n        .unwrap()\n        .into_gpu_only();\n    println!(\"bounds: {:?}\", model.bounding_volume());\n\n    let skybox = stage\n        .new_skybox_from_path(workspace_dir().join(\"img/hdr/helipad.hdr\"))\n        .unwrap();\n    stage.use_skybox(&skybox);\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    frame.present();\n    // ANCHOR_END: setup\n\n    cwd_to_manual_assets_dir();\n\n    // ANCHOR: lighting_on\n    stage.set_has_lighting(true);\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame.read_image().await.unwrap();\n    img.save(\"lighting/no-lights.png\").unwrap();\n    frame.present();\n    // ANCHOR_END: lighting_on\n\n    // ANCHOR: directional\n    use renderling::{\n        color::css_srgb_color_to_linear,\n        light::{AnalyticalLight, DirectionalLight, Lux},\n    };\n\n    let sunset_amber_sunlight_color = css_srgb_color_to_linear(250, 198, 104);\n\n    let directional: AnalyticalLight<DirectionalLight> = stage\n        .new_directional_light()\n        .with_direction(Vec3::new(-0.5, -0.5, 0.0))\n        .with_color(sunset_amber_sunlight_color)\n        .with_intensity(Lux::OUTDOOR_OVERCAST_HIGH);\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame.read_image().await.unwrap();\n    img.save(\"lighting/directional.png\").unwrap();\n    frame.present();\n    // ANCHOR_END: directional\n\n    // ANCHOR: remove_directional\n    stage.remove_light(&directional);\n    drop(directional);\n    // ANCHOR_END: remove_directional\n\n    // ANCHOR: point\n    use renderling::light::{Candela, PointLight};\n\n    let point: AnalyticalLight<PointLight> = stage\n        .new_point_light()\n        .with_position({\n            let bust_aabb = model.bounding_volume().unwrap();\n            bust_aabb.max\n        })\n        .with_color(sunset_amber_sunlight_color)\n        .with_intensity(Candela(100.0));\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame.read_image().await.unwrap();\n    img.save(\"lighting/point.png\").unwrap();\n    frame.present();\n    // ANCHOR_END: point\n\n    // ANCHOR: remove_point\n    stage.remove_light(&point);\n    drop(point);\n    // ANCHOR_END: remove_point\n\n    // ANCHOR: spot\n    use renderling::light::SpotLight;\n\n    let camera_eye = Vec3::new(0.5, 0.5, 0.8);\n    let camera_target = Vec3::new(0.0, 0.3, 0.0);\n    let position = camera_eye;\n    let direction = camera_target - camera_eye;\n    let spot: AnalyticalLight<SpotLight> = stage\n        .new_spot_light()\n        .with_position(position)\n        .with_direction(direction)\n        // the cutoff values determine the angle of the cone\n        .with_inner_cutoff(0.15)\n        .with_outer_cutoff(0.2)\n        .with_color(sunset_amber_sunlight_color)\n        .with_intensity(Candela(12_000.0));\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame.read_image().await.unwrap();\n    img.save(\"lighting/spot.png\").unwrap();\n    frame.present();\n    // ANCHOR_END: spot\n\n    // ANCHOR: remove_spot\n    stage.remove_light(&spot);\n    drop(spot);\n    // ANCHOR_END: remove_spot\n}\n\n#[tokio::test]\nasync fn manual_lighting_ibl() {\n    let _ = env_logger::builder().try_init();\n\n    cwd_to_manual_assets_dir();\n\n    // ANCHOR: ibl_setup\n    use renderling::{\n        camera::Camera,\n        context::Context,\n        glam::{Mat4, Vec3, Vec4},\n        gltf::GltfDocument,\n        stage::Stage,\n        types::GpuOnlyArray,\n    };\n\n    let ctx = Context::headless(512, 512).await;\n    let stage: Stage = ctx\n        .new_stage()\n        .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0));\n\n    let _camera: Camera = {\n        let aspect = 1.0;\n        let fovy = core::f32::consts::PI / 4.0;\n        let znear = 0.1;\n        let zfar = 10.0;\n        let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);\n        let eye = Vec3::new(0.5, 0.5, 0.8);\n        let target = Vec3::new(0.0, 0.3, 0.0);\n        let up = Vec3::Y;\n        let view = Mat4::look_at_rh(eye, target, up);\n\n        stage\n            .new_camera()\n            .with_projection_and_view(projection, view)\n    };\n\n    let model: GltfDocument<GpuOnlyArray> = stage\n        .load_gltf_document_from_path(workspace_dir().join(\"gltf/marble_bust_1k.glb\"))\n        .unwrap()\n        .into_gpu_only();\n\n    let skybox = stage\n        .new_skybox_from_path(workspace_dir().join(\"img/hdr/helipad.hdr\"))\n        .unwrap();\n    stage.use_skybox(&skybox);\n    // ANCHOR_END: ibl_setup\n\n    // ANCHOR: ibl\n    use renderling::pbr::ibl::Ibl;\n\n    let ibl: Ibl = stage.new_ibl(&skybox);\n    stage.use_ibl(&ibl);\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame.read_image().await.unwrap();\n    img.save(\"lighting/ibl.png\").unwrap();\n    frame.present();\n    // ANCHOR_END: ibl\n\n    // ANCHOR: mix\n    use renderling::{color::css_srgb_color_to_linear, light::Candela};\n\n    let sunset_amber_sunlight_color = css_srgb_color_to_linear(250, 198, 104);\n    let _point = stage\n        .new_point_light()\n        .with_position({\n            let bust_aabb = model.bounding_volume().unwrap();\n            bust_aabb.max\n        })\n        .with_color(sunset_amber_sunlight_color)\n        .with_intensity(Candela(100.0));\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame.read_image().await.unwrap();\n    img.save(\"lighting/ibl-analytical-mixed.png\").unwrap();\n    frame.present();\n    // ANCHOR_END: mix\n}\n"
  },
  {
    "path": "crates/examples/src/skybox.rs",
    "content": "//! Skybox manual page.\n\nuse crate::{cwd_to_manual_assets_dir, workspace_dir};\n\n#[tokio::test]\npub async fn manual_skybox() {\n    // ANCHOR: setup\n    use renderling::{\n        camera::Camera,\n        context::Context,\n        glam::{Mat4, Vec3, Vec4},\n        stage::Stage,\n    };\n\n    let ctx = Context::headless(256, 256).await;\n    let stage: Stage = ctx\n        .new_stage()\n        .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0))\n        .with_lighting(false);\n\n    let _camera: Camera = {\n        let aspect = 1.0;\n        let fovy = core::f32::consts::PI / 4.0;\n        let znear = 0.1;\n        let zfar = 10.0;\n        let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);\n        let eye = Vec3::new(0.5, 0.5, 0.8);\n        let target = Vec3::new(0.0, 0.3, 0.0);\n        let up = Vec3::Y;\n        let view = Mat4::look_at_rh(eye, target, up);\n\n        stage\n            .new_camera()\n            .with_projection_and_view(projection, view)\n    };\n\n    use renderling::{gltf::GltfDocument, types::GpuOnlyArray};\n    let model: GltfDocument<GpuOnlyArray> = stage\n        .load_gltf_document_from_path(workspace_dir().join(\"gltf/marble_bust_1k.glb\"))\n        .unwrap()\n        .into_gpu_only();\n    println!(\"bounds: {:?}\", model.bounding_volume());\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    frame.present();\n    // ANCHOR_END: setup\n\n    // ANCHOR: skybox\n    let skybox = stage\n        .new_skybox_from_path(workspace_dir().join(\"img/hdr/helipad.hdr\"))\n        .unwrap();\n    stage.use_skybox(&skybox);\n    // ANCHOR_END: skybox\n\n    cwd_to_manual_assets_dir();\n\n    // ANCHOR: render_skybox\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let image = frame.read_image().await.unwrap();\n    image.save(\"skybox.png\").unwrap();\n    frame.present();\n    // ANCHOR_END: render_skybox\n}\n"
  },
  {
    "path": "crates/examples/src/stage.rs",
    "content": "//! Stage manual page.\n\n#[tokio::test]\nasync fn manual_stage() {\n    let _ = env_logger::builder().try_init();\n\n    // ANCHOR: creation\n    use renderling::{context::Context, glam::Vec4, stage::Stage};\n\n    let ctx = Context::headless(256, 256).await;\n    let stage: Stage = ctx\n        .new_stage()\n        .with_background_color(Vec4::new(0.5, 0.5, 0.5, 1.0));\n    // ANCHOR_END: creation\n\n    // ANCHOR: camera\n    use renderling::{\n        camera::Camera,\n        glam::{Mat4, Vec3},\n    };\n\n    let camera: Camera = stage\n        .new_camera()\n        .with_default_perspective(256.0, 256.0)\n        .with_view(Mat4::look_at_rh(Vec3::splat(1.5), Vec3::ZERO, Vec3::Y));\n    // This is technically not necessary because Stage always \"uses\" the first\n    // camera created, but we do it here for demonstration.\n    stage.use_camera(&camera);\n    // ANCHOR_END: camera\n\n    // ANCHOR: unit_cube_vertices\n    use renderling::geometry::{Vertex, Vertices};\n\n    let vertices: Vertices = stage.new_vertices(renderling::math::unit_cube().into_iter().map(\n        |(position, normal)| {\n            Vertex::default()\n                .with_position(position)\n                .with_normal(normal)\n                .with_color({\n                    // The color can vary from vertex to vertex\n                    //\n                    // X axis is green\n                    let g: f32 = position.x + 0.5;\n                    // Y axis is blue\n                    let b: f32 = position.y + 0.5;\n                    // Z is red\n                    let r: f32 = position.z + 0.5;\n                    Vec4::new(r, g, b, 1.0)\n                })\n        },\n    ));\n    // ANCHOR_END: unit_cube_vertices\n\n    // ANCHOR: unload_vertices\n    use renderling::types::GpuOnlyArray;\n\n    let vertices: Vertices<GpuOnlyArray> = vertices.into_gpu_only();\n    // ANCHOR_END: unload_vertices\n\n    // ANCHOR: material\n    let material = stage\n        .new_material()\n        .with_albedo_factor(Vec4::ONE)\n        .with_has_lighting(false);\n    // ANCHOR_END: material\n\n    // ANCHOR: prim\n    let prim = stage\n        .new_primitive()\n        .with_vertices(&vertices)\n        .with_material(&material);\n    // ANCHOR_END: prim\n\n    // Excluded from the manual because it's off-topic\n    let current_dir =\n        std::path::PathBuf::from(std::env!(\"CARGO_WORKSPACE_DIR\")).join(\"manual/src/assets\");\n    let current_dir = current_dir.canonicalize().unwrap();\n    std::env::set_current_dir(current_dir).unwrap();\n\n    // ANCHOR: render\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n\n    let img = frame.read_image().await.unwrap();\n    img.save(\"stage-example.png\").unwrap();\n    frame.present();\n    // ANCHOR_END: render\n\n    // ANCHOR: committed_size_bytes\n    let bytes_committed = stage.used_gpu_buffer_byte_size();\n    println!(\"bytes_committed: {bytes_committed}\");\n    // ANCHOR_END: committed_size_bytes\n\n    // ANCHOR: removal\n    let staged_prim_count = stage.remove_primitive(&prim);\n    assert_eq!(0, staged_prim_count);\n    drop(vertices);\n    drop(material);\n    drop(prim);\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame.read_image().await.unwrap();\n    img.save(\"stage-example-gone.png\").unwrap();\n    frame.present();\n    // ANCHOR_END: removal\n}\n"
  },
  {
    "path": "crates/img-diff/Cargo.toml",
    "content": "[package]\nname = \"img-diff\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nglam = { workspace = true, features = [\"std\"] }\nimage.workspace = true\nlog.workspace = true\nrenderling_build = { path = \"../renderling-build\" }\nsnafu = \"^0.7\"\n"
  },
  {
    "path": "crates/img-diff/src/lib.rs",
    "content": "//! Provides image diffing for testing.\nuse glam::{Vec3, Vec4, Vec4Swizzles};\nuse image::{DynamicImage, Luma, Rgb, Rgb32FImage, Rgba32FImage};\nuse renderling_build::{test_img_dir, test_output_dir};\nuse snafu::prelude::*;\n\nconst PIXEL_MAGNITUDE_THRESHOLD: f32 = 0.1;\npub const LOW_PIXEL_THRESHOLD: f32 = 0.02;\nconst IMAGE_DIFF_THRESHOLD: f32 = 0.05;\n\nfn checkerboard_background_color(x: u32, y: u32) -> Vec3 {\n    let size = 16;\n    let x_square_index = x / size;\n    let x_grey = x_square_index.is_multiple_of(2);\n    let y_square_index = y / size;\n    let y_grey = y_square_index.is_multiple_of(2);\n    if (x_grey && y_grey) || (!x_grey && !y_grey) {\n        Vec3::from([0.5, 0.5, 0.5])\n    } else {\n        Vec3::from([1.0, 1.0, 1.0])\n    }\n}\n\n#[derive(Debug, Snafu)]\nenum ImgDiffError {\n    #[snafu(display(\"Images are different sizes. Expected {lhs:?}, saw {rhs:?}\"))]\n    ImageSize { lhs: (u32, u32), rhs: (u32, u32) },\n}\n\npub struct DiffCfg {\n    /// The threshold for a pixel to be considered different.\n    ///\n    /// Difference is measured as the magnitude of vector subtraction\n    /// between the two pixels.\n    pub pixel_threshold: f32,\n    /// The percentage of \"different\" pixels (as determined using\n    /// `pixel_threshold`) to \"correct\" pixels that the image must contain\n    /// before it is considered an error.\n    pub image_threshold: f32,\n    /// The name of the test.\n    pub test_name: Option<&'static str>,\n    /// The output directory to store comparisons in.\n    pub output_dir: std::path::PathBuf,\n}\n\nimpl Default for DiffCfg {\n    fn default() -> Self {\n        Self {\n            pixel_threshold: PIXEL_MAGNITUDE_THRESHOLD,\n            image_threshold: IMAGE_DIFF_THRESHOLD,\n            test_name: None,\n            output_dir: test_output_dir(),\n        }\n    }\n}\n\npub struct DiffResults {\n    num_pixels: usize,\n    diff_image: Rgb32FImage,\n    mask_image: DynamicImage,\n    max_delta_length: f32,\n    avg_delta_length: f32,\n}\n\nfn get_results(\n    left_image: &Rgba32FImage,\n    right_image: &Rgba32FImage,\n    threshold: f32,\n) -> Result<Option<DiffResults>, ImgDiffError> {\n    let lid @ (width, height) = left_image.dimensions();\n    let rid = right_image.dimensions();\n    snafu::ensure!(lid == rid, ImageSizeSnafu { lhs: lid, rhs: rid });\n\n    let results = left_image\n        .enumerate_pixels()\n        .flat_map(|(x, y, left_pixel)| {\n            let right_pixel = right_image.get_pixel(x, y);\n            if left_pixel == right_pixel {\n                None\n            } else {\n                // pre-multiply alpha\n                let left_pixel = Vec4::from(left_pixel.0);\n                let left_pixel = (left_pixel * left_pixel.w).xyz();\n                let right_pixel = Vec4::from(right_pixel.0);\n                let right_pixel = (right_pixel * right_pixel.w).xyz();\n                let delta = (left_pixel - right_pixel).abs();\n                if delta.length() > threshold {\n                    Some((x, y, delta))\n                } else {\n                    None\n                }\n            }\n        })\n        .collect::<Vec<_>>();\n\n    let mut max_delta_length: f32 = 0.0;\n    let mut sum_delta_length: f32 = 0.0;\n    let diffs: usize = results.len();\n    if diffs == 0 {\n        Ok(None)\n    } else {\n        let mut mask_image = image::ImageBuffer::from_pixel(width, height, Luma([255u8]));\n        let mut output_image = image::ImageBuffer::from_pixel(width, height, Rgb([0.0, 0.0, 0.0]));\n\n        for x in 0..width {\n            for y in 0..height {\n                output_image.put_pixel(x, y, Rgb(checkerboard_background_color(x, y).into()));\n            }\n        }\n\n        for (x, y, delta) in results {\n            let length = delta.length();\n            sum_delta_length += length;\n            max_delta_length = length.max(max_delta_length);\n            mask_image.put_pixel(x, y, Luma([0]));\n            output_image.put_pixel(x, y, Rgb(delta.into()));\n        }\n        Ok(Some(DiffResults {\n            num_pixels: diffs,\n            diff_image: output_image,\n            mask_image: mask_image.into(),\n            max_delta_length,\n            avg_delta_length: sum_delta_length / diffs as f32,\n        }))\n    }\n}\n\npub fn save_to(\n    dir: impl AsRef<std::path::Path>,\n    filename: impl AsRef<std::path::Path>,\n    seen: impl Into<DynamicImage>,\n) -> Result<(), String> {\n    let path = dir.as_ref().join(filename);\n    std::fs::create_dir_all(path.parent().unwrap()).unwrap();\n    let img: DynamicImage = seen.into();\n    let img_buffer = img.into_rgba8();\n    let img = DynamicImage::from(img_buffer);\n    img.save(path).map_err(|e| e.to_string())\n}\n\npub fn save(filename: impl AsRef<std::path::Path>, seen: impl Into<DynamicImage>) {\n    save_to(test_output_dir(), filename, seen).unwrap()\n}\n\npub fn assert_eq_cfg(\n    filename: &str,\n    lhs: impl Into<DynamicImage>,\n    rhs: impl Into<DynamicImage>,\n    cfg: DiffCfg,\n) -> Result<(), String> {\n    let lhs = lhs.into();\n    let lhs = lhs.into_rgba32f();\n    let rhs = rhs.into().into_rgba32f();\n    let DiffCfg {\n        pixel_threshold,\n        image_threshold,\n        test_name,\n        output_dir,\n    } = cfg;\n    let results = match get_results(&lhs, &rhs, pixel_threshold) {\n        Ok(maybe_diff) => maybe_diff,\n        Err(e) => return Err(format!(\"Asserting {filename} failed: {e}\")),\n    };\n    if let Some(DiffResults {\n        num_pixels: diffs,\n        diff_image,\n        mask_image,\n        max_delta_length,\n        avg_delta_length,\n    }) = results\n    {\n        println!(\"{filename} has {diffs} pixel differences (threshold={pixel_threshold})\");\n        println!(\"  max_delta_length: {max_delta_length}\");\n        println!(\n            \"  avg_delta_length: {avg_delta_length} (average of deltas of pixels past the \\\n             threshold)\"\n        );\n        let percent_diff = diffs as f32 / (lhs.width() * lhs.height()) as f32;\n        println!(\"{filename}'s image is {percent_diff} different (threshold={image_threshold})\");\n        if percent_diff < image_threshold {\n            return Ok(());\n        }\n\n        let mut dir = output_dir.join(test_name.unwrap_or(filename));\n        dir.set_extension(\"\");\n        std::fs::create_dir_all(&dir).expect(\"cannot create test output dir\");\n        let expected = dir.join(\"expected.png\");\n        let seen = dir.join(\"seen.png\");\n        let diff = dir.join(\"diff.png\");\n        let mask = dir.join(\"mask.png\");\n        let lhs = DynamicImage::from(lhs).into_rgba8();\n        let rhs = DynamicImage::from(rhs).into_rgba8();\n        lhs.save_with_format(&expected, image::ImageFormat::Png)\n            .expect(\"can't save expected\");\n        rhs.save_with_format(&seen, image::ImageFormat::Png)\n            .expect(\"can't save seen\");\n        let diff_image = DynamicImage::from(diff_image).into_rgba8();\n        diff_image\n            .save_with_format(&diff, image::ImageFormat::Png)\n            .expect(\"can't save diff\");\n        let mask_image = mask_image.into_rgba8();\n        mask_image\n            .save_with_format(&mask, image::ImageFormat::Png)\n            .expect(\"can't save diff mask\");\n        Err(format!(\n            \"{} has >= {} differences above the threshold\\nexpected: {}\\nseen: {}\\ndiff: {}\",\n            filename,\n            diffs,\n            expected.display(),\n            seen.display(),\n            diff.display()\n        ))\n    } else {\n        Ok(())\n    }\n}\n\npub fn assert_eq(filename: &str, lhs: impl Into<DynamicImage>, rhs: impl Into<DynamicImage>) {\n    assert_eq_cfg(filename, lhs, rhs, DiffCfg::default()).unwrap()\n}\n\npub fn assert_img_eq_cfg_result(\n    filename: &str,\n    seen: impl Into<DynamicImage>,\n    cfg: DiffCfg,\n) -> Result<(), String> {\n    let path = test_img_dir().join(filename);\n    let lhs = image::open(&path)\n        .unwrap_or_else(|e| panic!(\"can't open expected image '{}': {e}\", path.display(),));\n    assert_eq_cfg(filename, lhs, seen, cfg)\n}\n\npub fn assert_img_eq_cfg(filename: &str, seen: impl Into<DynamicImage>, cfg: DiffCfg) {\n    assert_img_eq_cfg_result(filename, seen, cfg).unwrap()\n}\n\npub fn assert_img_eq(filename: &str, seen: impl Into<DynamicImage>) {\n    assert_img_eq_cfg(filename, seen, DiffCfg::default())\n}\n\n/// Normalize the depth image to make it easier to see.\n///\n/// ## Warning\n/// This is only normalization, not linearization.\npub fn normalize_gray_img(seen: &mut image::GrayImage) {\n    let mut max = 0u8;\n    let mut min = u8::MAX;\n    seen.pixels().for_each(|Luma([c])| {\n        max = max.max(*c);\n        min = min.min(*c);\n    });\n    let total = (max - min) as f32;\n    seen.pixels_mut().for_each(|c| {\n        let comps = c.0.map(|u| {\n            let percent = (u as f32 - min as f32) / total;\n            let float = percent * 255.0;\n            float as u8\n        });\n        c.0 = comps;\n    });\n    log::info!(\"normalize_gray_img-min: {min}\");\n    log::info!(\"normalize_gray_img-max: {max}\");\n}\n\n#[cfg(test)]\nmod test {\n    use crate::assert_img_eq;\n\n    #[test]\n    fn can_compare_images_sanity() {\n        let img = image::open(\"../../test_img/jolt.png\").unwrap();\n        assert_img_eq(\"jolt.png\", img);\n    }\n}\n"
  },
  {
    "path": "crates/loading-bytes/Cargo.toml",
    "content": "[package]\nname = \"loading-bytes\"\nversion = \"0.1.1\"\nedition = \"2021\"\ndescription = \"Load bytes from paths on native and WASM\"\nrepository = \"https://github.com/schell/renderling\"\nlicense = \"MIT OR Apache-2.0\"\nkeywords = [\"image\", \"font\", \"binary\", \"loading\", \"bytes\"]\ncategories = [\"webassembly\", \"filesystem\", \"asynchronous\"]\nreadme = \"README.md\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nasync-fs = \"1.6\"\njs-sys = \"0.3\"\nsend_wrapper = {workspace=true}\nserde.workspace = true\nserde_json.workspace = true\nsnafu = {workspace = true}\nwasm-bindgen = {workspace = true}\nwasm-bindgen-futures = {workspace = true}\nweb-sys = { workspace = true, features = [\"Request\", \"RequestInit\", \"Response\", \"Window\"] }\n"
  },
  {
    "path": "crates/loading-bytes/README.md",
    "content": "# loading-bytes\n\nSometimes loading things really bites. \n\nLet's say you just want to compile your program on native and web, but you've \ndone a bit of loading in your call tree... \n\nYou know what I'm talking about, I mean reading from the filesystem. Easy stuff - right? \n\n## WRONG!\n\nWell, not really - it's still pretty easy - but it's a hassle, hoff!\n\n## NOT ANYMORE.\n\nThat's right folks! Step right up and use this here little library to load things from \nthe filesystem on native and through a web request on WASM! \n\nWhat you get back totally bytes!\n\n## BUT WAIT, THERE'S MORE!\n\nAct now and I'll throw in some \"nice\" enumerated error handling.\n\nOK. You got me. That's free!\n\nIn fact, the whole thing is free! Take it or leave it, folks, the choice is yours.\n\n...\n\n`loading-bytes` - from the same company that brought you `streaming-nibbles`🐟... \n\n...Just kidding! \n\nHappy hacking :) ☕☕☕\n"
  },
  {
    "path": "crates/loading-bytes/src/lib.rs",
    "content": "//! Abstraction over loading bytes on WASM or other.\nuse snafu::prelude::*;\nuse wasm_bindgen::UnwrapThrowExt;\n\n#[derive(Debug, Snafu)]\npub enum WasmError {\n    #[snafu(display(\"Could not create request to load '{path}': {msg:#?}\"))]\n    CreateRequest {\n        path: String,\n        msg: send_wrapper::SendWrapper<wasm_bindgen::JsValue>,\n    },\n\n    #[snafu(display(\"Fetch failed to load '{path}' by WASM error: {msg:#?}\"))]\n    Fetch {\n        path: String,\n        msg: send_wrapper::SendWrapper<wasm_bindgen::JsValue>,\n    },\n\n    #[snafu(display(\"Fetching '{path}' returned something that was not a Response: {msg:#?}\"))]\n    NotAResponse {\n        path: String,\n        msg: send_wrapper::SendWrapper<wasm_bindgen::JsValue>,\n    },\n\n    #[snafu(display(\"Could not get response array buffer '{path}': {msg:#?}\"))]\n    Array {\n        path: String,\n        msg: send_wrapper::SendWrapper<wasm_bindgen::JsValue>,\n    },\n\n    #[snafu(display(\"Could not get buffer from array '{path}': {msg:#?}\"))]\n    Buffer {\n        path: String,\n        msg: send_wrapper::SendWrapper<wasm_bindgen::JsValue>,\n    },\n\n    #[snafu(display(\"{other}\"))]\n    Other { other: String },\n}\n\n/// Enumeration of all errors this library may result in.\n#[derive(Debug, Snafu)]\npub enum LoadingBytesError {\n    #[snafu(display(\"{source}\"))]\n    Wasm { source: WasmError },\n\n    #[snafu(display(\"loading '{path}' by filesystem from CWD '{}' error: {source}\", cwd.display()))]\n    Fs {\n        path: String,\n        cwd: std::path::PathBuf,\n        source: std::io::Error,\n    },\n}\n\nimpl From<WasmError> for LoadingBytesError {\n    fn from(value: WasmError) -> Self {\n        LoadingBytesError::Wasm { source: value }\n    }\n}\n\npub async fn load_wasm(path: &str) -> Result<Vec<u8>, WasmError> {\n    use wasm_bindgen::JsCast;\n\n    let path = path.to_string();\n    let opts = web_sys::RequestInit::new();\n    opts.set_method(\"GET\");\n    let request = web_sys::Request::new_with_str_and_init(&path, &opts).map_err(|msg| {\n        CreateRequestSnafu {\n            path: path.clone(),\n            msg: send_wrapper::SendWrapper::new(msg),\n        }\n        .build()\n    })?;\n    let window = web_sys::window().unwrap();\n    let resp_value = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request))\n        .await\n        .map_err(|msg| {\n            FetchSnafu {\n                path: path.clone(),\n                msg: send_wrapper::SendWrapper::new(msg),\n            }\n            .build()\n        })?;\n    let resp: web_sys::Response = resp_value.dyn_into().map_err(|msg| {\n        NotAResponseSnafu {\n            path: path.clone(),\n            msg: send_wrapper::SendWrapper::new(msg),\n        }\n        .build()\n    })?;\n    let array_promise = resp.array_buffer().map_err(|msg| {\n        ArraySnafu {\n            path: path.clone(),\n            msg: send_wrapper::SendWrapper::new(msg),\n        }\n        .build()\n    })?;\n    let buffer = wasm_bindgen_futures::JsFuture::from(array_promise)\n        .await\n        .map_err(|msg| {\n            BufferSnafu {\n                path: path.clone(),\n                msg: send_wrapper::SendWrapper::new(msg),\n            }\n            .build()\n        })?;\n    assert!(buffer.is_instance_of::<js_sys::ArrayBuffer>());\n    let array: js_sys::Uint8Array = js_sys::Uint8Array::new(&buffer);\n    let mut bytes: Vec<u8> = vec![0; array.length() as usize];\n    array.copy_to(&mut bytes);\n    Ok(bytes)\n}\n\npub async fn post_json_wasm<T: serde::de::DeserializeOwned>(\n    path: &str,\n    data: &str,\n) -> Result<T, WasmError> {\n    use js_sys::JsString;\n    use wasm_bindgen::JsCast;\n\n    let path = path.to_string();\n    let opts = web_sys::RequestInit::new();\n    opts.set_method(\"POST\");\n    let headers = js_sys::Object::new();\n    js_sys::Reflect::set(\n        &headers,\n        &JsString::from(\"content-type\"),\n        &JsString::from(\"application/json\"),\n    )\n    .unwrap();\n    opts.set_headers(&headers);\n    let body = js_sys::JsString::from(data);\n    opts.set_body(&body.into());\n    let request = web_sys::Request::new_with_str_and_init(&path, &opts).map_err(|msg| {\n        CreateRequestSnafu {\n            path: path.clone(),\n            msg: send_wrapper::SendWrapper::new(msg),\n        }\n        .build()\n    })?;\n    let window = web_sys::window().unwrap();\n    let resp_value = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request))\n        .await\n        .map_err(|msg| {\n            FetchSnafu {\n                path: path.clone(),\n                msg: send_wrapper::SendWrapper::new(msg),\n            }\n            .build()\n        })?;\n    let resp: web_sys::Response = resp_value.dyn_into().map_err(|msg| {\n        NotAResponseSnafu {\n            path: path.clone(),\n            msg: send_wrapper::SendWrapper::new(msg),\n        }\n        .build()\n    })?;\n\n    snafu::ensure!(\n        resp.ok(),\n        OtherSnafu {\n            other: wasm_bindgen_futures::JsFuture::from(resp.text().unwrap())\n                .await\n                .unwrap()\n                .as_string()\n                .unwrap()\n        }\n    );\n\n    let value = wasm_bindgen_futures::JsFuture::from(resp.text().unwrap_throw())\n        .await\n        .unwrap_throw();\n    let s = value.as_string().expect_throw(&format!(\"{value:#?}\"));\n    let t = serde_json::from_str::<T>(&s).unwrap_throw();\n    Ok(t)\n}\n\n// TODO: deduplicate post_bin_wasm and post_json_wasm\npub async fn post_bin_wasm<T: serde::de::DeserializeOwned>(\n    path: &str,\n    data: &[u8],\n) -> Result<T, WasmError> {\n    use js_sys::JsString;\n    use wasm_bindgen::JsCast;\n\n    let path = path.to_string();\n    let opts = web_sys::RequestInit::new();\n    opts.set_method(\"POST\");\n    let headers = js_sys::Object::new();\n    js_sys::Reflect::set(\n        &headers,\n        &JsString::from(\"content-type\"),\n        &JsString::from(\"application/octet-stream\"),\n    )\n    .unwrap();\n    opts.set_headers(&headers);\n    let body = js_sys::Uint8Array::from(data);\n    opts.set_body(&body.into());\n    let request = web_sys::Request::new_with_str_and_init(&path, &opts).map_err(|msg| {\n        CreateRequestSnafu {\n            path: path.clone(),\n            msg: send_wrapper::SendWrapper::new(msg),\n        }\n        .build()\n    })?;\n    let window = web_sys::window().unwrap();\n    let resp_value = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request))\n        .await\n        .map_err(|msg| {\n            FetchSnafu {\n                path: path.clone(),\n                msg: send_wrapper::SendWrapper::new(msg),\n            }\n            .build()\n        })?;\n    let resp: web_sys::Response = resp_value.dyn_into().map_err(|msg| {\n        NotAResponseSnafu {\n            path: path.clone(),\n            msg: send_wrapper::SendWrapper::new(msg),\n        }\n        .build()\n    })?;\n\n    snafu::ensure!(\n        resp.ok(),\n        OtherSnafu {\n            other: wasm_bindgen_futures::JsFuture::from(resp.text().unwrap())\n                .await\n                .unwrap()\n                .as_string()\n                .unwrap()\n        }\n    );\n\n    let value = wasm_bindgen_futures::JsFuture::from(resp.text().unwrap_throw())\n        .await\n        .unwrap_throw();\n    let s = value.as_string().expect_throw(&format!(\"{value:#?}\"));\n    let t = serde_json::from_str::<T>(&s).unwrap_throw();\n    Ok(t)\n}\n\n/// Load the file at the given url fragment or path and return it as a vector of\n/// bytes, if possible.\npub async fn load(path: &str) -> Result<Vec<u8>, LoadingBytesError> {\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        let bytes = load_wasm(path).await?;\n        Ok(bytes)\n    }\n    #[cfg(not(target_arch = \"wasm32\"))]\n    {\n        let bytes: Vec<u8> = async_fs::read(path).await.with_context(|_| FsSnafu {\n            path: path.to_string(),\n            cwd: std::env::current_dir().unwrap(),\n        })?;\n        Ok(bytes)\n    }\n}\n"
  },
  {
    "path": "crates/renderling/Cargo.toml",
    "content": "[package]\nname = \"renderling\"\nversion = \"0.6.0\"\nedition = \"2021\"\ndescription = \"User-friendly real-time rendering. 🍖\"\nrepository = \"https://github.com/schell/renderling\"\nlicense = \"MIT OR Apache-2.0\"\nkeywords = [\"game\", \"graphics\", \"shader\", \"rendering\"]\ncategories = [\"rendering\", \"game-development\", \"graphics\"]\nreadme = \"../../README.md\"\nbuild = \"src/build.rs\"\n\n[package.metadata.rust-gpu.install]\nauto-install-rust-toolchain = true\n\n[package.metadata.rust-gpu.build]\noutput-dir = \"shaders\" \nmultimodule = true\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[lib]\ncrate-type = [\"rlib\", \"cdylib\"]\n\n[features]\ndefault = [\"gltf\", \"winit\"]\ngltf = [\"dep:gltf\", \"dep:serde_json\"]\ntest_i8_i16_extraction = []\ntest-utils = [\"dep:metal\", \"dep:wgpu-core\", \"dep:futures-lite\"]\nwasm = [\"wgpu/fragile-send-sync-non-atomic-wasm\"]\ndebug-slab = []\nlight-tiling-stats = [ \"dep:plotters\" ]\n\n[build-dependencies]\ncfg_aliases.workspace = true\nnaga.workspace = true\npathdiff = \"0.2.2\"\nquote.workspace = true\nrenderling_build = { path = \"../renderling-build\", version = \"0.1.0\" }\nserde.workspace = true\nserde_json.workspace = true\nsimilarity.workspace = true\n\n# dependencies for CPU and GPU code\n[dependencies]\nspirv-std.workspace = true\n\n# dependencies for GPU code\n[target.'cfg(target_arch = \"spirv\")'.dependencies]\ncrabslab = { workspace = true, features = [\"glam\"]  }\nglam = { workspace = true, default-features = false, features = [\"libm\"] }\n\n# dependencies for CPU code\n[target.'cfg(not(target_arch = \"spirv\"))'.dependencies]\nasync-channel = {workspace = true}\nbytemuck = {workspace = true}\ncraballoc.workspace = true\ncrabslab = { workspace = true, features = [\"default\"] }\ncrunch = \"0.5\"\ndagga = {workspace=true}\nfutures-lite = { workspace = true, optional = true }\nglam = { workspace = true, features = [\"std\"] }\ngltf = {workspace = true, optional = true}\nhalf = \"2.3\"\nimage = {workspace = true, features = [\"hdr\"]}\nlog = {workspace = true}\nplotters = { workspace = true, optional = true }\npretty_assertions.workspace = true\nrustc-hash = {workspace = true}\nserde_json = {workspace = true, optional = true}\nsnafu = {workspace = true}\nwgpu = { workspace = true, features = [\"spirv\"] }\nwinit = { workspace = true, optional = true }\n\n# dependencies for WASM CPU code\n[target.'cfg(target_arch = \"wasm32\")'.dependencies]\nwasm-bindgen.workspace = true\n\n[dev-dependencies]\nassert_approx_eq.workspace = true\nconsole_log.workspace = true\nctor = \"0.2.2\"\nenv_logger.workspace = true\nexample = { path = \"../example\" }\nfastrand = \"2.1.1\"\nfutures-lite.workspace = true\nhuman-repr = \"1.1.0\"\nicosahedron = \"0.1\"\nimg-diff = { path = \"../img-diff\" }\nloading-bytes = { workspace = true }\nnaga.workspace = true\nrenderling_build = { path = \"../renderling-build\" }\nttf-parser = \"0.20.0\"\nwasm-bindgen-test.workspace = true\nwinit.workspace = true\nwire-types = { path = \"../wire-types\" }\n\n[target.'cfg(not(target_arch = \"spirv\"))'.dev-dependencies]\nglam = { workspace = true, features = [\"std\", \"debug-glam-assert\"] }\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\nmetal = { workspace = true, optional = true }\nwgpu-core = { workspace = true, optional = true }\n\n[target.'cfg(target_os = \"macos\")'.dev-dependencies]\nmetal.workspace = true\nwgpu-core.workspace = true\n\n[dev-dependencies.web-sys]\nworkspace = true\nfeatures = [\n  \"Navigator\",\n  \"Window\"\n]\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/renderling/shaders/manifest.json",
    "content": "[\n  {\n    \"source_path\": \"shaders/atlas-shader-atlas_blit_fragment.spv\",\n    \"entry_point\": \"atlas::shader::atlas_blit_fragment\",\n    \"wgsl_entry_point\": \"atlasshaderatlas_blit_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/atlas-shader-atlas_blit_vertex.spv\",\n    \"entry_point\": \"atlas::shader::atlas_blit_vertex\",\n    \"wgsl_entry_point\": \"atlasshaderatlas_blit_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/bloom-shader-bloom_downsample_fragment.spv\",\n    \"entry_point\": \"bloom::shader::bloom_downsample_fragment\",\n    \"wgsl_entry_point\": \"bloomshaderbloom_downsample_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/bloom-shader-bloom_mix_fragment.spv\",\n    \"entry_point\": \"bloom::shader::bloom_mix_fragment\",\n    \"wgsl_entry_point\": \"bloomshaderbloom_mix_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/bloom-shader-bloom_upsample_fragment.spv\",\n    \"entry_point\": \"bloom::shader::bloom_upsample_fragment\",\n    \"wgsl_entry_point\": \"bloomshaderbloom_upsample_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/bloom-shader-bloom_vertex.spv\",\n    \"entry_point\": \"bloom::shader::bloom_vertex\",\n    \"wgsl_entry_point\": \"bloomshaderbloom_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/compositor-compositor_fragment.spv\",\n    \"entry_point\": \"compositor::compositor_fragment\",\n    \"wgsl_entry_point\": \"compositorcompositor_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/compositor-compositor_vertex.spv\",\n    \"entry_point\": \"compositor::compositor_vertex\",\n    \"wgsl_entry_point\": \"compositorcompositor_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/convolution-shader-brdf_lut_convolution_fragment.spv\",\n    \"entry_point\": \"convolution::shader::brdf_lut_convolution_fragment\",\n    \"wgsl_entry_point\": \"convolutionshaderbrdf_lut_convolution_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/convolution-shader-brdf_lut_convolution_vertex.spv\",\n    \"entry_point\": \"convolution::shader::brdf_lut_convolution_vertex\",\n    \"wgsl_entry_point\": \"convolutionshaderbrdf_lut_convolution_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/convolution-shader-generate_mipmap_fragment.spv\",\n    \"entry_point\": \"convolution::shader::generate_mipmap_fragment\",\n    \"wgsl_entry_point\": \"convolutionshadergenerate_mipmap_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/convolution-shader-generate_mipmap_vertex.spv\",\n    \"entry_point\": \"convolution::shader::generate_mipmap_vertex\",\n    \"wgsl_entry_point\": \"convolutionshadergenerate_mipmap_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/convolution-shader-prefilter_environment_cubemap_fragment.spv\",\n    \"entry_point\": \"convolution::shader::prefilter_environment_cubemap_fragment\",\n    \"wgsl_entry_point\": \"convolutionshaderprefilter_environment_cubemap_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/convolution-shader-prefilter_environment_cubemap_vertex.spv\",\n    \"entry_point\": \"convolution::shader::prefilter_environment_cubemap_vertex\",\n    \"wgsl_entry_point\": \"convolutionshaderprefilter_environment_cubemap_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/cubemap-shader-cubemap_sampling_test_fragment.spv\",\n    \"entry_point\": \"cubemap::shader::cubemap_sampling_test_fragment\",\n    \"wgsl_entry_point\": \"cubemapshadercubemap_sampling_test_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/cubemap-shader-cubemap_sampling_test_vertex.spv\",\n    \"entry_point\": \"cubemap::shader::cubemap_sampling_test_vertex\",\n    \"wgsl_entry_point\": \"cubemapshadercubemap_sampling_test_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/cull-shader-compute_copy_depth_to_pyramid.spv\",\n    \"entry_point\": \"cull::shader::compute_copy_depth_to_pyramid\",\n    \"wgsl_entry_point\": \"cullshadercompute_copy_depth_to_pyramid\"\n  },\n  {\n    \"source_path\": \"shaders/cull-shader-compute_copy_depth_to_pyramid_multisampled.spv\",\n    \"entry_point\": \"cull::shader::compute_copy_depth_to_pyramid_multisampled\",\n    \"wgsl_entry_point\": \"cullshadercompute_copy_depth_to_pyramid_multisampled\"\n  },\n  {\n    \"source_path\": \"shaders/cull-shader-compute_culling.spv\",\n    \"entry_point\": \"cull::shader::compute_culling\",\n    \"wgsl_entry_point\": \"cullshadercompute_culling\"\n  },\n  {\n    \"source_path\": \"shaders/cull-shader-compute_downsample_depth_pyramid.spv\",\n    \"entry_point\": \"cull::shader::compute_downsample_depth_pyramid\",\n    \"wgsl_entry_point\": \"cullshadercompute_downsample_depth_pyramid\"\n  },\n  {\n    \"source_path\": \"shaders/debug-shader-debug_overlay_fragment.spv\",\n    \"entry_point\": \"debug::shader::debug_overlay_fragment\",\n    \"wgsl_entry_point\": \"debugshaderdebug_overlay_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/debug-shader-debug_overlay_vertex.spv\",\n    \"entry_point\": \"debug::shader::debug_overlay_vertex\",\n    \"wgsl_entry_point\": \"debugshaderdebug_overlay_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/light-shader-light_tiling_bin_lights.spv\",\n    \"entry_point\": \"light::shader::light_tiling_bin_lights\",\n    \"wgsl_entry_point\": \"lightshaderlight_tiling_bin_lights\"\n  },\n  {\n    \"source_path\": \"shaders/light-shader-light_tiling_clear_tiles.spv\",\n    \"entry_point\": \"light::shader::light_tiling_clear_tiles\",\n    \"wgsl_entry_point\": \"lightshaderlight_tiling_clear_tiles\"\n  },\n  {\n    \"source_path\": \"shaders/light-shader-light_tiling_compute_tile_min_and_max_depth.spv\",\n    \"entry_point\": \"light::shader::light_tiling_compute_tile_min_and_max_depth\",\n    \"wgsl_entry_point\": \"lightshaderlight_tiling_compute_tile_min_and_max_depth\"\n  },\n  {\n    \"source_path\": \"shaders/light-shader-light_tiling_compute_tile_min_and_max_depth_multisampled.spv\",\n    \"entry_point\": \"light::shader::light_tiling_compute_tile_min_and_max_depth_multisampled\",\n    \"wgsl_entry_point\": \"lightshaderlight_tiling_compute_tile_min_and_max_depth_multisampled\"\n  },\n  {\n    \"source_path\": \"shaders/light-shader-light_tiling_depth_pre_pass.spv\",\n    \"entry_point\": \"light::shader::light_tiling_depth_pre_pass\",\n    \"wgsl_entry_point\": \"lightshaderlight_tiling_depth_pre_pass\"\n  },\n  {\n    \"source_path\": \"shaders/light-shader-shadow_mapping_fragment.spv\",\n    \"entry_point\": \"light::shader::shadow_mapping_fragment\",\n    \"wgsl_entry_point\": \"lightshadershadow_mapping_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/light-shader-shadow_mapping_vertex.spv\",\n    \"entry_point\": \"light::shader::shadow_mapping_vertex\",\n    \"wgsl_entry_point\": \"lightshadershadow_mapping_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/pbr-ibl-shader-di_convolution_fragment.spv\",\n    \"entry_point\": \"pbr::ibl::shader::di_convolution_fragment\",\n    \"wgsl_entry_point\": \"pbriblshaderdi_convolution_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/primitive-shader-primitive_fragment.spv\",\n    \"entry_point\": \"primitive::shader::primitive_fragment\",\n    \"wgsl_entry_point\": \"primitiveshaderprimitive_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/primitive-shader-primitive_vertex.spv\",\n    \"entry_point\": \"primitive::shader::primitive_vertex\",\n    \"wgsl_entry_point\": \"primitiveshaderprimitive_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/skybox-shader-skybox_cubemap_fragment.spv\",\n    \"entry_point\": \"skybox::shader::skybox_cubemap_fragment\",\n    \"wgsl_entry_point\": \"skyboxshaderskybox_cubemap_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/skybox-shader-skybox_cubemap_vertex.spv\",\n    \"entry_point\": \"skybox::shader::skybox_cubemap_vertex\",\n    \"wgsl_entry_point\": \"skyboxshaderskybox_cubemap_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/skybox-shader-skybox_equirectangular_fragment.spv\",\n    \"entry_point\": \"skybox::shader::skybox_equirectangular_fragment\",\n    \"wgsl_entry_point\": \"skyboxshaderskybox_equirectangular_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/skybox-shader-skybox_vertex.spv\",\n    \"entry_point\": \"skybox::shader::skybox_vertex\",\n    \"wgsl_entry_point\": \"skyboxshaderskybox_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/tonemapping-tonemapping_fragment.spv\",\n    \"entry_point\": \"tonemapping::tonemapping_fragment\",\n    \"wgsl_entry_point\": \"tonemappingtonemapping_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/tonemapping-tonemapping_vertex.spv\",\n    \"entry_point\": \"tonemapping::tonemapping_vertex\",\n    \"wgsl_entry_point\": \"tonemappingtonemapping_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/tutorial-implicit_isosceles_vertex.spv\",\n    \"entry_point\": \"tutorial::implicit_isosceles_vertex\",\n    \"wgsl_entry_point\": \"tutorialimplicit_isosceles_vertex\"\n  },\n  {\n    \"source_path\": \"shaders/tutorial-passthru_fragment.spv\",\n    \"entry_point\": \"tutorial::passthru_fragment\",\n    \"wgsl_entry_point\": \"tutorialpassthru_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/tutorial-slabbed_renderlet.spv\",\n    \"entry_point\": \"tutorial::slabbed_renderlet\",\n    \"wgsl_entry_point\": \"tutorialslabbed_renderlet\"\n  },\n  {\n    \"source_path\": \"shaders/tutorial-slabbed_vertices.spv\",\n    \"entry_point\": \"tutorial::slabbed_vertices\",\n    \"wgsl_entry_point\": \"tutorialslabbed_vertices\"\n  },\n  {\n    \"source_path\": \"shaders/tutorial-slabbed_vertices_no_instance.spv\",\n    \"entry_point\": \"tutorial::slabbed_vertices_no_instance\",\n    \"wgsl_entry_point\": \"tutorialslabbed_vertices_no_instance\"\n  },\n  {\n    \"source_path\": \"shaders/ui_slab-shader-ui_fragment.spv\",\n    \"entry_point\": \"ui_slab::shader::ui_fragment\",\n    \"wgsl_entry_point\": \"ui_slabshaderui_fragment\"\n  },\n  {\n    \"source_path\": \"shaders/ui_slab-shader-ui_vertex.spv\",\n    \"entry_point\": \"ui_slab::shader::ui_vertex\",\n    \"wgsl_entry_point\": \"ui_slabshaderui_vertex\"\n  }\n]"
  },
  {
    "path": "crates/renderling/src/atlas/atlas_image.rs",
    "content": "//! Images and texture formats.\n//!\n//! Used to represent textures before they are sent to the GPU.\nuse glam::UVec2;\nuse image::EncodableLayout;\nuse snafu::prelude::*;\n\nfn cwd() -> Option<String> {\n    #[cfg(target_arch = \"wasm32\")]\n    {\n        Some(\"localhost\".to_string())\n    }\n    #[cfg(not(target_arch = \"wasm32\"))]\n    {\n        let cwd = std::env::current_dir().ok()?;\n        Some(format!(\"{}\", cwd.display()))\n    }\n}\n\n#[derive(Debug, Snafu)]\npub enum AtlasImageError {\n    #[snafu(display(\"Cannot load image '{}' from cwd '{:?}': {source}\", path.display(), cwd()))]\n    CannotLoad {\n        source: std::io::Error,\n        path: std::path::PathBuf,\n    },\n\n    #[snafu(display(\"Image error: {source}\\nCurrent dir: {:?}\", cwd()))]\n    Image { source: image::error::ImageError },\n}\n\n#[derive(Clone, Copy, Debug)]\npub enum AtlasImageFormat {\n    R8,\n    R8G8,\n    R8G8B8,\n    R8G8B8A8,\n    R16,\n    R16G16,\n    R16G16B16,\n    R16G16B16A16,\n    R16G16B16A16FLOAT,\n    R32FLOAT,\n    R32G32B32FLOAT,\n    R32G32B32A32FLOAT,\n    D32FLOAT,\n}\n\nimpl From<AtlasImageFormat> for wgpu::TextureFormat {\n    fn from(value: AtlasImageFormat) -> Self {\n        match value {\n            AtlasImageFormat::R8 => wgpu::TextureFormat::R8Unorm,\n            AtlasImageFormat::R8G8 => wgpu::TextureFormat::Rg8Unorm,\n            AtlasImageFormat::R8G8B8 => wgpu::TextureFormat::Rgba8Unorm, /* No direct 3-channel */\n            // format, using\n            // 4-channel\n            AtlasImageFormat::R8G8B8A8 => wgpu::TextureFormat::Rgba8Unorm,\n            AtlasImageFormat::R16 => wgpu::TextureFormat::R16Unorm,\n            AtlasImageFormat::R16G16 => wgpu::TextureFormat::Rg16Unorm,\n            AtlasImageFormat::R16G16B16 => wgpu::TextureFormat::Rgba16Unorm, /* No direct 3-channel format, using 4-channel */\n            AtlasImageFormat::R16G16B16A16 => wgpu::TextureFormat::Rgba16Unorm,\n            AtlasImageFormat::R16G16B16A16FLOAT => wgpu::TextureFormat::Rgba16Float,\n            AtlasImageFormat::R32FLOAT => wgpu::TextureFormat::R32Float,\n            AtlasImageFormat::R32G32B32FLOAT => wgpu::TextureFormat::Rgba32Float, /* No direct 3-channel format, using 4-channel */\n            AtlasImageFormat::R32G32B32A32FLOAT => wgpu::TextureFormat::Rgba32Float,\n            AtlasImageFormat::D32FLOAT => wgpu::TextureFormat::Depth32Float,\n        }\n    }\n}\n\nimpl AtlasImageFormat {\n    pub fn from_wgpu_texture_format(value: wgpu::TextureFormat) -> Option<Self> {\n        match value {\n            wgpu::TextureFormat::R8Uint => Some(AtlasImageFormat::R8),\n            wgpu::TextureFormat::R16Uint => Some(AtlasImageFormat::R16),\n            wgpu::TextureFormat::R32Float => Some(AtlasImageFormat::R32FLOAT),\n            wgpu::TextureFormat::Rg8Uint => Some(AtlasImageFormat::R8G8),\n            wgpu::TextureFormat::Rg16Uint => Some(AtlasImageFormat::R16G16),\n            wgpu::TextureFormat::Rgba16Float => Some(AtlasImageFormat::R16G16B16A16FLOAT),\n            wgpu::TextureFormat::Depth32Float => Some(AtlasImageFormat::D32FLOAT),\n            _ => None,\n        }\n    }\n\n    pub fn zero_pixel(&self) -> &[u8] {\n        match self {\n            AtlasImageFormat::R8 => &[0],\n            AtlasImageFormat::R8G8 => &[0, 0],\n            AtlasImageFormat::R8G8B8 => &[0, 0, 0],\n            AtlasImageFormat::R8G8B8A8 => &[0, 0, 0, 0],\n            AtlasImageFormat::R16 => &[0, 0],\n            AtlasImageFormat::R16G16 => &[0, 0, 0, 0],\n            AtlasImageFormat::R16G16B16 => &[0, 0, 0, 0, 0, 0],\n            AtlasImageFormat::R16G16B16A16 => &[0, 0, 0, 0, 0, 0, 0, 0],\n            AtlasImageFormat::R16G16B16A16FLOAT => &[0, 0, 0, 0, 0, 0, 0, 0],\n            AtlasImageFormat::R32FLOAT | AtlasImageFormat::D32FLOAT => &[0, 0, 0, 0],\n            AtlasImageFormat::R32G32B32FLOAT => &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],\n            AtlasImageFormat::R32G32B32A32FLOAT => {\n                &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n            }\n        }\n    }\n}\n\n/// Image data in transit from CPU to GPU.\n#[derive(Clone, Debug)]\npub struct AtlasImage {\n    pub pixels: Vec<u8>,\n    pub size: UVec2,\n    pub format: AtlasImageFormat,\n    // Whether or not to convert from sRGB color space into linear color space.\n    pub apply_linear_transfer: bool,\n}\n\n#[cfg(feature = \"gltf\")]\nimpl From<gltf::image::Data> for AtlasImage {\n    fn from(value: gltf::image::Data) -> Self {\n        let pixels = value.pixels;\n        let size = UVec2::new(value.width, value.height);\n        let format = match value.format {\n            gltf::image::Format::R8 => AtlasImageFormat::R8,\n            gltf::image::Format::R8G8 => AtlasImageFormat::R8G8,\n            gltf::image::Format::R8G8B8 => AtlasImageFormat::R8G8B8,\n            gltf::image::Format::R8G8B8A8 => AtlasImageFormat::R8G8B8A8,\n            gltf::image::Format::R16 => AtlasImageFormat::R16,\n            gltf::image::Format::R16G16 => AtlasImageFormat::R16G16,\n            gltf::image::Format::R16G16B16 => AtlasImageFormat::R16G16B16,\n            gltf::image::Format::R16G16B16A16 => AtlasImageFormat::R16G16B16A16,\n            gltf::image::Format::R32G32B32FLOAT => AtlasImageFormat::R32G32B32FLOAT,\n            gltf::image::Format::R32G32B32A32FLOAT => AtlasImageFormat::R32G32B32A32FLOAT,\n        };\n\n        AtlasImage {\n            size,\n            pixels,\n            format,\n            // Determining this gets deferred until material construction\n            apply_linear_transfer: false,\n        }\n    }\n}\n\nimpl From<image::DynamicImage> for AtlasImage {\n    fn from(value: image::DynamicImage) -> Self {\n        let width = value.width();\n        let height = value.height();\n\n        use AtlasImageFormat::*;\n        let (pixels, format) = match value {\n            image::DynamicImage::ImageLuma8(img) => (img.into_vec(), R8),\n            i @ image::DynamicImage::ImageLumaA8(_) => (i.into_rgba8().into_vec(), R8G8B8A8),\n            image::DynamicImage::ImageRgb8(img) => (img.into_vec(), R8G8B8),\n            image::DynamicImage::ImageRgba8(img) => (img.into_vec(), R8G8B8A8),\n            image::DynamicImage::ImageLuma16(img) => (img.as_bytes().to_vec(), R16),\n            i @ image::DynamicImage::ImageLumaA16(_) => {\n                (i.into_rgba16().as_bytes().to_vec(), R16G16B16A16)\n            }\n            i @ image::DynamicImage::ImageRgb16(_) => (i.as_bytes().to_vec(), R16G16B16),\n            i @ image::DynamicImage::ImageRgba16(_) => (i.as_bytes().to_vec(), R16G16B16A16),\n            i @ image::DynamicImage::ImageRgb32F(_) => (i.as_bytes().to_vec(), R32G32B32FLOAT),\n            i @ image::DynamicImage::ImageRgba32F(_) => (i.as_bytes().to_vec(), R32G32B32A32FLOAT),\n            _ => todo!(),\n        };\n        AtlasImage {\n            pixels,\n            format,\n            // Most of the time when people are using `image` to load images, those images\n            // have color data that was authored in sRGB space.\n            apply_linear_transfer: true,\n            size: UVec2::new(width, height),\n        }\n    }\n}\n\nimpl TryFrom<std::path::PathBuf> for AtlasImage {\n    type Error = AtlasImageError;\n\n    fn try_from(value: std::path::PathBuf) -> Result<Self, Self::Error> {\n        let img = image::open(value).context(ImageSnafu)?;\n        Ok(img.into())\n    }\n}\n\nimpl AtlasImage {\n    pub fn from_hdr_path(p: impl AsRef<std::path::Path>) -> Result<Self, AtlasImageError> {\n        let bytes = std::fs::read(p.as_ref()).with_context(|_| CannotLoadSnafu {\n            path: std::path::PathBuf::from(p.as_ref()),\n        })?;\n        Self::from_hdr_bytes(&bytes)\n    }\n\n    pub fn from_hdr_bytes(bytes: &[u8]) -> Result<Self, AtlasImageError> {\n        // Decode HDR data.\n        let decoder = image::codecs::hdr::HdrDecoder::new(bytes).context(ImageSnafu)?;\n        let width = decoder.metadata().width;\n        let height = decoder.metadata().height;\n        let img = image::DynamicImage::from_decoder(decoder).unwrap();\n        let pixels = img.into_rgb32f();\n\n        // Add alpha data.\n        let mut pixel_data: Vec<f32> = Vec::new();\n        for pixel in pixels.pixels() {\n            pixel_data.push(pixel[0]);\n            pixel_data.push(pixel[1]);\n            pixel_data.push(pixel[2]);\n            pixel_data.push(1.0);\n        }\n        let mut pixels = vec![];\n        pixels.extend_from_slice(bytemuck::cast_slice(pixel_data.as_slice()));\n\n        Ok(Self {\n            pixels,\n            size: UVec2::new(width, height),\n            format: AtlasImageFormat::R32G32B32A32FLOAT,\n            apply_linear_transfer: false,\n        })\n    }\n\n    pub fn from_path(p: impl AsRef<std::path::Path>) -> Result<Self, AtlasImageError> {\n        Self::try_from(p.as_ref().to_path_buf())\n    }\n\n    pub fn into_rgba8(self) -> Option<image::RgbaImage> {\n        let pixels = convert_pixels(\n            self.pixels,\n            self.format,\n            wgpu::TextureFormat::Rgba8Unorm,\n            self.apply_linear_transfer,\n        );\n        image::RgbaImage::from_vec(self.size.x, self.size.y, pixels)\n    }\n\n    /// Returns a new [`AtlasImage`] with zeroed data.\n    pub fn new(size: UVec2, format: AtlasImageFormat) -> Self {\n        Self {\n            pixels: std::iter::repeat_n(format.zero_pixel(), (size.x * size.y) as usize)\n                .flatten()\n                .copied()\n                .collect(),\n            size,\n            format,\n            apply_linear_transfer: false,\n        }\n    }\n}\n\nfn apply_linear_xfer(bytes: &mut [u8], format: AtlasImageFormat) {\n    use crate::color::*;\n    match format {\n        AtlasImageFormat::R8\n        | AtlasImageFormat::R8G8\n        | AtlasImageFormat::R8G8B8\n        | AtlasImageFormat::R8G8B8A8 => {\n            bytes.iter_mut().for_each(linear_xfer_u8);\n        }\n        AtlasImageFormat::R16\n        | AtlasImageFormat::R16G16\n        | AtlasImageFormat::R16G16B16\n        | AtlasImageFormat::R16G16B16A16 => {\n            let bytes: &mut [u16] = bytemuck::cast_slice_mut(bytes);\n            bytes.iter_mut().for_each(linear_xfer_u16);\n        }\n        AtlasImageFormat::R16G16B16A16FLOAT => {\n            let bytes: &mut [u16] = bytemuck::cast_slice_mut(bytes);\n            bytes.iter_mut().for_each(linear_xfer_f16);\n        }\n        AtlasImageFormat::R32G32B32FLOAT\n        | AtlasImageFormat::R32G32B32A32FLOAT\n        | AtlasImageFormat::D32FLOAT\n        | AtlasImageFormat::R32FLOAT => {\n            let bytes: &mut [f32] = bytemuck::cast_slice_mut(bytes);\n            bytes.iter_mut().for_each(linear_xfer_f32);\n        }\n    }\n}\n\n/// Interpret/convert the `AtlasImage` pixel data into `wgpu::TextureFormat`\n/// pixels, if possible.\n///\n/// This applies the linear transfer function if `apply_linear_transfer` is\n/// `true`.\npub fn convert_pixels(\n    bytes: impl IntoIterator<Item = u8>,\n    from_format: AtlasImageFormat,\n    to_format: wgpu::TextureFormat,\n    apply_linear_transfer: bool,\n) -> Vec<u8> {\n    use crate::color::*;\n    let mut bytes = bytes.into_iter().collect::<Vec<_>>();\n    log::trace!(\"converting image of format {from_format:?}\");\n    // Convert using linear transfer, if needed\n    if apply_linear_transfer {\n        log::trace!(\"  converting to linear color space (from sRGB)\");\n        apply_linear_xfer(&mut bytes, from_format);\n    }\n\n    // Hamfisted conversion to `to_format`\n    match (from_format, to_format) {\n        (AtlasImageFormat::R8, wgpu::TextureFormat::Rgba8Unorm) => {\n            bytes.into_iter().flat_map(|r| [r, 0, 0, 255]).collect()\n        }\n        (AtlasImageFormat::R8G8, wgpu::TextureFormat::Rgba8Unorm) => bytes\n            .chunks_exact(2)\n            .flat_map(|p| {\n                if let [r, g] = p {\n                    [*r, *g, 0, 255]\n                } else {\n                    unreachable!()\n                }\n            })\n            .collect(),\n        (AtlasImageFormat::R8G8B8, wgpu::TextureFormat::Rgba8Unorm) => bytes\n            .chunks_exact(3)\n            .flat_map(|p| {\n                if let [r, g, b] = p {\n                    [*r, *g, *b, 255]\n                } else {\n                    unreachable!()\n                }\n            })\n            .collect(),\n        (AtlasImageFormat::R8G8B8A8, wgpu::TextureFormat::Rgba8Unorm) => bytes,\n        (AtlasImageFormat::R16, wgpu::TextureFormat::Rgba8Unorm) => {\n            bytemuck::cast_slice::<u8, u16>(&bytes)\n                .iter()\n                .flat_map(|r| [u16_to_u8(*r), 0, 0, 255])\n                .collect()\n        }\n        (AtlasImageFormat::R16G16, wgpu::TextureFormat::Rgba8Unorm) => {\n            bytemuck::cast_slice::<u8, u16>(&bytes)\n                .chunks_exact(2)\n                .flat_map(|p| {\n                    if let [r, g] = p {\n                        [u16_to_u8(*r), u16_to_u8(*g), 0, 255]\n                    } else {\n                        unreachable!()\n                    }\n                })\n                .collect()\n        }\n        (AtlasImageFormat::R16G16B16, wgpu::TextureFormat::Rgba8Unorm) => {\n            bytemuck::cast_slice::<u8, u16>(&bytes)\n                .chunks_exact(3)\n                .flat_map(|p| {\n                    if let [r, g, b] = p {\n                        [u16_to_u8(*r), u16_to_u8(*g), u16_to_u8(*b), 255]\n                    } else {\n                        unreachable!()\n                    }\n                })\n                .collect()\n        }\n\n        (AtlasImageFormat::R16G16B16A16, wgpu::TextureFormat::Rgba8Unorm) => {\n            bytemuck::cast_slice::<u8, u16>(&bytes)\n                .iter()\n                .copied()\n                .map(u16_to_u8)\n                .collect()\n        }\n        (AtlasImageFormat::R16G16B16A16FLOAT, wgpu::TextureFormat::Rgba8Unorm) => {\n            bytemuck::cast_slice::<u8, u16>(&bytes)\n                .iter()\n                .map(|bits| half::f16::from_bits(*bits).to_f32())\n                .collect::<Vec<_>>()\n                .chunks_exact(4)\n                .flat_map(|p| {\n                    if let [r, g, b, a] = p {\n                        [f32_to_u8(*r), f32_to_u8(*g), f32_to_u8(*b), f32_to_u8(*a)]\n                    } else {\n                        unreachable!()\n                    }\n                })\n                .collect()\n        }\n        (AtlasImageFormat::R32G32B32FLOAT, wgpu::TextureFormat::Rgba8Unorm) => {\n            bytemuck::cast_slice::<u8, f32>(&bytes)\n                .chunks_exact(3)\n                .flat_map(|p| {\n                    if let [r, g, b] = p {\n                        [f32_to_u8(*r), f32_to_u8(*g), f32_to_u8(*b), 255]\n                    } else {\n                        unreachable!()\n                    }\n                })\n                .collect()\n        }\n        (AtlasImageFormat::R32G32B32A32FLOAT, wgpu::TextureFormat::Rgba8Unorm)\n        | (AtlasImageFormat::R32FLOAT, wgpu::TextureFormat::Rgba8Unorm)\n        | (AtlasImageFormat::D32FLOAT, wgpu::TextureFormat::Rgba8Unorm) => {\n            bytemuck::cast_slice::<u8, f32>(&bytes)\n                .iter()\n                .copied()\n                .map(f32_to_u8)\n                .collect()\n        }\n        (AtlasImageFormat::R32FLOAT, wgpu::TextureFormat::R32Float) => bytes,\n        // TODO: add more atlas format conversions\n        (from, to) => panic!(\"cannot convert from atlas format {from:?} to {to:?}\"),\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/atlas/cpu.rs",
    "content": "use core::{ops::Deref, sync::atomic::AtomicUsize};\nuse std::sync::{Arc, Mutex, RwLock};\n\nuse craballoc::{\n    prelude::{Hybrid, SlabAllocator, WeakHybrid},\n    runtime::WgpuRuntime,\n    slab::SlabBuffer,\n};\nuse crabslab::Id;\nuse glam::{UVec2, UVec3};\nuse image::RgbaImage;\nuse snafu::{prelude::*, OptionExt};\n\nuse crate::{\n    atlas::{\n        shader::{AtlasBlittingDescriptor, AtlasDescriptor, AtlasTextureDescriptor},\n        TextureModes,\n    },\n    bindgroup::ManagedBindGroup,\n    texture::{self, CopiedTextureBuffer, Texture},\n};\n\nuse super::atlas_image::{convert_pixels, AtlasImage};\n\npub(crate) const ATLAS_SUGGESTED_SIZE: u32 = 2048;\npub(crate) const ATLAS_SUGGESTED_LAYERS: u32 = 8;\n\n#[derive(Debug, Snafu)]\npub enum AtlasError {\n    #[snafu(display(\"Cannot pack textures.\\natlas_size: {size:#?}\"))]\n    CannotPackTextures { size: wgpu::Extent3d },\n\n    #[snafu(display(\"Missing layer {index}\"))]\n    MissingLayer { index: u32, images: Vec<AtlasImage> },\n\n    #[snafu(display(\"Atlas size is invalid: {size:?}\"))]\n    Size { size: wgpu::Extent3d },\n\n    #[snafu(display(\"Missing slab during staging\"))]\n    StagingMissingSlab,\n\n    #[snafu(display(\"Missing bindgroup {layer}\"))]\n    MissingBindgroup { layer: u32 },\n\n    #[snafu(display(\"{source}\"))]\n    Texture {\n        source: crate::texture::TextureError,\n    },\n}\n\n/// A staged texture in the texture atlas.\n///\n/// An [`AtlasTexture`] can be acquired through:\n///\n/// * [`Atlas::add_image`]\n/// * [`Atlas::add_images`].\n/// * [`Atlas::set_images`]\n///\n/// Clones of this type all point to the same underlying data.\n///\n/// Dropping all clones of this type will cause it to be unloaded from the GPU.\n///\n/// If a value of this type has been given to another staged resource,\n/// like [`Material`](crate::material::Material), this will prevent the\n/// `AtlasTexture` from being dropped and unloaded.\n///\n/// Internally an `AtlasTexture` holds a reference to its descriptor,\n/// [`AtlasTextureDescriptor`].\n#[derive(Clone)]\npub struct AtlasTexture {\n    pub(crate) descriptor: Hybrid<AtlasTextureDescriptor>,\n}\n\nimpl AtlasTexture {\n    /// Get the GPU slab identifier of the underlying descriptor.\n    ///\n    /// This is for internal use.\n    pub fn id(&self) -> Id<AtlasTextureDescriptor> {\n        self.descriptor.id()\n    }\n\n    /// Return a copy of the underlying descriptor.\n    pub fn descriptor(&self) -> AtlasTextureDescriptor {\n        self.descriptor.get()\n    }\n\n    /// Return the texture modes of the underlying descriptor.\n    pub fn modes(&self) -> TextureModes {\n        self.descriptor.get().modes\n    }\n\n    /// Sets the texture modes of the underlying descriptor.\n    ///\n    /// ## Warning\n    ///\n    /// This also sets the modes for all clones of this value.\n    pub fn set_modes(&self, modes: TextureModes) {\n        self.descriptor.modify(|d| d.modes = modes);\n    }\n}\n\n/// Used to track textures internally.\n///\n/// We need a separate struct for tracking textures because the atlas\n/// reorganizes the layout (the packing) of textures each time a new\n/// texture is added.\n///\n/// This means the textures must be updated on the GPU, but we don't\n/// want these internal representations to keep unreferenced textures\n/// from dropping, so we have to maintain a separate representation\n/// here.\n#[derive(Clone, Debug)]\nstruct InternalAtlasTexture {\n    /// Cached value.\n    cache: AtlasTextureDescriptor,\n    weak: WeakHybrid<AtlasTextureDescriptor>,\n}\n\nimpl InternalAtlasTexture {\n    fn from_hybrid(hat: &Hybrid<AtlasTextureDescriptor>) -> Self {\n        InternalAtlasTexture {\n            cache: hat.get(),\n            weak: WeakHybrid::from_hybrid(hat),\n        }\n    }\n\n    fn has_external_references(&self) -> bool {\n        self.weak.has_external_references()\n    }\n\n    fn set(&mut self, at: AtlasTextureDescriptor) {\n        self.cache = at;\n        if let Some(hy) = self.weak.upgrade() {\n            hy.set(at);\n        } else if let Some(gpu) = self.weak.weak_gpu().upgrade() {\n            gpu.set(at)\n        } else {\n            log::warn!(\"could not set atlas texture, lost\");\n        }\n    }\n}\n\npub(crate) fn check_size(size: wgpu::Extent3d) {\n    let conditions = size.depth_or_array_layers >= 2\n        && size.width == size.height\n        && (size.width & (size.width - 1)) == 0;\n    if !conditions {\n        log::error!(\"{}\", AtlasError::Size { size });\n    }\n}\n\nfn fan_split_n<T>(n: usize, input: impl IntoIterator<Item = T>) -> Vec<Vec<T>> {\n    if n == 0 {\n        return vec![];\n    }\n    let mut output = vec![];\n    for _ in 0..n {\n        output.push(vec![]);\n    }\n    let mut i = 0;\n    for item in input.into_iter() {\n        // UNWRAP: safe because i % n\n        output\n            .get_mut(i)\n            .unwrap_or_else(|| panic!(\"could not unwrap i:{i} n:{n}\"))\n            .push(item);\n        i = (i + 1) % n;\n    }\n    output\n}\n\n#[derive(Clone)]\nenum AnotherPacking<'a> {\n    Img {\n        original_index: usize,\n        image: &'a AtlasImage,\n    },\n    Internal(InternalAtlasTexture),\n}\n\nimpl AnotherPacking<'_> {\n    fn size(&self) -> UVec2 {\n        match self {\n            AnotherPacking::Img {\n                original_index: _,\n                image,\n            } => image.size,\n            AnotherPacking::Internal(tex) => tex.cache.size_px,\n        }\n    }\n}\n\n#[derive(Clone, Default, Debug)]\npub struct Layer {\n    frames: Vec<InternalAtlasTexture>,\n}\n\n/// A texture atlas, used to store all the textures in a scene.\n///\n/// Clones of `Atlas` all point to the same internal data.\n#[derive(Clone)]\npub struct Atlas {\n    pub(crate) slab: SlabAllocator<WgpuRuntime>,\n    texture_array: Arc<RwLock<Texture>>,\n    layers: Arc<RwLock<Vec<Layer>>>,\n    label: Option<String>,\n    descriptor: Hybrid<AtlasDescriptor>,\n    /// Used for user updates into the atlas by blit images into specific\n    /// frames.\n    blitter: AtlasBlitter,\n}\n\nimpl Atlas {\n    const LABEL: Option<&str> = Some(\"atlas-texture\");\n\n    pub fn device(&self) -> &wgpu::Device {\n        self.slab.device()\n    }\n\n    /// Create the initial texture to use.\n    fn create_texture(\n        runtime: impl AsRef<WgpuRuntime>,\n        size: wgpu::Extent3d,\n        format: Option<wgpu::TextureFormat>,\n        label: Option<&str>,\n        usage: Option<wgpu::TextureUsages>,\n    ) -> Texture {\n        let device = &runtime.as_ref().device;\n        let queue = &runtime.as_ref().queue;\n        check_size(size);\n        let usage = usage.unwrap_or(wgpu::TextureUsages::empty());\n        let texture = device.create_texture(&wgpu::TextureDescriptor {\n            label: Some(label.unwrap_or(Self::LABEL.unwrap())),\n            size,\n            mip_level_count: 1,\n            sample_count: 1,\n            dimension: wgpu::TextureDimension::D2,\n            format: format.unwrap_or(wgpu::TextureFormat::Rgba8Unorm),\n            usage: usage\n                | wgpu::TextureUsages::TEXTURE_BINDING\n                | wgpu::TextureUsages::COPY_DST\n                | wgpu::TextureUsages::COPY_SRC,\n            view_formats: &[],\n        });\n\n        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {\n            label: Some(\"create atlas texture array\"),\n        });\n        if device.features().contains(wgpu::Features::CLEAR_TEXTURE) {\n            encoder.clear_texture(\n                &texture,\n                &wgpu::ImageSubresourceRange {\n                    aspect: wgpu::TextureAspect::All,\n                    base_mip_level: 0,\n                    mip_level_count: None,\n                    base_array_layer: 0,\n                    array_layer_count: None,\n                },\n            );\n        }\n        queue.submit(Some(encoder.finish()));\n\n        let sampler_desc = wgpu::SamplerDescriptor {\n            address_mode_u: wgpu::AddressMode::ClampToEdge,\n            address_mode_v: wgpu::AddressMode::ClampToEdge,\n            address_mode_w: wgpu::AddressMode::ClampToEdge,\n            mag_filter: wgpu::FilterMode::Nearest,\n            min_filter: wgpu::FilterMode::Nearest,\n            mipmap_filter: wgpu::FilterMode::Nearest,\n            ..Default::default()\n        };\n\n        Texture::from_wgpu_tex(device, texture, Some(sampler_desc), None)\n    }\n\n    /// Create a new atlas.\n    ///\n    /// Size _must_ be a power of two.\n    ///\n    /// ## Panics\n    /// Panics if `size` is not a power of two.\n    pub fn new(\n        slab: &SlabAllocator<WgpuRuntime>,\n        size: wgpu::Extent3d,\n        format: Option<wgpu::TextureFormat>,\n        label: Option<&str>,\n        usage: Option<wgpu::TextureUsages>,\n    ) -> Self {\n        let texture = Self::create_texture(slab.runtime(), size, format, label, usage);\n        let num_layers = texture.texture.size().depth_or_array_layers as usize;\n        let layers = vec![Layer::default(); num_layers];\n        log::trace!(\"creating new atlas with dimensions {size:?}, {num_layers} layers\");\n        let descriptor = slab.new_value(AtlasDescriptor {\n            size: UVec3::new(size.width, size.height, size.depth_or_array_layers),\n        });\n        let label = label.map(|s| s.to_owned());\n        let blitter = AtlasBlitter::new(\n            slab.device(),\n            texture.texture.format(),\n            wgpu::FilterMode::Linear,\n        );\n        Atlas {\n            slab: slab.clone(),\n            layers: Arc::new(RwLock::new(layers)),\n            descriptor,\n            label,\n            blitter,\n            texture_array: Arc::new(RwLock::new(texture)),\n        }\n    }\n\n    pub fn descriptor_id(&self) -> Id<AtlasDescriptor> {\n        self.descriptor.id()\n    }\n\n    pub fn len(&self) -> usize {\n        // UNWRAP: panic on purpose\n        let layers = self.layers.read().expect(\"atlas layers read\");\n        layers.iter().map(|layer| layer.frames.len()).sum::<usize>()\n    }\n\n    pub fn is_empty(&self) -> bool {\n        // UNWRAP: panic on purpose\n        self.len() == 0\n    }\n\n    /// Returns a reference to the current atlas texture array.\n    pub fn get_texture(&self) -> impl Deref<Target = Texture> + '_ {\n        // UNWRAP: panic on purpose\n        self.texture_array.read().expect(\"atlas texture_array read\")\n    }\n\n    pub fn get_layers(&self) -> impl Deref<Target = Vec<Layer>> + '_ {\n        // UNWRAP: panic on purpose\n        self.layers.read().expect(\"atlas layers read\")\n    }\n\n    /// Reset this atlas with all new images.\n    ///\n    /// Any existing `Hybrid<AtlasTexture>`s will be invalidated.\n    pub fn set_images(&self, images: &[AtlasImage]) -> Result<Vec<AtlasTexture>, AtlasError> {\n        log::debug!(\"setting images\");\n        {\n            // UNWRAP: panic on purpose\n            let texture = self.texture_array.read().expect(\"atlas texture_array read\");\n            let mut guard = self.layers.write().expect(\"atlas layers write\");\n            let layers: &mut Vec<_> = guard.as_mut();\n            let new_layers =\n                vec![Layer::default(); texture.texture.size().depth_or_array_layers as usize];\n            let _old_layers = std::mem::replace(layers, new_layers);\n        }\n        self.add_images(images)\n    }\n\n    pub fn get_size(&self) -> wgpu::Extent3d {\n        // UNWRAP: POP\n        self.texture_array\n            .read()\n            .expect(\"atlas texture_array read\")\n            .texture\n            .size()\n    }\n\n    /// Add the given images\n    pub fn add_images<'a>(\n        &self,\n        images: impl IntoIterator<Item = &'a AtlasImage>,\n    ) -> Result<Vec<AtlasTexture>, AtlasError> {\n        // UNWRAP: POP\n        let mut layers = self.layers.write().expect(\"atlas layers write\");\n        let mut texture_array = self\n            .texture_array\n            .write()\n            .expect(\"atlas texture_array write\");\n        let extent = texture_array.texture.size();\n\n        let newly_packed_layers = pack_images(&layers, images, extent)\n            .context(CannotPackTexturesSnafu { size: extent })?;\n\n        let mut staged = StagedResources::try_staging(\n            self.slab.runtime(),\n            extent,\n            newly_packed_layers,\n            Some(&self.slab),\n            &texture_array,\n            self.label.as_deref(),\n        )?;\n\n        // Commit our newly staged values, now that everything is done.\n        *texture_array = staged.texture;\n        *layers = staged.layers;\n\n        staged.image_additions.sort_by_key(|a| a.0);\n        Ok(staged\n            .image_additions\n            .into_iter()\n            .map(|a| AtlasTexture { descriptor: a.1 })\n            .collect())\n    }\n\n    /// Add one image.\n    ///\n    /// If you have more than one image, you should use [`Atlas::add_images`],\n    /// as every change in images causes a repacking, which might be\n    /// expensive.\n    pub fn add_image(&self, image: &AtlasImage) -> Result<AtlasTexture, AtlasError> {\n        // UNWRAP: safe because we know there's at least one image\n        Ok(self.add_images(Some(image))?.pop().unwrap())\n    }\n\n    /// Resize the atlas.\n    ///\n    /// This also distributes the images by size among all layers in an effort\n    /// to reduce the likelyhood that packing the atlas may fail.\n    ///\n    /// ## Errors\n    /// Errors if `size` has a width or height that is not a power of two, or\n    /// are unequal\n    pub fn resize(\n        &self,\n        runtime: impl AsRef<WgpuRuntime>,\n        extent: wgpu::Extent3d,\n    ) -> Result<(), AtlasError> {\n        let mut layers = self.layers.write().expect(\"atlas layers write\");\n        let mut texture_array = self\n            .texture_array\n            .write()\n            .expect(\"atlas texture_array write\");\n\n        let newly_packed_layers =\n            pack_images(&layers, &[], extent).context(CannotPackTexturesSnafu { size: extent })?;\n\n        let staged = StagedResources::try_staging(\n            runtime,\n            extent,\n            newly_packed_layers,\n            None::<&SlabAllocator<WgpuRuntime>>,\n            &texture_array,\n            self.label.as_deref(),\n        )?;\n\n        // Commit our newly staged values, now that everything is done.\n        *texture_array = staged.texture;\n        *layers = staged.layers;\n\n        Ok(())\n    }\n\n    /// Perform upkeep on the atlas.\n    ///\n    /// This removes any `TextureFrame`s that have no references and repacks the\n    /// atlas if any were removed.\n    ///\n    /// Returns `true` if the atlas texture was recreated.\n    #[must_use]\n    pub fn upkeep(&self, runtime: impl AsRef<WgpuRuntime>) -> bool {\n        let mut total_dropped = 0;\n        {\n            let mut layers = self.layers.write().expect(\"atlas layers write\");\n            for (i, layer) in layers.iter_mut().enumerate() {\n                let mut dropped = 0;\n                layer.frames.retain(|entry| {\n                    if entry.has_external_references() {\n                        true\n                    } else {\n                        dropped += 1;\n                        false\n                    }\n                });\n                total_dropped += dropped;\n                if dropped > 0 {\n                    log::trace!(\"removed {dropped} frames from layer {i}\");\n                }\n            }\n\n            layers.len()\n        };\n\n        if total_dropped > 0 {\n            log::trace!(\"repacking after dropping {total_dropped} frames from the atlas\");\n            // UNWRAP: safe because we can only remove frames from the atlas, which should\n            // only make it easier to pack.\n            self.resize(runtime.as_ref(), self.get_size()).unwrap();\n            true\n        } else {\n            false\n        }\n    }\n\n    /// Read the atlas image from the GPU into a [`CopiedTextureBuffer`].\n    ///\n    /// This is primarily for testing.\n    ///\n    /// ## Panics\n    /// Panics if the pixels read from the GPU cannot be read.\n    pub fn atlas_img_buffer(\n        &self,\n        runtime: impl AsRef<WgpuRuntime>,\n        layer: u32,\n    ) -> CopiedTextureBuffer {\n        let runtime = runtime.as_ref();\n        let tex = self.get_texture();\n        let size = tex.texture.size();\n        let (channels, subpixel_bytes) =\n            crate::texture::wgpu_texture_format_channels_and_subpixel_bytes_todo(\n                tex.texture.format(),\n            );\n        log::info!(\"atlas_texture_format: {:#?}\", tex.texture.format());\n        log::info!(\"atlas_texture_channels: {channels:#?}\");\n        log::info!(\"atlas_texture_subpixel_bytes: {subpixel_bytes:#?}\");\n        CopiedTextureBuffer::read_from(\n            runtime,\n            &tex.texture,\n            size.width as usize,\n            size.height as usize,\n            channels as usize,\n            subpixel_bytes as usize,\n            0,\n            Some(wgpu::Origin3d {\n                x: 0,\n                y: 0,\n                z: layer,\n            }),\n        )\n    }\n\n    /// Read the atlas image from the GPU.\n    ///\n    /// This is primarily for testing.\n    ///\n    /// The resulting image will be in a **linear** color space.\n    ///\n    /// ## Panics\n    /// Panics if the pixels read from the GPU cannot be converted into an\n    /// `RgbaImage`.\n    pub async fn atlas_img(&self, runtime: impl AsRef<WgpuRuntime>, layer: u32) -> RgbaImage {\n        let runtime = runtime.as_ref();\n        let buffer = self.atlas_img_buffer(runtime, layer);\n        buffer.into_linear_rgba(&runtime.device).await.unwrap()\n    }\n\n    // It's ok to hold this lock because this is just for testing.\n    #[allow(clippy::await_holding_lock)]\n    pub async fn read_images(&self, runtime: impl AsRef<WgpuRuntime>) -> Vec<RgbaImage> {\n        let mut images = vec![];\n        for i in 0..self.layers.read().expect(\"atlas layers read\").len() {\n            images.push(self.atlas_img(runtime.as_ref(), i as u32).await);\n        }\n        images\n    }\n\n    /// Update the given [`AtlasTexture`] with a\n    /// [`Texture`](crate::texture::Texture).\n    ///\n    /// This will blit the `Texture` into the frame of the [`Atlas`] pointed to\n    /// by the `AtlasTexture`.\n    ///\n    /// Returns a submission index that can be polled with\n    /// [`wgpu::Device::poll`].\n    pub fn update_texture(\n        &self,\n        atlas_texture: &AtlasTexture,\n        source_texture: &texture::Texture,\n    ) -> Result<wgpu::SubmissionIndex, AtlasError> {\n        self.update_textures(Some((atlas_texture, source_texture)))\n    }\n\n    /// Update the given [`AtlasTexture`]s with\n    /// [`Texture`](crate::texture::Texture)s.\n    ///\n    /// This will blit the `Texture` into the frame of the [`Atlas`] pointed to\n    /// by the `AtlasTexture`.\n    ///\n    /// Returns a submission index that can be polled with\n    /// [`wgpu::Device::poll`].\n    pub fn update_textures<'a>(\n        &self,\n        updates: impl IntoIterator<Item = (&'a AtlasTexture, &'a texture::Texture)>,\n    ) -> Result<wgpu::SubmissionIndex, AtlasError> {\n        let updates = updates.into_iter().collect::<Vec<_>>();\n        let op = AtlasBlittingOperation::new(&self.blitter, self, updates.len());\n        let runtime = self.slab.runtime();\n        let mut encoder = runtime\n            .device\n            .create_command_encoder(&wgpu::CommandEncoderDescriptor {\n                label: Some(\"Atlas::update_texture\"),\n            });\n        for (i, (atlas_texture, source_texture)) in updates.into_iter().enumerate() {\n            op.run(\n                runtime,\n                &mut encoder,\n                source_texture,\n                i as u32,\n                self,\n                atlas_texture,\n            )?;\n        }\n        Ok(runtime.queue.submit(Some(encoder.finish())))\n    }\n\n    /// Update the given [`AtlasTexture`]s with new data.\n    ///\n    /// This will blit the image data into the frame of the [`Atlas`] pointed to\n    /// by the `AtlasTexture`.\n    ///\n    /// Returns a submission index that can be polled with\n    /// [`wgpu::Device::poll`].\n    pub fn update_images<'a>(\n        &self,\n        updates: impl IntoIterator<Item = (&'a AtlasTexture, impl Into<AtlasImage>)>,\n    ) -> Result<wgpu::SubmissionIndex, AtlasError> {\n        let (atlas_textures, images): (Vec<_>, Vec<_>) = updates.into_iter().unzip();\n        let mut textures = vec![];\n        for image in images.into_iter() {\n            let image: AtlasImage = image.into();\n            let atlas_format = self.get_texture().texture.format();\n            let bytes = super::atlas_image::convert_pixels(\n                image.pixels,\n                image.format,\n                atlas_format,\n                image.apply_linear_transfer,\n            );\n            let (channels, subpixel_bytes) =\n                texture::wgpu_texture_format_channels_and_subpixel_bytes(atlas_format)\n                    .context(TextureSnafu)?;\n            let texture = texture::Texture::new_with(\n                self.slab.runtime(),\n                Some(\"atlas-image-update\"),\n                Some(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST),\n                None,\n                atlas_format,\n                channels,\n                subpixel_bytes,\n                image.size.x,\n                image.size.y,\n                1,\n                &bytes,\n            );\n            textures.push(texture);\n        }\n        self.update_textures(atlas_textures.into_iter().zip(textures.iter()))\n    }\n\n    /// Update the given [`AtlasTexture`]s with new data.\n    ///\n    /// This will blit the image data into the frame of the [`Atlas`] pointed to\n    /// by the `AtlasTexture`.\n    ///\n    /// Returns a submission index that can be polled with\n    /// [`wgpu::Device::poll`].\n    pub fn update_image(\n        &self,\n        atlas_texture: &AtlasTexture,\n        source_image: impl Into<AtlasImage>,\n    ) -> Result<wgpu::SubmissionIndex, AtlasError> {\n        self.update_images(Some((atlas_texture, source_image)))\n    }\n}\n\nfn pack_images<'a>(\n    layers: &[Layer],\n    images: impl IntoIterator<Item = &'a AtlasImage>,\n    extent: wgpu::Extent3d,\n) -> Option<Vec<crunch::PackedItems<AnotherPacking<'a>>>> {\n    let mut new_packing: Vec<AnotherPacking> = {\n        let layers: Vec<_> = layers.to_vec();\n        layers\n            .into_iter()\n            .flat_map(|layer| layer.frames)\n            // Filter out any textures that have been completely dropped\n            // by the user.\n            .filter_map(|tex| {\n                if tex.has_external_references() {\n                    Some(AnotherPacking::Internal(tex))\n                } else {\n                    None\n                }\n            })\n            .chain(\n                images\n                    .into_iter()\n                    .enumerate()\n                    .map(|(i, image)| AnotherPacking::Img {\n                        original_index: i,\n                        image,\n                    }),\n            )\n            .collect()\n    };\n    new_packing.sort_by_key(|a| a.size().length_squared());\n    let total_images = new_packing.len();\n    let new_packing_layers: Vec<Vec<AnotherPacking>> =\n        fan_split_n(extent.depth_or_array_layers as usize, new_packing);\n    log::trace!(\n        \"packing {total_images} textures into {} layers\",\n        new_packing_layers.len()\n    );\n    let mut newly_packed_layers: Vec<crunch::PackedItems<_>> = vec![];\n    for (i, new_layer) in new_packing_layers.into_iter().enumerate() {\n        log::trace!(\"  packing layer {i} into power of 2 {}\", extent.width);\n        let packed = crunch::pack_into_po2(\n            extent.width as usize,\n            new_layer.into_iter().map(|p| {\n                let size = p.size();\n                crunch::Item::new(p, size.x as usize, size.y as usize, crunch::Rotation::None)\n            }),\n        )\n        .ok()?;\n        log::trace!(\"  layer {i} packed with {} textures\", packed.items.len());\n        newly_packed_layers.push(packed);\n    }\n    Some(newly_packed_layers)\n}\n\n/// Internal atlas resources.\nstruct StagedResources {\n    texture: Texture,\n    image_additions: Vec<(usize, Hybrid<AtlasTextureDescriptor>)>,\n    layers: Vec<Layer>,\n}\n\nimpl StagedResources {\n    /// Stage the packed images, copying them to the next texture.\n    fn try_staging(\n        runtime: impl AsRef<WgpuRuntime>,\n        extent: wgpu::Extent3d,\n        newly_packed_layers: Vec<crunch::PackedItems<AnotherPacking>>,\n        slab: Option<&SlabAllocator<WgpuRuntime>>,\n        old_texture_array: &Texture,\n        label: Option<&str>,\n    ) -> Result<Self, AtlasError> {\n        let runtime = runtime.as_ref();\n        let new_texture_array = Atlas::create_texture(\n            runtime,\n            extent,\n            Some(old_texture_array.texture.format()),\n            label,\n            Some(old_texture_array.texture.usage()),\n        );\n        let mut output = vec![];\n        let mut encoder = runtime\n            .device\n            .create_command_encoder(&wgpu::CommandEncoderDescriptor {\n                label: Some(\"atlas staging\"),\n            });\n        let mut temporary_layers = vec![Layer::default(); extent.depth_or_array_layers as usize];\n        for (layer_index, packed_items) in newly_packed_layers.into_iter().enumerate() {\n            if packed_items.items.is_empty() {\n                continue;\n            }\n            // UNWRAP: safe because we know this index exists because we created it above\n            let layer = temporary_layers.get_mut(layer_index).unwrap();\n            for (frame_index, crunch::PackedItem { data: item, rect }) in\n                packed_items.items.into_iter().enumerate()\n            {\n                let offset_px = UVec2::new(rect.x as u32, rect.y as u32);\n                let size_px = UVec2::new(rect.w as u32, rect.h as u32);\n\n                match item {\n                    AnotherPacking::Img {\n                        original_index,\n                        image,\n                    } => {\n                        let atlas_texture = AtlasTextureDescriptor {\n                            offset_px,\n                            size_px,\n                            frame_index: frame_index as u32,\n                            layer_index: layer_index as u32,\n                            ..Default::default()\n                        };\n                        let texture = slab\n                            .context(StagingMissingSlabSnafu)?\n                            .new_value(atlas_texture);\n                        layer\n                            .frames\n                            .push(InternalAtlasTexture::from_hybrid(&texture));\n                        output.push((original_index, texture));\n\n                        let bytes = convert_pixels(\n                            image.pixels.clone(),\n                            image.format,\n                            old_texture_array.texture.format(),\n                            image.apply_linear_transfer,\n                        );\n\n                        let origin = wgpu::Origin3d {\n                            x: offset_px.x,\n                            y: offset_px.y,\n                            z: layer_index as u32,\n                        };\n                        let size = wgpu::Extent3d {\n                            width: size_px.x,\n                            height: size_px.y,\n                            depth_or_array_layers: 1,\n                        };\n                        log::trace!(\n                            \"  writing image data to frame {frame_index} in layer {layer_index}\"\n                        );\n                        log::trace!(\"    frame: {atlas_texture:?}\");\n                        log::trace!(\"    origin: {origin:?}\");\n                        log::trace!(\"    size: {size:?}\");\n\n                        // write the new image from the CPU to the new texture\n                        runtime.queue.write_texture(\n                            wgpu::TexelCopyTextureInfo {\n                                texture: &new_texture_array.texture,\n                                mip_level: 0,\n                                origin,\n                                aspect: wgpu::TextureAspect::All,\n                            },\n                            &bytes,\n                            wgpu::TexelCopyBufferLayout {\n                                offset: 0,\n                                bytes_per_row: Some(4 * size_px.x),\n                                rows_per_image: Some(size_px.y),\n                            },\n                            size,\n                        );\n                    }\n                    AnotherPacking::Internal(mut texture) => {\n                        let prev_t = texture.cache;\n                        let mut t = texture.cache;\n                        debug_assert_eq!(t.size_px, size_px);\n                        // copy the frame from the old texture to the new texture\n                        // in a new destination\n                        encoder.copy_texture_to_texture(\n                            wgpu::TexelCopyTextureInfo {\n                                texture: &old_texture_array.texture,\n                                mip_level: 0,\n                                origin: t.origin(),\n                                aspect: wgpu::TextureAspect::All,\n                            },\n                            wgpu::TexelCopyTextureInfo {\n                                texture: &new_texture_array.texture,\n                                mip_level: 0,\n                                origin: wgpu::Origin3d {\n                                    x: offset_px.x,\n                                    y: offset_px.y,\n                                    z: layer_index as u32,\n                                },\n                                aspect: wgpu::TextureAspect::All,\n                            },\n                            t.size_as_extent(),\n                        );\n\n                        t.layer_index = layer_index as u32;\n                        t.frame_index = frame_index as u32;\n                        t.offset_px = offset_px;\n\n                        log::trace!(\n                            \"  copied previous frame {}\",\n                            pretty_assertions::Comparison::new(&prev_t, &t)\n                        );\n\n                        texture.set(t);\n                        layer.frames.push(texture);\n                    }\n                }\n            }\n        }\n        runtime.queue.submit(Some(encoder.finish()));\n\n        Ok(Self {\n            texture: new_texture_array,\n            image_additions: output,\n            layers: temporary_layers,\n        })\n    }\n}\n\n/// A reusable blitting operation that copies a source texture into a specific\n/// frame of an [`Atlas`].\n#[derive(Clone)]\npub struct AtlasBlittingOperation {\n    atlas_slab_buffer: Arc<Mutex<SlabBuffer<wgpu::Buffer>>>,\n    pipeline: Arc<wgpu::RenderPipeline>,\n    bindgroups: Arc<Vec<ManagedBindGroup>>,\n    bindgroup_layout: Arc<wgpu::BindGroupLayout>,\n    sampler: Arc<wgpu::Sampler>,\n    from_texture_id: Arc<AtomicUsize>,\n    pub(crate) desc: Hybrid<AtlasBlittingDescriptor>,\n}\n\nimpl AtlasBlittingOperation {\n    pub fn new(\n        blitter: &AtlasBlitter,\n        into_atlas: &Atlas,\n        source_layers: usize,\n    ) -> AtlasBlittingOperation {\n        AtlasBlittingOperation {\n            desc: into_atlas\n                .slab\n                .new_value(AtlasBlittingDescriptor::default()),\n            atlas_slab_buffer: Arc::new(Mutex::new(into_atlas.slab.commit())),\n            bindgroups: {\n                let mut bgs = vec![];\n                for _ in 0..source_layers {\n                    bgs.push(ManagedBindGroup::default());\n                }\n                Arc::new(bgs)\n            },\n            pipeline: blitter.pipeline.clone(),\n            sampler: blitter.sampler.clone(),\n            bindgroup_layout: blitter.bind_group_layout.clone(),\n            from_texture_id: Default::default(),\n        }\n    }\n\n    /// Copies the data from texture this [`AtlasBlittingOperation`] was created\n    /// with into the atlas.\n    ///\n    /// The original items used to create the inner bind group are required\n    /// here, to determine whether or not the bind group needs to be\n    /// invalidated.\n    pub fn run(\n        &self,\n        runtime: impl AsRef<WgpuRuntime>,\n        encoder: &mut wgpu::CommandEncoder,\n        from_texture: &crate::texture::Texture,\n        from_layer: u32,\n        to_atlas: &Atlas,\n        atlas_texture: &AtlasTexture,\n    ) -> Result<(), AtlasError> {\n        let runtime = runtime.as_ref();\n\n        // update the descriptor\n        self.desc.set(AtlasBlittingDescriptor {\n            atlas_texture_id: atlas_texture.id(),\n            atlas_desc_id: to_atlas.descriptor_id(),\n        });\n        // sync the update\n        let _ = to_atlas.slab.commit();\n\n        let to_atlas_texture = to_atlas.get_texture();\n        let mut atlas_slab_buffer = self\n            .atlas_slab_buffer\n            .lock()\n            .expect(\"atlas slab buffer lock\");\n        let atlas_slab_invalid = atlas_slab_buffer.update_if_invalid();\n        let from_texture_has_been_replaced = {\n            let prev_id = self\n                .from_texture_id\n                .swap(from_texture.id(), std::sync::atomic::Ordering::Relaxed);\n            from_texture.id() != prev_id\n        };\n        let should_invalidate = atlas_slab_invalid || from_texture_has_been_replaced;\n        let view = from_texture\n            .texture\n            .create_view(&wgpu::TextureViewDescriptor {\n                label: Some(\"atlas-blitting\"),\n                base_array_layer: from_layer,\n                array_layer_count: Some(1),\n                dimension: Some(wgpu::TextureViewDimension::D2),\n                ..Default::default()\n            });\n        let bindgroup = self\n            .bindgroups\n            .get(from_layer as usize)\n            .context(MissingBindgroupSnafu { layer: from_layer })?\n            .get(should_invalidate, || {\n                runtime\n                    .device\n                    .create_bind_group(&wgpu::BindGroupDescriptor {\n                        label: Some(\"atlas-blitting\"),\n                        layout: &self.bindgroup_layout,\n                        entries: &[\n                            wgpu::BindGroupEntry {\n                                binding: 0,\n                                resource: wgpu::BindingResource::Buffer(\n                                    atlas_slab_buffer.deref().as_entire_buffer_binding(),\n                                ),\n                            },\n                            wgpu::BindGroupEntry {\n                                binding: 1,\n                                resource: wgpu::BindingResource::TextureView(&view),\n                            },\n                            wgpu::BindGroupEntry {\n                                binding: 2,\n                                resource: wgpu::BindingResource::Sampler(&self.sampler),\n                            },\n                        ],\n                    })\n            });\n\n        let atlas_texture = atlas_texture.descriptor();\n        let atlas_view = to_atlas_texture\n            .texture\n            .create_view(&wgpu::TextureViewDescriptor {\n                label: Some(\"atlas-blitting\"),\n                format: None,\n                dimension: Some(wgpu::TextureViewDimension::D2),\n                usage: None,\n                aspect: wgpu::TextureAspect::All,\n                base_mip_level: 0,\n                mip_level_count: None,\n                base_array_layer: atlas_texture.layer_index,\n                array_layer_count: Some(1),\n            });\n        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n            label: Some(\"atlas-blitter\"),\n            color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                view: &atlas_view,\n                resolve_target: None,\n                ops: wgpu::Operations {\n                    load: wgpu::LoadOp::Load,\n                    store: wgpu::StoreOp::Store,\n                },\n                depth_slice: None,\n            })],\n            depth_stencil_attachment: None,\n            timestamp_writes: None,\n            occlusion_query_set: None,\n        });\n        pass.set_pipeline(&self.pipeline);\n        pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]);\n        let id = self.desc.id();\n        pass.draw(0..6, id.inner()..id.inner() + 1);\n        Ok(())\n    }\n}\n\n/// A texture blitting utility.\n///\n/// [`AtlasBlitter`] copies textures to specific frames within the texture\n/// atlas.\n#[derive(Clone)]\npub struct AtlasBlitter {\n    pipeline: Arc<wgpu::RenderPipeline>,\n    bind_group_layout: Arc<wgpu::BindGroupLayout>,\n    sampler: Arc<wgpu::Sampler>,\n}\n\nimpl AtlasBlitter {\n    /// Creates a new [`AtlasBlitter`].\n    ///\n    /// # Arguments\n    /// - `device` - A [`wgpu::Device`]\n    /// - `format` - The [`wgpu::TextureFormat`] of the atlas being updated.\n    /// - `mag_filter` - The filtering algorithm to use when magnifying. This is\n    ///   used when the input source is smaller than the destination.\n    pub fn new(\n        device: &wgpu::Device,\n        format: wgpu::TextureFormat,\n        mag_filter: wgpu::FilterMode,\n    ) -> Self {\n        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {\n            label: Some(\"atlas-blitter\"),\n            address_mode_u: wgpu::AddressMode::ClampToEdge,\n            address_mode_v: wgpu::AddressMode::ClampToEdge,\n            address_mode_w: wgpu::AddressMode::ClampToEdge,\n            mag_filter,\n            ..Default::default()\n        });\n\n        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: Some(\"atlas-blitter\"),\n            entries: &[\n                wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::VERTEX,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n                wgpu::BindGroupLayoutEntry {\n                    binding: 1,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Texture {\n                        sample_type: wgpu::TextureSampleType::Float {\n                            filterable: mag_filter == wgpu::FilterMode::Linear,\n                        },\n                        view_dimension: wgpu::TextureViewDimension::D2,\n                        multisampled: false,\n                    },\n                    count: None,\n                },\n                wgpu::BindGroupLayoutEntry {\n                    binding: 2,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Sampler(\n                        if mag_filter == wgpu::FilterMode::Linear {\n                            wgpu::SamplerBindingType::Filtering\n                        } else {\n                            wgpu::SamplerBindingType::NonFiltering\n                        },\n                    ),\n                    count: None,\n                },\n            ],\n        });\n\n        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label: Some(\"atlas-blitter\"),\n            bind_group_layouts: &[&bind_group_layout],\n            push_constant_ranges: &[],\n        });\n\n        let vertex = crate::linkage::atlas_blit_vertex::linkage(device);\n        let fragment = crate::linkage::atlas_blit_fragment::linkage(device);\n        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n            label: Some(\"atlas-blitter\"),\n            layout: Some(&pipeline_layout),\n            vertex: wgpu::VertexState {\n                module: &vertex.module,\n                entry_point: Some(vertex.entry_point),\n                compilation_options: wgpu::PipelineCompilationOptions::default(),\n                buffers: &[],\n            },\n            primitive: wgpu::PrimitiveState {\n                topology: wgpu::PrimitiveTopology::TriangleList,\n                strip_index_format: None,\n                front_face: wgpu::FrontFace::Ccw,\n                cull_mode: None,\n                unclipped_depth: false,\n                polygon_mode: wgpu::PolygonMode::Fill,\n                conservative: false,\n            },\n            depth_stencil: None,\n            multisample: wgpu::MultisampleState::default(),\n            fragment: Some(wgpu::FragmentState {\n                module: &fragment.module,\n                entry_point: Some(fragment.entry_point),\n                compilation_options: wgpu::PipelineCompilationOptions::default(),\n                targets: &[Some(wgpu::ColorTargetState {\n                    format,\n                    blend: None,\n                    write_mask: wgpu::ColorWrites::ALL,\n                })],\n            }),\n            multiview: None,\n            cache: None,\n        });\n\n        Self {\n            pipeline: pipeline.into(),\n            bind_group_layout: bind_group_layout.into(),\n            sampler: sampler.into(),\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use crate::{\n        atlas::{shader::AtlasTextureDescriptor, TextureAddressMode},\n        context::Context,\n        geometry::Vertex,\n        material::Materials,\n        test::BlockOnFuture,\n    };\n    use glam::{UVec3, Vec2, Vec3, Vec4};\n\n    use super::*;\n\n    #[test]\n    // Ensures that textures are packed and rendered correctly.\n    fn atlas_uv_mapping() {\n        log::info!(\"{:?}\", std::env::current_dir());\n        let ctx = Context::headless(32, 32)\n            .block()\n            .with_default_atlas_texture_size(UVec3::new(1024, 1024, 2));\n        let stage = ctx\n            .new_stage()\n            .with_background_color(Vec3::splat(0.0).extend(1.0))\n            .with_bloom(false);\n        let (projection, view) = crate::camera::default_ortho2d(32.0, 32.0);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n        let dirt = AtlasImage::from_path(\"../../img/dirt.jpg\").unwrap();\n        let sandstone = AtlasImage::from_path(\"../../img/sandstone.png\").unwrap();\n        let texels = AtlasImage::from_path(\"../../test_img/atlas/uv_mapping.png\").unwrap();\n        log::info!(\"setting images\");\n        let atlas_entries = stage.set_images([dirt, sandstone, texels]).unwrap();\n        log::info!(\"  done setting images\");\n\n        let texels_entry = &atlas_entries[2];\n\n        let _rez = stage\n            .new_primitive()\n            .with_material(\n                stage\n                    .new_material()\n                    .with_albedo_texture(texels_entry)\n                    .with_has_lighting(false),\n            )\n            .with_vertices(stage.new_vertices({\n                let tl = Vertex::default()\n                    .with_position(Vec3::ZERO)\n                    .with_uv0(Vec2::ZERO);\n                let tr = Vertex::default()\n                    .with_position(Vec3::new(1.0, 0.0, 0.0))\n                    .with_uv0(Vec2::new(1.0, 0.0));\n                let bl = Vertex::default()\n                    .with_position(Vec3::new(0.0, 1.0, 0.0))\n                    .with_uv0(Vec2::new(0.0, 1.0));\n                let br = Vertex::default()\n                    .with_position(Vec3::new(1.0, 1.0, 0.0))\n                    .with_uv0(Vec2::splat(1.0));\n                [tl, bl, br, tl, br, tr]\n            }))\n            .with_transform(stage.new_transform().with_scale(Vec3::new(32.0, 32.0, 1.0)));\n\n        log::info!(\"rendering\");\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"atlas/uv_mapping.png\", img);\n    }\n\n    #[test]\n    // Ensures that textures with different wrapping modes are rendered correctly.\n    fn uv_wrapping() {\n        let icon_w = 32;\n        let icon_h = 41;\n        let sheet_w = icon_w * 3;\n        let sheet_h = icon_h * 3;\n        let w = sheet_w * 3 + 2;\n        let h = sheet_h;\n        let ctx = Context::headless(w, h).block();\n        let stage = ctx\n            .new_stage()\n            .with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));\n        let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n        let texels = AtlasImage::from_path(\"../../img/happy_mac.png\").unwrap();\n        let entries = stage.set_images(std::iter::repeat_n(texels, 3)).unwrap();\n        let clamp_tex = &entries[0];\n        let repeat_tex = &entries[1];\n        repeat_tex.set_modes(TextureModes {\n            s: TextureAddressMode::Repeat,\n            t: TextureAddressMode::Repeat,\n        });\n        let mirror_tex = &entries[2];\n        mirror_tex.set_modes(TextureModes {\n            s: TextureAddressMode::MirroredRepeat,\n            t: TextureAddressMode::MirroredRepeat,\n        });\n\n        let sheet_w = sheet_w as f32;\n        let sheet_h = sheet_h as f32;\n        let geometry = stage.new_vertices({\n            let tl = Vertex::default()\n                .with_position(Vec3::ZERO)\n                .with_uv0(Vec2::ZERO);\n            let tr = Vertex::default()\n                .with_position(Vec3::new(sheet_w, 0.0, 0.0))\n                .with_uv0(Vec2::new(3.0, 0.0));\n            let bl = Vertex::default()\n                .with_position(Vec3::new(0.0, sheet_h, 0.0))\n                .with_uv0(Vec2::new(0.0, 3.0));\n            let br = Vertex::default()\n                .with_position(Vec3::new(sheet_w, sheet_h, 0.0))\n                .with_uv0(Vec2::splat(3.0));\n            [tl, bl, br, tl, br, tr]\n        });\n        let _clamp_rez = stage\n            .new_primitive()\n            .with_vertices(&geometry)\n            .with_material(\n                stage\n                    .new_material()\n                    .with_albedo_texture(clamp_tex)\n                    .with_has_lighting(false),\n            );\n\n        let _repeat_rez = stage\n            .new_primitive()\n            .with_transform(stage.new_transform().with_translation(Vec3::new(\n                sheet_w + 1.0,\n                0.0,\n                0.0,\n            )))\n            .with_material(\n                stage\n                    .new_material()\n                    .with_albedo_texture(repeat_tex)\n                    .with_has_lighting(false),\n            )\n            .with_vertices(&geometry);\n\n        let _mirror_rez = stage\n            .new_primitive()\n            .with_transform(stage.new_transform().with_translation(Vec3::new(\n                sheet_w * 2.0 + 2.0,\n                0.0,\n                0.0,\n            )))\n            .with_material(\n                stage\n                    .new_material()\n                    .with_albedo_texture(mirror_tex)\n                    .with_has_lighting(false),\n            )\n            .with_vertices(geometry);\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"atlas/uv_wrapping.png\", img);\n    }\n\n    #[test]\n    // Ensures that textures with negative uv coords wrap correctly\n    fn negative_uv_wrapping() {\n        let icon_w = 32;\n        let icon_h = 41;\n        let sheet_w = icon_w * 3;\n        let sheet_h = icon_h * 3;\n        let w = sheet_w * 3 + 2;\n        let h = sheet_h;\n        let ctx = Context::headless(w, h).block();\n        let stage = ctx\n            .new_stage()\n            .with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));\n\n        let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n\n        let texels = AtlasImage::from_path(\"../../img/happy_mac.png\").unwrap();\n        let entries = stage.set_images(std::iter::repeat_n(texels, 3)).unwrap();\n\n        let clamp_tex = &entries[0];\n        let repeat_tex = &entries[1];\n        repeat_tex.set_modes(TextureModes {\n            s: TextureAddressMode::Repeat,\n            t: TextureAddressMode::Repeat,\n        });\n\n        let mirror_tex = &entries[2];\n        mirror_tex.set_modes(TextureModes {\n            s: TextureAddressMode::MirroredRepeat,\n            t: TextureAddressMode::MirroredRepeat,\n        });\n\n        let sheet_w = sheet_w as f32;\n        let sheet_h = sheet_h as f32;\n        let geometry = stage.new_vertices({\n            let tl = Vertex::default()\n                .with_position(Vec3::ZERO)\n                .with_uv0(Vec2::ZERO);\n            let tr = Vertex::default()\n                .with_position(Vec3::new(sheet_w, 0.0, 0.0))\n                .with_uv0(Vec2::new(-3.0, 0.0));\n            let bl = Vertex::default()\n                .with_position(Vec3::new(0.0, sheet_h, 0.0))\n                .with_uv0(Vec2::new(0.0, -3.0));\n            let br = Vertex::default()\n                .with_position(Vec3::new(sheet_w, sheet_h, 0.0))\n                .with_uv0(Vec2::splat(-3.0));\n            [tl, bl, br, tl, br, tr]\n        });\n        let _clamp_prim = stage\n            .new_primitive()\n            .with_vertices(&geometry)\n            .with_material(\n                stage\n                    .new_material()\n                    .with_albedo_texture(clamp_tex)\n                    .with_has_lighting(false),\n            );\n\n        let _repeat_rez = stage\n            .new_primitive()\n            .with_material(\n                stage\n                    .new_material()\n                    .with_albedo_texture(repeat_tex)\n                    .with_has_lighting(false),\n            )\n            .with_transform(stage.new_transform().with_translation(Vec3::new(\n                sheet_w + 1.0,\n                0.0,\n                0.0,\n            )))\n            .with_vertices(&geometry);\n\n        let _mirror_rez = stage\n            .new_primitive()\n            .with_material(\n                stage\n                    .new_material()\n                    .with_albedo_texture(mirror_tex)\n                    .with_has_lighting(false),\n            )\n            .with_transform(stage.new_transform().with_translation(Vec3::new(\n                sheet_w * 2.0 + 2.0,\n                0.0,\n                0.0,\n            )))\n            .with_vertices(&geometry);\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"atlas/negative_uv_wrapping.png\", img);\n    }\n\n    #[test]\n    fn transform_uvs_for_atlas() {\n        let mut tex = AtlasTextureDescriptor {\n            offset_px: UVec2::ZERO,\n            size_px: UVec2::ONE,\n            ..Default::default()\n        };\n        assert_eq!(Vec3::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(100)));\n        assert_eq!(Vec3::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(1)));\n        assert_eq!(Vec3::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(256)));\n        tex.offset_px = UVec2::splat(10);\n        assert_eq!(\n            Vec2::splat(0.1).extend(0.0),\n            tex.uv(Vec2::ZERO, UVec2::splat(100))\n        );\n    }\n\n    #[test]\n    fn can_load_and_read_atlas_texture_array() {\n        // tests that the atlas lays out textures in the way we expect\n        let ctx = Context::headless(100, 100)\n            .block()\n            .with_default_atlas_texture_size(UVec3::new(512, 512, 2));\n        let stage = ctx.new_stage();\n        let dirt = AtlasImage::from_path(\"../../img/dirt.jpg\").unwrap();\n        let sandstone = AtlasImage::from_path(\"../../img/sandstone.png\").unwrap();\n        let cheetah = AtlasImage::from_path(\"../../img/cheetah.jpg\").unwrap();\n        let texels = AtlasImage::from_path(\"../../img/happy_mac.png\").unwrap();\n        let _frames = stage\n            .set_images([dirt, sandstone, cheetah, texels])\n            .unwrap();\n        let materials: &Materials = stage.as_ref();\n        let img = materials.atlas().atlas_img(&ctx, 0).block();\n        img_diff::assert_img_eq(\"atlas/array0.png\", img);\n        let img = materials.atlas().atlas_img(&ctx, 1).block();\n        img_diff::assert_img_eq(\"atlas/array1.png\", img);\n    }\n\n    #[test]\n    fn upkeep_trims_the_atlas() {\n        // tests that Atlas::upkeep trims out unused images and repacks the atlas\n        let ctx = Context::headless(100, 100)\n            .block()\n            .with_default_atlas_texture_size(UVec3::new(512, 512, 2));\n        let stage = ctx.new_stage();\n        let dirt = AtlasImage::from_path(\"../../img/dirt.jpg\").unwrap();\n        let sandstone = AtlasImage::from_path(\"../../img/sandstone.png\").unwrap();\n        let cheetah = AtlasImage::from_path(\"../../img/cheetah.jpg\").unwrap();\n        let texels = AtlasImage::from_path(\"../../img/happy_mac.png\").unwrap();\n        let mut frames = stage\n            .add_images([\n                dirt,\n                sandstone,\n                cheetah,\n                texels.clone(),\n                texels.clone(),\n                texels.clone(),\n                texels.clone(),\n                texels,\n            ])\n            .unwrap();\n        let materials: &Materials = stage.as_ref();\n        assert_eq!(8, materials.atlas().len());\n\n        frames.pop();\n        frames.pop();\n        frames.pop();\n        frames.pop();\n\n        let _ = materials.atlas().upkeep(&ctx);\n        assert_eq!(4, materials.atlas().len());\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/atlas/shader.rs",
    "content": "use crabslab::{Id, Slab, SlabItem};\nuse glam::{UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};\nuse spirv_std::{image::Image2d, spirv, Sampler};\n\n/// Describes various qualities of the atlas, to be used on the GPU.\n#[derive(Clone, Copy, core::fmt::Debug, Default, PartialEq, SlabItem)]\npub struct AtlasDescriptor {\n    pub size: UVec3,\n}\n\n/// A texture inside the atlas.\n#[derive(Clone, Copy, Default, PartialEq, SlabItem, core::fmt::Debug)]\npub struct AtlasTextureDescriptor {\n    /// The top left offset of texture in the atlas.\n    pub offset_px: UVec2,\n    /// The size of the texture in the atlas.\n    pub size_px: UVec2,\n    /// The index of the layer within the atlas that this `AtlasTexture `belongs\n    /// to.\n    pub layer_index: u32,\n    /// The index of this frame within the layer.\n    pub frame_index: u32,\n    /// Various toggles of texture modes.\n    pub modes: super::TextureModes,\n}\n\nimpl AtlasTextureDescriptor {\n    /// Transform the given `uv` coordinates for this texture's address mode\n    /// and placement in the atlas of the given size.\n    pub fn uv(&self, mut uv: Vec2, atlas_size: UVec2) -> Vec3 {\n        uv.x = self.modes.s.wrap(uv.x);\n        uv.y = self.modes.t.wrap(uv.y);\n\n        // get the pixel index of the uv coordinate in terms of the original image\n        let mut px_index_s = (uv.x * self.size_px.x as f32) as u32;\n        let mut px_index_t = (uv.y * self.size_px.y as f32) as u32;\n\n        // convert the pixel index from image to atlas space\n        px_index_s += self.offset_px.x;\n        px_index_t += self.offset_px.y;\n\n        let sx = atlas_size.x as f32;\n        let sy = atlas_size.y as f32;\n        // normalize the pixels by dividing by the atlas size\n        let uv_s = px_index_s as f32 / sx;\n        let uv_t = px_index_t as f32 / sy;\n\n        Vec2::new(uv_s, uv_t).extend(self.layer_index as f32)\n    }\n\n    /// Constrain the input `clip_pos` to be within the bounds of this texture\n    /// within its atlas, in texture space.\n    pub fn constrain_clip_coords_to_texture_space(\n        &self,\n        clip_pos: Vec2,\n        atlas_size: UVec2,\n    ) -> Vec2 {\n        // Convert `clip_pos` into uv coords to figure out where in the texture\n        // this point lives\n        let input_uv = (clip_pos * Vec2::new(1.0, -1.0) + Vec2::splat(1.0)) * Vec2::splat(0.5);\n        self.uv(input_uv, atlas_size).xy()\n    }\n\n    /// Constrain the input `clip_pos` to be within the bounds of this texture\n    /// within its atlas.\n    pub fn constrain_clip_coords(&self, clip_pos: Vec2, atlas_size: UVec2) -> Vec2 {\n        let uv = self.constrain_clip_coords_to_texture_space(clip_pos, atlas_size);\n        // Convert `uv` back into clip space\n        (uv * Vec2::new(2.0, 2.0) - Vec2::splat(1.0)) * Vec2::new(1.0, -1.0)\n    }\n\n    #[cfg(cpu)]\n    /// Returns the frame of this texture as a [`wgpu::Origin3d`].\n    pub fn origin(&self) -> wgpu::Origin3d {\n        wgpu::Origin3d {\n            x: self.offset_px.x,\n            y: self.offset_px.y,\n            z: self.layer_index,\n        }\n    }\n\n    #[cfg(cpu)]\n    /// Returns the frame of this texture as a [`wgpu::Extent3d`].\n    pub fn size_as_extent(&self) -> wgpu::Extent3d {\n        wgpu::Extent3d {\n            width: self.size_px.x,\n            height: self.size_px.y,\n            depth_or_array_layers: 1,\n        }\n    }\n}\n\n#[derive(Clone, Copy, Default, SlabItem, core::fmt::Debug)]\npub struct AtlasBlittingDescriptor {\n    pub atlas_texture_id: Id<AtlasTextureDescriptor>,\n    pub atlas_desc_id: Id<AtlasDescriptor>,\n}\n\n/// Vertex shader for blitting a texture into a the frame of an\n/// [`AtlasTextureDescriptor`].\n///\n/// This is useful for copying textures of unsupported formats, or\n/// textures of different sizes.\n#[spirv(vertex)]\npub fn atlas_blit_vertex(\n    #[spirv(vertex_index)] vertex_id: u32,\n    #[spirv(instance_index)] atlas_blitting_desc_id: Id<AtlasBlittingDescriptor>,\n    #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &[u32],\n    out_uv: &mut Vec2,\n    #[spirv(position)] out_pos: &mut Vec4,\n) {\n    let i = vertex_id as usize;\n    *out_uv = crate::math::UV_COORD_QUAD_CCW[i];\n\n    crate::println!(\"atlas_blitting_desc_id: {atlas_blitting_desc_id:?}\");\n    let atlas_blitting_desc = slab.read_unchecked(atlas_blitting_desc_id);\n    crate::println!(\"atlas_blitting_desc: {atlas_blitting_desc:?}\");\n    let atlas_texture = slab.read_unchecked(atlas_blitting_desc.atlas_texture_id);\n    crate::println!(\"atlas_texture: {atlas_texture:?}\");\n    let atlas_desc = slab.read_unchecked(atlas_blitting_desc.atlas_desc_id);\n    crate::println!(\"atlas_desc: {atlas_desc:?}\");\n    let clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i];\n    crate::println!(\"clip_pos: {clip_pos:?}\");\n    *out_pos = atlas_texture\n        .constrain_clip_coords(clip_pos.xy(), atlas_desc.size.xy())\n        .extend(clip_pos.z)\n        .extend(clip_pos.w);\n    crate::println!(\"out_pos: {out_pos}\");\n}\n\n/// Fragment shader for blitting a texture into a frame of an atlas.\n#[spirv(fragment)]\npub fn atlas_blit_fragment(\n    #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,\n    #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,\n    in_uv: Vec2,\n    frag_color: &mut Vec4,\n) {\n    *frag_color = texture.sample(*sampler, in_uv);\n}\n"
  },
  {
    "path": "crates/renderling/src/atlas.rs",
    "content": "//! Texture atlas.\n//!\n//! All images are packed into an atlas at staging time.\n//! Texture descriptors describe where in the atlas an image is,\n//! and how it should sample pixels. These descriptors are packed into a buffer\n//! on the GPU. This keeps the number of texture binds to a minimum (one, in\n//! most cases).\n//!\n//! ## NOTE:\n//! `Atlas` is a temporary work around until we can use bindless techniques\n//! on web.\n//!\n//! `Atlas` is only available on CPU. Not available in shaders.\nuse crabslab::SlabItem;\n\n#[cfg(cpu)]\nmod atlas_image;\n#[cfg(cpu)]\npub use atlas_image::*;\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\npub mod shader;\n\n/// Method of addressing the edges of a texture.\n#[derive(\n    Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, SlabItem, core::fmt::Debug,\n)]\npub struct TextureModes {\n    pub s: TextureAddressMode,\n    pub t: TextureAddressMode,\n}\n\n/// Infinitely wrap the input between 0.0 and 1.0.\n///\n/// Only handles `input` >= 0.0.\npub fn repeat(mut input: f32) -> f32 {\n    let gto = input >= 1.0;\n    input %= 1.0;\n    if gto && input == 0.0 {\n        1.0\n    } else {\n        input\n    }\n}\n\n/// Clamp the input between 0.0 and 1.0.\npub fn clamp(input: f32) -> f32 {\n    if input > 1.0 {\n        1.0 - f32::EPSILON\n    } else if input < 0.0 {\n        0.0 + f32::EPSILON\n    } else {\n        input\n    }\n}\n\n/// How edges should be handled in texture addressing/wrapping.\n#[repr(u32)]\n#[derive(\n    Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, SlabItem, core::fmt::Debug,\n)]\npub enum TextureAddressMode {\n    #[default]\n    ClampToEdge,\n    Repeat,\n    MirroredRepeat,\n}\n\nimpl core::fmt::Display for TextureAddressMode {\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        f.write_str(match *self {\n            TextureAddressMode::ClampToEdge => \"clamp to edge\",\n            TextureAddressMode::Repeat => \"repeat\",\n            TextureAddressMode::MirroredRepeat => \"mirrored repeat\",\n        })\n    }\n}\n\nimpl TextureAddressMode {\n    /// Wrap the given s/t coord into a pixel index according to texture\n    /// addressing.\n    pub fn wrap(&self, input: f32) -> f32 {\n        match self {\n            TextureAddressMode::Repeat => {\n                let sign = if input >= 0.0 { 1.0f32 } else { -1.0 };\n                let input = repeat(input.abs());\n                if sign > 0.0 {\n                    input\n                } else {\n                    1.0 - input\n                }\n            }\n            TextureAddressMode::MirroredRepeat => {\n                let sign = if input >= 0.0 { 1.0f32 } else { -1.0 };\n                let i = input.abs();\n                // TODO: Remove this clippy allowance after <https://github.com/Rust-GPU/rust-gpu/pull/460>\n                // merges.\n                #[cfg_attr(\n                    cpu,\n                    expect(\n                        clippy::manual_is_multiple_of,\n                        reason = \"rust-gpu is not yet on rustc 1.91, which introduced this lint\"\n                    )\n                )]\n                let flip = ((i as u32) % 2) == 0;\n                let t = repeat(i);\n                if sign > 0.0 {\n                    if flip {\n                        t\n                    } else {\n                        1.0 - t\n                    }\n                } else if flip {\n                    1.0 - t\n                } else {\n                    t\n                }\n            }\n            _ => clamp(input),\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use glam::{UVec2, UVec3, Vec2, Vec3Swizzles, Vec4Swizzles};\n\n    use crate::atlas::shader::AtlasTextureDescriptor;\n\n    use super::*;\n\n    #[test]\n    fn can_repeat() {\n        assert_eq!(0.0, TextureAddressMode::Repeat.wrap(0.0));\n        assert_eq!(1.0, TextureAddressMode::Repeat.wrap(2.0));\n        assert_eq!(1.0, TextureAddressMode::Repeat.wrap(3.0));\n    }\n\n    #[test]\n    /// Tests that clip coordinates can be converted into texture coords within\n    /// a specific `AtlasTexture`, and back again.\n    fn constrain_clip_coords_sanity() {\n        let atlas_texture = AtlasTextureDescriptor {\n            offset_px: UVec2::splat(0),\n            size_px: UVec2::splat(800),\n            layer_index: 0,\n            frame_index: 0,\n            modes: TextureModes {\n                s: TextureAddressMode::ClampToEdge,\n                t: TextureAddressMode::ClampToEdge,\n            },\n        };\n        let atlas_size = UVec3::new(1024, 1024, 4);\n        let corners @ [tl, tr, br, bl] = [\n            crate::math::CLIP_SPACE_COORD_QUAD_CCW_TL,\n            crate::math::CLIP_SPACE_COORD_QUAD_CCW_TR,\n            crate::math::CLIP_SPACE_COORD_QUAD_CCW_BR,\n            crate::math::CLIP_SPACE_COORD_QUAD_CCW_BL,\n        ]\n        .map(|coord| {\n            atlas_texture.constrain_clip_coords_to_texture_space(coord.xy(), atlas_size.xy())\n        });\n        log::info!(\"uv_corners: {corners:#?}\");\n\n        let clip_br = crate::math::CLIP_SPACE_COORD_QUAD_CCW_BR.xy();\n        log::info!(\"clip_br: {clip_br}\");\n        let input_uv_br = (clip_br * Vec2::new(1.0, -1.0) + Vec2::splat(1.0)) * Vec2::splat(0.5);\n        log::info!(\"input_uv_br: {input_uv_br}\");\n        assert_eq!(Vec2::ONE, input_uv_br, \"incorrect uv\");\n\n        let d = 800.0 / 1024.0;\n        assert_eq!(Vec2::splat(0.0), tl, \"incorrect tl\");\n        assert_eq!(Vec2::new(d, 0.0), tr, \"incorrect tr\");\n        assert_eq!(Vec2::new(d, d), br, \"incorrect br\");\n        assert_eq!(Vec2::new(0.0, d), bl, \"incorrect bl\");\n\n        let corners = [\n            crate::math::CLIP_SPACE_COORD_QUAD_CCW_TL,\n            crate::math::CLIP_SPACE_COORD_QUAD_CCW_TR,\n            crate::math::CLIP_SPACE_COORD_QUAD_CCW_BR,\n            crate::math::CLIP_SPACE_COORD_QUAD_CCW_BL,\n        ]\n        .map(|coord| atlas_texture.constrain_clip_coords(coord.xy(), atlas_size.xy()));\n        log::info!(\"clip_corners: {corners:#?}\");\n        //     [\n        //     Vec2(\n        //         -1.0,\n        //         1.0,\n        //     ),\n        //     Vec2(\n        //         0.5625,\n        //         1.0,\n        //     ),\n        //     Vec2(\n        //         0.5625,\n        //         -0.5625,\n        //     ),\n        //     Vec2(\n        //         -1.0,\n        //         -0.5625,\n        //     ),\n        // ]\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/bindgroup.rs",
    "content": "//! A helper wrapper around `Arc<RwLock<Arc<wgpu::BindGroup>>>` that provides\n//! invalidation.\n\nuse std::sync::{Arc, RwLock};\n\n/// A [`wgpu::BindGroup`] with invalidation.\n///\n/// This struct exists to simplify the common pattern of invalidating and\n/// re-creating bindgroups.\n#[derive(Clone)]\npub struct ManagedBindGroup {\n    bindgroup: Arc<RwLock<Option<Arc<wgpu::BindGroup>>>>,\n}\n\nimpl Default for ManagedBindGroup {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl From<wgpu::BindGroup> for ManagedBindGroup {\n    fn from(value: wgpu::BindGroup) -> Self {\n        let mbg = ManagedBindGroup::new();\n        // UNWRAP: POP\n        *mbg.bindgroup.write().expect(\"bindgroup write\") = Some(value.into());\n        mbg\n    }\n}\n\nimpl ManagedBindGroup {\n    pub fn new() -> Self {\n        Self {\n            bindgroup: Arc::new(RwLock::new(None)),\n        }\n    }\n\n    pub fn get(\n        &self,\n        should_invalidate: bool,\n        fn_recreate: impl FnOnce() -> wgpu::BindGroup,\n    ) -> Arc<wgpu::BindGroup> {\n        let recreate = || {\n            let mut guard = self.bindgroup.write().expect(\"bindgroup write\");\n\n            let bg = Arc::new(fn_recreate());\n            *guard = Some(bg.clone());\n            bg\n        };\n        if should_invalidate {\n            recreate()\n        } else {\n            let maybe_buffer = self.bindgroup.read().expect(\"bindgroup read\").clone();\n            if let Some(buffer) = maybe_buffer {\n                buffer\n            } else {\n                recreate()\n            }\n        }\n    }\n\n    /// Invalidate the [`wgpu::BindGroup`], destroying it if it exists.\n    pub fn invalidate(&self) {\n        *self.bindgroup.write().expect(\"bindgroup write\") = None;\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/bloom/cpu.rs",
    "content": "//! Bloom.\nuse core::ops::Deref;\nuse std::sync::{Arc, RwLock};\n\nuse craballoc::{\n    prelude::{Hybrid, HybridArray, SlabAllocator},\n    runtime::WgpuRuntime,\n    slab::SlabBuffer,\n};\nuse crabslab::Id;\nuse glam::{UVec2, Vec2};\n\nuse crate::texture::{self, Texture};\n\nfn create_bindgroup_layout(device: &wgpu::Device, label: Option<&str>) -> wgpu::BindGroupLayout {\n    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n        label,\n        entries: &[\n            wgpu::BindGroupLayoutEntry {\n                binding: 0,\n                visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Buffer {\n                    ty: wgpu::BufferBindingType::Storage { read_only: true },\n                    has_dynamic_offset: false,\n                    min_binding_size: None,\n                },\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 1,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Texture {\n                    sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                    view_dimension: wgpu::TextureViewDimension::D2,\n                    multisampled: false,\n                },\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 2,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                count: None,\n            },\n        ],\n    })\n}\n\nfn create_bloom_downsample_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {\n    let label = Some(\"bloom downsample\");\n    let bindgroup_layout = create_bindgroup_layout(device, label);\n    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n        label,\n        bind_group_layouts: &[&bindgroup_layout],\n        push_constant_ranges: &[],\n    });\n    let vertex_module = crate::linkage::bloom_vertex::linkage(device);\n    let fragment_module = crate::linkage::bloom_downsample_fragment::linkage(device);\n    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n        label,\n        layout: Some(&layout),\n        vertex: wgpu::VertexState {\n            module: &vertex_module.module,\n            entry_point: Some(vertex_module.entry_point),\n            buffers: &[],\n            compilation_options: Default::default(),\n        },\n        primitive: wgpu::PrimitiveState {\n            topology: wgpu::PrimitiveTopology::TriangleList,\n            strip_index_format: None,\n            front_face: wgpu::FrontFace::Ccw,\n            cull_mode: None,\n            unclipped_depth: false,\n            polygon_mode: wgpu::PolygonMode::Fill,\n            conservative: false,\n        },\n        depth_stencil: None,\n        multisample: wgpu::MultisampleState::default(),\n        fragment: Some(wgpu::FragmentState {\n            module: &fragment_module.module,\n            entry_point: Some(fragment_module.entry_point),\n            targets: &[Some(wgpu::ColorTargetState {\n                format: wgpu::TextureFormat::Rgba16Float,\n                blend: None,\n                write_mask: wgpu::ColorWrites::all(),\n            })],\n            compilation_options: Default::default(),\n        }),\n        multiview: None,\n        cache: None,\n    })\n}\n\nfn create_bloom_upsample_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {\n    let label = Some(\"bloom upsample\");\n    let bindgroup_layout = create_bindgroup_layout(device, label);\n    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n        label,\n        bind_group_layouts: &[&bindgroup_layout],\n        push_constant_ranges: &[],\n    });\n    let vertex_module = crate::linkage::bloom_vertex::linkage(device);\n    let fragment_module = crate::linkage::bloom_upsample_fragment::linkage(device);\n    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n        label,\n        layout: Some(&layout),\n        vertex: wgpu::VertexState {\n            module: &vertex_module.module,\n            entry_point: Some(vertex_module.entry_point),\n            buffers: &[],\n            compilation_options: Default::default(),\n        },\n        primitive: wgpu::PrimitiveState {\n            topology: wgpu::PrimitiveTopology::TriangleList,\n            strip_index_format: None,\n            front_face: wgpu::FrontFace::Ccw,\n            cull_mode: None,\n            unclipped_depth: false,\n            polygon_mode: wgpu::PolygonMode::Fill,\n            conservative: false,\n        },\n        depth_stencil: None,\n        multisample: wgpu::MultisampleState::default(),\n        fragment: Some(wgpu::FragmentState {\n            module: &fragment_module.module,\n            entry_point: Some(fragment_module.entry_point),\n            targets: &[Some(wgpu::ColorTargetState {\n                format: wgpu::TextureFormat::Rgba16Float,\n                blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                write_mask: wgpu::ColorWrites::all(),\n            })],\n            compilation_options: Default::default(),\n        }),\n        multiview: None,\n        cache: None,\n    })\n}\n\nfn config_resolutions(resolution: UVec2) -> impl Iterator<Item = UVec2> {\n    let num_textures = resolution.x.min(resolution.y).ilog2();\n    (0..=num_textures).map(move |i| UVec2::new(resolution.x >> i, resolution.y >> i))\n}\n\nfn create_texture(\n    runtime: impl AsRef<WgpuRuntime>,\n    width: u32,\n    height: u32,\n    label: Option<&str>,\n    extra_usages: wgpu::TextureUsages,\n) -> texture::Texture {\n    let sampler = runtime\n        .as_ref()\n        .device\n        .create_sampler(&wgpu::SamplerDescriptor {\n            label,\n            address_mode_u: wgpu::AddressMode::ClampToEdge,\n            address_mode_v: wgpu::AddressMode::ClampToEdge,\n            address_mode_w: wgpu::AddressMode::ClampToEdge,\n            mag_filter: wgpu::FilterMode::Linear,\n            min_filter: wgpu::FilterMode::Linear,\n            mipmap_filter: wgpu::FilterMode::Linear,\n            ..Default::default()\n        });\n    Texture::new_with(\n        runtime,\n        label,\n        Some(\n            wgpu::TextureUsages::RENDER_ATTACHMENT\n                | wgpu::TextureUsages::TEXTURE_BINDING\n                | wgpu::TextureUsages::COPY_SRC\n                | extra_usages,\n        ),\n        Some(sampler),\n        wgpu::TextureFormat::Rgba16Float,\n        4,\n        2,\n        width,\n        height,\n        1,\n        &[],\n    )\n}\n\nfn create_textures(runtime: impl AsRef<WgpuRuntime>, resolution: UVec2) -> Vec<texture::Texture> {\n    let resolutions = config_resolutions(resolution).collect::<Vec<_>>();\n    log::trace!(\n        \"creating {} bloom textures at resolution {resolution}\",\n        resolutions.len()\n    );\n    let mut textures = vec![];\n    for (\n        i,\n        UVec2 {\n            x: width,\n            y: height,\n        },\n    ) in resolutions.into_iter().skip(1).enumerate()\n    {\n        let title = format!(\"bloom texture[{i}]\");\n        let label = Some(title.as_str());\n        let texture = create_texture(\n            runtime.as_ref(),\n            width,\n            height,\n            label,\n            wgpu::TextureUsages::empty(),\n        );\n        textures.push(texture);\n    }\n    textures\n}\n\nfn create_bindgroup(\n    device: &wgpu::Device,\n    layout: &wgpu::BindGroupLayout,\n    buffer: &wgpu::Buffer,\n    tex: &Texture,\n) -> wgpu::BindGroup {\n    let label = Some(\"bloom\");\n    device.create_bind_group(&wgpu::BindGroupDescriptor {\n        label,\n        layout,\n        entries: &[\n            wgpu::BindGroupEntry {\n                binding: 0,\n                resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()),\n            },\n            wgpu::BindGroupEntry {\n                binding: 1,\n                resource: wgpu::BindingResource::TextureView(&tex.view),\n            },\n            wgpu::BindGroupEntry {\n                binding: 2,\n                resource: wgpu::BindingResource::Sampler(&tex.sampler),\n            },\n        ],\n    })\n}\n\nfn create_bindgroups(\n    device: &wgpu::Device,\n    pipeline: &wgpu::RenderPipeline,\n    buffer: &wgpu::Buffer,\n    textures: &[Texture],\n) -> Vec<wgpu::BindGroup> {\n    let layout = pipeline.get_bind_group_layout(0);\n    textures\n        .iter()\n        .map(|tex| create_bindgroup(device, &layout, buffer, tex))\n        .collect()\n}\n\nfn create_mix_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {\n    let label = Some(\"bloom mix\");\n    let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n        label,\n        entries: &[\n            wgpu::BindGroupLayoutEntry {\n                binding: 0,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Buffer {\n                    ty: wgpu::BufferBindingType::Storage { read_only: true },\n                    has_dynamic_offset: false,\n                    min_binding_size: None,\n                },\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 1,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Texture {\n                    sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                    view_dimension: wgpu::TextureViewDimension::D2,\n                    multisampled: false,\n                },\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 2,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 3,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Texture {\n                    sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                    view_dimension: wgpu::TextureViewDimension::D2,\n                    multisampled: false,\n                },\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 4,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                count: None,\n            },\n        ],\n    });\n    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n        label,\n        bind_group_layouts: &[&bindgroup_layout],\n        push_constant_ranges: &[],\n    });\n    let vertex_module = crate::linkage::bloom_vertex::linkage(device);\n    let fragment_module = crate::linkage::bloom_mix_fragment::linkage(device);\n    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n        label,\n        layout: Some(&layout),\n        vertex: wgpu::VertexState {\n            module: &vertex_module.module,\n            entry_point: Some(vertex_module.entry_point),\n            buffers: &[],\n            compilation_options: Default::default(),\n        },\n        primitive: wgpu::PrimitiveState {\n            topology: wgpu::PrimitiveTopology::TriangleList,\n            strip_index_format: None,\n            front_face: wgpu::FrontFace::Ccw,\n            cull_mode: None,\n            unclipped_depth: false,\n            polygon_mode: wgpu::PolygonMode::Fill,\n            conservative: false,\n        },\n        depth_stencil: None,\n        multisample: wgpu::MultisampleState::default(),\n        fragment: Some(wgpu::FragmentState {\n            module: &fragment_module.module,\n            entry_point: Some(fragment_module.entry_point),\n            targets: &[Some(wgpu::ColorTargetState {\n                format: wgpu::TextureFormat::Rgba16Float,\n                blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                write_mask: wgpu::ColorWrites::all(),\n            })],\n            compilation_options: Default::default(),\n        }),\n        multiview: None,\n        cache: None,\n    })\n}\n\nfn create_mix_bindgroup(\n    device: &wgpu::Device,\n    pipeline: &wgpu::RenderPipeline,\n    slab_buffer: &wgpu::Buffer,\n    hdr_texture: &Texture,\n    bloom_texture: &Texture,\n) -> wgpu::BindGroup {\n    device.create_bind_group(&wgpu::BindGroupDescriptor {\n        label: Some(\"bloom mix\"),\n        layout: &pipeline.get_bind_group_layout(0),\n        entries: &[\n            wgpu::BindGroupEntry {\n                binding: 0,\n                resource: wgpu::BindingResource::Buffer(slab_buffer.as_entire_buffer_binding()),\n            },\n            wgpu::BindGroupEntry {\n                binding: 1,\n                resource: wgpu::BindingResource::TextureView(&hdr_texture.view),\n            },\n            wgpu::BindGroupEntry {\n                binding: 2,\n                resource: wgpu::BindingResource::Sampler(&hdr_texture.sampler),\n            },\n            wgpu::BindGroupEntry {\n                binding: 3,\n                resource: wgpu::BindingResource::TextureView(&bloom_texture.view),\n            },\n            wgpu::BindGroupEntry {\n                binding: 4,\n                resource: wgpu::BindingResource::Sampler(&bloom_texture.sampler),\n            },\n        ],\n    })\n}\n\n/// Performs a \"physically based\" bloom effect on a texture. CPU only.\n///\n/// Contains pipelines, down/upsampling textures, a buffer\n/// to communicate configuration to the shaders, and bindgroups.\n///\n/// Clones of [`Bloom`] all point to the same resources.\n#[derive(Clone)]\npub struct Bloom {\n    slab: SlabAllocator<WgpuRuntime>,\n    slab_buffer: SlabBuffer<wgpu::Buffer>,\n\n    downsample_pixel_sizes: HybridArray<Vec2>,\n    downsample_pipeline: Arc<wgpu::RenderPipeline>,\n\n    upsample_filter_radius: Hybrid<Vec2>,\n    upsample_pipeline: Arc<wgpu::RenderPipeline>,\n\n    textures: Arc<RwLock<Vec<texture::Texture>>>,\n    bindgroups: Arc<RwLock<Vec<wgpu::BindGroup>>>,\n    hdr_texture_downsample_bindgroup: Arc<RwLock<wgpu::BindGroup>>,\n\n    mix_pipeline: Arc<wgpu::RenderPipeline>,\n    mix_bindgroup: Arc<RwLock<wgpu::BindGroup>>,\n    mix_texture: Arc<RwLock<Texture>>,\n    mix_strength: Hybrid<f32>,\n}\n\nimpl Bloom {\n    pub fn new(runtime: impl AsRef<WgpuRuntime>, hdr_texture: &Texture) -> Self {\n        let runtime = runtime.as_ref();\n        let resolution = UVec2::new(hdr_texture.width(), hdr_texture.height());\n\n        let slab = SlabAllocator::new(runtime, \"bloom-slab\", wgpu::BufferUsages::empty());\n        let downsample_pixel_sizes = slab.new_array(\n            config_resolutions(resolution).map(|r| 1.0 / Vec2::new(r.x as f32, r.y as f32)),\n        );\n        let upsample_filter_radius =\n            slab.new_value(1.0 / Vec2::new(resolution.x as f32, resolution.y as f32));\n        let mix_strength = slab.new_value(0.04f32);\n        let slab_buffer = slab.commit();\n\n        let downsample_pipeline = Arc::new(create_bloom_downsample_pipeline(&runtime.device));\n        let upsample_pipeline = Arc::new(create_bloom_upsample_pipeline(&runtime.device));\n        let mix_pipeline = Arc::new(create_mix_pipeline(&runtime.device));\n\n        let hdr_texture_downsample_bindgroup = create_bindgroup(\n            &runtime.device,\n            &downsample_pipeline.get_bind_group_layout(0),\n            &slab_buffer,\n            hdr_texture,\n        );\n        let mix_texture = create_texture(\n            runtime,\n            resolution.x,\n            resolution.y,\n            Some(\"bloom mix\"),\n            wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,\n        );\n\n        let textures = create_textures(runtime, resolution);\n        let bindgroups = create_bindgroups(\n            &runtime.device,\n            &downsample_pipeline,\n            &slab_buffer,\n            &textures,\n        );\n\n        let mix_bindgroup = create_mix_bindgroup(\n            &runtime.device,\n            &mix_pipeline,\n            &slab_buffer,\n            hdr_texture,\n            &textures[0],\n        );\n\n        Self {\n            slab,\n            slab_buffer,\n            downsample_pixel_sizes,\n            downsample_pipeline,\n            upsample_filter_radius,\n            upsample_pipeline,\n            textures: Arc::new(RwLock::new(textures)),\n            bindgroups: Arc::new(RwLock::new(bindgroups)),\n            hdr_texture_downsample_bindgroup: Arc::new(RwLock::new(\n                hdr_texture_downsample_bindgroup,\n            )),\n            mix_pipeline,\n            mix_texture: Arc::new(RwLock::new(mix_texture)),\n            mix_bindgroup: Arc::new(RwLock::new(mix_bindgroup)),\n            mix_strength,\n        }\n    }\n\n    pub(crate) fn slab_allocator(&self) -> &SlabAllocator<WgpuRuntime> {\n        &self.slab\n    }\n\n    pub fn set_mix_strength(&self, strength: f32) {\n        self.mix_strength.set(strength);\n    }\n\n    pub fn get_mix_strength(&self) -> f32 {\n        self.mix_strength.get()\n    }\n\n    /// Set the filter radius in pixels.\n    pub fn set_filter_radius(&self, filter_radius: f32) {\n        let size = self.get_size();\n        let filter_radius = Vec2::new(filter_radius / size.x as f32, filter_radius / size.y as f32);\n        self.upsample_filter_radius.set(filter_radius);\n    }\n\n    pub fn get_filter_radius(&self) -> Vec2 {\n        self.upsample_filter_radius.get()\n    }\n\n    pub fn get_size(&self) -> UVec2 {\n        let mix_texture = self.get_mix_texture();\n        UVec2::new(mix_texture.width(), mix_texture.height())\n    }\n\n    /// Recreates this bloom using the new HDR texture.\n    pub fn set_hdr_texture(&self, runtime: impl AsRef<WgpuRuntime>, hdr_texture: &Texture) {\n        // UNWRAP: panic on purpose (here and on till the end of this fn)\n        let slab_buffer = self.slab.get_buffer().unwrap();\n        let resolution = UVec2::new(hdr_texture.width(), hdr_texture.height());\n        let runtime = runtime.as_ref();\n        let textures = create_textures(runtime, resolution);\n\n        *self.bindgroups.write().expect(\"bloom bindgroups write\") = create_bindgroups(\n            &runtime.device,\n            &self.downsample_pipeline,\n            &slab_buffer,\n            &textures,\n        );\n        *self\n            .hdr_texture_downsample_bindgroup\n            .write()\n            .expect(\"bloom hdr downsample bindgroup write\") = create_bindgroup(\n            &runtime.device,\n            &self.downsample_pipeline.get_bind_group_layout(0),\n            &slab_buffer,\n            hdr_texture,\n        );\n        *self.mix_texture.write().expect(\"bloom mix_texture write\") = create_texture(\n            runtime,\n            resolution.x,\n            resolution.y,\n            Some(\"bloom mix\"),\n            wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,\n        );\n        *self\n            .mix_bindgroup\n            .write()\n            .expect(\"bloom mix_bindgroup write\") = create_mix_bindgroup(\n            &runtime.device,\n            &self.mix_pipeline,\n            &slab_buffer,\n            hdr_texture,\n            &textures[0],\n        );\n        *self.textures.write().expect(\"bloom textures write\") = textures;\n    }\n\n    /// Returns a clone of the current mix texture.\n    ///\n    /// The mix texture is the result of mixing the bloom by the hdr using the\n    /// mix strength.\n    pub fn get_mix_texture(&self) -> Texture {\n        // UNWRAP: not safe but we want to panic\n        self.mix_texture\n            .read()\n            .expect(\"bloom mix_texture read\")\n            .clone()\n    }\n\n    pub(crate) fn render_downsamples(&self, device: &wgpu::Device, queue: &wgpu::Queue) {\n        struct DownsampleItem<'a> {\n            view: &'a wgpu::TextureView,\n            bindgroup: &'a wgpu::BindGroup,\n            pixel_size: Id<Vec2>,\n        }\n        // Get all the bindgroups (which are what we're reading from),\n        // starting with the hdr frame.\n        // Since `bindgroups` are one element greater (we pushed `hdr_texture_bindgroup`\n        // to the front) the last bindgroup will not be used, which is good - we\n        // don't need to read from the smallest texture during downsampling.\n        // UNWRAP: not safe but we want to panic\n        let textures_guard = self.textures.read().expect(\"bloom textures read\");\n        let hdr_texture_downsample_bindgroup_guard = self\n            .hdr_texture_downsample_bindgroup\n            .read()\n            .expect(\"bloom hdr downsample bindgroup read\");\n        let hdr_texture_downsample_bindgroup: &wgpu::BindGroup =\n            &hdr_texture_downsample_bindgroup_guard;\n        let bindgroups_guard = self.bindgroups.read().expect(\"bloom bindgroups read\");\n        let bindgroups =\n            std::iter::once(hdr_texture_downsample_bindgroup).chain(bindgroups_guard.iter());\n        let items = textures_guard\n            .iter()\n            .zip(bindgroups)\n            .zip(self.downsample_pixel_sizes.array().iter())\n            .map(|((tex, bindgroup), pixel_size)| DownsampleItem {\n                view: &tex.view,\n                bindgroup,\n                pixel_size,\n            });\n        for (\n            i,\n            DownsampleItem {\n                view,\n                bindgroup,\n                pixel_size,\n            },\n        ) in items.enumerate()\n        {\n            let title = format!(\"bloom downsample {i}\");\n            let label = Some(title.as_str());\n            let mut encoder =\n                device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });\n            {\n                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                    label,\n                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                        view,\n                        resolve_target: None,\n                        ops: wgpu::Operations {\n                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),\n                            store: wgpu::StoreOp::Store,\n                        },\n                        depth_slice: None,\n                    })],\n                    depth_stencil_attachment: None,\n                    timestamp_writes: None,\n                    occlusion_query_set: None,\n                });\n                render_pass.set_pipeline(&self.downsample_pipeline);\n                render_pass.set_bind_group(0, Some(bindgroup), &[]);\n                let id = pixel_size.into();\n                render_pass.draw(0..6, id..id + 1);\n            }\n            queue.submit(std::iter::once(encoder.finish()));\n        }\n    }\n\n    fn render_upsamples(&self, device: &wgpu::Device, queue: &wgpu::Queue) {\n        struct UpsampleItem<'a> {\n            view: &'a wgpu::TextureView,\n            bindgroup: &'a wgpu::BindGroup,\n        }\n        // Get all the bindgroups (which are what we're reading from),\n        // starting with the last mip.\n        // UNWRAP: not safe but we want to panic\n        let bindgroups_guard = self.bindgroups.read().expect(\"bloom bindgroups read\");\n        let bindgroups = bindgroups_guard.iter().rev();\n        // Get all the texture views (which are what we're writing to),\n        // starting with the second-to-last mip.\n        let textures_guard = self.textures.read().expect(\"bloom textures read\");\n        let views = textures_guard.iter().rev().skip(1).map(|t| &t.view);\n        let items = bindgroups\n            .zip(views)\n            .map(|(bindgroup, view)| UpsampleItem { view, bindgroup });\n        for (i, UpsampleItem { view, bindgroup }) in items.enumerate() {\n            let title = format!(\"bloom upsample {}\", textures_guard.len() - i - 1);\n            let label = Some(title.as_str());\n            let mut encoder =\n                device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });\n            {\n                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                    label,\n                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                        view,\n                        resolve_target: None,\n                        ops: wgpu::Operations {\n                            load: wgpu::LoadOp::Load,\n                            store: wgpu::StoreOp::Store,\n                        },\n                        depth_slice: None,\n                    })],\n                    depth_stencil_attachment: None,\n                    timestamp_writes: None,\n                    occlusion_query_set: None,\n                });\n                render_pass.set_pipeline(&self.upsample_pipeline);\n                render_pass.set_bind_group(0, Some(bindgroup), &[]);\n                let id = self.upsample_filter_radius.id().into();\n                render_pass.draw(0..6, id..id + 1);\n            }\n            queue.submit(std::iter::once(encoder.finish()));\n        }\n    }\n\n    fn render_mix(&self, device: &wgpu::Device, queue: &wgpu::Queue) {\n        let label = Some(\"bloom mix\");\n        // UNWRAP: not safe but we want to panic\n        let mix_texture = self.mix_texture.read().expect(\"bloom mix_texture read\");\n        let mix_bindgroup = self.mix_bindgroup.read().expect(\"bloom mix_bindgroup read\");\n        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });\n        {\n            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                label,\n                color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                    view: &mix_texture.view,\n                    resolve_target: None,\n                    ops: wgpu::Operations {\n                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),\n                        store: wgpu::StoreOp::Store,\n                    },\n                    depth_slice: None,\n                })],\n                depth_stencil_attachment: None,\n                timestamp_writes: None,\n                occlusion_query_set: None,\n            });\n            render_pass.set_pipeline(&self.mix_pipeline);\n            render_pass.set_bind_group(0, Some(mix_bindgroup.deref()), &[]);\n            let id = self.mix_strength.id().into();\n            render_pass.draw(0..6, id..id + 1);\n        }\n\n        queue.submit(std::iter::once(encoder.finish()));\n    }\n\n    pub fn bloom(&self, device: &wgpu::Device, queue: &wgpu::Queue) {\n        self.slab.commit();\n        assert!(\n            self.slab_buffer.is_valid(),\n            \"bloom slab buffer should never resize\"\n        );\n        self.render_downsamples(device, queue);\n        self.render_upsamples(device, queue);\n        self.render_mix(device, queue);\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use glam::Vec3;\n\n    use crate::{context::Context, test::BlockOnFuture};\n\n    use super::*;\n\n    #[test]\n    fn bloom_texture_sizes_sanity() {\n        let sizes = config_resolutions(UVec2::new(1024, 800)).collect::<Vec<_>>();\n        assert_eq!(\n            vec![\n                UVec2::new(1024, 800),\n                UVec2::new(512, 400),\n                UVec2::new(256, 200),\n                UVec2::new(128, 100),\n                UVec2::new(64, 50),\n                UVec2::new(32, 25),\n                UVec2::new(16, 12),\n                UVec2::new(8, 6),\n                UVec2::new(4, 3),\n                UVec2::new(2, 1),\n            ],\n            sizes\n        );\n        let pixel_sizes = config_resolutions(UVec2::new(1024, 800))\n            .map(|r| 1.0 / Vec2::new(r.x as f32, r.y as f32))\n            .collect::<Vec<_>>();\n        assert_eq!(\n            vec![\n                Vec2::new(0.0009765625, 0.00125),\n                Vec2::new(0.001953125, 0.0025),\n                Vec2::new(0.00390625, 0.005),\n                Vec2::new(0.0078125, 0.01),\n                Vec2::new(0.015625, 0.02),\n                Vec2::new(0.03125, 0.04),\n                Vec2::new(0.0625, 0.083333336),\n                Vec2::new(0.125, 0.16666667),\n                Vec2::new(0.25, 0.33333334),\n                Vec2::new(0.5, 1.0)\n            ],\n            pixel_sizes\n        );\n    }\n\n    #[test]\n    fn bloom_sanity() {\n        let width = 256;\n        let height = 128;\n        let ctx = Context::headless(width, height).block();\n        let stage = ctx.new_stage().with_bloom(false);\n        let projection = crate::camera::perspective(width as f32, height as f32);\n        let view = crate::camera::look_at(Vec3::new(0.0, 2.0, 18.0), Vec3::ZERO, Vec3::Y);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n        let skybox = stage\n            .new_skybox_from_path(\"../../img/hdr/night.hdr\")\n            .unwrap();\n        stage.use_skybox(&skybox);\n        let ibl = stage.new_ibl(&skybox);\n        stage.use_ibl(&ibl);\n\n        let _doc = stage\n            .load_gltf_document_from_path(\"../../gltf/EmissiveStrengthTest.glb\")\n            .unwrap();\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"bloom/without.png\", img);\n        frame.present();\n\n        // now render the whole thing with default values\n        stage.set_has_bloom(true);\n        stage.set_bloom_mix_strength(0.1);\n        stage.set_bloom_filter_radius(2.0);\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"bloom/with.png\", img);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/bloom/shader.rs",
    "content": "use crabslab::{Id, Slab};\nuse glam::{Vec2, Vec4, Vec4Swizzles};\nuse spirv_std::{image::Image2d, spirv, Sampler};\n\n/// Bloom vertex shader.\n///\n/// This is a pass-through vertex shader to facilitate a bloom effect.\n#[spirv(vertex)]\npub fn bloom_vertex(\n    #[spirv(vertex_index)] vertex_index: u32,\n    #[spirv(instance_index)] in_id: u32,\n    out_uv: &mut Vec2,\n    #[spirv(flat)] out_id: &mut u32,\n    #[spirv(position)] out_clip_pos: &mut Vec4,\n) {\n    let i = (vertex_index % 6) as usize;\n    *out_uv = crate::math::UV_COORD_QUAD_CCW[i];\n    *out_clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i];\n    *out_id = in_id;\n}\n\n/// Bloom downsampling shader.\n///\n/// Performs successive downsampling from a source texture.\n///\n/// As taken from Call Of Duty method - presented at ACM Siggraph 2014.\n///\n/// This particular method was designed to eliminate\n/// \"pulsating artifacts and temporal stability issues\".\n#[spirv(fragment)]\npub fn bloom_downsample_fragment(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n    // Remember to add bilinear minification filter for this texture!\n    // Remember to use a floating-point texture format (for HDR)!\n    // Remember to use edge clamping for this texture!\n    #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,\n    #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,\n    in_uv: Vec2,\n    #[spirv(flat)] in_pixel_size_id: Id<Vec2>,\n    // frag_color\n    downsample: &mut Vec4,\n) {\n    use glam::Vec3;\n\n    let Vec2 { x, y } = slab.read(in_pixel_size_id);\n\n    // Take 13 samples around current texel:\n    // a - b - c\n    // - j - k -\n    // d - e - f\n    // - l - m -\n    // g - h - i\n    // === ('e' is the current texel) ===\n    let a = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y + 2.0 * y));\n    let b = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y + 2.0 * y));\n    let c = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y + 2.0 * y));\n\n    let d = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y));\n    let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y));\n    let f = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y));\n\n    let g = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y - 2.0 * y));\n    let h = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y - 2.0 * y));\n    let i = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y - 2.0 * y));\n\n    let j = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y));\n    let k = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y));\n    let l = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y));\n    let m = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y));\n\n    // Apply weighted distribution:\n    // 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1\n    // a,b,d,e * 0.125\n    // b,c,e,f * 0.125\n    // d,e,g,h * 0.125\n    // e,f,h,i * 0.125\n    // j,k,l,m * 0.5\n    // This shows 5 square areas that are being sampled. But some of them overlap,\n    // so to have an energy preserving downsample we need to make some adjustments.\n    // The weights are the distributed so that the sum of j,k,l,m (e.g.)\n    // contribute 0.5 to the final color output. The code below is written\n    // to effectively yield this sum. We get:\n    // 0.125*5 + 0.03125*4 + 0.0625*4 = 1\n    let f1 = 0.125;\n    let f2 = 0.0625;\n    let f3 = 0.03125;\n    let center = e * f1;\n    let inner = (j + k + l + m) * f1;\n    let outer = (b + d + h + f) * f2;\n    let furthest = (a + c + g + i) * f3;\n    let min = Vec3::splat(f32::EPSILON).extend(1.0);\n    *downsample = (center + inner + outer + furthest).max(min);\n}\n\n/// Bloom upsampling shader.\n///\n/// This shader performs successive upsampling on a source texture.\n///\n/// Taken from Call Of Duty method, presented at ACM Siggraph 2014.\n#[spirv(fragment)]\npub fn bloom_upsample_fragment(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n    // Remember to add bilinear minification filter for this texture!\n    // Remember to use a floating-point texture format (for HDR)!\n    // Remember to use edge clamping for this texture!\n    #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,\n    #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,\n    in_uv: Vec2,\n    #[spirv(flat)] filter_radius_id: Id<Vec2>,\n    // frag_color\n    upsample: &mut Vec4,\n) {\n    // The filter kernel is applied with a radius, specified in texture\n    // coordinates, so that the radius will vary across mip resolutions.\n    let Vec2 { x, y } = slab.read(filter_radius_id);\n\n    // Take 9 samples around current texel:\n    // a - b - c\n    // d - e - f\n    // g - h - i\n    // === ('e' is the current texel) ===\n    let a = texture\n        .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y))\n        .xyz();\n    let b = texture\n        .sample(*sampler, Vec2::new(in_uv.x, in_uv.y + y))\n        .xyz();\n    let c = texture\n        .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y))\n        .xyz();\n\n    let d = texture\n        .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y))\n        .xyz();\n    let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y)).xyz();\n    let f = texture\n        .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y))\n        .xyz();\n\n    let g = texture\n        .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y))\n        .xyz();\n    let h = texture\n        .sample(*sampler, Vec2::new(in_uv.x, in_uv.y - y))\n        .xyz();\n    let i = texture\n        .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y))\n        .xyz();\n\n    // Apply weighted distribution, by using a 3x3 tent filter:\n    //  1   | 1 2 1 |\n    // -- * | 2 4 2 |\n    // 16   | 1 2 1 |\n    let mut sample = e * 4.0;\n    sample += (b + d + f + h) * 2.0;\n    sample += a + c + g + i;\n    sample *= 1.0 / 16.0;\n    *upsample = sample.extend(0.5);\n}\n\n#[spirv(fragment)]\n#[allow(clippy::too_many_arguments)]\n/// Bloom \"mix\" shader.\n///\n/// This is the final step in applying bloom in which the computed bloom is\n/// mixed with the source texture according to a strength factor.\npub fn bloom_mix_fragment(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n    #[spirv(descriptor_set = 0, binding = 1)] hdr_texture: &Image2d,\n    #[spirv(descriptor_set = 0, binding = 2)] hdr_sampler: &Sampler,\n    #[spirv(descriptor_set = 0, binding = 3)] bloom_texture: &Image2d,\n    #[spirv(descriptor_set = 0, binding = 4)] bloom_sampler: &Sampler,\n    in_uv: Vec2,\n    #[spirv(flat)] in_bloom_strength_id: Id<f32>,\n    frag_color: &mut Vec4,\n) {\n    let bloom_strength = slab.read(in_bloom_strength_id);\n    let hdr = hdr_texture.sample(*hdr_sampler, in_uv).xyz();\n    let bloom = bloom_texture.sample(*bloom_sampler, in_uv).xyz();\n    let color = hdr.lerp(bloom, bloom_strength);\n    *frag_color = color.extend(1.0)\n}\n"
  },
  {
    "path": "crates/renderling/src/bloom.rs",
    "content": "//! Physically based bloom.\n//!\n//! As described in [learnopengl.com's Physically Based Bloom\n//! article](https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom).\n#[cfg(not(target_arch = \"spirv\"))]\nmod cpu;\n#[cfg(not(target_arch = \"spirv\"))]\npub use cpu::*;\n\npub mod shader;\n"
  },
  {
    "path": "crates/renderling/src/build.rs",
    "content": "//! Generates linkage for shaders and sets up cfg aliases.\n\nfn main() {\n    if std::env::var(\"CARGO_CFG_TARGET_ARCH\").as_deref() != Ok(\"spirv\") {\n        let paths = renderling_build::RenderlingPaths::new();\n        if let Some(paths) = paths {\n            paths.generate_linkage(true, true, None);\n        }\n    }\n\n    cfg_aliases::cfg_aliases! {\n        cpu: { not(target_arch = \"spirv\") },\n        gpu: { target_arch = \"spirv\" },\n        gltf: { feature = \"gltf\" }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/bvol.rs",
    "content": "//! Bounding volumes and culling primitives.\n//!\n//! The initial implementation here was gleaned from `treeculler`, which\n//! unfortunately cannot compile to SPIR-V because of its use of `u8`.\n//!\n//! Also, here we use `glam`, whereas `treeculler` uses its own internal\n//! primitives.\n//!\n//! More resources:\n//! * <https://fgiesen.wordpress.com/2010/10/17/view-frustum-culling/>\n//! * <http://old.cescg.org/CESCG-2002/DSykoraJJelinek/>\n//! * <https://iquilezles.org/www/articles/frustumcorrect/frustumcorrect.htm>\n\nuse crabslab::SlabItem;\nuse glam::{Mat4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};\n#[cfg(gpu)]\nuse spirv_std::num_traits::Float;\n\nuse crate::{camera::shader::CameraDescriptor, transform::shader::TransformDescriptor};\n\n/// Normalize a plane.\npub fn normalize_plane(mut plane: Vec4) -> Vec4 {\n    let normal_magnitude = (plane.x.powi(2) + plane.y.powi(2) + plane.z.powi(2))\n        .sqrt()\n        .max(f32::EPSILON);\n    plane.x /= normal_magnitude;\n    plane.y /= normal_magnitude;\n    plane.z /= normal_magnitude;\n    plane.w /= normal_magnitude;\n    plane\n}\n\n/// Find the intersection point of three planes.\n///\n/// # Notes\n/// This assumes that the planes will not intersect in a line.\npub fn intersect_planes(p0: &Vec4, p1: &Vec4, p2: &Vec4) -> Vec3 {\n    let bxc = p1.xyz().cross(p2.xyz());\n    let cxa = p2.xyz().cross(p0.xyz());\n    let axb = p0.xyz().cross(p1.xyz());\n    let r = -bxc * p0.w - cxa * p1.w - axb * p2.w;\n    r * (1.0 / bxc.dot(p0.xyz()))\n}\n\n/// Calculates distance between plane and point\npub fn dist_bpp(plane: &Vec4, point: Vec3) -> f32 {\n    plane.x * point.x + plane.y * point.y + plane.z * point.z + plane.w\n}\n\n/// Calculates the most inside vertex of an AABB.\npub fn mi_vertex(plane: &Vec4, aabb: &Aabb) -> Vec3 {\n    Vec3::new(\n        if plane.x >= 0.0 {\n            aabb.max.x\n        } else {\n            aabb.min.x\n        },\n        if plane.y >= 0.0 {\n            aabb.max.y\n        } else {\n            aabb.min.y\n        },\n        if plane.z >= 0.0 {\n            aabb.max.z\n        } else {\n            aabb.min.z\n        },\n    )\n}\n\n/// Calculates the most outside vertex of an AABB.\npub fn mo_vertex(plane: &Vec4, aabb: &Aabb) -> Vec3 {\n    Vec3::new(\n        if plane.x >= 0.0 {\n            aabb.min.x\n        } else {\n            aabb.max.x\n        },\n        if plane.y >= 0.0 {\n            aabb.min.y\n        } else {\n            aabb.max.y\n        },\n        if plane.z >= 0.0 {\n            aabb.min.z\n        } else {\n            aabb.max.z\n        },\n    )\n}\n\n/// Axis aligned bounding box.\n#[derive(Clone, Copy, Debug, Default, PartialEq, SlabItem)]\npub struct Aabb {\n    pub min: Vec3,\n    pub max: Vec3,\n}\n\nimpl From<(Vec3, Vec3)> for Aabb {\n    fn from((a, b): (Vec3, Vec3)) -> Self {\n        Aabb::new(a, b)\n    }\n}\n\nimpl Aabb {\n    pub fn new(a: Vec3, b: Vec3) -> Self {\n        Self {\n            min: a.min(b),\n            max: a.max(b),\n        }\n    }\n\n    /// Return the length along the x axis.\n    pub fn width(&self) -> f32 {\n        self.max.x - self.min.x\n    }\n\n    /// Return the length along the y axis.\n    pub fn height(&self) -> f32 {\n        self.max.y - self.min.y\n    }\n\n    /// Return the length along the z axis.\n    pub fn depth(&self) -> f32 {\n        self.max.z - self.min.z\n    }\n\n    pub fn center(&self) -> Vec3 {\n        (self.min + self.max) * 0.5\n    }\n\n    pub fn extents(&self) -> Vec3 {\n        self.max - self.center()\n    }\n\n    pub fn diagonal_length(&self) -> f32 {\n        self.min.distance(self.max)\n    }\n\n    pub fn is_zero(&self) -> bool {\n        self.min == self.max\n    }\n\n    /// Returns the union of the two [`Aabb`]s.\n    pub fn union(a: Self, b: Self) -> Self {\n        Aabb {\n            min: a.min.min(a.max).min(b.min).min(b.max),\n            max: a.max.max(a.min).max(b.max).max(b.min),\n        }\n    }\n\n    /// Determines whether this `Aabb` can be seen by `camera` after being\n    /// transformed by `transform`.\n    pub fn is_outside_camera_view(\n        &self,\n        camera: &CameraDescriptor,\n        transform: TransformDescriptor,\n    ) -> bool {\n        let transform = Mat4::from(transform);\n        let min = transform.transform_point3(self.min);\n        let max = transform.transform_point3(self.max);\n        Aabb::new(min, max).is_inside_frustum(camera.frustum())\n    }\n\n    #[cfg(not(target_arch = \"spirv\"))]\n    /// Return a triangle mesh connecting this `Aabb`'s corners.\n    ///\n    /// ```ignore\n    ///    y           1_____2     _____\n    ///    |           /    /|    /|    |  (same box, left and front sides removed)\n    ///    |___x     0/___3/ |   /7|____|6\n    ///   /           |    | /   | /    /\n    /// z/            |____|/   4|/____/5\n    ///\n    /// 7 is min\n    /// 3 is max\n    /// ```\n    pub fn get_mesh(&self) -> Vec<(Vec3, Vec3)> {\n        let p0 = Vec3::new(self.min.x, self.max.y, self.max.z);\n        let p1 = Vec3::new(self.min.x, self.max.y, self.min.z);\n        let p2 = Vec3::new(self.max.x, self.max.y, self.min.z);\n        let p3 = Vec3::new(self.max.x, self.max.y, self.max.z);\n        let p4 = Vec3::new(self.min.x, self.min.y, self.max.z);\n        let p7 = Vec3::new(self.min.x, self.min.y, self.min.z);\n        let p6 = Vec3::new(self.max.x, self.min.y, self.min.z);\n        let p5 = Vec3::new(self.max.x, self.min.y, self.max.z);\n\n        let positions = crate::math::convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7]);\n        positions\n            .chunks_exact(3)\n            .flat_map(|chunk| match chunk {\n                [a, b, c] => {\n                    let n = crate::math::triangle_face_normal(*a, *b, *c);\n                    [(*a, n), (*b, n), (*c, n)]\n                }\n                _ => unreachable!(),\n            })\n            .collect()\n    }\n\n    /// Returns whether this `Aabb` intersects another `Aabb`.\n    ///\n    /// Returns `false` if the two are touching, but not overlapping.\n    pub fn intersects_aabb(&self, other: &Aabb) -> bool {\n        self.min.x < other.max.x\n            && self.max.x > other.min.x\n            && self.min.y < other.max.y\n            && self.max.y > other.min.y\n            && self.min.z < other.max.z\n            && self.max.z > other.min.z\n    }\n}\n\n/// Six planes of a view frustum.\n#[derive(Clone, Copy, Default, PartialEq, SlabItem, core::fmt::Debug)]\npub struct Frustum {\n    /// Planes constructing the sides of the frustum,\n    /// each expressed as a normal vector (xyz) and the distance (w)\n    /// from the origin along that vector.\n    pub planes: [Vec4; 6],\n    /// Points representing the corners of the frustum\n    pub points: [Vec3; 8],\n    /// Centroid of the corners of the frustum\n    pub center: Vec3,\n}\n\nimpl Frustum {\n    /// Compute a frustum in world space from the given [`CameraDescriptor`].\n    pub fn from_camera(camera: &CameraDescriptor) -> Self {\n        let viewprojection = camera.view_projection();\n        let mvp = viewprojection.to_cols_array_2d();\n\n        let left = normalize_plane(Vec4::new(\n            mvp[0][0] + mvp[0][3],\n            mvp[1][0] + mvp[1][3],\n            mvp[2][0] + mvp[2][3],\n            mvp[3][0] + mvp[3][3],\n        ));\n        let right = normalize_plane(Vec4::new(\n            -mvp[0][0] + mvp[0][3],\n            -mvp[1][0] + mvp[1][3],\n            -mvp[2][0] + mvp[2][3],\n            -mvp[3][0] + mvp[3][3],\n        ));\n        let bottom = normalize_plane(Vec4::new(\n            mvp[0][1] + mvp[0][3],\n            mvp[1][1] + mvp[1][3],\n            mvp[2][1] + mvp[2][3],\n            mvp[3][1] + mvp[3][3],\n        ));\n        let top = normalize_plane(Vec4::new(\n            -mvp[0][1] + mvp[0][3],\n            -mvp[1][1] + mvp[1][3],\n            -mvp[2][1] + mvp[2][3],\n            -mvp[3][1] + mvp[3][3],\n        ));\n        // For wgpu/Vulkan/D3D (z_ndc ∈ [0, 1]) the near plane is just row2,\n        // not row2 + row3 (which is the OpenGL z_ndc ∈ [-1, 1] convention).\n        let near = normalize_plane(Vec4::new(mvp[0][2], mvp[1][2], mvp[2][2], mvp[3][2]));\n        let far = normalize_plane(Vec4::new(\n            -mvp[0][2] + mvp[0][3],\n            -mvp[1][2] + mvp[1][3],\n            -mvp[2][2] + mvp[2][3],\n            -mvp[3][2] + mvp[3][3],\n        ));\n\n        // Account for the possibility that the projection is infinite.\n        //\n        // See <https://renderling.xyz/devlog/index.html#actual_frustum_culling>\n        // for more details.\n        let far = (-1.0 * near.xyz()).extend(far.w);\n\n        let flt = intersect_planes(&far, &left, &top);\n        let frt = intersect_planes(&far, &right, &top);\n        let flb = intersect_planes(&far, &left, &bottom);\n        let frb = intersect_planes(&far, &right, &bottom);\n        let nlt = intersect_planes(&near, &left, &top);\n        let nrt = intersect_planes(&near, &right, &top);\n        let nlb = intersect_planes(&near, &left, &bottom);\n        let nrb = intersect_planes(&near, &right, &bottom);\n\n        Self {\n            center: (nlt + nrt + nlb + nrb) / 4.0,\n            planes: [near, left, right, bottom, top, far],\n            points: [nlt, nrt, nlb, nrb, flt, frt, flb, frb],\n        }\n    }\n\n    #[cfg(not(target_arch = \"spirv\"))]\n    /// Return a triangle mesh connecting this `Frustum`'s corners.\n    pub fn get_mesh(&self) -> Vec<(Vec3, Vec3)> {\n        let [nlt, nrt, nlb, nrb, flt, frt, flb, frb] = self.points;\n        let p0 = nlt;\n        let p1 = flt;\n        let p2 = frt;\n        let p3 = nrt;\n        let p4 = nlb;\n        let p5 = nrb;\n        let p6 = frb;\n        let p7 = flb;\n        crate::math::convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7])\n            .chunks_exact(3)\n            .flat_map(|chunk| match chunk {\n                [a, b, c] => {\n                    let n = crate::math::triangle_face_normal(*a, *b, *c);\n                    [(*a, n), (*b, n), (*c, n)]\n                }\n                _ => unreachable!(),\n            })\n            .collect()\n    }\n\n    pub fn test_against_aabb(&self, aabb: &Aabb) -> bool {\n        for i in 0..3 {\n            let mut out = 0;\n            for j in 0..8 {\n                if self.points[j].to_array()[i] < aabb.min.to_array()[i] {\n                    out += 1;\n                }\n            }\n            if out == 8 {\n                return false;\n            }\n            out = 0;\n            for j in 0..8 {\n                if self.points[j].to_array()[i] > aabb.max.to_array()[i] {\n                    out += 1;\n                }\n            }\n            if out == 8 {\n                return false;\n            }\n        }\n        true\n    }\n\n    /// Returns the depth of the frustum.\n    pub fn depth(&self) -> f32 {\n        (self.planes[0].w - self.planes[5].w).abs()\n    }\n}\n\n/// Bounding box consisting of a center and three half extents.\n///\n/// Essentially a point at the center and a vector pointing from\n/// the center to the corner.\n///\n/// This is _not_ an axis aligned bounding box.\n#[derive(Clone, Copy, Default, PartialEq, SlabItem, core::fmt::Debug)]\npub struct BoundingBox {\n    pub center: Vec3,\n    pub half_extent: Vec3,\n}\n\nimpl BoundingBox {\n    pub fn from_min_max(min: Vec3, max: Vec3) -> Self {\n        let center = (min + max) / 2.0;\n        let half_extent = max - center;\n        Self {\n            center,\n            half_extent,\n        }\n    }\n\n    pub fn distance(&self, point: Vec3) -> f32 {\n        let p = point - self.center;\n        let component_edge_distance = p.abs() - self.half_extent;\n        let outside = component_edge_distance.max(Vec3::ZERO).length();\n        let inside = component_edge_distance\n            .x\n            .max(component_edge_distance.y)\n            .min(0.0);\n        inside + outside\n    }\n\n    #[cfg(cpu)]\n    /// Return a triangle mesh connecting this `Aabb`'s corners.\n    ///\n    /// ```ignore\n    ///    y           1_____2     _____\n    ///    |           /    /|    /|    |  (same box, left and front sides removed)\n    ///    |___x     0/___3/ |   /7|____|6\n    ///   /           |    | /   | /    /\n    /// z/            |____|/   4|/____/5\n    ///\n    /// 7 is min\n    /// 3 is max\n    /// ```\n    pub fn get_mesh(&self) -> [(Vec3, Vec3); 36] {\n        // Deriving the corner positions from centre and half-extent,\n\n        let p0 = Vec3::new(-self.half_extent.x, self.half_extent.y, self.half_extent.z);\n        let p1 = Vec3::new(-self.half_extent.x, self.half_extent.y, -self.half_extent.z);\n        let p2 = Vec3::new(self.half_extent.x, self.half_extent.y, -self.half_extent.z);\n        let p3 = self.half_extent;\n        let p4 = Vec3::new(-self.half_extent.x, -self.half_extent.y, self.half_extent.z);\n        let p5 = Vec3::new(self.half_extent.x, -self.half_extent.y, self.half_extent.z);\n        let p6 = Vec3::new(self.half_extent.x, -self.half_extent.y, -self.half_extent.z);\n        // min\n        let p7 = -self.half_extent;\n\n        let positions =\n            crate::math::convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7].map(|p| p + self.center));\n\n        // Attach per-triangle face normals.\n        let vertices: Vec<(Vec3, Vec3)> = positions\n            .chunks_exact(3)\n            .flat_map(|chunk| match chunk {\n                [a, b, c] => {\n                    let n = crate::math::triangle_face_normal(*a, *b, *c);\n                    [(*a, n), (*b, n), (*c, n)]\n                }\n                _ => unreachable!(),\n            })\n            .collect();\n\n        // Convert into fixed-size array (12 triangles × 3 vertices).\n        vertices\n            .try_into()\n            .unwrap_or_else(|v: Vec<(Vec3, Vec3)>| panic!(\"expected 36 vertices, got {}\", v.len()))\n    }\n\n    pub fn contains_point(&self, point: Vec3) -> bool {\n        let delta = (point - self.center).abs();\n        let extent = self.half_extent.abs();\n        delta.x <= extent.x && delta.y <= extent.y && delta.z <= extent.z\n    }\n}\n\n/// Bounding sphere consisting of a center and radius.\n#[derive(Clone, Copy, Debug, Default, PartialEq, SlabItem)]\npub struct BoundingSphere {\n    pub center: Vec3,\n    pub radius: f32,\n}\n\nimpl From<(Vec3, Vec3)> for BoundingSphere {\n    fn from((min, max): (Vec3, Vec3)) -> Self {\n        let center = (min + max) * 0.5;\n        let radius = center.distance(max);\n        BoundingSphere { center, radius }\n    }\n}\n\nimpl From<Aabb> for BoundingSphere {\n    fn from(value: Aabb) -> Self {\n        (value.min, value.max).into()\n    }\n}\n\nimpl BoundingSphere {\n    /// Creates a new bounding sphere.\n    pub fn new(center: impl Into<Vec3>, radius: f32) -> BoundingSphere {\n        BoundingSphere {\n            center: center.into(),\n            radius,\n        }\n    }\n\n    /// Determine whether this sphere is inside the camera's view frustum after\n    /// being transformed by `transform`.  \n    pub fn is_inside_camera_view(\n        &self,\n        camera: &CameraDescriptor,\n        transform: TransformDescriptor,\n    ) -> (bool, BoundingSphere) {\n        let center = Mat4::from(transform).transform_point3(self.center);\n        let scale = Vec3::splat(transform.scale.max_element());\n        let radius = Mat4::from_scale(scale)\n            .transform_point3(Vec3::new(self.radius, 0.0, 0.0))\n            .distance(Vec3::ZERO);\n        let sphere = BoundingSphere::new(center, radius);\n        (sphere.is_inside_frustum(camera.frustum()), sphere)\n    }\n\n    /// Transform this `BoundingSphere` by the given view projection matrix.\n    pub fn project_by(&self, view_projection: &Mat4) -> Self {\n        let center = self.center;\n        // Pick any direction to find a point on the surface.\n        let surface_point = self.center + self.radius * Vec3::Z;\n        let new_center = view_projection.project_point3(center);\n        let new_surface_point = view_projection.project_point3(surface_point);\n        let new_radius = new_center.distance(new_surface_point);\n        Self {\n            center: new_center,\n            radius: new_radius,\n        }\n    }\n\n    /// Returns an [`Aabb`] with x and y coordinates in viewport pixels and z\n    /// coordinate in NDC depth.\n    pub fn project_onto_viewport(&self, camera: &CameraDescriptor, viewport: Vec2) -> Aabb {\n        fn ndc_to_pixel(viewport: Vec2, ndc: Vec3) -> Vec2 {\n            let screen = Vec3::new((ndc.x + 1.0) * 0.5, 1.0 - (ndc.y + 1.0) * 0.5, ndc.z);\n            (screen * viewport.extend(1.0)).xy()\n        }\n\n        let viewproj = camera.view_projection();\n        let frustum = camera.frustum();\n\n        // Find the center and radius of the bounding sphere in pixel space.\n        // By pixel space, I mean where (0, 0) is the top-left of the screen\n        // and (w, h) is is the bottom-left.\n        let center_clip = viewproj * self.center.extend(1.0);\n        let front_center_ndc =\n            viewproj.project_point3(self.center + self.radius * frustum.planes[5].xyz());\n        let back_center_ndc =\n            viewproj.project_point3(self.center + self.radius * frustum.planes[0].xyz());\n        let center_ndc = center_clip.xyz() / center_clip.w;\n        let center_pixels = ndc_to_pixel(viewport, center_ndc);\n        let radius_pixels = viewport.x * (self.radius / center_clip.w);\n        Aabb::new(\n            (center_pixels - radius_pixels).extend(front_center_ndc.z),\n            (center_pixels + radius_pixels).extend(back_center_ndc.z),\n        )\n    }\n}\n\nimpl BVol for BoundingSphere {\n    fn get_aabb(&self) -> Aabb {\n        Aabb {\n            min: self.center - Vec3::splat(self.radius),\n            max: self.center + Vec3::splat(self.radius),\n        }\n    }\n\n    fn culls_this_plane(&self, plane: &Vec4) -> bool {\n        dist_bpp(plane, self.center) < -self.radius\n    }\n}\n\n/// Bounding volume trait.\npub trait BVol {\n    /// Returns an AABB that contains the bounding volume.\n    fn get_aabb(&self) -> Aabb;\n\n    /// Checks if the given bounding volume is culled by this plane.\n    ///\n    /// Returns true if it does, false otherwise.\n    fn culls_this_plane(&self, plane: &Vec4) -> bool;\n\n    fn is_inside_frustum(&self, frustum: Frustum) -> bool {\n        let (inside, _) = self.coherent_test_is_volume_outside_frustum(&frustum, 0);\n        !inside\n    }\n\n    /// Checks if bounding volume is outside the frustum \"coherently\".\n    ///\n    /// In order for a bounding volume to be inside the frustum, it must not be\n    /// culled by any plane.\n    ///\n    /// Coherence is provided by the `lpindex` argument, which should be the\n    /// index of the first plane found that culls this volume, given as part\n    /// of the return value of this function.\n    ///\n    /// Returns `true` if the volume is outside the frustum, `false` otherwise.\n    ///\n    /// Returns the index of first plane found that culls this volume, to cache\n    /// and use later as a short circuit.\n    fn coherent_test_is_volume_outside_frustum(\n        &self,\n        frustum: &Frustum,\n        lpindex: u32,\n    ) -> (bool, u32) {\n        if self.culls_this_plane(&frustum.planes[lpindex as usize]) {\n            return (true, lpindex);\n        }\n\n        for i in 0..6 {\n            if (i != lpindex) && self.culls_this_plane(&frustum.planes[i as usize]) {\n                return (true, i);\n            }\n        }\n\n        if !frustum.test_against_aabb(&self.get_aabb()) {\n            return (true, lpindex);\n        }\n\n        (false, lpindex)\n    }\n}\n\nimpl BVol for Aabb {\n    fn get_aabb(&self) -> Aabb {\n        *self\n    }\n\n    fn culls_this_plane(&self, plane: &Vec4) -> bool {\n        dist_bpp(plane, mi_vertex(plane, self)) < 0.0\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use glam::{Mat4, Quat};\n\n    use crate::{context::Context, geometry::Vertex, test::BlockOnFuture};\n\n    use super::*;\n\n    #[test]\n    fn bvol_frustum_is_in_world_space_sanity() {\n        let (p, v) = crate::camera::default_perspective(800.0, 600.0);\n        let camera = CameraDescriptor::new(p, v);\n        let aabb_outside = Aabb {\n            min: Vec3::new(-10.0, -12.0, 20.0),\n            max: Vec3::new(10.0, 12.0, 40.0),\n        };\n        assert!(!aabb_outside.is_inside_frustum(camera.frustum()));\n\n        let aabb_inside = Aabb {\n            min: Vec3::new(-3.0, -3.0, -3.0),\n            max: Vec3::new(3.0, 3.0, 3.0),\n        };\n        assert!(aabb_inside.is_inside_frustum(camera.frustum()));\n    }\n\n    #[test]\n    fn frustum_culling_debug_corner_case() {\n        // https://github.com/schell/renderling/issues/131\n        // https://renderling.xyz/devlog/index.html#frustum_culling_last_debugging__aabb_vs_frustum_corner_case\n        let camera = {\n            let aspect = 1.0;\n            let fovy = core::f32::consts::FRAC_PI_4;\n            let znear = 4.0;\n            let zfar = 1000.0;\n            let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);\n            let eye = Vec3::new(0.0, 0.0, 10.0);\n            let target = Vec3::ZERO;\n            let up = Vec3::Y;\n            let view = Mat4::look_at_rh(eye, target, up);\n            CameraDescriptor::new(projection, view)\n        };\n        let aabb = Aabb {\n            min: Vec3::new(-3.2869213, -3.0652206, -3.8715153),\n            max: Vec3::new(3.2869213, 3.0652206, 3.8715153),\n        };\n        let transform = TransformDescriptor {\n            translation: Vec3::new(7.5131035, -9.947085, -5.001645),\n            rotation: Quat::from_xyzw(0.4700742, 0.34307128, 0.6853008, -0.43783003),\n            scale: Vec3::new(1.0, 1.0, 1.0),\n        };\n        assert!(\n            !aabb.is_outside_camera_view(&camera, transform),\n            \"aabb should be inside the frustum\"\n        );\n    }\n\n    #[test]\n    fn bounding_box_from_min_max() {\n        let ctx = Context::headless(256, 256).block();\n        let stage = ctx\n            .new_stage()\n            .with_background_color(Vec4::ZERO)\n            .with_msaa_sample_count(4)\n            .with_lighting(true);\n        let _camera = stage.new_camera().with_projection_and_view(\n            // Fixed: was `near=10, far=-10` which reversed depth ordering.\n            // Using `near=0.1, far=20` ensures correct depth testing.\n            Mat4::orthographic_rh(-3.0, 3.0, -3.0, 3.0, 0.1, 20.0),\n            Mat4::look_at_rh(Vec3::new(-3.0, 3.0, 5.0), Vec3::ZERO, Vec3::Y),\n        );\n        let _lights = crate::test::make_two_directional_light_setup(&stage);\n\n        let white = stage.new_material();\n        let red = stage\n            .new_material()\n            .with_albedo_factor(Vec4::new(1.0, 0.0, 0.0, 1.0));\n\n        let _w = stage.new_primitive().with_material(&white).with_vertices(\n            stage.new_vertices(\n                crate::math::unit_cube()\n                    .into_iter()\n                    .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)),\n            ),\n        );\n\n        let mut corners = vec![];\n        for x in [-1.0, 1.0] {\n            for y in [-1.0, 1.0] {\n                for z in [-1.0, 1.0] {\n                    corners.push(Vec3::new(x, y, z));\n                }\n            }\n        }\n        let mut rs = vec![];\n        for corner in corners.iter() {\n            let bb = BoundingBox {\n                center: Vec3::new(0.5, 0.5, 0.5) * corner,\n                half_extent: Vec3::splat(0.25),\n            };\n            assert!(\n                bb.contains_point(bb.center),\n                \"BoundingBox {bb:?} does not contain center\"\n            );\n\n            rs.push(\n                stage.new_primitive().with_material(&red).with_vertices(\n                    stage.new_vertices(\n                        bb.get_mesh()\n                            .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)),\n                    ),\n                ),\n            );\n        }\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"bvol/bounding_box/get_mesh.png\", img);\n    }\n\n    /// Verifies that an orthographic projection with correct `near < far`\n    /// produces visible lit output.\n    ///\n    /// The original bug was that `orthographic_rh(-10, 10, -10, 10, 10, -10)`\n    /// (note: `near=10 > far=-10`) reversed the depth mapping, causing back\n    /// faces to win the depth test and produce zero lighting output.\n    ///\n    /// The fix is to use `near < far`, matching the convention of glTF, wgpu,\n    /// and every major rendering engine.\n    #[test]\n    fn orthographic_projection_lighting() {\n        let ctx = Context::headless(256, 256).block();\n        let view = Mat4::look_at_rh(Vec3::new(-3.0, 3.0, 5.0) * 0.5, Vec3::ZERO, Vec3::Y);\n\n        // Helper: render a lit unit cube and return the number of bright\n        // pixels (any RGB channel > 10).\n        let render = |projection: Mat4, label: &str| -> usize {\n            let stage = ctx\n                .new_stage()\n                .with_background_color(Vec4::ZERO)\n                .with_msaa_sample_count(4)\n                .with_lighting(true);\n            let _camera = stage\n                .new_camera()\n                .with_projection_and_view(projection, view);\n            let _lights = crate::test::make_two_directional_light_setup(&stage);\n            let white = stage.new_material().with_albedo_factor(Vec4::ONE);\n            let _prim = stage.new_primitive().with_material(&white).with_vertices(\n                stage.new_vertices(\n                    crate::math::unit_cube()\n                        .into_iter()\n                        .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)),\n                ),\n            );\n            let frame = ctx.get_next_frame().unwrap();\n            stage.render(&frame.view());\n            let img = frame.read_image().block().unwrap();\n            img_diff::save(&format!(\"bvol/ortho_lighting_{label}.png\"), img.clone());\n            let bright = img\n                .pixels()\n                .filter(|p| p.0[0] > 10 || p.0[1] > 10 || p.0[2] > 10)\n                .count();\n            bright\n        };\n\n        // Correct orthographic: near=-10, far=10 (near < far)\n        let ortho_bright = render(\n            Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, -10.0, 10.0),\n            \"ortho_correct\",\n        );\n\n        // Perspective for reference\n        let persp_bright = render(crate::camera::perspective(256.0, 256.0), \"perspective\");\n\n        assert!(\n            persp_bright > 100,\n            \"Sanity: perspective should have bright pixels, got {persp_bright}\"\n        );\n        assert!(\n            ortho_bright > 100,\n            \"Orthographic projection (near=-10, far=10) should produce visible lit output, got \\\n             only {ortho_bright} bright pixels (perspective had {persp_bright})\"\n        );\n    }\n\n    #[test]\n    fn aabb_intersection() {\n        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);\n        let b = Aabb::new(Vec3::splat(0.9), Vec3::splat(1.9));\n        assert!(a.intersects_aabb(&b));\n        assert!(b.intersects_aabb(&a));\n    }\n\n    #[test]\n    fn aabb_union() {\n        let a = Aabb::new(Vec3::splat(4.0), Vec3::splat(5.0));\n        let b = Aabb::new(Vec3::ZERO, Vec3::ONE);\n        let c = Aabb::union(a, b);\n        assert_eq!(\n            Aabb {\n                min: Vec3::ZERO,\n                max: Vec3::splat(5.0)\n            },\n            c\n        );\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/camera/cpu.rs",
    "content": "//! CPU side of [crate::camera].\n\nuse craballoc::{runtime::IsRuntime, slab::SlabAllocator, value::Hybrid};\nuse crabslab::Id;\n\nuse crate::camera::shader::CameraDescriptor;\n\nuse super::*;\n\n/// A camera used for transforming the stage during rendering.\n///\n/// * Use [`Stage::new_camera`](crate::stage::Stage::new_camera) to create a new\n///   camera.\n/// * Use [`Stage::use_camera`](crate::stage::Stage::use_camera) to set a camera\n///   on the stage.\n///\n/// ## Note\n///\n/// Clones of this type all point to the same underlying data.\n#[derive(Clone, Debug)]\npub struct Camera {\n    inner: Hybrid<CameraDescriptor>,\n}\n\nimpl AsRef<Camera> for Camera {\n    fn as_ref(&self) -> &Camera {\n        self\n    }\n}\n\nimpl Camera {\n    /// Stage a new camera on the given slab.\n    pub fn new(slab: &SlabAllocator<impl IsRuntime>) -> Self {\n        Self {\n            inner: slab.new_value(CameraDescriptor::default()),\n        }\n    }\n\n    /// Returns a pointer to the underlying descriptor on the GPU.\n    pub fn id(&self) -> Id<CameraDescriptor> {\n        self.inner.id()\n    }\n\n    /// Returns a copy of the underlying descriptor.\n    pub fn descriptor(&self) -> CameraDescriptor {\n        self.inner.get()\n    }\n\n    /// Set the camera to a default perspective projection and view based\n    /// on the width and height of the viewport.\n    ///\n    /// The default projection and view matrices are defined as:\n    ///\n    /// ```rust\n    /// use glam::*;\n    ///\n    /// let width = 800.0;\n    /// let height = 600.0;\n    /// let aspect = width / height;\n    /// let fovy = core::f32::consts::PI / 4.0;\n    /// let znear = 0.1;\n    /// let zfar = 100.0;\n    /// let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);\n    /// let eye = Vec3::new(0.0, 12.0, 20.0);\n    /// let target = Vec3::ZERO;\n    /// let up = Vec3::Y;\n    /// let view = Mat4::look_at_rh(eye, target, up);\n    /// assert_eq!(\n    ///     renderling::camera::default_perspective(width, height),\n    ///     (projection, view)\n    /// );\n    /// ```\n    pub fn set_default_perspective(&self, width: f32, height: f32) -> &Self {\n        self.inner\n            .modify(|d| *d = CameraDescriptor::default_perspective(width, height));\n        self\n    }\n\n    /// Set the camera to a default perspective projection and view based\n    /// on the width and height of the viewport.\n    ///\n    /// The default projection and view matrices are defined as:\n    ///\n    /// ```rust\n    /// use glam::*;\n    ///\n    /// let width = 800.0;\n    /// let height = 600.0;\n    /// let aspect = width / height;\n    /// let fovy = core::f32::consts::PI / 4.0;\n    /// let znear = 0.1;\n    /// let zfar = 100.0;\n    /// let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);\n    /// let eye = Vec3::new(0.0, 12.0, 20.0);\n    /// let target = Vec3::ZERO;\n    /// let up = Vec3::Y;\n    /// let view = Mat4::look_at_rh(eye, target, up);\n    /// assert_eq!(\n    ///     renderling::camera::default_perspective(width, height),\n    ///     (projection, view)\n    /// );\n    /// ```\n    pub fn with_default_perspective(self, width: f32, height: f32) -> Self {\n        self.set_default_perspective(width, height);\n        self\n    }\n\n    /// Set the camera to a default orthographic 2d projection and view based\n    /// on the width and height of the viewport.\n    pub fn set_default_ortho2d(&self, width: f32, height: f32) -> &Self {\n        self.inner\n            .modify(|d| *d = CameraDescriptor::default_ortho2d(width, height));\n        self\n    }\n\n    /// Set the camera to a default orthographic 2d projection and view based\n    /// on the width and height of the viewport.\n    pub fn with_default_ortho2d(self, width: f32, height: f32) -> Self {\n        self.set_default_ortho2d(width, height);\n        self\n    }\n\n    /// Set the projection and view matrices of this camera.\n    pub fn set_projection_and_view(\n        &self,\n        projection: impl Into<Mat4>,\n        view: impl Into<Mat4>,\n    ) -> &Self {\n        self.inner\n            .modify(|d| d.set_projection_and_view(projection.into(), view.into()));\n        self\n    }\n\n    /// Set the projection and view matrices and return this camera.\n    pub fn with_projection_and_view(\n        self,\n        projection: impl Into<Mat4>,\n        view: impl Into<Mat4>,\n    ) -> Self {\n        self.set_projection_and_view(projection, view);\n        self\n    }\n\n    /// Returns the projection and view matrices.\n    pub fn projection_and_view(&self) -> (Mat4, Mat4) {\n        let d = self.inner.get();\n        (d.projection(), d.view())\n    }\n\n    /// Set the projection matrix of this camera.\n    pub fn set_projection(&self, projection: impl Into<Mat4>) -> &Self {\n        self.inner.modify(|d| d.set_projection(projection.into()));\n        self\n    }\n\n    /// Set the projection matrix and return this camera.\n    pub fn with_projection(self, projection: impl Into<Mat4>) -> Self {\n        self.set_projection(projection);\n        self\n    }\n\n    /// Returns the projection matrix.\n    pub fn projection(&self) -> Mat4 {\n        self.inner.get().projection()\n    }\n\n    /// Set the view matrix of this camera.\n    pub fn set_view(&self, view: impl Into<Mat4>) -> &Self {\n        self.inner.modify(|d| d.set_view(view.into()));\n        self\n    }\n\n    /// Set the view matrix and return this camera.\n    pub fn with_view(self, view: impl Into<Mat4>) -> Self {\n        self.set_view(view);\n        self\n    }\n\n    /// Returns the view matrix.\n    pub fn view(&self) -> Mat4 {\n        self.inner.get().view()\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use craballoc::{runtime::CpuRuntime, slab::SlabAllocator};\n\n    use super::*;\n\n    #[test]\n    fn camera_position_sanity() {\n        let slab = SlabAllocator::new(CpuRuntime, \"camera test\", ());\n        let camera = Camera::new(&slab);\n        let projection = Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.01, 10.0);\n        let view = Mat4::look_at_rh(Vec3::ONE, Vec3::ZERO, Vec3::Y);\n        camera.set_projection_and_view(projection, view);\n        let position = camera.descriptor().position();\n        assert_eq!(Vec3::ONE, position);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/camera/shader.rs",
    "content": "//! [`CameraDescriptor`] and camera shader utilities.\n\nuse crabslab::SlabItem;\nuse glam::{Mat4, Vec2, Vec3, Vec4};\n\nuse crate::{bvol::Frustum, math::IsVector};\n\n/// GPU descriptor of a camera.\n///\n/// Used for transforming the stage during rendering.\n///\n/// Use [`CameraDescriptor::new`] to create a new camera.\n#[derive(Default, Clone, Copy, PartialEq, SlabItem, core::fmt::Debug)]\npub struct CameraDescriptor {\n    projection: Mat4,\n    view: Mat4,\n    position: Vec3,\n    frustum: Frustum,\n    /// Nearest center point on the frustum\n    z_near_point: Vec3,\n    /// Furthest center point on the frustum\n    z_far_point: Vec3,\n}\n\nimpl CameraDescriptor {\n    pub fn new(projection: Mat4, view: Mat4) -> Self {\n        CameraDescriptor::default().with_projection_and_view(projection, view)\n    }\n\n    pub fn default_perspective(width: f32, height: f32) -> Self {\n        let (projection, view) = super::default_perspective(width, height);\n        CameraDescriptor::new(projection, view)\n    }\n\n    pub fn default_ortho2d(width: f32, height: f32) -> Self {\n        let (projection, view) = super::default_ortho2d(width, height);\n        CameraDescriptor::new(projection, view)\n    }\n\n    pub fn projection(&self) -> Mat4 {\n        self.projection\n    }\n\n    /// Set the projection and view matrices for this camera.\n    ///\n    /// ## Note on depth ordering\n    /// The rendering pipeline uses `CompareFunction::Less` for depth testing.\n    /// This requires that the projection matrix maps closer objects to\n    /// *smaller* z_ndc values. For orthographic projections created with\n    /// `Mat4::orthographic_rh`, this means `near` must be less than `far`.\n    /// If `near > far`, the depth mapping is reversed and back faces will\n    /// be drawn over front faces, producing incorrect (typically black)\n    /// lighting output.\n    pub fn set_projection_and_view(&mut self, projection: Mat4, view: Mat4) {\n        // The z column of the projection matrix determines the depth mapping\n        // direction. A positive z coefficient (projection.z_axis.z > 0)\n        // indicates reversed depth (closer objects get larger z_ndc), which\n        // will cause incorrect results with the standard\n        // CompareFunction::Less depth test. This typically happens when\n        // `near > far` is passed to `Mat4::orthographic_rh`.\n        #[cfg(cpu)]\n        {\n            let z_coeff = projection.z_axis.z;\n            if z_coeff > 0.0 {\n                log::warn!(\n                    \"Projection matrix has a positive z coefficient ({z_coeff}), indicating \\\n                     reversed depth mapping. This will cause incorrect depth testing (back faces \\\n                     drawn over front faces) with the standard CompareFunction::Less. For \\\n                     orthographic projections, ensure near < far.\"\n                );\n            }\n        }\n        self.projection = projection;\n        self.view = view;\n        self.position = view.inverse().transform_point3(Vec3::ZERO);\n        let inverse = (projection * view).inverse();\n        self.z_near_point = inverse.project_point3(Vec3::ZERO);\n        self.z_far_point = inverse.project_point3(Vec2::ZERO.extend(1.0));\n        self.frustum = Frustum::from_camera(self);\n    }\n\n    pub fn with_projection_and_view(mut self, projection: Mat4, view: Mat4) -> Self {\n        self.set_projection_and_view(projection, view);\n        self\n    }\n\n    pub fn set_projection(&mut self, projection: Mat4) {\n        self.set_projection_and_view(projection, self.view);\n    }\n\n    pub fn with_projection(mut self, projection: Mat4) -> Self {\n        self.set_projection(projection);\n        self\n    }\n\n    pub fn view(&self) -> Mat4 {\n        self.view\n    }\n\n    pub fn set_view(&mut self, view: Mat4) {\n        self.set_projection_and_view(self.projection, view);\n    }\n\n    pub fn with_view(mut self, view: Mat4) -> Self {\n        self.set_view(view);\n        self\n    }\n\n    pub fn position(&self) -> Vec3 {\n        self.position\n    }\n\n    pub fn frustum(&self) -> Frustum {\n        self.frustum\n    }\n\n    pub fn view_projection(&self) -> Mat4 {\n        self.projection * self.view\n    }\n\n    pub fn near_plane(&self) -> Vec4 {\n        self.frustum.planes[0]\n    }\n\n    pub fn far_plane(&self) -> Vec4 {\n        self.frustum.planes[5]\n    }\n\n    /// Returns **roughly** the location of the znear plane.\n    pub fn z_near(&self) -> f32 {\n        self.z_near_point.distance(self.position)\n    }\n\n    pub fn z_far(&self) -> f32 {\n        self.z_far_point.distance(self.position)\n    }\n\n    pub fn depth(&self) -> f32 {\n        (self.z_far() - self.z_near()).abs()\n    }\n\n    /// Returns the normalized forward vector which points in the direction the\n    /// camera is looking.\n    pub fn forward(&self) -> Vec3 {\n        (self.z_far_point - self.z_near_point).alt_norm_or_zero()\n    }\n\n    pub fn frustum_near_point(&self) -> Vec3 {\n        self.forward() * self.z_near()\n    }\n\n    pub fn frustum_far_point(&self) -> Vec3 {\n        self.forward() * self.z_far()\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/camera.rs",
    "content": "//! Camera projection, view and utilities.\nuse glam::{Mat4, Vec3};\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\npub mod shader;\n\n/// Returns the projection and view matrices for a camera with default\n/// perspective.\n///\n/// The default projection and view matrices are defined as:\n///\n/// ```rust\n/// use glam::*;\n///\n/// let width = 800.0;\n/// let height = 600.0;\n/// let aspect = width / height;\n/// let fovy = core::f32::consts::PI / 4.0;\n/// let znear = 0.1;\n/// let zfar = 100.0;\n/// let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);\n/// let eye = Vec3::new(0.0, 12.0, 20.0);\n/// let target = Vec3::ZERO;\n/// let up = Vec3::Y;\n/// let view = Mat4::look_at_rh(eye, target, up);\n/// assert_eq!(\n///     renderling::camera::default_perspective(width, height),\n///     (projection, view)\n/// );\n/// ```\npub fn default_perspective(width: f32, height: f32) -> (Mat4, Mat4) {\n    let projection = perspective(width, height);\n    let eye = Vec3::new(0.0, 12.0, 20.0);\n    let target = Vec3::ZERO;\n    let up = Vec3::Y;\n    let view = Mat4::look_at_rh(eye, target, up);\n    (projection, view)\n}\n\npub fn perspective(width: f32, height: f32) -> Mat4 {\n    let aspect = width / height;\n    let fovy = core::f32::consts::PI / 4.0;\n    let znear = 0.1;\n    let zfar = 100.0;\n    Mat4::perspective_rh(fovy, aspect, znear, zfar)\n}\n\n/// Creates a right-handed orthographic projection matrix.\n///\n/// ## Note\n/// When constructing orthographic projections, `near` must be less than `far`\n/// to ensure correct depth testing. Reversed depth (`near > far`) will cause\n/// back faces to be drawn over front faces, producing incorrect lighting.\npub fn ortho(width: f32, height: f32) -> Mat4 {\n    let left = 0.0;\n    let right = width;\n    let bottom = height;\n    let top = 0.0;\n    let near = -1.0;\n    let far = 1.0;\n    Mat4::orthographic_rh(left, right, bottom, top, near, far)\n}\n\npub fn look_at(eye: impl Into<Vec3>, target: impl Into<Vec3>, up: impl Into<Vec3>) -> Mat4 {\n    Mat4::look_at_rh(eye.into(), target.into(), up.into())\n}\n\n/// Creates a typical 2d orthographic projection with +Y extending downward\n/// and the +Z axis coming out towards the viewer.\n///\n/// ## Note\n/// When constructing orthographic projections, `near` must be less than `far`\n/// to ensure correct depth testing. Reversed depth (`near > far`) will cause\n/// back faces to be drawn over front faces, producing incorrect lighting.\npub fn default_ortho2d(width: f32, height: f32) -> (Mat4, Mat4) {\n    let left = 0.0;\n    let right = width;\n    let bottom = height;\n    let top = 0.0;\n    let near = -1.0;\n    let far = 1.0;\n    let projection = Mat4::orthographic_rh(left, right, bottom, top, near, far);\n    let view = Mat4::IDENTITY;\n    (projection, view)\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::camera::shader::CameraDescriptor;\n\n    use super::*;\n    use glam::Vec3;\n\n    #[test]\n    fn forward() {\n        let eyes = [Vec3::new(0.0, 0.0, 5.0), Vec3::new(250.0, 200.0, 250.0)];\n\n        let expected_forwards = [\n            Vec3::new(0.0, 0.0, -1.0),\n            Vec3::new(-0.6154574, -0.49236593, -0.6154574),\n        ];\n\n        for (eye, expected_forward) in eyes.into_iter().zip(expected_forwards) {\n            let projection = Mat4::perspective_rh(45.0_f32.to_radians(), 800.0 / 600.0, 0.1, 100.0);\n            let view = Mat4::look_at_rh(eye, Vec3::ZERO, Vec3::Y);\n            let camera = CameraDescriptor::new(projection, view);\n\n            let forward = camera.forward();\n            let distance = forward.distance(expected_forward);\n            const THRESHOLD: f32 = 1e-3;\n            assert!(\n                distance < THRESHOLD,\n                \"Forward vector is incorrect\\nforward: {forward}\\nexpected: \\\n                 {expected_forward}\\ndistance: {distance}, threshold: {THRESHOLD}\"\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/color.rs",
    "content": "//! Color utils.\n\nuse glam::Vec4;\n#[cfg(target_arch = \"spirv\")]\nuse spirv_std::num_traits::Float;\n\n/// Applies a linear transfer function to an 8-bit unsigned integer color\n/// component.\n///\n/// This function simulates the gamma correction process by raising the\n/// component to the power of 2.2.\n///\n/// Converts from sRGB to linear color space.\npub fn linear_xfer_u8(c: &mut u8) {\n    *c = ((*c as f32 / 255.0).powf(2.2) * 255.0) as u8;\n}\n\n/// Applies an optical transfer function to an 8-bit unsigned integer color\n/// component.\n///\n/// This function simulates the inverse gamma correction process by raising the\n/// component to the power of 1/2.2.\n///\n/// Converts from linear to sRGB color space.\npub fn opto_xfer_u8(c: &mut u8) {\n    *c = ((*c as f32 / 255.0).powf(1.0 / 2.2) * 255.0) as u8;\n}\n\n/// Applies a linear transfer function to a 16-bit unsigned integer color\n/// component.\n///\n/// This function simulates the gamma correction process by raising the\n/// component to the power of 2.2.\n///\n/// Converts from sRGB to linear color space.\npub fn linear_xfer_u16(c: &mut u16) {\n    *c = ((*c as f32 / 65535.0).powf(2.2) * 65535.0) as u16;\n}\n\n#[cfg(not(target_arch = \"spirv\"))]\nmod cpu {\n    use super::*;\n    use glam::Vec3;\n\n    /// Applies a linear transfer function to a 16-bit floating-point color\n    /// component.\n    ///\n    /// This function simulates the gamma correction process by raising the\n    /// component to the power of 2.2.\n    ///\n    /// Converts from sRGB to linear color space.\n    pub fn linear_xfer_f16(c: &mut u16) {\n        let mut f = half::f16::from_bits(*c).to_f32();\n        super::linear_xfer_f32(&mut f);\n        *c = half::f16::from_f32(f).to_bits();\n    }\n\n    pub fn u16_to_u8(c: u16) -> u8 {\n        ((c as f32 / 65535.0) * 255.0) as u8\n    }\n\n    pub fn f32_to_u8(c: f32) -> u8 {\n        (c / 255.0) as u8\n    }\n\n    pub fn u8_to_f32(c: u8) -> f32 {\n        c as f32 / 255.0\n    }\n\n    /// Converts a CSS style sRGB color into a `Vec4`.\n    ///\n    /// This applies the linear transfer function to the input color,\n    /// returning a color in linear color space.\n    pub fn css_srgb_color_to_linear(r: u8, g: u8, b: u8) -> Vec4 {\n        let rgb = [r, g, b].map(u8_to_f32);\n        let mut color = Vec3::from_array(rgb).extend(1.0);\n        linear_xfer_vec4(&mut color);\n        color\n    }\n}\n#[cfg(not(target_arch = \"spirv\"))]\npub use cpu::*;\n\n/// Applies a linear transfer function to a 32-bit floating-point color\n/// component.\n///\n/// This function simulates the gamma correction process by raising the\n/// component to the power of 2.2.\n///\n/// Converts from sRGB to linear color space.\npub fn linear_xfer_f32(c: &mut f32) {\n    *c = c.powf(2.2);\n}\n\n/// Applies a linear transfer function to each component of a `Vec4`.\n///\n/// This function simulates the gamma correction process by raising each\n/// component to the power of 2.2.\n///\n/// Converts from sRGB to linear color space for each component.\npub fn linear_xfer_vec4(v: &mut Vec4) {\n    linear_xfer_f32(&mut v.x);\n    linear_xfer_f32(&mut v.y);\n    linear_xfer_f32(&mut v.z);\n}\n\n/// Converts an RGB hex color.\n///\n/// Converts a hex code like `0x6DC5D1` to a Vec4 with\n/// RGB components in the range `0.0` to `1.0` and an alpha of `1.0`.\n///\n/// ## Note\n/// This does **not** apply the linear transfer.\npub fn rgb_hex_color(hex: u32) -> Vec4 {\n    let r = ((hex >> 16) & 0xFF) as f32 / 255.0;\n    let g = ((hex >> 8) & 0xFF) as f32 / 255.0;\n    let b = (hex & 0xFF) as f32 / 255.0;\n    Vec4::new(r, g, b, 1.0)\n}\n\n#[cfg(test)]\nmod test {\n    use super::rgb_hex_color;\n\n    #[test]\n    fn can_rgb_hex_color() {\n        let hex = 0x6dc5d1;\n        let color = rgb_hex_color(hex);\n        let r = (color.x * 255.0) as u8;\n        let g = (color.y * 255.0) as u8;\n        let b = (color.z * 255.0) as u8;\n        assert_eq!(109, 0x6d);\n        assert_eq!([109, 197, 209], [r, g, b]);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/compositor/cpu.rs",
    "content": "//! CPU-side compositor for alpha-blending a source texture onto a target.\n\n/// Alpha-blends a source texture onto a target framebuffer using a\n/// fullscreen quad.\n///\n/// This is useful for overlaying MSAA-resolved UI content on top of an\n/// existing 3D scene without overwriting the scene's pixels.\n///\n/// ```ignore\n/// let compositor = Compositor::new(device, format);\n/// // ... render 3D scene to `view` ...\n/// // ... render UI to `ui_texture` ...\n/// compositor.composite(device, queue, &ui_texture_view, &view);\n/// ```\npub struct Compositor {\n    pipeline: wgpu::RenderPipeline,\n    bindgroup_layout: wgpu::BindGroupLayout,\n    sampler: wgpu::Sampler,\n}\n\nimpl Compositor {\n    /// Create a new compositor targeting the given texture format.\n    pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {\n        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {\n            label: Some(\"compositor\"),\n            address_mode_u: wgpu::AddressMode::ClampToEdge,\n            address_mode_v: wgpu::AddressMode::ClampToEdge,\n            address_mode_w: wgpu::AddressMode::ClampToEdge,\n            mag_filter: wgpu::FilterMode::Linear,\n            min_filter: wgpu::FilterMode::Linear,\n            ..Default::default()\n        });\n\n        let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: Some(\"compositor\"),\n            entries: &[\n                wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Texture {\n                        sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                        view_dimension: wgpu::TextureViewDimension::D2,\n                        multisampled: false,\n                    },\n                    count: None,\n                },\n                wgpu::BindGroupLayoutEntry {\n                    binding: 1,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                    count: None,\n                },\n            ],\n        });\n\n        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label: Some(\"compositor\"),\n            bind_group_layouts: &[&bindgroup_layout],\n            push_constant_ranges: &[],\n        });\n\n        let vertex = crate::linkage::compositor_vertex::linkage(device);\n        let fragment = crate::linkage::compositor_fragment::linkage(device);\n\n        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n            label: Some(\"compositor\"),\n            layout: Some(&pipeline_layout),\n            vertex: wgpu::VertexState {\n                module: &vertex.module,\n                entry_point: Some(vertex.entry_point),\n                compilation_options: wgpu::PipelineCompilationOptions::default(),\n                buffers: &[],\n            },\n            primitive: wgpu::PrimitiveState {\n                topology: wgpu::PrimitiveTopology::TriangleList,\n                strip_index_format: None,\n                front_face: wgpu::FrontFace::Ccw,\n                cull_mode: None,\n                unclipped_depth: false,\n                polygon_mode: wgpu::PolygonMode::Fill,\n                conservative: false,\n            },\n            depth_stencil: None,\n            multisample: wgpu::MultisampleState::default(),\n            fragment: Some(wgpu::FragmentState {\n                module: &fragment.module,\n                entry_point: Some(fragment.entry_point),\n                compilation_options: wgpu::PipelineCompilationOptions::default(),\n                targets: &[Some(wgpu::ColorTargetState {\n                    format,\n                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),\n                    write_mask: wgpu::ColorWrites::ALL,\n                })],\n            }),\n            multiview: None,\n            cache: None,\n        });\n\n        Self {\n            pipeline,\n            bindgroup_layout,\n            sampler,\n        }\n    }\n\n    /// Alpha-blend the `source` texture onto the `target` framebuffer.\n    ///\n    /// The existing content of `target` is preserved (`LoadOp::Load`)\n    /// and the source is drawn on top using the pipeline's alpha blend\n    /// state.\n    pub fn composite(\n        &self,\n        device: &wgpu::Device,\n        queue: &wgpu::Queue,\n        source: &wgpu::TextureView,\n        target: &wgpu::TextureView,\n    ) {\n        let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor {\n            label: Some(\"compositor\"),\n            layout: &self.bindgroup_layout,\n            entries: &[\n                wgpu::BindGroupEntry {\n                    binding: 0,\n                    resource: wgpu::BindingResource::TextureView(source),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 1,\n                    resource: wgpu::BindingResource::Sampler(&self.sampler),\n                },\n            ],\n        });\n\n        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {\n            label: Some(\"compositor\"),\n        });\n\n        {\n            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                label: Some(\"compositor\"),\n                color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                    view: target,\n                    resolve_target: None,\n                    ops: wgpu::Operations {\n                        load: wgpu::LoadOp::Load,\n                        store: wgpu::StoreOp::Store,\n                    },\n                    depth_slice: None,\n                })],\n                depth_stencil_attachment: None,\n                timestamp_writes: None,\n                occlusion_query_set: None,\n            });\n\n            pass.set_pipeline(&self.pipeline);\n            pass.set_bind_group(0, Some(&bindgroup), &[]);\n            pass.draw(0..6, 0..1);\n        }\n\n        queue.submit(std::iter::once(encoder.finish()));\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/compositor.rs",
    "content": "//! Compositor for alpha-blending a source texture onto a target framebuffer.\n//!\n//! This is used by the `renderling-ui` crate to overlay MSAA-resolved UI\n//! content onto a 3D scene without overwriting existing framebuffer content.\n\nuse glam::{Vec2, Vec4};\nuse spirv_std::{image::Image2d, spirv, Sampler};\n\n/// Fullscreen quad vertex shader for compositing.\n///\n/// Generates 6 vertices (two triangles) covering the full clip-space quad\n/// and passes through UV coordinates for texture sampling.\n#[spirv(vertex)]\npub fn compositor_vertex(\n    #[spirv(vertex_index)] vertex_id: u32,\n    out_uv: &mut Vec2,\n    #[spirv(position)] out_pos: &mut Vec4,\n) {\n    let i = vertex_id as usize;\n    *out_uv = crate::math::UV_COORD_QUAD_CCW[i];\n    *out_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i];\n}\n\n/// Passthrough fragment shader for compositing.\n///\n/// Samples the source texture at the given UV and outputs the color.\n/// Alpha blending is handled by the pipeline's blend state, not the shader.\n#[spirv(fragment)]\npub fn compositor_fragment(\n    #[spirv(descriptor_set = 0, binding = 0)] texture: &Image2d,\n    #[spirv(descriptor_set = 0, binding = 1)] sampler: &Sampler,\n    in_uv: Vec2,\n    frag_color: &mut Vec4,\n) {\n    *frag_color = texture.sample(*sampler, in_uv);\n}\n\n#[cfg(not(target_arch = \"spirv\"))]\nmod cpu;\n#[cfg(not(target_arch = \"spirv\"))]\npub use cpu::*;\n"
  },
  {
    "path": "crates/renderling/src/context.rs",
    "content": "//! Rendering context initialization\n//!\n//! This module contains [`Context`] initialization and frame management.\n//! This module provides the setup and management of rendering targets,\n//! frames, and surface configurations.\nuse core::fmt::Debug;\nuse std::{\n    ops::Deref,\n    sync::{Arc, RwLock},\n};\n\nuse glam::{UVec2, UVec3};\nuse snafu::prelude::*;\n\nuse crate::{\n    stage::Stage,\n    texture::{BufferDimensions, CopiedTextureBuffer, Texture, TextureError},\n};\npub use craballoc::runtime::WgpuRuntime;\n\n/// Represents the internal structure of a render target, which can either be a\n/// surface or a texture.\npub(crate) enum RenderTargetInner {\n    Surface {\n        surface: wgpu::Surface<'static>,\n        surface_config: wgpu::SurfaceConfiguration,\n    },\n    Texture {\n        texture: Arc<wgpu::Texture>,\n    },\n}\n\n#[repr(transparent)]\n/// Represents a render target that can either be a surface or a texture.\n/// It will be a surface if the context was created with a window or canvas,\n/// and a texture if the context is headless.\npub struct RenderTarget(pub(crate) RenderTargetInner);\n\n/// Converts a `wgpu::Texture` into a `RenderTarget`.\nimpl From<wgpu::Texture> for RenderTarget {\n    fn from(value: wgpu::Texture) -> Self {\n        RenderTarget(RenderTargetInner::Texture {\n            texture: Arc::new(value),\n        })\n    }\n}\n\nimpl RenderTarget {\n    /// Resizes the render target to the specified width and height using the\n    /// provided device.\n    pub fn resize(&mut self, width: u32, height: u32, device: &wgpu::Device) {\n        match &mut self.0 {\n            RenderTargetInner::Surface {\n                surface,\n                surface_config,\n            } => {\n                surface_config.width = width;\n                surface_config.height = height;\n                surface.configure(device, surface_config)\n            }\n            RenderTargetInner::Texture { texture } => {\n                let usage = texture.usage();\n                let format = texture.format();\n                let texture_desc = wgpu::TextureDescriptor {\n                    size: wgpu::Extent3d {\n                        width,\n                        height,\n                        depth_or_array_layers: 1,\n                    },\n                    mip_level_count: 1,\n                    sample_count: 1,\n                    dimension: wgpu::TextureDimension::D2,\n                    format,\n                    usage,\n                    label: Some(\"RenderTarget texture\"),\n                    view_formats: &[],\n                };\n                *texture = Arc::new(device.create_texture(&texture_desc));\n            }\n        }\n    }\n\n    /// Returns the format of the render target.\n    pub fn format(&self) -> wgpu::TextureFormat {\n        match &self.0 {\n            RenderTargetInner::Surface { surface_config, .. } => surface_config.format,\n            RenderTargetInner::Texture { texture } => texture.format(),\n        }\n    }\n\n    /// Checks if the render target is headless (i.e., a texture).\n    pub fn is_headless(&self) -> bool {\n        match &self.0 {\n            RenderTargetInner::Surface { .. } => false,\n            RenderTargetInner::Texture { .. } => true,\n        }\n    }\n\n    /// Returns the underlying target as a texture, if possible.\n    pub fn as_texture(&self) -> Option<&wgpu::Texture> {\n        match &self.0 {\n            RenderTargetInner::Surface { .. } => None,\n            RenderTargetInner::Texture { texture } => Some(texture),\n        }\n    }\n\n    /// Returns the size of the render target as a `UVec2`.\n    pub fn get_size(&self) -> UVec2 {\n        match &self.0 {\n            RenderTargetInner::Surface {\n                surface: _,\n                surface_config,\n            } => UVec2::new(surface_config.width, surface_config.height),\n            RenderTargetInner::Texture { texture } => {\n                let s = texture.size();\n                UVec2::new(s.width, s.height)\n            }\n        }\n    }\n}\n\n#[derive(Debug, Snafu)]\n#[snafu(visibility(pub(crate)))]\n/// Represents errors that can occur within the rendering context.\npub enum ContextError {\n    #[snafu(display(\"missing surface texture: {}\", source))]\n    Surface { source: wgpu::SurfaceError },\n\n    #[snafu(display(\"cannot create adaptor: {source}\"))]\n    CannotCreateAdaptor { source: wgpu::RequestAdapterError },\n\n    #[snafu(display(\"cannot request device: {}\", source))]\n    CannotRequestDevice { source: wgpu::RequestDeviceError },\n\n    #[snafu(display(\"surface is incompatible with adapter\"))]\n    IncompatibleSurface,\n\n    #[snafu(display(\"could not create surface: {}\", source))]\n    CreateSurface { source: wgpu::CreateSurfaceError },\n}\n\n/// A thin wrapper over [`wgpu::TextureView`] returned by [`Frame::view`].\npub struct FrameTextureView {\n    pub view: Arc<wgpu::TextureView>,\n    pub format: wgpu::TextureFormat,\n}\n\nimpl Deref for FrameTextureView {\n    type Target = wgpu::TextureView;\n\n    fn deref(&self) -> &Self::Target {\n        &self.view\n    }\n}\n\n/// Represents the surface of a frame, which can either be a surface texture or\n/// a texture.\npub(crate) enum FrameSurface {\n    Surface(wgpu::SurfaceTexture),\n    Texture(Arc<wgpu::Texture>),\n}\n\n/// Represents the current frame of a render target.\n///\n/// Returned by [`Context::get_next_frame`].\npub struct Frame {\n    pub(crate) runtime: WgpuRuntime,\n    pub(crate) surface: FrameSurface,\n}\n\nimpl Frame {\n    /// Returns the underlying texture of this target.\n    pub fn texture(&self) -> &wgpu::Texture {\n        match &self.surface {\n            FrameSurface::Surface(s) => &s.texture,\n            FrameSurface::Texture(t) => t,\n        }\n    }\n\n    /// Returns a view of the current frame's texture.\n    pub fn view(&self) -> wgpu::TextureView {\n        let texture = self.texture();\n        let format = texture.format().add_srgb_suffix();\n        texture.create_view(&wgpu::TextureViewDescriptor {\n            label: Some(\"Frame::view\"),\n            format: Some(format),\n            ..Default::default()\n        })\n    }\n\n    /// Copies the current frame to a buffer for further processing.\n    pub fn copy_to_buffer(&self, width: u32, height: u32) -> CopiedTextureBuffer {\n        let dimensions = BufferDimensions::new(4, 1, width as usize, height as usize);\n        // The output buffer lets us retrieve the self as an array\n        let buffer = self.runtime.device.create_buffer(&wgpu::BufferDescriptor {\n            label: Some(\"RenderTarget::copy_to_buffer\"),\n            size: (dimensions.padded_bytes_per_row * dimensions.height) as u64,\n            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,\n            mapped_at_creation: false,\n        });\n        let mut encoder =\n            self.runtime\n                .device\n                .create_command_encoder(&wgpu::CommandEncoderDescriptor {\n                    label: Some(\"post render screen capture encoder\"),\n                });\n        let texture = self.texture();\n        // Copy the data from the surface texture to the buffer\n        encoder.copy_texture_to_buffer(\n            texture.as_image_copy(),\n            wgpu::TexelCopyBufferInfo {\n                buffer: &buffer,\n                layout: wgpu::TexelCopyBufferLayout {\n                    offset: 0,\n                    bytes_per_row: Some(dimensions.padded_bytes_per_row as u32),\n                    rows_per_image: None,\n                },\n            },\n            wgpu::Extent3d {\n                width: dimensions.width as u32,\n                height: dimensions.height as u32,\n                depth_or_array_layers: 1,\n            },\n        );\n\n        self.runtime.queue.submit(std::iter::once(encoder.finish()));\n\n        CopiedTextureBuffer {\n            dimensions,\n            buffer,\n            format: texture.format(),\n        }\n    }\n\n    pub fn get_size(&self) -> UVec2 {\n        let s = self.texture().size();\n        UVec2::new(s.width, s.height)\n    }\n\n    /// Reads the current frame buffer into an image.\n    ///\n    /// This should be called after rendering, before presentation.\n    /// Good for getting headless screen grabs.\n    ///\n    /// The resulting image will be in the color space of the frame.\n    ///\n    /// ## Note\n    /// This operation can take a long time, depending on how big the screen is.\n    pub async fn read_image(&self) -> Result<image::RgbaImage, TextureError> {\n        let size = self.get_size();\n        let buffer = self.copy_to_buffer(size.x, size.y);\n        let is_srgb = self.texture().format().is_srgb();\n        let img = if is_srgb {\n            buffer.into_srgba(&self.runtime.device).await?\n        } else {\n            buffer.into_linear_rgba(&self.runtime.device).await?\n        };\n        Ok(img)\n    }\n\n    /// Reads the frame into an image in a sRGB color space.\n    ///\n    /// This should be called after rendering, before presentation.\n    /// Good for getting headless screen grabs.\n    ///\n    /// ## Note\n    /// This operation can take a long time, depending on how big the screen is.\n    pub async fn read_srgb_image(&self) -> Result<image::RgbaImage, TextureError> {\n        let size = self.get_size();\n        let buffer = self.copy_to_buffer(size.x, size.y);\n        log::trace!(\"read image has the format: {:?}\", buffer.format);\n        buffer.into_srgba(&self.runtime.device).await\n    }\n    /// Reads the frame into an image in a linear color space.\n    ///\n    /// This should be called after rendering, before presentation.\n    /// Good for getting headless screen grabs.\n    ///\n    /// ## Note\n    /// This operation can take a long time, depending on how big the screen is.\n    pub async fn read_linear_image(&self) -> Result<image::RgbaImage, TextureError> {\n        let size = self.get_size();\n        let buffer = self.copy_to_buffer(size.x, size.y);\n        buffer.into_linear_rgba(&self.runtime.device).await\n    }\n\n    /// Presents the surface frame if the frame is a `TargetFrame::Surface`.\n    /// If the frame is a `TargetFrame::Texture`, this is a no-op.\n    pub fn present(self) {\n        match self.surface {\n            FrameSurface::Surface(s) => s.present(),\n            FrameSurface::Texture(_) => {}\n        }\n    }\n}\n\n/// Configurable default values to use when creating new [`Stage`]s.\n#[derive(Debug, Clone, Copy)]\npub(crate) struct GlobalStageConfig {\n    pub(crate) atlas_size: wgpu::Extent3d,\n    pub(crate) shadow_map_atlas_size: wgpu::Extent3d,\n    pub(crate) use_compute_culling: bool,\n}\n\n/// Contains the adapter, device, queue, [`RenderTarget`] and configuration.\n///\n/// A `Context` is created to initialize rendering to a window, canvas or\n/// texture.\n///\n/// ```\n/// use renderling::context::Context;\n///\n/// let ctx = futures_lite::future::block_on(Context::headless(100, 100));\n/// ```\npub struct Context {\n    runtime: WgpuRuntime,\n    adapter: Arc<wgpu::Adapter>,\n    render_target: RenderTarget,\n    pub(crate) stage_config: Arc<RwLock<GlobalStageConfig>>,\n}\n\nimpl AsRef<WgpuRuntime> for Context {\n    fn as_ref(&self) -> &WgpuRuntime {\n        &self.runtime\n    }\n}\n\nimpl Context {\n    /// Creates a new `Context` with the specified target, adapter, device, and\n    /// queue.\n    pub fn new(\n        target: RenderTarget,\n        adapter: impl Into<Arc<wgpu::Adapter>>,\n        device: impl Into<Arc<wgpu::Device>>,\n        queue: impl Into<Arc<wgpu::Queue>>,\n    ) -> Self {\n        let adapter: Arc<wgpu::Adapter> = adapter.into();\n        let limits = adapter.limits();\n        let w = limits\n            .max_texture_dimension_2d\n            .min(crate::atlas::ATLAS_SUGGESTED_SIZE);\n        let stage_config = Arc::new(RwLock::new(GlobalStageConfig {\n            atlas_size: wgpu::Extent3d {\n                width: w,\n                height: w,\n                depth_or_array_layers: adapter\n                    .limits()\n                    .max_texture_array_layers\n                    .min(crate::atlas::ATLAS_SUGGESTED_LAYERS),\n            },\n            shadow_map_atlas_size: wgpu::Extent3d {\n                width: w,\n                height: w,\n                depth_or_array_layers: 4,\n            },\n            use_compute_culling: false,\n        }));\n        Self {\n            adapter,\n            runtime: WgpuRuntime {\n                device: device.into(),\n                queue: queue.into(),\n            },\n            render_target: target,\n            stage_config,\n        }\n    }\n\n    /// Attempts to create a new headless `Context` with the specified width,\n    /// height, and backends.\n    pub async fn try_new_headless(\n        width: u32,\n        height: u32,\n        backends: Option<wgpu::Backends>,\n    ) -> Result<Self, ContextError> {\n        log::trace!(\"creating headless context of size ({width}, {height})\");\n        let instance = crate::internal::new_instance(backends);\n        let (adapter, device, queue, target) =\n            crate::internal::new_headless_device_queue_and_target(width, height, &instance).await?;\n        Ok(Self::new(target, adapter, device, queue))\n    }\n\n    /// Attempts to create a new `Context` with a surface, using the specified\n    /// width, height, backends, and window.\n    pub async fn try_new_with_surface(\n        width: u32,\n        height: u32,\n        backends: Option<wgpu::Backends>,\n        window: impl Into<wgpu::SurfaceTarget<'static>>,\n    ) -> Result<Self, ContextError> {\n        let instance = crate::internal::new_instance(backends);\n        let (adapter, device, queue, target) =\n            crate::internal::new_windowed_adapter_device_queue(width, height, &instance, window)\n                .await?;\n        Ok(Self::new(target, adapter, device, queue))\n    }\n\n    #[cfg(feature = \"winit\")]\n    /// Create a [`Context`] from an existing [`winit::window::Window`].\n    ///\n    /// ## Panics\n    /// Panics if the context could not be created.\n    pub async fn from_winit_window(\n        backends: Option<wgpu::Backends>,\n        window: Arc<winit::window::Window>,\n    ) -> Self {\n        let inner_size = window.inner_size();\n        Self::try_new_with_surface(inner_size.width, inner_size.height, backends, window)\n            .await\n            .unwrap()\n    }\n\n    /// Creates a new headless renderer.\n    ///\n    /// Immediately proxies to `Context::try_new_headless` and unwraps.\n    ///\n    /// ## Panics\n    /// This function will panic if an adapter cannot be found. For example,\n    /// this would happen on machines without a GPU.\n    pub async fn headless(width: u32, height: u32) -> Self {\n        let result = Self::try_new_headless(width, height, None).await;\n        #[cfg(target_arch = \"wasm32\")]\n        {\n            use wasm_bindgen::UnwrapThrowExt;\n            result.expect_throw(\"Could not create context\")\n        }\n        #[cfg(not(target_arch = \"wasm32\"))]\n        {\n            result.expect(\"Could not create context\")\n        }\n    }\n\n    pub fn get_size(&self) -> UVec2 {\n        self.render_target.get_size()\n    }\n\n    /// Sets the size of the render target.\n    pub fn set_size(&mut self, size: UVec2) {\n        self.render_target\n            .resize(size.x, size.y, &self.runtime.device);\n    }\n\n    /// Creates a texture from an image buffer.\n    pub fn create_texture<P>(\n        &self,\n        label: Option<&str>,\n        img: &image::ImageBuffer<P, Vec<u8>>,\n    ) -> Result<Texture, TextureError>\n    where\n        P: image::PixelWithColorType,\n        image::ImageBuffer<P, Vec<u8>>: image::GenericImage + std::ops::Deref<Target = [u8]>,\n    {\n        let name = label.unwrap_or(\"unknown\");\n        Texture::from_image_buffer(\n            self,\n            img,\n            Some(&format!(\"Renderling::create_texture {}\", name)),\n            None,\n            None,\n        )\n    }\n\n    /// Creates a `Texture` from a `wgpu::Texture` and an optional sampler.\n    pub fn texture_from_wgpu_tex(\n        &self,\n        texture: impl Into<Arc<wgpu::Texture>>,\n        sampler: Option<wgpu::SamplerDescriptor>,\n    ) -> Texture {\n        Texture::from_wgpu_tex(self.get_device(), texture, sampler, None)\n    }\n\n    /// Returns a reference to the `WgpuRuntime`.\n    pub fn runtime(&self) -> &WgpuRuntime {\n        &self.runtime\n    }\n\n    /// Returns a reference to the `wgpu::Device`.\n    pub fn get_device(&self) -> &wgpu::Device {\n        &self.runtime.device\n    }\n\n    /// Returns a reference to the `wgpu::Queue`.\n    pub fn get_queue(&self) -> &wgpu::Queue {\n        &self.runtime.queue\n    }\n\n    /// Returns a reference to the `wgpu::Adapter`.\n    pub fn get_adapter(&self) -> &wgpu::Adapter {\n        &self.adapter\n    }\n\n    /// Returns the adapter in an owned wrapper.\n    pub fn get_adapter_owned(&self) -> Arc<wgpu::Adapter> {\n        self.adapter.clone()\n    }\n\n    /// Returns a reference to the `RenderTarget`.\n    pub fn get_render_target(&self) -> &RenderTarget {\n        &self.render_target\n    }\n\n    /// Gets the next frame from the render target.\n    ///\n    /// A surface context (window or canvas) will return the next swapchain\n    /// texture.\n    ///\n    /// A headless context will return the underlying headless texture.\n    ///\n    /// ## Errors\n    /// Errs if the render target is a surface and there was an error getting\n    /// the next swapchain texture. This can happen if the frame has already\n    /// been acquired.\n    pub fn get_next_frame(&self) -> Result<Frame, ContextError> {\n        Ok(Frame {\n            runtime: self.runtime.clone(),\n            surface: match &self.render_target.0 {\n                RenderTargetInner::Surface { surface, .. } => {\n                    let surface_texture = surface.get_current_texture().context(SurfaceSnafu)?;\n                    FrameSurface::Surface(surface_texture)\n                }\n                RenderTargetInner::Texture { texture, .. } => {\n                    FrameSurface::Texture(texture.clone())\n                }\n            },\n        })\n    }\n\n    /// Sets the default texture size for the material atlas.\n    ///\n    /// * Width is `size.x` and must be a power of two.\n    /// * Height is `size.y`, must match `size.x` and must be a power of two.\n    /// * Layers is `size.z` and must be two or greater.\n    pub fn set_default_atlas_texture_size(&self, size: impl Into<UVec3>) -> &Self {\n        let size = size.into();\n        let size = wgpu::Extent3d {\n            width: size.x,\n            height: size.y,\n            depth_or_array_layers: size.z,\n        };\n        crate::atlas::check_size(size);\n        self.stage_config\n            .write()\n            .expect(\"stage_config write\")\n            .atlas_size = size;\n        self\n    }\n\n    /// Sets the default texture size for the material atlas.\n    ///\n    /// * Width is `size.x` and must be a power of two.\n    /// * Height is `size.y`, must match `size.x` and must be a power of two.\n    /// * Layers is `size.z` and must be greater than zero.\n    ///\n    /// ## Panics\n    /// Will panic if the above conditions are not met.\n    pub fn with_default_atlas_texture_size(self, size: impl Into<UVec3>) -> Self {\n        self.set_default_atlas_texture_size(size);\n        self\n    }\n\n    /// Sets the default texture size for the shadow mapping atlas.\n    ///\n    /// * Width is `size.x` and must be a power of two.\n    /// * Height is `size.y`, must match `size.x` and must be a power of two.\n    /// * Layers is `size.z` and must be two or greater.\n    pub fn set_shadow_mapping_atlas_texture_size(&self, size: impl Into<UVec3>) -> &Self {\n        let size = size.into();\n        let size = wgpu::Extent3d {\n            width: size.x,\n            height: size.y,\n            depth_or_array_layers: size.z,\n        };\n        crate::atlas::check_size(size);\n        self.stage_config\n            .write()\n            .expect(\"stage_config write\")\n            .shadow_map_atlas_size = size;\n        self\n    }\n\n    /// Sets the default texture size for the shadow mapping atlas.\n    ///\n    /// * Width is `size.x` and must be a power of two.\n    /// * Height is `size.y`, must match `size.x` and must be a power of two.\n    /// * Layers is `size.z` and must be greater than zero.\n    ///\n    /// ## Panics\n    /// Will panic if the above conditions are not met.\n    pub fn with_shadow_mapping_atlas_texture_size(self, size: impl Into<UVec3>) -> Self {\n        self.set_shadow_mapping_atlas_texture_size(size);\n        self\n    }\n\n    /// Sets the use of direct drawing.\n    ///\n    /// Default is **false**.\n    ///\n    /// If set to **true**, all compute culling, including frustum and occlusion\n    /// culling, will **not** run.\n    pub fn set_use_direct_draw(&self, use_direct_drawing: bool) {\n        self.stage_config\n            .write()\n            .expect(\"stage_config write\")\n            .use_compute_culling = !use_direct_drawing;\n    }\n\n    /// Sets the use of direct drawing.\n    ///\n    /// Default is **false**.\n    ///\n    /// If set to **true**, all compute culling is turned **off**.\n    /// This includes frustum and occlusion culling.\n    pub fn with_use_direct_draw(self, use_direct_drawing: bool) -> Self {\n        self.set_use_direct_draw(use_direct_drawing);\n        self\n    }\n\n    /// Returns whether direct drawing is used.\n    pub fn get_use_direct_draw(&self) -> bool {\n        !self\n            .stage_config\n            .read()\n            .expect(\"stage_config read\")\n            .use_compute_culling\n    }\n\n    /// Creates and returns a new [`Stage`] renderer.\n    pub fn new_stage(&self) -> Stage {\n        Stage::new(self)\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/convolution.rs",
    "content": "//! Convolution shaders.\n//!\n//! These shaders convolve various functions to produce cached maps.\n\npub mod shader {\n    //! Shader side of convolution.\n    use crabslab::{Id, Slab, SlabItem};\n    use glam::{Vec2, Vec3, Vec4, Vec4Swizzles};\n    use spirv_std::{\n        image::{Cubemap, Image2d},\n        num_traits::Zero,\n        spirv, Sampler,\n    };\n\n    #[allow(unused_imports)]\n    use spirv_std::num_traits::Float;\n\n    use crate::{camera::shader::CameraDescriptor, math::IsVector};\n\n    // Allow manual bit rotation because this code is `no_std`.\n    #[allow(clippy::manual_rotate)]\n    fn radical_inverse_vdc(mut bits: u32) -> f32 {\n        bits = (bits << 16u32) | (bits >> 16u32);\n        bits = ((bits & 0x55555555u32) << 1u32) | ((bits & 0xAAAAAAAAu32) >> 1u32);\n        bits = ((bits & 0x33333333u32) << 2u32) | ((bits & 0xCCCCCCCCu32) >> 2u32);\n        bits = ((bits & 0x0F0F0F0Fu32) << 4u32) | ((bits & 0xF0F0F0F0u32) >> 4u32);\n        bits = ((bits & 0x00FF00FFu32) << 8u32) | ((bits & 0xFF00FF00u32) >> 8u32);\n        (bits as f32) * 2.328_306_4e-10 // / 0x100000000\n    }\n\n    fn hammersley(i: u32, n: u32) -> Vec2 {\n        Vec2::new(i as f32 / n as f32, radical_inverse_vdc(i))\n    }\n\n    fn importance_sample_ggx(xi: Vec2, n: Vec3, roughness: f32) -> Vec3 {\n        let a = roughness * roughness;\n\n        let phi = 2.0 * core::f32::consts::PI * xi.x;\n        let cos_theta = f32::sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y));\n        let sin_theta = f32::sqrt(1.0 - cos_theta * cos_theta);\n\n        // Convert spherical to cartesian coordinates\n        let h = Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta);\n\n        // Convert tangent-space vector to world-space vector\n        let up = if n.z.abs() < 0.999 {\n            Vec3::new(0.0, 0.0, 1.0)\n        } else {\n            Vec3::new(1.0, 0.0, 0.0)\n        };\n        let tangent = up.cross(n).alt_norm_or_zero();\n        let bitangent = n.cross(tangent);\n\n        let result = tangent * h.x + bitangent * h.y + n * h.z;\n        result.alt_norm_or_zero()\n    }\n\n    fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 {\n        let r = roughness;\n        let k = (r * r) / 2.0;\n\n        let nom = n_dot_v;\n        let denom = n_dot_v * (1.0 - k) + k;\n\n        if denom.is_zero() {\n            0.0\n        } else {\n            nom / denom\n        }\n    }\n\n    fn geometry_smith(normal: Vec3, view_dir: Vec3, light_dir: Vec3, roughness: f32) -> f32 {\n        let n_dot_v = normal.dot(view_dir).max(0.0);\n        let n_dot_l = normal.dot(light_dir).max(0.0);\n        let ggx1 = geometry_schlick_ggx(n_dot_v, roughness);\n        let ggx2 = geometry_schlick_ggx(n_dot_l, roughness);\n\n        ggx1 * ggx2\n    }\n\n    const SAMPLE_COUNT: u32 = 1024;\n\n    pub fn integrate_brdf(mut n_dot_v: f32, roughness: f32) -> Vec2 {\n        n_dot_v = n_dot_v.max(f32::EPSILON);\n        let v = Vec3::new(f32::sqrt(1.0 - n_dot_v * n_dot_v), 0.0, n_dot_v);\n\n        let mut a = 0.0f32;\n        let mut b = 0.0f32;\n\n        let n = Vec3::Z;\n\n        for i in 1..SAMPLE_COUNT {\n            let xi = hammersley(i, SAMPLE_COUNT);\n            let h = importance_sample_ggx(xi, n, roughness);\n            let l = (2.0 * v.dot(h) * h - v).alt_norm_or_zero();\n\n            let n_dot_l = l.z.max(0.0);\n            let n_dot_h = h.z.max(0.0);\n            let v_dot_h = v.dot(h).max(0.0);\n\n            if n_dot_l > 0.0 {\n                let g = geometry_smith(n, v, l, roughness);\n                let denom = n_dot_h * n_dot_v;\n                let g_vis = (g * v_dot_h) / denom;\n                let f_c = (1.0 - v_dot_h).powf(5.0);\n\n                a += (1.0 - f_c) * g_vis;\n                b += f_c * g_vis;\n            }\n        }\n\n        a /= SAMPLE_COUNT as f32;\n        b /= SAMPLE_COUNT as f32;\n\n        Vec2::new(a, b)\n    }\n\n    /// This function doesn't work on rust-gpu, presumably because of the loop.\n    pub fn integrate_brdf_doesnt_work(mut n_dot_v: f32, roughness: f32) -> Vec2 {\n        n_dot_v = n_dot_v.max(f32::EPSILON);\n        let v = Vec3::new(f32::sqrt(1.0 - n_dot_v * n_dot_v), 0.0, n_dot_v);\n\n        let mut a = 0.0f32;\n        let mut b = 0.0f32;\n\n        let n = Vec3::Z;\n\n        let mut i = 0u32;\n        while i < SAMPLE_COUNT {\n            i += 1;\n\n            let xi = hammersley(i, SAMPLE_COUNT);\n            let h = importance_sample_ggx(xi, n, roughness);\n            let l = (2.0 * v.dot(h) * h - v).alt_norm_or_zero();\n\n            let n_dot_l = l.z.max(0.0);\n            let n_dot_h = h.z.max(0.0);\n            let v_dot_h = v.dot(h).max(0.0);\n\n            if n_dot_l > 0.0 {\n                let g = geometry_smith(n, v, l, roughness);\n                let denom = n_dot_h * n_dot_v;\n                let g_vis = (g * v_dot_h) / denom;\n                let f_c = (1.0 - v_dot_h).powf(5.0);\n\n                a += (1.0 - f_c) * g_vis;\n                b += f_c * g_vis;\n            }\n        }\n\n        a /= SAMPLE_COUNT as f32;\n        b /= SAMPLE_COUNT as f32;\n\n        Vec2::new(a, b)\n    }\n\n    /// Used by [`prefilter_environment_cubemap_vertex`] to read the camera and\n    /// roughness values from the slab.\n    #[derive(Clone, Copy, Default, SlabItem)]\n    pub struct VertexPrefilterEnvironmentCubemapIds {\n        pub camera: Id<CameraDescriptor>,\n        // TODO: does this have to be an Id? Pretty sure it can be inline\n        pub roughness: Id<f32>,\n    }\n\n    /// Vertex shader for rendering a \"prefilter environment\" cubemap.\n    #[spirv(vertex)]\n    pub fn prefilter_environment_cubemap_vertex(\n        #[spirv(instance_index)] prefilter_id: Id<VertexPrefilterEnvironmentCubemapIds>,\n        #[spirv(vertex_index)] vertex_id: u32,\n        #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n        out_pos: &mut Vec3,\n        out_roughness: &mut f32,\n        #[spirv(position)] gl_pos: &mut Vec4,\n    ) {\n        let in_pos = crate::math::CUBE[vertex_id as usize];\n        let VertexPrefilterEnvironmentCubemapIds { camera, roughness } = slab.read(prefilter_id);\n        let camera = slab.read(camera);\n        *out_roughness = slab.read(roughness);\n        *out_pos = in_pos;\n        *gl_pos = camera.view_projection() * in_pos.extend(1.0);\n    }\n\n    /// Fragment shader for rendering a \"prefilter environment\" cubemap.\n    ///\n    /// Lambertian prefilter.\n    #[spirv(fragment)]\n    pub fn prefilter_environment_cubemap_fragment(\n        #[spirv(descriptor_set = 0, binding = 1)] environment_cubemap: &Cubemap,\n        #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,\n        in_pos: Vec3,\n        in_roughness: f32,\n        frag_color: &mut Vec4,\n    ) {\n        let mut n = in_pos.alt_norm_or_zero();\n        // `wgpu` and vulkan's y coords are flipped from opengl\n        n.y *= -1.0;\n        let r = n;\n        let v = r;\n\n        let mut total_weight = 0.0f32;\n        let mut prefiltered_color = Vec3::ZERO;\n\n        for i in 0..SAMPLE_COUNT {\n            let xi = hammersley(i, SAMPLE_COUNT);\n            let h = importance_sample_ggx(xi, n, in_roughness);\n            let l = (2.0 * v.dot(h) * h - v).alt_norm_or_zero();\n\n            let n_dot_l = n.dot(l).max(0.0);\n            if n_dot_l > 0.0 {\n                let mip_level = if in_roughness == 0.0 {\n                    0.0\n                } else {\n                    calc_lod(n_dot_l)\n                };\n                prefiltered_color += environment_cubemap\n                    .sample_by_lod(*sampler, l, mip_level)\n                    .xyz()\n                    * n_dot_l;\n                total_weight += n_dot_l;\n            }\n        }\n\n        prefiltered_color /= total_weight;\n        *frag_color = prefiltered_color.extend(1.0);\n    }\n\n    pub fn calc_lod_old(n: Vec3, v: Vec3, h: Vec3, roughness: f32) -> f32 {\n        // sample from the environment's mip level based on roughness/pdf\n        let d = crate::pbr::shader::normal_distribution_ggx(n, h, roughness);\n        let n_dot_h = n.dot(h).max(0.0);\n        let h_dot_v = h.dot(v).max(0.0);\n        let pdf = (d * n_dot_h / (4.0 * h_dot_v)).max(f32::EPSILON);\n\n        let resolution = 512.0; // resolution of source cubemap (per face)\n        let sa_texel = 4.0 * core::f32::consts::PI / (6.0 * resolution * resolution);\n        let sa_sample = 1.0 / (SAMPLE_COUNT as f32 * pdf + f32::EPSILON);\n\n        0.5 * (sa_sample / sa_texel).log2()\n    }\n\n    pub fn calc_lod(n_dot_l: f32) -> f32 {\n        let cube_width = 512.0;\n        let pdf = (n_dot_l * core::f32::consts::FRAC_1_PI).max(0.0);\n        0.5 * (6.0 * cube_width * cube_width / (SAMPLE_COUNT as f32 * pdf).max(f32::EPSILON)).log2()\n    }\n\n    #[spirv(vertex)]\n    /// Vertex shader for generating texture mips.\n    pub fn generate_mipmap_vertex(\n        #[spirv(vertex_index)] vertex_id: u32,\n        out_uv: &mut Vec2,\n        #[spirv(position)] gl_pos: &mut Vec4,\n    ) {\n        let i = vertex_id as usize;\n        *out_uv = crate::math::UV_COORD_QUAD_CCW[i];\n        *gl_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i];\n    }\n\n    #[spirv(fragment)]\n    /// Fragment shader for generating texture mips.\n    pub fn generate_mipmap_fragment(\n        #[spirv(descriptor_set = 0, binding = 0)] texture: &Image2d,\n        #[spirv(descriptor_set = 0, binding = 1)] sampler: &Sampler,\n        in_uv: Vec2,\n        frag_color: &mut Vec4,\n    ) {\n        *frag_color = texture.sample(*sampler, in_uv);\n    }\n\n    #[repr(C)]\n    #[derive(Clone, Copy)]\n    struct Vert {\n        pos: [f32; 3],\n        uv: [f32; 2],\n    }\n\n    /// A screen-space quad.\n    const BRDF_VERTS: [Vert; 6] = {\n        let bl = Vert {\n            pos: [-1.0, -1.0, 0.0],\n            uv: [0.0, 1.0],\n        };\n        let br = Vert {\n            pos: [1.0, -1.0, 0.0],\n            uv: [1.0, 1.0],\n        };\n        let tl = Vert {\n            pos: [-1.0, 1.0, 0.0],\n            uv: [0.0, 0.0],\n        };\n        let tr = Vert {\n            pos: [1.0, 1.0, 0.0],\n            uv: [1.0, 0.0],\n        };\n\n        [bl, br, tr, bl, tr, tl]\n    };\n\n    #[spirv(vertex)]\n    /// Vertex shader for creating a BRDF LUT.\n    pub fn brdf_lut_convolution_vertex(\n        #[spirv(vertex_index)] vertex_id: u32,\n        out_uv: &mut glam::Vec2,\n        #[spirv(position)] gl_pos: &mut glam::Vec4,\n    ) {\n        let Vert { pos, uv } = BRDF_VERTS[vertex_id as usize];\n        *out_uv = Vec2::from(uv);\n        *gl_pos = Vec3::from(pos).extend(1.0);\n    }\n\n    #[spirv(fragment)]\n    /// Fragment shader for creating a BRDF LUT.\n    pub fn brdf_lut_convolution_fragment(in_uv: glam::Vec2, out_color: &mut glam::Vec2) {\n        *out_color = integrate_brdf(in_uv.x, in_uv.y);\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn integrate_brdf_sanity() {\n        let points = [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)];\n        for (x, y) in points.into_iter() {\n            assert!(\n                !shader::integrate_brdf(x, y).is_nan(),\n                \"brdf is NaN at {x},{y}\"\n            );\n        }\n        let size = 32;\n        let mut img = image::RgbaImage::new(size, size);\n        for (x, y, image::Rgba([r, g, _, a])) in img.enumerate_pixels_mut() {\n            let u = x as f32 / size as f32;\n            let v = y as f32 / size as f32;\n            let brdf = shader::integrate_brdf(u, v);\n            *r = (brdf.x * 255.0) as u8;\n            *g = (brdf.y * 255.0) as u8;\n            *a = 255;\n        }\n        img_diff::assert_img_eq(\"skybox/brdf_cpu.png\", img);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/cubemap/cpu.rs",
    "content": "//! CPU side of the cubemap module.\nuse std::sync::Arc;\n\nuse glam::{Mat4, UVec2, Vec3, Vec4};\nuse image::GenericImageView;\n\nuse crate::{\n    stage::{Stage, StageRendering},\n    texture::Texture,\n};\n\nuse super::shader::{CubemapDescriptor, CubemapFaceDirection};\n\npub fn cpu_sample_cubemap(cubemap: &[image::DynamicImage; 6], coord: Vec3) -> Vec4 {\n    let coord = coord.normalize_or(Vec3::X);\n    let (face_index, uv) = CubemapDescriptor::get_face_index_and_uv(coord);\n\n    // Get the selected image\n    let image = &cubemap[face_index];\n\n    // Convert 2D UV to pixel coordinates\n    let (width, height) = image.dimensions();\n    let px = uv.x * (width as f32 - 1.0);\n    let py = uv.y * (height as f32 - 1.0);\n\n    // Sample using the nearest neighbor for simplicity\n    let image::Rgba([r, g, b, a]) = image.get_pixel(px.round() as u32, py.round() as u32);\n\n    // Convert the sampled pixel to Vec4\n    Vec4::new(\n        r as f32 / 255.0,\n        g as f32 / 255.0,\n        b as f32 / 255.0,\n        a as f32 / 255.0,\n    )\n}\n\n/// A cubemap that acts as a render target for an entire scene.\n///\n/// Use this to create and update a skybox with scene geometry.\npub struct SceneCubemap {\n    pipeline: Arc<wgpu::RenderPipeline>,\n    cubemap_texture: wgpu::Texture,\n    depth_texture: crate::texture::Texture,\n    clear_color: wgpu::Color,\n}\n\nimpl SceneCubemap {\n    pub fn new(\n        device: &wgpu::Device,\n        size: UVec2,\n        format: wgpu::TextureFormat,\n        clear_color: Vec4,\n    ) -> Self {\n        let label = Some(\"scene-to-cubemap\");\n        let cubemap_texture = device.create_texture(&wgpu::TextureDescriptor {\n            label,\n            size: wgpu::Extent3d {\n                width: size.x,\n                height: size.y,\n                depth_or_array_layers: 6,\n            },\n            mip_level_count: 1,\n            sample_count: 1,\n            dimension: wgpu::TextureDimension::D2,\n            format,\n            usage: wgpu::TextureUsages::RENDER_ATTACHMENT\n                | wgpu::TextureUsages::TEXTURE_BINDING\n                | wgpu::TextureUsages::COPY_DST\n                | wgpu::TextureUsages::COPY_SRC,\n            view_formats: &[],\n        });\n        let depth_texture = Texture::create_depth_texture(device, size.x, size.y, 1, label);\n        let pipeline = Arc::new(Stage::create_primitive_pipeline(device, format, 1));\n        Self {\n            pipeline,\n            cubemap_texture,\n            depth_texture,\n            clear_color: wgpu::Color {\n                r: clear_color.x as f64,\n                g: clear_color.y as f64,\n                b: clear_color.z as f64,\n                a: clear_color.w as f64,\n            },\n        }\n    }\n\n    pub fn run(&self, stage: &Stage) {\n        let previous_camera_id = stage.used_camera_id();\n\n        // create a new camera for our cube, and use it to render with\n        let camera = stage.geometry.new_camera();\n        stage.use_camera(&camera);\n\n        // By setting this to 90 degrees (PI/2 radians) we make sure the viewing field\n        // is exactly large enough to fill a single face of the cubemap such that all\n        // faces align correctly to each other at the edges.\n        let fovy = std::f32::consts::FRAC_PI_2;\n        let aspect = self.cubemap_texture.width() as f32 / self.cubemap_texture.height() as f32;\n        let projection = Mat4::perspective_lh(fovy, aspect, 1.0, 25.0);\n        // Render each face by rendering the scene from each camera angle into the\n        // cubemap\n        for (i, face) in CubemapFaceDirection::FACES.iter().enumerate() {\n            // Update the camera angle, no need to sync as calling `Stage::render` does this\n            // implicitly\n            camera.set_projection_and_view(projection, face.view());\n            let label_s = format!(\"scene-to-cubemap-{i}\");\n            let view = self\n                .cubemap_texture\n                .create_view(&wgpu::TextureViewDescriptor {\n                    label: Some(&label_s),\n                    base_array_layer: i as u32,\n                    array_layer_count: Some(1),\n                    dimension: Some(wgpu::TextureViewDimension::D2),\n                    ..Default::default()\n                });\n            let color_attachment = wgpu::RenderPassColorAttachment {\n                view: &view,\n                resolve_target: None,\n                ops: wgpu::Operations {\n                    load: wgpu::LoadOp::Clear(self.clear_color),\n                    store: wgpu::StoreOp::Store,\n                },\n                depth_slice: None,\n            };\n            let depth_stencil_attachment = wgpu::RenderPassDepthStencilAttachment {\n                view: &self.depth_texture.view,\n                depth_ops: Some(wgpu::Operations {\n                    load: wgpu::LoadOp::Clear(1.0),\n                    store: wgpu::StoreOp::Store,\n                }),\n                stencil_ops: None,\n            };\n            let (_, _) = StageRendering {\n                pipeline: &self.pipeline,\n                stage,\n                color_attachment,\n                depth_stencil_attachment,\n            }\n            .run();\n        }\n\n        stage.use_camera_id(previous_camera_id);\n    }\n}\n\n/// A render pipeline for blitting an equirectangular image as a cubemap.\npub struct EquirectangularImageToCubemapBlitter(pub wgpu::RenderPipeline);\n\nimpl EquirectangularImageToCubemapBlitter {\n    pub fn create_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {\n        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: Some(\"cubemap-making bindgroup\"),\n            entries: &[\n                wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::VERTEX,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n                wgpu::BindGroupLayoutEntry {\n                    binding: 1,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Texture {\n                        sample_type: wgpu::TextureSampleType::Float { filterable: false },\n                        view_dimension: wgpu::TextureViewDimension::D2,\n                        multisampled: false,\n                    },\n                    count: None,\n                },\n                wgpu::BindGroupLayoutEntry {\n                    binding: 2,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),\n                    count: None,\n                },\n            ],\n        })\n    }\n\n    pub fn create_bindgroup(\n        device: &wgpu::Device,\n        label: Option<&str>,\n        buffer: &wgpu::Buffer,\n        // The texture to sample the environment from\n        texture: &Texture,\n    ) -> wgpu::BindGroup {\n        device.create_bind_group(&wgpu::BindGroupDescriptor {\n            label,\n            layout: &Self::create_bindgroup_layout(device),\n            entries: &[\n                wgpu::BindGroupEntry {\n                    binding: 0,\n                    resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 1,\n                    resource: wgpu::BindingResource::TextureView(&texture.view),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 2,\n                    resource: wgpu::BindingResource::Sampler(&texture.sampler),\n                },\n            ],\n        })\n    }\n\n    /// Create the rendering pipeline that creates cubemaps from equirectangular\n    /// images.\n    pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {\n        log::trace!(\"creating cubemap-making render pipeline with format '{format:?}'\");\n        let vertex_linkage = crate::linkage::skybox_cubemap_vertex::linkage(device);\n        let fragment_linkage = crate::linkage::skybox_equirectangular_fragment::linkage(device);\n        let bg_layout = Self::create_bindgroup_layout(device);\n        let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label: Some(\"cubemap-making pipeline layout\"),\n            bind_group_layouts: &[&bg_layout],\n            push_constant_ranges: &[],\n        });\n        EquirectangularImageToCubemapBlitter(device.create_render_pipeline(\n            &wgpu::RenderPipelineDescriptor {\n                label: Some(\"cubemap-making pipeline\"),\n                layout: Some(&pp_layout),\n                vertex: wgpu::VertexState {\n                    module: &vertex_linkage.module,\n                    entry_point: Some(vertex_linkage.entry_point),\n                    buffers: &[],\n                    compilation_options: Default::default(),\n                },\n                primitive: wgpu::PrimitiveState {\n                    topology: wgpu::PrimitiveTopology::TriangleList,\n                    strip_index_format: None,\n                    front_face: wgpu::FrontFace::Ccw,\n                    cull_mode: None,\n                    unclipped_depth: false,\n                    polygon_mode: wgpu::PolygonMode::Fill,\n                    conservative: false,\n                },\n                depth_stencil: None,\n                multisample: wgpu::MultisampleState {\n                    mask: !0,\n                    alpha_to_coverage_enabled: false,\n                    count: 1,\n                },\n                fragment: Some(wgpu::FragmentState {\n                    module: &fragment_linkage.module,\n                    entry_point: Some(fragment_linkage.entry_point),\n                    targets: &[Some(wgpu::ColorTargetState {\n                        format,\n                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                        write_mask: wgpu::ColorWrites::ALL,\n                    })],\n                    compilation_options: Default::default(),\n                }),\n                multiview: None,\n                cache: None,\n            },\n        ))\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use craballoc::slab::SlabAllocator;\n    use glam::Vec4;\n    use image::GenericImageView;\n\n    use crate::{\n        context::Context,\n        geometry::Vertex,\n        math::{UNIT_INDICES, UNIT_POINTS},\n        test::BlockOnFuture,\n        texture::CopiedTextureBuffer,\n    };\n\n    use super::*;\n\n    #[test]\n    fn hand_rolled_cubemap_sampling() {\n        let width = 256;\n        let height = 256;\n        let ctx = Context::headless(width, height).block();\n        let stage = ctx\n            .new_stage()\n            .with_background_color(Vec4::ZERO)\n            .with_lighting(false)\n            .with_msaa_sample_count(4);\n        let projection = crate::camera::perspective(width as f32, height as f32);\n        let view = Mat4::look_at_rh(Vec3::splat(3.0), Vec3::ZERO, Vec3::Y);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n        // geometry is the \"clip cube\" where colors are normalized 3d space coords\n        let _rez = stage\n            .new_primitive()\n            .with_vertices(stage.new_vertices(UNIT_POINTS.map(|unit_cube_point| {\n                Vertex::default()\n                    // multiply by 2.0 because the unit cube's AABB bounds are at 0.5, and we want\n                    // 1.0\n                    .with_position(unit_cube_point * 2.0)\n                    // \"normalize\" (really \"shift\") the space coord from [-0.5, 0.5] to [0.0, 1.0]\n                    .with_color((unit_cube_point + 0.5).extend(1.0))\n            })))\n            .with_indices(stage.new_indices(UNIT_INDICES.map(|u| u as u32)));\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"cubemap/hand_rolled_cubemap_sampling/cube.png\", img);\n        frame.present();\n\n        let scene_cubemap = SceneCubemap::new(\n            ctx.get_device(),\n            UVec2::new(width, height),\n            wgpu::TextureFormat::Rgba8Unorm,\n            Vec4::ZERO,\n        );\n        scene_cubemap.run(&stage);\n\n        let slab = SlabAllocator::new(&ctx, \"cubemap-sampling-test\", wgpu::BufferUsages::empty());\n        let uv = slab.new_value(Vec3::ZERO);\n        let buffer = slab.commit();\n        let label = Some(\"cubemap-sampling-test\");\n        let bind_group_layout =\n            ctx.get_device()\n                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n                    label,\n                    entries: &[\n                        wgpu::BindGroupLayoutEntry {\n                            binding: 0,\n                            visibility: wgpu::ShaderStages::VERTEX,\n                            ty: wgpu::BindingType::Buffer {\n                                ty: wgpu::BufferBindingType::Storage { read_only: true },\n                                has_dynamic_offset: false,\n                                min_binding_size: None,\n                            },\n                            count: None,\n                        },\n                        wgpu::BindGroupLayoutEntry {\n                            binding: 1,\n                            visibility: wgpu::ShaderStages::FRAGMENT,\n                            ty: wgpu::BindingType::Texture {\n                                sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                                view_dimension: wgpu::TextureViewDimension::Cube,\n                                multisampled: false,\n                            },\n                            count: None,\n                        },\n                        wgpu::BindGroupLayoutEntry {\n                            binding: 2,\n                            visibility: wgpu::ShaderStages::FRAGMENT,\n                            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),\n                            count: None,\n                        },\n                    ],\n                });\n        let cubemap_sampling_pipeline_layout =\n            ctx.get_device()\n                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n                    label,\n                    bind_group_layouts: &[&bind_group_layout],\n                    push_constant_ranges: &[],\n                });\n        let vertex = crate::linkage::cubemap_sampling_test_vertex::linkage(ctx.get_device());\n        let fragment = crate::linkage::cubemap_sampling_test_fragment::linkage(ctx.get_device());\n        let cubemap_sampling_pipeline =\n            ctx.get_device()\n                .create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n                    label,\n                    layout: Some(&cubemap_sampling_pipeline_layout),\n                    vertex: wgpu::VertexState {\n                        module: &vertex.module,\n                        entry_point: Some(vertex.entry_point),\n                        compilation_options: wgpu::PipelineCompilationOptions::default(),\n                        buffers: &[],\n                    },\n                    primitive: wgpu::PrimitiveState {\n                        topology: wgpu::PrimitiveTopology::TriangleList,\n                        strip_index_format: None,\n                        front_face: wgpu::FrontFace::Ccw,\n                        cull_mode: None,\n                        unclipped_depth: false,\n                        polygon_mode: wgpu::PolygonMode::Fill,\n                        conservative: false,\n                    },\n                    depth_stencil: None,\n                    multisample: wgpu::MultisampleState::default(),\n                    fragment: Some(wgpu::FragmentState {\n                        module: &fragment.module,\n                        entry_point: Some(fragment.entry_point),\n                        compilation_options: Default::default(),\n                        targets: &[Some(wgpu::ColorTargetState {\n                            format: wgpu::TextureFormat::Rgba8Unorm,\n                            blend: None,\n                            write_mask: wgpu::ColorWrites::all(),\n                        })],\n                    }),\n                    multiview: None,\n                    cache: None,\n                });\n\n        let cubemap_view =\n            scene_cubemap\n                .cubemap_texture\n                .create_view(&wgpu::TextureViewDescriptor {\n                    label,\n                    dimension: Some(wgpu::TextureViewDimension::Cube),\n                    ..Default::default()\n                });\n        let cubemap_sampler = ctx.get_device().create_sampler(&wgpu::SamplerDescriptor {\n            label,\n            ..Default::default()\n        });\n        let bind_group = ctx\n            .get_device()\n            .create_bind_group(&wgpu::BindGroupDescriptor {\n                label,\n                layout: &bind_group_layout,\n                entries: &[\n                    wgpu::BindGroupEntry {\n                        binding: 0,\n                        resource: buffer.as_entire_binding(),\n                    },\n                    wgpu::BindGroupEntry {\n                        binding: 1,\n                        resource: wgpu::BindingResource::TextureView(&cubemap_view),\n                    },\n                    wgpu::BindGroupEntry {\n                        binding: 2,\n                        resource: wgpu::BindingResource::Sampler(&cubemap_sampler),\n                    },\n                ],\n            });\n        let render_target = ctx.get_device().create_texture(&wgpu::TextureDescriptor {\n            label,\n            size: wgpu::Extent3d {\n                width: 1,\n                height: 1,\n                depth_or_array_layers: 1,\n            },\n            mip_level_count: 1,\n            sample_count: 1,\n            dimension: wgpu::TextureDimension::D2,\n            format: wgpu::TextureFormat::Rgba8Unorm,\n            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,\n            view_formats: &[],\n        });\n        let render_target_view = render_target.create_view(&wgpu::TextureViewDescriptor::default());\n\n        let sample = |dir: Vec3| -> Vec4 {\n            uv.set(dir.normalize_or(Vec3::ZERO));\n            slab.commit();\n\n            let mut encoder = ctx\n                .get_device()\n                .create_command_encoder(&wgpu::CommandEncoderDescriptor { label });\n            {\n                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                    label,\n                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                        view: &render_target_view,\n                        resolve_target: None,\n                        ops: wgpu::Operations {\n                            load: wgpu::LoadOp::Clear(wgpu::Color {\n                                r: 0.0,\n                                g: 0.0,\n                                b: 0.0,\n                                a: 0.0,\n                            }),\n                            store: wgpu::StoreOp::Store,\n                        },\n                        depth_slice: None,\n                    })],\n                    depth_stencil_attachment: None,\n                    timestamp_writes: None,\n                    occlusion_query_set: None,\n                });\n                render_pass.set_pipeline(&cubemap_sampling_pipeline);\n                render_pass.set_bind_group(0, &bind_group, &[]);\n                render_pass.draw(0..6, 0..1);\n            }\n            let submission_index = ctx.get_queue().submit(Some(encoder.finish()));\n            ctx.get_device()\n                .poll(wgpu::PollType::WaitForSubmissionIndex(submission_index))\n                .unwrap();\n\n            let img = Texture::read(&ctx, &render_target, 1, 1, 4, 1)\n                .into_image::<u8, image::Rgba<u8>>(ctx.get_device())\n                .block()\n                .unwrap();\n            let image::Rgba([r, g, b, a]) = img.get_pixel(0, 0);\n            Vec4::new(\n                r as f32 / 255.0,\n                g as f32 / 255.0,\n                b as f32 / 255.0,\n                a as f32 / 255.0,\n            )\n        };\n\n        fn index_to_face_string(index: usize) -> &'static str {\n            match index {\n                0 => \"+X\",\n                1 => \"-X\",\n                2 => \"+Y\",\n                3 => \"-Y\",\n                4 => \"+Z\",\n                5 => \"-Z\",\n                _ => \"?\",\n            }\n        }\n\n        let mut cpu_cubemap = vec![];\n        for i in 0..6 {\n            let img = CopiedTextureBuffer::read_from(\n                &ctx,\n                &scene_cubemap.cubemap_texture,\n                width as usize,\n                height as usize,\n                4,\n                1,\n                0,\n                Some(wgpu::Origin3d { x: 0, y: 0, z: i }),\n            )\n            .into_image::<u8, image::Rgba<u8>>(ctx.get_device())\n            .block()\n            .unwrap();\n\n            img_diff::assert_img_eq(\n                &format!(\n                    \"cubemap/hand_rolled_cubemap_sampling/face_{}.png\",\n                    index_to_face_string(i as usize)\n                ),\n                img.clone(),\n            );\n\n            cpu_cubemap.push(img);\n        }\n        let cpu_cubemap = [\n            cpu_cubemap.remove(0),\n            cpu_cubemap.remove(0),\n            cpu_cubemap.remove(0),\n            cpu_cubemap.remove(0),\n            cpu_cubemap.remove(0),\n            cpu_cubemap.remove(0),\n        ];\n\n        {\n            // assert a few sanity checks on the cpu cubemap\n            println!(\"x samples sanity\");\n            let x_samples_uv = [\n                UVec2::ZERO,\n                UVec2::new(255, 0),\n                UVec2::new(127, 127),\n                UVec2::new(255, 255),\n                UVec2::new(0, 255),\n            ];\n\n            for uv in x_samples_uv {\n                let image::Rgba([r, g, b, a]) = cpu_cubemap[0].get_pixel(uv.x, uv.y);\n                println!(\"uv: {uv}\");\n                println!(\"rgba: {r} {g} {b} {a}\");\n            }\n        }\n\n        let mut uvs = vec![\n            // start with cardinal directions\n            Vec3::X,\n            Vec3::NEG_X,\n            Vec3::Y,\n            Vec3::NEG_Y,\n            Vec3::Z,\n            Vec3::NEG_Z,\n        ];\n\n        // add corners to the uvs to sample\n        for x in [-1.0, 1.0] {\n            for y in [-1.0, 1.0] {\n                for z in [-1.0, 1.0] {\n                    let uv = Vec3::new(x, y, z);\n                    uvs.push(uv);\n                }\n            }\n        }\n\n        // add in some deterministic pseudo-randomn points\n        {\n            let mut prng = crate::math::GpuRng::new(666);\n            let mut rf32 = move || prng.gen_f32(0.0, 1.0);\n            let mut rxvec3 = { || Vec3::new(f32::MAX, rf32(), rf32()).normalize_or(Vec3::X) };\n            // let mut rvec3 = || Vec3::new(rf32(), rf32(), rf32());\n            uvs.extend((0..20).map(|_| rxvec3()));\n        }\n\n        const THRESHOLD: f32 = 0.005;\n        for uv in uvs.into_iter() {\n            let nuv = uv.normalize_or(Vec3::X);\n            let color = sample(uv);\n            let (face_index, uv2d) =\n                CubemapDescriptor::get_face_index_and_uv(uv.normalize_or(Vec3::X));\n            let px = (uv2d.x * (width as f32 - 1.0)).round() as u32;\n            let py = (uv2d.y * (height as f32 - 1.0)).round() as u32;\n            let puv = UVec2::new(px, py);\n            let cpu_color = cpu_sample_cubemap(&cpu_cubemap, uv);\n            let dir_string = index_to_face_string(face_index);\n            println!(\n                \"__uv: {uv},\\n_nuv: {nuv},\\n_gpu: {color}\\n_cpu: {cpu_color}\\nfrom: \\\n                 {dir_string}({face_index}) {uv2d} {puv}\\n\"\n            );\n            let cmp = pretty_assertions::Comparison::new(&color, &cpu_color);\n            let distance = color.distance(cpu_color);\n            if distance > THRESHOLD {\n                println!(\"distance: {distance}\");\n                println!(\"{cmp}\");\n                panic!(\"distance {distance} greater than {THRESHOLD}\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/cubemap/shader.rs",
    "content": "use crabslab::{Array, Id, Slab};\nuse glam::{Mat4, Vec2, Vec3, Vec3Swizzles, Vec4};\nuse spirv_std::{num_traits::Zero, spirv};\n\nuse crate::{\n    atlas::shader::{AtlasDescriptor, AtlasTextureDescriptor},\n    math::{IsSampler, Sample2dArray},\n};\n\n/// Vertex shader for testing cubemap sampling.\n#[spirv(vertex)]\npub fn cubemap_sampling_test_vertex(\n    #[spirv(vertex_index)] vertex_index: u32,\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] uv: &Vec3,\n    out_uv: &mut Vec3,\n    #[spirv(position)] out_clip_coords: &mut Vec4,\n) {\n    let vertex_index = vertex_index as usize % 6;\n    *out_clip_coords = crate::math::CLIP_SPACE_COORD_QUAD_CCW[vertex_index];\n    *out_uv = *uv;\n}\n\n/// Vertex shader for testing cubemap sampling.\n#[spirv(fragment)]\npub fn cubemap_sampling_test_fragment(\n    #[spirv(descriptor_set = 0, binding = 1)] cubemap: &spirv_std::image::Cubemap,\n    #[spirv(descriptor_set = 0, binding = 2)] sampler: &spirv_std::Sampler,\n    in_uv: Vec3,\n    frag_color: &mut Vec4,\n) {\n    *frag_color = cubemap.sample(*sampler, in_uv);\n}\n\n/// Represents one side of a cubemap.\n///\n/// Assumes the camera is at the origin, inside the cube, with\n/// a left-handed coordinate system (+Z going into the screen).\n#[derive(Clone, Copy)]\npub struct CubemapFaceDirection {\n    /// Where is the camera\n    pub eye: Vec3,\n    /// Where is the camera looking\n    pub dir: Vec3,\n    /// Which direction is up\n    pub up: Vec3,\n}\n\nimpl CubemapFaceDirection {\n    pub const X: Self = Self {\n        eye: Vec3::ZERO,\n        dir: Vec3::X,\n        up: Vec3::Y,\n    };\n    pub const NEG_X: Self = Self {\n        eye: Vec3::ZERO,\n        dir: Vec3::NEG_X,\n        up: Vec3::Y,\n    };\n\n    pub const Y: Self = Self {\n        eye: Vec3::ZERO,\n        dir: Vec3::Y,\n        up: Vec3::NEG_Z,\n    };\n    pub const NEG_Y: Self = Self {\n        eye: Vec3::ZERO,\n        dir: Vec3::NEG_Y,\n        up: Vec3::Z,\n    };\n\n    pub const Z: Self = Self {\n        eye: Vec3::ZERO,\n        dir: Vec3::Z,\n        up: Vec3::Y,\n    };\n    pub const NEG_Z: Self = Self {\n        eye: Vec3::ZERO,\n        dir: Vec3::NEG_Z,\n        up: Vec3::Y,\n    };\n\n    pub const FACES: [Self; 6] = [\n        CubemapFaceDirection::X,\n        CubemapFaceDirection::NEG_X,\n        CubemapFaceDirection::Y,\n        CubemapFaceDirection::NEG_Y,\n        CubemapFaceDirection::Z,\n        CubemapFaceDirection::NEG_Z,\n    ];\n\n    pub fn right(&self) -> Vec3 {\n        -self.dir.cross(self.up)\n    }\n\n    /// The view from _inside_ the cube.\n    pub fn view(&self) -> Mat4 {\n        Mat4::look_at_lh(self.eye, self.eye + self.dir, self.up)\n    }\n}\n\npub struct CubemapDescriptor {\n    atlas_descriptor_id: Id<AtlasDescriptor>,\n    faces: Array<AtlasTextureDescriptor>,\n}\n\nimpl CubemapDescriptor {\n    /// Return the face index and UV coordinates that can be used to sample\n    /// a cubemap from the given directional coordinate.\n    pub fn get_face_index_and_uv(coord: Vec3) -> (usize, Vec2) {\n        let abs_x = coord.x.abs();\n        let abs_y = coord.y.abs();\n        let abs_z = coord.z.abs();\n\n        let (face_index, uv) = if abs_x >= abs_y && abs_x >= abs_z {\n            if coord.x > 0.0 {\n                (0, Vec2::new(-coord.z, -coord.y) / abs_x)\n            } else {\n                (1, Vec2::new(coord.z, -coord.y) / abs_x)\n            }\n        } else if abs_y >= abs_x && abs_y >= abs_z {\n            if coord.y > 0.0 {\n                (2, Vec2::new(coord.x, coord.z) / abs_y)\n            } else {\n                (3, Vec2::new(coord.x, -coord.z) / abs_y)\n            }\n        } else if coord.z > 0.0 {\n            (4, Vec2::new(coord.x, -coord.y) / abs_z)\n        } else {\n            (5, Vec2::new(-coord.x, -coord.y) / abs_z)\n        };\n\n        (face_index, (uv + Vec2::ONE) / 2.0)\n    }\n\n    /// Sample the cubemap with a directional coordinate.\n    pub fn sample<A, S>(&self, coord: Vec3, slab: &[u32], atlas: &A, sampler: &S) -> Vec4\n    where\n        A: Sample2dArray<Sampler = S>,\n        S: IsSampler,\n    {\n        let coord = if coord.length().is_zero() {\n            Vec3::X\n        } else {\n            coord.normalize()\n        };\n        let (face_index, uv) = Self::get_face_index_and_uv(coord);\n        let atlas_image = slab.read_unchecked(self.faces.at(face_index));\n        let atlas_desc = slab.read_unchecked(self.atlas_descriptor_id);\n        let uv = atlas_image.uv(uv, atlas_desc.size.xy());\n        atlas.sample_by_lod(*sampler, uv, 0.0)\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn cubemap_right() {\n        assert_eq!(Vec3::NEG_Z, CubemapFaceDirection::X.right());\n        assert_eq!(Vec3::Z, CubemapFaceDirection::NEG_X.right());\n        assert_eq!(Vec3::X, CubemapFaceDirection::Y.right());\n        assert_eq!(Vec3::X, CubemapFaceDirection::NEG_Y.right());\n        assert_eq!(Vec3::X, CubemapFaceDirection::Z.right());\n        assert_eq!(Vec3::NEG_X, CubemapFaceDirection::NEG_Z.right());\n\n        assert_eq!(\n            (1, Vec2::new(0.0, 1.0)),\n            CubemapDescriptor::get_face_index_and_uv(Vec3::NEG_ONE)\n        );\n    }\n\n    #[test]\n    fn cubemap_face_index() {\n        let center = Vec2::splat(0.5);\n        let data = [\n            (Vec3::X, 0, center),\n            (Vec3::NEG_X, 1, center),\n            (Vec3::Y, 2, center),\n            (Vec3::NEG_Y, 3, center),\n            (Vec3::Z, 4, center),\n            (Vec3::NEG_Z, 5, center),\n        ];\n        for (coord, expected_face_index, expected_uv) in data {\n            let (seen_face_index, seen_uv) = CubemapDescriptor::get_face_index_and_uv(coord);\n            dbg!((coord, seen_face_index, seen_uv));\n            assert_eq!(expected_face_index, seen_face_index);\n            assert_eq!(expected_uv, seen_uv);\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/cubemap.rs",
    "content": "//! Cubemap utilities.\n//!\n//! Shaders, render pipelines and layouts for creating and sampling cubemaps.\n//!\n//! For more info see:\n//! * <https://github.com/markpmlim/MetalCubemapping>\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\npub mod shader;\n\n#[cfg(test)]\nmod test {\n    use glam::{Vec2, Vec3};\n\n    use crate::cubemap::shader::{CubemapDescriptor, CubemapFaceDirection};\n\n    #[test]\n    fn cubemap_right() {\n        assert_eq!(Vec3::NEG_Z, CubemapFaceDirection::X.right());\n        assert_eq!(Vec3::Z, CubemapFaceDirection::NEG_X.right());\n        assert_eq!(Vec3::X, CubemapFaceDirection::Y.right());\n        assert_eq!(Vec3::X, CubemapFaceDirection::NEG_Y.right());\n        assert_eq!(Vec3::X, CubemapFaceDirection::Z.right());\n        assert_eq!(Vec3::NEG_X, CubemapFaceDirection::NEG_Z.right());\n\n        assert_eq!(\n            (1, Vec2::new(0.0, 1.0)),\n            CubemapDescriptor::get_face_index_and_uv(Vec3::NEG_ONE)\n        );\n    }\n\n    #[test]\n    fn cubemap_face_index() {\n        let center = Vec2::splat(0.5);\n        let data = [\n            (Vec3::X, 0, center),\n            (Vec3::NEG_X, 1, center),\n            (Vec3::Y, 2, center),\n            (Vec3::NEG_Y, 3, center),\n            (Vec3::Z, 4, center),\n            (Vec3::NEG_Z, 5, center),\n        ];\n        for (coord, expected_face_index, expected_uv) in data {\n            let (seen_face_index, seen_uv) = CubemapDescriptor::get_face_index_and_uv(coord);\n            dbg!((coord, seen_face_index, seen_uv));\n            assert_eq!(expected_face_index, seen_face_index);\n            assert_eq!(expected_uv, seen_uv);\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/cull/cpu.rs",
    "content": "//! CPU side of compute culling.\n\nuse craballoc::{\n    prelude::{GpuArray, Hybrid, SlabAllocator, SlabAllocatorError},\n    runtime::WgpuRuntime,\n    slab::SlabBuffer,\n};\nuse crabslab::{Array, Slab};\nuse glam::UVec2;\nuse snafu::{OptionExt, Snafu};\n\nuse crate::{bindgroup::ManagedBindGroup, texture::Texture};\n\nuse super::shader::DepthPyramidDescriptor;\n\n#[derive(Debug, Snafu)]\npub enum CullingError {\n    #[snafu(display(\n        \"Texture is not a depth texture, expected '{:?}' but saw '{seen:?}'\",\n        Texture::DEPTH_FORMAT\n    ))]\n    NotADepthTexture { seen: wgpu::TextureFormat },\n\n    #[snafu(display(\"Missing depth pyramid mip {index}\"))]\n    MissingMip { index: usize },\n\n    #[snafu(display(\"{source}\"))]\n    SlabError { source: SlabAllocatorError },\n\n    #[snafu(display(\"Could not read mip {index}\"))]\n    ReadMip { index: usize },\n}\n\nimpl From<SlabAllocatorError> for CullingError {\n    fn from(source: SlabAllocatorError) -> Self {\n        CullingError::SlabError { source }\n    }\n}\n\n/// Computes frustum and occlusion culling on the GPU.\npub struct ComputeCulling {\n    pipeline: wgpu::ComputePipeline,\n\n    pyramid_slab_buffer: SlabBuffer<wgpu::Buffer>,\n    stage_slab_buffer: SlabBuffer<wgpu::Buffer>,\n    indirect_slab_buffer: SlabBuffer<wgpu::Buffer>,\n\n    bindgroup_layout: wgpu::BindGroupLayout,\n    bindgroup: ManagedBindGroup,\n\n    pub(crate) compute_depth_pyramid: ComputeDepthPyramid,\n}\n\nimpl ComputeCulling {\n    const LABEL: Option<&'static str> = Some(\"compute-culling\");\n\n    fn new_bindgroup(\n        stage_slab_buffer: &wgpu::Buffer,\n        hzb_slab_buffer: &wgpu::Buffer,\n        indirect_buffer: &wgpu::Buffer,\n        layout: &wgpu::BindGroupLayout,\n        device: &wgpu::Device,\n    ) -> wgpu::BindGroup {\n        device.create_bind_group(&wgpu::BindGroupDescriptor {\n            label: Self::LABEL,\n            layout,\n            entries: &[\n                wgpu::BindGroupEntry {\n                    binding: 0,\n                    resource: wgpu::BindingResource::Buffer(\n                        stage_slab_buffer.as_entire_buffer_binding(),\n                    ),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 1,\n                    resource: wgpu::BindingResource::Buffer(\n                        hzb_slab_buffer.as_entire_buffer_binding(),\n                    ),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 2,\n                    resource: wgpu::BindingResource::Buffer(\n                        indirect_buffer.as_entire_buffer_binding(),\n                    ),\n                },\n            ],\n        })\n    }\n\n    pub fn new(\n        runtime: impl AsRef<WgpuRuntime>,\n        stage_slab_buffer: &SlabBuffer<wgpu::Buffer>,\n        indirect_slab_buffer: &SlabBuffer<wgpu::Buffer>,\n        depth_texture: &Texture,\n    ) -> Self {\n        let runtime = runtime.as_ref();\n        let device = &runtime.device;\n        let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: Self::LABEL,\n            entries: &[\n                wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::COMPUTE,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n                wgpu::BindGroupLayoutEntry {\n                    binding: 1,\n                    visibility: wgpu::ShaderStages::COMPUTE,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n                wgpu::BindGroupLayoutEntry {\n                    binding: 2,\n                    visibility: wgpu::ShaderStages::COMPUTE,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: false },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n            ],\n        });\n        let linkage = crate::linkage::compute_culling::linkage(device);\n        let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {\n            label: Self::LABEL,\n            layout: Some(\n                &device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n                    label: Self::LABEL,\n                    bind_group_layouts: &[&bindgroup_layout],\n                    push_constant_ranges: &[],\n                }),\n            ),\n            module: &linkage.module,\n            entry_point: Some(linkage.entry_point),\n            compilation_options: wgpu::PipelineCompilationOptions::default(),\n            cache: None,\n        });\n        let compute_depth_pyramid = ComputeDepthPyramid::new(runtime, depth_texture);\n        let pyramid_slab_buffer = compute_depth_pyramid\n            .compute_copy_depth\n            .pyramid_slab_buffer\n            .clone();\n        let bindgroup = Self::new_bindgroup(\n            stage_slab_buffer,\n            &pyramid_slab_buffer,\n            indirect_slab_buffer,\n            &bindgroup_layout,\n            device,\n        );\n        Self {\n            pipeline,\n            bindgroup_layout,\n            bindgroup: ManagedBindGroup::from(bindgroup),\n            compute_depth_pyramid,\n            pyramid_slab_buffer,\n            stage_slab_buffer: stage_slab_buffer.clone(),\n            indirect_slab_buffer: indirect_slab_buffer.clone(),\n        }\n    }\n\n    fn runtime(&self) -> &WgpuRuntime {\n        self.compute_depth_pyramid.depth_pyramid.slab.runtime()\n    }\n\n    pub fn run(&mut self, indirect_draw_count: u32, depth_texture: &Texture) {\n        log::trace!(\n            \"indirect_draw_count: {indirect_draw_count}, sample_count: {}\",\n            depth_texture.texture.sample_count()\n        );\n        // Compute the depth pyramid from last frame's depth buffer\n        self.compute_depth_pyramid.run(depth_texture);\n\n        let stage_slab_invalid = self.stage_slab_buffer.update_if_invalid();\n        let indirect_slab_invalid = self.indirect_slab_buffer.update_if_invalid();\n        let pyramid_slab_invalid = self.pyramid_slab_buffer.update_if_invalid();\n        let should_recreate_bindgroup =\n            stage_slab_invalid || indirect_slab_invalid || pyramid_slab_invalid;\n        log::trace!(\"stage_slab_invalid: {stage_slab_invalid}\");\n        log::trace!(\"indirect_slab_invalid: {indirect_slab_invalid}\");\n        log::trace!(\"pyramid_slab_invalid: {pyramid_slab_invalid}\");\n        let bindgroup = self.bindgroup.get(should_recreate_bindgroup, || {\n            log::debug!(\"recreating compute-culling bindgroup\");\n            Self::new_bindgroup(\n                &self.stage_slab_buffer,\n                &self.pyramid_slab_buffer,\n                &self.indirect_slab_buffer,\n                &self.bindgroup_layout,\n                self.compute_depth_pyramid.depth_pyramid.slab.device(),\n            )\n        });\n        let runtime = self.runtime();\n        let mut encoder = runtime\n            .device\n            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Self::LABEL });\n        {\n            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {\n                label: Self::LABEL,\n                timestamp_writes: None,\n            });\n            compute_pass.set_pipeline(&self.pipeline);\n            compute_pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]);\n            compute_pass.dispatch_workgroups(indirect_draw_count / 16 + 1, 1, 1);\n        }\n        runtime.queue.submit(Some(encoder.finish()));\n    }\n}\n\npub struct DepthPyramid {\n    slab: SlabAllocator<WgpuRuntime>,\n    desc: Hybrid<DepthPyramidDescriptor>,\n    mip: GpuArray<Array<f32>>,\n    mip_data: Vec<GpuArray<f32>>,\n}\n\nimpl DepthPyramid {\n    const LABEL: &str = \"depth-pyramid\";\n\n    fn allocate(\n        size: UVec2,\n        desc: &Hybrid<DepthPyramidDescriptor>,\n        slab: &SlabAllocator<WgpuRuntime>,\n    ) -> (Vec<GpuArray<f32>>, GpuArray<Array<f32>>) {\n        let mip_levels = size.min_element().ilog2();\n        let mip_data = (0..mip_levels)\n            .map(|i| {\n                let width = size.x >> i;\n                let height = size.y >> i;\n                slab.new_array(vec![0f32; (width * height) as usize])\n                    .into_gpu_only()\n            })\n            .collect::<Vec<_>>();\n        let mip = slab.new_array(mip_data.iter().map(|m| m.array()));\n        desc.set(DepthPyramidDescriptor {\n            size,\n            mip_level: 0,\n            mip: mip.array(),\n        });\n        (mip_data, mip.into_gpu_only())\n    }\n\n    pub fn new(runtime: impl AsRef<WgpuRuntime>, size: UVec2) -> Self {\n        let slab = SlabAllocator::new(runtime, Self::LABEL, wgpu::BufferUsages::empty());\n        let desc = slab.new_value(DepthPyramidDescriptor::default());\n        let (mip_data, mip) = Self::allocate(size, &desc, &slab);\n\n        Self {\n            slab,\n            desc,\n            mip_data,\n            mip,\n        }\n    }\n\n    pub fn resize(&mut self, size: UVec2) {\n        log::trace!(\"resizing depth pyramid to {size}\");\n        // drop the buffers\n        let mip = self.slab.new_array(vec![]);\n        self.mip_data = vec![];\n        self.desc.modify(|desc| desc.mip = mip.array());\n        self.mip = mip.into_gpu_only();\n\n        // Reclaim the dropped buffer slots\n        self.slab.commit();\n\n        // Reallocate\n        let (mip_data, mip) = Self::allocate(size, &self.desc, &self.slab);\n        self.mip_data = mip_data;\n        self.mip = mip;\n\n        // Run upkeep one more time to sync the resize\n        self.slab.commit();\n    }\n\n    pub fn size(&self) -> UVec2 {\n        self.desc.get().size\n    }\n\n    pub async fn read_images(&self) -> Result<Vec<image::GrayImage>, CullingError> {\n        let size = self.size();\n        let slab_data = self.slab.read(0..).await?;\n        let mut images = vec![];\n        let mut min = f32::MAX;\n        let mut max = f32::MIN;\n        for (i, mip) in self.mip_data.iter().enumerate() {\n            let depth_data: Vec<u8> = slab_data\n                .read_vec(mip.array())\n                .into_iter()\n                .map(|depth: f32| {\n                    if i == 0 {\n                        min = min.min(depth);\n                        max = max.max(depth);\n                    }\n                    crate::color::f32_to_u8(depth)\n                })\n                .collect();\n            log::trace!(\"min: {min}\");\n            log::trace!(\"max: {max}\");\n            let width = size.x >> i;\n            let height = size.y >> i;\n            let image = image::GrayImage::from_raw(width, height, depth_data)\n                .context(ReadMipSnafu { index: i })?;\n            images.push(image);\n        }\n        Ok(images)\n    }\n}\n\n/// Copies the depth texture to the top of the depth pyramid.\nstruct ComputeCopyDepth {\n    pipeline: wgpu::ComputePipeline,\n    bindgroup_layout: wgpu::BindGroupLayout,\n    sample_count: u32,\n    pyramid_slab_buffer: SlabBuffer<wgpu::Buffer>,\n    bindgroup: ManagedBindGroup,\n}\n\nimpl ComputeCopyDepth {\n    const LABEL: Option<&'static str> = Some(\"compute-occlusion-copy-depth-to-pyramid\");\n\n    fn create_bindgroup_layout(device: &wgpu::Device, sample_count: u32) -> wgpu::BindGroupLayout {\n        if sample_count > 1 {\n            log::trace!(\n                \"creating bindgroup layout with {sample_count} multisampled depth for {}\",\n                Self::LABEL.unwrap()\n            );\n        } else {\n            log::trace!(\n                \"creating bindgroup layout without multisampling for {}\",\n                Self::LABEL.unwrap()\n            );\n        }\n        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: Self::LABEL,\n            entries: &[\n                // slab\n                wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::COMPUTE,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: false },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n                // previous_mip: DepthPyramidImage\n                wgpu::BindGroupLayoutEntry {\n                    binding: 1,\n                    visibility: wgpu::ShaderStages::COMPUTE,\n                    ty: wgpu::BindingType::Texture {\n                        sample_type: wgpu::TextureSampleType::Depth,\n                        view_dimension: wgpu::TextureViewDimension::D2,\n                        multisampled: sample_count > 1,\n                    },\n                    count: None,\n                },\n            ],\n        })\n    }\n\n    fn create_pipeline(\n        device: &wgpu::Device,\n        bindgroup_layout: &wgpu::BindGroupLayout,\n        multisampled: bool,\n    ) -> wgpu::ComputePipeline {\n        let linkage = if multisampled {\n            log::trace!(\"creating multisampled shader for {}\", Self::LABEL.unwrap());\n            crate::linkage::compute_copy_depth_to_pyramid_multisampled::linkage(device)\n        } else {\n            log::trace!(\n                \"creating shader without multisampling for {}\",\n                Self::LABEL.unwrap()\n            );\n            crate::linkage::compute_copy_depth_to_pyramid::linkage(device)\n        };\n        device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {\n            label: Self::LABEL,\n            layout: Some(\n                &device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n                    label: Self::LABEL,\n                    bind_group_layouts: &[bindgroup_layout],\n                    push_constant_ranges: &[],\n                }),\n            ),\n            module: &linkage.module,\n            entry_point: Some(linkage.entry_point),\n            compilation_options: wgpu::PipelineCompilationOptions::default(),\n            cache: None,\n        })\n    }\n\n    fn create_bindgroup(\n        device: &wgpu::Device,\n        layout: &wgpu::BindGroupLayout,\n        pyramid_desc_buffer: &wgpu::Buffer,\n        depth_texture_view: &wgpu::TextureView,\n    ) -> wgpu::BindGroup {\n        device.create_bind_group(&wgpu::BindGroupDescriptor {\n            label: Self::LABEL,\n            layout,\n            entries: &[\n                wgpu::BindGroupEntry {\n                    binding: 0,\n                    resource: wgpu::BindingResource::Buffer(\n                        pyramid_desc_buffer.as_entire_buffer_binding(),\n                    ),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 1,\n                    resource: wgpu::BindingResource::TextureView(depth_texture_view),\n                },\n            ],\n        })\n    }\n\n    pub fn new(depth_pyramid: &DepthPyramid, depth_texture: &Texture) -> Self {\n        let device = depth_pyramid.slab.device();\n        let sample_count = depth_texture.texture.sample_count();\n        let bindgroup_layout = Self::create_bindgroup_layout(device, sample_count);\n        let pipeline = Self::create_pipeline(device, &bindgroup_layout, sample_count > 1);\n        let pyramid_slab_buffer = depth_pyramid.slab.commit();\n        let buffer = Self::create_bindgroup(\n            device,\n            &bindgroup_layout,\n            &pyramid_slab_buffer,\n            &depth_texture.view,\n        );\n        Self {\n            pipeline,\n            bindgroup: ManagedBindGroup::from(buffer),\n            bindgroup_layout,\n            pyramid_slab_buffer,\n            sample_count,\n        }\n    }\n\n    pub fn run(&mut self, pyramid: &mut DepthPyramid, depth_texture: &Texture) {\n        let _ = pyramid.desc.modify(|desc| {\n            desc.mip_level = 0;\n            desc.size\n        });\n\n        let runtime = pyramid.slab.runtime().clone();\n        let sample_count = depth_texture.texture.sample_count();\n        let sample_count_mismatch = sample_count != self.sample_count;\n        if sample_count_mismatch {\n            log::debug!(\n                \"sample count changed from {} to {}, updating {} bindgroup layout and pipeline\",\n                self.sample_count,\n                sample_count,\n                Self::LABEL.unwrap()\n            );\n            self.sample_count = sample_count;\n            self.bindgroup_layout = Self::create_bindgroup_layout(&runtime.device, sample_count);\n            self.pipeline =\n                Self::create_pipeline(&runtime.device, &self.bindgroup_layout, sample_count > 1);\n        }\n\n        let extent = depth_texture.texture.size();\n        let size = UVec2::new(extent.width, extent.height);\n        let size_changed = size != pyramid.size();\n        if size_changed {\n            pyramid.resize(size);\n        }\n\n        // TODO: check if we need to upkeep the depth pyramid slab here.\n        let _ = pyramid.slab.commit();\n        let should_recreate_bindgroup =\n            self.pyramid_slab_buffer.update_if_invalid() || sample_count_mismatch || size_changed;\n        let bindgroup = self.bindgroup.get(should_recreate_bindgroup, || {\n            Self::create_bindgroup(\n                &runtime.device,\n                &self.bindgroup_layout,\n                &self.pyramid_slab_buffer,\n                &depth_texture.view,\n            )\n        });\n\n        let mut encoder = runtime\n            .device\n            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Self::LABEL });\n        {\n            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {\n                label: Self::LABEL,\n                ..Default::default()\n            });\n            compute_pass.set_pipeline(&self.pipeline);\n            compute_pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]);\n            let x = size.x / 16 + 1;\n            let y = size.y / 16 + 1;\n            let z = 1;\n            compute_pass.dispatch_workgroups(x, y, z);\n        }\n        pyramid.slab.queue().submit(Some(encoder.finish()));\n    }\n}\n\n/// Downsamples the depth texture from one mip to the next.\nstruct ComputeDownsampleDepth {\n    pipeline: wgpu::ComputePipeline,\n    pyramid_slab_buffer: SlabBuffer<wgpu::Buffer>,\n    bindgroup: wgpu::BindGroup,\n    bindgroup_layout: wgpu::BindGroupLayout,\n}\n\nimpl ComputeDownsampleDepth {\n    const LABEL: Option<&'static str> = Some(\"compute-occlusion-downsample-depth\");\n\n    fn create_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {\n        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: Self::LABEL,\n            entries: &[\n                // slab\n                wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::COMPUTE,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: false },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n            ],\n        })\n    }\n\n    fn create_pipeline(\n        device: &wgpu::Device,\n        bindgroup_layout: &wgpu::BindGroupLayout,\n    ) -> wgpu::ComputePipeline {\n        let linkage = crate::linkage::compute_downsample_depth_pyramid::linkage(device);\n        device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {\n            label: Self::LABEL,\n            layout: Some(\n                &device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n                    label: Self::LABEL,\n                    bind_group_layouts: &[bindgroup_layout],\n                    push_constant_ranges: &[],\n                }),\n            ),\n            module: &linkage.module,\n            entry_point: Some(linkage.entry_point),\n            compilation_options: wgpu::PipelineCompilationOptions::default(),\n            cache: None,\n        })\n    }\n\n    fn create_bindgroup(\n        device: &wgpu::Device,\n        layout: &wgpu::BindGroupLayout,\n        pyramid_desc_buffer: &wgpu::Buffer,\n    ) -> wgpu::BindGroup {\n        device.create_bind_group(&wgpu::BindGroupDescriptor {\n            label: Self::LABEL,\n            layout,\n            entries: &[wgpu::BindGroupEntry {\n                binding: 0,\n                resource: wgpu::BindingResource::Buffer(\n                    pyramid_desc_buffer.as_entire_buffer_binding(),\n                ),\n            }],\n        })\n    }\n\n    pub fn new(pyramid: &DepthPyramid) -> Self {\n        let device = pyramid.slab.device();\n        let bindgroup_layout = Self::create_bindgroup_layout(device);\n        let pipeline = Self::create_pipeline(device, &bindgroup_layout);\n        let pyramid_slab_buffer = pyramid.slab.commit();\n        let bindgroup = Self::create_bindgroup(device, &bindgroup_layout, &pyramid_slab_buffer);\n        Self {\n            pipeline,\n            bindgroup,\n            bindgroup_layout,\n            pyramid_slab_buffer,\n        }\n    }\n\n    pub fn run(&mut self, pyramid: &DepthPyramid) {\n        let device = pyramid.slab.device();\n\n        if self.pyramid_slab_buffer.update_if_invalid() {\n            self.bindgroup =\n                Self::create_bindgroup(device, &self.bindgroup_layout, &self.pyramid_slab_buffer);\n        }\n\n        for i in 1..pyramid.mip_data.len() {\n            log::trace!(\"downsampling to mip {i}..{}\", pyramid.mip_data.len());\n            // Update the mip_level we're operating on.\n            let size = pyramid.desc.modify(|desc| {\n                desc.mip_level = i as u32;\n                desc.size\n            });\n            // Sync the change.\n            pyramid.slab.commit();\n            debug_assert!(\n                self.pyramid_slab_buffer.is_valid(),\n                \"pyramid slab should never resize here\"\n            );\n\n            let mut encoder = device\n                .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Self::LABEL });\n            {\n                let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {\n                    label: Self::LABEL,\n                    ..Default::default()\n                });\n                compute_pass.set_pipeline(&self.pipeline);\n                compute_pass.set_bind_group(0, Some(&self.bindgroup), &[]);\n                let w = size.x >> i;\n                let h = size.y >> i;\n                let x = w / 16 + 1;\n                let y = h / 16 + 1;\n                let z = 1;\n                compute_pass.dispatch_workgroups(x, y, z);\n            }\n            pyramid.slab.queue().submit(Some(encoder.finish()));\n        }\n    }\n}\n\n/// Computes occlusion culling on the GPU.\npub struct ComputeDepthPyramid {\n    pub(crate) depth_pyramid: DepthPyramid,\n    compute_copy_depth: ComputeCopyDepth,\n    compute_downsample_depth: ComputeDownsampleDepth,\n}\n\nimpl ComputeDepthPyramid {\n    const _LABEL: Option<&'static str> = Some(\"compute-depth-pyramid\");\n\n    pub fn new(runtime: impl AsRef<WgpuRuntime>, depth_texture: &Texture) -> Self {\n        let runtime = runtime.as_ref();\n        let depth_pyramid = DepthPyramid::new(runtime, depth_texture.size());\n        let compute_copy_depth = ComputeCopyDepth::new(&depth_pyramid, depth_texture);\n        let compute_downsample_depth = ComputeDownsampleDepth::new(&depth_pyramid);\n        Self {\n            depth_pyramid,\n            compute_copy_depth,\n            compute_downsample_depth,\n        }\n    }\n\n    /// Run depth pyramid copy and downsampling, then return the updated HZB\n    /// buffer.\n    pub fn run(&mut self, depth_texture: &Texture) {\n        let extent = depth_texture.texture.size();\n        let size = UVec2::new(extent.width, extent.height);\n        if size != self.depth_pyramid.size() {\n            log::debug!(\"depth texture size changed\");\n            self.depth_pyramid.resize(size);\n        }\n\n        self.compute_copy_depth\n            .run(&mut self.depth_pyramid, depth_texture);\n\n        self.compute_downsample_depth.run(&self.depth_pyramid);\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use std::collections::HashMap;\n\n    use crate::{\n        bvol::BoundingSphere,\n        context::Context,\n        cull::shader::DepthPyramidDescriptor,\n        draw::DrawIndirectArgs,\n        geometry::{Geometry, Vertex},\n        math::hex_to_vec4,\n        primitive::shader::PrimitiveDescriptor,\n        test::BlockOnFuture,\n    };\n    use crabslab::{Array, GrowableSlab, Id, Slab};\n    use glam::{Mat4, Quat, UVec2, UVec3, Vec2, Vec3, Vec4};\n\n    #[test]\n    fn occlusion_culling_sanity() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));\n        let camera_position = Vec3::new(0.0, 9.0, 9.0);\n        let _camera = stage.new_camera().with_projection_and_view(\n            Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 1.0, 24.0),\n            Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y),\n        );\n        let _rez = stage\n            .new_primitive()\n            .with_vertices(stage.new_vertices(crate::test::gpu_cube_vertices()))\n            .with_transform(\n                stage\n                    .new_transform()\n                    .with_scale(Vec3::new(6.0, 6.0, 6.0))\n                    .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),\n            );\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        frame.present();\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::save(\"cull/pyramid/frame.png\", img);\n        frame.present();\n\n        let depth_texture = stage.get_depth_texture();\n        let depth_img = depth_texture.read_image().block().unwrap().unwrap();\n        img_diff::save(\"cull/pyramid/depth.png\", depth_img);\n\n        let pyramid_images = futures_lite::future::block_on(\n            stage\n                .draw_calls\n                .read()\n                .unwrap()\n                .drawing_strategy\n                .as_indirect()\n                .unwrap()\n                .read_hzb_images(),\n        )\n        .unwrap();\n        for (i, img) in pyramid_images.into_iter().enumerate() {\n            img_diff::save(format!(\"cull/pyramid/mip_{i}.png\"), img);\n        }\n    }\n\n    #[test]\n    fn depth_pyramid_descriptor_sanity() {\n        let mut slab = vec![];\n        let size = UVec2::new(64, 32);\n        let mip_levels = size.min_element().ilog2();\n        let desc_id = slab.allocate::<DepthPyramidDescriptor>();\n        let mips_array = slab.allocate_array::<Array<f32>>(mip_levels as usize);\n        let mip_data_arrays = (0..mip_levels)\n            .map(|i| {\n                let w = size.x >> i;\n                let h = size.y >> i;\n                let len = (w * h) as usize;\n                log::info!(\"allocating {len} f32s for mip {i} of size {w}x{h}\");\n                let array = slab.allocate_array::<f32>(len);\n                let mut data: Vec<f32> = vec![];\n                for _y in 0..h {\n                    for x in 0..w {\n                        data.push(x as f32);\n                    }\n                }\n                slab.write_array(array, &data);\n                array\n            })\n            .collect::<Vec<_>>();\n        slab.write_array(mips_array, &mip_data_arrays);\n        let desc = DepthPyramidDescriptor {\n            size: UVec2::new(64, 32),\n            mip_level: 0,\n            mip: mips_array,\n        };\n        slab.write(desc_id, &desc);\n\n        // Test that `id_of_depth` returns the correct value.\n        for mip_level in 0..mip_levels {\n            let size = desc.size_at(mip_level);\n            log::info!(\"mip {mip_level} is size {size}\");\n            for y in 0..size.y {\n                for x in 0..size.x {\n                    let id = desc.id_of_depth(mip_level, UVec2::new(x, y), &slab);\n                    let depth = slab.read(id);\n                    assert_eq!(x as f32, depth, \"depth should be x value\");\n                }\n            }\n        }\n    }\n\n    #[test]\n    fn occlusion_culling_debugging() {\n        let ctx = Context::headless(128, 128).block();\n        let stage = ctx\n            .new_stage()\n            .with_lighting(false)\n            .with_bloom(false)\n            .with_background_color(Vec4::splat(1.0));\n        let _camera = {\n            let fovy = std::f32::consts::FRAC_PI_4;\n            let aspect = 1.0;\n            let znear = 0.1;\n            let zfar = 100.0;\n            let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);\n            // Camera is looking straight down Z, towards the origin with Y up\n            let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO, Vec3::Y);\n            stage\n                .new_camera()\n                .with_projection_and_view(projection, view)\n        };\n\n        let save_render = |s: &str| {\n            let frame = ctx.get_next_frame().unwrap();\n            stage.render(&frame.view());\n            let img = frame.read_image().block().unwrap();\n            img_diff::save(format!(\"cull/debugging_{s}.png\"), img);\n            frame.present();\n        };\n\n        // A hashmap to hold renderlet ids to their names.\n        let mut names = HashMap::<Id<PrimitiveDescriptor>, String>::default();\n\n        // Add four yellow cubes in each corner\n        let _ycubes = [\n            (Vec2::new(-1.0, 1.0), \"top_left\"),\n            (Vec2::new(1.0, 1.0), \"top_right\"),\n            (Vec2::new(1.0, -1.0), \"bottom_right\"),\n            (Vec2::new(-1.0, -1.0), \"bottom_left\"),\n        ]\n        .map(|(offset, suffix)| {\n            let yellow = hex_to_vec4(0xFFE6A5FF);\n            let renderlet = stage\n                .new_primitive()\n                .with_transform(\n                    stage\n                        .new_transform()\n                        // move it back behind the purple cube\n                        .with_translation((offset * 10.0).extend(-20.0))\n                        // scale it up since it's a unit cube\n                        .with_scale(Vec3::splat(2.0)),\n                )\n                .with_vertices(stage.new_vertices(crate::math::unit_cube().into_iter().map(\n                    |(p, n)| {\n                        Vertex::default()\n                            .with_position(p)\n                            .with_normal(n)\n                            .with_color(yellow)\n                    },\n                )))\n                .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length()));\n            names.insert(renderlet.id(), format!(\"yellow_cube_{suffix}\"));\n            renderlet\n        });\n\n        save_render(\"0_yellow_cubes\");\n\n        // We'll add a golden floor\n        let _floor = {\n            let golden = hex_to_vec4(0xFFBF61FF);\n            let renderlet = stage\n                .new_primitive()\n                .with_transform(\n                    stage\n                        .new_transform()\n                        // flip it so it's facing up, like a floor should be\n                        .with_rotation(Quat::from_rotation_x(std::f32::consts::FRAC_PI_2))\n                        // move it down and back a bit\n                        .with_translation(Vec3::new(0.0, -5.0, -10.0))\n                        // scale it up, since it's a unit quad\n                        .with_scale(Vec3::new(100.0, 100.0, 1.0)),\n                )\n                .with_vertices(\n                    stage.new_vertices(\n                        crate::math::UNIT_QUAD_CCW\n                            .map(|p| Vertex::default().with_position(p).with_color(golden)),\n                    ),\n                )\n                .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec2::splat(0.5).length()));\n            names.insert(renderlet.id(), \"floor\".into());\n            renderlet\n        };\n\n        save_render(\"1_floor\");\n\n        // Add a green cube\n        let _gcube = {\n            let green = hex_to_vec4(0x8ABFA3FF);\n            let renderlet = stage\n                .new_primitive()\n                .with_transform(\n                    stage\n                        .new_transform()\n                        // move it back behind the purple cube\n                        .with_translation(Vec3::new(0.0, 0.0, -10.0))\n                        // scale it up since it's a unit cube\n                        .with_scale(Vec3::splat(5.0)),\n                )\n                .with_vertices(stage.new_vertices(crate::math::unit_cube().into_iter().map(\n                    |(p, n)| {\n                        Vertex::default()\n                            .with_position(p)\n                            .with_normal(n)\n                            .with_color(green)\n                    },\n                )))\n                .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length()));\n            stage.add_primitive(&renderlet);\n            names.insert(renderlet.id(), \"green_cube\".into());\n            renderlet\n        };\n\n        save_render(\"2_green_cube\");\n\n        // And a purple cube\n        let _pcube = {\n            let purple = hex_to_vec4(0x605678FF);\n            let renderlet = stage\n                .new_primitive()\n                .with_transform(\n                    stage\n                        .new_transform()\n                        // move it back a bit\n                        .with_translation(Vec3::new(0.0, 0.0, -3.0))\n                        // scale it up since it's a unit cube\n                        .with_scale(Vec3::splat(5.0)),\n                )\n                .with_vertices(stage.new_vertices(crate::math::unit_cube().into_iter().map(\n                    |(p, n)| {\n                        Vertex::default()\n                            .with_position(p)\n                            .with_normal(n)\n                            .with_color(purple)\n                    },\n                )))\n                .with_bounds(BoundingSphere::new(Vec3::ZERO, Vec3::splat(0.5).length()));\n            names.insert(renderlet.id(), \"purple_cube\".into());\n            renderlet\n        };\n\n        // Do two renders, because depth pyramid operates on depth data one frame\n        // behind\n        save_render(\"3_purple_cube\");\n        save_render(\"3_purple_cube\");\n\n        // save the normalized depth image\n        let mut depth_img = stage\n            .get_depth_texture()\n            .read_image()\n            .block()\n            .unwrap()\n            .unwrap();\n        img_diff::normalize_gray_img(&mut depth_img);\n        img_diff::save(\"cull/debugging_4_depth.png\", depth_img);\n\n        // save the normalized pyramid images\n        let pyramid_images = futures_lite::future::block_on(\n            stage\n                .draw_calls\n                .read()\n                .unwrap()\n                .drawing_strategy\n                .as_indirect()\n                .unwrap()\n                .read_hzb_images(),\n        )\n        .unwrap();\n        for (i, mut img) in pyramid_images.into_iter().enumerate() {\n            img_diff::normalize_gray_img(&mut img);\n            img_diff::save(format!(\"cull/debugging_pyramid_mip_{i}.png\"), img);\n        }\n\n        // The stage's slab, which contains the `Renderlet`s and their `BoundingSphere`s\n        let stage_slab = futures_lite::future::block_on({\n            let geometry: &Geometry = stage.as_ref();\n            geometry.slab_allocator().read(..)\n        })\n        .unwrap();\n        let draw_calls = stage.draw_calls.read().unwrap();\n        let indirect_draws = draw_calls.drawing_strategy.as_indirect().unwrap();\n        // The HZB slab, which contains a `DepthPyramidDescriptor` at index 0, and all\n        // the pyramid's mips\n        let depth_pyramid_slab = futures_lite::future::block_on(\n            indirect_draws\n                .compute_culling\n                .compute_depth_pyramid\n                .depth_pyramid\n                .slab\n                .read(..),\n        )\n        .unwrap();\n        // The indirect draw buffer\n        let mut args_slab = futures_lite::future::block_on(indirect_draws.slab.read(..)).unwrap();\n        let args: &mut [DrawIndirectArgs] = bytemuck::cast_slice_mut(&mut args_slab);\n        // Number of `DrawIndirectArgs` in the `args` buffer.\n        let num_draw_calls = draw_calls.draw_count();\n\n        // Print our names so we know what we're working with\n        let mut pnames = names.iter().collect::<Vec<_>>();\n        pnames.sort();\n        for (id, name) in pnames.into_iter() {\n            log::info!(\"id: {id:?}, name: {name}\");\n        }\n\n        for i in 0..num_draw_calls as u32 {\n            let renderlet_id = Id::<PrimitiveDescriptor>::new(args[i as usize].first_instance);\n            let name = names.get(&renderlet_id).unwrap();\n            if name != \"green_cube\" {\n                continue;\n            }\n            log::info!(\"\");\n            log::info!(\"name: {name}\");\n            crate::cull::shader::compute_culling(\n                &stage_slab,\n                &depth_pyramid_slab,\n                args,\n                UVec3::new(i, 0, 0),\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/cull/shader.rs",
    "content": "use crabslab::{Array, Id, Slab, SlabItem};\nuse glam::{UVec2, UVec3, Vec2, Vec3Swizzles};\n#[allow(unused_imports)]\nuse spirv_std::num_traits::Float;\nuse spirv_std::{\n    arch::IndexUnchecked,\n    image::{sample_with, Image, ImageWithMethods},\n    spirv,\n};\n\nuse crate::{\n    draw::DrawIndirectArgs, geometry::shader::GeometryDescriptor,\n    primitive::shader::PrimitiveDescriptor,\n};\n\n#[spirv(compute(threads(16)))]\npub fn compute_culling(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] stage_slab: &[u32],\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] depth_pyramid_slab: &[u32],\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 2)] args: &mut [DrawIndirectArgs],\n    #[spirv(global_invocation_id)] global_id: UVec3,\n) {\n    let gid = global_id.x as usize;\n    if gid >= args.len() {\n        return;\n    }\n\n    crate::println!(\"gid: {gid}\");\n    // Get the draw arg\n    let arg = unsafe { args.index_unchecked_mut(gid) };\n    // Get the primitive using the draw arg's primitive id\n    let primitive_id = Id::<PrimitiveDescriptor>::new(arg.first_instance);\n    let primitive = stage_slab.read_unchecked(primitive_id);\n    crate::println!(\"primitive: {primitive_id:?}\");\n\n    arg.vertex_count = primitive.get_vertex_count();\n    arg.instance_count = if primitive.visible { 1 } else { 0 };\n\n    if primitive.bounds.radius == 0.0 {\n        crate::println!(\"primitive bounding radius is zero, cannot cull\");\n        return;\n    }\n\n    let config: GeometryDescriptor = stage_slab.read(Id::new(0));\n    if !config.perform_frustum_culling {\n        return;\n    }\n\n    let camera = stage_slab.read(config.camera_id);\n    let model = stage_slab.read(primitive.transform_id);\n    // Compute frustum culling, and then occlusion culling, if need be\n    let (primitive_is_inside_frustum, sphere_in_world_coords) =\n        primitive.bounds.is_inside_camera_view(&camera, model);\n\n    if primitive_is_inside_frustum {\n        arg.instance_count = 1;\n        crate::println!(\"primitive is inside frustum\");\n        crate::println!(\"znear: {}\", camera.frustum().planes[0]);\n        crate::println!(\" zfar: {}\", camera.frustum().planes[5]);\n        if !config.perform_occlusion_culling {\n            return;\n        }\n\n        // Compute occlusion culling using the hierachical z-buffer.\n        let hzb_desc = depth_pyramid_slab.read_unchecked::<DepthPyramidDescriptor>(0u32.into());\n        let viewport_size = Vec2::new(hzb_desc.size.x as f32, hzb_desc.size.y as f32);\n        let sphere_aabb = sphere_in_world_coords.project_onto_viewport(&camera, viewport_size);\n        crate::println!(\"sphere_aabb: {sphere_aabb:#?}\");\n\n        let size_in_pixels = sphere_aabb.max.xy() - sphere_aabb.min.xy();\n        let size_in_pixels = if size_in_pixels.x > size_in_pixels.y {\n            size_in_pixels.x\n        } else {\n            size_in_pixels.y\n        };\n        crate::println!(\"primitive size in pixels: {size_in_pixels}\");\n\n        let mip_level = size_in_pixels.log2().floor() as u32;\n        let max_mip_level = hzb_desc.mip.len() as u32 - 1;\n        let mip_level = if mip_level > max_mip_level {\n            crate::println!(\"mip_level maxed out at {mip_level}, setting to {max_mip_level}\");\n            max_mip_level\n        } else {\n            mip_level\n        };\n        crate::println!(\n            \"selected mip level: {mip_level} {}x{}\",\n            viewport_size.x as u32 >> mip_level,\n            viewport_size.y as u32 >> mip_level\n        );\n\n        let center = sphere_aabb.center().xy();\n        crate::println!(\"center: {center}\");\n\n        let x = center.x.round() as u32 >> mip_level;\n        let y = center.y.round() as u32 >> mip_level;\n        crate::println!(\"mip (x, y): ({x}, {y})\");\n\n        let depth_id = hzb_desc.id_of_depth(mip_level, UVec2::new(x, y), depth_pyramid_slab);\n        let depth_in_hzb = depth_pyramid_slab.read_unchecked(depth_id);\n        crate::println!(\"depth_in_hzb: {depth_in_hzb}\");\n\n        let depth_of_sphere = sphere_aabb.min.z;\n        crate::println!(\"depth_of_sphere: {depth_of_sphere}\");\n\n        let primitive_is_behind_something = depth_of_sphere > depth_in_hzb;\n        let primitive_surrounds_camera = depth_of_sphere > 1.0;\n\n        if primitive_is_behind_something || primitive_surrounds_camera {\n            crate::println!(\"CULLED\");\n            arg.instance_count = 0;\n        }\n    } else {\n        arg.instance_count = 0;\n    }\n}\n\n/// A hierarchichal depth buffer.\n///\n/// AKA HZB\n#[derive(Clone, Copy, Default, SlabItem)]\npub struct DepthPyramidDescriptor {\n    /// Size of the top layer mip.\n    pub size: UVec2,\n    /// Current mip level.\n    ///\n    /// This will be updated for each run of the downsample compute shader.\n    pub mip_level: u32,\n    /// Pointer to the mip data.\n    ///\n    /// This points to the depth data at each mip level.\n    ///\n    /// The depth data itself is somewhere else in the slab.\n    pub mip: Array<Array<f32>>,\n}\n\nimpl DepthPyramidDescriptor {\n    fn should_skip_invocation(&self, global_invocation: UVec3) -> bool {\n        let current_size = self.size >> self.mip_level;\n        !(global_invocation.x < current_size.x && global_invocation.y < current_size.y)\n    }\n\n    #[cfg(test)]\n    pub fn size_at(&self, mip_level: u32) -> UVec2 {\n        UVec2::new(self.size.x >> mip_level, self.size.y >> mip_level)\n    }\n\n    /// Return the [`Id`] of the depth at the given `mip_level` and coordinate.\n    pub fn id_of_depth(&self, mip_level: u32, coord: UVec2, slab: &[u32]) -> Id<f32> {\n        let mip_array = slab.read(self.mip.at(mip_level as usize));\n        let width_at_mip = self.size.x >> mip_level;\n        let index = coord.y * width_at_mip + coord.x;\n        mip_array.at(index as usize)\n    }\n}\n\npub type DepthImage2d = Image!(2D, type=f32, sampled, depth);\npub type DepthImage2dMultisampled = Image!(2D, type=f32, sampled, depth, multisampled);\n\n/// Copies a depth texture to the top mip of a pyramid of mips.\n///\n/// It is assumed that a [`DepthPyramidDescriptor`] is stored at index `0` in\n/// the given slab.\n#[spirv(compute(threads(16, 16, 1)))]\npub fn compute_copy_depth_to_pyramid(\n    #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &mut [u32],\n    #[spirv(descriptor_set = 0, binding = 1)] depth_texture: &DepthImage2d,\n    #[spirv(global_invocation_id)] global_id: UVec3,\n) {\n    let desc = slab.read_unchecked::<DepthPyramidDescriptor>(0u32.into());\n    if desc.should_skip_invocation(global_id) {\n        return;\n    }\n\n    let depth = depth_texture\n        .fetch_with(global_id.xy(), sample_with::lod(0))\n        .x;\n    let dest_id = desc.id_of_depth(0, global_id.xy(), slab);\n    slab.write(dest_id, &depth);\n}\n\n/// Copies a depth texture to the top mip of a pyramid of mips.\n///\n/// It is assumed that a [`DepthPyramidDescriptor`] is stored at index `0` in\n/// the given slab.\n#[spirv(compute(threads(16, 16, 1)))]\npub fn compute_copy_depth_to_pyramid_multisampled(\n    #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &mut [u32],\n    #[spirv(descriptor_set = 0, binding = 1)] depth_texture: &DepthImage2dMultisampled,\n    #[spirv(global_invocation_id)] global_id: UVec3,\n) {\n    let desc = slab.read_unchecked::<DepthPyramidDescriptor>(0u32.into());\n    if desc.should_skip_invocation(global_id) {\n        return;\n    }\n\n    let depth = depth_texture\n        .fetch_with(global_id.xy(), sample_with::sample_index(0))\n        .x;\n    let dest_id = desc.id_of_depth(0, global_id.xy(), slab);\n    slab.write(dest_id, &depth);\n}\n\n/// Downsample from `DepthPyramidDescriptor::mip_level-1` into\n/// `DepthPyramidDescriptor::mip_level`.\n///\n/// It is assumed that a [`DepthPyramidDescriptor`] is stored at index `0` in\n/// the given slab.\n///\n/// The `DepthPyramidDescriptor`'s `mip_level` field will point to that of the\n/// mip level being downsampled to (the mip level being written into).\n///\n/// This shader should be called in a loop from from `1..mip_count`.\n#[spirv(compute(threads(16, 16, 1)))]\npub fn compute_downsample_depth_pyramid(\n    #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] slab: &mut [u32],\n    #[spirv(global_invocation_id)] global_id: UVec3,\n) {\n    let desc = slab.read_unchecked::<DepthPyramidDescriptor>(0u32.into());\n    if desc.should_skip_invocation(global_id) {\n        return;\n    }\n    // Sample the texel.\n    //\n    // The texel will look like this:\n    //\n    // a b\n    // c d\n    let a_coord = global_id.xy() * 2;\n    let a = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord, slab));\n    let b = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord + UVec2::new(1, 0), slab));\n    let c = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord + UVec2::new(0, 1), slab));\n    let d = slab.read(desc.id_of_depth(desc.mip_level - 1, a_coord + UVec2::new(1, 1), slab));\n    // Take the maximum depth of the region (max depth means furthest away)\n    let depth_value = a.max(b).max(c).max(d);\n    // Write the texel in the next mip\n    let depth_id = desc.id_of_depth(desc.mip_level, global_id.xy(), slab);\n    slab.write(depth_id, &depth_value);\n}\n"
  },
  {
    "path": "crates/renderling/src/cull.rs",
    "content": "//! Compute based culling.\n//!\n//! Frustum culling as explained in\n//! [the vulkan guide](https://vkguide.dev/docs/gpudriven/compute_culling/).\n\n#[cfg(not(target_arch = \"spirv\"))]\nmod cpu;\n#[cfg(not(target_arch = \"spirv\"))]\npub use cpu::*;\n\npub mod shader;\n"
  },
  {
    "path": "crates/renderling/src/debug/cpu.rs",
    "content": "//! CPU side of drawing debugging overlays.\n\nuse std::sync::{Arc, Mutex};\n\n#[derive(Clone)]\npub struct DebugOverlay {\n    pipeline: Arc<wgpu::RenderPipeline>,\n    bindgroup_layout: Arc<wgpu::BindGroupLayout>,\n    bindgroup: Arc<Mutex<Option<wgpu::BindGroup>>>,\n}\n\nimpl DebugOverlay {\n    const LABEL: Option<&'static str> = Some(\"debug-overlay\");\n\n    fn create_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {\n        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: Self::LABEL,\n            entries: &[\n                // stage slab\n                wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n                // draw calls\n                wgpu::BindGroupLayoutEntry {\n                    binding: 1,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n            ],\n        })\n    }\n\n    fn create_pipeline_layout(\n        device: &wgpu::Device,\n        bindgroup_layout: &wgpu::BindGroupLayout,\n    ) -> wgpu::PipelineLayout {\n        device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label: Self::LABEL,\n            bind_group_layouts: &[bindgroup_layout],\n            push_constant_ranges: &[],\n        })\n    }\n\n    fn create_pipeline(\n        device: &wgpu::Device,\n        bindgroup_layout: &wgpu::BindGroupLayout,\n        format: wgpu::TextureFormat,\n    ) -> wgpu::RenderPipeline {\n        let vertex = crate::linkage::debug_overlay_vertex::linkage(device);\n        let fragment = crate::linkage::debug_overlay_fragment::linkage(device);\n        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n            label: Self::LABEL,\n            layout: Some(&Self::create_pipeline_layout(device, bindgroup_layout)),\n            vertex: wgpu::VertexState {\n                module: &vertex.module,\n                entry_point: None,\n                compilation_options: wgpu::PipelineCompilationOptions::default(),\n                buffers: &[],\n            },\n            primitive: wgpu::PrimitiveState {\n                topology: wgpu::PrimitiveTopology::TriangleList,\n                strip_index_format: None,\n                front_face: wgpu::FrontFace::Ccw,\n                cull_mode: None,\n                unclipped_depth: false,\n                polygon_mode: wgpu::PolygonMode::Fill,\n                conservative: false,\n            },\n\n            depth_stencil: None,\n            multisample: wgpu::MultisampleState::default(),\n            fragment: Some(wgpu::FragmentState {\n                module: &fragment.module,\n                entry_point: None,\n                compilation_options: wgpu::PipelineCompilationOptions::default(),\n                targets: &[Some(wgpu::ColorTargetState {\n                    format,\n                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                    write_mask: wgpu::ColorWrites::all(),\n                })],\n            }),\n            multiview: None,\n            cache: None,\n        })\n    }\n\n    pub fn create_bindgroup(\n        &self,\n        device: &wgpu::Device,\n        slab_buffer: &wgpu::Buffer,\n        indirect_draw_buffer: &wgpu::Buffer,\n    ) -> wgpu::BindGroup {\n        device.create_bind_group(&wgpu::BindGroupDescriptor {\n            label: Self::LABEL,\n            layout: &self.bindgroup_layout,\n            entries: &[\n                wgpu::BindGroupEntry {\n                    binding: 0,\n                    resource: wgpu::BindingResource::Buffer(slab_buffer.as_entire_buffer_binding()),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 1,\n                    resource: wgpu::BindingResource::Buffer(\n                        indirect_draw_buffer.as_entire_buffer_binding(),\n                    ),\n                },\n            ],\n        })\n    }\n\n    pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {\n        let bindgroup_layout = Arc::new(Self::create_bindgroup_layout(device));\n        Self {\n            pipeline: Self::create_pipeline(device, &bindgroup_layout, format).into(),\n            bindgroup_layout,\n            bindgroup: Arc::new(Mutex::new(None)),\n        }\n    }\n\n    pub fn render(\n        &self,\n        device: &wgpu::Device,\n        queue: &wgpu::Queue,\n        view: &wgpu::TextureView,\n        slab_buffer: &wgpu::Buffer,\n        indirect_draw_buffer: &wgpu::Buffer,\n    ) {\n        let mut encoder =\n            device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Self::LABEL });\n        {\n            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                label: Self::LABEL,\n                color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                    view,\n                    resolve_target: None,\n                    ops: wgpu::Operations {\n                        load: wgpu::LoadOp::Load,\n                        store: wgpu::StoreOp::Store,\n                    },\n                    depth_slice: None,\n                })],\n                depth_stencil_attachment: None,\n                timestamp_writes: None,\n                occlusion_query_set: None,\n            });\n            render_pass.set_pipeline(&self.pipeline);\n            // UNWRAP: panic on purpose\n            let mut guard = self.bindgroup.lock().expect(\"debug bindgroup lock\");\n            if guard.is_none() {\n                *guard = Some(self.create_bindgroup(device, slab_buffer, indirect_draw_buffer));\n            }\n            render_pass.set_bind_group(0, guard.as_ref(), &[]);\n            render_pass.draw(0..6, 0..1);\n        }\n        queue.submit(Some(encoder.finish()));\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/debug.rs",
    "content": "//! Debug overlay.\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\npub mod shader {\n    use crabslab::{Id, Slab};\n    use glam::{Vec2, Vec3Swizzles, Vec4, Vec4Swizzles};\n    use spirv_std::{arch::IndexUnchecked, spirv};\n\n    use crate::{\n        draw::DrawIndirectArgs, geometry::shader::GeometryDescriptor,\n        primitive::shader::PrimitiveDescriptor, sdf, transform::shader::TransformDescriptor,\n    };\n    /// Renders an implicit quad.\n    #[spirv(vertex)]\n    pub fn debug_overlay_vertex(\n        #[spirv(vertex_index)] vertex_id: u32,\n        #[spirv(position)] clip_pos: &mut Vec4,\n    ) {\n        *clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[vertex_id as usize % 6];\n    }\n\n    /// Renders a debug overlay on top of the current framebuffer.\n    ///\n    /// Displays useful information in real time.\n    #[spirv(fragment)]\n    pub fn debug_overlay_fragment(\n        #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n        #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] draw_calls: &[DrawIndirectArgs],\n        #[spirv(frag_coord)] frag_coord: Vec4,\n        frag_color: &mut Vec4,\n    ) {\n        let camera_id_id = Id::from(GeometryDescriptor::OFFSET_OF_CAMERA_ID);\n        let camera_id = slab.read_unchecked(camera_id_id);\n        let camera = slab.read_unchecked(camera_id);\n        let resolution_id = Id::from(GeometryDescriptor::OFFSET_OF_RESOLUTION);\n        let viewport_size = slab.read_unchecked(resolution_id);\n\n        *frag_color = Vec4::ZERO;\n\n        for i in 0..draw_calls.len() {\n            let draw_call = unsafe { draw_calls.index_unchecked(i) };\n            let primitive_id: Id<PrimitiveDescriptor> =\n                Id::<PrimitiveDescriptor>::new(draw_call.first_instance);\n            let transform_id =\n                slab.read_unchecked(primitive_id + PrimitiveDescriptor::OFFSET_OF_TRANSFORM_ID);\n            let mut model = TransformDescriptor::IDENTITY;\n            slab.read_into_if_some::<TransformDescriptor>(transform_id, &mut model);\n            let bounds = slab.read_unchecked(primitive_id + PrimitiveDescriptor::OFFSET_OF_BOUNDS);\n\n            let (_, sphere_in_world_coords) = bounds.is_inside_camera_view(&camera, model);\n            let sphere_aabb = sphere_in_world_coords.project_onto_viewport(\n                &camera,\n                Vec2::new(viewport_size.x as f32, viewport_size.y as f32),\n            );\n\n            let sdf_circle = sdf::Box {\n                center: sphere_aabb.center().xy(),\n                half_extent: (sphere_aabb.max.xy() - sphere_aabb.min.xy()) * 0.5,\n            };\n\n            let distance = sdf_circle.distance(frag_coord.xy() + 0.5);\n\n            // Here we use `step_le`, which I have annotated with `#inline(always)`.\n            // I did this because without it, it seems to do the opposite of expected.\n            // I found this by inlining by hand.\n            let alpha = crate::math::step_le(sphere_aabb.max.z, 1.0);\n            if distance.abs() < 0.5 {\n                *frag_color = Vec4::new(0.0, 0.0, 0.0, 1.0 * alpha);\n            } else if distance.abs() <= 2.0 {\n                *frag_color = Vec4::new(1.0, 1.0, 1.0, 0.5 * alpha);\n            } else if distance.abs() <= 3.0 {\n                *frag_color = Vec4::new(0.5, 0.5, 0.5, 1.0 * alpha);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/draw/cpu.rs",
    "content": "//! CPU-only side of renderling/draw.rs\n\nuse craballoc::{\n    prelude::{Gpu, SlabAllocator, WgpuRuntime},\n    slab::SlabBuffer,\n};\nuse crabslab::Id;\n\nuse crate::{\n    context::Context,\n    cull::{ComputeCulling, CullingError},\n    primitive::{shader::PrimitiveDescriptor, Primitive},\n    texture::Texture,\n};\n\nuse super::DrawIndirectArgs;\n\n/// Issues indirect draw calls.\n///\n/// Issues draw calls and performs culling.\npub struct IndirectDraws {\n    pub(crate) slab: SlabAllocator<WgpuRuntime>,\n    pub(crate) draws: Vec<Gpu<DrawIndirectArgs>>,\n    pub(crate) compute_culling: ComputeCulling,\n}\n\nimpl IndirectDraws {\n    fn new(\n        runtime: impl AsRef<WgpuRuntime>,\n        stage_slab_buffer: &SlabBuffer<wgpu::Buffer>,\n        depth_texture: &Texture,\n    ) -> Self {\n        let runtime = runtime.as_ref();\n        let indirect_slab =\n            SlabAllocator::new(runtime, \"indirect-slab\", wgpu::BufferUsages::INDIRECT);\n        Self {\n            compute_culling: ComputeCulling::new(\n                runtime,\n                stage_slab_buffer,\n                &indirect_slab.commit(),\n                depth_texture,\n            ),\n            slab: indirect_slab,\n            draws: vec![],\n        }\n    }\n\n    pub(crate) fn slab_allocator(&self) -> &SlabAllocator<WgpuRuntime> {\n        &self.slab\n    }\n\n    fn invalidate(&mut self) {\n        if !self.draws.is_empty() {\n            log::trace!(\"draining indirect draws after invalidation\");\n            let _ = self.draws.drain(..);\n        }\n    }\n\n    /// Read the images from the hierarchical z-buffer used for occlusion\n    /// culling.\n    ///\n    /// This is primarily for testing.\n    pub async fn read_hzb_images(&self) -> Result<Vec<image::GrayImage>, CullingError> {\n        self.compute_culling\n            .compute_depth_pyramid\n            .depth_pyramid\n            .read_images()\n            .await\n    }\n}\n\nimpl From<Id<PrimitiveDescriptor>> for DrawIndirectArgs {\n    fn from(id: Id<PrimitiveDescriptor>) -> Self {\n        // This is obviously incomplete, but that's ok because\n        // the rest of this struct is filled out on the GPU during\n        // culling.\n        DrawIndirectArgs {\n            vertex_count: 0,\n            instance_count: 0,\n            first_vertex: 0,\n            first_instance: id.inner(),\n        }\n    }\n}\n\n/// The drawing method used to send geometry to the GPU.\n///\n/// This is one of either:\n/// * Indirect drawing - standard drawing method that includes compute culling.\n/// * Direct drawing - fallback drawing method for web targets. Does not include\n///   compute culling, as the MULTI_DRAW_INDIRECT `wgpu` feature is required and\n///   not available on web.\npub(crate) struct DrawingStrategy {\n    indirect: Option<IndirectDraws>,\n}\n\nimpl DrawingStrategy {\n    pub(crate) fn as_indirect(&self) -> Option<&IndirectDraws> {\n        self.indirect.as_ref()\n    }\n}\n\n/// Used to determine which objects are drawn and maintains the\n/// list of all [`Primitive`]s.\npub struct DrawCalls {\n    /// Internal representation of all staged renderlets.\n    renderlets: Vec<Primitive>,\n    pub(crate) drawing_strategy: DrawingStrategy,\n}\n\nimpl DrawCalls {\n    /// Create a new [`DrawCalls`].\n    ///\n    /// `use_compute_culling` can be used to set whether frustum culling is used\n    /// as a GPU compute step before drawing. This is a native-only option.\n    ///\n    /// ## Note\n    /// A [`Context`] is required because `DrawCalls` needs to query for the set\n    /// of available driver features.\n    pub fn new(\n        ctx: &Context,\n        use_compute_culling: bool,\n        stage_slab_buffer: &SlabBuffer<wgpu::Buffer>,\n        depth_texture: &Texture,\n    ) -> Self {\n        let supported_features = ctx.get_adapter().features();\n        log::trace!(\"supported features: {supported_features:#?}\");\n        let can_use_multi_draw_indirect = ctx.get_adapter().features().contains(\n            wgpu::Features::INDIRECT_FIRST_INSTANCE | wgpu::Features::MULTI_DRAW_INDIRECT,\n        );\n        if use_compute_culling && !can_use_multi_draw_indirect {\n            log::warn!(\n                \"`use_compute_culling` is `true`, but the MULTI_DRAW_INDIRECT feature is not \\\n                 available. No compute culling will occur.\"\n            )\n        }\n        let can_use_compute_culling = use_compute_culling && can_use_multi_draw_indirect;\n        Self {\n            renderlets: vec![],\n            drawing_strategy: DrawingStrategy {\n                indirect: if can_use_compute_culling {\n                    log::debug!(\"Using indirect drawing method and compute culling\");\n                    Some(IndirectDraws::new(ctx, stage_slab_buffer, depth_texture))\n                } else {\n                    log::debug!(\"Using direct drawing method\");\n                    None\n                },\n            },\n        }\n    }\n\n    pub(crate) fn drawing_strategy(&self) -> &DrawingStrategy {\n        &self.drawing_strategy\n    }\n\n    /// Returns whether compute culling is available.\n    pub fn get_compute_culling_available(&self) -> bool {\n        matches!(\n            &self.drawing_strategy,\n            DrawingStrategy { indirect: Some(_) }\n        )\n    }\n\n    /// Add a renderlet to the drawing queue.\n    ///\n    /// Returns the number of draw calls in the queue.\n    pub fn add_primitive(&mut self, renderlet: &Primitive) -> usize {\n        log::trace!(\"adding renderlet {:?}\", renderlet.id());\n        if let Some(indirect) = &mut self.drawing_strategy.indirect {\n            indirect.invalidate();\n        }\n        self.renderlets.push(renderlet.clone());\n        self.renderlets.len()\n    }\n\n    /// Erase the given renderlet from the internal list of renderlets to be\n    /// drawn each frame.\n    ///\n    /// Returns the number of draw calls remaining in the queue.\n    pub fn remove_primitive(&mut self, renderlet: &Primitive) -> usize {\n        let id = renderlet.id();\n        self.renderlets.retain(|ir| ir.descriptor.id() != id);\n\n        if let Some(indirect) = &mut self.drawing_strategy.indirect {\n            indirect.invalidate();\n        }\n\n        self.renderlets.len()\n    }\n\n    /// Sort draw calls using a function compairing [`Primitive`]s.\n    pub fn sort_primitives(&mut self, f: impl Fn(&Primitive, &Primitive) -> std::cmp::Ordering) {\n        self.renderlets.sort_by(f);\n        if let Some(indirect) = &mut self.drawing_strategy.indirect {\n            indirect.invalidate();\n        }\n    }\n\n    /// Returns the number of draw calls (direct or indirect) that will be\n    /// made during pre-rendering (compute culling) and rendering.\n    pub fn draw_count(&self) -> usize {\n        self.renderlets.len()\n    }\n\n    /// Perform pre-draw steps like frustum and occlusion culling, if available.\n    ///\n    /// Returns the indirect draw buffer.\n    pub fn pre_draw(\n        &mut self,\n        depth_texture: &Texture,\n    ) -> Result<Option<SlabBuffer<wgpu::Buffer>>, CullingError> {\n        let num_draw_calls = self.draw_count();\n        // Only do compute culling if there are things we need to draw, otherwise\n        // `wgpu` will err with something like:\n        // \"Buffer with 'indirect draw upkeep' label binding size is zero\"\n        if num_draw_calls > 0 {\n            log::trace!(\"num_draw_calls: {num_draw_calls}\");\n            // TODO: Cull on GPU even when `multidraw_indirect` is unavailable.\n            //\n            // We can do this without multidraw by running GPU culling and then\n            // copying `indirect_buffer` back to the CPU.\n            if let Some(indirect) = &mut self.drawing_strategy.indirect {\n                if indirect.draws.len() != self.renderlets.len() {\n                    indirect.invalidate();\n                    // Pre-upkeep to reclaim resources - this is necessary because\n                    // the draw buffer has to be contiguous (it can't start with a bunch of trash)\n                    let indirect_buffer = indirect.slab.commit();\n                    if indirect_buffer.is_new_this_commit() {\n                        log::warn!(\"new indirect buffer\");\n                    }\n                    indirect.draws = self\n                        .renderlets\n                        .iter()\n                        .map(|r| {\n                            indirect\n                                .slab\n                                .new_value(DrawIndirectArgs::from(r.descriptor.id()))\n                                .into_gpu_only()\n                        })\n                        .collect();\n                }\n                let indirect_buffer = indirect.slab.commit();\n                log::trace!(\"performing culling on {num_draw_calls} renderlets\");\n                indirect\n                    .compute_culling\n                    .run(num_draw_calls as u32, depth_texture);\n                Ok(Some(indirect_buffer))\n            } else {\n                Ok(None)\n            }\n        } else {\n            Ok(None)\n        }\n    }\n\n    /// Draw into the given `RenderPass` by directly calling each draw.\n    pub fn draw_direct(&self, render_pass: &mut wgpu::RenderPass) {\n        if self.renderlets.is_empty() {\n            log::warn!(\"no internal renderlets, nothing to draw\");\n        }\n        for ir in self.renderlets.iter() {\n            // UNWRAP: panic on purpose\n            let desc = ir.descriptor.get();\n            let vertex_range = 0..desc.get_vertex_count();\n            let id = ir.descriptor.id();\n            let instance_range = id.inner()..id.inner() + 1;\n            render_pass.draw(vertex_range, instance_range);\n        }\n    }\n\n    /// Draw into the given `RenderPass`.\n    ///\n    /// This method draws using the indirect draw buffer, if possible, otherwise\n    /// it falls back to `draw_direct`.\n    pub fn draw(&self, render_pass: &mut wgpu::RenderPass) {\n        let num_draw_calls = self.draw_count();\n        if num_draw_calls > 0 {\n            if let Some(indirect) = &self.drawing_strategy.indirect {\n                log::trace!(\"drawing {num_draw_calls} renderlets using indirect\");\n                if let Some(indirect_buffer) = indirect.slab.get_buffer() {\n                    render_pass.multi_draw_indirect(&indirect_buffer, 0, num_draw_calls as u32);\n                } else {\n                    log::warn!(\n                        \"could not get the indirect buffer - was `DrawCall::upkeep` called?\"\n                    );\n                }\n            } else {\n                log::trace!(\"drawing {num_draw_calls} renderlets using direct\");\n                self.draw_direct(render_pass);\n            }\n        } else {\n            log::warn!(\"zero draw calls\");\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/draw.rs",
    "content": "//! Handles queueing draw calls.\n//!\n//! [`DrawCalls`] is used to maintain the list of all staged\n//! [`PrimitiveDescriptor`](crate::primitive::shader::PrimitiveDescriptor)s.\n//! It also performs frustum culling and issues draw calls during\n//! [`Stage::render`](crate::stage::Stage::render).\nuse crabslab::SlabItem;\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\n/// Argument buffer layout for draw_indirect commands.\n#[repr(C)]\n#[cfg_attr(cpu, derive(bytemuck::Pod, bytemuck::Zeroable))]\n#[derive(Clone, Copy, Default, SlabItem, core::fmt::Debug)]\npub struct DrawIndirectArgs {\n    pub vertex_count: u32,\n    pub instance_count: u32,\n    pub first_vertex: u32,\n    pub first_instance: u32,\n}\n"
  },
  {
    "path": "crates/renderling/src/geometry/cpu.rs",
    "content": "//! CPU side of the [super::geometry](geometry) module.\nuse std::sync::{Arc, Mutex};\n\nuse craballoc::{\n    runtime::{IsRuntime, WgpuRuntime},\n    slab::{SlabAllocator, SlabBuffer},\n    value::{GpuArray, Hybrid, HybridArray, IsContainer},\n};\nuse crabslab::{Array, Id};\nuse glam::{Mat4, UVec2, Vec4};\n\nuse crate::{\n    camera::Camera,\n    geometry::{\n        shader::{GeometryDescriptor, SkinDescriptor},\n        MorphTarget, Vertex,\n    },\n    transform::{shader::TransformDescriptor, NestedTransform, Transform},\n    types::{GpuCpuArray, GpuOnlyArray},\n};\n\n/// A contiguous array of vertices, staged on the GPU.\n///\n/// The type variable `Ct` denotes whether the staged data lives on the GPU\n/// only, or on the CPU and the GPU.\n///\n/// # Note\n/// The amount of data staged in `Vertices` can potentially be very large, and\n/// it is common to unload the data from the CPU with\n/// [`Vertices::into_gpu_only`].\n///\n/// The only reason to keep data on the CPU is if it needs to be inspected and\n/// modified _in place_. This type of modification can be done with\n/// [`Vertices::modify_vertex`].\n///\n/// After unloading it is still possible to _replace_ a [`Vertex`] at a\n/// specific index using [`Vertices::set_vertex`].\npub struct Vertices<Ct: IsContainer = GpuCpuArray> {\n    inner: Ct::Container<Vertex>,\n}\n\nimpl<Ct> Clone for Vertices<Ct>\nwhere\n    Ct: IsContainer,\n    Ct::Container<Vertex>: Clone,\n{\n    fn clone(&self) -> Self {\n        Self {\n            inner: self.inner.clone(),\n        }\n    }\n}\n\nimpl<Ct> std::fmt::Debug for Vertices<Ct>\nwhere\n    Ct: IsContainer<Pointer<Vertex> = Array<Vertex>>,\n{\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        f.debug_struct(\"Vertices\")\n            .field(\"array\", &Ct::get_pointer(&self.inner))\n            .finish()\n    }\n}\n\nimpl From<Vertices> for Vertices<GpuOnlyArray> {\n    fn from(value: Vertices) -> Self {\n        value.into_gpu_only()\n    }\n}\n\nimpl From<&Vertices> for Vertices<GpuOnlyArray> {\n    fn from(value: &Vertices) -> Self {\n        value.clone().into_gpu_only()\n    }\n}\n\nimpl From<&Vertices<GpuOnlyArray>> for Vertices<GpuOnlyArray> {\n    fn from(value: &Vertices<GpuOnlyArray>) -> Self {\n        value.clone()\n    }\n}\n\nimpl Vertices {\n    /// Stage a new array of vertex data on the GPU.\n    ///\n    /// The resulting `Vertices` will live on the GPU and CPU, allowing for\n    /// modification. If you would like to unload the CPU side, use\n    /// [`Vertices::into_gpu_only`].\n    pub(crate) fn new(\n        slab: &SlabAllocator<WgpuRuntime>,\n        vertices: impl IntoIterator<Item = Vertex>,\n    ) -> Self {\n        Vertices {\n            inner: slab.new_array(vertices),\n        }\n    }\n\n    /// Unload the CPU side of vertex data.\n    ///\n    /// After this operation the data can still be updated using\n    /// [`Vertices::set_item`], but it cannot be modified in-place.\n    pub fn into_gpu_only(self) -> Vertices<GpuOnlyArray> {\n        Vertices {\n            inner: self.inner.into_gpu_only(),\n        }\n    }\n\n    /// Returns a [`Vertex`] at a specific index, if any.\n    pub fn get_vertex(&self, index: usize) -> Option<Vertex> {\n        self.inner.get(index)\n    }\n\n    /// Return all vertices as a vector.\n    pub fn get_vec(&self) -> Vec<Vertex> {\n        self.inner.get_vec()\n    }\n\n    /// Modify a vertex at a specific index, if it exists.\n    pub fn modify_vertex<T: 'static>(\n        &self,\n        index: usize,\n        f: impl FnOnce(&mut Vertex) -> T,\n    ) -> Option<T> {\n        self.inner.modify(index, f)\n    }\n}\n\nimpl<T> Vertices<T>\nwhere\n    T: IsContainer<Pointer<Vertex> = Array<Vertex>>,\n{\n    /// Returns a pointer to the underlying data on the GPU.\n    pub fn array(&self) -> Array<Vertex> {\n        T::get_pointer(&self.inner)\n    }\n}\n\nimpl Vertices<GpuOnlyArray> {\n    /// Set the [`Vertex`] at the given index to the given value, if the item at\n    /// the index exists.\n    pub fn set_vertex(&self, index: usize, value: &Vertex) {\n        self.inner.set_item(index, value)\n    }\n}\n\n/// A contiguous array of indices, staged on the GPU.\n/// The type variable `Ct` denotes whether the data lives on the GPU only, or on\n/// the CPU and the GPU.\npub struct Indices<Ct: IsContainer = GpuCpuArray> {\n    inner: Ct::Container<u32>,\n}\n\nimpl<Ct> std::fmt::Debug for Indices<Ct>\nwhere\n    Ct: IsContainer<Pointer<u32> = Array<u32>>,\n{\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        f.debug_struct(\"Indices\")\n            .field(\"array\", &Ct::get_pointer(&self.inner))\n            .finish()\n    }\n}\n\nimpl<Ct> Clone for Indices<Ct>\nwhere\n    Ct: IsContainer,\n    Ct::Container<u32>: Clone,\n{\n    fn clone(&self) -> Self {\n        Self {\n            inner: self.inner.clone(),\n        }\n    }\n}\n\nimpl From<Indices> for Indices<GpuOnlyArray> {\n    fn from(value: Indices) -> Self {\n        value.into_gpu_only()\n    }\n}\n\nimpl From<&Indices> for Indices<GpuOnlyArray> {\n    fn from(value: &Indices) -> Self {\n        value.clone().into_gpu_only()\n    }\n}\n\nimpl From<&Indices<GpuOnlyArray>> for Indices<GpuOnlyArray> {\n    fn from(value: &Indices<GpuOnlyArray>) -> Self {\n        value.clone()\n    }\n}\n\nimpl<T> Indices<T>\nwhere\n    T: IsContainer<Pointer<u32> = Array<u32>>,\n{\n    /// Returns a pointer to the underlying data on the GPU.\n    pub fn array(&self) -> Array<u32> {\n        T::get_pointer(&self.inner)\n    }\n}\n\nimpl Indices {\n    /// Stage a new array of index data on the GPU.\n    ///\n    /// The resulting `Indices` will live on the GPU and CPU, allowing for\n    /// modification. If you would like to unload the CPU side, use\n    /// [`Indices::into_gpu_only`].\n    pub fn new(geometry: &Geometry, indices: impl IntoIterator<Item = u32>) -> Self {\n        Indices {\n            inner: geometry.slab.new_array(indices),\n        }\n    }\n\n    /// Unload the CPU side of this index data.\n    pub fn into_gpu_only(self) -> Indices<GpuOnlyArray> {\n        Indices {\n            inner: self.inner.into_gpu_only(),\n        }\n    }\n}\n\n/// Holds morph targets for animated nodes.\n#[derive(Clone)]\npub struct MorphTargets {\n    // Held onto so the values don't drop from under us\n    _targets: Arc<Vec<GpuArray<MorphTarget>>>,\n    arrays: HybridArray<Array<MorphTarget>>,\n}\n\nimpl From<&MorphTargets> for MorphTargets {\n    fn from(value: &MorphTargets) -> Self {\n        value.clone()\n    }\n}\n\nimpl std::fmt::Debug for MorphTargets {\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        f.debug_struct(\"MorphTargets\")\n            .field(\"arrays\", &self.arrays)\n            .field(\"targets\", &\"...\")\n            .finish()\n    }\n}\n\nimpl MorphTargets {\n    pub(crate) fn new(\n        slab: &SlabAllocator<impl IsRuntime>,\n        morph_targets: impl IntoIterator<Item = impl IntoIterator<Item = MorphTarget>>,\n    ) -> Self {\n        let targets = morph_targets\n            .into_iter()\n            .map(|verts| slab.new_array(verts).into_gpu_only())\n            .collect::<Vec<_>>();\n        let arrays = slab.new_array(targets.iter().map(|ts| ts.array()));\n        Self {\n            _targets: targets.into(),\n            arrays,\n        }\n    }\n    /// Returns a pointer to the underlying morph targets data on the GPU.\n    pub fn array(&self) -> Array<Array<MorphTarget>> {\n        self.arrays.array()\n    }\n}\n\n/// Holds morph targets weights for animated nodes.\n#[derive(Clone, Debug)]\npub struct MorphTargetWeights {\n    inner: HybridArray<f32>,\n}\n\nimpl From<&MorphTargetWeights> for MorphTargetWeights {\n    fn from(value: &MorphTargetWeights) -> Self {\n        value.clone()\n    }\n}\n\nimpl MorphTargetWeights {\n    pub(crate) fn new(\n        slab: &SlabAllocator<impl IsRuntime>,\n        data: impl IntoIterator<Item = f32>,\n    ) -> Self {\n        Self {\n            inner: slab.new_array(data),\n        }\n    }\n\n    /// Returns a pointer to the underlying morph targets weights data on the\n    /// GPU.\n    pub fn array(&self) -> Array<f32> {\n        self.inner.array()\n    }\n\n    /// Return the weight at the given index, if any.\n    pub fn get_item(&self, index: usize) -> Option<f32> {\n        self.inner.get(index)\n    }\n\n    /// Update the weight at the given index.\n    pub fn set_item(&self, index: usize, weight: f32) {\n        self.inner.set_item(index, weight);\n    }\n}\n\n/// Wrapper around the geometry slab, which holds mesh data and more.\n#[derive(Clone)]\npub struct Geometry {\n    slab: SlabAllocator<WgpuRuntime>,\n    descriptor: Hybrid<GeometryDescriptor>,\n    /// A plain white cube to use as default geometry.\n    default_vertices: Vertices,\n    /// Holds the current camera just in case the user drops it,\n    /// this way we never lose a camera that is in use. Dropping\n    /// the camera would cause a blank screen, which is very confusing\n    /// =(\n    camera: Arc<Mutex<Option<Camera>>>,\n}\n\nimpl AsRef<WgpuRuntime> for Geometry {\n    fn as_ref(&self) -> &WgpuRuntime {\n        self.slab.runtime()\n    }\n}\n\nimpl AsRef<SlabAllocator<WgpuRuntime>> for Geometry {\n    fn as_ref(&self) -> &SlabAllocator<WgpuRuntime> {\n        &self.slab\n    }\n}\n\nimpl Geometry {\n    pub fn new(runtime: impl AsRef<WgpuRuntime>, resolution: UVec2, atlas_size: UVec2) -> Self {\n        let runtime = runtime.as_ref();\n        let slab = SlabAllocator::new(runtime, \"geometry\", wgpu::BufferUsages::empty());\n        let descriptor = slab.new_value(GeometryDescriptor {\n            atlas_size,\n            resolution,\n            ..Default::default()\n        });\n        let default_vertices = Vertices::new(\n            &slab,\n            crate::math::unit_cube().into_iter().map(|(p, n)| {\n                Vertex::default()\n                    .with_position(p)\n                    .with_normal(n)\n                    .with_color(Vec4::ONE)\n            }),\n        );\n        Self {\n            slab,\n            descriptor,\n            default_vertices,\n            camera: Default::default(),\n        }\n    }\n\n    pub fn runtime(&self) -> &WgpuRuntime {\n        self.as_ref()\n    }\n\n    pub fn slab_allocator(&self) -> &SlabAllocator<WgpuRuntime> {\n        self.as_ref()\n    }\n\n    pub fn descriptor(&self) -> &Hybrid<GeometryDescriptor> {\n        &self.descriptor\n    }\n\n    /// Returns the vertices of a white unit cube.\n    pub fn default_vertices(&self) -> &Vertices {\n        &self.default_vertices\n    }\n\n    #[must_use]\n    pub fn commit(&self) -> SlabBuffer<wgpu::Buffer> {\n        self.slab.commit()\n    }\n\n    /// Create a new camera.\n    ///\n    /// If this is the first camera created, it will be automatically used.\n    pub fn new_camera(&self) -> Camera {\n        let c = Camera::new(&self.slab);\n        if self.descriptor.get().camera_id.is_none() {\n            self.use_camera(&c);\n        }\n        c\n    }\n\n    /// Set all geometry to use the given camera.\n    pub fn use_camera(&self, camera: &Camera) {\n        let id = camera.id();\n        log::info!(\"using camera: {id:?}\");\n        // Save a clone so we never lose the active camera, even if the user drops it\n        self.descriptor.modify(|cfg| cfg.camera_id = id);\n        *self.camera.lock().expect(\"geometry camera lock\") = Some(camera.clone());\n    }\n\n    /// Stage a new transform.\n    pub fn new_transform(&self) -> Transform {\n        Transform::new(&self.slab)\n    }\n\n    /// Stage vertex geometry data on the GPU.\n    pub fn new_vertices(&self, vertices: impl IntoIterator<Item = Vertex>) -> Vertices {\n        Vertices::new(self.slab_allocator(), vertices)\n    }\n\n    /// Stage indices that point to offsets of an array of vertices.\n    pub fn new_indices(&self, indices: impl IntoIterator<Item = u32>) -> Indices {\n        Indices::new(self, indices)\n    }\n\n    /// Stage new morph targets on the GPU.\n    pub fn new_morph_targets(\n        &self,\n        data: impl IntoIterator<Item = impl IntoIterator<Item = MorphTarget>>,\n    ) -> MorphTargets {\n        MorphTargets::new(&self.slab, data)\n    }\n\n    /// Create new morph target weights.\n    pub fn new_morph_target_weights(\n        &self,\n        data: impl IntoIterator<Item = f32>,\n    ) -> MorphTargetWeights {\n        MorphTargetWeights::new(&self.slab, data)\n    }\n\n    /// Create a new array of matrices.\n    pub fn new_matrices(&self, data: impl IntoIterator<Item = Mat4>) -> HybridArray<Mat4> {\n        self.slab.new_array(data)\n    }\n\n    pub fn new_skin(\n        &self,\n        joints: impl IntoIterator<Item = impl Into<SkinJoint>>,\n        inverse_bind_matrices: impl IntoIterator<Item = impl Into<Mat4>>,\n    ) -> Skin {\n        Skin::new(self.slab_allocator(), joints, inverse_bind_matrices)\n    }\n}\n\n/// A vertex skin.\n///\n/// For more info on vertex skinning, see\n/// <https://github.khronos.org/glTF-Tutorials/gltfTutorial/gltfTutorial_019_SimpleSkin.html>\n#[derive(Clone)]\npub struct Skin {\n    descriptor: Hybrid<SkinDescriptor>,\n    joints: HybridArray<Id<TransformDescriptor>>,\n    // Held onto so the transforms don't drop from under us\n    _skin_joints: Arc<Mutex<Vec<SkinJoint>>>,\n    // Contains the 4x4 inverse-bind matrices.\n    //\n    // When None, each matrix is assumed to be the 4x4 identity matrix which implies that the\n    // inverse-bind matrices were pre-applied.\n    _inverse_bind_matrices: Arc<Mutex<Option<GpuArray<Mat4>>>>,\n}\n\nimpl From<&Skin> for Skin {\n    fn from(value: &Skin) -> Self {\n        value.clone()\n    }\n}\n\nimpl core::fmt::Debug for Skin {\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        f.debug_struct(\"Skin\")\n            .field(\"descriptor\", &self.descriptor)\n            .field(\"joints\", &self.joints)\n            .field(\"joint_transforms\", &\"...\")\n            .field(\"inverse_bind_matrices\", &\"...\")\n            .finish()\n    }\n}\n\nimpl Skin {\n    /// Stage a new skin on the GPU.\n    pub fn new(\n        slab: &SlabAllocator<impl IsRuntime>,\n        joints: impl IntoIterator<Item = impl Into<SkinJoint>>,\n        inverse_bind_matrices: impl IntoIterator<Item = impl Into<Mat4>>,\n    ) -> Self {\n        let descriptor = slab.new_value(SkinDescriptor::default());\n        let skin_joints = joints.into_iter().map(|t| t.into()).collect::<Vec<_>>();\n        let joints = skin_joints.iter().map(|sj| sj.0.id()).collect::<Vec<_>>();\n        let inverse_bind_matrices = inverse_bind_matrices\n            .into_iter()\n            .map(|m| m.into())\n            .collect::<Vec<_>>();\n        let inverse_bind_matrices = if inverse_bind_matrices.is_empty() {\n            None\n        } else {\n            Some(slab.new_array(inverse_bind_matrices).into_gpu_only())\n        };\n\n        Skin {\n            descriptor,\n            joints: slab.new_array(joints),\n            // We hold on to the transforms so they don't get dropped if the user drops them.\n            _skin_joints: Arc::new(Mutex::new(skin_joints)),\n            _inverse_bind_matrices: Arc::new(Mutex::new(inverse_bind_matrices)),\n        }\n    }\n\n    /// Return a pointer to the underlying descriptor data on the GPU.\n    pub fn id(&self) -> Id<SkinDescriptor> {\n        self.descriptor.id()\n    }\n\n    /// Return a copy of the underlying descriptor.\n    pub fn descriptor(&self) -> SkinDescriptor {\n        self.descriptor.get()\n    }\n}\n\n/// A joint in a skinned rigging.\n///\n/// This is a thin wrapper over [`Transform`] and\n/// [`NestedTransform`]. You can create a [`SkinJoint`]\n/// from either of those types using the [`From`] trait.\n///\n/// You can also pass an iterator of either [`Transform`] or [`NestedTransform`]\n/// to [`Stage::new_skin`](crate::stage::Stage::new_skin).\npub struct SkinJoint(pub(crate) Transform);\n\nimpl From<Transform> for SkinJoint {\n    fn from(transform: Transform) -> Self {\n        SkinJoint(transform)\n    }\n}\n\nimpl From<&Transform> for SkinJoint {\n    fn from(transform: &Transform) -> Self {\n        transform.clone().into()\n    }\n}\n\nimpl From<NestedTransform> for SkinJoint {\n    fn from(value: NestedTransform) -> Self {\n        SkinJoint(value.global_transform)\n    }\n}\n\nimpl From<&NestedTransform> for SkinJoint {\n    fn from(value: &NestedTransform) -> Self {\n        value.clone().into()\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/geometry/shader.rs",
    "content": "use crabslab::{Array, Id, Slab, SlabItem};\nuse glam::Mat4;\n\nuse crate::{\n    camera::shader::CameraDescriptor, geometry::Vertex, transform::shader::TransformDescriptor,\n};\n\n/// A vertex skin descriptor.\n///\n/// For more info on vertex skinning, see\n/// <https://github.khronos.org/glTF-Tutorials/gltfTutorial/gltfTutorial_019_SimpleSkin.html>\n#[derive(Clone, Copy, Default, SlabItem, core::fmt::Debug)]\npub struct SkinDescriptor {\n    // Ids of the skeleton nodes' global transforms used as joints in this skin.\n    pub joints_array: Array<Id<TransformDescriptor>>,\n    // Contains the 4x4 inverse-bind matrices.\n    //\n    // When is none, each matrix is assumed to be the 4x4 identity matrix\n    // which implies that the inverse-bind matrices were pre-applied.\n    pub inverse_bind_matrices_array: Array<Mat4>,\n}\n\nimpl SkinDescriptor {\n    pub fn get_joint_matrix(&self, i: usize, vertex: Vertex, slab: &[u32]) -> Mat4 {\n        let joint_index = vertex.joints[i] as usize;\n        let joint_id = slab.read(self.joints_array.at(joint_index));\n        let joint_transform = slab.read(joint_id);\n        // First apply the inverse bind matrix to bring the vertex into the joint's\n        // local space, then apply the joint's current transformation to move it\n        // into world space.\n        let inverse_bind_matrix = slab.read(self.inverse_bind_matrices_array.at(joint_index));\n        Mat4::from(joint_transform) * inverse_bind_matrix\n    }\n\n    pub fn get_skinning_matrix(&self, vertex: Vertex, slab: &[u32]) -> Mat4 {\n        let mut skinning_matrix = Mat4::ZERO;\n        for i in 0..vertex.joints.len() {\n            let joint_matrix = self.get_joint_matrix(i, vertex, slab);\n            // Ensure weights are applied correctly to the joint matrix\n            let weight = vertex.weights[i];\n            skinning_matrix += weight * joint_matrix;\n        }\n\n        if skinning_matrix == Mat4::ZERO {\n            Mat4::IDENTITY\n        } else {\n            skinning_matrix\n        }\n    }\n}\n\n/// Holds configuration info for vertex and shading render passes of\n/// geometry.\n///\n/// This descriptor lives at the root (index 0) of the geometry slab.\n#[derive(Clone, Copy, PartialEq, SlabItem, core::fmt::Debug)]\n#[offsets]\npub struct GeometryDescriptor {\n    pub camera_id: Id<CameraDescriptor>,\n    pub atlas_size: glam::UVec2,\n    pub resolution: glam::UVec2,\n    pub debug_channel: crate::pbr::debug::DebugChannel,\n    pub has_lighting: bool,\n    pub has_skinning: bool,\n    pub perform_frustum_culling: bool,\n    pub perform_occlusion_culling: bool,\n}\n\nimpl Default for GeometryDescriptor {\n    fn default() -> Self {\n        Self {\n            camera_id: Id::NONE,\n            atlas_size: Default::default(),\n            resolution: glam::UVec2::ONE,\n            debug_channel: Default::default(),\n            has_lighting: true,\n            has_skinning: true,\n            perform_frustum_culling: true,\n            perform_occlusion_culling: false,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/geometry.rs",
    "content": "//! Types and functions for staging geometry.\nuse crate::math::IsVector;\nuse crabslab::SlabItem;\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\nuse glam::{Vec2, Vec3, Vec4};\n\npub mod shader;\n\n/// A displacement target.\n///\n/// Use to displace vertices using weights defined on the mesh.\n///\n/// For more info on morph targets in general, see\n/// <https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#morph-targets>\n#[derive(Clone, Copy, Default, PartialEq, SlabItem, core::fmt::Debug)]\npub struct MorphTarget {\n    pub position: Vec3,\n    pub normal: Vec3,\n    pub tangent: Vec3,\n    // TODO: Extend MorphTargets to include UV and Color.\n    // I think this would take a contribution to the `gltf` crate.\n}\n\n/// A vertex in a mesh.\n#[derive(Clone, Copy, core::fmt::Debug, PartialEq, SlabItem)]\npub struct Vertex {\n    pub position: Vec3,\n    pub color: Vec4,\n    pub uv0: Vec2,\n    pub uv1: Vec2,\n    pub normal: Vec3,\n    pub tangent: Vec4,\n    // Indices that point to this vertex's 'joint' transforms.\n    pub joints: [u32; 4],\n    // The weights of influence that each joint has over this vertex\n    pub weights: [f32; 4],\n}\n\nimpl Default for Vertex {\n    fn default() -> Self {\n        Self {\n            position: Default::default(),\n            color: Vec4::ONE,\n            uv0: Vec2::ZERO,\n            uv1: Vec2::ZERO,\n            normal: Vec3::Z,\n            tangent: Vec4::Y,\n            joints: [0; 4],\n            weights: [0.0; 4],\n        }\n    }\n}\n\nimpl Vertex {\n    pub fn with_position(mut self, p: impl Into<Vec3>) -> Self {\n        self.position = p.into();\n        self\n    }\n\n    pub fn with_color(mut self, c: impl Into<Vec4>) -> Self {\n        self.color = c.into();\n        self\n    }\n\n    pub fn with_uv0(mut self, uv: impl Into<Vec2>) -> Self {\n        self.uv0 = uv.into();\n        self\n    }\n\n    pub fn with_uv1(mut self, uv: impl Into<Vec2>) -> Self {\n        self.uv1 = uv.into();\n        self\n    }\n\n    pub fn with_normal(mut self, n: impl Into<Vec3>) -> Self {\n        self.normal = n.into();\n        self\n    }\n\n    pub fn with_tangent(mut self, t: impl Into<Vec4>) -> Self {\n        self.tangent = t.into();\n        self\n    }\n\n    pub fn generate_normal(a: Vec3, b: Vec3, c: Vec3) -> Vec3 {\n        let ab = a - b;\n        let ac = a - c;\n        ab.cross(ac).normalize()\n    }\n\n    pub fn generate_tangent(a: Vec3, a_uv: Vec2, b: Vec3, b_uv: Vec2, c: Vec3, c_uv: Vec2) -> Vec4 {\n        let ab = b - a;\n        let ac = c - a;\n        let n = ab.cross(ac);\n        let d_uv1 = b_uv - a_uv;\n        let d_uv2 = c_uv - a_uv;\n        let denom = d_uv1.x * d_uv2.y - d_uv2.x * d_uv1.y;\n        let denom_sign = if denom >= 0.0 { 1.0 } else { -1.0 };\n        let denom = denom.abs().max(f32::EPSILON) * denom_sign;\n        let f = 1.0 / denom;\n        let s = f * Vec3::new(\n            d_uv2.y * ab.x - d_uv1.y * ac.x,\n            d_uv2.y * ab.y - d_uv1.y * ac.y,\n            d_uv2.y * ab.z - d_uv1.y * ac.z,\n        );\n        let t = f * Vec3::new(\n            d_uv1.x * ac.x - d_uv2.x * ab.x,\n            d_uv1.x * ac.y - d_uv2.x * ab.y,\n            d_uv1.x * ac.z - d_uv2.x * ab.z,\n        );\n        let n_cross_t_dot_s_sign = if n.cross(t).dot(s) >= 0.0 { 1.0 } else { -1.0 };\n        (s - s.dot(n) * n)\n            .alt_norm_or_zero()\n            .extend(n_cross_t_dot_s_sign)\n    }\n\n    #[cfg(cpu)]\n    /// A triangle list mesh of points.\n    pub fn cube_mesh() -> [Vertex; 36] {\n        let mut mesh = [Vertex::default(); 36];\n        let unit_cube = crate::math::unit_cube();\n        debug_assert_eq!(36, unit_cube.len());\n        for (i, (position, normal)) in unit_cube.into_iter().enumerate() {\n            mesh[i].position = position;\n            mesh[i].normal = normal;\n        }\n        mesh\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/gltf/anime.rs",
    "content": "//! Animation helpers for gltf.\nuse glam::{Quat, Vec3};\nuse snafu::prelude::*;\n\nuse crate::{geometry::MorphTargetWeights, gltf::GltfNode, transform::NestedTransform};\n\n#[derive(Debug, Snafu)]\npub enum InterpolationError {\n    #[snafu(display(\"No keyframes\"))]\n    NoKeyframes,\n\n    #[snafu(display(\"Not enough keyframes\"))]\n    NotEnoughKeyframes,\n\n    #[snafu(display(\"No node with index {index}\"))]\n    MissingNode { index: usize },\n\n    #[snafu(display(\"Property with index {} is missing\", index))]\n    MissingPropertyIndex { index: usize },\n\n    #[snafu(display(\"No previous keyframe, first is {first:?}\"))]\n    NoPreviousKeyframe { first: Keyframe },\n\n    #[snafu(display(\"No next keyframe, last is {last:?}\"))]\n    NoNextKeyframe { last: Keyframe },\n\n    #[snafu(display(\"Mismatched properties\"))]\n    MismatchedProperties,\n}\n\n#[derive(Debug, Clone, Copy)]\npub enum Interpolation {\n    Linear,\n    Step,\n    CubicSpline,\n}\n\nimpl std::fmt::Display for Interpolation {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(match self {\n            Interpolation::Linear => \"linear\",\n            Interpolation::Step => \"step\",\n            Interpolation::CubicSpline => \"cubic spline\",\n        })\n    }\n}\n\nimpl From<gltf::animation::Interpolation> for Interpolation {\n    fn from(value: gltf::animation::Interpolation) -> Self {\n        match value {\n            gltf::animation::Interpolation::Linear => Interpolation::Linear,\n            gltf::animation::Interpolation::Step => Interpolation::Step,\n            gltf::animation::Interpolation::CubicSpline => Interpolation::CubicSpline,\n        }\n    }\n}\n\nimpl Interpolation {\n    fn is_cubic_spline(&self) -> bool {\n        matches!(self, Interpolation::CubicSpline)\n    }\n}\n\n#[derive(Debug, Clone, Copy)]\npub struct Keyframe(pub f32);\n\n#[derive(Debug)]\npub enum TweenProperty {\n    Translation(Vec3),\n    Rotation(Quat),\n    Scale(Vec3),\n    MorphTargetWeights(Vec<f32>),\n}\n\nimpl TweenProperty {\n    fn as_translation(&self) -> Option<&Vec3> {\n        match self {\n            TweenProperty::Translation(a) => Some(a),\n            _ => None,\n        }\n    }\n\n    fn as_rotation(&self) -> Option<&Quat> {\n        match self {\n            TweenProperty::Rotation(a) => Some(a),\n            _ => None,\n        }\n    }\n\n    fn as_scale(&self) -> Option<&Vec3> {\n        match self {\n            TweenProperty::Scale(a) => Some(a),\n            _ => None,\n        }\n    }\n\n    fn as_morph_target_weights(&self) -> Option<&Vec<f32>> {\n        match self {\n            TweenProperty::MorphTargetWeights(ws) => Some(ws),\n            _ => None,\n        }\n    }\n\n    pub fn description(&self) -> &'static str {\n        match self {\n            TweenProperty::Translation(_) => \"translation\",\n            TweenProperty::Rotation(_) => \"rotation\",\n            TweenProperty::Scale(_) => \"scale\",\n            TweenProperty::MorphTargetWeights(_) => \"morph target\",\n        }\n    }\n}\n\n/// Holds many keyframes worth of tweening properties.\n#[derive(Debug, Clone)]\npub enum TweenProperties {\n    Translations(Vec<Vec3>),\n    Rotations(Vec<Quat>),\n    Scales(Vec<Vec3>),\n    MorphTargetWeights(Vec<Vec<f32>>),\n}\n\nimpl TweenProperties {\n    pub fn get(&self, index: usize) -> Option<TweenProperty> {\n        match self {\n            TweenProperties::Translations(translations) => translations\n                .get(index)\n                .map(|translation| TweenProperty::Translation(*translation)),\n            TweenProperties::Rotations(rotations) => rotations\n                .get(index)\n                .map(|rotation| TweenProperty::Rotation(*rotation)),\n            TweenProperties::Scales(scales) => {\n                scales.get(index).map(|scale| TweenProperty::Scale(*scale))\n            }\n            TweenProperties::MorphTargetWeights(weights) => weights\n                .get(index)\n                .map(|weights| TweenProperty::MorphTargetWeights(weights.clone())),\n        }\n    }\n\n    pub fn get_cubic(&self, index: usize) -> Option<[TweenProperty; 3]> {\n        let start = 3 * index;\n        let end = start + 3;\n        match self {\n            TweenProperties::Translations(translations) => {\n                if let Some([p0, p1, p2]) = translations.get(start..end) {\n                    Some([\n                        TweenProperty::Translation(*p0),\n                        TweenProperty::Translation(*p1),\n                        TweenProperty::Translation(*p2),\n                    ])\n                } else {\n                    None\n                }\n            }\n            TweenProperties::Rotations(rotations) => {\n                if let Some([p0, p1, p2]) = rotations.get(start..end) {\n                    Some([\n                        TweenProperty::Rotation(*p0),\n                        TweenProperty::Rotation(*p1),\n                        TweenProperty::Rotation(*p2),\n                    ])\n                } else {\n                    None\n                }\n            }\n            TweenProperties::Scales(scales) => {\n                if let Some([p0, p1, p2]) = scales.get(start..end) {\n                    Some([\n                        TweenProperty::Scale(*p0),\n                        TweenProperty::Scale(*p1),\n                        TweenProperty::Scale(*p2),\n                    ])\n                } else {\n                    None\n                }\n            }\n            TweenProperties::MorphTargetWeights(weights) => {\n                if let Some([p0, p1, p2]) = weights.get(start..end) {\n                    Some([\n                        TweenProperty::MorphTargetWeights(p0.clone()),\n                        TweenProperty::MorphTargetWeights(p1.clone()),\n                        TweenProperty::MorphTargetWeights(p2.clone()),\n                    ])\n                } else {\n                    None\n                }\n            }\n        }\n    }\n\n    pub fn description(&self) -> &'static str {\n        match self {\n            TweenProperties::Translations(_) => \"translation\",\n            TweenProperties::Rotations(_) => \"rotation\",\n            TweenProperties::Scales(_) => \"scale\",\n            TweenProperties::MorphTargetWeights(_) => \"morph targets\",\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct Tween {\n    // Times (inputs)\n    pub keyframes: Vec<Keyframe>,\n    // Properties (outputs)\n    pub properties: TweenProperties,\n    // The type of interpolation\n    pub interpolation: Interpolation,\n    // The gltf \"nodes\" index of the target node this tween applies to\n    pub target_node_index: usize,\n}\n\nimpl Tween {\n    /// Compute the interpolated tween property at the given time.\n    ///\n    /// If the given time is before the first keyframe or after the the last\n    /// keyframe, `Ok(None)` is returned.\n    ///\n    /// See <https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_007_Animations.md>\n    pub fn interpolate(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {\n        snafu::ensure!(!self.keyframes.is_empty(), NoKeyframesSnafu);\n\n        match self.interpolation {\n            Interpolation::Linear => self.interpolate_linear(time),\n            Interpolation::Step => self.interpolate_step(time),\n            Interpolation::CubicSpline => self.interpolate_cubic(time),\n        }\n    }\n\n    /// Compute the interpolated tween property at the given time.\n    ///\n    /// If the time is greater than the last keyframe, the time will be wrapped\n    /// to loop the tween.\n    ///\n    /// Returns `None` if the properties don't match.\n    pub fn interpolate_wrap(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {\n        let total = self.length_in_seconds();\n        let time = time % total;\n        self.interpolate(time)\n    }\n\n    fn get_previous_keyframe(\n        &self,\n        time: f32,\n    ) -> Result<Option<(usize, &Keyframe)>, InterpolationError> {\n        snafu::ensure!(!self.keyframes.is_empty(), NoKeyframesSnafu);\n        Ok(self\n            .keyframes\n            .iter()\n            .enumerate()\n            .rfind(|(_, keyframe)| keyframe.0 <= time))\n    }\n\n    fn get_next_keyframe(\n        &self,\n        time: f32,\n    ) -> Result<Option<(usize, &Keyframe)>, InterpolationError> {\n        snafu::ensure!(!self.keyframes.is_empty(), NoKeyframesSnafu);\n        Ok(self\n            .keyframes\n            .iter()\n            .enumerate()\n            .find(|(_, keyframe)| keyframe.0 > time))\n    }\n\n    fn interpolate_step(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {\n        if let Some((prev_keyframe_ndx, _)) = self.get_previous_keyframe(time)? {\n            self.properties\n                .get(prev_keyframe_ndx)\n                .context(MissingPropertyIndexSnafu {\n                    index: prev_keyframe_ndx,\n                })\n                .map(Some)\n        } else {\n            Ok(None)\n        }\n    }\n\n    fn interpolate_cubic(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {\n        snafu::ensure!(self.keyframes.len() >= 2, NotEnoughKeyframesSnafu);\n\n        let (prev_keyframe_ndx, prev_keyframe) =\n            if let Some(prev) = self.get_previous_keyframe(time)? {\n                prev\n            } else {\n                return Ok(None);\n            };\n        let prev_time = prev_keyframe.0;\n\n        let (next_keyframe_ndx, next_keyframe) = if let Some(next) = self.get_next_keyframe(time)? {\n            next\n        } else {\n            return Ok(None);\n        };\n        let next_time = next_keyframe.0;\n\n        // UNWRAP: safe because we know this was found above\n        let [_, from, from_out] =\n            self.properties\n                .get_cubic(prev_keyframe_ndx)\n                .context(MissingPropertyIndexSnafu {\n                    index: prev_keyframe_ndx,\n                })?;\n        // UNWRAP: safe because we know this is either the first index or was found\n        // above\n        let [to_in, to, _] =\n            self.properties\n                .get_cubic(next_keyframe_ndx)\n                .context(MissingPropertyIndexSnafu {\n                    index: next_keyframe_ndx,\n                })?;\n\n        let delta_time = next_time - prev_time;\n        let amount = (time - prev_time) / (next_time - prev_time);\n\n        fn cubic_spline<T>(\n            previous_point: T,\n            previous_tangent: T,\n            next_point: T,\n            next_tangent: T,\n            t: f32,\n        ) -> T\n        where\n            T: std::ops::Mul<f32, Output = T> + std::ops::Add<Output = T>,\n        {\n            let t2 = t * t;\n            let t3 = t2 * t;\n            previous_point * (2.0 * t3 - 3.0 * t2 + 1.0)\n                + previous_tangent * (t3 - 2.0 * t2 + t)\n                + next_point * (-2.0 * t3 + 3.0 * t2)\n                + next_tangent * (t3 - t2)\n        }\n\n        Ok(Some(match from {\n            TweenProperty::Translation(from) => {\n                let from_out = *from_out\n                    .as_translation()\n                    .context(MismatchedPropertiesSnafu)?;\n                let to_in = *to_in.as_translation().context(MismatchedPropertiesSnafu)?;\n                let to = *to.as_translation().context(MismatchedPropertiesSnafu)?;\n                let previous_tangent = delta_time * from_out;\n                let next_tangent = delta_time * to_in;\n                TweenProperty::Translation(cubic_spline(\n                    from,\n                    previous_tangent,\n                    to,\n                    next_tangent,\n                    amount,\n                ))\n            }\n            TweenProperty::Rotation(from) => {\n                let from_out = *from_out.as_rotation().context(MismatchedPropertiesSnafu)?;\n                let to_in = *to_in.as_rotation().context(MismatchedPropertiesSnafu)?;\n                let to = *to.as_rotation().context(MismatchedPropertiesSnafu)?;\n                let previous_tangent = from_out * delta_time;\n                let next_tangent = to_in * delta_time;\n                TweenProperty::Rotation(cubic_spline(\n                    from,\n                    previous_tangent,\n                    to,\n                    next_tangent,\n                    amount,\n                ))\n            }\n            TweenProperty::Scale(from) => {\n                let from_out = *from_out.as_scale().context(MismatchedPropertiesSnafu)?;\n                let to_in = *to_in.as_scale().context(MismatchedPropertiesSnafu)?;\n                let to = *to.as_scale().context(MismatchedPropertiesSnafu)?;\n                let previous_tangent = from_out * delta_time;\n                let next_tangent = to_in * delta_time;\n                TweenProperty::Scale(cubic_spline(\n                    from,\n                    previous_tangent,\n                    to,\n                    next_tangent,\n                    amount,\n                ))\n            }\n            TweenProperty::MorphTargetWeights(from) => {\n                let from_out = from_out\n                    .as_morph_target_weights()\n                    .context(MismatchedPropertiesSnafu)?;\n                let to_in = to_in\n                    .as_morph_target_weights()\n                    .context(MismatchedPropertiesSnafu)?;\n                let to = to\n                    .as_morph_target_weights()\n                    .context(MismatchedPropertiesSnafu)?;\n\n                let weights = from\n                    .into_iter()\n                    .zip(from_out.iter().zip(to_in.iter().zip(to.iter())))\n                    .map(|(from, (from_out, (to_in, to)))| -> f32 {\n                        let previous_tangent = from_out * delta_time;\n                        let next_tangent = to_in * delta_time;\n                        cubic_spline(from, previous_tangent, *to, next_tangent, amount)\n                    });\n                TweenProperty::MorphTargetWeights(weights.collect())\n            }\n        }))\n    }\n\n    fn interpolate_linear(&self, time: f32) -> Result<Option<TweenProperty>, InterpolationError> {\n        let last_keyframe = self.keyframes.len() - 1;\n        let last_time = self.keyframes[last_keyframe].0;\n        let time = time.min(last_time);\n        let (prev_keyframe_ndx, prev_keyframe) =\n            if let Some(prev) = self.get_previous_keyframe(time)? {\n                prev\n            } else {\n                return Ok(None);\n            };\n        let prev_time = prev_keyframe.0;\n\n        let (next_keyframe_ndx, next_keyframe) = if let Some(next) = self.get_next_keyframe(time)? {\n            next\n        } else {\n            return Ok(None);\n        };\n        let next_time = next_keyframe.0;\n\n        // UNWRAP: safe because we know this was found above\n        let from = self.properties.get(prev_keyframe_ndx).unwrap();\n\n        // UNWRAP: safe because we know this is either the first index or was found\n        // above\n        let to = self.properties.get(next_keyframe_ndx).unwrap();\n\n        let amount = (time - prev_time) / (next_time - prev_time);\n        Ok(Some(match from {\n            TweenProperty::Translation(a) => {\n                let b = to.as_translation().context(MismatchedPropertiesSnafu)?;\n                TweenProperty::Translation(a.lerp(*b, amount))\n            }\n            TweenProperty::Rotation(a) => {\n                let a = a.normalize();\n                let b = to\n                    .as_rotation()\n                    .context(MismatchedPropertiesSnafu)?\n                    .normalize();\n                TweenProperty::Rotation(a.slerp(b, amount))\n            }\n            TweenProperty::Scale(a) => {\n                let b = to.as_scale().context(MismatchedPropertiesSnafu)?;\n                TweenProperty::Scale(a.lerp(*b, amount))\n            }\n            TweenProperty::MorphTargetWeights(a) => {\n                let b = to\n                    .as_morph_target_weights()\n                    .context(MismatchedPropertiesSnafu)?;\n                TweenProperty::MorphTargetWeights(\n                    a.into_iter()\n                        .zip(b)\n                        .map(|(a, b)| a + (b - a) * amount)\n                        .collect(),\n                )\n            }\n        }))\n    }\n\n    pub fn length_in_seconds(&self) -> f32 {\n        if self.keyframes.is_empty() {\n            return 0.0;\n        }\n\n        let last_keyframe = self.keyframes.len() - 1;\n        self.keyframes[last_keyframe].0\n    }\n\n    pub fn get_first_keyframe_property(&self) -> Option<TweenProperty> {\n        match &self.properties {\n            TweenProperties::Translations(ts) => {\n                if self.interpolation.is_cubic_spline() {\n                    ts.get(1).copied().map(TweenProperty::Translation)\n                } else {\n                    ts.first().copied().map(TweenProperty::Translation)\n                }\n            }\n            TweenProperties::Rotations(rs) => {\n                if self.interpolation.is_cubic_spline() {\n                    rs.get(1).copied().map(TweenProperty::Rotation)\n                } else {\n                    rs.first().copied().map(TweenProperty::Rotation)\n                }\n            }\n            TweenProperties::Scales(ss) => {\n                if self.interpolation.is_cubic_spline() {\n                    ss.get(1).copied().map(TweenProperty::Scale)\n                } else {\n                    ss.first().copied().map(TweenProperty::Scale)\n                }\n            }\n            TweenProperties::MorphTargetWeights(ws) => {\n                if self.interpolation.is_cubic_spline() {\n                    ws.get(1).cloned().map(TweenProperty::MorphTargetWeights)\n                } else {\n                    ws.first().cloned().map(TweenProperty::MorphTargetWeights)\n                }\n            }\n        }\n    }\n\n    pub fn get_last_keyframe_property(&self) -> Option<TweenProperty> {\n        match &self.properties {\n            TweenProperties::Translations(ts) => {\n                if self.interpolation.is_cubic_spline() {\n                    let second_last = ts.len() - 2;\n                    ts.get(second_last).copied().map(TweenProperty::Translation)\n                } else {\n                    ts.last().copied().map(TweenProperty::Translation)\n                }\n            }\n            TweenProperties::Rotations(rs) => {\n                if self.interpolation.is_cubic_spline() {\n                    let second_last = rs.len() - 2;\n                    rs.get(second_last).copied().map(TweenProperty::Rotation)\n                } else {\n                    rs.last().copied().map(TweenProperty::Rotation)\n                }\n            }\n            TweenProperties::Scales(ss) => {\n                if self.interpolation.is_cubic_spline() {\n                    let second_last = ss.len() - 2;\n                    ss.get(second_last).copied().map(TweenProperty::Scale)\n                } else {\n                    ss.last().copied().map(TweenProperty::Scale)\n                }\n            }\n            TweenProperties::MorphTargetWeights(ws) => {\n                if self.interpolation.is_cubic_spline() {\n                    let second_last = ws.len() - 2;\n                    ws.get(second_last)\n                        .cloned()\n                        .map(TweenProperty::MorphTargetWeights)\n                } else {\n                    ws.last().cloned().map(TweenProperty::MorphTargetWeights)\n                }\n            }\n        }\n    }\n}\n\n#[derive(Clone, Debug)]\npub struct AnimationNode {\n    pub transform: NestedTransform,\n    pub morph_weights: MorphTargetWeights,\n}\n\nimpl From<&GltfNode> for (usize, AnimationNode) {\n    fn from(node: &GltfNode) -> Self {\n        (\n            node.index,\n            AnimationNode {\n                transform: node.transform.clone(),\n                morph_weights: node.weights.clone(),\n            },\n        )\n    }\n}\n\n#[derive(Debug, Snafu)]\npub enum AnimationError {\n    #[snafu(display(\"Missing inputs\"))]\n    MissingInputs,\n\n    #[snafu(display(\"Missing outputs\"))]\n    MissingOutputs,\n}\n\n#[derive(Default, Debug, Clone)]\npub struct Animation {\n    pub tweens: Vec<Tween>,\n    // The name of this animation, if any.\n    pub name: Option<String>,\n}\n\nimpl Animation {\n    pub fn from_gltf(\n        buffer_data: &[gltf::buffer::Data],\n        animation: gltf::Animation,\n    ) -> Result<Self, AnimationError> {\n        let index = animation.index();\n        let name = animation.name().map(String::from);\n        log::trace!(\"  animation {index} {name:?}\");\n        let mut r_animation = Animation {\n            name,\n            ..Default::default()\n        };\n        for (i, channel) in animation.channels().enumerate() {\n            log::trace!(\"  channel {i}\");\n            let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()]));\n            let inputs = reader.read_inputs().context(MissingInputsSnafu)?;\n            let outputs = reader.read_outputs().context(MissingOutputsSnafu)?;\n            let keyframes = inputs.map(Keyframe).collect::<Vec<_>>();\n            log::trace!(\"    with {} keyframes\", keyframes.len());\n            let interpolation = channel.sampler().interpolation().into();\n            log::trace!(\"    using {interpolation} interpolation\");\n            let index = channel.target().node().index();\n            let name = channel.target().node().name();\n            log::trace!(\"    of node {index} {name:?}\");\n            let tween = Tween {\n                properties: match outputs {\n                    gltf::animation::util::ReadOutputs::Translations(ts) => {\n                        log::trace!(\"    tweens translations\");\n                        TweenProperties::Translations(ts.map(Vec3::from).collect())\n                    }\n                    gltf::animation::util::ReadOutputs::Rotations(rs) => {\n                        log::trace!(\"    tweens rotations\");\n                        TweenProperties::Rotations(rs.into_f32().map(Quat::from_array).collect())\n                    }\n                    gltf::animation::util::ReadOutputs::Scales(ss) => {\n                        log::trace!(\"    tweens scales\");\n                        TweenProperties::Scales(ss.map(Vec3::from).collect())\n                    }\n                    gltf::animation::util::ReadOutputs::MorphTargetWeights(ws) => {\n                        log::trace!(\"    tweens morph target weights\");\n                        let ws = ws.into_f32().collect::<Vec<_>>();\n                        let num_morph_targets = ws.len() / keyframes.len();\n                        log::trace!(\"      weights length  : {}\", ws.len());\n                        log::trace!(\"      keyframes length: {}\", keyframes.len());\n                        log::trace!(\"      morph targets   : {}\", num_morph_targets);\n                        TweenProperties::MorphTargetWeights(\n                            ws.chunks_exact(num_morph_targets)\n                                .map(|chunk| chunk.to_vec())\n                                .collect(),\n                        )\n                    }\n                },\n                keyframes,\n                interpolation,\n                target_node_index: index,\n            };\n            r_animation.tweens.push(tween);\n        }\n\n        let total_time = r_animation.length_in_seconds();\n        log::trace!(\"  taking {total_time} seconds in total\");\n        Ok(r_animation)\n    }\n\n    pub fn length_in_seconds(&self) -> f32 {\n        self.tweens\n            .iter()\n            .flat_map(|tween| tween.keyframes.iter().map(|k| k.0))\n            .max_by(f32::total_cmp)\n            .unwrap_or_default()\n    }\n\n    pub fn get_properties_at_time(\n        &self,\n        t: f32,\n    ) -> Result<Vec<(usize, TweenProperty)>, InterpolationError> {\n        let mut tweens = vec![];\n        for tween in self.tweens.iter() {\n            let prop = if let Some(prop) = tween.interpolate(t)? {\n                prop\n            } else if t >= tween.length_in_seconds() {\n                tween.get_last_keyframe_property().unwrap()\n            } else {\n                tween.get_first_keyframe_property().unwrap()\n            };\n            tweens.push((tween.target_node_index, prop));\n        }\n\n        Ok(tweens.into_iter().collect())\n    }\n\n    pub fn into_animator(\n        self,\n        nodes: impl IntoIterator<Item = (usize, AnimationNode)>,\n    ) -> Animator {\n        Animator::new(nodes, self)\n    }\n\n    pub fn target_node_indices(&self) -> impl Iterator<Item = usize> + '_ {\n        self.tweens.iter().map(|t| t.target_node_index)\n    }\n}\n\n/// Combines [`NestedTransform`] and [`Animation`] to progress an animation.\n///\n/// Applies animations to a list of `(usize, [NestedTransform])` and keeps track\n/// of how much time has elapsed.\n///\n/// To function without errors, the [`Animation`]'s tweens'\n/// [`Tween::target_node_index`] must point to the index of [`NestedTransform`].\n#[derive(Default, Debug, Clone)]\npub struct Animator {\n    /// A time to use as the current amount of seconds elapsed in the running\n    /// of the current animation.\n    pub timestamp: f32,\n    /// All nodes under this animator's control.\n    pub nodes: rustc_hash::FxHashMap<usize, AnimationNode>,\n    /// The animation that will apply to the nodes.\n    pub animation: Animation,\n}\n\nimpl Animator {\n    /// Create a new animator with the given nodes and animation.\n    pub fn new(\n        nodes: impl IntoIterator<Item = impl Into<(usize, AnimationNode)>>,\n        animation: Animation,\n    ) -> Self {\n        let nodes = nodes.into_iter().map(|n| n.into());\n        let nodes = rustc_hash::FxHashMap::from_iter(nodes);\n        Animator {\n            nodes,\n            animation,\n            ..Default::default()\n        }\n    }\n\n    /// Progress the animator's animation, applying any tweened properties to\n    /// the animator's nodes.\n    pub fn progress(&mut self, dt_seconds: f32) -> Result<(), InterpolationError> {\n        log::trace!(\n            \"progressing '{}' {dt_seconds} seconds\",\n            self.animation.name.as_deref().unwrap_or(\"\")\n        );\n        let max_length_seconds = self.animation.length_in_seconds();\n        log::trace!(\"  total length: {max_length_seconds}s\");\n        self.timestamp = (self.timestamp + dt_seconds) % max_length_seconds;\n        log::trace!(\"  current time: {}s\", self.timestamp);\n        let properties = self.animation.get_properties_at_time(self.timestamp)?;\n        log::trace!(\"  {} properties\", properties.len());\n        for (node_index, property) in properties.into_iter() {\n            log::trace!(\"    {node_index} {}\", property.description());\n            // There's plenty of reasons why a node referenced by an animation might not\n            // exist in the animator's \"nodes\":\n            // * the node is not in this scene\n            // * business logic has removed it\n            // * ...and the beat goes on\n            // So we won't fret if we can't find it...\n            if let Some(node) = self.nodes.get(&node_index) {\n                match property {\n                    TweenProperty::Translation(translation) => {\n                        node.transform.set_local_translation(translation);\n                    }\n                    TweenProperty::Rotation(rotation) => {\n                        node.transform.set_local_rotation(rotation);\n                    }\n                    TweenProperty::Scale(scale) => {\n                        node.transform.set_local_scale(scale);\n                    }\n                    TweenProperty::MorphTargetWeights(new_weights) => {\n                        if node.morph_weights.array().is_empty() {\n                            log::error!(\n                                \"animation is applied to morph targets but node {node_index} is \\\n                                 missing weights\"\n                            );\n                        } else {\n                            for (i, w) in new_weights.into_iter().enumerate() {\n                                node.morph_weights.set_item(i, w);\n                            }\n                        }\n                    }\n                }\n            } else {\n                log::warn!(\"missing node {node_index} in animation\");\n            }\n        }\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use crate::{context::Context, gltf::Animator, test::BlockOnFuture};\n    use glam::Vec3;\n\n    #[test]\n    fn gltf_simple_animation() {\n        let ctx = Context::headless(16, 16).block();\n        let stage = ctx\n            .new_stage()\n            .with_bloom(false)\n            .with_background_color(Vec3::ZERO.extend(1.0));\n        let projection = crate::camera::perspective(50.0, 50.0);\n        let view = crate::camera::look_at(Vec3::Z * 3.0, Vec3::ZERO, Vec3::Y);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n\n        let doc = stage\n            .load_gltf_document_from_path(\"../../gltf/animated_triangle.gltf\")\n            .unwrap();\n\n        let nodes = doc\n            .recursive_nodes_in_scene(doc.default_scene.unwrap_or_default())\n            .collect::<Vec<_>>();\n\n        let mut animator = Animator::new(nodes, doc.animations.first().unwrap().clone());\n        log::info!(\"animator: {animator:#?}\");\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::save(\"animation/triangle.png\", img);\n        frame.present();\n\n        let dt = 1.0 / 8.0;\n        for i in 1..=10 {\n            animator.progress(dt).unwrap();\n            let frame = ctx.get_next_frame().unwrap();\n            stage.render(&frame.view());\n            let img = frame.read_image().block().unwrap();\n            img_diff::save(format!(\"animation/triangle{i}.png\"), img);\n            frame.present();\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/gltf.rs",
    "content": "//! GLTF support.\n//!\n//! # Loading GLTF files\n//!\n//! Loading GLTF files is accomplished through\n//! [`Stage::load_gltf_document_from_path`]\n//! and [`Stage::load_gltf_document_from_bytes`].\nuse std::{collections::HashMap, sync::Arc};\n\nuse craballoc::prelude::*;\nuse crabslab::{Array, Id};\nuse glam::{Mat4, Quat, Vec2, Vec3, Vec4};\nuse rustc_hash::{FxHashMap, FxHashSet};\nuse snafu::{OptionExt, ResultExt, Snafu};\n\nuse crate::{\n    atlas::{\n        shader::AtlasTextureDescriptor, AtlasError, AtlasImage, AtlasTexture, TextureAddressMode,\n        TextureModes,\n    },\n    bvol::Aabb,\n    camera::Camera,\n    geometry::{Indices, MorphTarget, MorphTargetWeights, MorphTargets, Skin, Vertex, Vertices},\n    light::{shader::LightStyle, AnalyticalLight, Candela, Lux},\n    material::Material,\n    primitive::Primitive,\n    stage::{Stage, StageError},\n    transform::{shader::TransformDescriptor, NestedTransform},\n    types::{GpuCpuArray, GpuOnlyArray},\n};\n\nmod anime;\npub use anime::*;\n\n#[derive(Debug, Snafu)]\npub enum StageGltfError {\n    #[snafu(\n        display(\n            \"GLTF error with '{}': {source}\",\n            path\n                .as_ref()\n                .map(|p| p.display().to_string())\n                .unwrap_or(\"<bytes>\".to_string())),\n        visibility(pub(crate))\n    )]\n    Gltf {\n        source: gltf::Error,\n        path: Option<std::path::PathBuf>,\n    },\n\n    #[snafu(display(\"{source}\"))]\n    Atlas { source: crate::atlas::AtlasError },\n\n    #[snafu(display(\"Wrong image at index {index} atlas offset {offset}\"))]\n    WrongImage { offset: usize, index: usize },\n\n    #[snafu(display(\"Missing image {index} '{name}'\"))]\n    MissingImage { index: usize, name: String },\n\n    #[snafu(display(\"Missing texture at gltf index {index} slab index {tex_id:?}\"))]\n    MissingTexture {\n        index: usize,\n        tex_id: Id<AtlasTextureDescriptor>,\n    },\n\n    #[snafu(display(\"Missing material with index {index}\"))]\n    MissingMaterial { index: usize },\n\n    #[snafu(display(\"Missing primitive with index {index}\"))]\n    MissingPrimitive { index: usize },\n\n    #[snafu(display(\"Missing mesh with index {index}\"))]\n    MissingMesh { index: usize },\n\n    #[snafu(display(\"Missing node with index {index}\"))]\n    MissingNode { index: usize },\n\n    #[snafu(display(\"Missing light with index {index}\"))]\n    MissingLight { index: usize },\n\n    #[snafu(display(\"Unsupported primitive mode: {:?}\", mode))]\n    PrimitiveMode { mode: gltf::mesh::Mode },\n\n    #[snafu(display(\"No {} attribute for mesh\", attribute.to_string()))]\n    MissingAttribute { attribute: gltf::Semantic },\n\n    #[snafu(display(\"No weights array\"))]\n    MissingWeights,\n\n    #[snafu(display(\"Missing sampler\"))]\n    MissingSampler,\n\n    #[snafu(display(\"Missing gltf camera at index {index}\"))]\n    MissingCamera { index: usize },\n\n    #[snafu(display(\"Node has no skin\"))]\n    NoSkin,\n\n    #[snafu(display(\"Missing gltf skin at index {index}\"))]\n    MissingSkin { index: usize },\n\n    #[snafu(display(\"{source}\"))]\n    Animation { source: anime::AnimationError },\n}\n\nimpl From<AtlasError> for StageGltfError {\n    fn from(source: AtlasError) -> Self {\n        Self::Atlas { source }\n    }\n}\n\nimpl From<gltf::scene::Transform> for TransformDescriptor {\n    fn from(transform: gltf::scene::Transform) -> Self {\n        let (translation, rotation, scale) = transform.decomposed();\n        TransformDescriptor {\n            translation: Vec3::from_array(translation),\n            rotation: Quat::from_array(rotation),\n            scale: Vec3::from_array(scale),\n        }\n    }\n}\n\npub fn from_gltf_light_kind(kind: gltf::khr_lights_punctual::Kind) -> LightStyle {\n    match kind {\n        gltf::khr_lights_punctual::Kind::Directional => LightStyle::Directional,\n        gltf::khr_lights_punctual::Kind::Point => LightStyle::Point,\n        gltf::khr_lights_punctual::Kind::Spot { .. } => LightStyle::Spot,\n    }\n}\n\npub fn gltf_light_intensity_units(kind: gltf::khr_lights_punctual::Kind) -> &'static str {\n    match kind {\n        gltf::khr_lights_punctual::Kind::Directional => \"lux (lm/m^2)\",\n        // sr is \"steradian\"\n        _ => \"candelas (lm/sr)\",\n    }\n}\n\nimpl TextureAddressMode {\n    fn from_gltf(mode: gltf::texture::WrappingMode) -> TextureAddressMode {\n        match mode {\n            gltf::texture::WrappingMode::ClampToEdge => TextureAddressMode::ClampToEdge,\n            gltf::texture::WrappingMode::MirroredRepeat => TextureAddressMode::MirroredRepeat,\n            gltf::texture::WrappingMode::Repeat => TextureAddressMode::Repeat,\n        }\n    }\n}\n\npub fn get_vertex_count(primitive: &gltf::Primitive<'_>) -> u32 {\n    if let Some(indices) = primitive.indices() {\n        let count = indices.count() as u32;\n        log::trace!(\"    has {count} indices\");\n        count\n    } else if let Some(positions) = primitive.get(&gltf::Semantic::Positions) {\n        let count = positions.count() as u32;\n        log::trace!(\"    has {count} positions\");\n        count\n    } else {\n        log::trace!(\"    has no indices nor positions\");\n        0\n    }\n}\n\nimpl Material {\n    pub fn preprocess_images(\n        material: gltf::Material,\n        images: &mut [AtlasImage],\n    ) -> Result<(), StageGltfError> {\n        let pbr = material.pbr_metallic_roughness();\n        if material.unlit() {\n            if let Some(info) = pbr.base_color_texture() {\n                let texture = info.texture();\n                // The index of the image in the original gltf document\n                let image_index = texture.source().index();\n                let name = texture.name().unwrap_or(\"unknown\");\n                // Update the image to ensure it gets transferred correctly\n                let image = images.get_mut(image_index).context(MissingImageSnafu {\n                    index: image_index,\n                    name,\n                })?;\n                image.apply_linear_transfer = true;\n            }\n        } else {\n            if let Some(info) = pbr.base_color_texture() {\n                let texture = info.texture();\n                let name = texture.name().unwrap_or(\"unknown\");\n                let image_index = texture.source().index();\n                // Update the image to ensure it gets transferred correctly\n                let image = images.get_mut(image_index).context(MissingImageSnafu {\n                    index: image_index,\n                    name,\n                })?;\n                image.apply_linear_transfer = true;\n            }\n\n            if let Some(emissive_tex) = material.emissive_texture() {\n                let texture = emissive_tex.texture();\n                let name = texture.name().unwrap_or(\"unknown\");\n                let image_index = texture.source().index();\n                // Update the image to ensure it gets transferred correctly\n                let image = images.get_mut(image_index).context(MissingImageSnafu {\n                    index: image_index,\n                    name,\n                })?;\n                image.apply_linear_transfer = true;\n            }\n        }\n        Ok(())\n    }\n\n    pub fn from_gltf(\n        stage: &Stage,\n        material: gltf::Material,\n        entries: &[AtlasTexture],\n    ) -> Result<Material, StageGltfError> {\n        let name = material.name().map(String::from);\n        log::trace!(\"loading material {:?} {name:?}\", material.index());\n        let pbr = material.pbr_metallic_roughness();\n        let builder = stage.new_material();\n        if material.unlit() {\n            log::trace!(\"  is unlit\");\n            builder.set_has_lighting(false);\n\n            if let Some(info) = pbr.base_color_texture() {\n                let texture = info.texture();\n                let index = texture.index();\n                if let Some(tex) = entries.get(index) {\n                    builder.set_albedo_texture(tex);\n                    builder.set_albedo_tex_coord(info.tex_coord());\n                }\n            }\n            builder.set_albedo_factor(pbr.base_color_factor().into());\n        } else {\n            log::trace!(\"  is pbr\");\n            builder.set_has_lighting(true);\n\n            if let Some(info) = pbr.base_color_texture() {\n                let texture = info.texture();\n                let index = texture.index();\n                if let Some(tex) = entries.get(index) {\n                    builder.set_albedo_texture(tex);\n                    builder.set_albedo_tex_coord(info.tex_coord());\n                }\n            }\n            builder.set_albedo_factor(pbr.base_color_factor().into());\n\n            if let Some(info) = pbr.metallic_roughness_texture() {\n                let index = info.texture().index();\n                if let Some(tex) = entries.get(index) {\n                    builder.set_metallic_roughness_texture(tex);\n                    builder.set_metallic_roughness_tex_coord(info.tex_coord());\n                }\n            } else {\n                builder.set_metallic_factor(pbr.metallic_factor());\n                builder.set_roughness_factor(pbr.roughness_factor());\n            }\n\n            if let Some(norm_tex) = material.normal_texture() {\n                if let Some(tex) = entries.get(norm_tex.texture().index()) {\n                    builder.set_normal_texture(tex);\n                    builder.set_normal_tex_coord(norm_tex.tex_coord());\n                }\n            }\n\n            if let Some(occlusion_tex) = material.occlusion_texture() {\n                if let Some(tex) = entries.get(occlusion_tex.texture().index()) {\n                    builder.set_ambient_occlusion_texture(tex);\n                    builder.set_ambient_occlusion_tex_coord(occlusion_tex.tex_coord());\n                    builder.set_ambient_occlusion_strength(occlusion_tex.strength());\n                }\n            }\n\n            if let Some(emissive_tex) = material.emissive_texture() {\n                let texture = emissive_tex.texture();\n                let index = texture.index();\n                if let Some(tex) = entries.get(index) {\n                    builder.set_emissive_texture(tex);\n                    builder.set_emissive_tex_coord(emissive_tex.tex_coord());\n                }\n            }\n            builder.set_emissive_strength_multiplier(material.emissive_strength().unwrap_or(1.0));\n        };\n        Ok(builder)\n    }\n}\n\npub struct GltfPrimitive<Ct: IsContainer = GpuCpuArray> {\n    pub indices: Indices<Ct>,\n    pub vertices: Vertices<Ct>,\n    pub bounding_box: (Vec3, Vec3),\n    pub material_index: Option<usize>,\n    pub morph_targets: MorphTargets,\n}\n\nimpl<Ct> core::fmt::Debug for GltfPrimitive<Ct>\nwhere\n    Ct: IsContainer<Pointer<Vertex> = Array<Vertex>>,\n    Ct: IsContainer<Pointer<u32> = Array<u32>>,\n{\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        f.debug_struct(\"GltfPrimitive\")\n            .field(\"indices\", &self.indices)\n            .field(\"vertices\", &self.vertices)\n            .field(\"bounding_box\", &self.bounding_box)\n            .field(\"material_index\", &self.material_index)\n            .field(\"morph_targets\", &self.morph_targets)\n            .finish()\n    }\n}\n\nimpl GltfPrimitive {\n    pub fn from_gltf(\n        stage: &Stage,\n        primitive: gltf::Primitive,\n        buffer_data: &[gltf::buffer::Data],\n    ) -> Self {\n        let material_index = primitive.material().index();\n\n        let reader = primitive.reader(|buffer| {\n            let data = buffer_data.get(buffer.index())?;\n            Some(data.0.as_slice())\n        });\n\n        let indices = reader\n            .read_indices()\n            .map(|is| {\n                let indices = is.into_u32().collect::<Vec<_>>();\n                assert_eq!(indices.len() % 3, 0, \"indices do not form triangles\");\n                indices\n            })\n            .unwrap_or_default();\n\n        let positions = reader\n            .read_positions()\n            .into_iter()\n            .flat_map(|ps| ps.map(Vec3::from))\n            .collect::<Vec<_>>();\n\n        let uv0s = reader\n            .read_tex_coords(0)\n            .into_iter()\n            .flat_map(|uvs| uvs.into_f32().map(Vec2::from))\n            .chain(std::iter::repeat(Vec2::ZERO))\n            .take(positions.len())\n            .collect::<Vec<_>>();\n\n        let uv1s = reader\n            .read_tex_coords(0)\n            .into_iter()\n            .flat_map(|uvs| uvs.into_f32().map(Vec2::from))\n            .chain(std::iter::repeat(Vec2::ZERO))\n            .take(positions.len());\n\n        let mut normals = vec![Vec3::Z; positions.len()];\n        if let Some(ns) = reader.read_normals() {\n            let ns = ns.map(Vec3::from).collect::<Vec<_>>();\n            debug_assert_eq!(positions.len(), ns.len());\n            normals = ns;\n        } else {\n            log::trace!(\"    generating normals\");\n\n            let indices = if indices.is_empty() {\n                (0..positions.len() as u32).collect::<Vec<_>>()\n            } else {\n                indices.to_vec()\n            };\n\n            indices.chunks(3).for_each(|chunk| match chunk {\n                [i, j, k] => {\n                    let a = positions[*i as usize];\n                    let b = positions[*j as usize];\n                    let c = positions[*k as usize];\n                    let n = Vertex::generate_normal(a, b, c);\n                    normals[*i as usize] = n;\n                    normals[*j as usize] = n;\n                    normals[*k as usize] = n;\n                }\n                _ => panic!(\"not triangles!\"),\n            });\n        }\n\n        let mut tangents = vec![Vec4::ZERO; positions.len()];\n        if let Some(ts) = reader.read_tangents() {\n            let ts = ts.map(Vec4::from).collect::<Vec<_>>();\n            debug_assert_eq!(positions.len(), ts.len());\n            tangents = ts;\n        } else {\n            log::trace!(\"    generating tangents\");\n            let indices = if indices.is_empty() {\n                (0..positions.len() as u32).collect::<Vec<_>>()\n            } else {\n                indices.to_vec()\n            };\n\n            indices.chunks(3).for_each(|chunk| match chunk {\n                [i, j, k] => {\n                    let a = positions[*i as usize];\n                    let b = positions[*j as usize];\n                    let c = positions[*k as usize];\n                    let a_uv = uv0s[*i as usize];\n                    let b_uv = uv0s[*j as usize];\n                    let c_uv = uv0s[*k as usize];\n\n                    let t = Vertex::generate_tangent(a, a_uv, b, b_uv, c, c_uv);\n                    tangents[*i as usize] = t;\n                    tangents[*j as usize] = t;\n                    tangents[*k as usize] = t;\n                }\n                _ => panic!(\"not triangles!\"),\n            });\n        }\n        let colors = reader\n            .read_colors(0)\n            .into_iter()\n            .flat_map(|cs| cs.into_rgba_f32().map(Vec4::from))\n            .chain(std::iter::repeat(Vec4::ONE))\n            .take(positions.len());\n\n        let joints = reader\n            .read_joints(0)\n            .into_iter()\n            .flat_map(|js| {\n                js.into_u16()\n                    .map(|[a, b, c, d]| [a as u32, b as u32, c as u32, d as u32])\n            })\n            .chain(std::iter::repeat([u32::MAX; 4]))\n            .take(positions.len());\n        let joints = joints.collect::<Vec<_>>();\n        let mut all_joints = FxHashSet::default();\n        for js in joints.iter() {\n            all_joints.extend(*js);\n        }\n        log::debug!(\"  joints: {all_joints:?}\");\n\n        const UNWEIGHTED_WEIGHTS: [f32; 4] = [1.0, 0.0, 0.0, 0.0];\n        let mut logged_weights_not_f32 = false;\n        let weights = reader\n            .read_weights(0)\n            .into_iter()\n            .flat_map(|ws| {\n                if !logged_weights_not_f32 {\n                    match ws {\n                        gltf::mesh::util::ReadWeights::U8(_) => log::warn!(\"weights are u8\"),\n                        gltf::mesh::util::ReadWeights::U16(_) => log::warn!(\"weights are u16\"),\n                        gltf::mesh::util::ReadWeights::F32(_) => {}\n                    }\n                    logged_weights_not_f32 = true;\n                }\n                ws.into_f32().map(|weights| {\n                    // normalize the weights\n                    let sum = weights[0] + weights[1] + weights[2] + weights[3];\n                    weights.map(|w| w / sum)\n                })\n            })\n            .chain(std::iter::repeat(UNWEIGHTED_WEIGHTS))\n            .take(positions.len());\n\n        // See the GLTF spec on morph targets\n        // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#morph-targets\n        //\n        // TODO: Generate morph target normals and tangents if absent.\n        // Although the spec says we have to generate normals or tangents if not\n        // specified, we are explicitly *not* doing that here.\n        let morph_targets: Vec<Vec<MorphTarget>> = reader\n            .read_morph_targets()\n            .map(|(may_ps, may_ns, may_ts)| {\n                let ps = may_ps\n                    .into_iter()\n                    .flat_map(|iter| iter.map(Vec3::from_array))\n                    .chain(std::iter::repeat(Vec3::ZERO))\n                    .take(positions.len());\n\n                let ns = may_ns\n                    .into_iter()\n                    .flat_map(|iter| iter.map(Vec3::from_array))\n                    .chain(std::iter::repeat(Vec3::ZERO))\n                    .take(positions.len());\n\n                let ts = may_ts\n                    .into_iter()\n                    .flat_map(|iter| iter.map(Vec3::from_array))\n                    .chain(std::iter::repeat(Vec3::ZERO))\n                    .take(positions.len());\n\n                ps.zip(ns)\n                    .zip(ts)\n                    .map(|((position, normal), tangent)| MorphTarget {\n                        position,\n                        normal,\n                        tangent,\n                    })\n                    .collect()\n            })\n            .collect();\n        log::debug!(\n            \"  {} morph_targets: {:?}\",\n            morph_targets.len(),\n            morph_targets.iter().map(|mt| mt.len()).collect::<Vec<_>>()\n        );\n        let morph_targets = stage.new_morph_targets(morph_targets);\n        let vs = joints.into_iter().zip(weights);\n        let vs = colors.zip(vs);\n        let vs = tangents.into_iter().zip(vs);\n        let vs = normals.into_iter().zip(vs);\n        let vs = uv1s.zip(vs);\n        let vs = uv0s.into_iter().zip(vs);\n        let vs = positions.into_iter().zip(vs);\n\n        let mut min = Vec3::splat(f32::INFINITY);\n        let mut max = Vec3::splat(f32::NEG_INFINITY);\n        let vertices = vs\n            .map(\n                |(position, (uv0, (uv1, (normal, (tangent, (color, (joints, weights)))))))| {\n                    min = min.min(position);\n                    max = max.max(position);\n                    Vertex {\n                        position,\n                        color,\n                        uv0,\n                        uv1,\n                        normal,\n                        tangent,\n                        joints,\n                        weights,\n                    }\n                },\n            )\n            .collect::<Vec<_>>();\n        let vertices = stage.new_vertices(vertices);\n        log::debug!(\n            \"{} vertices, {:?}\",\n            vertices.array().len(),\n            vertices.array()\n        );\n        let indices = stage.new_indices(indices);\n        log::debug!(\"{} indices, {:?}\", indices.array().len(), indices.array());\n        let (bbmin, bbmax) = {\n            let gltf::mesh::Bounds { min, max } = primitive.bounding_box();\n            (Vec3::from_array(min), Vec3::from_array(max))\n        };\n        if bbmin != min {\n            log::warn!(\"gltf supplied bounding box min ({bbmin:?}) doesn't match seen ({min:?})\");\n        }\n        if bbmax != max {\n            log::warn!(\"gltf supplied bounding box max ({bbmax:?}) doesn't match seen ({max:?})\");\n        }\n        let bounding_box = (min, max);\n\n        log::info!(\"primitive '{}' bounds: {bounding_box:?}\", primitive.index());\n\n        Self {\n            vertices,\n            indices,\n            material_index,\n            morph_targets,\n            bounding_box,\n        }\n    }\n\n    pub fn into_gpu_only(self) -> GltfPrimitive<GpuOnlyArray> {\n        let Self {\n            indices,\n            vertices,\n            bounding_box,\n            material_index,\n            morph_targets,\n        } = self;\n        GltfPrimitive {\n            indices: indices.into_gpu_only(),\n            vertices: vertices.into_gpu_only(),\n            bounding_box,\n            material_index,\n            morph_targets,\n        }\n    }\n}\n\npub struct GltfMesh<Ct: IsContainer = GpuCpuArray> {\n    /// Mesh primitives, aka meshlets\n    pub primitives: Vec<GltfPrimitive<Ct>>,\n    /// Morph target weights\n    pub weights: MorphTargetWeights,\n}\n\nimpl<Ct> core::fmt::Debug for GltfMesh<Ct>\nwhere\n    Ct: IsContainer<Pointer<Vertex> = Array<Vertex>>,\n    Ct: IsContainer<Pointer<u32> = Array<u32>>,\n{\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        f.debug_struct(\"GltfMesh\")\n            .field(\"primitives\", &self.primitives)\n            .field(\"weights\", &self.weights)\n            .finish()\n    }\n}\n\nimpl GltfMesh {\n    fn from_gltf(stage: &Stage, buffer_data: &[gltf::buffer::Data], mesh: gltf::Mesh) -> Self {\n        log::debug!(\"Loading primitives for mesh {}\", mesh.index());\n        let primitives = mesh\n            .primitives()\n            .map(|prim| GltfPrimitive::from_gltf(stage, prim, buffer_data))\n            .collect::<Vec<_>>();\n        log::debug!(\"  loaded {} primitives\\n\", primitives.len());\n        let weights = mesh.weights().unwrap_or(&[]).iter().copied();\n        GltfMesh {\n            primitives,\n            weights: stage.new_morph_target_weights(weights),\n        }\n    }\n\n    pub fn into_gpu_only(self) -> GltfMesh<GpuOnlyArray> {\n        let Self {\n            primitives,\n            weights,\n        } = self;\n        let primitives = primitives\n            .into_iter()\n            .map(GltfPrimitive::into_gpu_only)\n            .collect::<Vec<_>>();\n        GltfMesh {\n            primitives,\n            weights,\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct GltfCamera {\n    pub index: usize,\n    pub name: Option<String>,\n    pub node_transform: NestedTransform,\n    pub camera: Camera,\n}\n\nimpl AsRef<Camera> for GltfCamera {\n    fn as_ref(&self) -> &Camera {\n        &self.camera\n    }\n}\n\nimpl GltfCamera {\n    fn new(stage: &Stage, gltf_camera: gltf::Camera<'_>, transform: &NestedTransform) -> Self {\n        log::debug!(\"camera: {}\", gltf_camera.name().unwrap_or(\"unknown\"));\n        log::debug!(\"  transform: {:#?}\", transform.global_descriptor());\n        let projection = match gltf_camera.projection() {\n            gltf::camera::Projection::Orthographic(o) => glam::Mat4::orthographic_rh(\n                -o.xmag(),\n                o.xmag(),\n                -o.ymag(),\n                o.ymag(),\n                o.znear(),\n                o.zfar(),\n            ),\n            gltf::camera::Projection::Perspective(p) => {\n                let fovy = p.yfov();\n                let aspect = p.aspect_ratio().unwrap_or(1.0);\n                if let Some(zfar) = p.zfar() {\n                    glam::Mat4::perspective_rh(fovy, aspect, p.znear(), zfar)\n                } else {\n                    glam::Mat4::perspective_infinite_rh(\n                        p.yfov(),\n                        p.aspect_ratio().unwrap_or(1.0),\n                        p.znear(),\n                    )\n                }\n            }\n        };\n        let view = Mat4::from(transform.global_descriptor()).inverse();\n        let camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n        // what else can we get out of the camera\n        let extensions = gltf_camera.extensions();\n        log::debug!(\"  extensions: {extensions:#?}\");\n        let extras = gltf_camera.extras();\n        log::debug!(\"  extras: {extras:#?}\");\n        GltfCamera {\n            index: gltf_camera.index(),\n            name: gltf_camera.name().map(String::from),\n            node_transform: transform.clone(),\n            camera,\n        }\n    }\n}\n\n/// A node in a GLTF document, ready to be 'drawn'.\n#[derive(Clone, Debug)]\npub struct GltfNode {\n    /// Index of this node in the `StagedGltfDocument`'s `nodes` field.\n    pub index: usize,\n    /// Name of the node, if any,\n    pub name: Option<String>,\n    /// Id of the light this node refers to.\n    pub light: Option<usize>,\n    /// Index of the mesh in the document's meshes, if any.\n    pub mesh: Option<usize>,\n    /// Index into the cameras array, if any.\n    pub camera: Option<usize>,\n    /// Index of the skin in the document's skins, if any.\n    pub skin: Option<usize>,\n    /// Indices of the children of this node.\n    ///\n    /// Each element indexes into the `GltfDocument`'s `nodes` field.\n    pub children: Vec<usize>,\n    /// Morph target weights\n    pub weights: MorphTargetWeights,\n    /// This node's transform.\n    pub transform: NestedTransform,\n}\n\nimpl GltfNode {\n    pub fn global_transform(&self) -> TransformDescriptor {\n        self.transform.global_descriptor()\n    }\n}\n\n#[derive(Clone, Debug)]\npub struct GltfSkin {\n    pub index: usize,\n    // Indices of the skeleton nodes used as joints in this skin, unused internally\n    // but possibly useful.\n    pub joint_nodes: Vec<usize>,\n    // Index of the node used as the skeleton root.\n    // When None, joints transforms resolve to scene root.\n    pub skeleton: Option<usize>,\n    // Skin as seen by renderling\n    pub skin: Skin,\n}\n\nimpl GltfSkin {\n    pub fn from_gltf(\n        stage: &Stage,\n        buffer_data: &[gltf::buffer::Data],\n        nodes: &[GltfNode],\n        gltf_skin: gltf::Skin,\n    ) -> Result<Self, StageGltfError> {\n        log::debug!(\"reading skin {} {:?}\", gltf_skin.index(), gltf_skin.name());\n        let joint_nodes = gltf_skin.joints().map(|n| n.index()).collect::<Vec<_>>();\n        log::debug!(\"  has {} joints\", joint_nodes.len());\n        let mut joint_transforms = vec![];\n        for node_index in joint_nodes.iter() {\n            let gltf_node: &GltfNode = nodes\n                .get(*node_index)\n                .context(MissingNodeSnafu { index: *node_index })?;\n            let transform_id = gltf_node.transform.global_descriptor();\n            log::debug!(\"    joint node {node_index} is {transform_id:?}\");\n            joint_transforms.push(gltf_node.transform.clone());\n        }\n        let reader = gltf_skin.reader(|b| buffer_data.get(b.index()).map(|d| d.0.as_slice()));\n        let mut inverse_bind_matrices = vec![];\n        if let Some(mats) = reader.read_inverse_bind_matrices() {\n            let invs = mats\n                .into_iter()\n                .map(|m| Mat4::from_cols_array_2d(&m))\n                .collect::<Vec<_>>();\n            log::debug!(\"  has {} inverse bind matrices\", invs.len());\n            inverse_bind_matrices = invs;\n        } else {\n            log::debug!(\"  no inverse bind matrices\");\n        }\n        let skeleton = if let Some(n) = gltf_skin.skeleton() {\n            let index = n.index();\n            log::debug!(\"  skeleton is node {index}, {:?}\", n.name());\n            Some(index)\n        } else {\n            log::debug!(\"  skeleton is assumed to be the scene root\");\n            None\n        };\n        Ok(GltfSkin {\n            index: gltf_skin.index(),\n            skin: stage.new_skin(joint_transforms, inverse_bind_matrices),\n            joint_nodes,\n            skeleton,\n        })\n    }\n}\n\n/// A loaded GLTF document.\n///\n/// After being loaded, a [`GltfDocument`] is a collection of staged resources.\n///\n/// All primitives are automatically added to the [`Stage`] they were loaded\n/// from.\n///\n/// ## Note\n///\n/// After being loaded, the `meshes` field contains [`Vertices`] and [`Indices`]\n/// that can be inspected from the CPU. This has memory implications, so if your\n/// document contains lots of geometric data it is advised that you unload that\n/// data from the CPU using [`GltfDocument::into_gpu_only`].\npub struct GltfDocument<Ct: IsContainer = GpuCpuArray> {\n    pub animations: Vec<Animation>,\n    pub cameras: Vec<GltfCamera>,\n    pub default_scene: Option<usize>,\n    pub extensions: Option<serde_json::Value>,\n    pub textures: Vec<AtlasTexture>,\n    pub lights: Vec<AnalyticalLight>,\n    pub meshes: Vec<GltfMesh<Ct>>,\n    pub nodes: Vec<GltfNode>,\n    pub default_material: Material,\n    pub materials: Vec<Material>,\n    // map of node index to primitives\n    pub primitives: FxHashMap<usize, Vec<Primitive>>,\n    /// Vector of scenes - each being a list of nodes.\n    pub scenes: Vec<Vec<usize>>,\n    pub skins: Vec<GltfSkin>,\n}\n\nimpl GltfDocument {\n    pub fn from_gltf(\n        stage: &Stage,\n        document: &gltf::Document,\n        buffer_data: Vec<gltf::buffer::Data>,\n        images: Vec<gltf::image::Data>,\n    ) -> Result<GltfDocument, StageError> {\n        let textures = {\n            let mut images = images.into_iter().map(AtlasImage::from).collect::<Vec<_>>();\n            for gltf_material in document.materials() {\n                Material::preprocess_images(gltf_material, &mut images)?;\n            }\n            // Arc these images because they could be large and we don't want duplicates\n            let images = images.into_iter().map(Arc::new).collect::<Vec<_>>();\n\n            log::debug!(\"Loading {} images into the atlas\", images.len());\n\n            log::debug!(\"Writing textures\");\n            #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]\n            struct Texture {\n                source: usize,\n                modes: TextureModes,\n            }\n            let mut textures = vec![];\n            let mut deduped_textures = FxHashMap::<Texture, Vec<usize>>::default();\n            for (i, texture) in document.textures().enumerate() {\n                let index = texture.index();\n                debug_assert_eq!(i, index);\n                let name = texture.name().unwrap_or(\"unknown\");\n                log::trace!(\"  texture {i} '{name}'\",);\n                let source = texture.source().index();\n                let modes = TextureModes {\n                    s: TextureAddressMode::from_gltf(texture.sampler().wrap_s()),\n                    t: TextureAddressMode::from_gltf(texture.sampler().wrap_t()),\n                };\n                let tex = Texture { modes, source };\n                textures.push(tex);\n                let entry = deduped_textures.entry(tex).or_default();\n                entry.push(i);\n            }\n\n            // Prepare the textures for packing\n            let mut deduped_textures = deduped_textures.into_iter().collect::<Vec<_>>();\n            deduped_textures.sort();\n            let mut prepared_images = vec![];\n            for (tex, refs) in deduped_textures.iter() {\n                let image = images\n                    .get(tex.source)\n                    .context(MissingImageSnafu {\n                        index: refs[0],\n                        name: \"unknown\".to_owned(),\n                    })?\n                    .clone();\n                prepared_images.push(image);\n            }\n            let duplicated_image_count = prepared_images.len() - images.len();\n            if duplicated_image_count > 0 {\n                log::debug!(\"had to duplicate {duplicated_image_count} images...\");\n            }\n            drop(images);\n\n            let prepared_images: Vec<AtlasImage> = prepared_images\n                .into_iter()\n                .map(|aimg| match Arc::try_unwrap(aimg) {\n                    Ok(img) => img,\n                    Err(aimg) => aimg.as_ref().clone(),\n                })\n                .collect();\n            let hybrid_textures = stage.add_images(prepared_images)?;\n            let mut texture_lookup = FxHashMap::<usize, AtlasTexture>::default();\n            for (hybrid, (tex, refs)) in hybrid_textures.into_iter().zip(deduped_textures) {\n                hybrid.set_modes(tex.modes);\n                for tex_index in refs.into_iter() {\n                    texture_lookup.insert(tex_index, hybrid.clone());\n                }\n            }\n            let mut textures = texture_lookup.into_iter().collect::<Vec<_>>();\n            textures.sort_by_key(|(index, _)| *index);\n            textures\n                .into_iter()\n                .map(|(_, hybrid)| hybrid)\n                .collect::<Vec<_>>()\n        };\n\n        log::debug!(\"Creating materials\");\n        let mut default_material = stage.default_material().clone();\n        let mut materials = vec![];\n        for gltf_material in document.materials() {\n            let material_index = gltf_material.index();\n            let material = Material::from_gltf(stage, gltf_material, &textures)?;\n            if let Some(index) = material_index {\n                log::trace!(\"  created material {index}\");\n                debug_assert_eq!(index, materials.len(), \"unexpected material index\");\n                materials.push(material);\n            } else {\n                log::trace!(\"  created default material\");\n                default_material = material;\n            }\n        }\n        log::trace!(\"  created {} materials\", materials.len());\n\n        log::debug!(\"Loading meshes\");\n        let mut meshes = vec![];\n        for mesh in document.meshes() {\n            let mesh = GltfMesh::from_gltf(stage, &buffer_data, mesh);\n            meshes.push(mesh);\n        }\n        log::trace!(\"  loaded {} meshes\", meshes.len());\n\n        log::debug!(\"Loading {} nodes\", document.nodes().count());\n        let mut nodes = vec![];\n        let mut node_transforms = HashMap::<usize, NestedTransform>::new();\n\n        fn transform_for_node(\n            nesting_level: usize,\n            stage: &Stage,\n            cache: &mut HashMap<usize, NestedTransform>,\n            node: &gltf::Node,\n        ) -> NestedTransform {\n            let padding = std::iter::repeat_n(\" \", nesting_level * 2)\n                .collect::<Vec<_>>()\n                .join(\"\");\n            let nt = if let Some(nt) = cache.get(&node.index()) {\n                nt.clone()\n            } else {\n                let TransformDescriptor {\n                    translation,\n                    rotation,\n                    scale,\n                } = node.transform().into();\n                let transform = stage\n                    .new_nested_transform()\n                    .with_local_translation(translation)\n                    .with_local_rotation(rotation)\n                    .with_local_scale(scale);\n                for node in node.children() {\n                    let child_transform =\n                        transform_for_node(nesting_level + 1, stage, cache, &node);\n                    transform.add_child(&child_transform);\n                }\n                cache.insert(node.index(), transform.clone());\n                transform\n            };\n            let t = nt.local_descriptor();\n            log::trace!(\n                \"{padding}{} {:?} {:?} {:?} {:?}\",\n                node.index(),\n                node.name(),\n                t.translation,\n                t.rotation,\n                t.scale\n            );\n            nt\n        }\n        let mut camera_index_to_node_index = HashMap::<usize, usize>::new();\n        let mut light_index_to_node_index = HashMap::<usize, usize>::new();\n        for (i, node) in document.nodes().enumerate() {\n            let node_index = node.index();\n            if let Some(camera) = node.camera() {\n                camera_index_to_node_index.insert(camera.index(), node_index);\n            }\n            if let Some(light) = node.light() {\n                light_index_to_node_index.insert(light.index(), node_index);\n            }\n\n            debug_assert_eq!(i, node_index);\n            let children = node.children().map(|node| node.index()).collect::<Vec<_>>();\n            let mesh = node.mesh().map(|mesh| mesh.index());\n            let skin = node.skin().map(|skin| skin.index());\n            let camera = node.camera().map(|camera| camera.index());\n            let light = node.light().map(|light| light.index());\n            let weights = node.weights().map(|w| w.to_vec()).unwrap_or_default();\n            // From the glTF spec:\n            //\n            // A mesh with morph targets MAY also define an optional mesh.weights property\n            // that stores the default targets' weights. These weights MUST be used when\n            // node.weights is undefined. When mesh.weights is undefined, the default\n            // targets' weights are zeros.\n            let weights = if weights.is_empty() {\n                if let Some(mesh) = node.mesh() {\n                    meshes[mesh.index()].weights.clone()\n                } else {\n                    stage.new_morph_target_weights(weights)\n                }\n            } else {\n                stage.new_morph_target_weights(weights)\n            };\n            let transform = transform_for_node(0, stage, &mut node_transforms, &node);\n            nodes.push(GltfNode {\n                index: node.index(),\n                name: node.name().map(String::from),\n                light,\n                mesh,\n                camera,\n                skin,\n                children,\n                weights,\n                transform,\n            });\n        }\n        log::trace!(\"  loaded {} nodes\", nodes.len());\n\n        log::trace!(\"Loading cameras\");\n        let mut cameras = vec![];\n        for camera in document.cameras() {\n            let camera_index = camera.index();\n            let node_index =\n                *camera_index_to_node_index\n                    .get(&camera_index)\n                    .context(MissingCameraSnafu {\n                        index: camera_index,\n                    })?;\n            let transform = node_transforms\n                .get(&node_index)\n                .context(MissingNodeSnafu { index: node_index })?;\n            cameras.push(GltfCamera::new(stage, camera, transform));\n        }\n\n        log::trace!(\"Loading lights\");\n        let mut lights = vec![];\n        if let Some(gltf_lights) = document.lights() {\n            for gltf_light in gltf_lights {\n                let node_index = *light_index_to_node_index.get(&gltf_light.index()).context(\n                    MissingCameraSnafu {\n                        index: gltf_light.index(),\n                    },\n                )?;\n\n                let node_transform = node_transforms\n                    .get(&node_index)\n                    .context(MissingNodeSnafu { index: node_index })?\n                    .clone();\n\n                let color = Vec3::from(gltf_light.color()).extend(1.0);\n                let intensity = gltf_light.intensity();\n                let light_bundle = match gltf_light.kind() {\n                    gltf::khr_lights_punctual::Kind::Directional => {\n                        stage\n                            .new_directional_light()\n                            .with_direction(Vec3::NEG_Z)\n                            .with_color(color)\n                            // Assumed to be lux\n                            .with_intensity(Lux(intensity))\n                            .into_generic()\n                    }\n\n                    gltf::khr_lights_punctual::Kind::Point => stage\n                        .new_point_light()\n                        .with_position(Vec3::ZERO)\n                        .with_color(color)\n                        // Assumed to be Candelas.\n                        //\n                        // This is converted to radiometric units in the PBR shader.\n                        .with_intensity(Candela(intensity))\n                        .into_generic(),\n\n                    gltf::khr_lights_punctual::Kind::Spot {\n                        inner_cone_angle,\n                        outer_cone_angle,\n                    } => stage\n                        .new_spot_light()\n                        .with_position(Vec3::ZERO)\n                        .with_direction(Vec3::NEG_Z)\n                        .with_inner_cutoff(inner_cone_angle)\n                        .with_outer_cutoff(outer_cone_angle)\n                        .with_color(color)\n                        // Assumed to be Candelas.\n                        //\n                        // This is converted to radiometric units in the PBR shader.\n                        .with_intensity(Candela(intensity))\n                        .into_generic(),\n                };\n\n                log::trace!(\n                    \"  linking light {:?} with node transform {:?}: {:#?}\",\n                    light_bundle.id(),\n                    node_transform.global_id(),\n                    node_transform.global_descriptor()\n                );\n                light_bundle.link_node_transform(&node_transform);\n                lights.push(light_bundle);\n            }\n        }\n\n        log::trace!(\"Loading skins\");\n        let mut skins = vec![];\n        for skin in document.skins() {\n            skins.push(GltfSkin::from_gltf(stage, &buffer_data, &nodes, skin)?);\n        }\n\n        log::trace!(\"Loading animations\");\n        let mut animations = vec![];\n        for animation in document.animations() {\n            animations.push(Animation::from_gltf(&buffer_data, animation).context(AnimationSnafu)?);\n        }\n\n        log::debug!(\"Loading scenes\");\n        let scenes = document\n            .scenes()\n            .map(|scene| scene.nodes().map(|node| node.index()).collect())\n            .collect();\n\n        log::debug!(\"Creating renderlets\");\n        let mut renderlets = FxHashMap::default();\n        for gltf_node in nodes.iter() {\n            let mut node_renderlets = vec![];\n            let maybe_skin = if let Some(skin_index) = gltf_node.skin {\n                log::debug!(\"  node {} {:?} has skin\", gltf_node.index, gltf_node.name);\n                let gltf_skin = skins\n                    .get(skin_index)\n                    .context(MissingSkinSnafu { index: skin_index })?;\n                Some(gltf_skin.skin.clone())\n            } else {\n                None\n            };\n\n            if let Some(mesh_index) = gltf_node.mesh {\n                log::trace!(\n                    \"  node {} {:?} has mesh {mesh_index}\",\n                    gltf_node.index,\n                    gltf_node.name\n                );\n                let mesh = meshes\n                    .get(mesh_index)\n                    .context(MissingMeshSnafu { index: mesh_index })?;\n                let num_prims = mesh.primitives.len();\n                log::trace!(\"    has {num_prims} primitives\");\n                for (prim, i) in mesh.primitives.iter().zip(1..) {\n                    let material = prim\n                        .material_index\n                        .and_then(|index| materials.get(index))\n                        .unwrap_or(&default_material);\n                    let renderlet = stage\n                        .new_primitive()\n                        .with_vertices(&prim.vertices)\n                        .with_indices(&prim.indices)\n                        .with_transform(&gltf_node.transform)\n                        .with_material(material)\n                        .with_bounds(prim.bounding_box.into())\n                        .with_morph_targets(&prim.morph_targets, &gltf_node.weights);\n                    if let Some(skin) = maybe_skin.as_ref() {\n                        renderlet.set_skin(skin);\n                    }\n                    log::trace!(\n                        \"    created renderlet {i}/{num_prims}: {:#?}\",\n                        renderlet.id()\n                    );\n                    node_renderlets.push(renderlet);\n                }\n            }\n            if !node_renderlets.is_empty() {\n                renderlets.insert(gltf_node.index, node_renderlets);\n            }\n        }\n\n        log::debug!(\"Extensions used: {:?}\", document.extensions_used());\n        log::debug!(\"Extensions required: {:?}\", document.extensions_required());\n        log::debug!(\"Done loading gltf\");\n\n        Ok(GltfDocument {\n            textures,\n            animations,\n            lights,\n            cameras,\n            materials,\n            default_material,\n            meshes,\n            nodes,\n            scenes,\n            skins,\n            default_scene: document.default_scene().map(|scene| scene.index()),\n            primitives: renderlets,\n            extensions: document\n                .extensions()\n                .cloned()\n                .map(serde_json::Value::Object),\n        })\n    }\n\n    /// Unload vertex and index data from the CPU.\n    ///\n    /// The data can still be updated from the CPU, but will not be inspectable.\n    pub fn into_gpu_only(self) -> GltfDocument<GpuOnlyArray> {\n        let Self {\n            animations,\n            cameras,\n            default_scene,\n            extensions,\n            textures,\n            lights,\n            meshes,\n            nodes,\n            default_material,\n            materials,\n            primitives,\n            scenes,\n            skins,\n        } = self;\n        let meshes = meshes\n            .into_iter()\n            .map(GltfMesh::into_gpu_only)\n            .collect::<Vec<_>>();\n        GltfDocument {\n            animations,\n            cameras,\n            default_scene,\n            extensions,\n            textures,\n            lights,\n            meshes,\n            nodes,\n            default_material,\n            materials,\n            primitives,\n            scenes,\n            skins,\n        }\n    }\n}\n\nimpl<Ct> GltfDocument<Ct>\nwhere\n    Ct: IsContainer<Pointer<Vertex> = Array<Vertex>>,\n    Ct: IsContainer<Pointer<u32> = Array<u32>>,\n{\n    pub fn renderlets_iter(&self) -> impl Iterator<Item = &Primitive> {\n        self.primitives.iter().flat_map(|(_, rs)| rs.iter())\n    }\n\n    fn collect_nodes_recursive<'a>(&'a self, node_index: usize, nodes: &mut Vec<&'a GltfNode>) {\n        if let Some(node) = self.nodes.get(node_index) {\n            nodes.push(node);\n            for child_index in node.children.iter() {\n                self.collect_nodes_recursive(*child_index, nodes);\n            }\n        }\n    }\n\n    /// Returns the root (top-level) nodes in the given scene.\n    ///\n    /// This roughly follows [`gltf::Scene::nodes`](https://docs.rs/gltf/latest/gltf/scene/struct.Scene.html#method.nodes),\n    /// returning only the nodes directly referenced by the scene — not\n    /// their children.\n    ///\n    /// Use [`recursive_nodes_in_scene`](Self::recursive_nodes_in_scene)\n    /// if you need all nodes (including descendants).\n    pub fn root_nodes_in_scene(&self, scene_index: usize) -> impl Iterator<Item = &GltfNode> {\n        let scene = self.scenes.get(scene_index);\n        let mut nodes = vec![];\n        if let Some(indices) = scene {\n            for node_index in indices {\n                if let Some(node) = self.nodes.get(*node_index) {\n                    nodes.push(node);\n                }\n            }\n        }\n        nodes.into_iter()\n    }\n\n    /// Returns all nodes in the given scene, recursively including\n    /// children.\n    ///\n    /// Root nodes are visited first, followed by their descendants in\n    /// depth-first order.\n    pub fn recursive_nodes_in_scene(&self, scene_index: usize) -> impl Iterator<Item = &GltfNode> {\n        let scene = self.scenes.get(scene_index);\n        let mut nodes = vec![];\n        if let Some(indices) = scene {\n            for node_index in indices {\n                self.collect_nodes_recursive(*node_index, &mut nodes);\n            }\n        }\n        nodes.into_iter()\n    }\n\n    /// Returns the bounding volume of this document, if possible.\n    ///\n    /// This function will return `None` if this document does not contain\n    /// meshes.\n    pub fn bounding_volume(&self) -> Option<Aabb> {\n        let mut aabbs = vec![];\n        for node in self.nodes.iter() {\n            if let Some(mesh_index) = node.mesh {\n                let mesh = self.meshes.get(mesh_index)?;\n                for prim in mesh.primitives.iter() {\n                    let (prim_min, prim_max) = prim.bounding_box;\n                    let prim_aabb = Aabb::new(prim_min, prim_max);\n                    aabbs.push(prim_aabb);\n                }\n            }\n        }\n        let mut aabbs = aabbs.into_iter();\n        let mut aabb = aabbs.next()?;\n        for next_aabb in aabbs {\n            aabb = Aabb::union(aabb, next_aabb);\n        }\n        Some(aabb)\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use crate::{context::Context, geometry::Vertex, test::BlockOnFuture};\n    use glam::{Vec3, Vec4};\n\n    #[test]\n    fn get_vertex_count_primitive_sanity() {\n        let (document, _, _) =\n            gltf::import(\"../../gltf/gltfTutorial_008_SimpleMeshes.gltf\").unwrap();\n        let prim = document\n            .meshes()\n            .next()\n            .unwrap()\n            .primitives()\n            .next()\n            .unwrap();\n        let vertex_count = super::get_vertex_count(&prim);\n        assert_eq!(3, vertex_count);\n    }\n\n    #[test]\n    // ensures we can\n    // * read simple meshes\n    // * support multiple nodes that reference the same mesh\n    // * support primitives w/ positions and normal attributes\n    // * support transforming nodes (T * R * S)\n    fn stage_gltf_simple_meshes() {\n        let ctx = Context::headless(100, 50).block();\n        let projection = crate::camera::perspective(100.0, 50.0);\n        let position = Vec3::new(1.0, 0.5, 1.5);\n        let view = crate::camera::look_at(position, Vec3::new(1.0, 0.5, 0.0), Vec3::Y);\n        let stage = ctx\n            .new_stage()\n            .with_lighting(false)\n            .with_bloom(false)\n            .with_background_color(Vec3::splat(0.0).extend(1.0));\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n        let _doc = stage\n            .load_gltf_document_from_path(\"../../gltf/gltfTutorial_008_SimpleMeshes.gltf\")\n            .unwrap();\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"gltf/simple_meshes.png\", img);\n    }\n\n    #[test]\n    // Ensures we can read a minimal gltf file with a simple triangle mesh.\n    fn minimal_mesh() {\n        let ctx = Context::headless(20, 20).block();\n        let stage = ctx\n            .new_stage()\n            .with_lighting(false)\n            .with_bloom(false)\n            .with_background_color(Vec3::splat(0.0).extend(1.0));\n\n        let projection = crate::camera::perspective(20.0, 20.0);\n        let eye = Vec3::new(0.5, 0.5, 2.0);\n        let view = crate::camera::look_at(eye, Vec3::new(0.5, 0.5, 0.0), Vec3::Y);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n\n        let _doc = stage\n            .load_gltf_document_from_path(\"../../gltf/gltfTutorial_003_MinimalGltfFile.gltf\")\n            .unwrap();\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"gltf/minimal_mesh.png\", img);\n    }\n\n    #[test]\n    // Tests importing a gltf file and rendering the first image as a 2d object.\n    //\n    // This ensures we are decoding images correctly.\n    fn gltf_images() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx\n            .new_stage()\n            .with_lighting(false)\n            .with_background_color(Vec4::splat(1.0));\n        let (projection, view) = crate::camera::default_ortho2d(100.0, 100.0);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n        let doc = stage\n            .load_gltf_document_from_path(\"../../gltf/cheetah_cone.glb\")\n            .unwrap();\n        assert!(!doc.textures.is_empty());\n        let material = stage\n            .new_material()\n            .with_albedo_texture(&doc.textures[0])\n            .with_has_lighting(false);\n        let _rez = stage\n            .new_primitive()\n            .with_material(&material)\n            .with_vertices(\n                stage.new_vertices([\n                    Vertex::default()\n                        .with_position([0.0, 0.0, 0.0])\n                        .with_uv0([0.0, 0.0]),\n                    Vertex::default()\n                        .with_position([1.0, 0.0, 0.0])\n                        .with_uv0([1.0, 0.0]),\n                    Vertex::default()\n                        .with_position([1.0, 1.0, 0.0])\n                        .with_uv0([1.0, 1.0]),\n                    Vertex::default()\n                        .with_position([0.0, 1.0, 0.0])\n                        .with_uv0([0.0, 1.0]),\n                ]),\n            )\n            .with_indices(stage.new_indices([0u32, 3, 2, 0, 2, 1]))\n            .with_transform(\n                stage\n                    .new_transform()\n                    .with_scale(Vec3::new(100.0, 100.0, 1.0)),\n            );\n        println!(\"material_id: {:#?}\", material.id());\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_linear_image().block().unwrap();\n        img_diff::assert_img_eq(\"gltf/images.png\", img);\n    }\n\n    #[test]\n    fn simple_texture() {\n        let size = 100;\n        let ctx = Context::headless(size, size).block();\n        let stage = ctx\n            .new_stage()\n            .with_background_color(Vec3::splat(0.0).extend(1.0))\n            // There are no lights in the scene and the material isn't marked as \"unlit\", so\n            // let's force it to be unlit.\n            .with_lighting(false)\n            .with_bloom(false);\n        let projection = crate::camera::perspective(size as f32, size as f32);\n        let view =\n            crate::camera::look_at(Vec3::new(0.5, 0.5, 1.25), Vec3::new(0.5, 0.5, 0.0), Vec3::Y);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n\n        let _doc = stage\n            .load_gltf_document_from_path(\"../../gltf/gltfTutorial_013_SimpleTexture.gltf\")\n            .unwrap();\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"gltf/simple_texture.png\", img);\n    }\n\n    #[test]\n    // Demonstrates how to load and render a gltf file containing lighting and a\n    // normal map.\n    fn normal_mapping_brick_sphere() {\n        let ctx = Context::headless(1920, 1080).block();\n        let stage = ctx\n            .new_stage()\n            .with_lighting(true)\n            .with_background_color(Vec4::new(0.01, 0.01, 0.01, 1.0));\n\n        let _doc = stage\n            .load_gltf_document_from_path(\"../../gltf/normal_mapping_brick_sphere.glb\")\n            .unwrap();\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"gltf/normal_mapping_brick_sphere.png\", img);\n    }\n\n    #[test]\n    fn rigged_fox() {\n        let ctx = Context::headless(256, 256).block();\n        let stage = ctx\n            .new_stage()\n            .with_lighting(false)\n            .with_vertex_skinning(false)\n            .with_bloom(false)\n            .with_background_color(Vec3::splat(0.5).extend(1.0));\n\n        let aspect = 256.0 / 256.0;\n        let fovy = core::f32::consts::PI / 4.0;\n        let znear = 0.1;\n        let zfar = 1000.0;\n        let projection = glam::Mat4::perspective_rh(fovy, aspect, znear, zfar);\n        let y = 50.0;\n        let eye = Vec3::new(120.0, y, 120.0);\n        let target = Vec3::new(0.0, y, 0.0);\n        let up = Vec3::Y;\n        let view = glam::Mat4::look_at_rh(eye, target, up);\n\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n        let _doc = stage\n            .load_gltf_document_from_path(\"../../gltf/Fox.glb\")\n            .unwrap();\n\n        // render a frame without vertex skinning as a baseline\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"gltf/skinning/rigged_fox_no_skinning.png\", img);\n\n        // render a frame with vertex skinning to ensure our rigging is correct\n        stage.set_has_vertex_skinning(true);\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq_cfg(\n            \"gltf/skinning/rigged_fox_no_skinning.png\",\n            img,\n            img_diff::DiffCfg {\n                test_name: Some(\"gltf/skinning/rigged_fox\"),\n                ..Default::default()\n            },\n        );\n\n        // let mut animator = doc\n        //     .animations\n        //     .get(0)\n        //     .unwrap()\n        //     .clone()\n        //     .into_animator(doc.nodes.iter().map(|n| (n.index,\n        // n.transform.clone()))); animator.progress(0.0).unwrap();\n        // let frame = ctx.get_next_frame().unwrap();\n        // stage.render(&frame.view());\n        // let img = frame.read_image().unwrap();\n        // img_diff::assert_img_eq_cfg(\n        //     \"gltf/skinning/rigged_fox_no_skinning.png\",\n        //     img,\n        //     img_diff::DiffCfg {\n        //         test_name: Some(\"gltf/skinning/rigged_fox_0\"),\n        //         ..Default::default()\n        //     },\n        // );\n\n        // let slab = futures_lite::future::block_on(stage.read(\n        //     ctx.get_device(),\n        //     ctx.get_queue(),\n        //     Some(\"stage slab\"),\n        //     ..,\n        // ))\n        // .unwrap();\n\n        // assert_eq!(1, doc.skins.len());\n        // let skin = doc.skins[0].skin.get();\n        // for joint_index in 0..skin.joints.len() {\n        //     // skin.get_joint_matrix(, , )\n        // }\n    }\n\n    #[test]\n    fn camera_position_sanity() {\n        // Test that the camera has the expected translation,\n        // taking into account that the gltf files may have been\n        // saved with Y up, or with Z up\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx.new_stage();\n        let doc = stage\n            .load_gltf_document_from_path(\n                crate::test::workspace_dir()\n                    .join(\"gltf\")\n                    .join(\"shadow_mapping_sanity_camera.gltf\"),\n            )\n            .unwrap();\n        let camera_a = doc.cameras.first().unwrap();\n\n        let desc = camera_a.camera.descriptor();\n        const THRESHOLD: f32 = 10e-6;\n        let a = Vec3::new(14.699949, 4.958309, 12.676651);\n        let b = Vec3::new(14.699949, -12.676651, 4.958309);\n        let distance_a = a.distance(desc.position());\n        let distance_b = b.distance(desc.position());\n        if distance_a > THRESHOLD && distance_b > THRESHOLD {\n            println!(\"desc: {desc:#?}\");\n            println!(\"distance_a: {distance_a}\");\n            println!(\"distance_b: {distance_b}\");\n            println!(\"threshold: {THRESHOLD}\");\n            panic!(\"distance greater than threshold\");\n        }\n\n        let doc = stage\n            .load_gltf_document_from_path(\n                crate::test::workspace_dir()\n                    .join(\"gltf\")\n                    .join(\"shadow_mapping_sanity.gltf\"),\n            )\n            .unwrap();\n        let camera_b = doc.cameras.first().unwrap();\n\n        let eq = |a: Vec3, b: Vec3| {\n            let c = Vec3::new(b.x, -b.z, b.y);\n            println!(\"a: {a}\");\n            println!(\"b: {b}\");\n            println!(\"c: {c}\");\n            a.distance(b) <= 10e-6 || c.distance(c) <= 10e-6\n        };\n        assert!(eq(\n            camera_a.camera.descriptor().position(),\n            camera_b.camera.descriptor().position()\n        ));\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/internal/cpu.rs",
    "content": "//! Internal CPU utilities and stuff.\nuse std::sync::Arc;\n\nuse snafu::{OptionExt, ResultExt};\n\nuse crate::context::{\n    CannotCreateAdaptorSnafu, CannotRequestDeviceSnafu, ContextError, IncompatibleSurfaceSnafu,\n    RenderTarget, RenderTargetInner,\n};\n\n/// Create a new [`wgpu::Adapter`].\npub async fn adapter(\n    instance: &wgpu::Instance,\n    compatible_surface: Option<&wgpu::Surface<'_>>,\n) -> Result<wgpu::Adapter, ContextError> {\n    log::trace!(\n        \"creating adapter for a {} context\",\n        if compatible_surface.is_none() {\n            \"headless\"\n        } else {\n            \"surface-based\"\n        }\n    );\n    let adapter = instance\n        .request_adapter(&wgpu::RequestAdapterOptions {\n            power_preference: wgpu::PowerPreference::default(),\n            compatible_surface,\n            force_fallback_adapter: false,\n        })\n        .await\n        .context(CannotCreateAdaptorSnafu)?;\n\n    log::info!(\"Adapter selected: {:?}\", adapter.get_info());\n    let info = adapter.get_info();\n    log::info!(\n        \"using adapter: '{}' backend:{:?} driver:'{}'\",\n        info.name,\n        info.backend,\n        info.driver\n    );\n    Ok(adapter)\n}\n\n/// Create a new [`wgpu::Device`].\npub async fn device(\n    adapter: &wgpu::Adapter,\n) -> Result<(wgpu::Device, wgpu::Queue), wgpu::RequestDeviceError> {\n    let wanted_features = wgpu::Features::INDIRECT_FIRST_INSTANCE\n        | wgpu::Features::MULTI_DRAW_INDIRECT\n        //// when debugging rust-gpu shader miscompilation it's nice to have this\n        //| wgpu::Features::SPIRV_SHADER_PASSTHROUGH\n        // this one is a funny requirement, it seems it is needed if using storage buffers in\n        // vertex shaders, even if those shaders are read-only\n        | wgpu::Features::VERTEX_WRITABLE_STORAGE\n        | wgpu::Features::CLEAR_TEXTURE;\n    let supported_features = adapter.features();\n    let required_features = wanted_features.intersection(supported_features);\n    let unsupported_features = wanted_features.difference(supported_features);\n    if !unsupported_features.is_empty() {\n        log::error!(\"requested but unsupported features: {unsupported_features:#?}\");\n        log::warn!(\"requested and supported features: {supported_features:#?}\");\n    }\n    let limits = adapter.limits();\n    log::info!(\"adapter limits: {limits:#?}\");\n    adapter\n        .request_device(&wgpu::DeviceDescriptor {\n            required_features,\n            required_limits: adapter.limits(),\n            label: None,\n            memory_hints: wgpu::MemoryHints::default(),\n            trace: wgpu::Trace::Off,\n        })\n        .await\n}\n\n/// Create a new instance.\n///\n/// This is for internal use. It is not necessary to create your own `wgpu`\n/// instance to use this library.\npub fn new_instance(backends: Option<wgpu::Backends>) -> wgpu::Instance {\n    log::info!(\n        \"creating instance - available backends: {:#?}\",\n        wgpu::Instance::enabled_backend_features()\n    );\n    // BackendBit::PRIMARY => Vulkan + Metal + DX12 + Browser WebGPU\n    let backends = backends.unwrap_or(wgpu::Backends::PRIMARY);\n    let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {\n        backends,\n        ..Default::default()\n    });\n\n    #[cfg(not(target_arch = \"wasm32\"))]\n    {\n        let adapters = instance.enumerate_adapters(backends);\n        log::trace!(\"available adapters: {adapters:#?}\");\n    }\n\n    instance\n}\n\n/// Create a new suite of `wgpu` machinery using a window or canvas.\n///\n/// ## Note\n/// This function is used internally.\npub async fn new_windowed_adapter_device_queue(\n    width: u32,\n    height: u32,\n    instance: &wgpu::Instance,\n    window: impl Into<wgpu::SurfaceTarget<'static>>,\n) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, RenderTarget), ContextError> {\n    let surface = instance\n        .create_surface(window)\n        .map_err(|e| ContextError::CreateSurface { source: e })?;\n    let adapter = adapter(instance, Some(&surface)).await?;\n    let surface_caps = surface.get_capabilities(&adapter);\n    let fmt = if surface_caps\n        .formats\n        .contains(&wgpu::TextureFormat::Rgba8UnormSrgb)\n    {\n        wgpu::TextureFormat::Rgba8UnormSrgb\n    } else {\n        surface_caps\n            .formats\n            .iter()\n            .copied()\n            .find(|f| f.is_srgb())\n            .unwrap_or(surface_caps.formats[0])\n    };\n    let view_fmts = if fmt.is_srgb() {\n        vec![]\n    } else {\n        vec![fmt.add_srgb_suffix()]\n    };\n    log::info!(\"surface capabilities: {surface_caps:#?}\");\n    let mut surface_config = surface\n        .get_default_config(&adapter, width, height)\n        .context(IncompatibleSurfaceSnafu)?;\n    surface_config.view_formats = view_fmts;\n    let (device, queue) = device(&adapter).await.context(CannotRequestDeviceSnafu)?;\n    surface.configure(&device, &surface_config);\n    let target = RenderTarget(RenderTargetInner::Surface {\n        surface,\n        surface_config,\n    });\n    Ok((adapter, device, queue, target))\n}\n\n/// Create a new suite of `wgpu` machinery that renders to a texture.\n///\n/// ## Note\n/// This function is used internally.\npub async fn new_headless_device_queue_and_target(\n    width: u32,\n    height: u32,\n    instance: &wgpu::Instance,\n) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, RenderTarget), ContextError> {\n    let adapter = adapter(instance, None).await?;\n    let texture_desc = wgpu::TextureDescriptor {\n        size: wgpu::Extent3d {\n            width,\n            height,\n            depth_or_array_layers: 1,\n        },\n        mip_level_count: 1,\n        sample_count: 1,\n        dimension: wgpu::TextureDimension::D2,\n        format: wgpu::TextureFormat::Rgba8UnormSrgb,\n        usage: wgpu::TextureUsages::COPY_SRC\n            | wgpu::TextureUsages::RENDER_ATTACHMENT\n            | wgpu::TextureUsages::TEXTURE_BINDING,\n        label: None,\n        view_formats: &[],\n    };\n    let (device, queue) = device(&adapter).await.context(CannotRequestDeviceSnafu)?;\n    let texture = Arc::new(device.create_texture(&texture_desc));\n    let target = RenderTarget(RenderTargetInner::Texture { texture });\n    Ok((adapter, device, queue, target))\n}\n"
  },
  {
    "path": "crates/renderling/src/internal.rs",
    "content": "//! Internal types and functions.\n//!\n//! ## Note\n//! The types and functions exposed by this module are used internally, and\n//! are _not_ required to be used by users of this library.\n//!\n//! They are public here because they are needed for integration tests, and\n//! on the off-chance that somebody wants to build something with them.\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n"
  },
  {
    "path": "crates/renderling/src/lib.rs",
    "content": "//! <div style=\"float: right; padding: 1em;\">\n//!    <img\n//!       style=\"image-rendering: pixelated; image-rendering: -moz-crisp-edges;\n//! image-rendering: crisp-edges;\"       alt=\"renderling mascot\" width=\"180\"\n//!       src=\"https://github.com/user-attachments/assets/83eafc47-287c-4b5b-8fd7-2063e56b2338\"\n//!    />\n//! </div>\n//!\n//! `renderling` is a \"GPU driven\" renderer with a focus on simplicity and ease\n//! of use, targeting WebGPU.\n//!\n//! Shaders are written in Rust using [`rust-gpu`](https://rust-gpu.github.io/).\n//!\n//! ## Hello triangle\n//!\n//! Here we'll run through the classic \"hello triangle\", which will\n//! display a colored triangle.\n//!\n//! ### Context creation\n//!\n//! First you must create a [`Context`].\n//! The `Context` holds the render target - either a native window, an HTML\n//! canvas or a texture.\n//!\n//! ```\n//! use renderling::{context::Context, geometry::Vertex, stage::Stage};\n//!\n//! // create a headless context with dimensions 100, 100.\n//! let ctx = futures_lite::future::block_on(Context::headless(100, 100));\n//! ```\n//!\n//! [`Context::headless`] creates a `Context` that renders to a texture.\n//!\n//! [`Context::from_winit_window`] creates a `Context` that renders to a native\n//! window.\n//!\n//! [`Context::try_new_with_surface`] creates a `Context` that renders to any\n//! [`wgpu::SurfaceTarget`].\n//!\n//! See the [`renderling::context`](context) module documentation for\n//! more info.\n//!\n//! ### Staging resources\n//!\n//! We then create a \"stage\" to place the camera, geometry, materials and\n//! lights.\n//!\n//! ```\n//! # use renderling::{context::Context, stage::Stage};\n//! # let ctx = futures_lite::future::block_on(Context::headless(100, 100));\n//! let stage: Stage = ctx\n//!     .new_stage()\n//!     .with_background_color([1.0, 1.0, 1.0, 1.0])\n//!     // For this demo we won't use lighting\n//!     .with_lighting(false);\n//! ```\n//!\n//! The [`Stage`] is neat in that it allows you to \"stage\" data\n//! directly onto the GPU. Those values can be modified on the CPU and\n//! synchronization will happen during [`Stage::render`].\n//!\n//! Use one of the many `Stage::new_*` functions to stage data on the GPU:\n//! * [`Stage::new_camera`]\n//! * [`Stage::new_vertices`]\n//! * [`Stage::new_indices`]\n//! * [`Stage::new_material`]\n//! * [`Stage::new_primitive`]\n//! * ...and more\n//!\n//! In order to render, we need to \"stage\" a\n//! [`Primitive`], which is a bundle of rendering\n//! resources, roughly representing a singular mesh.\n//!\n//! But first we'll need a list of [`Vertex`] organized\n//! as triangles with counter-clockwise winding. Here we'll use the builder\n//! pattern to create a staged [`Primitive`] using our vertices.\n//!\n//! We'll also create a [`Camera`] so we can see the stage.\n//!\n//! ```\n//! # use renderling::{context::Context, geometry::Vertex, stage::Stage};\n//! # let ctx = futures_lite::future::block_on(Context::headless(100, 100));\n//! # let stage: Stage = ctx.new_stage();\n//! let vertices = stage.new_vertices([\n//!     Vertex::default()\n//!         .with_position([0.0, 0.0, 0.0])\n//!         .with_color([0.0, 1.0, 1.0, 1.0]),\n//!     Vertex::default()\n//!         .with_position([0.0, 100.0, 0.0])\n//!         .with_color([1.0, 1.0, 0.0, 1.0]),\n//!     Vertex::default()\n//!         .with_position([100.0, 0.0, 0.0])\n//!         .with_color([1.0, 0.0, 1.0, 1.0]),\n//! ]);\n//! let triangle_prim = stage.new_primitive().with_vertices(vertices);\n//!\n//! let camera = stage.new_camera().with_default_ortho2d(100.0, 100.0);\n//! ```\n//!\n//! ### Rendering\n//!\n//! Finally, we get the next frame from the context with\n//! [`Context::get_next_frame`]. Then we render to it using [`Stage::render`]\n//! and then present the frame with [`Frame::present`].\n//!\n//! ```\n//! # use renderling::{context::Context, geometry::Vertex, stage::Stage};\n//! # let ctx = futures_lite::future::block_on(Context::headless(100, 100));\n//! # let stage = ctx.new_stage();\n//! # let camera = stage.new_camera().with_default_ortho2d(100.0, 100.0);\n//! # let vertices = stage.new_vertices([\n//! #     Vertex::default()\n//! #         .with_position([0.0, 0.0, 0.0])\n//! #         .with_color([0.0, 1.0, 1.0, 1.0]),\n//! #     Vertex::default()\n//! #         .with_position([0.0, 100.0, 0.0])\n//! #         .with_color([1.0, 1.0, 0.0, 1.0]),\n//! #     Vertex::default()\n//! #         .with_position([100.0, 0.0, 0.0])\n//! #         .with_color([1.0, 0.0, 1.0, 1.0]),\n//! #     ]);\n//! # let triangle_prim = stage\n//! #     .new_primitive()\n//! #     .with_vertices(vertices);\n//! let frame = ctx.get_next_frame().unwrap();\n//! stage.render(&frame.view());\n//! let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n//! frame.present();\n//! ```\n//!\n//! Here for our purposes we also read the rendered frame as an image.\n//! Saving `img` should give us this:\n//!\n//! ![renderling hello triangle](https://github.com/schell/renderling/blob/main/test_img/cmy_triangle/hdr.png?raw=true)\n//!\n//! ### Modifying resources\n//!\n//! Later, if we want to modify any of the staged values, we can do so through\n//! each resource's struct, using `set_*`, `modify_*` and `with_*` functions.\n//!\n//! The changes made will be synchronized to the GPU at the beginning of the\n//! next [`Stage::render`] function.\n//!\n//! ### Removing and hiding primitives\n//!\n//! To remove primitives from the stage, use [`Stage::remove_primitive`].\n//! This will remove the primitive from rendering entirely, but the GPU\n//! resources will not be released until all clones have been dropped.\n//!\n//! If you just want to mark a [`Primitive`] invisible, use\n//! [`Primitive::set_visible`].\n//!\n//! ### Releasing resources\n//!\n//! GPU resources are automatically released when all clones are dropped.\n//! The data they occupy on the GPU is reclaimed during calls to\n//! [`Stage::render`].\n//! If you would like to manually reclaim the resources of fully dropped\n//! resources without rendering, you can do so with\n//! [`Stage::commit`].\n//!\n//! #### Ensuring resources are released\n//!\n//! Keep in mind that many resource functions (like [`Primitive::set_material`]\n//! for example) take another resource as a parameter. In these functions the\n//! parameter resource is cloned and held internally. This is done to keep\n//! resources that are in use from being released. Therefore if you want a\n//! resource to be released, you must ensure that all references to it are\n//! removed. You can use the `remove_*` functions on many resources for this\n//! purpose, like [`Primitive::remove_material`], for example, which would\n//! remove the material from the primitive. After that call, if no other\n//! primitives are using that material and the material is dropped from\n//! user code, the next call to [`Stage::render`] or [`Stage::commit`] will\n//! reclaim the GPU resources of the material to be re-used.\n//!\n//! Other resources like [`Vertices`], [`Indices`], [`Transform`],\n//! [`NestedTransform`] and others can simply be dropped.\n//!\n//! # Next steps\n//!\n//! For further introduction to what renderling can do, take a tour of the\n//! [`Stage`] type, or get started with [the manual](#todo).\n//!\n//! # WARNING\n//!\n//! This is very much a work in progress.\n//!\n//! Your mileage may vary, but I hope you get good use out of this library.\n//!\n//! PRs, criticisms and ideas are all very much welcomed [at the\n//! repo](https://github.com/schell/renderling).\n//!\n//! 😀☕\n#![cfg_attr(gpu, no_std)]\n#![deny(clippy::disallowed_methods)]\n\n#[cfg(doc)]\nuse crate::{camera::Camera, geometry::*, primitive::Primitive, stage::Stage, transform::*};\n\npub mod atlas;\n#[cfg(cpu)]\npub(crate) mod bindgroup;\npub mod bloom;\npub mod bvol;\npub mod camera;\npub mod color;\npub mod compositor;\n#[cfg(cpu)]\npub mod context;\npub mod convolution;\npub mod cubemap;\npub mod cull;\npub mod debug;\npub mod draw;\npub mod geometry;\n#[cfg(all(cpu, gltf))]\npub mod gltf;\n#[cfg(cpu)]\npub mod internal;\npub mod light;\n#[cfg(cpu)]\npub mod linkage;\npub mod material;\npub mod math;\npub mod pbr;\npub mod primitive;\npub mod sdf;\npub mod skybox;\npub mod stage;\npub mod sync;\n#[cfg(cpu)]\npub mod texture;\npub mod tonemapping;\npub mod transform;\npub mod tutorial;\n#[cfg(cpu)]\npub mod types;\npub mod ui_slab;\n\npub extern crate glam;\n\n// TODO: document the crate's feature flags here.\n// Similar to [ndarray](https://docs.rs/ndarray/latest/ndarray/#crate-feature-flags).\n\n#[macro_export]\n/// A wrapper around `std::println` that is a noop on the GPU.\nmacro_rules! println {\n    ($($arg:tt)*) => {\n        #[cfg(cpu)]\n        {\n            std::println!($($arg)*);\n        }\n    }\n}\n\n#[cfg(all(cpu, any(test, feature = \"test-utils\")))]\n#[allow(unused, reason = \"Used in debugging on macos\")]\npub fn capture_gpu_frame<T>(\n    ctx: &crate::context::Context,\n    path: impl AsRef<std::path::Path>,\n    f: impl FnOnce() -> T,\n) -> T {\n    let path = path.as_ref();\n    let parent = path.parent().unwrap();\n    std::fs::create_dir_all(parent).unwrap();\n\n    #[cfg(target_os = \"macos\")]\n    {\n        if path.exists() {\n            log::info!(\n                \"deleting {} before writing gpu frame capture\",\n                path.display()\n            );\n            std::fs::remove_dir_all(path).unwrap();\n        }\n\n        if std::env::var(\"METAL_CAPTURE_ENABLED\").is_err() {\n            log::error!(\"Env var METAL_CAPTURE_ENABLED must be set\");\n            panic!(\"missing METAL_CAPTURE_ENABLED=1\");\n        }\n\n        let m = metal::CaptureManager::shared();\n        let desc = metal::CaptureDescriptor::new();\n\n        desc.set_destination(metal::MTLCaptureDestination::GpuTraceDocument);\n        desc.set_output_url(path);\n        let maybe_metal_device = unsafe { ctx.get_device().as_hal::<wgpu_core::api::Metal>() };\n        if let Some(metal_device) = maybe_metal_device {\n            desc.set_capture_device(metal_device.raw_device().try_lock().unwrap().as_ref());\n        } else {\n            panic!(\"not a capturable device\")\n        }\n        m.start_capture(&desc).unwrap();\n        let t = f();\n        m.stop_capture();\n        t\n    }\n    #[cfg(not(target_os = \"macos\"))]\n    {\n        log::warn!(\"capturing a GPU frame is only supported on macos\");\n        f()\n    }\n}\n\n#[cfg(all(cpu, any(test, feature = \"test-utils\")))]\n#[allow(unused, reason = \"Used in sync tests in userland\")]\n/// Marker trait to block on futures in synchronous code.\n///\n/// This is a simple convenience.\n/// Many of the tests in this crate render something and then read a\n/// texture in order to perform a diff on the result using a known image.\n/// Since reading from the GPU is async, this trait helps cut down\n/// boilerplate.\npub trait BlockOnFuture {\n    type Output;\n\n    /// Block on the future using [`futures_util::future::block_on`].\n    fn block(self) -> Self::Output;\n}\n\n#[cfg(all(cpu, any(test, feature = \"test-utils\")))]\nimpl<T: std::future::Future> BlockOnFuture for T {\n    type Output = <Self as std::future::Future>::Output;\n\n    fn block(self) -> Self::Output {\n        futures_lite::future::block_on(self)\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use crate::{atlas::AtlasImage, context::Context, geometry::Vertex, light::Lux};\n\n    use glam::{Mat3, Mat4, Quat, UVec2, Vec2, Vec3, Vec4};\n    use img_diff::DiffCfg;\n    use light::AnalyticalLight;\n    use pretty_assertions::assert_eq;\n    use stage::Stage;\n\n    #[allow(unused_imports)]\n    pub use renderling_build::{test_output_dir, workspace_dir};\n\n    pub use super::BlockOnFuture;\n\n    #[cfg_attr(not(target_arch = \"wasm32\"), ctor::ctor)]\n    fn init_logging() {\n        let _ = env_logger::builder().is_test(true).try_init();\n        log::info!(\"logging is on\");\n    }\n\n    #[allow(unused, reason = \"Used in debugging on macos\")]\n    pub fn capture_gpu_frame<T>(\n        ctx: &Context,\n        path: impl AsRef<std::path::Path>,\n        f: impl FnOnce() -> T,\n    ) -> T {\n        let path = workspace_dir().join(\"test_output\").join(path);\n        super::capture_gpu_frame(ctx, path, f)\n    }\n\n    pub fn make_two_directional_light_setup(stage: &Stage) -> (AnalyticalLight, AnalyticalLight) {\n        let sunlight_a = stage\n            .new_directional_light()\n            .with_direction(Vec3::new(-0.8, -1.0, 0.5).normalize())\n            .with_color(Vec4::ONE)\n            .with_intensity(Lux::OUTDOOR_DIRECT_SUNLIGHT_HIGH);\n        let sunlight_b = stage\n            .new_directional_light()\n            .with_direction(Vec3::new(1.0, 1.0, -0.1).normalize())\n            .with_color(Vec4::ONE)\n            .with_intensity(Lux::OUTDOOR_FOXS_WEDDING);\n        (sunlight_a.into_generic(), sunlight_b.into_generic())\n    }\n\n    #[test]\n    fn sanity_transmute() {\n        let zerof32 = 0f32;\n        let zerof32asu32: u32 = zerof32.to_bits();\n        assert_eq!(0, zerof32asu32);\n\n        let foure_45 = 4e-45f32;\n        let in_u32: u32 = foure_45.to_bits();\n        assert_eq!(3, in_u32);\n\n        let u32max = u32::MAX;\n        let f32nan: f32 = f32::from_bits(u32max);\n        assert!(f32nan.is_nan());\n\n        let u32max: u32 = f32nan.to_bits();\n        assert_eq!(u32::MAX, u32max);\n    }\n\n    pub fn right_tri_vertices() -> Vec<Vertex> {\n        vec![\n            Vertex::default()\n                .with_position([0.0, 0.0, 0.0])\n                .with_color([0.0, 1.0, 1.0, 1.0]),\n            Vertex::default()\n                .with_position([0.0, 100.0, 0.0])\n                .with_color([1.0, 1.0, 0.0, 1.0]),\n            Vertex::default()\n                .with_position([100.0, 0.0, 0.0])\n                .with_color([1.0, 0.0, 1.0, 1.0]),\n        ]\n    }\n\n    #[test]\n    // This tests our ability to draw a CMYK triangle in the top left corner.\n    fn cmy_triangle_sanity() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));\n        let (p, v) = crate::camera::default_ortho2d(100.0, 100.0);\n        let _camera = stage.new_camera().with_projection_and_view(p, v);\n\n        let _prim = stage\n            .new_primitive()\n            .with_vertices(stage.new_vertices(right_tri_vertices()));\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        frame.present();\n\n        let depth_texture = stage.get_depth_texture();\n        let depth_img = depth_texture.read_image().block().unwrap().unwrap();\n        img_diff::assert_img_eq(\"cmy_triangle/depth.png\", depth_img);\n\n        let hdr_img = stage\n            .hdr_texture\n            .read()\n            .unwrap()\n            .read_hdr_image(&ctx)\n            .block()\n            .unwrap();\n        img_diff::assert_img_eq(\"cmy_triangle/hdr.png\", hdr_img);\n\n        let bloom_mix = stage\n            .bloom\n            .get_mix_texture()\n            .read_hdr_image(&ctx)\n            .block()\n            .unwrap();\n        img_diff::assert_img_eq(\"cmy_triangle/bloom_mix.png\", bloom_mix);\n    }\n\n    #[test]\n    // This tests our ability to draw a CMYK triangle in the top left corner, using\n    // CW geometry.\n    fn cmy_triangle_backface() {\n        use img_diff::DiffCfg;\n\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));\n        let (p, v) = crate::camera::default_ortho2d(100.0, 100.0);\n        let _camera = stage.new_camera().with_projection_and_view(p, v);\n        let _rez = stage.new_primitive().with_vertices(stage.new_vertices({\n            let mut vs = right_tri_vertices();\n            vs.reverse();\n            vs\n        }));\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_linear_image().block().unwrap();\n        img_diff::assert_img_eq_cfg(\n            \"cmy_triangle/hdr.png\",\n            img,\n            DiffCfg {\n                test_name: Some(\"cmy_triangle_backface.png\"),\n                ..Default::default()\n            },\n        );\n    }\n\n    #[test]\n    // This tests our ability to update the transform of a `Renderlet` after it\n    // has already been sent to the GPU.\n    // We do this by writing over the previous transform in the stage.\n    fn cmy_triangle_update_transform() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));\n        let (p, v) = crate::camera::default_ortho2d(100.0, 100.0);\n        let _camera = stage.new_camera().with_projection_and_view(p, v);\n        let transform = stage.new_transform();\n        let _renderlet = stage\n            .new_primitive()\n            .with_vertices(stage.new_vertices(right_tri_vertices()))\n            .with_transform(&transform);\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n\n        transform\n            .set_translation(Vec3::new(100.0, 0.0, 0.0))\n            .set_rotation(Quat::from_axis_angle(Vec3::Z, std::f32::consts::FRAC_PI_2))\n            .set_scale(Vec3::new(0.5, 0.5, 1.0));\n\n        stage.render(&frame.view());\n        let img = frame.read_linear_image().block().unwrap();\n        img_diff::assert_img_eq(\"cmy_triangle/update_transform.png\", img);\n    }\n\n    /// Points around a pyramid height=1 with the base around the origin.\n    ///\n    ///    yb\n    ///    |               *top\n    ///    |___x       tl_____tr\n    ///   /    g        /    /\n    /// z/r          bl/____/br\n    fn pyramid_points() -> [Vec3; 5] {\n        let tl = Vec3::new(-0.5, -0.5, -0.5);\n        let tr = Vec3::new(0.5, -0.5, -0.5);\n        let br = Vec3::new(0.5, -0.5, 0.5);\n        let bl = Vec3::new(-0.5, -0.5, 0.5);\n        let top = Vec3::new(0.0, 0.5, 0.0);\n        [tl, tr, br, bl, top]\n    }\n\n    fn pyramid_indices() -> [u16; 18] {\n        let (tl, tr, br, bl, top) = (0, 1, 2, 3, 4);\n        [\n            tl, br, bl, tl, tr, br, br, top, bl, bl, top, tl, tl, top, tr, tr, top, br,\n        ]\n    }\n\n    fn cmy_gpu_vertex(p: Vec3) -> Vertex {\n        let r: f32 = p.z + 0.5;\n        let g: f32 = p.x + 0.5;\n        let b: f32 = p.y + 0.5;\n        Vertex::default()\n            .with_position([p.x.min(1.0), p.y.min(1.0), p.z.min(1.0)])\n            .with_color([r, g, b, 1.0])\n    }\n\n    pub fn gpu_cube_vertices() -> Vec<Vertex> {\n        math::UNIT_INDICES\n            .iter()\n            .map(|i| cmy_gpu_vertex(math::UNIT_POINTS[*i]))\n            .collect()\n    }\n\n    #[test]\n    // Tests our ability to draw a CMYK cube.\n    fn cmy_cube_sanity() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));\n        let camera_position = Vec3::new(0.0, 12.0, 20.0);\n        let _camera = stage.new_camera().with_projection_and_view(\n            Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0),\n            Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y),\n        );\n        let _rez = stage\n            .new_primitive()\n            .with_vertices(stage.new_vertices(gpu_cube_vertices()))\n            .with_transform(\n                stage\n                    .new_transform()\n                    .with_scale(Vec3::new(6.0, 6.0, 6.0))\n                    .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),\n            );\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"cmy_cube/sanity.png\", img);\n    }\n\n    #[test]\n    // Tests our ability to draw a CMYK cube using indexed geometry.\n    fn cmy_cube_indices() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));\n        let camera_position = Vec3::new(0.0, 12.0, 20.0);\n        let _camera = stage.new_camera().with_projection_and_view(\n            Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0),\n            Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y),\n        );\n\n        let _rez = stage\n            .new_primitive()\n            .with_vertices(stage.new_vertices(math::UNIT_POINTS.map(cmy_gpu_vertex)))\n            .with_indices(stage.new_indices(math::UNIT_INDICES.map(|i| i as u32)))\n            .with_transform(\n                stage\n                    .new_transform()\n                    .with_scale(Vec3::new(6.0, 6.0, 6.0))\n                    .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),\n            );\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq_cfg(\n            \"cmy_cube/sanity.png\",\n            img,\n            DiffCfg {\n                test_name: Some(\"cmy_cube/indices\"),\n                ..Default::default()\n            },\n        );\n    }\n\n    #[test]\n    // Test our ability to create two cubes and toggle the visibility of one of\n    // them.\n    fn cmy_cube_visible() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));\n        let (projection, view) = camera::default_perspective(100.0, 100.0);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n        let geometry = stage.new_vertices(gpu_cube_vertices());\n        let _cube_one = stage\n            .new_primitive()\n            .with_vertices(&geometry)\n            .with_transform(\n                stage\n                    .new_transform()\n                    .with_translation(Vec3::new(-4.5, 0.0, 0.0))\n                    .with_scale(Vec3::new(6.0, 6.0, 6.0))\n                    .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),\n            );\n\n        let cube_two = stage\n            .new_primitive()\n            .with_vertices(&geometry)\n            .with_transform(\n                stage\n                    .new_transform()\n                    .with_translation(Vec3::new(4.5, 0.0, 0.0))\n                    .with_scale(Vec3::new(6.0, 6.0, 6.0))\n                    .with_rotation(Quat::from_axis_angle(Vec3::Y, std::f32::consts::FRAC_PI_4)),\n            );\n\n        // we should see two colored cubes\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"cmy_cube/visible_before.png\", img.clone());\n        let img_before = img;\n        frame.present();\n\n        // update cube two making it invisible\n        cube_two.set_visible(false);\n\n        // we should see only one colored cube\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"cmy_cube/visible_after.png\", img);\n        frame.present();\n\n        // update cube two making in visible again\n        cube_two.set_visible(true);\n\n        // we should see two colored cubes again\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_eq(\"cmy_cube/visible_before_again.png\", img_before, img);\n    }\n\n    #[test]\n    // Tests the ability to specify indexed vertices, as well as the ability to\n    // update a field within a struct stored on the slab by using a `Hybrid`.\n    fn cmy_cube_remesh() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx\n            .new_stage()\n            .with_lighting(false)\n            .with_background_color(Vec4::splat(1.0));\n        let (projection, view) = camera::default_perspective(100.0, 100.0);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n        let cube = stage\n            .new_primitive()\n            .with_vertices(\n                stage\n                    .new_vertices(math::UNIT_INDICES.map(|i| cmy_gpu_vertex(math::UNIT_POINTS[i]))),\n            )\n            .with_transform(\n                stage\n                    .new_transform()\n                    .with_scale(Vec3::new(10.0, 10.0, 10.0)),\n            );\n\n        // we should see a cube (in sRGB color space)\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"cmy_cube/remesh_before.png\", img);\n        frame.present();\n\n        // Update the cube mesh to a pyramid by overwriting the `.vertices` field\n        // of `Renderlet`\n        let pyramid_points = pyramid_points();\n        let pyramid_geometry = stage\n            .new_vertices(pyramid_indices().map(|i| cmy_gpu_vertex(pyramid_points[i as usize])));\n        cube.set_vertices(pyramid_geometry);\n\n        // we should see a pyramid (in sRGB color space)\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"cmy_cube/remesh_after.png\", img);\n    }\n\n    fn gpu_uv_unit_cube() -> Vec<Vertex> {\n        let p: [Vec3; 8] = math::UNIT_POINTS;\n        let tl = Vec2::new(0.0, 0.0);\n        let tr = Vec2::new(1.0, 0.0);\n        let bl = Vec2::new(0.0, 1.0);\n        let br = Vec2::new(1.0, 1.0);\n\n        vec![\n            // top\n            Vertex::default().with_position(p[0]).with_uv0(bl),\n            Vertex::default().with_position(p[2]).with_uv0(tr),\n            Vertex::default().with_position(p[1]).with_uv0(tl),\n            Vertex::default().with_position(p[0]).with_uv0(bl),\n            Vertex::default().with_position(p[3]).with_uv0(br),\n            Vertex::default().with_position(p[2]).with_uv0(tr),\n            // bottom\n            Vertex::default().with_position(p[4]).with_uv0(bl),\n            Vertex::default().with_position(p[6]).with_uv0(tr),\n            Vertex::default().with_position(p[5]).with_uv0(tl),\n            Vertex::default().with_position(p[4]).with_uv0(bl),\n            Vertex::default().with_position(p[7]).with_uv0(br),\n            Vertex::default().with_position(p[6]).with_uv0(tr),\n            // left\n            Vertex::default().with_position(p[7]).with_uv0(bl),\n            Vertex::default().with_position(p[0]).with_uv0(tr),\n            Vertex::default().with_position(p[1]).with_uv0(tl),\n            Vertex::default().with_position(p[7]).with_uv0(bl),\n            Vertex::default().with_position(p[4]).with_uv0(br),\n            Vertex::default().with_position(p[0]).with_uv0(tr),\n            // right\n            Vertex::default().with_position(p[5]).with_uv0(bl),\n            Vertex::default().with_position(p[2]).with_uv0(tr),\n            Vertex::default().with_position(p[3]).with_uv0(tl),\n            Vertex::default().with_position(p[5]).with_uv0(bl),\n            Vertex::default().with_position(p[6]).with_uv0(br),\n            Vertex::default().with_position(p[2]).with_uv0(tr),\n            // front\n            Vertex::default().with_position(p[4]).with_uv0(bl),\n            Vertex::default().with_position(p[3]).with_uv0(tr),\n            Vertex::default().with_position(p[0]).with_uv0(tl),\n            Vertex::default().with_position(p[4]).with_uv0(bl),\n            Vertex::default().with_position(p[5]).with_uv0(br),\n            Vertex::default().with_position(p[3]).with_uv0(tr),\n        ]\n    }\n\n    #[test]\n    // Tests that updating the material actually updates the rendering of an unlit\n    // mesh\n    fn unlit_textured_cube_material() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx.new_stage().with_background_color(Vec4::splat(0.0));\n        let (projection, view) = camera::default_perspective(100.0, 100.0);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n\n        let sandstone = AtlasImage::from(image::open(\"../../img/sandstone.png\").unwrap());\n        let dirt = AtlasImage::from(image::open(\"../../img/dirt.jpg\").unwrap());\n        let entries = stage.set_images([sandstone, dirt]).unwrap();\n\n        let material = stage\n            .new_material()\n            .with_albedo_texture(&entries[0])\n            .with_has_lighting(false);\n        let cube = stage\n            .new_primitive()\n            .with_vertices(stage.new_vertices(gpu_uv_unit_cube()))\n            .with_transform(\n                stage\n                    .new_transform()\n                    .with_scale(Vec3::new(10.0, 10.0, 10.0)),\n            )\n            .with_material(&material);\n        println!(\"cube: {:?}\", cube.descriptor());\n\n        // we should see a cube with a stoney texture\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"unlit_textured_cube_material_before.png\", img);\n        frame.present();\n\n        // update the material's texture on the GPU\n        material.set_albedo_texture(&entries[1]);\n\n        // we should see a cube with a dirty texture\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"unlit_textured_cube_material_after.png\", img);\n\n        // let size = stage.atlas.get_size();\n        // for i in 0..size.depth_or_array_layers {\n        //     let atlas_img = stage.atlas.atlas_img(&ctx, i);\n        //     img_diff::save(\n        //         &format!(\"unlit_texture_cube_atlas_layer_{i}.png\"),\n        //         atlas_img,\n        //     );\n        // }\n    }\n\n    #[test]\n    // Ensures that we can render multiple nodes with mesh primitives\n    // that share the same geometry, but have different materials.\n    fn multi_node_scene() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx\n            .new_stage()\n            .with_background_color(Vec3::splat(0.0).extend(1.0));\n\n        let (projection, view) = camera::default_ortho2d(100.0, 100.0);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n\n        // now test the textures functionality\n        let img = AtlasImage::from_path(\"../../img/cheetah.jpg\").unwrap();\n        let entries = stage.set_images([img]).unwrap();\n\n        let geometry = stage.new_vertices([\n            Vertex {\n                position: Vec3::new(0.0, 0.0, 0.0),\n                color: Vec4::new(1.0, 1.0, 0.0, 1.0),\n                uv0: Vec2::new(0.0, 0.0),\n                uv1: Vec2::new(0.0, 0.0),\n                ..Default::default()\n            },\n            Vertex {\n                position: Vec3::new(100.0, 100.0, 0.0),\n                color: Vec4::new(0.0, 1.0, 1.0, 1.0),\n                uv0: Vec2::new(1.0, 1.0),\n                uv1: Vec2::new(1.0, 1.0),\n                ..Default::default()\n            },\n            Vertex {\n                position: Vec3::new(100.0, 0.0, 0.0),\n                color: Vec4::new(1.0, 0.0, 1.0, 1.0),\n                uv0: Vec2::new(1.0, 0.0),\n                uv1: Vec2::new(1.0, 0.0),\n                ..Default::default()\n            },\n        ]);\n        let _color_prim = stage.new_primitive().with_vertices(&geometry);\n\n        let material = stage\n            .new_material()\n            .with_albedo_texture(&entries[0])\n            .with_has_lighting(false);\n        let transform = stage\n            .new_transform()\n            .with_translation(Vec3::new(15.0, 35.0, 0.5))\n            .with_scale(Vec3::new(0.5, 0.5, 1.0));\n        let _rez = stage\n            .new_primitive()\n            .with_vertices(&geometry)\n            .with_material(material)\n            .with_transform(transform);\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"stage/shared_node_with_different_materials.png\", img);\n    }\n\n    #[test]\n    /// Tests shading with directional light.\n    fn scene_cube_directional() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx\n            .new_stage()\n            .with_bloom(false)\n            .with_background_color(Vec3::splat(0.0).extend(1.0));\n\n        let (projection, _) = camera::default_perspective(100.0, 100.0);\n        let view = Mat4::look_at_rh(Vec3::new(1.8, 1.8, 1.8), Vec3::ZERO, Vec3::Y);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n\n        let red = Vec3::X.extend(1.0);\n        let green = Vec3::Y.extend(1.0);\n        let blue = Vec3::Z.extend(1.0);\n        let _dir_red = stage\n            .new_directional_light()\n            .with_direction(Vec3::NEG_Y)\n            .with_color(red)\n            .with_intensity(Lux::OUTDOOR_FULL_DAYLIGHT_LOW);\n        let _dir_green = stage\n            .new_directional_light()\n            .with_direction(Vec3::NEG_X)\n            .with_color(green)\n            .with_intensity(Lux::OUTDOOR_FULL_DAYLIGHT_LOW);\n        let _dir_blue = stage\n            .new_directional_light()\n            .with_direction(Vec3::NEG_Z)\n            .with_color(blue)\n            .with_intensity(Lux::OUTDOOR_FULL_DAYLIGHT_LOW);\n\n        let _rez = stage\n            .new_primitive()\n            .with_material(stage.default_material());\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        println!(\n            \"lighting_descriptor: {:#?}\",\n            stage.lighting.lighting_descriptor.get()\n        );\n        let img = frame.read_image().block().unwrap();\n        let depth_texture = stage.get_depth_texture();\n        let depth_img = depth_texture.read_image().block().unwrap().unwrap();\n        img_diff::assert_img_eq(\"stage/cube_directional_depth.png\", depth_img);\n        img_diff::assert_img_eq(\"stage/cube_directional.png\", img);\n    }\n\n    #[test]\n    // Test to make sure that we can reconstruct a normal matrix without using the\n    // inverse transpose of a model matrix, so long as we have the T R S\n    // transformation components (we really only need the scale).\n    //\n    // see Eric's comment here https://computergraphics.stackexchange.com/questions/1502/why-is-the-transposed-inverse-of-the-model-view-matrix-used-to-transform-the-nor?newreg=ffeabc7602da4fa2bc15fb9c84179dff\n    // see Eric's blog post here https://lxjk.github.io/2017/10/01/Stop-Using-Normal-Matrix.html\n    // squaring a vector https://math.stackexchange.com/questions/1419887/squaring-a-vector#1419889\n    // more convo wrt shaders https://github.com/mrdoob/three.js/issues/18497\n    fn square_scale_norm_check() {\n        let quat = Quat::from_axis_angle(Vec3::Z, std::f32::consts::FRAC_PI_4);\n        let scale = Vec3::new(10.0, 20.0, 1.0);\n        let model_matrix = Mat4::from_translation(Vec3::new(10.0, 10.0, 20.0))\n            * Mat4::from_quat(quat)\n            * Mat4::from_scale(scale);\n        let normal_matrix = model_matrix.inverse().transpose();\n        let scale2 = scale * scale;\n\n        for i in 0..9 {\n            for j in 0..9 {\n                for k in 0..9 {\n                    if i == 0 && j == 0 && k == 0 {\n                        continue;\n                    }\n                    let norm = Vec3::new(i as f32, j as f32, k as f32).normalize();\n                    let model = Mat3::from_mat4(model_matrix);\n                    let norm_a = (Mat3::from_mat4(normal_matrix) * norm).normalize();\n                    let norm_b = (model * (norm / scale2)).normalize();\n                    assert!(\n                        norm_a.abs_diff_eq(norm_b, f32::EPSILON),\n                        \"norm:{norm}, scale2:{scale2}\"\n                    );\n                }\n            }\n        }\n    }\n\n    #[test]\n    // shows how to \"nest\" children to make them appear transformed by their\n    // parent's transform\n    fn scene_parent_sanity() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx.new_stage().with_background_color(Vec4::splat(0.0));\n        let (projection, view) = camera::default_ortho2d(100.0, 100.0);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n\n        let root_node = stage\n            .new_nested_transform()\n            .with_local_scale(Vec3::new(25.0, 25.0, 1.0));\n        println!(\"root_node: {:#?}\", root_node.global_descriptor());\n\n        let offset = Vec3::new(1.0, 1.0, 0.0);\n\n        let cyan_node = stage.new_nested_transform().with_local_translation(offset);\n        println!(\"cyan_node: {:#?}\", cyan_node.global_descriptor());\n\n        let yellow_node = stage.new_nested_transform().with_local_translation(offset);\n        println!(\"yellow_node: {:#?}\", yellow_node.global_descriptor());\n\n        let red_node = stage.new_nested_transform().with_local_translation(offset);\n        println!(\"red_node: {:#?}\", red_node.global_descriptor());\n\n        root_node.add_child(&cyan_node);\n        println!(\"cyan_node: {:#?}\", cyan_node.global_descriptor());\n        cyan_node.add_child(&yellow_node);\n        println!(\"yellow_node: {:#?}\", yellow_node.global_descriptor());\n        yellow_node.add_child(&red_node);\n        println!(\"red_node: {:#?}\", red_node.global_descriptor());\n\n        let geometry = stage.new_vertices({\n            let size = 1.0;\n            [\n                Vertex::default().with_position([0.0, 0.0, 0.0]),\n                Vertex::default().with_position([size, size, 0.0]),\n                Vertex::default().with_position([size, 0.0, 0.0]),\n            ]\n        });\n        let _cyan_primitive = stage\n            .new_primitive()\n            .with_vertices(&geometry)\n            .with_material(\n                stage\n                    .new_material()\n                    .with_albedo_factor(Vec4::new(0.0, 1.0, 1.0, 1.0))\n                    .with_has_lighting(false),\n            )\n            .with_transform(&cyan_node);\n        let _yellow_primitive = stage\n            .new_primitive()\n            .with_vertices(&geometry)\n            .with_material(\n                stage\n                    .new_material()\n                    .with_albedo_factor(Vec4::new(1.0, 1.0, 0.0, 1.0))\n                    .with_has_lighting(false),\n            )\n            .with_transform(&yellow_node);\n        let _red_primitive = stage\n            .new_primitive()\n            .with_vertices(&geometry)\n            .with_material(\n                stage\n                    .new_material()\n                    .with_albedo_factor(Vec4::new(1.0, 0.0, 0.0, 1.0))\n                    .with_has_lighting(false),\n            )\n            .with_transform(&red_node);\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"scene_parent_sanity.png\", img);\n    }\n\n    #[test]\n    // sanity tests that we can extract the position of the camera using the\n    // camera's view transform\n    fn camera_position_from_view_matrix() {\n        let position = Vec3::new(1.0, 2.0, 12.0);\n        let view = Mat4::look_at_rh(position, Vec3::new(1.0, 2.0, 0.0), Vec3::Y);\n        let extracted_position = view.inverse().transform_point3(Vec3::ZERO);\n        assert_eq!(position, extracted_position);\n    }\n\n    #[test]\n    fn can_resize_context_and_stage() {\n        let size = UVec2::new(100, 100);\n        let mut ctx = Context::headless(size.x, size.y).block();\n        let stage = ctx.new_stage();\n\n        // create the CMY cube\n        let camera_position = Vec3::new(0.0, 12.0, 20.0);\n        let _camera = stage.new_camera().with_projection_and_view(\n            Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0),\n            Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y),\n        );\n        let _rez = stage\n            .new_primitive()\n            .with_vertices(stage.new_vertices(gpu_cube_vertices()))\n            .with_transform(\n                stage\n                    .new_transform()\n                    .with_scale(Vec3::new(6.0, 6.0, 6.0))\n                    .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),\n            );\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        assert_eq!(size, UVec2::new(img.width(), img.height()));\n        img_diff::assert_img_eq(\"stage/resize_100.png\", img);\n        frame.present();\n\n        let new_size = UVec2::new(200, 200);\n        ctx.set_size(new_size);\n        stage.set_size(new_size);\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        assert_eq!(new_size, UVec2::new(img.width(), img.height()));\n        img_diff::assert_img_eq(\"stage/resize_200.png\", img);\n        frame.present();\n    }\n\n    #[test]\n    fn can_direct_draw_cube() {\n        let size = UVec2::new(100, 100);\n        let ctx = Context::headless(size.x, size.y)\n            .block()\n            .with_use_direct_draw(true);\n        let stage = ctx.new_stage();\n\n        // create the CMY cube\n        let camera_position = Vec3::new(0.0, 12.0, 20.0);\n        let _camera = stage.new_camera().with_projection_and_view(\n            Mat4::perspective_rh(std::f32::consts::PI / 4.0, 1.0, 0.1, 100.0),\n            Mat4::look_at_rh(camera_position, Vec3::ZERO, Vec3::Y),\n        );\n        let _rez = stage\n            .new_primitive()\n            .with_vertices(stage.new_vertices(gpu_cube_vertices()))\n            .with_transform(\n                stage\n                    .new_transform()\n                    .with_scale(Vec3::new(6.0, 6.0, 6.0))\n                    .with_rotation(Quat::from_axis_angle(Vec3::Y, -std::f32::consts::FRAC_PI_4)),\n            );\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        assert_eq!(size, UVec2::new(img.width(), img.height()));\n        img_diff::assert_img_eq(\"stage/resize_100.png\", img);\n        frame.present();\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/light/cpu/test.rs",
    "content": "//! Tests of the lighting system.\n\nuse glam::{Vec3, Vec4, Vec4Swizzles};\n\nuse spirv_std::num_traits::Zero;\n\nuse crate::{\n    bvol::BoundingBox,\n    camera::Camera,\n    color::linear_xfer_vec4,\n    context::Context,\n    geometry::Vertex,\n    light::{shader::SpotLightCalculation, LightTiling, LightTilingConfig},\n    math::GpuRng,\n    primitive::{shader::PrimitivePbrVertexInfo, Primitive},\n    stage::Stage,\n    test::BlockOnFuture,\n    transform::shader::TransformDescriptor,\n};\n\nuse super::*;\n\n#[test]\n/// Ensures that a spot light can determine if a point lies inside or outside\n/// its cone of emission.\nfn spot_one_calc() {\n    let (doc, _, _) = gltf::import(\n        crate::test::workspace_dir()\n            .join(\"gltf\")\n            .join(\"spot_one.glb\"),\n    )\n    .unwrap();\n    let light = doc.lights().unwrap().next().unwrap();\n    let spot = if let gltf::khr_lights_punctual::Kind::Spot {\n        inner_cone_angle,\n        outer_cone_angle,\n    } = light.kind()\n    {\n        (inner_cone_angle, outer_cone_angle)\n    } else {\n        panic!(\"not a spot light\");\n    };\n    log::info!(\"spot: {spot:#?}\");\n\n    let light_node = doc.nodes().find(|node| node.light().is_some()).unwrap();\n    let parent_transform = TransformDescriptor::from(light_node.transform());\n    log::info!(\"parent_transform: {parent_transform:#?}\");\n\n    let spot_descriptor = SpotLightDescriptor {\n        position: Vec3::ZERO,\n        direction: Vec3::NEG_Z,\n        inner_cutoff: spot.0,\n        outer_cutoff: spot.1,\n        color: Vec3::from(light.color()).extend(1.0),\n        intensity: Candela(light.intensity()),\n    };\n\n    let specific_points = [\n        (Vec3::ZERO, true, true, Some(1.0)),\n        (Vec3::new(0.5, 0.0, 0.0), false, true, None),\n        (Vec3::new(0.5, 0.0, 0.5), false, false, None),\n        (Vec3::new(1.0, 0.0, 0.0), false, false, Some(0.0)),\n    ];\n    for (i, (point, inside_inner, inside_outer, maybe_contribution)) in\n        specific_points.into_iter().enumerate()\n    {\n        log::info!(\"{i} descriptor: {spot_descriptor:#?}\");\n        let spot_calc = SpotLightCalculation::new(spot_descriptor, parent_transform.into(), point);\n        log::info!(\"{i} spot_calc@{point}:\\n{spot_calc:#?}\");\n        assert_eq!(\n            (inside_inner, inside_outer),\n            (\n                spot_calc.fragment_is_inside_inner_cone,\n                spot_calc.fragment_is_inside_outer_cone\n            ),\n        );\n        if let Some(expected_contribution) = maybe_contribution {\n            assert_eq!(expected_contribution, spot_calc.contribution);\n        }\n    }\n}\n\n#[test]\n/// Ensures that a spot light illuminates only the objects within its cone of\n/// emission.\nfn spot_one_frame() {\n    let m = 32.0;\n    let (w, h) = (16.0f32 * m, 9.0 * m);\n    let ctx = Context::headless(w as u32, h as u32).block();\n    let stage = ctx.new_stage().with_msaa_sample_count(4);\n    let doc = stage\n        .load_gltf_document_from_path(\n            crate::test::workspace_dir()\n                .join(\"gltf\")\n                .join(\"spot_one.glb\"),\n        )\n        .unwrap();\n    let camera = doc.cameras.first().unwrap();\n    camera\n        .as_ref()\n        .set_projection(crate::camera::perspective(w, h));\n    stage.use_camera(camera);\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame.read_image().block().unwrap();\n    img_diff::assert_img_eq(\"light/spot_lights/one.png\", img);\n    frame.present();\n}\n\n#[test]\n/// Test the spot lights.\n///\n/// This should render a cube with two spot lights illuminating a spot on two\n/// of its sides.\nfn spot_lights() {\n    let w = 800.0;\n    let h = 800.0;\n    let ctx = Context::headless(w as u32, h as u32).block();\n    let stage = ctx\n        .new_stage()\n        .with_lighting(true)\n        .with_msaa_sample_count(4);\n\n    let doc = stage\n        .load_gltf_document_from_path(\n            crate::test::workspace_dir()\n                .join(\"gltf\")\n                .join(\"spot_lights.glb\"),\n        )\n        .unwrap();\n    let camera = doc.cameras.first().unwrap();\n    camera\n        .as_ref()\n        .set_projection(crate::camera::perspective(w, h));\n    stage.use_camera(camera);\n\n    let down_light = doc.lights.first().unwrap();\n    log::info!(\"down_light: {:#?}\", down_light.as_spot().unwrap());\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame.read_image().block().unwrap();\n    img_diff::assert_img_eq(\"light/spot_lights/frame.png\", img);\n    frame.present();\n}\n\n#[test]\nfn light_tiling_light_bounds() {\n    let magnification = 8;\n    let w = 16.0 * 2.0f32.powi(magnification);\n    let h = 9.0 * 2.0f32.powi(magnification);\n    let ctx = Context::headless(w as u32, h as u32).block();\n    let stage = ctx.new_stage().with_msaa_sample_count(4);\n    let doc = stage\n        .load_gltf_document_from_path(\n            crate::test::workspace_dir()\n                .join(\"gltf\")\n                .join(\"light_tiling_test.glb\"),\n        )\n        .unwrap();\n    let camera = doc.cameras.first().unwrap();\n\n    stage.use_camera(camera);\n\n    let _lights = crate::test::make_two_directional_light_setup(&stage);\n\n    // Here we only want to render the bounding boxes of the renderlets,\n    // so mark the renderlets themeselves invisible\n    doc.renderlets_iter().for_each(|r| {\n        r.set_visible(false);\n    });\n\n    let colors = [0x6DE1D2FF, 0xFFD63AFF, 0x6DE1D2FF, 0xF75A5AFF].map(|albedo_factor| {\n        stage.new_material().with_albedo_factor({\n            let mut color = crate::math::hex_to_vec4(albedo_factor);\n            linear_xfer_vec4(&mut color);\n            color\n        })\n    });\n    let mut resources = vec![];\n    for (i, node) in doc.nodes.iter().enumerate() {\n        if node.mesh.is_none() {\n            continue;\n        }\n        let transform = Mat4::from(node.transform.global_descriptor());\n        if let Some(mesh_index) = node.mesh {\n            log::info!(\"mesh: {}\", node.name.as_deref().unwrap_or(\"unknown\"));\n            let mesh = &doc.meshes[mesh_index];\n            for prim in mesh.primitives.iter() {\n                let (min, max) = prim.bounding_box;\n                let min = transform.transform_point3(min);\n                let max = transform.transform_point3(max);\n                let bb = BoundingBox::from_min_max(min, max);\n                if bb.half_extent.min_element().is_zero() {\n                    log::warn!(\"bounding box is not a volume, skipping\");\n                    continue;\n                }\n                log::info!(\"min: {min}, max: {max}\");\n                resources.push(\n                    stage\n                        .new_primitive()\n                        .with_vertices(\n                            stage.new_vertices(\n                                bb.get_mesh().map(|(p, n)| {\n                                    Vertex::default().with_position(p).with_normal(n)\n                                }),\n                            ),\n                        )\n                        .with_material(&colors[i % colors.len()]),\n                );\n            }\n        }\n    }\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    // let img = frame.read_image().block().unwrap();\n    // img_diff::save(\"light/tiling/bounds.png\", img);\n    frame.present();\n}\n\nfn gen_vec3(prng: &mut GpuRng) -> Vec3 {\n    let x = prng.gen_f32(-120.0, 120.0);\n    let y = prng.gen_f32(0.0, 80.0);\n    let z = prng.gen_f32(-120.0, 120.0);\n    Vec3::new(x, y, z)\n}\n\nstruct GeneratedLight {\n    _unused_transform: Transform,\n    _light: AnalyticalLight<PointLight>,\n    mesh_renderlet: Primitive,\n}\n\nfn gen_light(stage: &Stage, prng: &mut GpuRng, bounding_boxes: &[BoundingBox]) -> GeneratedLight {\n    let mut position = gen_vec3(prng);\n    while bounding_boxes.iter().any(|bb| bb.contains_point(position)) {\n        position = gen_vec3(prng);\n    }\n    assert!(!position.x.is_nan());\n    assert!(!position.y.is_nan());\n    assert!(!position.z.is_nan());\n\n    let color = Vec4::new(\n        prng.gen_f32(0.0, 1.0),\n        prng.gen_f32(0.0, 1.0),\n        prng.gen_f32(0.0, 1.0),\n        1.0,\n    );\n\n    let scale = prng.gen_f32(0.1, 1.0);\n\n    let light_bb = BoundingBox {\n        center: Vec3::ZERO,\n        half_extent: Vec3::new(scale, scale, scale) * 0.5,\n    };\n\n    let _unused_transform = stage.new_transform().with_translation(position);\n    let vertices = stage.new_vertices(\n        light_bb\n            .get_mesh()\n            .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)),\n    );\n    let material = stage\n        .new_material()\n        .with_albedo_factor(color)\n        .with_has_lighting(false)\n        .with_emissive_factor(color.xyz())\n        .with_emissive_strength_multiplier(100.0);\n    let mesh_renderlet = stage\n        .new_primitive()\n        .with_vertices(vertices)\n        .with_material(material);\n    let _light = {\n        // suffix the actual analytical light\n        let intensity = scale * 100.0 * 683.0;\n        stage\n            .new_point_light()\n            .with_position(position)\n            .with_color(color)\n            .with_intensity(Candela(intensity))\n    };\n\n    GeneratedLight {\n        _unused_transform,\n        _light,\n        mesh_renderlet,\n    }\n}\n\nfn size() -> UVec2 {\n    UVec2::new(\n        (10.0 * 2.0f32.powi(8)) as u32,\n        (9.0 * 2.0f32.powi(8)) as u32,\n    )\n}\n\nfn make_camera(stage: &Stage) -> Camera {\n    let size = size();\n    let eye = Vec3::new(250.0, 200.0, 250.0);\n    let target = Vec3::ZERO;\n    log::info!(\"make_camera: forward {}\", (target - eye).normalize());\n    stage.new_camera().with_projection_and_view(\n        Mat4::perspective_rh(\n            std::f32::consts::FRAC_PI_4,\n            size.x as f32 / size.y as f32,\n            50.0,\n            600.0,\n        ),\n        Mat4::look_at_rh(eye, target, Vec3::Y),\n    )\n}\n\n/// Ensures that `LightTile`s are cleared by the clear_tiles shader.\n#[test]\nfn clear_tiles_sanity() {\n    let _ = env_logger::builder().is_test(true).try_init();\n    let s = 256;\n    let depth_texture_size = UVec2::splat(s);\n    let ctx = Context::headless(s, s).block();\n    let stage = ctx.new_stage();\n    let lighting: &Lighting = stage.as_ref();\n    let tiling_config = LightTilingConfig::default();\n    let tiling = LightTiling::new_hybrid(lighting, false, depth_texture_size, tiling_config);\n    let desc = tiling.tiling_descriptor.get();\n    let tile_dimensions = desc.tile_grid_size();\n\n    // Write to the tiles to ensure we know the starting state, that way we can\n    // ensure each step of tiling is correct.\n    {\n        let mut rng = GpuRng::new(0);\n        let max_distance = UVec2::ZERO.manhattan_distance(tile_dimensions) as f32;\n        for i in 0..tiling.tiles().len() {\n            tiling.tiles().modify(i, |item| {\n                let x = i as u32 % tile_dimensions.x;\n                let y = i as u32 / tile_dimensions.x;\n                let tile_coord = UVec2::new(x, y);\n                let distance = tile_coord.manhattan_distance(tile_dimensions) as f32;\n                // This should produce an image where pixels get darker towards the lower right\n                // corner.\n                let min = distance / max_distance;\n                // This should produce an image where pixels get darker towards the upper left\n                // corner.\n                let max = 1.0 - distance / max_distance;\n\n                item.depth_min = crate::light::shader::quantize_depth_f32_to_u32(min);\n                item.depth_max = crate::light::shader::quantize_depth_f32_to_u32(max);\n\n                // This should produce an image that looks like noise\n                item.next_light_index = rng.gen_u32(0, 32);\n            });\n        }\n        let _ = lighting.commit();\n        ctx.get_device().poll(wgpu::PollType::Wait).unwrap();\n\n        let (mins, maxs, lights) = futures_lite::future::block_on(tiling.read_images(lighting));\n        img_diff::assert_img_eq(\"light/tiling/clear_tiles/1-mins.png\", mins);\n        img_diff::assert_img_eq(\"light/tiling/clear_tiles/1-maxs.png\", maxs);\n        img_diff::assert_img_eq(\"light/tiling/clear_tiles/1-lights.png\", lights);\n    }\n\n    // Run the clear_tiles shader to ensure that the tiles are cleared.\n    {\n        tiling.prepare(lighting, depth_texture_size);\n        let stage_commit_result = stage.commit();\n        let bindgroup = tiling.get_bindgroup(\n            ctx.get_device(),\n            &stage_commit_result.geometry_buffer,\n            &stage_commit_result.lighting_buffer,\n            &stage.depth_texture.read().unwrap(),\n        );\n        let label = Some(\"light-tiling-clear-tiles-test\");\n        let mut encoder = ctx\n            .get_device()\n            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label });\n        {\n            tiling.clear_tiles(&mut encoder, bindgroup.as_ref(), depth_texture_size);\n        }\n        ctx.runtime().queue.submit(Some(encoder.finish()));\n\n        let (mins, maxs, lights) = futures_lite::future::block_on(tiling.read_images(lighting));\n        img_diff::assert_img_eq(\"light/tiling/clear_tiles/2-mins.png\", mins);\n        img_diff::assert_img_eq(\"light/tiling/clear_tiles/2-maxs.png\", maxs);\n        img_diff::assert_img_eq(\"light/tiling/clear_tiles/2-lights.png\", lights);\n    }\n}\n\n#[test]\nfn min_max_depth_sanity() {\n    let _ = env_logger::builder().is_test(true).try_init();\n    let s = 256;\n    let depth_texture_size = UVec2::splat(s);\n    let ctx = Context::headless(s, s).block();\n    let stage = ctx.new_stage();\n    let _doc = stage\n        .load_gltf_document_from_path(\n            crate::test::workspace_dir()\n                .join(\"gltf\")\n                .join(\"light_tiling_test.glb\"),\n        )\n        .unwrap();\n    let camera = make_camera(&stage);\n    stage.use_camera(camera);\n    snapshot(\n        &ctx,\n        &stage,\n        \"light/tiling/min_max_depth/1-scene.png\",\n        false,\n    );\n\n    let lighting = &stage.lighting;\n    let tiling = LightTiling::new_hybrid(lighting, false, depth_texture_size, Default::default());\n    tiling.prepare(lighting, depth_texture_size);\n\n    let stage_commit_result = stage.commit();\n    let bindgroup = tiling.get_bindgroup(\n        ctx.get_device(),\n        &stage_commit_result.geometry_buffer,\n        &stage_commit_result.lighting_buffer,\n        &stage.depth_texture.read().unwrap(),\n    );\n    let label = Some(\"light-tiling-min-max-depth-test\");\n\n    // Clear the tiles, which is verified in `clear_tiles_sanity`, then assert the\n    // min/max depth\n    {\n        let mut encoder = ctx\n            .get_device()\n            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label });\n        {\n            tiling.clear_tiles(&mut encoder, bindgroup.as_ref(), depth_texture_size);\n            tiling.compute_min_max_depth(&mut encoder, bindgroup.as_ref(), depth_texture_size);\n        }\n        ctx.runtime().queue.submit(Some(encoder.finish()));\n        let (mins, maxs, _lights) = futures_lite::future::block_on(tiling.read_images(lighting));\n        img_diff::assert_img_eq(\"light/tiling/min_max_depth/2-mins.png\", mins);\n        img_diff::assert_img_eq(\"light/tiling/min_max_depth/2-maxs.png\", maxs);\n    }\n}\n\n#[test]\nfn light_bins_sanity() {\n    let _ = env_logger::builder().is_test(true).try_init();\n    let s = 256;\n    let depth_texture_size = UVec2::splat(s);\n    let ctx = Context::headless(s, s).block();\n    let stage = ctx.new_stage();\n    let doc = stage\n        .load_gltf_document_from_path(\n            crate::test::workspace_dir()\n                .join(\"gltf\")\n                .join(\"light_tiling_test.glb\"),\n        )\n        .unwrap();\n    let camera = make_camera(&stage);\n    stage.use_camera(camera);\n    snapshot(&ctx, &stage, \"light/tiling/bins/1-scene.png\", false);\n\n    let lighting = &stage.lighting;\n    let tiling = LightTiling::new_hybrid(lighting, false, depth_texture_size, Default::default());\n    assert_eq!(\n        tiling.tiling_descriptor.get().tiles_array,\n        tiling.tiles().array()\n    );\n    tiling.prepare(lighting, depth_texture_size);\n\n    let stage_commit_result = stage.commit();\n    let bindgroup = tiling.get_bindgroup(\n        ctx.get_device(),\n        &stage_commit_result.geometry_buffer,\n        &stage_commit_result.lighting_buffer,\n        &stage.depth_texture.read().unwrap(),\n    );\n    let label = Some(\"light-tiling-min-max-depth-test\");\n\n    {\n        {\n            let mut encoder = ctx\n                .get_device()\n                .create_command_encoder(&wgpu::CommandEncoderDescriptor { label });\n            tiling.clear_tiles(&mut encoder, bindgroup.as_ref(), depth_texture_size);\n            tiling.compute_min_max_depth(&mut encoder, bindgroup.as_ref(), depth_texture_size);\n            tiling.compute_bins(&mut encoder, bindgroup.as_ref(), depth_texture_size);\n            ctx.runtime().queue.submit(Some(encoder.finish()));\n        }\n        let (mut mins, mut maxs, mut lights) =\n            futures_lite::future::block_on(tiling.read_images(lighting));\n        img_diff::normalize_gray_img(&mut mins);\n        img_diff::normalize_gray_img(&mut maxs);\n        img_diff::normalize_gray_img(&mut lights);\n        img_diff::assert_img_eq(\"light/tiling/bins/2-mins.png\", mins);\n        img_diff::assert_img_eq(\"light/tiling/bins/2-maxs.png\", maxs);\n        img_diff::assert_img_eq(\"light/tiling/bins/2-lights.png\", lights);\n    }\n    let directional_light = doc.lights.first().unwrap();\n    let tiles = futures_lite::future::block_on(tiling.read_tiles(lighting));\n    for tile in tiles.into_iter() {\n        let light_bin =\n            futures_lite::future::block_on(lighting.light_slab.read_array(tile.lights_array))\n                .unwrap();\n        // Assert either the light is the correct one, or we're using the zero frustum\n        // optimization discussed in <http://renderling.xyz/articles/live/light_tiling.html#zero-volume-frustum-optimization>\n        if tile.depth_min != tile.depth_max {\n            assert_eq!(light_bin[0], directional_light.id());\n            assert_eq!(light_bin[1], Id::NONE);\n        } else {\n            assert_eq!(0, tile.next_light_index);\n            assert_eq!(light_bin[0], Id::NONE);\n        }\n    }\n}\n\n// Ensures point lights are being binned properly.\n#[test]\nfn light_bins_point() {\n    let ctx = Context::headless(256, 256).block();\n    let stage = ctx\n        .new_stage()\n        .with_msaa_sample_count(1)\n        .with_bloom_mix_strength(0.08);\n    let mut doc = stage\n        .load_gltf_document_from_path(\n            crate::test::workspace_dir()\n                .join(\"gltf\")\n                .join(\"pedestal.glb\"),\n        )\n        .unwrap();\n\n    doc.materials\n        .get_mut(0)\n        .unwrap()\n        .set_albedo_factor(Vec4::ONE)\n        .set_roughness_factor(1.0)\n        .set_metallic_factor(0.0);\n\n    let camera = doc.cameras.first().unwrap();\n    let view = Mat4::look_at_rh(Vec3::new(-7.0, 5.0, 7.0), Vec3::ZERO, Vec3::Y);\n    let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_6, 1.0, 0.1, 15.0);\n    camera.camera.set_projection_and_view(proj, view);\n\n    let _point_light = stage\n        .new_point_light()\n        .with_position(Vec3::new(1.1, 1.0, 1.1))\n        .with_color(Vec4::ONE)\n        .with_intensity(Candela(5.0));\n    snapshot(\n        &ctx,\n        &stage,\n        \"light/tiling/light_bins_point/1-scene.png\",\n        true,\n    );\n\n    let tiling = stage.new_light_tiling(LightTilingConfig {\n        max_lights_per_tile: 2,\n        ..Default::default()\n    });\n    tiling.run(&stage);\n\n    let (_mins, _maxs, lights) = futures_lite::future::block_on(tiling.read_images(stage.as_ref()));\n    img_diff::save(\"light/tiling/light_bins_point/2-lights.png\", lights);\n\n    let mut found = 0;\n    for tile in futures_lite::future::block_on(tiling.read_tiles(stage.as_ref())) {\n        if tile.depth_min != tile.depth_max && found < 3 {\n            found += 1;\n            log::info!(\"tile: {tile:#?}\");\n        }\n    }\n}\n\nfn tiling_e2e_sanity_with(\n    tile_size: u32,\n    max_lights_per_tile: u32,\n    i: u32,\n    minimum_illuminance: f32,\n    save_images: bool,\n) {\n    println!(\n        \"tiling e2e with tile_size:{tile_size}, max_lights_per_tile:{max_lights_per_tile}, \\\n         minimum_illuminance: {minimum_illuminance}\"\n    );\n    let size = size();\n    let ctx = Context::headless(size.x, size.y).block();\n    let stage = ctx\n        .new_stage()\n        .with_bloom(true)\n        .with_bloom_mix_strength(0.5)\n        .with_msaa_sample_count(1);\n\n    let doc = stage\n        .load_gltf_document_from_path(\n            crate::test::workspace_dir()\n                .join(\"gltf\")\n                .join(\"light_tiling_test.glb\"),\n        )\n        .unwrap();\n\n    let camera = make_camera(&stage);\n    stage.use_camera(camera);\n\n    let _ = stage.lighting.commit();\n\n    let moonlight = doc.lights.first().unwrap();\n    let _shadow = {\n        let sm = stage\n            .new_shadow_map(moonlight, UVec2::splat(1024), 0.1, 256.0)\n            .unwrap();\n        sm.shadowmap_descriptor.modify(|d| {\n            d.bias_min = 0.0;\n            d.bias_max = 0.0;\n            d.pcf_samples = 2;\n        });\n        sm.update(&stage, doc.renderlets_iter()).unwrap();\n        sm\n    };\n\n    let mut bounding_boxes = vec![];\n    for node in doc.nodes.iter() {\n        if node.mesh.is_none() {\n            continue;\n        }\n        let transform = Mat4::from(node.transform.global_descriptor());\n        if let Some(mesh_index) = node.mesh {\n            let mesh = &doc.meshes[mesh_index];\n            for prim in mesh.primitives.iter() {\n                let (min, max) = prim.bounding_box;\n                let min = transform.transform_point3(min);\n                let max = transform.transform_point3(max);\n                let bb = BoundingBox::from_min_max(min, max);\n                if bb.half_extent.min_element().is_zero() {\n                    continue;\n                }\n                bounding_boxes.push(bb);\n            }\n        }\n    }\n\n    let mut prng = crate::math::GpuRng::new(666);\n    let mut lights: Vec<GeneratedLight> = vec![];\n\n    for _ in 0..MAX_LIGHTS {\n        lights.push(gen_light(&stage, &mut prng, &bounding_boxes));\n    }\n    println!(\"created lights\");\n\n    // Remove the light meshes\n    for generated_light in lights.iter() {\n        stage.remove_primitive(&generated_light.mesh_renderlet);\n    }\n    snapshot(\n        &ctx,\n        &stage,\n        \"light/tiling/e2e/4-scene-no-tiling.png\",\n        false,\n    );\n\n    let config = LightTilingConfig {\n        tile_size,\n        max_lights_per_tile,\n        minimum_illuminance,\n    };\n    println!(\"generating rendering with config: {config:#?}\");\n    let tiling = stage.new_light_tiling(config);\n    tiling.run(&stage);\n    snapshot(\n        &ctx,\n        &stage,\n        &format!(\n            \"light/tiling/e2e/\\\n             6-scene-{tile_size}-{max_lights_per_tile}-lights-{i}-{minimum_illuminance}-min-lux.\\\n             png\"\n        ),\n        save_images,\n    );\n\n    #[cfg(feature = \"light-tiling-stats\")]\n    {\n        use stats::*;\n        // Stats\n        let mut stats = LightTilingStats::default();\n        for number_of_lights_exponent in 2..=MAX_LIGHTS.ilog2() {\n            let number_of_lights = 2usize.pow(number_of_lights_exponent);\n            let mut run = LightTilingStatsRun {\n                number_of_lights,\n                iterations: vec![],\n            };\n\n            for (i, generated_light) in lights.iter().enumerate() {\n                stage.remove_light(&generated_light._light);\n                if i < number_of_lights {\n                    stage.add_light(&generated_light._light);\n                }\n            }\n\n            const NUM_RUNS: usize = 2;\n            for (i, with_tiling) in (0..NUM_RUNS).zip([true, false].iter().cycle()) {\n                log::info!(\n                    \"{number_of_lights} {i} {} running\",\n                    if true { \"tiling\" } else { \"non-tiling\" }\n                );\n                if *with_tiling {\n                    tiling.run(&stage);\n                }\n                stage.lighting.lighting_descriptor.modify(|desc| {\n                    desc.light_tiling_descriptor_id = if *with_tiling {\n                        tiling.tiling_descriptor.id()\n                    } else {\n                        Id::NONE\n                    }\n                });\n                let start = std::time::Instant::now();\n                let frame = ctx.get_next_frame().unwrap();\n                stage.render(&frame.view());\n                frame.present();\n                ctx.get_device().poll(wgpu::PollType::Wait).unwrap();\n                let duration = start.elapsed();\n                run.iterations.push((*with_tiling, duration));\n            }\n            stats.runs.push(run);\n        }\n        plot(stats, &format!(\"frame-time-{tile_size}-{max_lights_per_tile}-lights-{i}-{minimum_illuminance}-min-lux\"));\n    }\n}\n\n#[test]\n/// Test the light tiling feature, end to end.\nfn tiling_e2e_sanity() {\n    let _ = env_logger::builder().is_test(true).try_init();\n    let config = LightTilingConfig::default();\n    tiling_e2e_sanity_with(\n        config.tile_size,\n        config.max_lights_per_tile,\n        0,\n        config.minimum_illuminance,\n        false,\n    );\n\n    #[cfg(feature = \"light-tiling-stats\")]\n    {\n        let tile_sizes = [4, 8, 16];\n        let max_lights_per_tile = [16, 32, 64, 128, 256];\n        let minimum_illuminance_lux = [0.05, 0.1, 0.3, 1.0, 2.0];\n        for tile_size in tile_sizes {\n            for max_lights_per_tile in max_lights_per_tile {\n                for (i, minimum_illuminance) in minimum_illuminance_lux.iter().enumerate() {\n                    tiling_e2e_sanity_with(\n                        tile_size,\n                        max_lights_per_tile,\n                        i as u32,\n                        *minimum_illuminance,\n                        true,\n                    )\n                }\n            }\n        }\n    }\n}\n\nfn snapshot(ctx: &crate::context::Context, stage: &Stage, path: &str, save: bool) {\n    let frame = ctx.get_next_frame().unwrap();\n    let start = std::time::Instant::now();\n    stage.render(&frame.view());\n    let elapsed = start.elapsed();\n    log::info!(\"shapshot: {}s '{path}'\", elapsed.as_secs_f32());\n    let img = frame.read_image().block().unwrap();\n    if save {\n        img_diff::save(path, img);\n    } else {\n        img_diff::assert_img_eq(path, img);\n    }\n    frame.present();\n}\n\n#[test]\n/// Ensures that setting an ambient light color produces a visibly different\n/// render than the default (zero) ambient.\nfn ambient_light() {\n    let ctx = Context::headless(256, 256).block();\n    let stage = ctx\n        .new_stage()\n        .with_lighting(true)\n        .with_msaa_sample_count(4);\n\n    let doc = stage\n        .load_gltf_document_from_path(\n            crate::test::workspace_dir()\n                .join(\"gltf\")\n                .join(\"pedestal.glb\"),\n        )\n        .unwrap();\n\n    let camera = doc.cameras.first().unwrap();\n    camera.camera.set_projection_and_view(\n        Mat4::perspective_rh(std::f32::consts::FRAC_PI_6, 1.0, 0.1, 15.0),\n        Mat4::look_at_rh(Vec3::new(-7.0, 5.0, 7.0), Vec3::ZERO, Vec3::Y),\n    );\n\n    let position = Vec3::new(1.1, 1.0, 1.1);\n    let dir_light = stage\n        .new_directional_light()\n        .with_direction(-position)\n        .with_color(Vec4::ONE)\n        .with_intensity(Lux::OUTDOOR_FOXS_WEDDING);\n\n    let shadow_map = stage\n        .new_shadow_map(&dir_light, UVec2::splat(256), 0.1, 15.0)\n        .unwrap();\n    shadow_map.update(&stage, doc.renderlets_iter()).unwrap();\n\n    // Render with default ambient (Vec4::ZERO)\n    assert_eq!(stage.ambient_color(), Vec4::ZERO);\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let default_img = frame.read_image().block().unwrap();\n    img_diff::assert_img_eq(\"light/ambient/default.png\", default_img.clone());\n    frame.present();\n\n    // Render with orange ambient light\n    stage.set_ambient_color(Vec4::new(1.0, 0.5, 0.0, 0.3));\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let orange_img = frame.read_image().block().unwrap();\n    img_diff::assert_img_eq(\"light/ambient/orange.png\", orange_img.clone());\n    frame.present();\n\n    // The ambient light should visibly change the rendered image\n    assert_ne!(\n        default_img, orange_img,\n        \"ambient light should visibly affect the rendered image\"\n    );\n}\n\nconst MAX_LIGHTS: usize = 2usize.pow(10);\n\n#[cfg(feature = \"light-tiling-stats\")]\nmod stats {\n    #![allow(dead_code)]\n    use core::time::Duration;\n\n    use plotters::{\n        chart::{ChartBuilder, SeriesLabelPosition},\n        prelude::{BitMapBackend, Circle, EmptyElement, IntoDrawingArea, PathElement},\n        series::{LineSeries, PointSeries},\n        style::{Color, IntoFont, ShapeStyle},\n    };\n\n    pub struct LightTilingStatsRun {\n        pub number_of_lights: usize,\n        pub iterations: Vec<(bool, Duration)>,\n    }\n\n    impl LightTilingStatsRun {\n        fn avg_frame_time(&self, with_tiling: bool) -> f32 {\n            let total: Duration = self\n                .iterations\n                .iter()\n                .filter_map(|(had_tiling, dur)| {\n                    if *had_tiling == with_tiling {\n                        Some(dur)\n                    } else {\n                        None\n                    }\n                })\n                .sum();\n            total.as_secs_f32() / self.iterations.len() as f32\n        }\n    }\n\n    #[derive(Default)]\n    pub struct LightTilingStats {\n        pub runs: Vec<LightTilingStatsRun>,\n    }\n\n    pub fn plot(stats: LightTilingStats, filename: &str) {\n        let path = crate::test::workspace_dir()\n            .join(format!(\"test_output/light/tiling/e2e/{filename}.png\"));\n        let root_drawing_area = BitMapBackend::new(&path, (800, 600)).into_drawing_area();\n        root_drawing_area.fill(&plotters::style::WHITE).unwrap();\n\n        let mut chart = ChartBuilder::on(&root_drawing_area)\n            .caption(\n                \"Renderling lighting frame time\",\n                (\"sans-serif\", 50).into_font(),\n            )\n            .margin(30)\n            .margin_right(100)\n            .margin_left(60)\n            .x_label_area_size(30)\n            .y_label_area_size(30)\n            .build_cartesian_2d(\n                0..super::MAX_LIGHTS + 1,\n                0.0..stats\n                    .runs\n                    .iter()\n                    .flat_map(|r| [r.avg_frame_time(true), r.avg_frame_time(false)])\n                    .max_by(|a, b| a.total_cmp(b))\n                    .unwrap_or_default(),\n            )\n            .unwrap();\n        fn y_fmt(coord: &f32) -> String {\n            let fps = 1.0 / coord;\n            format!(\"{coord}s ({fps:.2} fps)\")\n        }\n        chart\n            .configure_mesh()\n            .x_desc(\"number of lights\")\n            .y_label_formatter(&y_fmt)\n            .draw()\n            .unwrap();\n        chart\n            .draw_series(LineSeries::new(\n                stats\n                    .runs\n                    .iter()\n                    .map(|r| (r.number_of_lights, r.avg_frame_time(false))),\n                plotters::style::RED,\n            ))\n            .unwrap()\n            .label(\"without tiling\")\n            .legend(|(x, y)| {\n                PathElement::new(vec![(x, y), (x + 20, y)], plotters::style::RED.filled())\n            });\n        chart\n            .draw_series(LineSeries::new(\n                stats\n                    .runs\n                    .iter()\n                    .map(|r| (r.number_of_lights, r.avg_frame_time(true))),\n                plotters::style::BLUE,\n            ))\n            .unwrap()\n            .label(\"with tiling\")\n            .legend(|(x, y)| {\n                PathElement::new(vec![(x, y), (x + 20, y)], plotters::style::BLUE.filled())\n            });\n        chart\n            .draw_series(PointSeries::of_element(\n                stats\n                    .runs\n                    .iter()\n                    .map(|r| (r.number_of_lights, r.avg_frame_time(false))),\n                5,\n                ShapeStyle::from(&plotters::style::RED).filled(),\n                &|(num_lights, seconds_per_frame), size, style| {\n                    EmptyElement::at((num_lights, seconds_per_frame))\n                        + Circle::new((0, 0), size, style)\n                },\n            ))\n            .unwrap();\n        chart\n            .draw_series(PointSeries::of_element(\n                stats\n                    .runs\n                    .iter()\n                    .map(|r| (r.number_of_lights, r.avg_frame_time(true))),\n                5,\n                ShapeStyle::from(&plotters::style::BLUE).filled(),\n                &|(num_lights, seconds_per_frame), size, style| {\n                    EmptyElement::at((num_lights, seconds_per_frame))\n                        + Circle::new((0, 0), size, style)\n                },\n            ))\n            .unwrap();\n\n        chart\n            .configure_series_labels()\n            .position(SeriesLabelPosition::UpperLeft)\n            .margin(20)\n            .label_font((\"sans-serif\", 20))\n            .draw()\n            .unwrap();\n        root_drawing_area.present().unwrap();\n    }\n}\n\n#[test]\n/// For all light types that have a position:\n///\n/// Ensures that a light with a translated position renders the same\n/// as a light at the origin that has a linked `NestedTransform` applied with\n/// that same translation.\n///\n/// In other words, light w/ nested transform is the same as light with\n/// that same transform pre-applied.\nfn pedestal() {\n    let ctx = crate::context::Context::headless(256, 256).block();\n    let stage = ctx\n        .new_stage()\n        .with_lighting(false)\n        .with_msaa_sample_count(4)\n        .with_bloom_mix_strength(0.08);\n    let mut doc = stage\n        .load_gltf_document_from_path(\n            crate::test::workspace_dir()\n                .join(\"gltf\")\n                .join(\"pedestal.glb\"),\n        )\n        .unwrap();\n\n    doc.materials\n        .get_mut(0)\n        .unwrap()\n        .set_albedo_factor(Vec4::ONE)\n        .set_roughness_factor(1.0)\n        .set_metallic_factor(0.0);\n\n    let camera = doc.cameras.first().unwrap();\n    camera.camera.set_projection_and_view(\n        Mat4::perspective_rh(std::f32::consts::FRAC_PI_6, 1.0, 0.1, 15.0),\n        Mat4::look_at_rh(Vec3::new(-7.0, 5.0, 7.0), Vec3::ZERO, Vec3::Y),\n    );\n\n    let color = {\n        // let mut c = hex_to_vec4(0xEEDF7AFF);\n        // linear_xfer_vec4(&mut c);\n        // c\n        Vec4::ONE\n    };\n    let position = Vec3::new(1.1, 1.0, 1.1);\n    stage.set_has_lighting(true);\n\n    let mut dir_infos = vec![];\n    {\n        log::info!(\"adding dir light\");\n        let dir_light = stage\n            .new_directional_light()\n            .with_direction(-position)\n            .with_color(color)\n            .with_intensity(Lux::OUTDOOR_FOXS_WEDDING);\n        let _ = stage.commit();\n        //snapshot(&ctx, &stage, \"light/pedestal/directional.png\", false);\n\n        let geometry_slab =\n            futures_lite::future::block_on(stage.geometry.slab_allocator().read(..)).unwrap();\n\n        let renderlet = doc.renderlets_iter().next().unwrap();\n        log::info!(\"renderlet: {:#?}\", renderlet.descriptor());\n\n        for vertex_index in 0..renderlet.descriptor().vertices_array.len() {\n            let mut info = PrimitivePbrVertexInfo::default();\n            crate::primitive::shader::primitive_vertex(\n                renderlet.id(),\n                vertex_index as u32,\n                &geometry_slab,\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut info,\n            );\n\n            dir_infos.push(info);\n        }\n        stage.remove_light(&dir_light);\n    }\n    assert_eq!(0, stage.lighting.lights().len());\n\n    // Point lights\n    {\n        log::info!(\"adding point light with pre-applied position\");\n        let point_light = stage\n            .new_point_light()\n            .with_position(position)\n            .with_color(color)\n            .with_intensity(Candela(5000.0));\n        let _ = stage.commit();\n        snapshot(&ctx, &stage, \"light/pedestal/point.png\", false);\n        stage.remove_light(&point_light);\n    }\n\n    {\n        log::info!(\"adding point light with nested transform\");\n        let transform = stage.new_nested_transform();\n        transform.set_local_translation(position);\n\n        let point_light = stage\n            .new_point_light()\n            .with_position(Vec3::ZERO)\n            .with_color(color)\n            .with_intensity(Candela(5000.0));\n        point_light.link_node_transform(&transform);\n\n        let _ = stage.commit();\n        snapshot(&ctx, &stage, \"light/pedestal/point.png\", false);\n        stage.remove_light(&point_light);\n    }\n\n    {\n        log::info!(\"adding spot light with pre-applied position\");\n        let spot = stage\n            .new_spot_light()\n            .with_position(position)\n            .with_direction(-position)\n            .with_color(color)\n            .with_intensity(Candela(40_000.0))\n            .with_inner_cutoff(core::f32::consts::PI / 5.0)\n            .with_outer_cutoff(core::f32::consts::PI / 4.0);\n        snapshot(&ctx, &stage, \"light/pedestal/spot.png\", false);\n\n        let geometry_slab =\n            futures_lite::future::block_on(stage.geometry.slab_allocator().read(..)).unwrap();\n\n        let renderlet = doc.renderlets_iter().next().unwrap();\n        log::info!(\"renderlet: {:#?}\", renderlet.descriptor());\n        let mut spot_infos = vec![];\n\n        for vertex_index in 0..renderlet.descriptor().vertices_array.len() {\n            let mut info = PrimitivePbrVertexInfo::default();\n            crate::primitive::shader::primitive_vertex(\n                renderlet.id(),\n                vertex_index as u32,\n                &geometry_slab,\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut Default::default(),\n                &mut info,\n            );\n            spot_infos.push(info);\n        }\n\n        // assert that the output of the vertex shader is the same for the first\n        // renderlet, regardless of the lighting\n        pretty_assertions::assert_eq!(dir_infos, spot_infos);\n        stage.remove_light(&spot);\n    }\n\n    {\n        log::info!(\"adding spot light with node position\");\n\n        let node_transform = stage.new_nested_transform();\n        node_transform.set_local_translation(position);\n\n        let spot = stage\n            .new_spot_light()\n            .with_position(Vec3::ZERO)\n            .with_direction(-position)\n            .with_color(color)\n            .with_intensity(Candela(40_000.0))\n            .with_inner_cutoff(core::f32::consts::PI / 5.0)\n            .with_outer_cutoff(core::f32::consts::PI / 4.0);\n        spot.link_node_transform(&node_transform);\n        snapshot(&ctx, &stage, \"light/pedestal/spot.png\", false);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/light/cpu.rs",
    "content": "//! CPU-only lighting and shadows.\nuse std::sync::{Arc, RwLock};\n\n#[cfg(doc)]\nuse crate::stage::Stage;\nuse craballoc::{\n    prelude::{Hybrid, SlabAllocator, WgpuRuntime},\n    slab::SlabBuffer,\n    value::HybridArray,\n};\nuse crabslab::Id;\nuse glam::{Mat4, UVec2, Vec3, Vec4};\nuse snafu::prelude::*;\n\nuse crate::{\n    atlas::{Atlas, AtlasBlitter, AtlasError},\n    geometry::Geometry,\n    transform::{shader::TransformDescriptor, NestedTransform, Transform},\n};\n\nuse super::shader::{\n    DirectionalLightDescriptor, LightDescriptor, LightStyle, LightingDescriptor,\n    PointLightDescriptor, SpotLightDescriptor,\n};\n\npub use super::{\n    shader::{Candela, Lux},\n    shadow_map::ShadowMap,\n};\n\n#[derive(Debug, Snafu)]\n#[snafu(visibility(pub(crate)))]\npub enum LightingError {\n    #[snafu(display(\"{source}\"))]\n    Atlas { source: AtlasError },\n\n    #[snafu(display(\"Driver poll error: {source}\"))]\n    Poll { source: wgpu::PollError },\n}\n\nimpl From<AtlasError> for LightingError {\n    fn from(source: AtlasError) -> Self {\n        LightingError::Atlas { source }\n    }\n}\n\n/// Describes shared behaviour between all analytical lights.\npub trait IsLight: Clone {\n    /// Return the style of this light.\n    fn style(&self) -> LightStyle;\n\n    fn light_space_transforms(\n        &self,\n        // Another transform applied to the light.\n        parent_transform: &TransformDescriptor,\n        // Near limits of the light's reach\n        //\n        // The maximum should be the `Camera`'s `Frustum::depth()`.\n        // TODO: in `DirectionalLightDescriptor::shadow_mapping_projection_and_view`, take Frustum\n        // as a parameter and then figure out the minimal view projection that includes that\n        // frustum\n        z_near: f32,\n        // Far limits of the light's reach\n        z_far: f32,\n    ) -> Vec<Mat4>;\n}\n\n/// A directional light.\n///\n/// An analitical light that casts light in parallel, infinitely.\n#[derive(Clone, Debug)]\npub struct DirectionalLight {\n    descriptor: Hybrid<DirectionalLightDescriptor>,\n}\n\nimpl IsLight for DirectionalLight {\n    fn style(&self) -> LightStyle {\n        LightStyle::Directional\n    }\n\n    fn light_space_transforms(\n        &self,\n        parent_transform: &TransformDescriptor,\n        z_near: f32,\n        z_far: f32,\n    ) -> Vec<Mat4> {\n        let m = Mat4::from(*parent_transform);\n        vec![{\n            let (p, v) = self\n                .descriptor()\n                .shadow_mapping_projection_and_view(&m, z_near, z_far);\n            p * v\n        }]\n    }\n}\n\nimpl DirectionalLight {\n    /// Returns a pointer to the descriptor data on the GPU slab.\n    pub fn id(&self) -> Id<DirectionalLightDescriptor> {\n        self.descriptor.id()\n    }\n\n    /// Returns the a copy of the descriptor.\n    pub fn descriptor(&self) -> DirectionalLightDescriptor {\n        self.descriptor.get()\n    }\n}\n\n/// A [`DirectionalLight`] comes wrapped in [`AnalyticalLight`], giving the\n/// [`AnalyticalLight`] the ability to simulate sunlight or other lights that\n/// are \"infinitely\" far away.\nimpl AnalyticalLight<DirectionalLight> {\n    /// Set the direction of the directional light.\n    pub fn set_direction(&self, direction: Vec3) -> &Self {\n        self.inner.descriptor.modify(|d| d.direction = direction);\n        self\n    }\n\n    /// Set the direction and return the directional light.\n    pub fn with_direction(self, direction: Vec3) -> Self {\n        self.set_direction(direction);\n        self\n    }\n\n    /// Modify the direction of the directional light.\n    pub fn modify_direction<T: 'static>(&self, f: impl FnOnce(&mut Vec3) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.direction))\n    }\n\n    /// Get the direction of the directional light.\n    pub fn direction(&self) -> Vec3 {\n        self.inner.descriptor.get().direction\n    }\n\n    /// Set the color of the directional light.\n    pub fn set_color(&self, color: Vec4) -> &Self {\n        self.inner.descriptor.modify(|d| d.color = color);\n        self\n    }\n\n    /// Set the color and return the directional light.\n    pub fn with_color(self, color: Vec4) -> Self {\n        self.set_color(color);\n        self\n    }\n\n    /// Modify the color of the directional light.\n    pub fn modify_color<T: 'static>(&self, f: impl FnOnce(&mut Vec4) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.color))\n    }\n\n    /// Get the color of the directional light.\n    pub fn color(&self) -> Vec4 {\n        self.inner.descriptor.get().color\n    }\n\n    /// Set the intensity of the directional light.\n    pub fn set_intensity(&self, intensity: Lux) -> &Self {\n        self.inner.descriptor.modify(|d| d.intensity = intensity);\n        self\n    }\n\n    /// Set the intensity and return the directional light.\n    pub fn with_intensity(self, intensity: Lux) -> Self {\n        self.set_intensity(intensity);\n        self\n    }\n\n    /// Modify the intensity of the directional light.\n    pub fn modify_intensity<T: 'static>(&self, f: impl FnOnce(&mut Lux) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.intensity))\n    }\n\n    /// Get the intensity of the directional light.\n    pub fn intensity(&self) -> Lux {\n        self.inner.descriptor.get().intensity\n    }\n}\n\n/// A point light.\n///\n/// An analytical light that emits light in all directions from a single point.\n#[derive(Clone, Debug)]\npub struct PointLight {\n    descriptor: Hybrid<PointLightDescriptor>,\n}\n\nimpl IsLight for PointLight {\n    fn style(&self) -> LightStyle {\n        LightStyle::Point\n    }\n\n    fn light_space_transforms(\n        &self,\n        t: &TransformDescriptor,\n        // Near limits of the light's reach\n        //\n        // The maximum should be the `Camera`'s `Frustum::depth()`.\n        z_near: f32,\n        // Far limits of the light's reach\n        z_far: f32,\n    ) -> Vec<Mat4> {\n        let m = Mat4::from(*t);\n        let (p, vs) = self\n            .descriptor()\n            .shadow_mapping_projection_and_view_matrices(&m, z_near, z_far);\n        vs.into_iter().map(|v| p * v).collect()\n    }\n}\n\nimpl PointLight {\n    /// Returns a pointer to the descriptor data on the GPU slab.\n    pub fn id(&self) -> Id<PointLightDescriptor> {\n        self.descriptor.id()\n    }\n\n    /// Returns a copy of the descriptor.\n    pub fn descriptor(&self) -> PointLightDescriptor {\n        self.descriptor.get()\n    }\n}\n\n/// A [`PointLight`] comes wrapped in [`AnalyticalLight`], giving the\n/// [`AnalyticalLight`] the ability to simulate lights that\n/// emit from a single point in space and attenuate exponentially with\n/// distance.\nimpl AnalyticalLight<PointLight> {\n    /// Set the position of the point light.\n    pub fn set_position(&self, position: Vec3) -> &Self {\n        self.inner.descriptor.modify(|d| d.position = position);\n        self\n    }\n\n    /// Set the position and return the point light.\n    pub fn with_position(self, position: Vec3) -> Self {\n        self.set_position(position);\n        self\n    }\n\n    /// Modify the position of the point light.\n    pub fn modify_position<T: 'static>(&self, f: impl FnOnce(&mut Vec3) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.position))\n    }\n\n    /// Get the position of the point light.\n    pub fn position(&self) -> Vec3 {\n        self.inner.descriptor.get().position\n    }\n\n    /// Set the color of the point light.\n    pub fn set_color(&self, color: Vec4) -> &Self {\n        self.inner.descriptor.modify(|d| d.color = color);\n        self\n    }\n\n    /// Set the color and return the point light.\n    pub fn with_color(self, color: Vec4) -> Self {\n        self.set_color(color);\n        self\n    }\n\n    /// Modify the color of the point light.\n    pub fn modify_color<T: 'static>(&self, f: impl FnOnce(&mut Vec4) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.color))\n    }\n\n    /// Get the color of the point light.\n    pub fn color(&self) -> Vec4 {\n        self.inner.descriptor.get().color\n    }\n\n    /// Set the intensity of the point light.\n    pub fn set_intensity(&self, intensity: Candela) -> &Self {\n        self.inner.descriptor.modify(|d| d.intensity = intensity);\n        self\n    }\n\n    /// Set the intensity and return the point light.\n    pub fn with_intensity(self, intensity: Candela) -> Self {\n        self.set_intensity(intensity);\n        self\n    }\n\n    /// Modify the intensity of the point light.\n    pub fn modify_intensity<T: 'static>(&self, f: impl FnOnce(&mut Candela) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.intensity))\n    }\n\n    /// Get the intensity of the point light.\n    pub fn intensity(&self) -> Candela {\n        self.inner.descriptor.get().intensity\n    }\n}\n\n/// A spot light.\n///\n/// An analytical light that emits light in a cone shape.\n#[derive(Clone, Debug)]\npub struct SpotLight {\n    descriptor: Hybrid<SpotLightDescriptor>,\n}\n\nimpl IsLight for SpotLight {\n    fn style(&self) -> LightStyle {\n        LightStyle::Spot\n    }\n\n    fn light_space_transforms(\n        &self,\n        t: &TransformDescriptor,\n        // Near limits of the light's reach\n        //\n        // The maximum should be the `Camera`'s `Frustum::depth()`.\n        z_near: f32,\n        // Far limits of the light's reach\n        z_far: f32,\n    ) -> Vec<Mat4> {\n        let m = Mat4::from(*t);\n        vec![{\n            let (p, v) = self\n                .descriptor()\n                .shadow_mapping_projection_and_view(&m, z_near, z_far);\n            p * v\n        }]\n    }\n}\n\nimpl SpotLight {\n    /// Returns a pointer to the descriptor data on the GPU slab.\n    pub fn id(&self) -> Id<SpotLightDescriptor> {\n        self.descriptor.id()\n    }\n\n    /// Returns a copy of the descriptor.\n    pub fn descriptor(&self) -> SpotLightDescriptor {\n        self.descriptor.get()\n    }\n}\n\n/// A [`SpotLight`] comes wrapped in [`AnalyticalLight`], giving the\n/// [`AnalyticalLight`] the ability to simulate lights that\n/// emit from a single point in space in a specific direction, with\n/// a specific spread.\nimpl AnalyticalLight<SpotLight> {\n    /// Set the position of the spot light.\n    pub fn set_position(&self, position: Vec3) -> &Self {\n        self.inner.descriptor.modify(|d| d.position = position);\n        self\n    }\n\n    /// Set the position and return the spot light.\n    pub fn with_position(self, position: Vec3) -> Self {\n        self.set_position(position);\n        self\n    }\n\n    /// Modify the position of the spot light.\n    pub fn modify_position<T: 'static>(&self, f: impl FnOnce(&mut Vec3) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.position))\n    }\n\n    /// Get the position of the spot light.\n    pub fn position(&self) -> Vec3 {\n        self.inner.descriptor.get().position\n    }\n\n    /// Set the direction of the spot light.\n    pub fn set_direction(&self, direction: Vec3) -> &Self {\n        self.inner.descriptor.modify(|d| d.direction = direction);\n        self\n    }\n\n    /// Set the direction and return the spot light.\n    pub fn with_direction(self, direction: Vec3) -> Self {\n        self.set_direction(direction);\n        self\n    }\n\n    /// Modify the direction of the spot light.\n    pub fn modify_direction<T: 'static>(&self, f: impl FnOnce(&mut Vec3) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.direction))\n    }\n\n    /// Get the direction of the spot light.\n    pub fn direction(&self) -> Vec3 {\n        self.inner.descriptor.get().direction\n    }\n\n    /// Set the inner cutoff of the spot light.\n    pub fn set_inner_cutoff(&self, inner_cutoff: f32) -> &Self {\n        self.inner\n            .descriptor\n            .modify(|d| d.inner_cutoff = inner_cutoff);\n        self\n    }\n\n    /// Set the inner cutoff and return the spot light.\n    pub fn with_inner_cutoff(self, inner_cutoff: f32) -> Self {\n        self.set_inner_cutoff(inner_cutoff);\n        self\n    }\n\n    /// Modify the inner cutoff of the spot light.\n    pub fn modify_inner_cutoff<T: 'static>(&self, f: impl FnOnce(&mut f32) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.inner_cutoff))\n    }\n\n    /// Get the inner cutoff of the spot light.\n    pub fn inner_cutoff(&self) -> f32 {\n        self.inner.descriptor.get().inner_cutoff\n    }\n\n    /// Set the outer cutoff of the spot light.\n    pub fn set_outer_cutoff(&self, outer_cutoff: f32) -> &Self {\n        self.inner\n            .descriptor\n            .modify(|d| d.outer_cutoff = outer_cutoff);\n        self\n    }\n\n    /// Set the outer cutoff and return the spot light.\n    pub fn with_outer_cutoff(self, outer_cutoff: f32) -> Self {\n        self.set_outer_cutoff(outer_cutoff);\n        self\n    }\n\n    /// Modify the outer cutoff of the spot light.\n    pub fn modify_outer_cutoff<T: 'static>(&self, f: impl FnOnce(&mut f32) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.outer_cutoff))\n    }\n\n    /// Get the outer cutoff of the spot light.\n    pub fn outer_cutoff(&self) -> f32 {\n        self.inner.descriptor.get().outer_cutoff\n    }\n\n    /// Set the color of the spot light.\n    pub fn set_color(&self, color: Vec4) -> &Self {\n        self.inner.descriptor.modify(|d| d.color = color);\n        self\n    }\n\n    /// Set the color and return the spot light.\n    pub fn with_color(self, color: Vec4) -> Self {\n        self.set_color(color);\n        self\n    }\n\n    /// Modify the color of the spot light.\n    pub fn modify_color<T: 'static>(&self, f: impl FnOnce(&mut Vec4) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.color))\n    }\n\n    /// Get the color of the spot light.\n    pub fn color(&self) -> Vec4 {\n        self.inner.descriptor.get().color\n    }\n\n    /// Set the intensity of the spot light.\n    pub fn set_intensity(&self, intensity: Candela) -> &Self {\n        self.inner.descriptor.modify(|d| d.intensity = intensity);\n        self\n    }\n\n    /// Set the intensity and return the spot light.\n    pub fn with_intensity(self, intensity: Candela) -> Self {\n        self.set_intensity(intensity);\n        self\n    }\n\n    /// Modify the intensity of the spot light.\n    pub fn modify_intensity<T: 'static>(&self, f: impl FnOnce(&mut Candela) -> T) -> T {\n        self.inner.descriptor.modify(|d| f(&mut d.intensity))\n    }\n\n    /// Get the intensity of the spot light.\n    pub fn intensity(&self) -> Candela {\n        self.inner.descriptor.get().intensity\n    }\n}\n\n#[derive(Clone)]\npub enum Light {\n    Directional(DirectionalLight),\n    Point(PointLight),\n    Spot(SpotLight),\n}\n\nimpl From<DirectionalLight> for Light {\n    fn from(light: DirectionalLight) -> Self {\n        Light::Directional(light)\n    }\n}\n\nimpl From<PointLight> for Light {\n    fn from(light: PointLight) -> Self {\n        Light::Point(light)\n    }\n}\n\nimpl From<SpotLight> for Light {\n    fn from(light: SpotLight) -> Self {\n        Light::Spot(light)\n    }\n}\n\nimpl IsLight for Light {\n    fn style(&self) -> LightStyle {\n        match self {\n            Light::Directional(light) => light.style(),\n            Light::Point(light) => light.style(),\n            Light::Spot(light) => light.style(),\n        }\n    }\n\n    fn light_space_transforms(\n        &self,\n        // Another transform applied to the light.\n        parent_transform: &TransformDescriptor,\n        // Near limits of the light's reach\n        //\n        // The maximum should be the `Camera`'s `Frustum::depth()`.\n        z_near: f32,\n        // Far limits of the light's reach\n        z_far: f32,\n    ) -> Vec<Mat4> {\n        match self {\n            Light::Directional(light) => {\n                light.light_space_transforms(parent_transform, z_near, z_far)\n            }\n            Light::Point(light) => light.light_space_transforms(parent_transform, z_near, z_far),\n            Light::Spot(light) => light.light_space_transforms(parent_transform, z_near, z_far),\n        }\n    }\n}\n\n/// A bundle of lighting resources representing one analytical light in a scene.\n///\n/// Create an [`AnalyticalLight`] with:\n/// * [`Stage::new_directional_light`]\n/// * [`Stage::new_point_light`]\n/// * [`Stage::new_spot_light`].\n///\n/// Lights may be added and removed from rendering with [`Stage::add_light`] and\n/// [`Stage::remove_light`].\n/// The GPU resources a light uses will not be released until\n/// [`Stage::remove_light`] is called _and_ the light is dropped.\n#[derive(Clone)]\npub struct AnalyticalLight<T = Light> {\n    /// The generic light descriptor.\n    pub(crate) light_descriptor: Hybrid<LightDescriptor>,\n    /// The specific light.\n    inner: T,\n    /// The light's global transform.\n    ///\n    /// This value lives in the lighting slab.\n    transform: Transform,\n    /// The light's nested transform.\n    ///\n    /// This value comes from the light's node, if it belongs to one.\n    /// This may have been set if this light originated from a GLTF file.\n    /// This value lives on the geometry slab and must be referenced here\n    /// to keep the two in sync, which is required to animate lights.\n    node_transform: Arc<RwLock<Option<NestedTransform>>>,\n}\n\nimpl<T: IsLight> core::fmt::Display for AnalyticalLight<T> {\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        f.write_fmt(format_args!(\n            \"AnalyticalLight type={} light-id={:?} node-nested-transform-global-id:{:?}\",\n            self.inner.style(),\n            self.light_descriptor.id(),\n            self.node_transform\n                .read()\n                .unwrap()\n                .as_ref()\n                .map(|h| h.global_id())\n        ))\n    }\n}\n\nimpl<T: IsLight> IsLight for AnalyticalLight<T> {\n    fn style(&self) -> LightStyle {\n        self.inner.style()\n    }\n\n    fn light_space_transforms(\n        &self,\n        // Another transform applied to the light.\n        parent_transform: &TransformDescriptor,\n        // Near limits of the light's reach\n        //\n        // The maximum should be the `Camera`'s `Frustum::depth()`.\n        // TODO: in `DirectionalLightDescriptor::shadow_mapping_projection_and_view`, take Frustum\n        // as a parameter and then figure out the minimal view projection that includes that\n        // frustum\n        z_near: f32,\n        // Far limits of the light's reach\n        z_far: f32,\n    ) -> Vec<Mat4> {\n        self.inner\n            .light_space_transforms(parent_transform, z_near, z_far)\n    }\n}\n\nimpl AnalyticalLight {\n    /// Returns a reference to the inner `DirectionalLight`, if this light is\n    /// directional.\n    pub fn as_directional(&self) -> Option<&DirectionalLight> {\n        match &self.inner {\n            Light::Directional(light) => Some(light),\n            _ => None,\n        }\n    }\n\n    /// Returns a reference to the inner `PointLight`, if this light is a point\n    /// light.\n    pub fn as_point(&self) -> Option<&PointLight> {\n        match &self.inner {\n            Light::Point(light) => Some(light),\n            _ => None,\n        }\n    }\n\n    /// Returns a reference to the inner `SpotLight`, if this light is a spot\n    /// light.\n    pub fn as_spot(&self) -> Option<&SpotLight> {\n        match &self.inner {\n            Light::Spot(light) => Some(light),\n            _ => None,\n        }\n    }\n}\n\nimpl<T: IsLight> AnalyticalLight<T> {\n    /// Returns a pointer to this light on the GPU\n    pub fn id(&self) -> Id<LightDescriptor> {\n        self.light_descriptor.id()\n    }\n\n    /// Returns a copy of the descriptor on the GPU.\n    pub fn descriptor(&self) -> LightDescriptor {\n        self.light_descriptor.get()\n    }\n\n    /// Link this light to a node's `NestedTransform`.\n    pub fn link_node_transform(&self, transform: &NestedTransform) {\n        *self\n            .node_transform\n            .write()\n            .expect(\"light node_transform write\") = Some(transform.clone());\n    }\n\n    /// Get a reference to the inner light.\n    pub fn inner(&self) -> &T {\n        &self.inner\n    }\n\n    /// Get a reference to the light's global transform.\n    ///\n    /// This value lives in the lighting slab.\n    ///\n    /// ## Note\n    /// If a [`NestedTransform`] has been linked to this light by using\n    /// [`Self::link_node_transform`], the transform returned by this\n    /// function may be overwritten at any point by the given\n    /// [`NestedTransform`].\n    pub fn transform(&self) -> &Transform {\n        &self.transform\n    }\n\n    /// Get a reference to the light's linked global node transform.\n    ///\n    /// ## Note\n    /// The returned transform, if any, is the global transform of a linked\n    /// `NestedTransform`. To change this value, you should do so through\n    /// the `NestedTransform`, which is likely held in the\n    pub fn linked_node_transform(&self) -> Option<NestedTransform> {\n        self.node_transform\n            .read()\n            .unwrap()\n            .as_ref()\n            .map(|t| t.clone())\n    }\n\n    /// Convert this light into a generic light, hiding the specific light type\n    /// that it is.\n    ///\n    /// This is useful if you want to store your lights together.\n    pub fn into_generic(self) -> AnalyticalLight\n    where\n        Light: From<T>,\n    {\n        let AnalyticalLight {\n            light_descriptor,\n            inner,\n            transform,\n            node_transform,\n        } = self;\n        let inner = Light::from(inner);\n        AnalyticalLight {\n            light_descriptor,\n            inner,\n            transform,\n            node_transform,\n        }\n    }\n}\n\n/// Manages lighting for an entire scene.\n#[derive(Clone)]\npub struct Lighting {\n    pub(crate) geometry_slab: SlabAllocator<WgpuRuntime>,\n    pub(crate) light_slab: SlabAllocator<WgpuRuntime>,\n    pub(crate) light_slab_buffer: Arc<RwLock<SlabBuffer<wgpu::Buffer>>>,\n    pub(crate) geometry_slab_buffer: Arc<RwLock<SlabBuffer<wgpu::Buffer>>>,\n    pub(crate) lighting_descriptor: Hybrid<LightingDescriptor>,\n    pub(crate) analytical_lights: Arc<RwLock<Vec<AnalyticalLight>>>,\n    pub(crate) analytical_lights_array: Arc<RwLock<Option<HybridArray<Id<LightDescriptor>>>>>,\n    pub(crate) shadow_map_update_pipeline: Arc<wgpu::RenderPipeline>,\n    pub(crate) shadow_map_update_bindgroup_layout: Arc<wgpu::BindGroupLayout>,\n    pub(crate) shadow_map_update_blitter: AtlasBlitter,\n    pub(crate) shadow_map_atlas: Atlas,\n}\n\npub struct LightingBindGroupLayoutEntries {\n    pub light_slab: wgpu::BindGroupLayoutEntry,\n    pub shadow_map_image: wgpu::BindGroupLayoutEntry,\n    pub shadow_map_sampler: wgpu::BindGroupLayoutEntry,\n}\n\nimpl LightingBindGroupLayoutEntries {\n    pub fn new(starting_binding: u32) -> Self {\n        Self {\n            light_slab: wgpu::BindGroupLayoutEntry {\n                binding: starting_binding,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Buffer {\n                    ty: wgpu::BufferBindingType::Storage { read_only: true },\n                    has_dynamic_offset: false,\n                    min_binding_size: None,\n                },\n                count: None,\n            },\n            shadow_map_image: wgpu::BindGroupLayoutEntry {\n                binding: starting_binding + 1,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Texture {\n                    sample_type: wgpu::TextureSampleType::Float { filterable: false },\n                    view_dimension: wgpu::TextureViewDimension::D2Array,\n                    multisampled: false,\n                },\n                count: None,\n            },\n            shadow_map_sampler: wgpu::BindGroupLayoutEntry {\n                binding: starting_binding + 2,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),\n                count: None,\n            },\n        }\n    }\n}\n\nimpl Lighting {\n    /// Create the atlas used to store all shadow maps.\n    fn create_shadow_map_atlas(\n        light_slab: &SlabAllocator<WgpuRuntime>,\n        size: wgpu::Extent3d,\n    ) -> Atlas {\n        let usage = wgpu::TextureUsages::RENDER_ATTACHMENT\n            | wgpu::TextureUsages::TEXTURE_BINDING\n            | wgpu::TextureUsages::COPY_SRC;\n        Atlas::new(\n            light_slab,\n            size,\n            Some(wgpu::TextureFormat::R32Float),\n            Some(\"shadow-map-atlas\"),\n            Some(usage),\n        )\n    }\n\n    /// Create a new [`Lighting`] manager.\n    pub fn new(atlas_size: wgpu::Extent3d, geometry: &Geometry) -> Self {\n        let runtime = geometry.runtime();\n        let light_slab = SlabAllocator::new(runtime, \"light-slab\", wgpu::BufferUsages::empty());\n        let lighting_descriptor = light_slab.new_value(LightingDescriptor::default());\n        let light_slab_buffer = light_slab.commit();\n        let shadow_map_update_bindgroup_layout: Arc<_> =\n            ShadowMap::create_update_bindgroup_layout(&runtime.device).into();\n        let shadow_map_update_pipeline =\n            ShadowMap::create_update_pipeline(&runtime.device, &shadow_map_update_bindgroup_layout)\n                .into();\n        Self {\n            shadow_map_atlas: Self::create_shadow_map_atlas(&light_slab, atlas_size),\n            analytical_lights: Default::default(),\n            analytical_lights_array: Default::default(),\n            geometry_slab: geometry.slab_allocator().clone(),\n            light_slab,\n            light_slab_buffer: Arc::new(RwLock::new(light_slab_buffer)),\n            lighting_descriptor,\n            geometry_slab_buffer: Arc::new(RwLock::new(geometry.slab_allocator().commit())),\n            shadow_map_update_pipeline,\n            shadow_map_update_bindgroup_layout,\n            shadow_map_update_blitter: AtlasBlitter::new(\n                &runtime.device,\n                wgpu::TextureFormat::R32Float,\n                wgpu::FilterMode::Nearest,\n            ),\n        }\n    }\n\n    pub fn slab_allocator(&self) -> &SlabAllocator<WgpuRuntime> {\n        &self.light_slab\n    }\n\n    /// Add an [`AnalyticalLight`] to the internal list of lights.\n    ///\n    /// This is called implicitly by:\n    ///\n    /// * [`Lighting::new_directional_light`].\n    /// * [`Lighting::new_point_light`].\n    /// * [`Lighting::new_spot_light`].\n    ///\n    /// This can be used to add the light back to the scene after using\n    /// [`Lighting::remove_light`].\n    pub fn add_light<T>(&self, bundle: &AnalyticalLight<T>)\n    where\n        T: IsLight,\n        Light: From<T>,\n    {\n        log::trace!(\n            \"adding light {:?} ({})\",\n            bundle.light_descriptor.id(),\n            bundle.inner.style()\n        );\n        // Update our list of weakly ref'd light bundles\n        self.analytical_lights\n            .write()\n            .expect(\"analytical_lights write\")\n            .push(bundle.clone().into_generic());\n        // Invalidate the array of lights\n        *self\n            .analytical_lights_array\n            .write()\n            .expect(\"analytical_lights_array write\") = None;\n    }\n\n    /// Remove an [`AnalyticalLight`] from the internal list of lights.\n    ///\n    /// Use this to exclude a light from rendering, without dropping the light.\n    ///\n    /// After calling this function you can include the light again using\n    /// [`Lighting::add_light`].\n    pub fn remove_light<T: IsLight>(&self, bundle: &AnalyticalLight<T>) {\n        log::trace!(\n            \"removing light {:?} ({})\",\n            bundle.light_descriptor.id(),\n            bundle.inner.style()\n        );\n        // Remove the light from the list of weakly ref'd light bundles\n        let mut guard = self\n            .analytical_lights\n            .write()\n            .expect(\"analytical_lights write\");\n        guard.retain(|stored_light| {\n            stored_light.light_descriptor.id() != bundle.light_descriptor.id()\n        });\n        *self\n            .analytical_lights_array\n            .write()\n            .expect(\"analytical_lights_array write\") = None;\n    }\n\n    /// Return an iterator over all lights.\n    pub fn lights(&self) -> Vec<AnalyticalLight> {\n        self.analytical_lights\n            .read()\n            .expect(\"analytical_lights read\")\n            .clone()\n    }\n\n    /// Create a new [`AnalyticalLight<DirectionalLight>`].\n    ///\n    /// The light is automatically added with [`Lighting::add_light`].\n    pub fn new_directional_light(&self) -> AnalyticalLight<DirectionalLight> {\n        let descriptor = self\n            .light_slab\n            .new_value(DirectionalLightDescriptor::default());\n        let transform = Transform::new(&self.light_slab);\n        let light_descriptor = self.light_slab.new_value({\n            let mut light = LightDescriptor::from(descriptor.id());\n            light.transform_id = transform.id();\n            light\n        });\n\n        let bundle = AnalyticalLight {\n            light_descriptor,\n            inner: DirectionalLight { descriptor },\n            transform,\n            node_transform: Default::default(),\n        };\n        self.add_light(&bundle);\n\n        bundle\n    }\n\n    /// Create a new [`AnalyticalLight<PointLight>`].\n    ///\n    /// The light is automatically added with [`Lighting::add_light`].\n    pub fn new_point_light(&self) -> AnalyticalLight<PointLight> {\n        let descriptor = self.light_slab.new_value(PointLightDescriptor::default());\n        let transform = Transform::new(&self.light_slab);\n        let light_descriptor = self.light_slab.new_value({\n            let mut light = LightDescriptor::from(descriptor.id());\n            light.transform_id = transform.id();\n            light\n        });\n\n        let bundle = AnalyticalLight {\n            light_descriptor,\n            inner: PointLight { descriptor },\n            transform,\n            node_transform: Default::default(),\n        };\n        self.add_light(&bundle);\n\n        bundle\n    }\n\n    /// Create a new [`AnalyticalLight<SpotLight>`].\n    ///\n    /// The light is automatically added with [`Lighting::add_light`].\n    pub fn new_spot_light(&self) -> AnalyticalLight<SpotLight> {\n        let descriptor = self.light_slab.new_value(SpotLightDescriptor::default());\n        let transform = Transform::new(&self.light_slab);\n        let light_descriptor = self.light_slab.new_value({\n            let mut light = LightDescriptor::from(descriptor.id());\n            light.transform_id = transform.id();\n            light\n        });\n\n        let bundle = AnalyticalLight {\n            light_descriptor,\n            inner: SpotLight { descriptor },\n            transform,\n            node_transform: Default::default(),\n        };\n        self.add_light(&bundle);\n\n        bundle\n    }\n\n    /// Set the global ambient light color and intensity.\n    ///\n    /// XYZ components are the RGB color, W is the intensity.\n    /// The ambient term is added to the final shaded color, modulated by\n    /// the surface albedo and ambient occlusion.\n    ///\n    /// Defaults to `Vec4::ZERO` (no ambient contribution).\n    pub fn set_ambient_color(&self, color: Vec4) {\n        self.lighting_descriptor.modify(|d| d.ambient_color = color);\n    }\n\n    /// Get the current global ambient light color and intensity.\n    pub fn ambient_color(&self) -> Vec4 {\n        self.lighting_descriptor.get().ambient_color\n    }\n\n    /// Enable shadow mapping for the given [`AnalyticalLight`], creating\n    /// a new [`ShadowMap`].\n    pub fn new_shadow_map<T>(\n        &self,\n        analytical_light_bundle: &AnalyticalLight<T>,\n        // Size of the shadow map\n        size: UVec2,\n        // Distance to the near plane of the shadow map's frustum.\n        //\n        // Only objects within the shadow map's frustum will cast shadows.\n        z_near: f32,\n        // Distance to the far plane of the shadow map's frustum\n        //\n        // Only objects within the shadow map's frustum will cast shadows.\n        z_far: f32,\n    ) -> Result<ShadowMap, LightingError>\n    where\n        T: IsLight,\n        Light: From<T>,\n    {\n        ShadowMap::new(self, analytical_light_bundle, size, z_near, z_far)\n    }\n\n    #[must_use]\n    pub fn commit(&self) -> SlabBuffer<wgpu::Buffer> {\n        log::trace!(\"committing lights\");\n\n        // Sync any lights whose node transforms have changed\n        for light in self\n            .analytical_lights\n            .read()\n            .expect(\"analytical_lights read\")\n            .iter()\n        {\n            if let Some(node_transform) = light\n                .node_transform\n                .read()\n                .expect(\"light node_transform read\")\n                .as_ref()\n            {\n                let global_node_transform = node_transform.global_descriptor();\n                if node_transform.global_descriptor() != light.transform.descriptor() {\n                    light.transform.set_descriptor(global_node_transform);\n                }\n            }\n        }\n\n        let lights_array = {\n            let mut array_guard = self\n                .analytical_lights_array\n                .write()\n                .expect(\"analytical_lights_array write\");\n\n            // Create a new array if lights have been invalidated by\n            // `Lighting::add_light` or `Lighting::remove_light`\n            array_guard\n                .get_or_insert_with(|| {\n                    log::trace!(\"  analytical lights array was invalidated\");\n                    let lights_guard = self\n                        .analytical_lights\n                        .read()\n                        .expect(\"analytical_lights read\");\n                    let new_lights = lights_guard\n                        .iter()\n                        .map(|bundle| bundle.light_descriptor.id());\n                    let array = self.light_slab.new_array(new_lights);\n                    log::trace!(\"  lights array is now: {:?}\", array.array());\n                    array\n                })\n                .array()\n        };\n\n        self.lighting_descriptor.modify(\n            |LightingDescriptor {\n                 analytical_lights_array,\n                 shadow_map_atlas_descriptor_id,\n                 update_shadow_map_id,\n                 update_shadow_map_texture_index,\n                 // Don't change the tiling descriptor\n                 light_tiling_descriptor_id: _,\n                 // Don't change the ambient color (set via set_ambient_color)\n                 ambient_color: _,\n             }| {\n                *analytical_lights_array = lights_array;\n                *shadow_map_atlas_descriptor_id = self.shadow_map_atlas.descriptor_id();\n                *update_shadow_map_id = Id::NONE;\n                *update_shadow_map_texture_index = 0;\n            },\n        );\n\n        let buffer = self.light_slab.commit();\n        log::trace!(\"  light slab creation time: {}\", buffer.creation_time());\n        buffer\n    }\n}\n\n#[cfg(test)]\nmod test;\n"
  },
  {
    "path": "crates/renderling/src/light/shader.rs",
    "content": "//! Shader functions for the lighting system.\n//!\n//! Directional lights are in lux, spot and point lights are in\n//! candelas. Conversion happens in the [PBR shader during radiance\n//! accumulation](crate::pbr::shader::shade_fragment).\n//!\n//! More info is here\n//! <https://www.realtimerendering.com/blog/physical-units-for-lights>.\n//!\n//! ## Note\n//!\n//! The glTF spec [1] says directional light is in lux, whereas spot and point\n//! are in candelas. The same goes for this library's shaders, but not a ton of\n//! work has gone into verifying that conversion from these units into\n//! radiometric units is accurate _in any way_. The shaders roughly do a\n//! conversion by dividing by 683 [2] or some other constant involving 683 [3].\n//!\n//! More work needs to be done here. PRs would be very appreciated.\n//!\n//! [1]: https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_lights_punctual/README.md\n//! [2]: https://depts.washington.edu/mictech/optics/me557/Radiometry.pdf\n//! [3]: https://projects.blender.org/blender/blender-addons/commit/9d903a93f03b\nuse crabslab::{Array, Id, Slab, SlabItem};\nuse glam::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};\n#[cfg(gpu)]\nuse spirv_std::num_traits::Float;\nuse spirv_std::{spirv, Image};\n\nuse crate::{\n    atlas::shader::{AtlasDescriptor, AtlasTextureDescriptor},\n    cubemap::shader::{CubemapDescriptor, CubemapFaceDirection},\n    geometry::shader::GeometryDescriptor,\n    math::{Fetch, IsSampler, IsVector, Sample2dArray},\n    primitive::shader::{PrimitiveDescriptor, VertexInfo},\n    transform::shader::TransformDescriptor,\n};\n\n#[derive(Clone, Copy, Default, SlabItem, core::fmt::Debug)]\n#[offsets]\npub struct LightingDescriptor {\n    /// List of all analytical lights in the scene.\n    pub analytical_lights_array: Array<Id<LightDescriptor>>,\n    /// Shadow mapping atlas info.\n    pub shadow_map_atlas_descriptor_id: Id<AtlasDescriptor>,\n    /// `Id` of the [`ShadowMapDescriptor`] to use when updating\n    /// a shadow map.\n    ///\n    /// This changes from each run of the `shadow_mapping_vertex`.\n    pub update_shadow_map_id: Id<ShadowMapDescriptor>,\n    /// The index of the shadow map atlas texture to update.\n    pub update_shadow_map_texture_index: u32,\n    /// `Id` of the [`LightTilingDescriptor`] to use when performing\n    /// light tiling.\n    pub light_tiling_descriptor_id: Id<LightTilingDescriptor>,\n    /// Global ambient light color and intensity.\n    ///\n    /// XYZ components are the RGB color, W is the intensity.\n    /// The ambient term is added to the final shaded color,\n    /// modulated by the surface albedo and ambient occlusion.\n    ///\n    /// Defaults to `Vec4::ZERO` (no ambient contribution).\n    pub ambient_color: Vec4,\n}\n\n#[derive(Clone, Copy, SlabItem, core::fmt::Debug)]\npub struct ShadowMapDescriptor {\n    pub light_space_transforms_array: Array<Mat4>,\n    /// Near plane of the projection matrix\n    pub z_near: f32,\n    /// Far plane of the projection matrix\n    pub z_far: f32,\n    /// Pointers to the atlas textures where the shadow map depth\n    /// data is stored.\n    ///\n    /// This will be an array of one `Id` for directional and spot lights,\n    /// and an array of four `Id`s for a point light.\n    pub atlas_textures_array: Array<Id<AtlasTextureDescriptor>>,\n    pub bias_min: f32,\n    pub bias_max: f32,\n    pub pcf_samples: u32,\n}\n\nimpl Default for ShadowMapDescriptor {\n    fn default() -> Self {\n        Self {\n            light_space_transforms_array: Default::default(),\n            z_near: Default::default(),\n            z_far: Default::default(),\n            atlas_textures_array: Default::default(),\n            bias_min: 0.0005,\n            bias_max: 0.005,\n            pcf_samples: 4,\n        }\n    }\n}\n\n#[cfg(test)]\n#[derive(Default, Debug, Clone, Copy, PartialEq)]\npub struct ShadowMappingVertexInfo {\n    pub renderlet_id: Id<PrimitiveDescriptor>,\n    pub vertex_index: u32,\n    pub vertex: crate::geometry::Vertex,\n    pub transform: TransformDescriptor,\n    pub model_matrix: Mat4,\n    pub world_pos: Vec3,\n    pub view_projection: Mat4,\n    pub clip_pos: Vec4,\n}\n\n/// Shadow mapping vertex shader.\n///\n/// It is assumed that a [`LightingDescriptor`] is stored at `Id(0)` of the\n/// `light_slab`.\n///\n/// This shader reads the [`LightingDescriptor`] to find the shadow map to\n/// be updated, then determines the clip positions to emit based on the\n/// shadow map's atlas texture.\n///\n/// It then renders the renderlet into the designated atlas frame.\n// Note:\n// If this is taking too long to render for each renderlet, think about\n// a frustum and occlusion culling pass to generate the list of renderlets.\n#[spirv(vertex)]\n#[allow(clippy::too_many_arguments)]\npub fn shadow_mapping_vertex(\n    // Points at a `Renderlet`\n    #[spirv(instance_index)] renderlet_id: Id<PrimitiveDescriptor>,\n    // Which vertex within the renderlet are we rendering\n    #[spirv(vertex_index)] vertex_index: u32,\n    // The slab where the renderlet's geometry is staged\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32],\n    // The slab where the scene's lighting data is staged\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] light_slab: &[u32],\n\n    #[spirv(position)] out_clip_pos: &mut Vec4,\n    #[cfg(test)] out_comparison_info: &mut ShadowMappingVertexInfo,\n) {\n    let renderlet = geometry_slab.read_unchecked(renderlet_id);\n    if !renderlet.visible {\n        // put it outside the clipping frustum\n        *out_clip_pos = Vec4::new(100.0, 100.0, 100.0, 1.0);\n        return;\n    }\n\n    let VertexInfo {\n        world_pos,\n        vertex: _vertex,\n        transform: _transform,\n        model_matrix: _model_matrix,\n    } = renderlet.get_vertex_info(vertex_index, geometry_slab);\n\n    let lighting_desc = light_slab.read_unchecked(Id::<LightingDescriptor>::new(0));\n    let shadow_desc = light_slab.read_unchecked(lighting_desc.update_shadow_map_id);\n    let light_space_transform_id = shadow_desc\n        .light_space_transforms_array\n        .at(lighting_desc.update_shadow_map_texture_index as usize);\n    let light_space_transform = light_slab.read_unchecked(light_space_transform_id);\n    let clip_pos = light_space_transform * world_pos.extend(1.0);\n    #[cfg(test)]\n    {\n        *out_comparison_info = ShadowMappingVertexInfo {\n            renderlet_id,\n            vertex_index,\n            vertex: _vertex,\n            transform: _transform,\n            model_matrix: _model_matrix,\n            world_pos,\n            view_projection: light_space_transform,\n            clip_pos,\n        };\n    }\n    *out_clip_pos = clip_pos;\n}\n\n#[spirv(fragment)]\npub fn shadow_mapping_fragment(clip_pos: Vec4, frag_color: &mut Vec4) {\n    *frag_color = (clip_pos.xyz() / clip_pos.w).extend(1.0);\n}\n\n/// Contains values needed to determine the outgoing radiance of a fragment.\n///\n/// For more info, see the **Spotlight** section of the\n/// [learnopengl](https://learnopengl.com/Lighting/Light-casters)\n/// article.\n#[derive(Clone, Copy, Default, core::fmt::Debug)]\npub struct SpotLightCalculation {\n    /// Position of the light in world space\n    pub light_position: Vec3,\n    /// Position of the fragment in world space\n    pub frag_position: Vec3,\n    /// Unit vector (LightDir) pointing from the fragment to the light\n    pub frag_to_light: Vec3,\n    /// Distance from the fragment to the light\n    pub frag_to_light_distance: f32,\n    /// Unit vector (SpotDir) direction that the light is pointing in\n    pub light_direction: Vec3,\n    /// The cosine of the cutoff angle (Phi ϕ) that specifies the spotlight's\n    /// radius.\n    ///\n    /// Everything inside this angle is lit by the spotlight.\n    pub cos_inner_cutoff: f32,\n    /// The cosine of the cutoff angle (Gamma γ) that specifies the spotlight's\n    /// outer radius.\n    ///\n    /// Everything outside this angle is not lit by the spotlight.\n    ///\n    /// Fragments between `inner_cutoff` and `outer_cutoff` have an intensity\n    /// between `1.0` and `0.0`.\n    pub cos_outer_cutoff: f32,\n    /// Whether the fragment is inside the `inner_cutoff` cone.\n    pub fragment_is_inside_inner_cone: bool,\n    /// Whether the fragment is inside the `outer_cutoff` cone.\n    pub fragment_is_inside_outer_cone: bool,\n    /// `outer_cutoff` - `inner_cutoff`\n    pub epsilon: f32,\n    /// Cosine of the angle (Theta θ) between `frag_to_light` (LightDir) vector\n    /// and the `light_direction` (SpotDir) vector.\n    ///\n    /// θ  should be smaller than `outer_cutoff` (Gamma γ) to be\n    /// inside the spotlight, but since these are all cosines of angles, we\n    /// actually compare using `>`.\n    pub cos_theta: f32,\n    pub contribution_unclamped: f32,\n    /// The intensity level between `0.0` and `1.0` that should be used to\n    /// determine outgoing radiance.\n    pub contribution: f32,\n}\n\nimpl SpotLightCalculation {\n    /// Calculate the values required to determine outgoing radiance of a spot\n    /// light.\n    pub fn new(\n        spot_light_descriptor: SpotLightDescriptor,\n        node_transform: Mat4,\n        fragment_world_position: Vec3,\n    ) -> Self {\n        let light_position = node_transform.transform_point3(spot_light_descriptor.position);\n        let frag_position = fragment_world_position;\n        let frag_to_light = light_position - frag_position;\n        let frag_to_light_distance = frag_to_light.length();\n        if frag_to_light_distance == 0.0 {\n            crate::println!(\"frag_to_light_distance: {frag_to_light_distance}\");\n            return Self::default();\n        }\n        let frag_to_light = frag_to_light.alt_norm_or_zero();\n        let light_direction = node_transform\n            .transform_vector3(spot_light_descriptor.direction)\n            .alt_norm_or_zero();\n        let cos_inner_cutoff = spot_light_descriptor.inner_cutoff.cos();\n        let cos_outer_cutoff = spot_light_descriptor.outer_cutoff.cos();\n        let epsilon = cos_inner_cutoff - cos_outer_cutoff;\n        let cos_theta = frag_to_light.dot(-light_direction);\n        let fragment_is_inside_inner_cone = cos_theta > cos_inner_cutoff;\n        let fragment_is_inside_outer_cone = cos_theta > cos_outer_cutoff;\n        let contribution_unclamped = (cos_theta - cos_outer_cutoff) / epsilon;\n        let contribution = contribution_unclamped.clamp(0.0, 1.0);\n        Self {\n            light_position,\n            frag_position,\n            frag_to_light,\n            frag_to_light_distance,\n            light_direction,\n            cos_inner_cutoff,\n            cos_outer_cutoff,\n            fragment_is_inside_inner_cone,\n            fragment_is_inside_outer_cone,\n            epsilon,\n            cos_theta,\n            contribution_unclamped,\n            contribution,\n        }\n    }\n}\n\n/// Description of a spot light.\n///\n/// ## Tips\n///\n/// If your spotlight is not illuminating your scenery, ensure that the\n/// `inner_cutoff` and `outer_cutoff` values are \"correct\". `outer_cutoff`\n/// should be _greater than_ `inner_cutoff` and the values should be a large\n/// enough to cover at least one pixel at the distance between the light and\n/// the scenery.\n#[repr(C)]\n#[derive(Copy, Clone, SlabItem, core::fmt::Debug)]\npub struct SpotLightDescriptor {\n    // TODO: add `range` to SpotLightDescriptor\n    // See <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_lights_punctual/README.md#light-shared-properties>\n    pub position: Vec3,\n    pub direction: Vec3,\n    pub inner_cutoff: f32,\n    pub outer_cutoff: f32,\n    pub color: Vec4,\n    pub intensity: Candela,\n}\n\nimpl Default for SpotLightDescriptor {\n    fn default() -> Self {\n        let white = Vec4::splat(1.0);\n        let inner_cutoff = 0.077143565;\n        let outer_cutoff = 0.09075713;\n        let direction = Vec3::new(0.0, -1.0, 0.0);\n        let color = white;\n        let intensity = Candela::COMMON_WAX_CANDLE;\n\n        Self {\n            position: Default::default(),\n            direction,\n            inner_cutoff,\n            outer_cutoff,\n            color,\n            intensity,\n        }\n    }\n}\n\nimpl SpotLightDescriptor {\n    pub fn shadow_mapping_projection_and_view(\n        &self,\n        parent_light_transform: &Mat4,\n        z_near: f32,\n        z_far: f32,\n    ) -> (Mat4, Mat4) {\n        let fovy = 2.0 * self.outer_cutoff;\n        let aspect = 1.0;\n        let projection = Mat4::perspective_rh(fovy, aspect, z_near, z_far);\n        let direction = parent_light_transform\n            .transform_vector3(self.direction)\n            .alt_norm_or_zero();\n        let position = parent_light_transform.transform_point3(self.position);\n        let up = direction.orthonormal_vectors()[0];\n        let view = Mat4::look_to_rh(position, direction, up);\n        (projection, view)\n    }\n}\n\n/// A unit of luminous intensity, lumen per steradian (lm/sr).\n///\n/// Candelas measure the luminous power per unit solid angle emitted in a\n/// particular direction. A common wax candle has a luminous intensity of\n/// roughly 1 candela.\n///\n/// The type provides a collection of const `Candela` lighting levels for use in\n/// constructing analytical lights.\n#[repr(transparent)]\n#[derive(Clone, Copy, Default, Debug, SlabItem)]\npub struct Candela(pub f32);\n\nimpl core::fmt::Display for Candela {\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        self.0.fmt(f)?;\n        if self.0 == 1.0 {\n            f.write_str(\" candela (lm/sr)\")\n        } else {\n            f.write_str(\" candelas (lm/sr)\")\n        }\n    }\n}\n\nimpl Candela {\n    pub const COMMON_WAX_CANDLE: Self = Candela(1.0);\n}\n\n/// A unit of illuminance, lumen per meter squared (lm/m^2).\n///\n/// Lux measures the amount of light that falls on a surface, which is\n/// appropriate for directional lights, as they simulate light coming from a\n/// specific direction, like sunlight.\n///\n/// The type provides a collection of const Lux lighting levels for use in\n/// constructing analytical lights.\n#[repr(transparent)]\n#[derive(Clone, Copy, Default, Debug, SlabItem)]\npub struct Lux(pub f32);\n\nimpl core::fmt::Display for Lux {\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        self.0.fmt(f)?;\n        f.write_str(\" lux (lm/m^2)\")\n    }\n}\n\nimpl Lux {\n    pub const OUTDOOR_TWILIGHT: Self = Lux(1.0);\n    pub const OUTDOOR_STREET_LIGHT_MIN: Self = Lux(5.0);\n    pub const OUTDOOR_SUNSET: Self = Lux(10.0);\n    pub const INDOOR_LOUNGE: Self = Lux(50.0);\n    pub const INDOOR_HALLWAY: Self = Lux(80.0);\n    pub const OUTDOOR_OVERCAST_LOW: Self = Lux(100.0);\n    pub const INDOOR_OFFICE_LOW: Self = Lux(320.0);\n    pub const OUTDOOR_SUNRISE_OR_SUNSET: Self = Lux(400.0);\n    pub const INDOOR_OFFICE_HIGH: Self = Lux(500.0);\n    pub const OUTDOOR_OVERCAST_HIGH: Self = Lux(1000.0);\n    pub const OUTDOOR_FOXS_WEDDING: Self = Lux(3000.0);\n    pub const OUTDOOR_FULL_DAYLIGHT_LOW: Self = Lux(10_000.0);\n    pub const OUTDOOR_FULL_DAYLIGHT_HIGH: Self = Lux(25_000.0);\n    pub const OUTDOOR_DIRECT_SUNLIGHT_LOW: Self = Lux(32_000.0);\n    pub const OUTDOOR_DIRECT_SUNLIGHT_HIGH: Self = Lux(130_000.0);\n}\n\n#[repr(C)]\n#[derive(Copy, Clone, SlabItem, Debug)]\npub struct DirectionalLightDescriptor {\n    pub direction: Vec3,\n    pub color: Vec4,\n    /// Intensity of the directional light in lux (lm/m^2).\n    pub intensity: Lux,\n}\n\nimpl Default for DirectionalLightDescriptor {\n    fn default() -> Self {\n        let direction = Vec3::new(0.0, -1.0, 0.0);\n        let color = Vec4::splat(1.0);\n        let intensity = Lux::OUTDOOR_TWILIGHT;\n\n        Self {\n            direction,\n            color,\n            intensity,\n        }\n    }\n}\n\nimpl DirectionalLightDescriptor {\n    pub fn shadow_mapping_projection_and_view(\n        &self,\n        parent_light_transform: &Mat4,\n        // Near limits of the light's reach\n        //\n        // The maximum should be the `Camera`'s `Frustum::depth()`.\n        // TODO: in `DirectionalLightDescriptor::shadow_mapping_projection_and_view`, take Frustum\n        // as a parameter and then figure out the minimal view projection that includes that\n        // frustum\n        z_near: f32,\n        // Far limits of the light's reach\n        z_far: f32,\n    ) -> (Mat4, Mat4) {\n        crate::println!(\"descriptor: {self:#?}\");\n        let depth = (z_far - z_near).abs();\n        let hd = depth * 0.5;\n        let projection = Mat4::orthographic_rh(-hd, hd, -hd, hd, z_near, z_far);\n        let direction = parent_light_transform\n            .transform_vector3(self.direction)\n            .alt_norm_or_zero();\n        let position = -direction * depth * 0.5;\n        crate::println!(\"direction: {direction}\");\n        crate::println!(\"position: {position}\");\n        let up = direction.orthonormal_vectors()[0];\n        let view = Mat4::look_to_rh(position, direction, up);\n        (projection, view)\n    }\n}\n\n#[repr(C)]\n#[derive(Copy, Clone, SlabItem, core::fmt::Debug)]\npub struct PointLightDescriptor {\n    // TODO: add `range` to PointLightDescriptor\n    // See <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_lights_punctual/README.md#light-shared-properties>\n    pub position: Vec3,\n    pub color: Vec4,\n    /// Intensity as candelas.\n    pub intensity: Candela,\n}\n\nimpl Default for PointLightDescriptor {\n    fn default() -> Self {\n        let color = Vec4::splat(1.0);\n        let intensity = Candela::COMMON_WAX_CANDLE;\n\n        Self {\n            position: Default::default(),\n            color,\n            intensity,\n        }\n    }\n}\n\nimpl PointLightDescriptor {\n    pub fn shadow_mapping_view_matrix(\n        &self,\n        face_index: usize,\n        parent_light_transform: &Mat4,\n    ) -> Mat4 {\n        let eye = parent_light_transform.transform_point3(self.position);\n        let mut face = CubemapFaceDirection::FACES[face_index];\n        face.eye = eye;\n        face.view()\n    }\n\n    pub fn shadow_mapping_projection_matrix(z_near: f32, z_far: f32) -> Mat4 {\n        Mat4::perspective_lh(core::f32::consts::FRAC_PI_2, 1.0, z_near, z_far)\n    }\n\n    pub fn shadow_mapping_projection_and_view_matrices(\n        &self,\n        parent_light_transform: &Mat4,\n        z_near: f32,\n        z_far: f32,\n    ) -> (Mat4, [Mat4; 6]) {\n        let p = Self::shadow_mapping_projection_matrix(z_near, z_far);\n        let eye = parent_light_transform.transform_point3(self.position);\n        (\n            p,\n            CubemapFaceDirection::FACES.map(|mut face| {\n                face.eye = eye;\n                face.view()\n            }),\n        )\n    }\n}\n\n/// Returns the radius of illumination in meters.\n///\n/// * Moonlight: < 1 lux.\n///   - Full moon on a clear night: 0.25 lux.\n///   - Quarter moon: 0.01 lux\n///   - Starlight overcast moonless night sky: 0.0001 lux.\n/// * General indoor lighting: Around 100 to 300 lux.\n/// * Office lighting: Typically around 300 to 500 lux.\n/// * Reading or task lighting: Around 500 to 750 lux.\n/// * Detailed work (e.g., drafting, surgery): 1000 lux or more.\npub fn radius_of_illumination(intensity_candelas: f32, minimum_illuminance_lux: f32) -> f32 {\n    (intensity_candelas / minimum_illuminance_lux).sqrt()\n}\n\n#[repr(u32)]\n#[derive(Copy, Clone, PartialEq, core::fmt::Debug)]\npub enum LightStyle {\n    Directional = 0,\n    Point = 1,\n    Spot = 2,\n}\n\nimpl core::fmt::Display for LightStyle {\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        match self {\n            LightStyle::Directional => f.write_str(\"directional\"),\n            LightStyle::Point => f.write_str(\"point\"),\n            LightStyle::Spot => f.write_str(\"spot\"),\n        }\n    }\n}\n\nimpl SlabItem for LightStyle {\n    const SLAB_SIZE: usize = { 1 };\n\n    fn read_slab(index: usize, slab: &[u32]) -> Self {\n        let proxy = u32::read_slab(index, slab);\n        match proxy {\n            0 => LightStyle::Directional,\n            1 => LightStyle::Point,\n            2 => LightStyle::Spot,\n            _ => LightStyle::Directional,\n        }\n    }\n\n    fn write_slab(&self, index: usize, slab: &mut [u32]) -> usize {\n        let proxy = *self as u32;\n        proxy.write_slab(index, slab)\n    }\n}\n\n/// A generic light that is used as a slab pointer to a\n/// specific light type.\n#[repr(C)]\n#[derive(Copy, Clone, PartialEq, SlabItem, core::fmt::Debug)]\npub struct LightDescriptor {\n    /// The type of the light\n    pub light_type: LightStyle,\n    /// The index of the light in the lighting slab\n    pub index: u32,\n    /// The id of a transform to apply to the position and direction of the\n    /// light.\n    ///\n    /// This `Id` points to a transform on the lighting slab.\n    ///\n    /// The value of this descriptor can be synchronized with that of a node\n    /// transform on the geometry slab from\n    /// [`crate::light::AnalyticalLight::link_node_transform`].\n    pub transform_id: Id<TransformDescriptor>,\n    /// The id of the shadow map in use by this light.\n    pub shadow_map_desc_id: Id<ShadowMapDescriptor>,\n}\n\nimpl Default for LightDescriptor {\n    fn default() -> Self {\n        Self {\n            light_type: LightStyle::Directional,\n            index: Id::<()>::NONE.inner(),\n            transform_id: Id::NONE,\n            shadow_map_desc_id: Id::NONE,\n        }\n    }\n}\n\nimpl From<Id<DirectionalLightDescriptor>> for LightDescriptor {\n    fn from(id: Id<DirectionalLightDescriptor>) -> Self {\n        Self {\n            light_type: LightStyle::Directional,\n            index: id.inner(),\n            transform_id: Id::NONE,\n            shadow_map_desc_id: Id::NONE,\n        }\n    }\n}\n\nimpl From<Id<SpotLightDescriptor>> for LightDescriptor {\n    fn from(id: Id<SpotLightDescriptor>) -> Self {\n        Self {\n            light_type: LightStyle::Spot,\n            index: id.inner(),\n            transform_id: Id::NONE,\n            shadow_map_desc_id: Id::NONE,\n        }\n    }\n}\n\nimpl From<Id<PointLightDescriptor>> for LightDescriptor {\n    fn from(id: Id<PointLightDescriptor>) -> Self {\n        Self {\n            light_type: LightStyle::Point,\n            index: id.inner(),\n            transform_id: Id::NONE,\n            shadow_map_desc_id: Id::NONE,\n        }\n    }\n}\n\nimpl LightDescriptor {\n    pub fn into_directional_id(self) -> Id<DirectionalLightDescriptor> {\n        Id::from(self.index)\n    }\n\n    pub fn into_spot_id(self) -> Id<SpotLightDescriptor> {\n        Id::from(self.index)\n    }\n\n    pub fn into_point_id(self) -> Id<PointLightDescriptor> {\n        Id::from(self.index)\n    }\n}\n\n/// Parameters to the shadow mapping calculation function.\n///\n/// This is mostly just to appease clippy.\npub struct ShadowCalculation {\n    pub shadow_map_desc: ShadowMapDescriptor,\n    pub shadow_map_atlas_size: UVec2,\n    pub surface_normal_in_world_space: Vec3,\n    pub frag_pos_in_world_space: Vec3,\n    pub frag_to_light_in_world_space: Vec3,\n    pub bias_min: f32,\n    pub bias_max: f32,\n    pub pcf_samples: u32,\n}\n\nimpl ShadowCalculation {\n    /// Reads various required parameters from the slab and creates a\n    /// `ShadowCalculation`.\n    pub fn new(\n        light_slab: &[u32],\n        light: LightDescriptor,\n        in_pos: Vec3,\n        surface_normal: Vec3,\n        light_direction: Vec3,\n    ) -> Self {\n        let shadow_map_desc = light_slab.read_unchecked(light.shadow_map_desc_id);\n        let atlas_size = {\n            let lighting_desc_id = Id::<LightingDescriptor>::new(0);\n            let atlas_desc_id = light_slab.read_unchecked(\n                lighting_desc_id + LightingDescriptor::OFFSET_OF_SHADOW_MAP_ATLAS_DESCRIPTOR_ID,\n            );\n            let atlas_desc = light_slab.read_unchecked(atlas_desc_id);\n            atlas_desc.size\n        };\n\n        ShadowCalculation {\n            shadow_map_desc,\n            shadow_map_atlas_size: atlas_size.xy(),\n            surface_normal_in_world_space: surface_normal,\n            frag_pos_in_world_space: in_pos,\n            frag_to_light_in_world_space: light_direction,\n            bias_min: shadow_map_desc.bias_min,\n            bias_max: shadow_map_desc.bias_max,\n            pcf_samples: shadow_map_desc.pcf_samples,\n        }\n    }\n\n    fn get_atlas_texture_at(&self, light_slab: &[u32], index: usize) -> AtlasTextureDescriptor {\n        let atlas_texture_id =\n            light_slab.read_unchecked(self.shadow_map_desc.atlas_textures_array.at(index));\n        light_slab.read_unchecked(atlas_texture_id)\n    }\n\n    fn get_frag_pos_in_light_space(&self, light_slab: &[u32], index: usize) -> Vec3 {\n        let light_space_transform_id = self.shadow_map_desc.light_space_transforms_array.at(index);\n        let light_space_transform = light_slab.read_unchecked(light_space_transform_id);\n        light_space_transform.project_point3(self.frag_pos_in_world_space)\n    }\n\n    /// Returns shadow _intensity_ for directional and spot lights.\n    ///\n    /// Returns `0.0` when the fragment is in full light.\n    /// Returns `1.0` when the fragment is in full shadow.\n    pub fn run_directional_or_spot<T, S>(\n        &self,\n        light_slab: &[u32],\n        shadow_map: &T,\n        shadow_map_sampler: &S,\n    ) -> f32\n    where\n        S: IsSampler,\n        T: Sample2dArray<Sampler = S>,\n    {\n        let ShadowCalculation {\n            shadow_map_desc: _,\n            shadow_map_atlas_size,\n            frag_pos_in_world_space: _,\n            surface_normal_in_world_space: surface_normal,\n            frag_to_light_in_world_space: light_direction,\n            bias_min,\n            bias_max,\n            pcf_samples,\n        } = self;\n        let frag_pos_in_light_space = self.get_frag_pos_in_light_space(light_slab, 0);\n        crate::println!(\"frag_pos_in_light_space: {frag_pos_in_light_space}\");\n        if !crate::math::is_inside_clip_space(frag_pos_in_light_space.xyz()) {\n            return 0.0;\n        }\n        // The range of coordinates in the light's clip space is -1.0 to 1.0 for x and\n        // y, but the texture space is [0, 1], and Y increases downward, so we\n        // do this conversion to flip Y and also normalize to the range [0.0,\n        // 1.0]. Z should already be 0.0 to 1.0.\n        let proj_coords_uv = (frag_pos_in_light_space.xy() * Vec2::new(1.0, -1.0)\n            + Vec2::splat(1.0))\n            * Vec2::splat(0.5);\n        crate::println!(\"proj_coords_uv: {proj_coords_uv}\");\n\n        let shadow_map_atlas_texture = self.get_atlas_texture_at(light_slab, 0);\n        // With these projected coordinates we can sample the depth map as the\n        // resulting [0,1] coordinates from proj_coords directly correspond to\n        // the transformed NDC coordinates from the `ShadowMap::update` render pass.\n        // This gives us the closest depth from the light's point of view:\n        let pcf_samples_2 = *pcf_samples as i32 / 2;\n        let texel_size = 1.0\n            / Vec2::new(\n                shadow_map_atlas_texture.size_px.x as f32,\n                shadow_map_atlas_texture.size_px.y as f32,\n            );\n        let mut shadow = 0.0f32;\n        let mut total = 0.0f32;\n        for x in -pcf_samples_2..=pcf_samples_2 {\n            for y in -pcf_samples_2..=pcf_samples_2 {\n                let proj_coords = shadow_map_atlas_texture.uv(\n                    proj_coords_uv + Vec2::new(x as f32, y as f32) * texel_size,\n                    *shadow_map_atlas_size,\n                );\n                let shadow_map_depth = shadow_map\n                    .sample_by_lod(*shadow_map_sampler, proj_coords, 0.0)\n                    .x;\n                // To get the current depth at this fragment we simply retrieve the projected\n                // vector's z coordinate which equals the depth of this fragment\n                // from the light's perspective.\n                let fragment_depth = frag_pos_in_light_space.z;\n\n                // If the `current_depth`, which is the depth of the fragment from the lights\n                // POV, is greater than the `closest_depth` of the shadow map at\n                // that fragment, the fragment is in shadow\n                crate::println!(\"current_depth: {fragment_depth}\");\n                crate::println!(\"closest_depth: {shadow_map_depth}\");\n                let bias = (bias_max * (1.0 - surface_normal.dot(*light_direction))).max(*bias_min);\n\n                if (fragment_depth - bias) >= shadow_map_depth {\n                    shadow += 1.0\n                }\n                total += 1.0;\n            }\n        }\n        shadow / total.max(1.0)\n    }\n\n    pub const POINT_SAMPLE_OFFSET_DIRECTIONS: [Vec3; 21] = [\n        Vec3::ZERO,\n        Vec3::new(1.0, 1.0, 1.0),\n        Vec3::new(1.0, -1.0, 1.0),\n        Vec3::new(-1.0, -1.0, 1.0),\n        Vec3::new(-1.0, 1.0, 1.0),\n        Vec3::new(1.0, 1.0, -1.0),\n        Vec3::new(1.0, -1.0, -1.0),\n        Vec3::new(-1.0, -1.0, -1.0),\n        Vec3::new(-1.0, 1.0, -1.0),\n        Vec3::new(1.0, 1.0, 0.0),\n        Vec3::new(1.0, -1.0, 0.0),\n        Vec3::new(-1.0, -1.0, 0.0),\n        Vec3::new(-1.0, 1.0, 0.0),\n        Vec3::new(1.0, 0.0, 1.0),\n        Vec3::new(-1.0, 0.0, 1.0),\n        Vec3::new(1.0, 0.0, -1.0),\n        Vec3::new(-1.0, 0.0, -1.0),\n        Vec3::new(0.0, 1.0, 1.0),\n        Vec3::new(0.0, -1.0, 1.0),\n        Vec3::new(0.0, -1.0, -1.0),\n        Vec3::new(0.0, 1.0, -1.0),\n    ];\n    /// Returns shadow _intensity_ for point lights.\n    ///\n    /// Returns `0.0` when the fragment is in full light.\n    /// Returns `1.0` when the fragment is in full shadow.\n    pub fn run_point<T, S>(\n        &self,\n        light_slab: &[u32],\n        shadow_map: &T,\n        shadow_map_sampler: &S,\n        light_pos_in_world_space: Vec3,\n    ) -> f32\n    where\n        S: IsSampler,\n        T: Sample2dArray<Sampler = S>,\n    {\n        let ShadowCalculation {\n            shadow_map_desc,\n            shadow_map_atlas_size,\n            frag_pos_in_world_space,\n            surface_normal_in_world_space: surface_normal,\n            frag_to_light_in_world_space: frag_to_light,\n            bias_min,\n            bias_max,\n            pcf_samples,\n        } = self;\n\n        let light_to_frag_dir = frag_pos_in_world_space - light_pos_in_world_space;\n        crate::println!(\"light_to_frag_dir: {light_to_frag_dir}\");\n\n        let pcf_samplesf = (*pcf_samples as f32)\n            .max(1.0)\n            .min(Self::POINT_SAMPLE_OFFSET_DIRECTIONS.len() as f32);\n        let pcf_samples = pcf_samplesf as usize;\n        let view_distance = light_to_frag_dir.length();\n        let disk_radius = (1.0 + view_distance / shadow_map_desc.z_far) / 25.0;\n        let mut shadow = 0.0f32;\n        for i in 0..pcf_samples {\n            let sample_offset = Self::POINT_SAMPLE_OFFSET_DIRECTIONS[i] * disk_radius;\n            crate::println!(\"sample_offset: {sample_offset}\");\n            let sample_dir = (light_to_frag_dir + sample_offset).alt_norm_or_zero();\n            let (face_index, uv) = CubemapDescriptor::get_face_index_and_uv(sample_dir);\n            crate::println!(\"face_index: {face_index}\",);\n            crate::println!(\"uv: {uv}\");\n            let frag_pos_in_light_space = self.get_frag_pos_in_light_space(light_slab, face_index);\n            let face_texture = self.get_atlas_texture_at(light_slab, face_index);\n            let uv_tex = face_texture.uv(uv, *shadow_map_atlas_size);\n            let shadow_map_depth = shadow_map.sample_by_lod(*shadow_map_sampler, uv_tex, 0.0).x;\n            let fragment_depth = frag_pos_in_light_space.z;\n            let bias = (bias_max * (1.0 - surface_normal.dot(*frag_to_light))).max(*bias_min);\n            if (fragment_depth - bias) > shadow_map_depth {\n                shadow += 1.0\n            }\n        }\n\n        shadow / pcf_samplesf\n    }\n}\n\n/// Depth pre-pass for the light tiling feature.\n///\n/// This shader writes all staged [`PrimitiveDescriptor`]'s depth into a buffer.\n///\n/// This shader is very much like [`shadow_mapping_vertex`], except that\n/// shader gets its projection+view matrix from the light stored in a\n/// `ShadowMapDescriptor`.\n///\n/// Here we want to render as normal forward pass would, with the\n/// `PrimitiveDescriptor` and the `Camera`'s view projection matrix.\n/// ## Note\n/// This shader will likely be expanded to include parts of occlusion culling\n/// and order independent transparency.\n#[spirv(vertex)]\npub fn light_tiling_depth_pre_pass(\n    // Points at a `Renderlet`.\n    #[spirv(instance_index)] renderlet_id: Id<PrimitiveDescriptor>,\n    // Which vertex within the renderlet are we rendering?\n    #[spirv(vertex_index)] vertex_index: u32,\n    // The slab where the renderlet's geometry is staged\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32],\n    // Output clip coords\n    #[spirv(position)] out_clip_pos: &mut Vec4,\n) {\n    let renderlet = geometry_slab.read_unchecked(renderlet_id);\n    if !renderlet.visible {\n        // put it outside the clipping frustum\n        *out_clip_pos = Vec3::splat(100.0).extend(1.0);\n        return;\n    }\n\n    let camera_id = geometry_slab\n        .read_unchecked(Id::<GeometryDescriptor>::new(0) + GeometryDescriptor::OFFSET_OF_CAMERA_ID);\n    let camera = geometry_slab.read_unchecked(camera_id);\n\n    let VertexInfo { world_pos, .. } = renderlet.get_vertex_info(vertex_index, geometry_slab);\n\n    *out_clip_pos = camera.view_projection() * world_pos.extend(1.0);\n}\n\npub type DepthImage2d = Image!(2D, type=f32, sampled, depth);\n\npub type DepthImage2dMultisampled = Image!(2D, type=f32, sampled, depth, multisampled=true);\n\n/// A tile of screen space used to cull lights.\n#[derive(Clone, Copy, Default, SlabItem)]\n#[offsets]\npub struct LightTile {\n    /// Minimum depth of objects found within the frustum of the tile.\n    pub depth_min: u32,\n    /// Maximum depth of objects foudn within the frustum of the tile.\n    pub depth_max: u32,\n    /// The count of lights in this tile.\n    ///\n    /// Also, the next available light index.\n    pub next_light_index: u32,\n    /// List of light ids that intersect this tile's frustum.\n    pub lights_array: Array<Id<LightDescriptor>>,\n}\n\nimpl core::fmt::Debug for LightTile {\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        f.debug_struct(\"LightTile\")\n            .field(\"depth_min\", &dequantize_depth_u32_to_f32(self.depth_min))\n            .field(\"depth_max\", &dequantize_depth_u32_to_f32(self.depth_max))\n            .field(\"next_light_index\", &self.next_light_index)\n            .field(\"lights_array\", &self.lights_array)\n            .finish()\n    }\n}\n\n/// Descriptor of the light tiling operation, which culls lights by accumulating\n/// them into lists that illuminate tiles of the screen.\n#[derive(Clone, Copy, SlabItem, core::fmt::Debug)]\npub struct LightTilingDescriptor {\n    /// Size of the [`Stage`](crate::stage::Stage)'s depth texture.\n    pub depth_texture_size: UVec2,\n    /// Configurable tile size.\n    pub tile_size: u32,\n    /// Array pointing to the lighting \"tiles\".\n    pub tiles_array: Array<LightTile>,\n    /// Minimum illuminance.\n    ///\n    /// Used to determine whether a light illuminates a tile.\n    pub minimum_illuminance_lux: f32,\n}\n\nimpl Default for LightTilingDescriptor {\n    fn default() -> Self {\n        Self {\n            depth_texture_size: Default::default(),\n            tile_size: 16,\n            tiles_array: Default::default(),\n            minimum_illuminance_lux: 0.1,\n        }\n    }\n}\n\nimpl LightTilingDescriptor {\n    /// Returns the dimensions of the grid of tiles.\n    pub fn tile_grid_size(&self) -> UVec2 {\n        let dims_f32 = self.depth_texture_size.as_vec2() / self.tile_size as f32;\n        dims_f32.ceil().as_uvec2()\n    }\n\n    pub fn tile_coord_for_fragment(&self, frag_coord: Vec2) -> UVec2 {\n        let frag_coord = frag_coord.as_uvec2();\n        frag_coord / self.tile_size\n    }\n\n    pub fn tile_index_for_fragment(&self, frag_coord: Vec2) -> usize {\n        let tile_coord = self.tile_coord_for_fragment(frag_coord);\n        let tile_dimensions = self.tile_grid_size();\n        (tile_coord.y * tile_dimensions.x + tile_coord.x) as usize\n    }\n}\n\n/// Quantizes a fragment depth from `f32` to `u32`.\npub fn quantize_depth_f32_to_u32(depth: f32) -> u32 {\n    (u32::MAX as f32 * depth).round() as u32\n}\n\n/// Reconstructs a previously quantized depth from a `u32`.\npub fn dequantize_depth_u32_to_f32(depth: u32) -> f32 {\n    depth as f32 / u32::MAX as f32\n}\n\n/// Helper for determining the next light to check during an\n/// invocation of the light list computation.\npub(crate) struct NextLightIndex {\n    current_step: usize,\n    tile_size: u32,\n    lights: Array<Id<LightDescriptor>>,\n    global_id: UVec3,\n}\n\nimpl Iterator for NextLightIndex {\n    type Item = Id<Id<LightDescriptor>>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        let next_index = self.next_index();\n        self.current_step += 1;\n        if next_index < self.lights.len() {\n            Some(self.lights.at(next_index))\n        } else {\n            None\n        }\n    }\n}\n\nimpl NextLightIndex {\n    pub fn new(\n        global_id: UVec3,\n        tile_size: u32,\n        analytical_lights_array: Array<Id<LightDescriptor>>,\n    ) -> Self {\n        Self {\n            current_step: 0,\n            tile_size,\n            lights: analytical_lights_array,\n            global_id,\n        }\n    }\n\n    pub fn next_index(&self) -> usize {\n        // Determine the xy coord of this invocation within the _tile_\n        let frag_tile_xy = self.global_id.xy() % self.tile_size;\n        // Determine the index of this invocation within the _tile_\n        let offset = frag_tile_xy.y * self.tile_size + frag_tile_xy.x;\n        let stride = (self.tile_size * self.tile_size) as usize;\n        self.current_step * stride + offset as usize\n    }\n}\n\nstruct LightTilingInvocation {\n    global_id: UVec3,\n    descriptor: LightTilingDescriptor,\n}\n\nimpl LightTilingInvocation {\n    fn new(global_id: UVec3, descriptor: LightTilingDescriptor) -> Self {\n        Self {\n            global_id,\n            descriptor,\n        }\n    }\n\n    /// The fragment's position.\n    ///\n    /// X range is 0 to (width - 1), Y range is 0 to (height - 1).\n    fn frag_pos(&self) -> UVec2 {\n        self.global_id.xy()\n    }\n\n    /// The number of tiles in X and Y within the depth texture.\n    fn tile_grid_size(&self) -> UVec2 {\n        self.descriptor.tile_grid_size()\n    }\n\n    /// The tile's coordinate among all tiles in the tile grid.\n    ///\n    /// The units are in tile x y.\n    fn tile_coord(&self) -> UVec2 {\n        self.global_id.xy() / self.descriptor.tile_size\n    }\n\n    /// The tile's index in all the [`LightTilingDescriptor`]'s `tile_array`.\n    fn tile_index(&self) -> usize {\n        let tile_pos = self.tile_coord();\n        let tile_dimensions = self.tile_grid_size();\n        (tile_pos.y * tile_dimensions.x + tile_pos.x) as usize\n    }\n\n    /// The tile's normalized midpoint.\n    fn tile_ndc_midpoint(&self) -> Vec2 {\n        let min_coord = self.tile_coord().as_vec2();\n        let mid_coord = min_coord + 0.5;\n        crate::math::convert_pixel_to_ndc(mid_coord, self.tile_grid_size())\n    }\n\n    /// Compute the min and max depth of one fragment/invocation for light\n    /// tiling.\n    ///\n    /// The min and max is stored in a tile on lighting slab.\n    fn compute_min_and_max_depth(\n        &self,\n        depth_texture: &impl Fetch<UVec2, Output = Vec4>,\n        lighting_slab: &mut [u32],\n    ) {\n        let frag_pos = self.frag_pos();\n        let depth_texture_size = self.descriptor.depth_texture_size;\n        if frag_pos.x >= depth_texture_size.x || frag_pos.y >= depth_texture_size.y {\n            return;\n        }\n        // Depth frag value at the fragment position\n        let frag_depth: f32 = depth_texture.fetch(frag_pos).x;\n        // Fragment depth scaled to min/max of u32 values\n        //\n        // This is so we can compare with normal atomic ops instead of using the float\n        // extension\n        let frag_depth_u32 = quantize_depth_f32_to_u32(frag_depth);\n\n        // The tile's index in all the tiles\n        let tile_index = self.tile_index();\n        let lighting_desc = lighting_slab.read_unchecked(Id::<LightingDescriptor>::new(0));\n        let tiling_desc = lighting_slab.read_unchecked(lighting_desc.light_tiling_descriptor_id);\n        // index of the tile's min depth atomic value in the lighting slab\n        let tile_id = tiling_desc.tiles_array.at(tile_index);\n        let min_depth_index = tile_id + LightTile::OFFSET_OF_DEPTH_MIN;\n        // index of the tile's max depth atomic value in the lighting slab\n        let max_depth_index = tile_id + LightTile::OFFSET_OF_DEPTH_MAX;\n\n        let _prev_min_depth = crate::sync::atomic_u_min::<\n            { spirv_std::memory::Scope::Workgroup as u32 },\n            { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() },\n        >(lighting_slab, min_depth_index, frag_depth_u32);\n        let _prev_max_depth = crate::sync::atomic_u_max::<\n            { spirv_std::memory::Scope::Workgroup as u32 },\n            { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() },\n        >(lighting_slab, max_depth_index, frag_depth_u32);\n    }\n\n    /// Determine whether this invocation should run.\n    fn should_invoke(&self) -> bool {\n        self.global_id.x < self.descriptor.depth_texture_size.x\n            && self.global_id.y < self.descriptor.depth_texture_size.y\n    }\n\n    /// Clears one tile.\n    ///\n    /// ## Note\n    /// This is only valid to call from the [`light_tiling_clear_tiles`] shader.\n    fn clear_tile(&self, lighting_slab: &mut [u32]) {\n        let dimensions = self.tile_grid_size();\n        let index = (self.global_id.y * dimensions.x + self.global_id.x) as usize;\n        if index < self.descriptor.tiles_array.len() {\n            let tile_id = self.descriptor.tiles_array.at(index);\n            let mut tile = lighting_slab.read(tile_id);\n            tile.depth_min = u32::MAX;\n            tile.depth_max = 0;\n            tile.next_light_index = 0;\n            lighting_slab.write(tile_id, &tile);\n            // Zero out the light list and the ratings\n            for id in tile.lights_array.iter() {\n                lighting_slab.write(id, &Id::NONE);\n            }\n        }\n    }\n\n    // The difficulty here is that in SPIRV we can access `lighting_slab` atomically\n    // without wrapping it in a type, but on CPU we must pass an array of\n    // (something like) `AtomicU32`. I'm not sure how to model this interaction\n    // to test it on the CPU.\n    fn compute_light_lists(&self, geometry_slab: &[u32], lighting_slab: &mut [u32]) {\n        let index = self.tile_index();\n        let tile_id = self.descriptor.tiles_array.at(index);\n        // Construct the tile's frustum in clip space.\n        let depth_min_u32 = lighting_slab.read_unchecked(tile_id + LightTile::OFFSET_OF_DEPTH_MIN);\n        let depth_max_u32 = lighting_slab.read_unchecked(tile_id + LightTile::OFFSET_OF_DEPTH_MAX);\n        let depth_min = dequantize_depth_u32_to_f32(depth_min_u32);\n        let depth_max = dequantize_depth_u32_to_f32(depth_max_u32);\n\n        if depth_min == depth_max {\n            // If we would construct a frustum with zero volume, abort.\n            //\n            // See <http://renderling.xyz/articles/live/light_tiling.html#zero-volume-frustum-optimization>\n            // for more info.\n            return;\n        }\n\n        let camera_id = geometry_slab.read_unchecked(\n            Id::<GeometryDescriptor>::new(0) + GeometryDescriptor::OFFSET_OF_CAMERA_ID,\n        );\n        let camera = geometry_slab.read_unchecked(camera_id);\n\n        // let (ndc_tile_min, ndc_tile_max) = self.tile_ndc_min_max();\n        // // This is the AABB frustum, in NDC coords\n        // let ndc_tile_aabb = Aabb::new(\n        //     ndc_tile_min.extend(depth_min),\n        //     ndc_tile_max.extend(depth_max),\n        // );\n\n        // Get the frustum (here simplified to a line) in world coords, since we'll be\n        // using it to compare against the radius of illumination of each light\n        let tile_ndc_midpoint = self.tile_ndc_midpoint();\n        let tile_line_ndc = (\n            tile_ndc_midpoint.extend(depth_min),\n            tile_ndc_midpoint.extend(depth_max),\n        );\n        let inverse_viewproj = camera.view_projection().inverse();\n        let tile_line = (\n            inverse_viewproj.project_point3(tile_line_ndc.0),\n            inverse_viewproj.project_point3(tile_line_ndc.1),\n        );\n\n        let tile_index = self.tile_index();\n        let tile_id = self.descriptor.tiles_array.at(tile_index);\n        let tile_lights_array = lighting_slab.read(tile_id + LightTile::OFFSET_OF_LIGHTS_ARRAY);\n        let next_light_id = tile_id + LightTile::OFFSET_OF_NEXT_LIGHT_INDEX;\n\n        // List of all analytical lights in the scene\n        let analytical_lights_array = lighting_slab.read_unchecked(\n            Id::<LightingDescriptor>::new(0)\n                + LightingDescriptor::OFFSET_OF_ANALYTICAL_LIGHTS_ARRAY,\n        );\n\n        // Each invocation will calculate a few lights' contribution to the tile, until\n        // all lights have been visited\n        let next_light = NextLightIndex::new(\n            self.global_id,\n            self.descriptor.tile_size,\n            analytical_lights_array,\n        );\n        for id_of_light_id in next_light {\n            let light_id = lighting_slab.read_unchecked(id_of_light_id);\n            let light = lighting_slab.read_unchecked(light_id);\n            let transform = lighting_slab.read(light.transform_id);\n            // Get the distance to the light in world coords, and the\n            // intensity of the light.\n            let (distance, intensity_candelas) = match light.light_type {\n                LightStyle::Directional => {\n                    let directional_light = lighting_slab.read(light.into_directional_id());\n                    // Very hand-wavey conversion\n                    (0.0, directional_light.intensity.0 / 683.0)\n                }\n                LightStyle::Point => {\n                    let point_light = lighting_slab.read(light.into_point_id());\n                    let center = Mat4::from(transform).transform_point3(point_light.position);\n                    let distance = crate::math::distance_to_line(center, tile_line.0, tile_line.1);\n                    // Again, very hand-wavey\n                    (distance, point_light.intensity.0 / 683.0)\n                }\n                LightStyle::Spot => {\n                    // TODO: take into consideration the direction the spot light is pointing\n                    let spot_light = lighting_slab.read(light.into_spot_id());\n                    let center = Mat4::from(transform).transform_point3(spot_light.position);\n                    let distance = crate::math::distance_to_line(center, tile_line.0, tile_line.1);\n                    // Again, very hand-wavey\n                    (distance, spot_light.intensity.0 / 683.0)\n                }\n            };\n\n            let radius =\n                radius_of_illumination(intensity_candelas, self.descriptor.minimum_illuminance_lux);\n            let should_add = radius >= distance;\n            if should_add {\n                // If the light should be added to the bin, get the next available index in the\n                // bin, then write the id of the light into that index.\n                let next_index = crate::sync::atomic_i_increment::<\n                    { spirv_std::memory::Scope::Workgroup as u32 },\n                    { spirv_std::memory::Semantics::WORKGROUP_MEMORY.bits() },\n                >(lighting_slab, next_light_id);\n                if next_index as usize >= tile_lights_array.len() {\n                    // We've already filled the bin, so abort.\n                    //\n                    // TODO: Figure out a better way to handle light tile list overrun.\n                    break;\n                } else {\n                    // Get the id that corresponds to the next available index in the ratings bin\n                    let binned_light_id = tile_lights_array.at(next_index as usize);\n                    // Write to that location\n                    lighting_slab.write(binned_light_id, &light_id);\n                }\n            }\n        }\n    }\n}\n\n#[spirv(compute(threads(16, 16, 1)))]\npub fn light_tiling_clear_tiles(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32],\n    #[spirv(global_invocation_id)] global_id: UVec3,\n) {\n    let lighting_descriptor = lighting_slab.read(Id::<LightingDescriptor>::new(0));\n    let light_tiling_descriptor =\n        lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id);\n    let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor);\n    invocation.clear_tile(lighting_slab);\n}\n\n/// Compute the min and max depth value for a tile.\n///\n/// This shader must be called **once for each fragment in the depth texture**.\n#[spirv(compute(threads(16, 16, 1)))]\npub fn light_tiling_compute_tile_min_and_max_depth(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32],\n    #[spirv(descriptor_set = 0, binding = 2)] depth_texture: &DepthImage2d,\n    #[spirv(global_invocation_id)] global_id: UVec3,\n) {\n    let lighting_descriptor = lighting_slab.read(Id::<LightingDescriptor>::new(0));\n    let light_tiling_descriptor =\n        lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id);\n    let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor);\n    invocation.compute_min_and_max_depth(depth_texture, lighting_slab);\n}\n\n/// Compute the min and max depth value for a tile, multisampled.\n///\n/// This shader must be called **once for each fragment in the depth texture**.\n#[spirv(compute(threads(16, 16, 1)))]\npub fn light_tiling_compute_tile_min_and_max_depth_multisampled(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32],\n    #[spirv(descriptor_set = 0, binding = 2)] depth_texture: &DepthImage2dMultisampled,\n    #[spirv(global_invocation_id)] global_id: UVec3,\n) {\n    let lighting_descriptor = lighting_slab.read(Id::<LightingDescriptor>::new(0));\n    let light_tiling_descriptor =\n        lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id);\n    let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor);\n    invocation.compute_min_and_max_depth(depth_texture, lighting_slab);\n}\n\n#[spirv(compute(threads(16, 16, 1)))]\npub fn light_tiling_bin_lights(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32],\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] lighting_slab: &mut [u32],\n    #[spirv(global_invocation_id)] global_id: UVec3,\n) {\n    let lighting_descriptor = lighting_slab.read(Id::<LightingDescriptor>::new(0));\n    let light_tiling_descriptor =\n        lighting_slab.read(lighting_descriptor.light_tiling_descriptor_id);\n    let invocation = LightTilingInvocation::new(global_id, light_tiling_descriptor);\n    if invocation.should_invoke() {\n        invocation.compute_light_lists(geometry_slab, lighting_slab);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/light/shadow_map.rs",
    "content": "//! Shadow mapping.\n\nuse core::{ops::Deref, sync::atomic::AtomicUsize};\nuse std::sync::Arc;\n\nuse craballoc::{\n    prelude::Hybrid,\n    value::{HybridArray, HybridWriteGuard},\n};\nuse crabslab::Id;\nuse glam::{Mat4, UVec2};\nuse snafu::ResultExt;\n\nuse crate::{\n    atlas::{shader::AtlasTextureDescriptor, AtlasBlittingOperation, AtlasImage, AtlasTexture},\n    bindgroup::ManagedBindGroup,\n    light::{IsLight, Light},\n    primitive::Primitive,\n};\n\nuse super::{\n    shader::{LightStyle, ShadowMapDescriptor},\n    AnalyticalLight, Lighting, LightingError, PollSnafu,\n};\n\n/// Projects shadows from a single light source for specific objects.\n///\n/// A `ShadowMap` is essentially a depth map rendering of the scene from one\n/// light's point of view. We use this rendering of the scene to determine if\n/// an object lies in shadow.\n///\n/// To create a new [`ShadowMap`], use\n/// [`Stage::new_shadow_map`](crate::stage::Stage::new_shadow_map).\n#[derive(Clone)]\npub struct ShadowMap {\n    /// Last time the stage slab was bound.\n    pub(crate) stage_slab_buffer_creation_time: Arc<AtomicUsize>,\n    /// Last time the light slab was bound.\n    pub(crate) light_slab_buffer_creation_time: Arc<AtomicUsize>,\n    /// This shadow map's light transform,\n    pub(crate) shadowmap_descriptor: Hybrid<ShadowMapDescriptor>,\n    /// This shadow map's transforms.\n    ///\n    /// Directional and spot lights have 1, point lights\n    /// have 6.\n    pub(crate) light_space_transforms: HybridArray<Mat4>,\n    /// Bindgroup for the shadow map update shader\n    pub(crate) update_bindgroup: ManagedBindGroup,\n    pub(crate) atlas_textures: Vec<AtlasTexture>,\n    pub(crate) _atlas_textures_array: HybridArray<Id<AtlasTextureDescriptor>>,\n    pub(crate) update_texture: crate::texture::Texture,\n    pub(crate) blitting_op: AtlasBlittingOperation,\n    pub(crate) light_bundle: AnalyticalLight,\n}\n\nimpl ShadowMap {\n    const LABEL: Option<&str> = Some(\"shadow-map\");\n\n    pub fn create_update_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {\n        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: Self::LABEL,\n            entries: &[\n                wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::VERTEX,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n                wgpu::BindGroupLayoutEntry {\n                    binding: 1,\n                    visibility: wgpu::ShaderStages::VERTEX,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n            ],\n        })\n    }\n\n    pub fn create_update_pipeline(\n        device: &wgpu::Device,\n        bindgroup_layout: &wgpu::BindGroupLayout,\n    ) -> wgpu::RenderPipeline {\n        let shadow_map_update_layout =\n            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n                label: ShadowMap::LABEL,\n                bind_group_layouts: &[bindgroup_layout],\n                push_constant_ranges: &[],\n            });\n        let shadow_map_update_vertex = crate::linkage::shadow_mapping_vertex::linkage(device);\n        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n            label: Self::LABEL,\n            layout: Some(&shadow_map_update_layout),\n            vertex: wgpu::VertexState {\n                module: &shadow_map_update_vertex.module,\n                entry_point: Some(shadow_map_update_vertex.entry_point),\n                compilation_options: wgpu::PipelineCompilationOptions::default(),\n                buffers: &[],\n            },\n            primitive: wgpu::PrimitiveState {\n                topology: wgpu::PrimitiveTopology::TriangleList,\n                strip_index_format: None,\n                front_face: wgpu::FrontFace::Ccw,\n                cull_mode: Some(wgpu::Face::Front),\n                unclipped_depth: false,\n                polygon_mode: wgpu::PolygonMode::Fill,\n                conservative: false,\n            },\n            depth_stencil: Some(wgpu::DepthStencilState {\n                format: wgpu::TextureFormat::Depth32Float,\n                depth_write_enabled: true,\n                depth_compare: wgpu::CompareFunction::Less,\n                stencil: wgpu::StencilState::default(),\n                bias: wgpu::DepthBiasState::default(),\n            }),\n            multisample: wgpu::MultisampleState::default(),\n            fragment: None,\n            multiview: None,\n            cache: None,\n        })\n    }\n\n    /// Create the bindgroup for the shadow map update shader.\n    fn create_update_bindgroup(\n        device: &wgpu::Device,\n        bindgroup_layout: &wgpu::BindGroupLayout,\n        geometry_slab_buffer: &wgpu::Buffer,\n        light_slab_buffer: &wgpu::Buffer,\n    ) -> wgpu::BindGroup {\n        device.create_bind_group(&wgpu::BindGroupDescriptor {\n            label: Self::LABEL,\n            layout: bindgroup_layout,\n            entries: &[\n                wgpu::BindGroupEntry {\n                    binding: 0,\n                    resource: wgpu::BindingResource::Buffer(\n                        geometry_slab_buffer.as_entire_buffer_binding(),\n                    ),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 1,\n                    resource: wgpu::BindingResource::Buffer(\n                        light_slab_buffer.as_entire_buffer_binding(),\n                    ),\n                },\n            ],\n        })\n    }\n\n    /// Returns the [`Id`] of the inner [`ShadowMapDescriptor`].\n    pub fn descriptor_id(&self) -> Id<ShadowMapDescriptor> {\n        self.shadowmap_descriptor.id()\n    }\n\n    /// Returns a guard on the inner [`ShadowMapDescriptor`].\n    ///\n    /// Use this to update descriptor values before calling `ShadowMap::update`.\n    pub fn descriptor_lock(&self) -> HybridWriteGuard<'_, ShadowMapDescriptor> {\n        self.shadowmap_descriptor.lock()\n    }\n\n    /// Enable shadow mapping for the given [`AnalyticalLight`], creating\n    /// a new [`ShadowMap`].\n    pub fn new<T>(\n        lighting: &Lighting,\n        analytical_light_bundle: &AnalyticalLight<T>,\n        // Size of the shadow map\n        size: UVec2,\n        // Distance to the shadow map frustum's near plane\n        z_near: f32,\n        // Distance to the shadow map frustum's far plane\n        z_far: f32,\n    ) -> Result<Self, LightingError>\n    where\n        T: IsLight,\n        Light: From<T>,\n    {\n        let stage_slab_buffer = lighting\n            .geometry_slab_buffer\n            .read()\n            .expect(\"geometry_slab_buffer read\");\n        let is_point_light = analytical_light_bundle.style() == LightStyle::Point;\n        let count = if is_point_light { 6 } else { 1 };\n        let atlas = &lighting.shadow_map_atlas;\n        let image = AtlasImage::new(size, crate::atlas::AtlasImageFormat::R32FLOAT);\n        // UNWRAP: safe because we know there's one in here\n        let atlas_textures = atlas.add_images(vec![&image; count])?;\n        let atlas_len = atlas.len();\n        // Regardless of light type, we only create one depth texture,\n        // but that texture may be of layer 1 or 6\n        let label = format!(\"shadow-map-{atlas_len}\");\n        let update_texture = crate::texture::Texture::create_depth_texture_for_shadow_map(\n            atlas.device(),\n            size.x,\n            size.y,\n            1,\n            Some(&label),\n            is_point_light,\n        );\n        let atlas_textures_array = lighting\n            .light_slab\n            .new_array(atlas_textures.iter().map(|t| t.id()));\n        let blitting_op = AtlasBlittingOperation::new(\n            &lighting.shadow_map_update_blitter,\n            atlas,\n            if is_point_light { 6 } else { 1 },\n        );\n        let light_space_transforms =\n            lighting\n                .light_slab\n                .new_array(analytical_light_bundle.light_space_transforms(\n                    &analytical_light_bundle.transform().descriptor(),\n                    z_near,\n                    z_far,\n                ));\n        let shadowmap_descriptor = lighting.light_slab.new_value(ShadowMapDescriptor {\n            light_space_transforms_array: light_space_transforms.array(),\n            z_near,\n            z_far,\n            atlas_textures_array: atlas_textures_array.array(),\n            bias_min: 0.0005,\n            bias_max: 0.005,\n            pcf_samples: 4,\n        });\n        // Set the descriptor in the light, so the shader knows to use it\n        analytical_light_bundle.light_descriptor.modify(|light| {\n            light.shadow_map_desc_id = shadowmap_descriptor.id();\n        });\n        let light_slab_buffer = lighting.commit();\n        let update_bindgroup = ManagedBindGroup::from(ShadowMap::create_update_bindgroup(\n            lighting.light_slab.device(),\n            &lighting.shadow_map_update_bindgroup_layout,\n            stage_slab_buffer.deref(),\n            &light_slab_buffer,\n        ));\n\n        Ok(ShadowMap {\n            stage_slab_buffer_creation_time: Arc::new(stage_slab_buffer.creation_time().into()),\n            light_slab_buffer_creation_time: Arc::new(light_slab_buffer.creation_time().into()),\n            shadowmap_descriptor,\n            light_space_transforms,\n            update_bindgroup,\n            atlas_textures,\n            _atlas_textures_array: atlas_textures_array,\n            update_texture,\n            blitting_op,\n            light_bundle: analytical_light_bundle.clone().into_generic(),\n        })\n    }\n\n    /// Update the `ShadowMap`, rendering the given [`Primitive`]s to the map as\n    /// shadow casters.\n    ///\n    /// The `ShadowMap` contains a weak reference to the [`AnalyticalLight`]\n    /// used to create it. Updates made to this `AnalyticalLight` will\n    /// automatically propogate to this `ShadowMap`.\n    ///\n    /// ## Errors\n    /// If the `AnalyticalLight` used to create this `ShadowMap` has been\n    /// dropped, calling this function will err.\n    pub fn update<'a>(\n        &self,\n        lighting: impl AsRef<Lighting>,\n        renderlets: impl IntoIterator<Item = &'a Primitive>,\n    ) -> Result<(), LightingError> {\n        let lighting = lighting.as_ref();\n        let shadow_desc = self.shadowmap_descriptor.get();\n        let new_transforms = self.light_bundle.light_space_transforms(\n            &self.light_bundle.transform().descriptor(),\n            shadow_desc.z_near,\n            shadow_desc.z_far,\n        );\n        for (i, t) in (0..self.light_space_transforms.len()).zip(new_transforms) {\n            self.light_space_transforms.set_item(i, t);\n        }\n        if lighting.geometry_slab.has_queued_updates() {\n            lighting.geometry_slab.commit();\n        }\n        let renderlets = renderlets.into_iter().collect::<Vec<_>>();\n\n        let device = lighting.light_slab.device();\n        let queue = lighting.light_slab.queue();\n        let mut light_slab_buffer = lighting\n            .light_slab_buffer\n            .write()\n            .expect(\"light_slab_buffer write\");\n        let mut stage_slab_buffer = lighting\n            .geometry_slab_buffer\n            .write()\n            .expect(\"geometry_slab_buffer write\");\n\n        let bindgroup = {\n            light_slab_buffer.update_if_invalid();\n            stage_slab_buffer.update_if_invalid();\n            let stored_light_buffer_creation_time = self.light_slab_buffer_creation_time.swap(\n                light_slab_buffer.creation_time(),\n                std::sync::atomic::Ordering::Relaxed,\n            );\n            let stored_stage_buffer_creation_time = self.stage_slab_buffer_creation_time.swap(\n                stage_slab_buffer.creation_time(),\n                std::sync::atomic::Ordering::Relaxed,\n            );\n            let should_invalidate = light_slab_buffer.creation_time()\n                > stored_light_buffer_creation_time\n                || stage_slab_buffer.creation_time() > stored_stage_buffer_creation_time;\n            self.update_bindgroup.get(should_invalidate, || {\n                log::trace!(\"recreating shadow mapping bindgroup\");\n                Self::create_update_bindgroup(\n                    device,\n                    &lighting.shadow_map_update_bindgroup_layout,\n                    &stage_slab_buffer,\n                    &light_slab_buffer,\n                )\n            })\n        };\n        for (i, atlas_texture) in self.atlas_textures.iter().enumerate() {\n            let mut encoder = device\n                .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Self::LABEL });\n\n            // Update the lighting descriptor to point to this shadow map, which tells the\n            // vertex shader which shadow map we're updating.\n            lighting.lighting_descriptor.modify(|ld| {\n                let id = self.shadowmap_descriptor.id();\n                log::trace!(\"updating the shadow map {id:?} {i}\");\n                ld.update_shadow_map_id = id;\n                ld.update_shadow_map_texture_index = i as u32;\n            });\n            // Sync those changes\n            let _ = lighting.light_slab.commit();\n            let label = format!(\"{}-view-{i}\", Self::LABEL.unwrap());\n            let view = self\n                .update_texture\n                .texture\n                .create_view(&wgpu::TextureViewDescriptor {\n                    label: Some(&label),\n                    base_array_layer: i as u32,\n                    array_layer_count: Some(1),\n                    dimension: Some(wgpu::TextureViewDimension::D2),\n                    ..Default::default()\n                });\n            {\n                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                    label: Self::LABEL,\n                    color_attachments: &[],\n                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {\n                        view: &view,\n                        depth_ops: Some(wgpu::Operations {\n                            load: wgpu::LoadOp::Clear(1.0),\n                            store: wgpu::StoreOp::Store,\n                        }),\n                        stencil_ops: None,\n                    }),\n                    ..Default::default()\n                });\n                render_pass.set_pipeline(&lighting.shadow_map_update_pipeline);\n                render_pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]);\n                let mut count = 0;\n                for rlet in renderlets.iter() {\n                    let id = rlet.id();\n                    let rlet = rlet.descriptor();\n                    let vertex_range = 0..rlet.get_vertex_count();\n                    let instance_range = id.inner()..id.inner() + 1;\n                    render_pass.draw(vertex_range, instance_range);\n                    count += 1;\n                }\n                log::trace!(\"rendered {count} renderlets to the shadow map\");\n            }\n            // Then copy the depth texture to our shadow map atlas in the lighting struct\n            self.blitting_op.run(\n                lighting.light_slab.runtime(),\n                &mut encoder,\n                &self.update_texture,\n                i as u32,\n                &lighting.shadow_map_atlas,\n                atlas_texture,\n            )?;\n            let submission = queue.submit(Some(encoder.finish()));\n            device\n                .poll(wgpu::PollType::WaitForSubmissionIndex(submission))\n                .context(PollSnafu)?;\n        }\n        Ok(())\n    }\n}\n\n#[cfg(test)]\n#[allow(clippy::unused_enumerate_index)]\nmod test {\n    use glam::{UVec2, Vec3};\n\n    use crate::{context::Context, test::BlockOnFuture};\n\n    #[test]\n    fn shadow_mapping_just_cuboid() {\n        let w = 800.0;\n        let h = 800.0;\n        let ctx = Context::headless(w as u32, h as u32).block();\n        let stage = ctx\n            .new_stage()\n            .with_lighting(true)\n            .with_msaa_sample_count(4);\n\n        // let hdr_path =\n        //     std::path::PathBuf::from(std::env!(\"CARGO_WORKSPACE_DIR\")).join(\"img/hdr/\n        // night.hdr\"); let hdr_img =\n        // AtlasImage::from_hdr_path(hdr_path).unwrap();\n\n        // let skybox = Skybox::new(&ctx, hdr_img, camera.id());\n        // stage.set_skybox(skybox);\n        let doc = stage\n            .load_gltf_document_from_path(\n                crate::test::workspace_dir()\n                    .join(\"gltf\")\n                    .join(\"shadow_mapping_only_cuboid.gltf\"),\n            )\n            .unwrap();\n        let camera = doc.cameras.first().unwrap();\n        camera\n            .as_ref()\n            .set_projection(crate::camera::perspective(w, h));\n        stage.use_camera(camera);\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        frame.present();\n\n        // Rendering the scene without shadows as a sanity check\n        img_diff::assert_img_eq(\"shadows/shadow_mapping_just_cuboid/scene_before.png\", img);\n\n        let gltf_light = doc.lights.first().unwrap();\n        let shadow_map = stage\n            .new_shadow_map(gltf_light, UVec2::splat(256), 0.0, 20.0)\n            .unwrap();\n        shadow_map.shadowmap_descriptor.modify(|desc| {\n            desc.bias_min = 0.00008;\n            desc.bias_max = 0.00008;\n        });\n        shadow_map.update(&stage, doc.renderlets_iter()).unwrap();\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"shadows/shadow_mapping_just_cuboid/scene_after.png\", img);\n        frame.present();\n    }\n\n    #[test]\n    fn shadow_mapping_just_cuboid_red_and_blue() {\n        let w = 800.0;\n        let h = 800.0;\n        let ctx = Context::headless(w as u32, h as u32).block();\n        let stage = ctx\n            .new_stage()\n            .with_lighting(true)\n            .with_msaa_sample_count(4);\n\n        let doc = stage\n            .load_gltf_document_from_path(\n                crate::test::workspace_dir()\n                    .join(\"gltf\")\n                    .join(\"shadow_mapping_only_cuboid_red_and_blue.gltf\"),\n            )\n            .unwrap();\n        let camera = doc.cameras.first().unwrap();\n        camera\n            .as_ref()\n            .set_projection(crate::camera::perspective(w, h));\n        stage.use_camera(camera);\n\n        let gltf_light_a = doc.lights.first().unwrap();\n        let gltf_light_b = doc.lights.get(1).unwrap();\n        let shadow_map_a = stage\n            .new_shadow_map(gltf_light_a, UVec2::splat(256), 0.0, 20.0)\n            .unwrap();\n        shadow_map_a.shadowmap_descriptor.modify(|desc| {\n            desc.bias_min = 0.00008;\n            desc.bias_max = 0.00008;\n        });\n        shadow_map_a.update(&stage, doc.renderlets_iter()).unwrap();\n        let shadow_map_b = stage\n            .new_shadow_map(gltf_light_b, UVec2::splat(256), 0.0, 20.0)\n            .unwrap();\n        shadow_map_b.shadowmap_descriptor.modify(|desc| {\n            desc.bias_min = 0.00008;\n            desc.bias_max = 0.00008;\n        });\n        shadow_map_b.update(&stage, doc.renderlets_iter()).unwrap();\n\n        let frame = ctx.get_next_frame().unwrap();\n\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\n            \"shadows/shadow_mapping_just_cuboid/red_and_blue/frame.png\",\n            img,\n        );\n        frame.present();\n    }\n\n    #[test]\n    fn shadow_mapping_sanity() {\n        let w = 800.0;\n        let h = 800.0;\n        let ctx = Context::headless(w as u32, h as u32)\n            .block()\n            .with_shadow_mapping_atlas_texture_size([1024, 1024, 2]);\n        let stage = ctx.new_stage().with_lighting(true);\n\n        let doc = stage\n            .load_gltf_document_from_path(\n                crate::test::workspace_dir()\n                    .join(\"gltf\")\n                    .join(\"shadow_mapping_sanity.gltf\"),\n            )\n            .unwrap();\n        let camera = doc.cameras.first().unwrap();\n        camera\n            .as_ref()\n            .set_projection(crate::camera::perspective(w, h));\n        stage.use_camera(camera);\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        frame.present();\n\n        // Rendering the scene without shadows as a sanity check\n        img_diff::assert_img_eq(\"shadows/shadow_mapping_sanity/scene_before.png\", img);\n\n        let gltf_light = doc.lights.first().unwrap();\n        assert_eq!(\n            gltf_light.descriptor().transform_id,\n            gltf_light.transform().id(),\n            \"light's global transform id is different from its transform_id\"\n        );\n\n        let shadows = stage\n            .new_shadow_map(gltf_light, UVec2::new(w as u32, h as u32), 0.0, 20.0)\n            .unwrap();\n        shadows.update(&stage, doc.renderlets_iter()).unwrap();\n\n        // Extra sanity checks\n        // {\n        //     use crate::texture::DepthTexture;\n        //     use image::Luma;\n        //     {\n        //         // Ensure the state of the \"update texture\", which receives the depth\n        // of the scene on update         let shadow_map_update_texture =\n        //             DepthTexture::try_new_from(&ctx,\n        // shadows.update_texture.clone()).unwrap();         let mut\n        // shadow_map_update_img = shadow_map_update_texture.read_image().unwrap();\n        //         img_diff::normalize_gray_img(&mut shadow_map_update_img);\n        //         img_diff::save(\n        //             \"shadows/shadow_mapping_sanity/shadows_update_texture.png\",\n        //             shadow_map_update_img,\n        //         );\n        //     }\n\n        //     {\n        //         let lighting: &Lighting = stage.as_ref();\n        //         let shadow_depth_buffer =\n        // lighting.shadow_map_atlas.atlas_img_buffer(&ctx, 0);         let\n        // shadow_depth_img = shadow_depth_buffer             .into_image::<f32,\n        // Luma<f32>>(ctx.get_device())             .unwrap();\n        //         let shadow_depth_img = shadow_depth_img.into_luma8();\n        //         let mut depth_img = shadow_depth_img.clone();\n        //         img_diff::normalize_gray_img(&mut depth_img);\n        //         img_diff::save(\"shadows/shadow_mapping_sanity/depth.png\", depth_img);\n        //     }\n        // }\n\n        // Now do the rendering *with the shadow map* to see if it works.\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n\n        let img = frame.read_image().block().unwrap();\n        frame.present();\n        img_diff::assert_img_eq_cfg(\n            \"shadows/shadow_mapping_sanity/stage_render.png\",\n            img,\n            img_diff::DiffCfg {\n                image_threshold: 0.01,\n                ..Default::default()\n            },\n        );\n    }\n\n    #[test]\n    fn shadow_mapping_spot_lights() {\n        let w = 800.0;\n        let h = 800.0;\n        let ctx = Context::headless(w as u32, h as u32).block();\n        let stage = ctx\n            .new_stage()\n            .with_lighting(true)\n            .with_msaa_sample_count(4);\n\n        let doc = stage\n            .load_gltf_document_from_path(\n                crate::test::workspace_dir()\n                    .join(\"gltf\")\n                    .join(\"shadow_mapping_spots.glb\"),\n            )\n            .unwrap();\n        let camera = doc.cameras.first().unwrap();\n        camera\n            .as_ref()\n            .set_projection(crate::camera::perspective(w, h));\n        stage.use_camera(camera);\n\n        let mut shadow_maps = vec![];\n        let z_near = 0.1;\n        let z_far = 100.0;\n        for (_i, light_bundle) in doc.lights.iter().enumerate() {\n            {\n                let desc = light_bundle.as_spot().unwrap().descriptor();\n                let (p, v) = desc.shadow_mapping_projection_and_view(\n                    &light_bundle.transform().as_mat4(),\n                    z_near,\n                    z_far,\n                );\n                let temp_camera = stage.new_camera().with_projection_and_view(p, v);\n                stage.use_camera(temp_camera);\n                let frame = ctx.get_next_frame().unwrap();\n                stage.render(&frame.view());\n                let _img = frame.read_image().block().unwrap();\n                // img_diff::assert_img_eq(\n                //     &format!(\"shadows/shadow_mapping_spots/light_pov_{i}.png\"),\n                //     img,\n                // );\n                frame.present();\n            }\n            let shadow = stage\n                .new_shadow_map(light_bundle, UVec2::splat(256), z_near, z_far)\n                .unwrap();\n            shadow.shadowmap_descriptor.modify(|desc| {\n                desc.bias_min = f32::EPSILON;\n                desc.bias_max = f32::EPSILON;\n            });\n\n            shadow.update(&stage, doc.renderlets_iter()).unwrap();\n            shadow_maps.push(shadow);\n        }\n\n        stage.use_camera(camera);\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"shadows/shadow_mapping_spots/frame.png\", img);\n        frame.present();\n    }\n\n    #[test]\n    fn shadow_mapping_point_lights() {\n        let w = 800.0;\n        let h = 800.0;\n        let ctx = Context::headless(w as u32, h as u32).block();\n        let stage = ctx\n            .new_stage()\n            .with_lighting(true)\n            .with_background_color(Vec3::splat(0.05087).extend(1.0))\n            .with_msaa_sample_count(4);\n        let doc = stage\n            .load_gltf_document_from_path(\n                crate::test::workspace_dir()\n                    .join(\"gltf\")\n                    .join(\"shadow_mapping_points.glb\"),\n            )\n            .unwrap();\n        let camera = doc.cameras.first().unwrap();\n        camera\n            .as_ref()\n            .set_projection(crate::camera::perspective(w, h));\n        stage.use_camera(camera);\n\n        let mut shadows = vec![];\n        let z_near = 0.1;\n        let z_far = 100.0;\n        for (i, light_bundle) in doc.lights.iter().enumerate() {\n            {\n                let desc = light_bundle.as_point().unwrap().descriptor();\n                println!(\"point light {i}: {desc:?}\");\n                let (p, vs) = desc.shadow_mapping_projection_and_view_matrices(\n                    &light_bundle.transform().as_mat4(),\n                    z_near,\n                    z_far,\n                );\n                for (_j, v) in vs.into_iter().enumerate() {\n                    stage.use_camera(stage.new_camera().with_projection_and_view(p, v));\n                    let frame = ctx.get_next_frame().unwrap();\n                    stage.render(&frame.view());\n                    let _img = frame.read_image().block().unwrap();\n                    // img_diff::assert_img_eq(\n                    //     &format!(\"shadows/shadow_mapping_points/light_{i}_pov_{j}.png\"),\n                    //     img,\n                    // );\n                    frame.present();\n                }\n            }\n            let shadow = stage\n                .new_shadow_map(light_bundle, UVec2::splat(256), z_near, z_far)\n                .unwrap();\n            shadow.shadowmap_descriptor.modify(|desc| {\n                desc.pcf_samples = 16;\n                desc.bias_min = 0.00010;\n                desc.bias_max = 0.010;\n            });\n            shadow.update(&stage, doc.renderlets_iter()).unwrap();\n            shadows.push(shadow);\n        }\n\n        stage.use_camera(camera);\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"shadows/shadow_mapping_points/frame.png\", img);\n        frame.present();\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/light/tiling.rs",
    "content": "//! This module implements light tiling, a technique used in rendering to\n//! efficiently manage and apply lighting effects across a scene.\n//!\n//! Light tiling divides the rendering surface into a grid of tiles, allowing\n//! for the efficient computation of lighting effects by processing each tile\n//! independently. This approach helps in optimizing the rendering pipeline by\n//! reducing the number of lighting calculations needed, especially in complex\n//! scenes with multiple light sources.\n//!\n//! The `LightTiling` struct and its associated methods provide the necessary\n//! functionality to set up and execute light tiling operations. It includes the\n//! creation of compute pipelines for clearing tiles, computing minimum and\n//! maximum depths, and binning lights into tiles.\n//!\n//! For more detailed information on light tiling and its implementation, refer to [this blog post](https://renderling.xyz/articles/live/light_tiling.html).\n\nuse core::sync::atomic::AtomicUsize;\nuse std::sync::Arc;\n\nuse craballoc::{\n    slab::SlabBuffer,\n    value::{GpuArrayContainer, Hybrid, HybridArrayContainer, IsContainer},\n};\nuse crabslab::Id;\nuse glam::{UVec2, UVec3};\n\nuse crate::{\n    bindgroup::ManagedBindGroup,\n    light::{\n        shader::{LightTile, LightTilingDescriptor},\n        Lighting,\n    },\n    stage::Stage,\n};\n\n/// Shaders and resources for conducting light tiling.\n///\n/// This struct takes a container type variable in order to allow\n/// tests to read and write [`LightTile`] values on the GPU.\n///\n/// For info on what light tiling is, see\n/// <https://renderling.xyz/articles/live/light_tiling.html>.\npub struct LightTiling<Ct: IsContainer = GpuArrayContainer> {\n    pub(crate) tiling_descriptor: Hybrid<LightTilingDescriptor>,\n    /// Container is a type variable for testing, as we have to load\n    /// the tiles with known values from the CPU.\n    tiles: Ct::Container<LightTile>,\n    /// Cache of the id of the Stage's depth texture.\n    ///\n    /// Used to invalidate our tiling bindgroup.\n    depth_texture_id: Arc<AtomicUsize>,\n\n    bindgroup: ManagedBindGroup,\n    bindgroup_layout: Arc<wgpu::BindGroupLayout>,\n    bindgroup_creation_time: Arc<AtomicUsize>,\n\n    clear_tiles_pipeline: Arc<wgpu::ComputePipeline>,\n    compute_min_max_depth_pipeline: Arc<wgpu::ComputePipeline>,\n    compute_bins_pipeline: Arc<wgpu::ComputePipeline>,\n}\n\nconst LABEL: Option<&'static str> = Some(\"light-tiling\");\n\nimpl<Ct: IsContainer> LightTiling<Ct> {\n    fn create_bindgroup_layout(device: &wgpu::Device, multisampled: bool) -> wgpu::BindGroupLayout {\n        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: LABEL,\n            entries: &[\n                // Geometry slab\n                wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::COMPUTE,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n                // Lighting slab\n                wgpu::BindGroupLayoutEntry {\n                    binding: 1,\n                    visibility: wgpu::ShaderStages::COMPUTE,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: false },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n                // Depth texture\n                wgpu::BindGroupLayoutEntry {\n                    binding: 2,\n                    visibility: wgpu::ShaderStages::COMPUTE,\n                    ty: wgpu::BindingType::Texture {\n                        sample_type: wgpu::TextureSampleType::Depth,\n                        view_dimension: wgpu::TextureViewDimension::D2,\n                        multisampled,\n                    },\n                    count: None,\n                },\n            ],\n        })\n    }\n\n    fn create_clear_tiles_pipeline(\n        device: &wgpu::Device,\n        multisampled: bool,\n    ) -> wgpu::ComputePipeline {\n        const LABEL: Option<&'static str> = Some(\"light-tiling-clear-tiles\");\n        let module = crate::linkage::light_tiling_clear_tiles::linkage(device);\n        let (pipeline_layout, _) = Self::create_layouts(device, multisampled);\n        device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {\n            label: LABEL,\n            layout: Some(&pipeline_layout),\n            module: &module.module,\n            entry_point: Some(module.entry_point),\n            compilation_options: wgpu::PipelineCompilationOptions::default(),\n            cache: None,\n        })\n    }\n\n    fn create_compute_min_max_depth_pipeline(\n        device: &wgpu::Device,\n        multisampled: bool,\n    ) -> wgpu::ComputePipeline {\n        const LABEL: Option<&'static str> = Some(\"light-tiling-compute-min-max-depth\");\n        let module = crate::linkage::light_tiling_compute_tile_min_and_max_depth::linkage(device);\n        let (pipeline_layout, _) = Self::create_layouts(device, multisampled);\n        device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {\n            label: LABEL,\n            layout: Some(&pipeline_layout),\n            module: &module.module,\n            entry_point: Some(module.entry_point),\n            compilation_options: wgpu::PipelineCompilationOptions::default(),\n            cache: None,\n        })\n    }\n\n    fn create_compute_bins_pipeline(\n        device: &wgpu::Device,\n        multisampled: bool,\n    ) -> wgpu::ComputePipeline {\n        const LABEL: Option<&'static str> = Some(\"light-tiling-compute-bins\");\n        let module = crate::linkage::light_tiling_bin_lights::linkage(device);\n        let (pipeline_layout, _) = Self::create_layouts(device, multisampled);\n        device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {\n            label: LABEL,\n            layout: Some(&pipeline_layout),\n            module: &module.module,\n            entry_point: Some(module.entry_point),\n            compilation_options: wgpu::PipelineCompilationOptions::default(),\n            cache: None,\n        })\n    }\n\n    /// All pipelines share the same layout, so we do it here, once.\n    fn create_layouts(\n        device: &wgpu::Device,\n        multisampled: bool,\n    ) -> (wgpu::PipelineLayout, wgpu::BindGroupLayout) {\n        let bindgroup_layout = Self::create_bindgroup_layout(device, multisampled);\n        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label: LABEL,\n            bind_group_layouts: &[&bindgroup_layout],\n            push_constant_ranges: &[],\n        });\n        (pipeline_layout, bindgroup_layout)\n    }\n\n    pub(crate) fn prepare(&self, lighting: &Lighting, depth_texture_size: UVec2) {\n        self.tiling_descriptor.modify(|d| {\n            d.depth_texture_size = depth_texture_size;\n        });\n        lighting.lighting_descriptor.modify(|desc| {\n            desc.light_tiling_descriptor_id = self.tiling_descriptor.id();\n        });\n    }\n\n    pub(crate) fn clear_tiles(\n        &self,\n        encoder: &mut wgpu::CommandEncoder,\n        bindgroup: &wgpu::BindGroup,\n        depth_texture_size: UVec2,\n    ) {\n        let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {\n            label: Some(\"light-tiling-clear-tiles\"),\n            timestamp_writes: None,\n        });\n        compute_pass.set_pipeline(&self.clear_tiles_pipeline);\n        compute_pass.set_bind_group(0, bindgroup, &[]);\n\n        let tile_size = self.tiling_descriptor.get().tile_size;\n        let dims_f32 = depth_texture_size.as_vec2() / tile_size as f32;\n        let workgroups = (dims_f32 / 16.0).ceil().as_uvec2();\n        let x = workgroups.x;\n        let y = workgroups.y;\n        let z = 1;\n        compute_pass.dispatch_workgroups(x, y, z);\n    }\n\n    const WORKGROUP_SIZE: UVec3 = UVec3::new(16, 16, 1);\n\n    pub(crate) fn compute_min_max_depth(\n        &self,\n        encoder: &mut wgpu::CommandEncoder,\n        bindgroup: &wgpu::BindGroup,\n        depth_texture_size: UVec2,\n    ) {\n        let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {\n            label: Some(\"light-tiling-compute-min-max-depth\"),\n            timestamp_writes: None,\n        });\n        compute_pass.set_pipeline(&self.compute_min_max_depth_pipeline);\n        compute_pass.set_bind_group(0, bindgroup, &[]);\n\n        let x = (depth_texture_size.x / Self::WORKGROUP_SIZE.x) + 1;\n        let y = (depth_texture_size.y / Self::WORKGROUP_SIZE.y) + 1;\n        let z = 1;\n        compute_pass.dispatch_workgroups(x, y, z);\n    }\n\n    pub(crate) fn compute_bins(\n        &self,\n        encoder: &mut wgpu::CommandEncoder,\n        bindgroup: &wgpu::BindGroup,\n        depth_texture_size: UVec2,\n    ) {\n        let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {\n            label: Some(\"light-tiling-compute-bins\"),\n            timestamp_writes: None,\n        });\n        compute_pass.set_pipeline(&self.compute_bins_pipeline);\n        compute_pass.set_bind_group(0, bindgroup, &[]);\n\n        let tile_size = self.tiling_descriptor.get().tile_size;\n        let x = (depth_texture_size.x / tile_size) + 1;\n        let y = (depth_texture_size.y / tile_size) + 1;\n        let z = 1;\n        compute_pass.dispatch_workgroups(x, y, z);\n    }\n\n    /// Get the bindgroup.\n    pub fn get_bindgroup(\n        &self,\n        device: &wgpu::Device,\n        geometry_slab: &SlabBuffer<wgpu::Buffer>,\n        lighting_slab: &SlabBuffer<wgpu::Buffer>,\n        depth_texture: &crate::texture::Texture,\n    ) -> Arc<wgpu::BindGroup> {\n        // UNWRAP: safe because we know there are elements\n        let latest_buffer_creation = [geometry_slab.creation_time(), lighting_slab.creation_time()]\n            .into_iter()\n            .max()\n            .unwrap();\n        let prev_buffer_creation = self\n            .bindgroup_creation_time\n            .swap(latest_buffer_creation, std::sync::atomic::Ordering::Relaxed);\n        let prev_depth_texture_id = self\n            .depth_texture_id\n            .swap(depth_texture.id(), std::sync::atomic::Ordering::Relaxed);\n        let should_invalidate = prev_buffer_creation < latest_buffer_creation\n            || prev_depth_texture_id < depth_texture.id();\n        self.bindgroup.get(should_invalidate, || {\n            device.create_bind_group(&wgpu::BindGroupDescriptor {\n                label: Some(\"light-tiling\"),\n                layout: &self.bindgroup_layout,\n                entries: &[\n                    wgpu::BindGroupEntry {\n                        binding: 0,\n                        resource: geometry_slab.as_entire_binding(),\n                    },\n                    wgpu::BindGroupEntry {\n                        binding: 1,\n                        resource: lighting_slab.as_entire_binding(),\n                    },\n                    wgpu::BindGroupEntry {\n                        binding: 2,\n                        resource: wgpu::BindingResource::TextureView(&depth_texture.view),\n                    },\n                ],\n            })\n        })\n    }\n\n    /// Set the minimum illuminance, in lux, to determine if a light illuminates\n    /// a tile.\n    pub fn set_minimum_illuminance(&self, minimum_illuminance_lux: f32) {\n        self.tiling_descriptor.modify(|desc| {\n            desc.minimum_illuminance_lux = minimum_illuminance_lux;\n        });\n    }\n\n    /// Run light tiling, resulting in edits to the lighting slab.\n    pub fn run(&self, stage: &Stage) {\n        let depth_texture = stage.depth_texture.read().expect(\"depth_texture read\");\n        let depth_texture_size = depth_texture.size();\n        let lighting = stage.as_ref();\n        self.prepare(lighting, depth_texture_size);\n\n        let light_slab = &lighting.light_slab;\n        let geometry_slab = &lighting.geometry_slab;\n        let runtime = light_slab.runtime();\n        let label = Some(\"light-tiling-run\");\n        let bindgroup = self.get_bindgroup(\n            &runtime.device,\n            &geometry_slab.commit(),\n            &light_slab.commit(),\n            &depth_texture,\n        );\n\n        let mut encoder = runtime\n            .device\n            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label });\n        {\n            self.clear_tiles(&mut encoder, bindgroup.as_ref(), depth_texture_size);\n            self.compute_min_max_depth(&mut encoder, bindgroup.as_ref(), depth_texture_size);\n            self.compute_bins(&mut encoder, bindgroup.as_ref(), depth_texture_size);\n        }\n        runtime.queue.submit(Some(encoder.finish()));\n    }\n\n    pub fn tiles(&self) -> &Ct::Container<LightTile> {\n        &self.tiles\n    }\n\n    #[cfg(test)]\n    /// Read the tiles from the light slab.\n    pub(crate) async fn read_tiles(&self, lighting: &Lighting) -> Vec<LightTile> {\n        lighting\n            .light_slab\n            .read_array(self.tiling_descriptor.get().tiles_array)\n            .await\n            .unwrap()\n    }\n\n    #[cfg(test)]\n    #[allow(dead_code)]\n    pub(crate) fn read_tile(&self, lighting: &Lighting, tile_coord: UVec2) -> LightTile {\n        let desc = self.tiling_descriptor.get();\n        let tile_index = tile_coord.y * desc.tile_grid_size().x + tile_coord.x;\n        let tile_id = desc.tiles_array.at(tile_index as usize);\n        futures_lite::future::block_on(lighting.light_slab.read_one(tile_id)).unwrap()\n    }\n\n    #[cfg(test)]\n    /// Returns a tuple containing an image of depth mins, depth maximums and\n    /// number of lights.\n    pub(crate) async fn read_images(\n        &self,\n        lighting: &Lighting,\n    ) -> (image::GrayImage, image::GrayImage, image::GrayImage) {\n        use crabslab::Slab;\n\n        use crate::light::shader::dequantize_depth_u32_to_f32;\n\n        let tile_dimensions = self.tiling_descriptor.get().tile_grid_size();\n        let slab = lighting.light_slab.read(..).await.unwrap();\n        let tiling_descriptor_id_in_lighting = lighting\n            .lighting_descriptor\n            .get()\n            .light_tiling_descriptor_id;\n        let tiling_descriptor_id = self.tiling_descriptor.id();\n        assert_eq!(tiling_descriptor_id_in_lighting, tiling_descriptor_id);\n        let desc = slab.read(\n            lighting\n                .lighting_descriptor\n                .get()\n                .light_tiling_descriptor_id,\n        );\n        let should_be_len = tile_dimensions.x * tile_dimensions.y;\n        if should_be_len != desc.tiles_array.len() as u32 {\n            log::error!(\n                \"LightTilingDescriptor's tiles array is borked: {:?}\\nexpected {should_be_len} \\\n                 tiles\\ntile_dimensions: {tile_dimensions}\",\n                desc.tiles_array,\n            );\n        }\n        let mut mins_img = image::GrayImage::new(tile_dimensions.x, tile_dimensions.y);\n        let mut maxs_img = image::GrayImage::new(tile_dimensions.x, tile_dimensions.y);\n        let mut lights_img = image::GrayImage::new(tile_dimensions.x, tile_dimensions.y);\n        slab.read_vec(desc.tiles_array)\n            .into_iter()\n            .enumerate()\n            .for_each(|(i, tile)| {\n                let x = i as u32 % tile_dimensions.x;\n                let y = i as u32 / tile_dimensions.x;\n                let min = dequantize_depth_u32_to_f32(tile.depth_min);\n                let max = dequantize_depth_u32_to_f32(tile.depth_max);\n\n                mins_img.get_pixel_mut(x, y).0[0] = crate::math::scaled_f32_to_u8(min);\n                maxs_img.get_pixel_mut(x, y).0[0] = crate::math::scaled_f32_to_u8(max);\n                lights_img.get_pixel_mut(x, y).0[0] = crate::math::scaled_f32_to_u8(\n                    tile.next_light_index as f32 / tile.lights_array.len() as f32,\n                );\n            });\n\n        (mins_img, maxs_img, lights_img)\n    }\n}\n\n/// Parameters for tuning light tiling.\n#[derive(Debug, Clone, Copy)]\npub struct LightTilingConfig {\n    /// The size of each tile, in pixels.\n    ///\n    /// Default is `16`.\n    pub tile_size: u32,\n    /// The maximum number of lights per tile.\n    ///\n    /// Default is `32`.\n    pub max_lights_per_tile: u32,\n    /// The minimum illuminance, in lux.\n    ///\n    /// Used to determine the radius of illumination of a light,\n    /// which is then used to determine if a light illuminates a tile.\n    ///\n    /// * Moonlight: < 1 lux.\n    ///   - Full moon on a clear night: 0.25 lux.\n    ///   - Quarter moon: 0.01 lux\n    ///   - Starlight overcast moonless night sky: 0.0001 lux.\n    /// * General indoor lighting: Around 100 to 300 lux.\n    /// * Office lighting: Typically around 300 to 500 lux.\n    /// * Reading or task lighting: Around 500 to 750 lux.\n    /// * Detailed work (e.g., drafting, surgery): 1000 lux or more.\n    ///\n    /// Default is `0.1`.\n    pub minimum_illuminance: f32,\n}\n\nimpl Default for LightTilingConfig {\n    fn default() -> Self {\n        LightTilingConfig {\n            tile_size: 16,\n            max_lights_per_tile: 32,\n            minimum_illuminance: 0.1,\n        }\n    }\n}\n\nimpl LightTiling<HybridArrayContainer> {\n    /// Creates a new [`LightTiling`] struct with a `HybridArray` of tiles.\n    pub(crate) fn new_hybrid(\n        lighting: &Lighting,\n        multisampled: bool,\n        depth_texture_size: UVec2,\n        config: LightTilingConfig,\n    ) -> Self {\n        log::trace!(\"creating LightTiling\");\n        let lighting_slab = lighting.slab_allocator();\n        let runtime = lighting_slab.runtime();\n        let desc = LightTilingDescriptor {\n            depth_texture_size,\n            tile_size: config.tile_size,\n            minimum_illuminance_lux: config.minimum_illuminance,\n            ..Default::default()\n        };\n        let tiling_descriptor = lighting_slab.new_value(desc);\n        lighting.lighting_descriptor.modify(|desc| {\n            desc.light_tiling_descriptor_id = tiling_descriptor.id();\n        });\n        log::trace!(\"created tiling descriptor: {tiling_descriptor:#?}\");\n        let tiled_size = desc.tile_grid_size();\n        log::trace!(\"  grid size: {tiled_size}\");\n        let mut tiles = Vec::new();\n        for _ in 0..tiled_size.x * tiled_size.y {\n            let lights =\n                lighting_slab.new_array(vec![Id::NONE; config.max_lights_per_tile as usize]);\n            tiles.push(LightTile {\n                lights_array: lights.array(),\n                ..Default::default()\n            });\n        }\n        let tiles = lighting_slab.new_array(tiles);\n        tiling_descriptor.modify(|d| {\n            let tiles_array = tiles.array();\n            log::trace!(\"  setting tiles array: {tiles_array:?}\");\n            d.tiles_array = tiles_array;\n        });\n        let clear_tiles_pipeline = Arc::new(Self::create_clear_tiles_pipeline(\n            &runtime.device,\n            multisampled,\n        ));\n        let compute_min_max_depth_pipeline = Arc::new(Self::create_compute_min_max_depth_pipeline(\n            &runtime.device,\n            multisampled,\n        ));\n        let compute_bins_pipeline = Arc::new(Self::create_compute_bins_pipeline(\n            &runtime.device,\n            multisampled,\n        ));\n        let bindgroup_layout =\n            Arc::new(Self::create_bindgroup_layout(&runtime.device, multisampled));\n\n        Self {\n            tiling_descriptor,\n            tiles,\n            // The inner bindgroup is created on-demand\n            bindgroup: ManagedBindGroup::default(),\n            bindgroup_creation_time: Default::default(),\n            bindgroup_layout,\n            depth_texture_id: Default::default(),\n            clear_tiles_pipeline,\n            compute_min_max_depth_pipeline,\n            compute_bins_pipeline,\n        }\n    }\n}\n\nimpl LightTiling {\n    /// Creates a new [`LightTiling`] struct.\n    pub fn new(\n        lighting: &Lighting,\n        multisampled: bool,\n        depth_texture_size: UVec2,\n        config: LightTilingConfig,\n    ) -> Self {\n        // Note to self, I wish we had `fmap` here.\n        let LightTiling {\n            tiling_descriptor,\n            tiles,\n            bindgroup_creation_time,\n            depth_texture_id,\n            bindgroup_layout,\n            bindgroup,\n            clear_tiles_pipeline,\n            compute_min_max_depth_pipeline,\n            compute_bins_pipeline,\n        } = LightTiling::new_hybrid(lighting, multisampled, depth_texture_size, config);\n        Self {\n            tiling_descriptor,\n            tiles: tiles.into_gpu_only(),\n            depth_texture_id,\n            bindgroup,\n            bindgroup_layout,\n            bindgroup_creation_time,\n            clear_tiles_pipeline,\n            compute_min_max_depth_pipeline,\n            compute_bins_pipeline,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/light.rs",
    "content": "//! Lighting effects.\n//!\n//! This module includes support for various types of lights such as\n//! directional, point, and spot lights.\n//!\n//! Additionally, the module provides shadow mapping to create realistic\n//! shadows.\n//!\n//! Also provided is an implementation of light tiling, a technique that\n//! optimizes the rendering of thousands of analytical lights. If you find your\n//! scene performing poorly under the load of very many lights, [`LightTiling`]\n//! can speed things up.\n//!\n//! ## Analytical lights\n//!\n//! Analytical lights are a fundamental lighting effect in a scene.\n//! These lights can be created directly from the [`Stage`] using the methods\n//! * [`Stage::new_directional_light`]\n//! * [`Stage::new_point_light`]\n//! * [`Stage::new_spot_light`]\n//!\n//! Each of these methods returns an [`AnalyticalLight`] instance that can be\n//! manipulated to simulate different lighting conditions.\n//!\n//! Once created, these lights can be positioned and oriented using\n//! [`Transform`] or [`NestedTransform`] objects. The [`Transform`] allows you\n//! to set the position, rotation, and scale of the light, while\n//! [`NestedTransform`] enables hierarchical transformations, which is useful\n//! for complex scenes where lights need to follow specific objects or\n//! structures.\n//!\n//! By adjusting the properties of these lights, such as intensity, color, and\n//! direction, you can achieve a wide range of lighting effects, from simulating\n//! sunlight with directional lights to creating focused spotlights or ambient\n//! point lights. These lights can also be combined with shadow mapping\n//! techniques to enhance the realism of shadows in the scene.\n//!\n//! ## Shadow mapping\n//!\n//! Shadow mapping is a technique used to add realistic shadows to a scene by\n//! simulating the way light interacts with objects.\n//!\n//! To create a [`ShadowMap`], use the [`Stage::new_shadow_map`] method, passing\n//! in the light source and desired parameters such as the size of the shadow\n//! map and the near and far planes of the light's frustum.  Once created, the\n//! shadow map needs to be updated each frame (or as needed) using the\n//! [`ShadowMap::update`] method, which renders the scene from the light's\n//! perspective to determine which areas are in shadow.\n//!\n//! This technique allows for dynamic shadows that change with the movement of\n//! lights and objects, enhancing the realism of the scene. Proper\n//! configuration of shadow map parameters, such as bias and resolution, is\n//! crucial to achieving high-quality shadows without artifacts, and varies\n//! with each scene.\n//!\n//! ## Light tiling\n//!\n//! Light tiling is a technique used to optimize the rendering of scenes with a\n//! large number of lights.\n//!\n//! It divides the rendering surface into a grid of tiles, allowing for\n//! efficient computation of lighting effects by processing each tile\n//! independently and cutting down on the overall lighting calculations.\n//!\n//! To create a [`LightTiling`], use the [`Stage::new_light_tiling`] method,\n//! providing a [`LightTilingConfig`] to specify parameters such as tile size\n//! and maximum lights per tile.\n//!\n//! Once created, the [`LightTiling`] instance should be kept in sync with the\n//! scene by calling the [`LightTiling::run`] method each frame, or however you\n//! see fit. This method updates the lighting calculations for each tile based\n//! on the current scene configuration, ensuring optimal performance even with\n//! many lights.\n//!\n//! By using light tiling, you can significantly improve the performance of your\n//! rendering pipeline, especially in complex scenes with numerous light\n//! sources.\n\n#[cfg(doc)]\nuse crate::{\n    stage::Stage,\n    transform::{NestedTransform, Transform},\n};\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\n#[cfg(cpu)]\nmod shadow_map;\n#[cfg(cpu)]\npub use shadow_map::*;\n\n#[cfg(cpu)]\nmod tiling;\n#[cfg(cpu)]\npub use tiling::*;\n\npub mod shader;\n\n#[cfg(test)]\nmod test {\n    use crabslab::Array;\n    use glam::{UVec2, UVec3, Vec2, Vec3};\n\n    use crate::math::{GpuRng, IsVector};\n\n    use super::shader::*;\n\n    #[cfg(feature = \"gltf\")]\n    #[test]\n    fn position_direction_sanity() {\n        // With GLTF, the direction of a light is given by the light's node's transform.\n        // Specifically we get the node's transform and use the rotation quaternion to\n        // rotate the vector Vec3::NEG_Z - the result is our direction.\n\n        use glam::{Mat4, Quat};\n        println!(\"{:#?}\", std::env::current_dir());\n        let (document, _buffers, _images) = gltf::import(\"../../gltf/four_spotlights.glb\").unwrap();\n        for node in document.nodes() {\n            use glam::Vec3;\n\n            println!(\"node: {} {:?}\", node.index(), node.name());\n\n            let gltf_transform = node.transform();\n            let (translation, rotation, _scale) = gltf_transform.decomposed();\n            let position = Vec3::from_array(translation);\n            let direction =\n                Mat4::from_quat(Quat::from_array(rotation)).transform_vector3(Vec3::NEG_Z);\n            println!(\"position: {position}\");\n            println!(\"direction: {direction}\");\n\n            // In Blender, our lights are sitting at (0, 0, 1) pointing at -Z, +Z, +X and\n            // +Y. But alas, it is a bit more complicated than that because this\n            // file is exported with UP being +Y, so Z and Y have been\n            // flipped...\n            assert_eq!(Vec3::Y, position);\n            let expected_direction = match node.name() {\n                Some(\"light_negative_z\") => Vec3::NEG_Y,\n                Some(\"light_positive_z\") => Vec3::Y,\n                Some(\"light_positive_x\") => Vec3::X,\n                Some(\"light_positive_y\") => Vec3::NEG_Z,\n                n => panic!(\"unexpected node '{n:?}'\"),\n            };\n            // And also there are rounding ... imprecisions...\n            assert_approx_eq::assert_approx_eq!(expected_direction.x, direction.x);\n            assert_approx_eq::assert_approx_eq!(expected_direction.y, direction.y);\n            assert_approx_eq::assert_approx_eq!(expected_direction.z, direction.z);\n        }\n    }\n\n    #[test]\n    /// Test that we can determine if a point is inside clip space or not.\n    fn clip_space_bounds_sanity() {\n        let inside = Vec3::ONE;\n        assert!(\n            crate::math::is_inside_clip_space(inside),\n            \"should be inside\"\n        );\n        let inside = Vec3::new(0.5, -0.5, 0.8);\n        assert!(\n            crate::math::is_inside_clip_space(inside),\n            \"should be inside\"\n        );\n        let outside_neg_z = Vec3::new(0.5, -0.5, -0.8);\n        assert!(\n            !crate::math::is_inside_clip_space(outside_neg_z),\n            \"negative z should be outside (wgpu z range is [0, 1])\"\n        );\n        let outside = Vec3::new(0.5, 0.0, 1.3);\n        assert!(\n            !crate::math::is_inside_clip_space(outside),\n            \"should be outside\"\n        );\n    }\n\n    #[test]\n    fn finding_orthogonal_vectors_sanity() {\n        const THRESHOLD: f32 = f32::EPSILON * 3.0;\n\n        let mut prng = GpuRng::new(0);\n        for _ in 0..100 {\n            let v2 = prng.gen_vec2(Vec2::splat(-100.0), Vec2::splat(100.0));\n            let v2_ortho = v2.orthonormal_vectors();\n            let v2_dot = v2.dot(v2_ortho);\n            if v2_dot.abs() >= THRESHOLD {\n                panic!(\"{v2} • {v2_ortho} < {THRESHOLD}, saw {v2_dot}\")\n            }\n\n            let v3 = prng\n                .gen_vec3(Vec3::splat(-100.0), Vec3::splat(100.0))\n                .alt_norm_or_zero();\n            for v3_ortho in v3.orthonormal_vectors() {\n                let v3_dot = v3.dot(v3_ortho);\n                if v3_dot.abs() >= THRESHOLD {\n                    panic!(\"{v3} • {v3_ortho} < {THRESHOLD}, saw {v3_dot}\");\n                }\n            }\n        }\n    }\n\n    #[test]\n    fn next_light_sanity() {\n        {\n            let lights_array = Array::new(0, 1);\n            // When there's only one light we only need one invocation to check that one\n            // light (per tile)\n            let mut next_light = NextLightIndex::new(UVec3::new(0, 0, 0), 16, lights_array);\n            assert_eq!(Some(0u32.into()), next_light.next());\n            assert_eq!(None, next_light.next());\n            // The next invocation won't check anything\n            let mut next_light = NextLightIndex::new(UVec3::new(1, 0, 0), 16, lights_array);\n            assert_eq!(None, next_light.next());\n            // Neither will the next row\n            let mut next_light = NextLightIndex::new(UVec3::new(0, 1, 0), 16, lights_array);\n            assert_eq!(None, next_light.next());\n        }\n        {\n            let lights_array = Array::new(0, 2);\n            // When there's two lights we need two invocations\n            let mut next_light = NextLightIndex::new(UVec3::new(0, 0, 0), 16, lights_array);\n            assert_eq!(Some(0u32.into()), next_light.next());\n            assert_eq!(None, next_light.next());\n            // The next invocation checks the second light\n            let mut next_light = NextLightIndex::new(UVec3::new(1, 0, 0), 16, lights_array);\n            assert_eq!(Some(1u32.into()), next_light.next());\n            assert_eq!(None, next_light.next());\n            // The next one doesn't check anything\n            let mut next_light = NextLightIndex::new(UVec3::new(2, 0, 0), 16, lights_array);\n            assert_eq!(None, next_light.next());\n        }\n        {\n            // With 256 lights (16*16), each fragment in the tile checks exactly one light\n            let lights_array = Array::new(0, 16 * 16);\n            let mut checked_lights = vec![];\n            for y in 0..16 {\n                for x in 0..16 {\n                    let mut next_light = NextLightIndex::new(UVec3::new(x, y, 0), 16, lights_array);\n                    let next_index = next_light.next_index();\n                    let checked_light = next_light.next().unwrap();\n                    assert_eq!(next_index, checked_light.index());\n                    checked_lights.push(checked_light);\n                    assert_eq!(None, next_light.next());\n                }\n            }\n            println!(\"checked_lights: {checked_lights:#?}\");\n            assert_eq!(256, checked_lights.len());\n        }\n    }\n\n    #[test]\n    fn frag_coord_to_tile_index() {\n        let tiling_desc = LightTilingDescriptor {\n            depth_texture_size: UVec2::new(1024, 800),\n            ..Default::default()\n        };\n        for x in 0..16 {\n            let index = tiling_desc.tile_index_for_fragment(Vec2::new(x as f32, 0.0));\n            assert_eq!(0, index);\n        }\n        let index = tiling_desc.tile_index_for_fragment(Vec2::new(16.0, 0.0));\n        assert_eq!(1, index);\n        let index = tiling_desc.tile_index_for_fragment(Vec2::new(0.0, 16.0));\n        assert_eq!(1024 / 16, index);\n\n        let tiling_desc = LightTilingDescriptor {\n            depth_texture_size: UVec2::new(\n                (10.0 * 2.0f32.powi(8)) as u32,\n                (9.0 * 2.0f32.powi(8)) as u32,\n            ),\n            ..Default::default()\n        };\n        let frag_coord = Vec2::new(1917.0, 979.0);\n        let tile_coord = tiling_desc.tile_coord_for_fragment(frag_coord);\n        assert_eq!(UVec2::new(119, 61), tile_coord);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/atlas_blit_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"atlas::shader::atlas_blit_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/atlas-shader-atlas_blit_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"atlas_blit_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"atlasshaderatlas_blit_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/atlas-shader-atlas_blit_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"atlas_blit_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/atlas_blit_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"atlas::shader::atlas_blit_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/atlas-shader-atlas_blit_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"atlas_blit_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"atlasshaderatlas_blit_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/atlas-shader-atlas_blit_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"atlas_blit_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/bloom_downsample_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"bloom::shader::bloom_downsample_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/bloom-shader-bloom_downsample_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"bloom_downsample_fragment\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"bloomshaderbloom_downsample_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/bloom-shader-bloom_downsample_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"bloom_downsample_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/bloom_mix_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"bloom::shader::bloom_mix_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/bloom-shader-bloom_mix_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"bloom_mix_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"bloomshaderbloom_mix_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/bloom-shader-bloom_mix_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"bloom_mix_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/bloom_upsample_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"bloom::shader::bloom_upsample_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/bloom-shader-bloom_upsample_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"bloom_upsample_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"bloomshaderbloom_upsample_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/bloom-shader-bloom_upsample_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"bloom_upsample_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/bloom_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"bloom::shader::bloom_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/bloom-shader-bloom_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"bloom_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"bloomshaderbloom_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/bloom-shader-bloom_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"bloom_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/brdf_lut_convolution_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolution::shader::brdf_lut_convolution_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/convolution-shader-brdf_lut_convolution_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"brdf_lut_convolution_fragment\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolutionshaderbrdf_lut_convolution_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/convolution-shader-brdf_lut_convolution_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"brdf_lut_convolution_fragment\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/brdf_lut_convolution_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolution::shader::brdf_lut_convolution_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/convolution-shader-brdf_lut_convolution_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"brdf_lut_convolution_vertex\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolutionshaderbrdf_lut_convolution_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/convolution-shader-brdf_lut_convolution_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"brdf_lut_convolution_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/compositor_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"compositor::compositor_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/compositor-compositor_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"compositor_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"compositorcompositor_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/compositor-compositor_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"compositor_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/compositor_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"compositor::compositor_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/compositor-compositor_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"compositor_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"compositorcompositor_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/compositor-compositor_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"compositor_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/compute_copy_depth_to_pyramid.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"cull::shader::compute_copy_depth_to_pyramid\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/cull-shader-compute_copy_depth_to_pyramid.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"compute_copy_depth_to_pyramid\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"cullshadercompute_copy_depth_to_pyramid\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/cull-shader-compute_copy_depth_to_pyramid.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"compute_copy_depth_to_pyramid\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/compute_copy_depth_to_pyramid_multisampled.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"cull::shader::compute_copy_depth_to_pyramid_multisampled\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\n            \"../../shaders/cull-shader-compute_copy_depth_to_pyramid_multisampled.spv\"\n        )\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"compute_copy_depth_to_pyramid_multisampled\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"cullshadercompute_copy_depth_to_pyramid_multisampled\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\n            \"../../shaders/cull-shader-compute_copy_depth_to_pyramid_multisampled.wgsl\"\n        )\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"compute_copy_depth_to_pyramid_multisampled\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/compute_culling.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"cull::shader::compute_culling\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/cull-shader-compute_culling.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"compute_culling\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"cullshadercompute_culling\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/cull-shader-compute_culling.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"compute_culling\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/compute_downsample_depth_pyramid.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"cull::shader::compute_downsample_depth_pyramid\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/cull-shader-compute_downsample_depth_pyramid.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"compute_downsample_depth_pyramid\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"cullshadercompute_downsample_depth_pyramid\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/cull-shader-compute_downsample_depth_pyramid.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"compute_downsample_depth_pyramid\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/cubemap_sampling_test_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"cubemap::shader::cubemap_sampling_test_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/cubemap-shader-cubemap_sampling_test_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"cubemap_sampling_test_fragment\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"cubemapshadercubemap_sampling_test_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/cubemap-shader-cubemap_sampling_test_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"cubemap_sampling_test_fragment\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/cubemap_sampling_test_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"cubemap::shader::cubemap_sampling_test_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/cubemap-shader-cubemap_sampling_test_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"cubemap_sampling_test_vertex\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"cubemapshadercubemap_sampling_test_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/cubemap-shader-cubemap_sampling_test_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"cubemap_sampling_test_vertex\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/debug_overlay_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"debug::shader::debug_overlay_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/debug-shader-debug_overlay_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"debug_overlay_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"debugshaderdebug_overlay_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/debug-shader-debug_overlay_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"debug_overlay_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/debug_overlay_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"debug::shader::debug_overlay_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/debug-shader-debug_overlay_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"debug_overlay_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"debugshaderdebug_overlay_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/debug-shader-debug_overlay_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"debug_overlay_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/di_convolution_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"pbr::ibl::shader::di_convolution_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/pbr-ibl-shader-di_convolution_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"di_convolution_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"pbriblshaderdi_convolution_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/pbr-ibl-shader-di_convolution_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"di_convolution_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/generate_mipmap_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolution::shader::generate_mipmap_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/convolution-shader-generate_mipmap_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"generate_mipmap_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolutionshadergenerate_mipmap_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/convolution-shader-generate_mipmap_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"generate_mipmap_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/generate_mipmap_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolution::shader::generate_mipmap_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/convolution-shader-generate_mipmap_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"generate_mipmap_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolutionshadergenerate_mipmap_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/convolution-shader-generate_mipmap_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"generate_mipmap_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/implicit_isosceles_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"tutorial::implicit_isosceles_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/tutorial-implicit_isosceles_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"implicit_isosceles_vertex\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"tutorialimplicit_isosceles_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/tutorial-implicit_isosceles_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"implicit_isosceles_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/light_tiling_bin_lights.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"light::shader::light_tiling_bin_lights\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/light-shader-light_tiling_bin_lights.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"light_tiling_bin_lights\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"lightshaderlight_tiling_bin_lights\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/light-shader-light_tiling_bin_lights.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"light_tiling_bin_lights\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/light_tiling_clear_tiles.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"light::shader::light_tiling_clear_tiles\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/light-shader-light_tiling_clear_tiles.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"light_tiling_clear_tiles\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"lightshaderlight_tiling_clear_tiles\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/light-shader-light_tiling_clear_tiles.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"light_tiling_clear_tiles\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"light::shader::light_tiling_compute_tile_min_and_max_depth\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\n            \"../../shaders/light-shader-light_tiling_compute_tile_min_and_max_depth.spv\"\n        )\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"light_tiling_compute_tile_min_and_max_depth\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"lightshaderlight_tiling_compute_tile_min_and_max_depth\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\n            \"../../shaders/light-shader-light_tiling_compute_tile_min_and_max_depth.wgsl\"\n        )\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"light_tiling_compute_tile_min_and_max_depth\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/light_tiling_compute_tile_min_and_max_depth_multisampled.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str =\n        \"light::shader::light_tiling_compute_tile_min_and_max_depth_multisampled\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\n            \"../../shaders/light-shader-light_tiling_compute_tile_min_and_max_depth_multisampled.\\\n             spv\"\n        )\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"light_tiling_compute_tile_min_and_max_depth_multisampled\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str =\n        \"lightshaderlight_tiling_compute_tile_min_and_max_depth_multisampled\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\n            \"../../shaders/light-shader-light_tiling_compute_tile_min_and_max_depth_multisampled.\\\n             wgsl\"\n        )\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"light_tiling_compute_tile_min_and_max_depth_multisampled\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/light_tiling_depth_pre_pass.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"light::shader::light_tiling_depth_pre_pass\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/light-shader-light_tiling_depth_pre_pass.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"light_tiling_depth_pre_pass\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"lightshaderlight_tiling_depth_pre_pass\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/light-shader-light_tiling_depth_pre_pass.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"light_tiling_depth_pre_pass\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/passthru_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"tutorial::passthru_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/tutorial-passthru_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"passthru_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"tutorialpassthru_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/tutorial-passthru_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"passthru_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/prefilter_environment_cubemap_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolution::shader::prefilter_environment_cubemap_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\n            \"../../shaders/convolution-shader-prefilter_environment_cubemap_fragment.spv\"\n        )\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"prefilter_environment_cubemap_fragment\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolutionshaderprefilter_environment_cubemap_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\n            \"../../shaders/convolution-shader-prefilter_environment_cubemap_fragment.wgsl\"\n        )\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"prefilter_environment_cubemap_fragment\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/prefilter_environment_cubemap_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolution::shader::prefilter_environment_cubemap_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\n            \"../../shaders/convolution-shader-prefilter_environment_cubemap_vertex.spv\"\n        )\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"prefilter_environment_cubemap_vertex\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"convolutionshaderprefilter_environment_cubemap_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\n            \"../../shaders/convolution-shader-prefilter_environment_cubemap_vertex.wgsl\"\n        )\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"prefilter_environment_cubemap_vertex\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/primitive_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"primitive::shader::primitive_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/primitive-shader-primitive_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"primitive_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"primitiveshaderprimitive_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/primitive-shader-primitive_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"primitive_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/primitive_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"primitive::shader::primitive_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/primitive-shader-primitive_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"primitive_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"primitiveshaderprimitive_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/primitive-shader-primitive_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"primitive_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/shadow_mapping_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"light::shader::shadow_mapping_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/light-shader-shadow_mapping_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"shadow_mapping_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"lightshadershadow_mapping_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/light-shader-shadow_mapping_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"shadow_mapping_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/shadow_mapping_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"light::shader::shadow_mapping_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/light-shader-shadow_mapping_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"shadow_mapping_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"lightshadershadow_mapping_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/light-shader-shadow_mapping_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"shadow_mapping_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/skybox_cubemap_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"skybox::shader::skybox_cubemap_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/skybox-shader-skybox_cubemap_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"skybox_cubemap_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"skyboxshaderskybox_cubemap_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/skybox-shader-skybox_cubemap_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"skybox_cubemap_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/skybox_cubemap_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"skybox::shader::skybox_cubemap_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/skybox-shader-skybox_cubemap_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"skybox_cubemap_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"skyboxshaderskybox_cubemap_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/skybox-shader-skybox_cubemap_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"skybox_cubemap_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/skybox_equirectangular_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"skybox::shader::skybox_equirectangular_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/skybox-shader-skybox_equirectangular_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"skybox_equirectangular_fragment\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"skyboxshaderskybox_equirectangular_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/skybox-shader-skybox_equirectangular_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"skybox_equirectangular_fragment\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/skybox_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"skybox::shader::skybox_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/skybox-shader-skybox_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"skybox_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"skyboxshaderskybox_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/skybox-shader-skybox_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"skybox_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/slabbed_renderlet.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"tutorial::slabbed_renderlet\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/tutorial-slabbed_renderlet.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"slabbed_renderlet\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"tutorialslabbed_renderlet\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/tutorial-slabbed_renderlet.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"slabbed_renderlet\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/slabbed_vertices.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"tutorial::slabbed_vertices\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/tutorial-slabbed_vertices.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"slabbed_vertices\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"tutorialslabbed_vertices\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/tutorial-slabbed_vertices.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"slabbed_vertices\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/slabbed_vertices_no_instance.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"tutorial::slabbed_vertices_no_instance\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/tutorial-slabbed_vertices_no_instance.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating native linkage for {}\",\n            \"slabbed_vertices_no_instance\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"tutorialslabbed_vertices_no_instance\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/tutorial-slabbed_vertices_no_instance.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\n            \"creating web linkage for {}\",\n            \"slabbed_vertices_no_instance\"\n        );\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/tonemapping_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"tonemapping::tonemapping_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/tonemapping-tonemapping_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"tonemapping_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"tonemappingtonemapping_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/tonemapping-tonemapping_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"tonemapping_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/tonemapping_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"tonemapping::tonemapping_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/tonemapping-tonemapping_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"tonemapping_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"tonemappingtonemapping_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/tonemapping-tonemapping_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"tonemapping_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/ui_fragment.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"ui_slab::shader::ui_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/ui_slab-shader-ui_fragment.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"ui_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"ui_slabshaderui_fragment\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/ui_slab-shader-ui_fragment.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"ui_fragment\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage/ui_vertex.rs",
    "content": "#![allow(dead_code)]\n//! Automatically generated by Renderling's `build.rs`.\nuse crate::linkage::ShaderLinkage;\n#[cfg(not(target_arch = \"wasm32\"))]\nmod target {\n    pub const ENTRY_POINT: &str = \"ui_slab::shader::ui_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_spirv!(\"../../shaders/ui_slab-shader-ui_vertex.spv\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating native linkage for {}\", \"ui_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\n#[cfg(target_arch = \"wasm32\")]\nmod target {\n    pub const ENTRY_POINT: &str = \"ui_slabshaderui_vertex\";\n    pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n        wgpu::include_wgsl!(\"../../shaders/ui_slab-shader-ui_vertex.wgsl\")\n    }\n    pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n        log::debug!(\"creating web linkage for {}\", \"ui_vertex\");\n        super::ShaderLinkage {\n            entry_point: ENTRY_POINT,\n            module: device.create_shader_module(descriptor()).into(),\n        }\n    }\n}\npub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n    target::linkage(device)\n}\n"
  },
  {
    "path": "crates/renderling/src/linkage.rs",
    "content": "//! Provides convenient wrappers around renderling shader linkage.\n//!\n//! For internal use.\n// # Warning!\n// Please don't put anything in `crates/renderling/src/linkage/*`.\n// The files there are all auto-generated by the shader compilation machinery.\n// It is common to delete everything in that directory and regenerate.\nuse std::sync::Arc;\n\npub mod atlas_blit_fragment;\npub mod atlas_blit_vertex;\npub mod bloom_downsample_fragment;\npub mod bloom_mix_fragment;\npub mod bloom_upsample_fragment;\npub mod bloom_vertex;\npub mod brdf_lut_convolution_fragment;\npub mod brdf_lut_convolution_vertex;\npub mod compositor_fragment;\npub mod compositor_vertex;\npub mod compute_copy_depth_to_pyramid;\npub mod compute_copy_depth_to_pyramid_multisampled;\npub mod compute_culling;\npub mod compute_downsample_depth_pyramid;\npub mod cubemap_sampling_test_fragment;\npub mod cubemap_sampling_test_vertex;\npub mod debug_overlay_fragment;\npub mod debug_overlay_vertex;\npub mod di_convolution_fragment;\npub mod generate_mipmap_fragment;\npub mod generate_mipmap_vertex;\npub mod light_tiling_bin_lights;\npub mod light_tiling_clear_tiles;\npub mod light_tiling_compute_tile_min_and_max_depth;\npub mod light_tiling_compute_tile_min_and_max_depth_multisampled;\npub mod light_tiling_depth_pre_pass;\npub mod prefilter_environment_cubemap_fragment;\npub mod prefilter_environment_cubemap_vertex;\npub mod primitive_fragment;\npub mod primitive_vertex;\npub mod shadow_mapping_fragment;\npub mod shadow_mapping_vertex;\npub mod skybox_cubemap_fragment;\npub mod skybox_cubemap_vertex;\npub mod skybox_equirectangular_fragment;\npub mod skybox_vertex;\npub mod tonemapping_fragment;\npub mod tonemapping_vertex;\n\n// 2D/UI shaders\npub mod ui_fragment;\npub mod ui_vertex;\n\n// Tutorial shaders\npub mod implicit_isosceles_vertex;\npub mod passthru_fragment;\npub mod slabbed_renderlet;\npub mod slabbed_vertices;\npub mod slabbed_vertices_no_instance;\n\npub struct ShaderLinkage {\n    pub module: Arc<wgpu::ShaderModule>,\n    pub entry_point: &'static str,\n}\n\n#[cfg(test)]\nmod test {\n    use naga::valid::ValidationFlags;\n\n    #[test]\n    // Ensure that the shaders can be converted to WGSL.\n    // This is necessary for WASM using WebGPU, because WebGPU only accepts\n    // WGSL as a shading language.\n    fn validate_shaders() {\n        fn validate_src(path: &std::path::PathBuf) {\n            log::info!(\"validating source\");\n            log::info!(\"  reading '{}'\", path.display());\n            let bytes = std::fs::read(path).unwrap();\n            log::info!(\"  {:0.2}k bytes read\", bytes.len() as f32 / 1000.0);\n            let opts = naga::front::spv::Options::default();\n            let module = match naga::front::spv::parse_u8_slice(&bytes, &opts) {\n                Ok(m) => m,\n                Err(e) => {\n                    log::error!(\"{e}\");\n                    panic!(\"SPIR-V parse error\");\n                }\n            };\n            log::info!(\"  SPIR-V parsed\");\n            let mut validator =\n                naga::valid::Validator::new(Default::default(), naga::valid::Capabilities::empty());\n            let is_valid;\n            let info = match validator.validate(&module) {\n                Ok(i) => {\n                    is_valid = true;\n                    log::info!(\"  SPIR-V validated\");\n                    i\n                }\n                Err(e) => {\n                    log::error!(\"{}\", e.emit_to_string(\"\"));\n                    is_valid = false;\n                    let mut validator = naga::valid::Validator::new(\n                        ValidationFlags::empty(),\n                        naga::valid::Capabilities::empty(),\n                    );\n                    validator.validate(&module).unwrap()\n                }\n            };\n            let wgsl = naga::back::wgsl::write_string(\n                &module,\n                &info,\n                naga::back::wgsl::WriterFlags::empty(),\n            )\n            .unwrap();\n            log::info!(\"  output WGSL generated\");\n\n            let print_var_name = path\n                .file_stem()\n                .unwrap()\n                .to_str()\n                .unwrap()\n                .replace('-', \"_\");\n            let maybe_output_path = if std::env::var(\"print_wgsl\").is_ok() || !is_valid {\n                let dir = std::path::PathBuf::from(\"../../test_output\");\n                std::fs::create_dir_all(&dir).unwrap();\n                let output_path = dir.join(print_var_name).with_extension(\"wgsl\");\n                log::info!(\"writing WGSL to '{}'\", output_path.display());\n                Some(output_path)\n            } else {\n                log::info!(\"  to save the generated WGSL, use an env var 'print_wgsl=1'\");\n                None\n            };\n\n            if let Some(output_path) = maybe_output_path {\n                std::fs::write(&output_path, &wgsl).unwrap();\n                log::info!(\"  wrote generated WGSL to {}\", output_path.display());\n            }\n\n            if !is_valid {\n                panic!(\"SPIR-V validation error\");\n            }\n\n            let module = match naga::front::wgsl::parse_str(&wgsl) {\n                Ok(m) => m,\n                Err(e) => {\n                    log::error!(\"{}\", e.emit_to_string(&wgsl));\n                    panic!(\"wgsl parse error\");\n                }\n            };\n            log::info!(\"  output WGSL parsed\");\n            let mut validator =\n                naga::valid::Validator::new(Default::default(), naga::valid::Capabilities::empty());\n            let _info = match validator.validate(&module) {\n                Ok(i) => i,\n                Err(e) => {\n                    log::error!(\"{}\", e.emit_to_string(&wgsl));\n                    panic!(\"wgsl validation error\");\n                }\n            };\n            log::info!(\"  wgsl output validated\");\n        }\n\n        let may_entries = std::fs::read_dir(\"src/linkage\").unwrap();\n        for may_entry in may_entries {\n            let entry = may_entry.unwrap();\n            let path = entry.path();\n            let ext = path.extension().unwrap().to_str().unwrap();\n            if let Ok(filename) = std::env::var(\"only_shader\") {\n                let stem = path.file_stem().unwrap().to_str().unwrap();\n                if filename != stem {\n                    log::info!(\n                        \"  '{}' doesn't match 'only_shader' env '{}', skipping\",\n                        filename,\n                        stem\n                    );\n                    continue;\n                }\n            }\n            if ext == \"spv\" {\n                validate_src(&path);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/material/cpu.rs",
    "content": "//! CPU side of materials.\nuse std::sync::{Arc, Mutex};\n\nuse craballoc::{\n    // Craballoc is used for memory allocation and management.\n    runtime::WgpuRuntime,\n    slab::{SlabAllocator, SlabBuffer},\n    value::Hybrid,\n};\nuse crabslab::Id;\nuse glam::{Vec3, Vec4};\n\nuse crate::{\n    atlas::{Atlas, AtlasTexture},\n    material::shader::MaterialDescriptor,\n};\n\n/// Wrapper around the materials slab, which holds material textures in an\n/// atlas.\n#[derive(Clone)]\npub struct Materials {\n    slab: SlabAllocator<WgpuRuntime>,\n    atlas: Atlas,\n    default_material: Material,\n}\n\nimpl AsRef<WgpuRuntime> for Materials {\n    fn as_ref(&self) -> &WgpuRuntime {\n        self.slab.runtime()\n    }\n}\n\nimpl Materials {\n    /// Creates a new `Materials` instance with the specified runtime and atlas\n    /// size.\n    ///\n    /// # Arguments\n    ///\n    /// * `runtime` - A reference to the WgpuRuntime.\n    /// * `atlas_size` - The size of the atlas texture.\n    pub fn new(runtime: impl AsRef<WgpuRuntime>, atlas_size: wgpu::Extent3d) -> Self {\n        let slab = SlabAllocator::new(runtime, \"materials\", wgpu::BufferUsages::empty());\n        let atlas = Atlas::new(&slab, atlas_size, None, Some(\"materials-atlas\"), None);\n        let default_material = Material {\n            descriptor: slab.new_value(Default::default()),\n            albedo_texture: Default::default(),\n            metallic_roughness_texture: Default::default(),\n            normal_mapping_texture: Default::default(),\n            ao_texture: Default::default(),\n            emissive_texture: Default::default(),\n        };\n        Self {\n            slab,\n            atlas,\n            default_material,\n        }\n    }\n\n    /// Returns a reference to the WgpuRuntime.\n    pub fn runtime(&self) -> &WgpuRuntime {\n        self.as_ref()\n    }\n\n    /// Returns a reference to the slab allocator.\n    pub fn slab_allocator(&self) -> &SlabAllocator<WgpuRuntime> {\n        &self.slab\n    }\n\n    /// Returns a reference to the atlas.\n    pub fn atlas(&self) -> &Atlas {\n        &self.atlas\n    }\n\n    /// Returns the default material.\n    pub fn default_material(&self) -> &Material {\n        &self.default_material\n    }\n\n    /// Runs atlas upkeep and commits all changes to the GPU.\n    ///\n    /// Returns `true` if the atlas texture was recreated.\n    #[must_use]\n    pub fn commit(&self) -> (bool, SlabBuffer<wgpu::Buffer>) {\n        // Atlas upkeep must be called first because it generates updates into the slab\n        (self.atlas.upkeep(self.runtime()), self.slab.commit())\n    }\n\n    /// Stage a new [`Material`] on the materials slab.\n    pub fn new_material(&self) -> Material {\n        let descriptor = self.slab.new_value(MaterialDescriptor::default());\n        Material {\n            descriptor,\n            albedo_texture: Default::default(),\n            metallic_roughness_texture: Default::default(),\n            normal_mapping_texture: Default::default(),\n            ao_texture: Default::default(),\n            emissive_texture: Default::default(),\n        }\n    }\n}\n\n/// A material staged on the GPU.\n///\n/// Internally a `Material` holds references to:\n/// * its descriptor, [`MaterialDescriptor`], which lives on the GPU\n/// * [`AtlasTexture`]s that determine how the material presents:\n///   * albedo color\n///   * metallic roughness\n///   * normal mapping\n///   * ambient occlusion\n///   * emissive\n///\n/// ## Note\n///\n/// Clones of `Material` all point to the same underlying data.\n/// Changing a value on one `Material` will change that value for all clones as\n/// well.\n#[derive(Clone)]\npub struct Material {\n    descriptor: Hybrid<MaterialDescriptor>,\n\n    albedo_texture: Arc<Mutex<Option<AtlasTexture>>>,\n    metallic_roughness_texture: Arc<Mutex<Option<AtlasTexture>>>,\n    normal_mapping_texture: Arc<Mutex<Option<AtlasTexture>>>,\n    ao_texture: Arc<Mutex<Option<AtlasTexture>>>,\n    emissive_texture: Arc<Mutex<Option<AtlasTexture>>>,\n}\n\nimpl From<&Material> for Material {\n    fn from(value: &Material) -> Self {\n        value.clone()\n    }\n}\n\nimpl Material {\n    /// Returns the unique identifier of the material descriptor.\n    pub fn id(&self) -> Id<MaterialDescriptor> {\n        self.descriptor.id()\n    }\n\n    /// Returns a copy of the underlying descriptor.\n    pub fn descriptor(&self) -> MaterialDescriptor {\n        self.descriptor.get()\n    }\n\n    /// Sets the emissive factor of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The emissive factor as a `Vec3`.\n    pub fn set_emissive_factor(&self, param: Vec3) -> &Self {\n        self.descriptor.modify(|d| d.emissive_factor = param);\n        self\n    }\n    /// Sets the emissive factor.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The emissive factor as a `Vec3`.\n    pub fn with_emissive_factor(self, param: Vec3) -> Self {\n        self.set_emissive_factor(param);\n        self\n    }\n    /// Sets the emissive strength multiplier of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The emissive strength multiplier as a `f32`.\n    pub fn set_emissive_strength_multiplier(&self, param: f32) -> &Self {\n        self.descriptor\n            .modify(|d| d.emissive_strength_multiplier = param);\n        self\n    }\n    /// Sets the emissive strength multiplier.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The emissive strength multiplier as a `f32`.\n    pub fn with_emissive_strength_multiplier(self, param: f32) -> Self {\n        self.set_emissive_strength_multiplier(param);\n        self\n    }\n    /// Sets the albedo factor of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The albedo factor as a `Vec4`.\n    pub fn set_albedo_factor(&self, param: Vec4) -> &Self {\n        self.descriptor.modify(|d| d.albedo_factor = param);\n        self\n    }\n    /// Sets the albedo factor.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The albedo factor as a `Vec4`.\n    pub fn with_albedo_factor(self, param: Vec4) -> Self {\n        self.set_albedo_factor(param);\n        self\n    }\n    /// Sets the metallic factor of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The metallic factor as a `f32`.\n    pub fn set_metallic_factor(&self, param: f32) -> &Self {\n        self.descriptor.modify(|d| d.metallic_factor = param);\n        self\n    }\n    /// Sets the metallic factor.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The metallic factor as a `f32`.\n    pub fn with_metallic_factor(self, param: f32) -> Self {\n        self.set_metallic_factor(param);\n        self\n    }\n    /// Sets the roughness factor of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The roughness factor as a `f32`.\n    pub fn set_roughness_factor(&self, param: f32) -> &Self {\n        self.descriptor.modify(|d| d.roughness_factor = param);\n        self\n    }\n    /// Sets the roughness factor.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The roughness factor as a `f32`.\n    pub fn with_roughness_factor(self, param: f32) -> Self {\n        self.set_roughness_factor(param);\n        self\n    }\n    /// Sets the albedo texture coordinate of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The texture coordinate as a `u32`.\n    pub fn set_albedo_tex_coord(&self, param: u32) -> &Self {\n        self.descriptor.modify(|d| d.albedo_tex_coord = param);\n        self\n    }\n    /// Sets the albedo texture coordinate.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The texture coordinate as a `u32`.\n    pub fn with_albedo_tex_coord(self, param: u32) -> Self {\n        self.set_albedo_tex_coord(param);\n        self\n    }\n    /// Sets the metallic roughness texture coordinate of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The texture coordinate as a `u32`.\n    pub fn set_metallic_roughness_tex_coord(&self, param: u32) -> &Self {\n        self.descriptor\n            .modify(|d| d.metallic_roughness_tex_coord = param);\n        self\n    }\n    /// Sets the metallic roughness texture coordinate.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The texture coordinate as a `u32`.\n    pub fn with_metallic_roughness_tex_coord(self, param: u32) -> Self {\n        self.set_metallic_roughness_tex_coord(param);\n        self\n    }\n    /// Sets the normal texture coordinate of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The texture coordinate as a `u32`.\n    pub fn set_normal_tex_coord(&self, param: u32) -> &Self {\n        self.descriptor.modify(|d| d.normal_tex_coord = param);\n        self\n    }\n    /// Sets the normal texture coordinate.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The texture coordinate as a `u32`.\n    pub fn with_normal_tex_coord(self, param: u32) -> Self {\n        self.set_normal_tex_coord(param);\n        self\n    }\n    /// Sets the ambient occlusion texture coordinate of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The texture coordinate as a `u32`.\n    pub fn set_ambient_occlusion_tex_coord(&self, param: u32) -> &Self {\n        self.descriptor.modify(|d| d.ao_tex_coord = param);\n        self\n    }\n    /// Sets the ambient occlusion texture coordinate.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The texture coordinate as a `u32`.\n    pub fn with_ambient_occlusion_tex_coord(self, param: u32) -> Self {\n        self.set_ambient_occlusion_tex_coord(param);\n        self\n    }\n    /// Sets the emissive texture coordinate of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The texture coordinate as a `u32`.\n    pub fn set_emissive_tex_coord(&self, param: u32) -> &Self {\n        self.descriptor.modify(|d| d.emissive_tex_coord = param);\n        self\n    }\n    /// Sets the emissive texture coordinate.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The texture coordinate as a `u32`.\n    pub fn with_emissive_tex_coord(self, param: u32) -> Self {\n        self.set_emissive_tex_coord(param);\n        self\n    }\n    /// Sets whether the material has lighting.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - A boolean indicating if the material has lighting.\n    pub fn set_has_lighting(&self, param: bool) -> &Self {\n        self.descriptor.modify(|d| d.has_lighting = param);\n        self\n    }\n    /// Sets whether the material has lighting.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - A boolean indicating if the material has lighting.\n    pub fn with_has_lighting(self, param: bool) -> Self {\n        self.set_has_lighting(param);\n        self\n    }\n    /// Sets the ambient occlusion strength of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The ambient occlusion strength as a `f32`.\n    pub fn set_ambient_occlusion_strength(&self, param: f32) -> &Self {\n        self.descriptor.modify(|d| d.ao_strength = param);\n        self\n    }\n    /// Sets the ambient occlusion strength.\n    ///\n    /// # Arguments\n    ///\n    /// * `param` - The ambient occlusion strength as a `f32`.\n    pub fn with_ambient_occlusion_strength(self, param: f32) -> Self {\n        self.set_ambient_occlusion_strength(param);\n        self\n    }\n\n    /// Remove the albedo texture.\n    ///\n    /// This causes any `[Primitive]` that references this material to fall back\n    /// to using the albedo factor for color.\n    pub fn remove_albedo_texture(&self) {\n        self.descriptor.modify(|d| d.albedo_texture_id = Id::NONE);\n        self.albedo_texture\n            .lock()\n            .expect(\"albedo_texture lock\")\n            .take();\n    }\n\n    /// Sets the albedo color texture.\n    pub fn set_albedo_texture(&self, texture: &AtlasTexture) -> &Self {\n        self.descriptor\n            .modify(|d| d.albedo_texture_id = texture.id());\n        *self.albedo_texture.lock().expect(\"albedo_texture lock\") = Some(texture.clone());\n        self\n    }\n\n    /// Replace the albedo texture.\n    pub fn with_albedo_texture(self, texture: &AtlasTexture) -> Self {\n        self.descriptor\n            .modify(|d| d.albedo_texture_id = texture.id());\n        *self.albedo_texture.lock().expect(\"albedo_texture lock\") = Some(texture.clone());\n        self\n    }\n\n    /// Remove the metallic roughness texture.\n    ///\n    /// This causes any `[Renderlet]` that references this material to fall back\n    /// to using the metallic and roughness factors for appearance.\n    pub fn remove_metallic_roughness_texture(&self) {\n        self.descriptor\n            .modify(|d| d.metallic_roughness_texture_id = Id::NONE);\n        self.metallic_roughness_texture\n            .lock()\n            .expect(\"metallic_roughness_texture lock\")\n            .take();\n    }\n\n    /// Sets the metallic roughness texture of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `texture` - A reference to the metallic roughness `AtlasTexture`.\n    pub fn set_metallic_roughness_texture(&self, texture: &AtlasTexture) -> &Self {\n        self.descriptor\n            .modify(|d| d.metallic_roughness_texture_id = texture.id());\n        *self\n            .metallic_roughness_texture\n            .lock()\n            .expect(\"metallic_roughness_texture lock\") = Some(texture.clone());\n        self\n    }\n\n    /// Sets the metallic roughness texture and returns the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `texture` - A reference to the metallic roughness `AtlasTexture`.\n    pub fn with_metallic_roughness_texture(self, texture: &AtlasTexture) -> Self {\n        self.set_metallic_roughness_texture(texture);\n        self\n    }\n\n    /// Remove the normal texture.\n    ///\n    /// This causes any `[Renderlet]` that references this material to fall back\n    /// to using the default normal mapping.\n    pub fn remove_normal_texture(&self) {\n        self.descriptor.modify(|d| d.normal_texture_id = Id::NONE);\n        self.normal_mapping_texture\n            .lock()\n            .expect(\"normal_mapping_texture lock\")\n            .take();\n    }\n\n    /// Sets the normal texture of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `texture` - A reference to the normal `AtlasTexture`.\n    pub fn set_normal_texture(&self, texture: &AtlasTexture) -> &Self {\n        self.descriptor\n            .modify(|d| d.normal_texture_id = texture.id());\n        *self\n            .normal_mapping_texture\n            .lock()\n            .expect(\"normal_mapping_texture lock\") = Some(texture.clone());\n        self\n    }\n\n    /// Sets the normal texture and returns the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `texture` - A reference to the normal `AtlasTexture`.\n    pub fn with_normal_texture(self, texture: &AtlasTexture) -> Self {\n        self.set_normal_texture(texture);\n        self\n    }\n\n    /// Remove the ambient occlusion texture.\n    ///\n    /// This causes any `[Renderlet]` that references this material to fall back\n    /// to using the default ambient occlusion.\n    pub fn remove_ambient_occlusion_texture(&self) {\n        self.descriptor.modify(|d| d.ao_texture_id = Id::NONE);\n        self.ao_texture.lock().expect(\"ao_texture lock\").take();\n    }\n\n    /// Sets the ambient occlusion texture of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `texture` - A reference to the ambient occlusion `AtlasTexture`.\n    pub fn set_ambient_occlusion_texture(&self, texture: &AtlasTexture) -> &Self {\n        self.descriptor.modify(|d| d.ao_texture_id = texture.id());\n        *self.ao_texture.lock().expect(\"ao_texture lock\") = Some(texture.clone());\n        self\n    }\n\n    /// Sets the ambient occlusion texture and returns the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `texture` - A reference to the ambient occlusion `AtlasTexture`.\n    pub fn with_ambient_occlusion_texture(self, texture: &AtlasTexture) -> Self {\n        self.set_ambient_occlusion_texture(texture);\n        self\n    }\n\n    /// Remove the emissive texture.\n    ///\n    /// This causes any `[Renderlet]` that references this material to fall back\n    /// to using the emissive factor for appearance.\n    pub fn remove_emissive_texture(&self) {\n        self.descriptor.modify(|d| d.emissive_texture_id = Id::NONE);\n        self.emissive_texture\n            .lock()\n            .expect(\"emissive_texture lock\")\n            .take();\n    }\n\n    /// Sets the emissive texture of the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `texture` - A reference to the emissive `AtlasTexture`.\n    pub fn set_emissive_texture(&self, texture: &AtlasTexture) -> &Self {\n        self.descriptor\n            .modify(|d| d.emissive_texture_id = texture.id());\n        *self.emissive_texture.lock().expect(\"emissive_texture lock\") = Some(texture.clone());\n        self\n    }\n\n    /// Sets the emissive texture and returns the material.\n    ///\n    /// # Arguments\n    ///\n    /// * `texture` - A reference to the emissive `AtlasTexture`.\n    pub fn with_emissive_texture(self, texture: &AtlasTexture) -> Self {\n        self.set_emissive_texture(texture);\n        self\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/material.rs",
    "content": "//! Atlas images, used for materials. CPU and GPU.\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\npub mod shader {\n    //! Material shader types.\n\n    use crabslab::{Id, SlabItem};\n    use glam::{Vec3, Vec4};\n\n    use crate::atlas::shader::AtlasTextureDescriptor;\n\n    /// Represents a material on the GPU.\n    #[repr(C)]\n    #[derive(Clone, Copy, PartialEq, SlabItem, core::fmt::Debug)]\n    pub struct MaterialDescriptor {\n        pub emissive_factor: Vec3,\n        pub emissive_strength_multiplier: f32,\n        pub albedo_factor: Vec4,\n        pub metallic_factor: f32,\n        pub roughness_factor: f32,\n\n        pub albedo_texture_id: Id<AtlasTextureDescriptor>,\n        pub metallic_roughness_texture_id: Id<AtlasTextureDescriptor>,\n        pub normal_texture_id: Id<AtlasTextureDescriptor>,\n        pub ao_texture_id: Id<AtlasTextureDescriptor>,\n        pub emissive_texture_id: Id<AtlasTextureDescriptor>,\n\n        pub albedo_tex_coord: u32,\n        pub metallic_roughness_tex_coord: u32,\n        pub normal_tex_coord: u32,\n        pub ao_tex_coord: u32,\n        pub emissive_tex_coord: u32,\n\n        pub has_lighting: bool,\n        pub ao_strength: f32,\n    }\n\n    impl Default for MaterialDescriptor {\n        fn default() -> Self {\n            Self {\n                emissive_factor: Vec3::ZERO,\n                emissive_strength_multiplier: 1.0,\n                albedo_factor: Vec4::ONE,\n                metallic_factor: 1.0,\n                roughness_factor: 1.0,\n                albedo_texture_id: Id::NONE,\n                metallic_roughness_texture_id: Id::NONE,\n                normal_texture_id: Id::NONE,\n                ao_texture_id: Id::NONE,\n                albedo_tex_coord: 0,\n                metallic_roughness_tex_coord: 0,\n                normal_tex_coord: 0,\n                ao_tex_coord: 0,\n                has_lighting: true,\n                ao_strength: 0.0,\n                emissive_texture_id: Id::NONE,\n                emissive_tex_coord: 0,\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/math.rs",
    "content": "//! Mathematical helper types and functions.\n//!\n//! Primarily this module adds some traits to help using `glam` types on the GPU\n//! without panicking, as well as a few traits to aid in writing generic shader\n//! code that can be run on the CPU.\n//!\n//! Lastly, it provides some common geometry and constants used in many shaders.\nuse core::ops::Mul;\nuse spirv_std::{\n    image::{sample_with, Cubemap, Image2d, Image2dArray, ImageWithMethods},\n    Image, Sampler,\n};\n\nuse glam::*;\npub use spirv_std::num_traits::{clamp, Float, Zero};\n\npub trait Fetch<Coords> {\n    type Output;\n\n    fn fetch(&self, coords: Coords) -> Self::Output;\n}\n\nimpl Fetch<UVec2> for Image!(2D, type=f32, sampled, depth) {\n    type Output = Vec4;\n\n    fn fetch(&self, coords: UVec2) -> Self::Output {\n        self.fetch_with(coords, sample_with::lod(0))\n    }\n}\n\nimpl Fetch<UVec2> for Image!(2D, type=f32, sampled, depth, multisampled=true) {\n    type Output = Vec4;\n\n    fn fetch(&self, coords: UVec2) -> Self::Output {\n        // TODO: check whether this is doing what we think it's doing.\n        // (We think its doing roughly the same thing as the non-multisampled version\n        // above)\n        self.fetch_with(coords, sample_with::sample_index(0))\n    }\n}\n\npub trait IsSampler: Copy + Clone {}\n\nimpl IsSampler for () {}\n\nimpl IsSampler for Sampler {}\n\npub trait Sample2d {\n    type Sampler: IsSampler;\n\n    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec2, lod: f32) -> glam::Vec4;\n}\n\nimpl Sample2d for Image2d {\n    type Sampler = Sampler;\n\n    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec2, lod: f32) -> glam::Vec4 {\n        self.sample_by_lod(sampler, uv, lod)\n    }\n}\n\nimpl Sample2d for Image!(2D, type=f32, sampled, depth) {\n    type Sampler = Sampler;\n\n    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec2, lod: f32) -> glam::Vec4 {\n        self.sample_by_lod(sampler, uv, lod)\n    }\n}\n\npub trait Sample2dArray {\n    type Sampler: IsSampler;\n\n    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec3, lod: f32) -> glam::Vec4;\n}\n\nimpl Sample2dArray for Image2dArray {\n    type Sampler = Sampler;\n\n    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec3, lod: f32) -> glam::Vec4 {\n        self.sample_by_lod(sampler, uv, lod)\n    }\n}\n\nimpl Sample2dArray for Image!(2D, type=f32, sampled, arrayed, depth) {\n    type Sampler = Sampler;\n\n    fn sample_by_lod(&self, sampler: Self::Sampler, uv: glam::Vec3, lod: f32) -> glam::Vec4 {\n        self.sample_by_lod(sampler, uv, lod)\n    }\n}\n\npub trait SampleCube {\n    type Sampler: IsSampler;\n\n    fn sample_by_lod(&self, sampler: Self::Sampler, uv: Vec3, lod: f32) -> glam::Vec4;\n}\n\nimpl SampleCube for Cubemap {\n    type Sampler = Sampler;\n\n    fn sample_by_lod(&self, sampler: Self::Sampler, uv: Vec3, lod: f32) -> glam::Vec4 {\n        self.sample_by_lod(sampler, uv, lod)\n    }\n}\n\n#[cfg(not(target_arch = \"spirv\"))]\nmod cpu {\n\n    use super::*;\n\n    /// A CPU texture with no dimensions that always returns the same\n    /// value when sampled.\n    pub struct ConstTexture(Vec4);\n\n    impl Fetch<UVec2> for ConstTexture {\n        type Output = Vec4;\n\n        fn fetch(&self, _coords: UVec2) -> Self::Output {\n            self.0\n        }\n    }\n\n    impl Sample2d for ConstTexture {\n        type Sampler = ();\n\n        fn sample_by_lod(&self, _sampler: Self::Sampler, _uv: glam::Vec2, _lod: f32) -> Vec4 {\n            self.0\n        }\n    }\n\n    impl Sample2dArray for ConstTexture {\n        type Sampler = ();\n\n        fn sample_by_lod(&self, _sampler: Self::Sampler, _uv: glam::Vec3, _lod: f32) -> glam::Vec4 {\n            self.0\n        }\n    }\n\n    impl SampleCube for ConstTexture {\n        type Sampler = ();\n\n        fn sample_by_lod(&self, _sampler: Self::Sampler, _uv: Vec3, _lod: f32) -> glam::Vec4 {\n            self.0\n        }\n    }\n\n    impl ConstTexture {\n        pub fn new(value: Vec4) -> Self {\n            Self(value)\n        }\n    }\n\n    #[derive(Debug)]\n    pub struct CpuTexture2d<P: image::Pixel, Container> {\n        pub image: image::ImageBuffer<P, Container>,\n        convert_fn: fn(&P) -> Vec4,\n    }\n\n    impl<P: image::Pixel, Container> CpuTexture2d<P, Container> {\n        pub fn from_image(\n            image: image::ImageBuffer<P, Container>,\n            convert_fn: fn(&P) -> Vec4,\n        ) -> Self {\n            Self { image, convert_fn }\n        }\n    }\n\n    impl<P, Container> Fetch<UVec2> for CpuTexture2d<P, Container>\n    where\n        P: image::Pixel,\n        Container: std::ops::Deref<Target = [P::Subpixel]>,\n    {\n        type Output = Vec4;\n\n        fn fetch(&self, coords: UVec2) -> Self::Output {\n            let x = coords.x.clamp(0, self.image.width() - 1);\n            let y = coords.y.clamp(0, self.image.height() - 1);\n            let p = self.image.get_pixel(x, y);\n            (self.convert_fn)(p)\n        }\n    }\n\n    impl<P, Container> Sample2d for CpuTexture2d<P, Container>\n    where\n        P: image::Pixel,\n        Container: std::ops::Deref<Target = [P::Subpixel]>,\n    {\n        type Sampler = ();\n\n        fn sample_by_lod(&self, _sampler: Self::Sampler, uv: glam::Vec2, _lod: f32) -> Vec4 {\n            // TODO: lerp the CPU texture sampling\n            // TODO: use configurable wrap mode on CPU sampling\n            let px = uv.x.clamp(0.0, 1.0) * (self.image.width() as f32 - 1.0);\n            let py = uv.y.clamp(0.0, 1.0) * (self.image.height() as f32 - 1.0);\n            self.fetch(UVec2::new(px.round() as u32, py.round() as u32))\n        }\n    }\n\n    pub struct CpuTexture2dArray<P: image::Pixel, Container> {\n        pub images: Vec<image::ImageBuffer<P, Container>>,\n        convert_fn: fn(&P) -> Vec4,\n    }\n\n    impl<P: image::Pixel, Container> CpuTexture2dArray<P, Container> {\n        pub fn from_images(\n            images: impl IntoIterator<Item = image::ImageBuffer<P, Container>>,\n            convert_fn: fn(&P) -> Vec4,\n        ) -> Self {\n            let images = images.into_iter().collect();\n            Self { images, convert_fn }\n        }\n    }\n\n    impl<P, Container> Sample2dArray for CpuTexture2dArray<P, Container>\n    where\n        P: image::Pixel,\n        Container: std::ops::Deref<Target = [P::Subpixel]>,\n    {\n        type Sampler = ();\n\n        /// Panics if `uv.z` is greater than length of images.\n        fn sample_by_lod(&self, _sampler: Self::Sampler, uv: glam::Vec3, _lod: f32) -> Vec4 {\n            // TODO: lerp the CPU texture sampling\n            // TODO: use configurable wrap mode on CPU sampling\n            let img = &self.images[uv.z as usize];\n            let px = (uv.x.clamp(0.0, 1.0) * (img.width() as f32 - 1.0)).round() as u32;\n            let py = (uv.y.clamp(0.0, 1.0) * (img.height() as f32 - 1.0)).round() as u32;\n            println!(\"sampling: ({px}, {py})\");\n            let p = img.get_pixel(px, py);\n            (self.convert_fn)(p)\n        }\n    }\n\n    /// A CPU-side cubemap texture.\n    ///\n    /// Provided primarily for testing purposes.\n    #[derive(Default)]\n    pub struct CpuCubemap {\n        pub images: [image::DynamicImage; 6],\n    }\n\n    impl SampleCube for CpuCubemap {\n        type Sampler = ();\n\n        fn sample_by_lod(\n            &self,\n            _sampler: Self::Sampler,\n            direction: glam::Vec3,\n            _lod: f32,\n        ) -> glam::Vec4 {\n            crate::cubemap::cpu_sample_cubemap(&self.images, direction)\n        }\n    }\n\n    /// Convert a u8 in range 0-255 to an f32 in range 0.0 - 1.0.\n    pub fn scaled_u8_to_f32(u: u8) -> f32 {\n        u as f32 / 255.0\n    }\n\n    pub fn luma_u8_to_vec4(p: &image::Luma<u8>) -> Vec4 {\n        let shade = scaled_u8_to_f32(p.0[0]);\n        Vec3::splat(shade).extend(1.0)\n    }\n\n    /// Convert an f32 in range 0.0 - 1.0 into a u8 in range 0-255.\n    pub fn scaled_f32_to_u8(f: f32) -> u8 {\n        (f * 255.0) as u8\n    }\n\n    /// Convert a u32 in rang 0-u32::MAX to a u8 in rang 0-255.\n    pub fn scaled_u32_to_u8(u: u32) -> u8 {\n        ((u as f32 / u32::MAX as f32) * 255.0) as u8\n    }\n}\n#[cfg(not(target_arch = \"spirv\"))]\npub use cpu::*;\n\n/// Additional/replacement methods for glam vector types.\n///\n/// These are required because `naga` (`wgpu`'s translation layer) doesn't like\n/// certain contstants like `f32::INFINITY` or `f32::NaN`, which cause errors in\n/// naga's WGSL output.\n///\n/// See [this issue](https://github.com/gfx-rs/naga/issues/2461) and `crate::linkage::test`\n/// for more info.\npub trait IsVector {\n    /// Type returned by the `orthogonal_vectors` extension function.\n    type OrthogonalVectors;\n\n    /// Normalize or return zero.\n    fn alt_norm_or_zero(&self) -> Self;\n\n    /// Return a vector with `signum_or_zero` applied to each component.\n    fn signum_or_zero(&self) -> Self;\n\n    /// Returns the dot product of a vector with itself (the square of its\n    /// length).\n    fn dot2(&self) -> f32;\n\n    /// Returns normalized orthogonal vectors.\n    fn orthonormal_vectors(&self) -> Self::OrthogonalVectors;\n}\n\nimpl IsVector for glam::Vec2 {\n    type OrthogonalVectors = Vec2;\n\n    fn alt_norm_or_zero(&self) -> Self {\n        if self.length().is_zero() {\n            glam::Vec2::ZERO\n        } else {\n            self.normalize()\n        }\n    }\n\n    fn signum_or_zero(&self) -> Self {\n        Vec2::new(signum_or_zero(self.x), signum_or_zero(self.y))\n    }\n\n    fn dot2(&self) -> f32 {\n        self.dot(*self)\n    }\n\n    fn orthonormal_vectors(&self) -> Self::OrthogonalVectors {\n        Vec3::new(self.x, self.y, 0.0).cross(Vec3::Z).xy()\n    }\n}\n\nimpl IsVector for glam::Vec3 {\n    type OrthogonalVectors = [Vec3; 2];\n\n    fn alt_norm_or_zero(&self) -> Self {\n        if self.length().is_zero() {\n            glam::Vec3::ZERO\n        } else {\n            self.normalize()\n        }\n    }\n\n    fn signum_or_zero(&self) -> Self {\n        Vec3::new(\n            signum_or_zero(self.x),\n            signum_or_zero(self.y),\n            signum_or_zero(self.z),\n        )\n    }\n\n    fn dot2(&self) -> f32 {\n        self.dot(*self)\n    }\n\n    fn orthonormal_vectors(&self) -> Self::OrthogonalVectors {\n        // From https://graphics.pixar.com/library/OrthonormalB/paper.pdf\n        let s = self.alt_norm_or_zero();\n        let sign = signum_or_zero(s.z);\n        let a = -1.0 / (sign + s.z);\n        let b = s.x * s.y * a;\n        [\n            Self::new(1.0 + sign * s.x * s.x * a, sign * b, -sign * s.x),\n            Self::new(b, sign + s.y * s.y * a, -s.y),\n        ]\n    }\n}\n\n/// Quantize an f32\n///\n/// Determine the distance from a point to a line segment.\npub fn distance_to_line(p: Vec3, a: Vec3, b: Vec3) -> f32 {\n    let ab_distance = a.distance(b);\n    if ab_distance <= f32::EPSILON {\n        p.distance(a)\n    } else {\n        let tri_area = (p - a).cross(p - b).length();\n        tri_area / ab_distance\n    }\n}\n\n/// Additional/replacement methods for glam matrix types.\n///\n/// These are required because `naga` (`wgpu`'s translation layer) doesn't like\n/// certain contstants like `f32::INFINITY` or `f32::NaN`, which cause errors in\n/// naga's WGSL output.\n///\n/// See [this issue](https://github.com/gfx-rs/naga/issues/2461) and `crate::linkage::test`\n/// for more info.\npub trait IsMatrix {\n    /// Extracts `scale`, `rotation` and `translation` from `self`. The input\n    /// matrix is expected to be a 3D affine transformation matrix otherwise\n    /// the output will be invalid.\n    ///\n    /// Will return `(Vec3::ONE, Quat::IDENTITY, Vec3::ZERO)` if the determinant\n    /// of `self` is zero or if the resulting scale vector contains any zero\n    /// elements when `glam_assert` is enabled.\n    ///\n    /// This is required instead of using\n    /// [`glam::Mat4::to_scale_rotation_translation`], because that uses\n    /// f32::signum, which compares against `f32::NAN`, which causes an error\n    /// in naga's WGSL output.\n    ///\n    /// See [this issue](https://github.com/gfx-rs/naga/issues/2461) and `crate::linkage::test`\n    /// for more info.\n    fn to_scale_rotation_translation_or_id(&self) -> (glam::Vec3, glam::Quat, glam::Vec3);\n}\n\n/// From the columns of a 3x3 rotation matrix.\n///\n/// All of this because we can't use NaNs.\n#[inline]\nfn from_rotation_axes(x_axis: glam::Vec3, y_axis: glam::Vec3, z_axis: glam::Vec3) -> glam::Quat {\n    // Based on https://github.com/microsoft/DirectXMath `XM$quaternionRotationMatrix`\n    let (m00, m01, m02) = x_axis.into();\n    let (m10, m11, m12) = y_axis.into();\n    let (m20, m21, m22) = z_axis.into();\n    if m22 <= 0.0 {\n        // x^2 + y^2 >= z^2 + w^2\n        let dif10 = m11 - m00;\n        let omm22 = 1.0 - m22;\n        if dif10 <= 0.0 {\n            // x^2 >= y^2\n            let four_xsq = omm22 - dif10;\n            let inv4x = 0.5 / four_xsq.sqrt();\n            glam::Quat::from_xyzw(\n                four_xsq * inv4x,\n                (m01 + m10) * inv4x,\n                (m02 + m20) * inv4x,\n                (m12 - m21) * inv4x,\n            )\n        } else {\n            // y^2 >= x^2\n            let four_ysq = omm22 + dif10;\n            let inv4y = 0.5 / four_ysq.sqrt();\n            glam::Quat::from_xyzw(\n                (m01 + m10) * inv4y,\n                four_ysq * inv4y,\n                (m12 + m21) * inv4y,\n                (m20 - m02) * inv4y,\n            )\n        }\n    } else {\n        // z^2 + w^2 >= x^2 + y^2\n        let sum10 = m11 + m00;\n        let opm22 = 1.0 + m22;\n        if sum10 <= 0.0 {\n            // z^2 >= w^2\n            let four_zsq = opm22 - sum10;\n            let inv4z = 0.5 / four_zsq.sqrt();\n            glam::Quat::from_xyzw(\n                (m02 + m20) * inv4z,\n                (m12 + m21) * inv4z,\n                four_zsq * inv4z,\n                (m01 - m10) * inv4z,\n            )\n        } else {\n            // w^2 >= z^2\n            let four_wsq = opm22 + sum10;\n            let inv4w = 0.5 / four_wsq.sqrt();\n            glam::Quat::from_xyzw(\n                (m12 - m21) * inv4w,\n                (m20 - m02) * inv4w,\n                (m01 - m10) * inv4w,\n                four_wsq * inv4w,\n            )\n        }\n    }\n}\n\nconst fn srt_id() -> (Vec3, Quat, Vec3) {\n    (Vec3::ONE, Quat::IDENTITY, Vec3::ZERO)\n}\n\nimpl IsMatrix for glam::Mat4 {\n    #[inline]\n    fn to_scale_rotation_translation_or_id(&self) -> (glam::Vec3, glam::Quat, glam::Vec3) {\n        let det = self.determinant();\n        if det == 0.0 {\n            crate::println!(\"det == 0.0, returning identity\");\n            return srt_id();\n        }\n\n        let det_sign = if det >= 0.0 { 1.0 } else { -1.0 };\n\n        let scale = glam::Vec3::new(\n            self.x_axis.length() * det_sign,\n            self.y_axis.length(),\n            self.z_axis.length(),\n        );\n\n        if !scale.cmpne(glam::Vec3::ZERO).all() {\n            return srt_id();\n        }\n\n        let inv_scale = scale.recip();\n\n        let rotation = from_rotation_axes(\n            self.x_axis.mul(inv_scale.x).xyz(),\n            self.y_axis.mul(inv_scale.y).xyz(),\n            self.z_axis.mul(inv_scale.z).xyz(),\n        );\n\n        let translation = self.w_axis.xyz();\n\n        (scale, rotation, translation)\n    }\n}\n\n/// Returns `1.0` if `n` is greater than or equal to `0.0`.\n/// Returns `1.0` if `n` is greater than or equal to `-0.0`.\n/// Returns `-1.0` if `n` is less than `0.0`.\n/// Returns `0.0` if `n` is `NaN`.\npub fn signum_or_zero(n: f32) -> f32 {\n    ((n >= 0.0) as u32) as f32 - ((n < 0.0) as u32) as f32\n}\n\n/// Return `1.0` when `value` is greater than or equal to `edge` and `0.0` where\n/// `value` is less than `edge`.\n#[inline(always)]\npub fn step_ge(value: f32, edge: f32) -> f32 {\n    ((value >= edge) as u32) as f32\n}\n\n/// Return `1.0` when `value` is less than or equal to `edge`\n/// and `0.0` when `value` is greater than `edge`.\n#[inline(always)]\npub fn step_le(value: f32, edge: f32) -> f32 {\n    ((value <= edge) as u32) as f32\n}\n\npub fn smoothstep(edge_in: f32, edge_out: f32, mut x: f32) -> f32 {\n    // Scale, and clamp x to 0..1 range\n    x = clamp((x - edge_in) / (edge_out - edge_in), 0.0, 1.0);\n    x * x * (3.0 - 2.0 * x)\n}\n\npub fn triangle_face_normal(p1: Vec3, p2: Vec3, p3: Vec3) -> Vec3 {\n    let a = p1 - p2;\n    let b = p1 - p3;\n    let n: Vec3 = a.cross(b).alt_norm_or_zero();\n    #[cfg(cpu)]\n    debug_assert_ne!(\n        Vec3::ZERO,\n        n,\n        \"normal is zero - p1: {p1}, p2: {p2}, p3: {p3}\"\n    );\n    n\n}\n\n/// Convert a color from a hexadecimal number (eg. `0x52b14eff`) into a Vec4.\npub fn hex_to_vec4(color: u32) -> Vec4 {\n    let r = ((color >> 24) & 0xFF) as f32 / 255.0;\n    let g = ((color >> 16) & 0xFF) as f32 / 255.0;\n    let b = ((color >> 8) & 0xFF) as f32 / 255.0;\n    let a = (color & 0xFF) as f32 / 255.0;\n\n    Vec4::new(r, g, b, a)\n}\n\npub const UNIT_QUAD_CCW: [Vec3; 6] = {\n    let tl = Vec3::new(-0.5, 0.5, 0.0);\n    let tr = Vec3::new(0.5, 0.5, 0.0);\n    let bl = Vec3::new(-0.5, -0.5, 0.0);\n    let br = Vec3::new(0.5, -0.5, 0.0);\n    [bl, br, tr, tr, tl, bl]\n};\n\npub const CLIP_QUAD_CCW: [Vec3; 6] = {\n    let tl = Vec3::new(-1.0, 1.0, 0.0);\n    let tr = Vec3::new(1.0, 1.0, 0.0);\n    let bl = Vec3::new(-1.0, -1.0, 0.0);\n    let br = Vec3::new(1.0, -1.0, 0.0);\n    [bl, br, tr, tr, tl, bl]\n};\n\npub const CLIP_SPACE_COORD_QUAD_CCW_TL: Vec4 = Vec4::new(-1.0, 1.0, 0.5, 1.0);\npub const CLIP_SPACE_COORD_QUAD_CCW_BL: Vec4 = Vec4::new(-1.0, -1.0, 0.5, 1.0);\npub const CLIP_SPACE_COORD_QUAD_CCW_TR: Vec4 = Vec4::new(1.0, 1.0, 0.5, 1.0);\npub const CLIP_SPACE_COORD_QUAD_CCW_BR: Vec4 = Vec4::new(1.0, -1.0, 0.5, 1.0);\n\npub const CLIP_SPACE_COORD_QUAD_CCW: [Vec4; 6] = {\n    [\n        CLIP_SPACE_COORD_QUAD_CCW_BL,\n        CLIP_SPACE_COORD_QUAD_CCW_BR,\n        CLIP_SPACE_COORD_QUAD_CCW_TR,\n        CLIP_SPACE_COORD_QUAD_CCW_TR,\n        CLIP_SPACE_COORD_QUAD_CCW_TL,\n        CLIP_SPACE_COORD_QUAD_CCW_BL,\n    ]\n};\n\npub const UV_COORD_QUAD_CCW: [Vec2; 6] = {\n    let tl = Vec2::new(0.0, 0.0);\n    let tr = Vec2::new(1.0, 0.0);\n    let bl = Vec2::new(0.0, 1.0);\n    let br = Vec2::new(1.0, 1.0);\n    [bl, br, tr, tr, tl, bl]\n};\n\npub const POINTS_2D_TEX_QUAD: [Vec2; 6] = {\n    let tl = Vec2::new(0.0, 0.0);\n    let tr = Vec2::new(1.0, 0.0);\n    let bl = Vec2::new(0.0, 1.0);\n    let br = Vec2::new(1.0, 1.0);\n    [tl, bl, tr, tr, bl, br]\n};\n\n/// Points around the unit cube.\n///\n///    y           1_____2     _____\n///    |           /    /|    /|    |  (same box, left and front sides removed)\n///    |___x     0/___3/ |   /7|____|6\n///   /           |    | /   | /    /\n/// z/            |____|/   4|/____/5\npub const UNIT_POINTS: [Vec3; 8] = {\n    let p0 = Vec3::new(-0.5, 0.5, 0.5);\n    let p1 = Vec3::new(-0.5, 0.5, -0.5);\n    let p2 = Vec3::new(0.5, 0.5, -0.5);\n    let p3 = Vec3::new(0.5, 0.5, 0.5);\n\n    let p4 = Vec3::new(-0.5, -0.5, 0.5);\n    let p7 = Vec3::new(-0.5, -0.5, -0.5);\n    let p6 = Vec3::new(0.5, -0.5, -0.5);\n    let p5 = Vec3::new(0.5, -0.5, 0.5);\n\n    [p0, p1, p2, p3, p4, p5, p6, p7]\n};\n\n/// Triangle faces of the unit cube, winding CCW.\npub const UNIT_INDICES: [usize; 36] = [\n    0, 2, 1, 0, 3, 2, // top\n    0, 4, 3, 4, 5, 3, // front\n    3, 6, 2, 3, 5, 6, // right\n    1, 7, 0, 7, 4, 0, // left\n    4, 6, 5, 4, 7, 6, // bottom\n    2, 7, 1, 2, 6, 7, // back\n];\n\n#[cfg(not(target_arch = \"spirv\"))]\npub fn unit_cube() -> Vec<(Vec3, Vec3)> {\n    UNIT_INDICES\n        .chunks_exact(3)\n        .flat_map(|chunk| match chunk {\n            [a, b, c] => {\n                let a = UNIT_POINTS[*a];\n                let b = UNIT_POINTS[*b];\n                let c = UNIT_POINTS[*c];\n                let n = triangle_face_normal(a, b, c);\n                [(a, n), (b, n), (c, n)]\n            }\n            _ => unreachable!(),\n        })\n        .collect::<Vec<_>>()\n}\n\n/// Points on the unit cube that create a triangle-list mesh.\n///\n/// Use [`unit_cube`] for a mesh that includes normals.\n///\n/// `rust-gpu` doesn't like nested/double indexing so we do this here.\n/// See [this comment on discord](https://discord.com/channels/750717012564770887/750717499737243679/1131395331368693770)\npub const CUBE: [Vec3; 36] = {\n    let p0 = Vec3::new(-0.5, 0.5, 0.5);\n    let p1 = Vec3::new(-0.5, 0.5, -0.5);\n    let p2 = Vec3::new(0.5, 0.5, -0.5);\n    let p3 = Vec3::new(0.5, 0.5, 0.5);\n    let p4 = Vec3::new(-0.5, -0.5, 0.5);\n    let p7 = Vec3::new(-0.5, -0.5, -0.5);\n    let p6 = Vec3::new(0.5, -0.5, -0.5);\n    let p5 = Vec3::new(0.5, -0.5, 0.5);\n    convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7])\n};\n\npub fn reflect(i: Vec3, n: Vec3) -> Vec3 {\n    let n = n.alt_norm_or_zero();\n    i - 2.0 * n.dot(i) * n\n}\n\n/// Returns `true` if `p` is inside normalized device coordinates.\n///\n/// x and y are in [-1, 1]; z is in [0, 1] (wgpu / Vulkan / D3D convention).\npub fn is_inside_clip_space(p: Vec3) -> bool {\n    p.x.abs() <= 1.0 && p.y.abs() <= 1.0 && p.z >= 0.0 && p.z <= 1.0\n}\n\npub const fn convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7]: [Vec3; 8]) -> [Vec3; 36] {\n    [\n        p0, p2, p1, p0, p3, p2, // top\n        p0, p4, p3, p4, p5, p3, // front\n        p3, p6, p2, p3, p5, p6, // right\n        p1, p7, p0, p7, p4, p0, // left\n        p4, p6, p5, p4, p7, p6, // bottom\n        p2, p7, p1, p2, p6, p7, // back\n    ]\n}\n\n/// An PCG PRNG that is optimized for GPUs, in that it is fast to evaluate and\n/// accepts sequential ids as it's initial state without sacrificing on RNG\n/// quality.\n///\n/// * <https://www.reedbeta.com/blog/hash-functions-for-gpu-rendering/>\n/// * <https://jcgt.org/published/0009/03/02/>\n///\n/// Thanks to Firestar99 at\n/// <https://github.com/Firestar99/nanite-at-home/blob/c55915d16ad3b5b4b706d8017633f0870dd2603e/space-engine-shader/src/utils/gpurng.rs#L19>\npub struct GpuRng(pub u32);\n\nimpl GpuRng {\n    pub fn new(state: u32) -> GpuRng {\n        Self(state)\n    }\n\n    pub fn gen(&mut self) -> u32 {\n        let state = self.0;\n        self.0 = if cfg!(gpu) {\n            self.0 * 747796405 + 2891336453\n        } else {\n            self.0.wrapping_sub(747796405).wrapping_add(2891336453)\n        };\n        let word = (state >> ((state >> 28) + 4)) ^ state;\n        let word = if cfg!(gpu) {\n            word * 277803737\n        } else {\n            word.wrapping_mul(277803737)\n        };\n        (word >> 22) ^ word\n    }\n\n    pub fn gen_u32(&mut self, min: u32, max: u32) -> u32 {\n        let range = max - min;\n        let percent = self.gen_f32(0.0, 1.0);\n        min + (range as f32 * percent).round() as u32\n    }\n\n    pub fn gen_f32(&mut self, min: f32, max: f32) -> f32 {\n        let range = max - min;\n        let numerator = self.gen();\n        let percentage = numerator as f32 / u32::MAX as f32;\n        min + range * percentage\n    }\n\n    pub fn gen_vec3(&mut self, min: Vec3, max: Vec3) -> Vec3 {\n        let x = self.gen_f32(min.x, max.x);\n        let y = self.gen_f32(min.y, max.y);\n        let z = self.gen_f32(min.z, max.z);\n        Vec3::new(x, y, z)\n    }\n\n    pub fn gen_vec2(&mut self, min: Vec2, max: Vec2) -> Vec2 {\n        let x = self.gen_f32(min.x, max.x);\n        let y = self.gen_f32(min.y, max.y);\n        Vec2::new(x, y)\n    }\n}\n\n/// Convert a pixel coordinate in screen space (origin is top left, Y increases\n/// downwards) to normalized device coordinates (origin is center, Y increses\n/// upwards).\npub fn convert_pixel_to_ndc(pixel_coord: Vec2, viewport_size: UVec2) -> Vec2 {\n    // Normalize the point to the range [0.0, 1.0];\n    let mut normalized = pixel_coord / viewport_size.as_vec2();\n    // Flip the Y axis to increase upward\n    normalized.y = 1.0 - normalized.y;\n    // Move the origin to the center\n    (normalized * 2.0) - 1.0\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn step_sanity() {\n        assert_eq!(0.0, step_le(0.0, -0.33333));\n        assert_eq!(1.0, step_le(0.0, 0.33333));\n        assert_eq!(1.0, step_le(0.0, 0.0));\n    }\n\n    #[test]\n    #[allow(clippy::bool_comparison)]\n    fn nan_sanity() {\n        let n = f32::NAN;\n        assert!(n.is_nan());\n        assert!((n <= 0.0) == false);\n        assert!((n > 0.0) == false);\n    }\n\n    #[test]\n    fn signum_sanity() {\n        assert_eq!(1.0, signum_or_zero(0.33));\n        assert_eq!(1.0, signum_or_zero(0.0));\n        assert_eq!(1.0, signum_or_zero(-0.0));\n        assert_eq!(-1.0, signum_or_zero(-0.33));\n\n        let nan = f32::NAN;\n        assert_eq!(0.0, signum_or_zero(nan));\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/mesh.rs",
    "content": "//! Sometimes you just need a mesh.\nuse wgpu::util::DeviceExt;\n\n/// A vertex buffer.\npub struct Mesh {\n    vertex_buffer: wgpu::Buffer,\n    vertex_buffer_len: usize,\n    vertex_indices: Option<(wgpu::Buffer, usize)>,\n}\n\nimpl Mesh {\n    pub fn new<V: bytemuck::Pod>(\n        device: &wgpu::Device,\n        label: Option<&str>,\n        vertices: impl IntoIterator<Item = V>,\n        may_indices: Option<impl IntoIterator<Item = u16>>,\n    ) -> Self {\n        let vertices = vertices.into_iter().collect::<Vec<_>>();\n        let vertex_buffer_len = vertices.len();\n        let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {\n            label,\n            contents: bytemuck::cast_slice(&vertices),\n            usage: wgpu::BufferUsages::VERTEX,\n        });\n        let vertex_indices = if let Some(indices) = may_indices {\n            let indices = indices.into_iter().collect::<Vec<_>>();\n            let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {\n                label,\n                contents: bytemuck::cast_slice(&indices),\n                usage: wgpu::BufferUsages::INDEX,\n            });\n            Some((index_buffer, indices.len()))\n        } else {\n            None\n        };\n        Self {\n            vertex_buffer,\n            vertex_buffer_len,\n            vertex_indices,\n        }\n    }\n\n    pub fn from_vertices<V: bytemuck::Pod>(\n        device: &wgpu::Device,\n        label: Option<&str>,\n        vertices: impl IntoIterator<Item = V>,\n    ) -> Self {\n        Self::new(device, label, vertices, None as Option<Vec<u16>>)\n    }\n\n    pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {\n        render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));\n        match &self.vertex_indices {\n            Some((index_buffer, len)) => {\n                render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);\n                render_pass.draw_indexed(0..*len as u32, 0, 0..1);\n            }\n            None => {\n                render_pass.draw(0..self.vertex_buffer_len as u32, 0..1);\n            }\n        }\n    }\n\n    pub fn update<V: bytemuck::Pod>(\n        &mut self,\n        device: &wgpu::Device,\n        label: Option<&str>,\n        vertices: impl IntoIterator<Item = V>,\n        may_indices: Option<impl IntoIterator<Item = u16>>,\n    ) {\n        let vertices = vertices.into_iter().collect::<Vec<_>>();\n        self.vertex_buffer_len = vertices.len();\n        self.vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {\n            label,\n            contents: bytemuck::cast_slice(&vertices),\n            usage: wgpu::BufferUsages::VERTEX,\n        });\n        self.vertex_indices = if let Some(indices) = may_indices {\n            let indices = indices.into_iter().collect::<Vec<_>>();\n            let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {\n                label,\n                contents: bytemuck::cast_slice(&indices),\n                usage: wgpu::BufferUsages::INDEX,\n            });\n            Some((buffer, indices.len()))\n        } else {\n            None\n        };\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/pbr/brdf/cpu.rs",
    "content": "//! CPU side of BRDF stuff.\nuse craballoc::runtime::WgpuRuntime;\n\nuse crate::texture;\n\n/// Pre-computed texture of the brdf integration.\n#[derive(Clone)]\npub struct BrdfLut {\n    pub(crate) inner: texture::Texture,\n}\n\nimpl BrdfLut {\n    /// Create a new pre-computed BRDF look-up texture.\n    pub fn new(runtime: impl AsRef<WgpuRuntime>) -> Self {\n        let runtime = runtime.as_ref();\n        let device = &runtime.device;\n        let queue = &runtime.queue;\n        let vertex_linkage = crate::linkage::brdf_lut_convolution_vertex::linkage(device);\n        let fragment_linkage = crate::linkage::brdf_lut_convolution_fragment::linkage(device);\n        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n            label: Some(\"brdf_lut_convolution\"),\n            layout: None,\n            vertex: wgpu::VertexState {\n                module: &vertex_linkage.module,\n                entry_point: Some(vertex_linkage.entry_point),\n                buffers: &[],\n                compilation_options: Default::default(),\n            },\n            primitive: wgpu::PrimitiveState {\n                topology: wgpu::PrimitiveTopology::TriangleList,\n                strip_index_format: None,\n                front_face: wgpu::FrontFace::Ccw,\n                cull_mode: None,\n                unclipped_depth: false,\n                polygon_mode: wgpu::PolygonMode::Fill,\n                conservative: false,\n            },\n            depth_stencil: None,\n            multisample: wgpu::MultisampleState {\n                mask: !0,\n                alpha_to_coverage_enabled: false,\n                count: 1,\n            },\n            fragment: Some(wgpu::FragmentState {\n                module: &fragment_linkage.module,\n                entry_point: Some(fragment_linkage.entry_point),\n                targets: &[Some(wgpu::ColorTargetState {\n                    format: wgpu::TextureFormat::Rg16Float,\n                    blend: Some(wgpu::BlendState {\n                        color: wgpu::BlendComponent::REPLACE,\n                        alpha: wgpu::BlendComponent::REPLACE,\n                    }),\n                    write_mask: wgpu::ColorWrites::ALL,\n                })],\n                compilation_options: Default::default(),\n            }),\n            multiview: None,\n            cache: None,\n        });\n\n        let framebuffer = texture::Texture::new_with(\n            runtime,\n            Some(\"brdf_lut\"),\n            Some(\n                wgpu::TextureUsages::RENDER_ATTACHMENT\n                    | wgpu::TextureUsages::TEXTURE_BINDING\n                    | wgpu::TextureUsages::COPY_SRC,\n            ),\n            None,\n            wgpu::TextureFormat::Rg16Float,\n            2,\n            2,\n            512,\n            512,\n            1,\n            &[],\n        );\n\n        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());\n        {\n            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                label: Some(\"brdf_lut_convolution\"),\n                color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                    view: &framebuffer.view,\n                    resolve_target: None,\n                    ops: wgpu::Operations {\n                        load: wgpu::LoadOp::Clear(wgpu::Color::RED),\n                        store: wgpu::StoreOp::Store,\n                    },\n                    depth_slice: None,\n                })],\n                depth_stencil_attachment: None,\n                ..Default::default()\n            });\n\n            render_pass.set_pipeline(&pipeline);\n            render_pass.draw(0..6, 0..1);\n        }\n        queue.submit([encoder.finish()]);\n\n        BrdfLut { inner: framebuffer }\n    }\n\n    /// Return the underlying [`Texture`](crate::texture::Texture).\n    pub fn texture(&self) -> &texture::Texture {\n        &self.inner\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use crate::{context::Context, pbr::brdf::BrdfLut, test::BlockOnFuture, texture::Texture};\n\n    #[test]\n    fn precomputed_brdf() {\n        assert_eq!(2, std::mem::size_of::<u16>());\n        let r = Context::headless(32, 32).block();\n        let brdf_lut = BrdfLut::new(&r);\n        assert_eq!(\n            wgpu::TextureFormat::Rg16Float,\n            brdf_lut.texture().texture.format()\n        );\n        let copied_buffer = Texture::read(&r, &brdf_lut.texture().texture, 512, 512, 2, 2);\n        let pixels = copied_buffer.pixels(r.get_device()).block().unwrap();\n        let pixels: Vec<f32> = bytemuck::cast_slice::<u8, u16>(pixels.as_slice())\n            .iter()\n            .copied()\n            .map(|bits| half::f16::from_bits(bits).to_f32())\n            .collect();\n        assert_eq!(512 * 512 * 2, pixels.len());\n        let pixels: Vec<f32> = pixels\n            .chunks_exact(2)\n            .flat_map(|pixel| match pixel {\n                [r, g] => [*r, *g, 0.0, 1.0],\n                _ => unreachable!(),\n            })\n            .collect();\n\n        let img: image::ImageBuffer<image::Rgba<f32>, Vec<f32>> =\n            image::ImageBuffer::from_vec(512, 512, pixels).unwrap();\n        let img = image::DynamicImage::from(img);\n        let img = img.into_rgba8();\n        img_diff::assert_img_eq(\"skybox/brdf_lut.png\", img);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/pbr/brdf/shader.rs",
    "content": "//! Shader side of BRDF stuff.\n\nuse glam::{Vec2, Vec3, Vec4Swizzles};\n\nuse crate::math::{IsSampler, IsVector, Sample2d};\n\npub fn sample_brdf<T: Sample2d<Sampler = S>, S: IsSampler>(\n    brdf: &T,\n    brdf_sampler: &S,\n    // camera position in world space\n    camera_pos: Vec3,\n    // fragment position in world space\n    in_pos: Vec3,\n    // normal vector\n    n: Vec3,\n    roughness: f32,\n) -> Vec2 {\n    let v = (camera_pos - in_pos).alt_norm_or_zero();\n    brdf.sample_by_lod(*brdf_sampler, Vec2::new(n.dot(v).max(0.0), roughness), 0.0)\n        .xy()\n}\n"
  },
  {
    "path": "crates/renderling/src/pbr/brdf.rs",
    "content": "//! BRDF computation.\n//!\n//! Helpers for computing (and holding onto) a Bidirectional Reflectance\n//! Distribution Function.\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\npub mod shader;\n"
  },
  {
    "path": "crates/renderling/src/pbr/debug.rs",
    "content": "//! Debugging helpers.\nuse crabslab::SlabItem;\n\n/// Used to debug shaders by early exiting the shader and attempting to display\n/// the value as shaded colors.\n#[repr(u32)]\n#[derive(Clone, Copy, Default, PartialEq, PartialOrd, SlabItem, core::fmt::Debug)]\npub enum DebugChannel {\n    #[default]\n    None,\n\n    /// Displays the first set of UV coordinates as a color.\n    UvCoords0,\n\n    /// Displays the second set of UV coordinates as a color.\n    UvCoords1,\n\n    /// Displays normals after normal mapping, in world space\n    Normals,\n\n    /// Displays only the vertex color for the fragment.\n    VertexColor,\n\n    /// Displays vertex normals.\n    VertexNormals,\n\n    /// Displays uv normals. These are normals coming from a normal map texture.\n    /// These are the normals in tangent space.\n    UvNormals,\n\n    /// Displays vertex normals.\n    Tangents,\n\n    /// Displays bitangents as calculated from normals and tangents.\n    Bitangents,\n\n    /// Displays only the diffuse irradiance value.\n    DiffuseIrradiance,\n\n    /// Displays only the specular reflection value.\n    SpecularReflection,\n\n    /// Displays only the BRDF value for the fragment.\n    Brdf,\n\n    /// Displays only the albedo color for the fragment.\n    Albedo,\n\n    /// Displays only the roughness value for the fragment.\n    Roughness,\n\n    /// Displays only the metallic value for the fragment.\n    Metallic,\n\n    /// Displays only the occlusion color for the fragment.\n    Occlusion,\n\n    /// Displays only the calculated emissive effect (emissive_tex_color *\n    /// emissive_factor * emissive_strength) of the fragment.\n    Emissive,\n\n    /// Displays only the emissive color (from the emissive map texture) of the\n    /// fragment.\n    UvEmissive,\n\n    /// Displays only teh emissive factor of the fragment.\n    EmissiveFactor,\n\n    /// Displays only the emissive strength of the fragment\n    /// (KHR_materials_emissive_strength).\n    EmissiveStrength,\n}\n"
  },
  {
    "path": "crates/renderling/src/pbr/ibl/cpu.rs",
    "content": "//! CPU side of IBL\n\nuse core::sync::atomic::AtomicBool;\nuse std::sync::Arc;\n\nuse craballoc::{runtime::WgpuRuntime, slab::SlabAllocator, value::Hybrid};\nuse crabslab::Id;\nuse glam::{Mat4, Vec3};\n\nuse crate::{\n    camera::Camera, convolution::shader::VertexPrefilterEnvironmentCubemapIds, skybox::Skybox,\n    texture,\n};\n\n/// Image based lighting resources.\n#[derive(Clone)]\npub struct Ibl {\n    is_empty: Arc<AtomicBool>,\n    // Cubemap texture of the pre-computed irradiance cubemap\n    pub(crate) irradiance_cubemap: texture::Texture,\n    // Cubemap texture and mip maps of the specular highlights,\n    // where each mip level is a different roughness.\n    pub(crate) prefiltered_environment_cubemap: texture::Texture,\n}\n\nimpl Ibl {\n    /// Create a new [`Ibl`] resource.\n    pub fn new(runtime: impl AsRef<WgpuRuntime>, skybox: &Skybox) -> Self {\n        log::trace!(\"creating new IBL\");\n        let runtime = runtime.as_ref();\n        let slab = SlabAllocator::new(runtime, \"ibl\", wgpu::BufferUsages::VERTEX);\n        let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 10.0);\n        let camera = Camera::new(&slab).with_projection(proj);\n        let roughness = slab.new_value(0.0f32);\n        let prefilter_ids = slab.new_value(VertexPrefilterEnvironmentCubemapIds {\n            camera: camera.id(),\n            roughness: roughness.id(),\n        });\n\n        let buffer = slab.commit();\n        let mut buffer_upkeep = || {\n            let possibly_new_buffer = slab.commit();\n            debug_assert!(!possibly_new_buffer.is_new_this_commit());\n        };\n\n        let views = [\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(1.0, 0.0, 0.0),\n                Vec3::new(0.0, -1.0, 0.0),\n            ),\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(-1.0, 0.0, 0.0),\n                Vec3::new(0.0, -1.0, 0.0),\n            ),\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(0.0, -1.0, 0.0),\n                Vec3::new(0.0, 0.0, -1.0),\n            ),\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(0.0, 1.0, 0.0),\n                Vec3::new(0.0, 0.0, 1.0),\n            ),\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(0.0, 0.0, 1.0),\n                Vec3::new(0.0, -1.0, 0.0),\n            ),\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(0.0, 0.0, -1.0),\n                Vec3::new(0.0, -1.0, 0.0),\n            ),\n        ];\n\n        let environment_cubemap = skybox.environment_cubemap_texture();\n\n        // Convolve the environment map.\n        let irradiance_cubemap = create_irradiance_map(\n            runtime,\n            &buffer,\n            &mut buffer_upkeep,\n            environment_cubemap,\n            &camera,\n            views,\n        );\n\n        // Generate specular IBL pre-filtered environment map.\n        let prefiltered_environment_cubemap = create_prefiltered_environment_map(\n            runtime,\n            &buffer,\n            &mut buffer_upkeep,\n            &camera,\n            &roughness,\n            prefilter_ids.id(),\n            environment_cubemap,\n            views,\n        );\n\n        Self {\n            is_empty: Arc::new(skybox.is_empty().into()),\n            irradiance_cubemap,\n            prefiltered_environment_cubemap,\n        }\n    }\n\n    /// Returns whether this [`Ibl`] is empty.\n    ///\n    /// An [`Ibl`] is empty if it was created from an empty [`Skybox`].\n    pub fn is_empty(&self) -> bool {\n        self.is_empty.load(std::sync::atomic::Ordering::Relaxed)\n    }\n}\n\nfn create_irradiance_map(\n    runtime: impl AsRef<WgpuRuntime>,\n    buffer: &wgpu::Buffer,\n    buffer_upkeep: impl FnMut(),\n    environment_texture: &texture::Texture,\n    camera: &Camera,\n    views: [Mat4; 6],\n) -> texture::Texture {\n    let runtime = runtime.as_ref();\n    let device = &runtime.device;\n    let pipeline = crate::pbr::ibl::DiffuseIrradianceConvolutionRenderPipeline::new(\n        device,\n        wgpu::TextureFormat::Rgba16Float,\n    );\n\n    let bindgroup = crate::pbr::ibl::diffuse_irradiance_convolution_bindgroup(\n        device,\n        Some(\"irradiance\"),\n        buffer,\n        environment_texture,\n    );\n\n    texture::Texture::render_cubemap(\n        runtime,\n        \"diffuse-irradiance\",\n        &pipeline.0,\n        buffer_upkeep,\n        camera,\n        &bindgroup,\n        views,\n        32,\n        None,\n    )\n}\n\n/// Pipeline for creating a prefiltered environment map from an existing\n/// environment cubemap.\npub(crate) fn create_prefiltered_environment_pipeline_and_bindgroup(\n    device: &wgpu::Device,\n    buffer: &wgpu::Buffer,\n    environment_texture: &crate::texture::Texture,\n) -> (wgpu::RenderPipeline, wgpu::BindGroup) {\n    let label = Some(\"prefiltered environment\");\n    let bindgroup_layout_desc = wgpu::BindGroupLayoutDescriptor {\n        label,\n        entries: &[\n            wgpu::BindGroupLayoutEntry {\n                binding: 0,\n                visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Buffer {\n                    ty: wgpu::BufferBindingType::Storage { read_only: true },\n                    has_dynamic_offset: false,\n                    min_binding_size: None,\n                },\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 1,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Texture {\n                    sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                    view_dimension: wgpu::TextureViewDimension::Cube,\n                    multisampled: false,\n                },\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 2,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                count: None,\n            },\n        ],\n    };\n    let bg_layout = device.create_bind_group_layout(&bindgroup_layout_desc);\n    let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor {\n        label,\n        layout: &bg_layout,\n        entries: &[\n            wgpu::BindGroupEntry {\n                binding: 0,\n                resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()),\n            },\n            wgpu::BindGroupEntry {\n                binding: 1,\n                resource: wgpu::BindingResource::TextureView(&environment_texture.view),\n            },\n            wgpu::BindGroupEntry {\n                binding: 2,\n                resource: wgpu::BindingResource::Sampler(&environment_texture.sampler),\n            },\n        ],\n    });\n    let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n        label,\n        bind_group_layouts: &[&bg_layout],\n        push_constant_ranges: &[],\n    });\n    let vertex_linkage = crate::linkage::prefilter_environment_cubemap_vertex::linkage(device);\n    let fragment_linkage = crate::linkage::prefilter_environment_cubemap_fragment::linkage(device);\n    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n        label: Some(\"prefiltered environment\"),\n        layout: Some(&pp_layout),\n        vertex: wgpu::VertexState {\n            module: &vertex_linkage.module,\n            entry_point: Some(vertex_linkage.entry_point),\n            buffers: &[],\n            compilation_options: Default::default(),\n        },\n        primitive: wgpu::PrimitiveState {\n            topology: wgpu::PrimitiveTopology::TriangleList,\n            strip_index_format: None,\n            front_face: wgpu::FrontFace::Ccw,\n            cull_mode: None,\n            unclipped_depth: false,\n            polygon_mode: wgpu::PolygonMode::Fill,\n            conservative: false,\n        },\n        depth_stencil: None,\n        multisample: wgpu::MultisampleState {\n            mask: !0,\n            alpha_to_coverage_enabled: false,\n            count: 1,\n        },\n        fragment: Some(wgpu::FragmentState {\n            module: &fragment_linkage.module,\n            entry_point: Some(fragment_linkage.entry_point),\n            targets: &[Some(wgpu::ColorTargetState {\n                format: wgpu::TextureFormat::Rgba16Float,\n                blend: Some(wgpu::BlendState {\n                    color: wgpu::BlendComponent::REPLACE,\n                    alpha: wgpu::BlendComponent::REPLACE,\n                }),\n                write_mask: wgpu::ColorWrites::ALL,\n            })],\n            compilation_options: Default::default(),\n        }),\n        multiview: None,\n        cache: None,\n    });\n    (pipeline, bindgroup)\n}\n\n#[allow(clippy::too_many_arguments)]\nfn create_prefiltered_environment_map(\n    runtime: impl AsRef<WgpuRuntime>,\n    buffer: &wgpu::Buffer,\n    mut buffer_upkeep: impl FnMut(),\n    camera: &Camera,\n    roughness: &Hybrid<f32>,\n    prefilter_id: Id<VertexPrefilterEnvironmentCubemapIds>,\n    environment_texture: &texture::Texture,\n    views: [Mat4; 6],\n) -> texture::Texture {\n    let (pipeline, bindgroup) =\n        crate::pbr::ibl::create_prefiltered_environment_pipeline_and_bindgroup(\n            &runtime.as_ref().device,\n            buffer,\n            environment_texture,\n        );\n    let mut cubemap_faces = Vec::new();\n\n    for (i, view) in views.iter().enumerate() {\n        for mip_level in 0..5 {\n            let mip_width: u32 = 128 >> mip_level;\n            let mip_height: u32 = 128 >> mip_level;\n\n            let mut encoder =\n                runtime\n                    .as_ref()\n                    .device\n                    .create_command_encoder(&wgpu::CommandEncoderDescriptor {\n                        label: Some(\"specular convolution\"),\n                    });\n\n            let cubemap_face = texture::Texture::new_with(\n                runtime.as_ref(),\n                Some(&format!(\"cubemap{i}{mip_level}prefiltered_environment\")),\n                Some(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC),\n                None,\n                wgpu::TextureFormat::Rgba16Float,\n                4,\n                2,\n                mip_width,\n                mip_height,\n                1,\n                &[],\n            );\n\n            // update the roughness for these mips\n            roughness.set(mip_level as f32 / 4.0);\n            // update the view to point at one of the cube faces\n            camera.set_view(*view);\n            buffer_upkeep();\n            {\n                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                    label: Some(&format!(\"cubemap{i}\")),\n                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                        view: &cubemap_face.view,\n                        resolve_target: None,\n                        ops: wgpu::Operations {\n                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),\n                            store: wgpu::StoreOp::Store,\n                        },\n                        depth_slice: None,\n                    })],\n                    depth_stencil_attachment: None,\n                    ..Default::default()\n                });\n\n                render_pass.set_pipeline(&pipeline);\n                render_pass.set_bind_group(0, Some(&bindgroup), &[]);\n                render_pass.draw(0..36, prefilter_id.inner()..prefilter_id.inner() + 1);\n            }\n\n            runtime.as_ref().queue.submit([encoder.finish()]);\n            cubemap_faces.push(cubemap_face);\n        }\n    }\n\n    texture::Texture::new_cubemap_texture(\n        runtime,\n        Some(\"prefiltered-environment-cubemap\"),\n        128,\n        cubemap_faces.as_slice(),\n        wgpu::TextureFormat::Rgba16Float,\n        5,\n    )\n}\n\npub fn diffuse_irradiance_convolution_bindgroup_layout(\n    device: &wgpu::Device,\n) -> wgpu::BindGroupLayout {\n    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n        label: Some(\"convolution bindgroup\"),\n        entries: &[\n            wgpu::BindGroupLayoutEntry {\n                binding: 0,\n                visibility: wgpu::ShaderStages::VERTEX,\n                ty: wgpu::BindingType::Buffer {\n                    ty: wgpu::BufferBindingType::Storage { read_only: true },\n                    has_dynamic_offset: false,\n                    min_binding_size: None,\n                },\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 1,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Texture {\n                    sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                    view_dimension: wgpu::TextureViewDimension::Cube,\n                    multisampled: false,\n                },\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 2,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                count: None,\n            },\n        ],\n    })\n}\n\npub fn diffuse_irradiance_convolution_bindgroup(\n    device: &wgpu::Device,\n    label: Option<&str>,\n    buffer: &wgpu::Buffer,\n    // The texture to sample the environment from\n    texture: &crate::texture::Texture,\n) -> wgpu::BindGroup {\n    device.create_bind_group(&wgpu::BindGroupDescriptor {\n        label,\n        layout: &diffuse_irradiance_convolution_bindgroup_layout(device),\n        entries: &[\n            wgpu::BindGroupEntry {\n                binding: 0,\n                resource: wgpu::BindingResource::Buffer(buffer.as_entire_buffer_binding()),\n            },\n            wgpu::BindGroupEntry {\n                binding: 1,\n                resource: wgpu::BindingResource::TextureView(&texture.view),\n            },\n            wgpu::BindGroupEntry {\n                binding: 2,\n                resource: wgpu::BindingResource::Sampler(&texture.sampler),\n            },\n        ],\n    })\n}\n\npub struct DiffuseIrradianceConvolutionRenderPipeline(pub wgpu::RenderPipeline);\n\nimpl DiffuseIrradianceConvolutionRenderPipeline {\n    /// Create the rendering pipeline that performs a convolution.\n    pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {\n        let vertex_linkage = crate::linkage::skybox_cubemap_vertex::linkage(device);\n        let fragment_linkage = crate::linkage::di_convolution_fragment::linkage(device);\n        let bg_layout = diffuse_irradiance_convolution_bindgroup_layout(device);\n        let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label: Some(\"convolution pipeline layout\"),\n            bind_group_layouts: &[&bg_layout],\n            push_constant_ranges: &[],\n        });\n\n        DiffuseIrradianceConvolutionRenderPipeline(device.create_render_pipeline(\n            &wgpu::RenderPipelineDescriptor {\n                label: Some(\"convolution pipeline\"),\n                layout: Some(&pp_layout),\n                vertex: wgpu::VertexState {\n                    module: &vertex_linkage.module,\n                    entry_point: Some(vertex_linkage.entry_point),\n                    buffers: &[],\n                    compilation_options: Default::default(),\n                },\n                primitive: wgpu::PrimitiveState {\n                    topology: wgpu::PrimitiveTopology::TriangleList,\n                    strip_index_format: None,\n                    front_face: wgpu::FrontFace::Ccw,\n                    cull_mode: None,\n                    unclipped_depth: false,\n                    polygon_mode: wgpu::PolygonMode::Fill,\n                    conservative: false,\n                },\n                depth_stencil: None,\n                multisample: wgpu::MultisampleState {\n                    mask: !0,\n                    alpha_to_coverage_enabled: false,\n                    count: 1,\n                },\n                fragment: Some(wgpu::FragmentState {\n                    module: &fragment_linkage.module,\n                    entry_point: Some(fragment_linkage.entry_point),\n                    targets: &[Some(wgpu::ColorTargetState {\n                        format,\n                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                        write_mask: wgpu::ColorWrites::ALL,\n                    })],\n                    compilation_options: Default::default(),\n                }),\n                multiview: None,\n                cache: None,\n            },\n        ))\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use glam::{Mat4, Vec3};\n\n    use crate::{\n        context::Context,\n        test::{workspace_dir, BlockOnFuture},\n        texture::CopiedTextureBuffer,\n    };\n\n    #[test]\n    /// Creates an Ibl and reads out its diffuse irradiance and prefiltered\n    /// environment cubemap mips and compares them against known images to\n    /// ensure creation is valid.\n    fn creates_valid_cubemaps() {\n        let ctx = Context::headless(600, 400).block();\n        let proj = crate::camera::perspective(600.0, 400.0);\n        let view = crate::camera::look_at(Vec3::new(0.0, 0.0, 2.0), Vec3::ZERO, Vec3::Y);\n        let stage = ctx.new_stage();\n        let _camera = stage.new_camera().with_projection_and_view(proj, view);\n        let skybox = stage\n            .new_skybox_from_path(workspace_dir().join(\"img/hdr/resting_place.hdr\"))\n            .unwrap();\n        let ibl = stage.new_ibl(&skybox);\n        stage.use_ibl(&ibl);\n        assert_eq!(\n            wgpu::TextureFormat::Rgba16Float,\n            ibl.irradiance_cubemap.texture.format()\n        );\n        assert_eq!(\n            wgpu::TextureFormat::Rgba16Float,\n            ibl.prefiltered_environment_cubemap.texture.format()\n        );\n        for i in 0..6 {\n            // save out the irradiance face\n            let copied_buffer = CopiedTextureBuffer::read_from(\n                &ctx,\n                &ibl.irradiance_cubemap.texture,\n                32,\n                32,\n                4,\n                2,\n                0,\n                Some(wgpu::Origin3d { x: 0, y: 0, z: i }),\n            );\n            let pixels = copied_buffer.pixels(ctx.get_device()).block().unwrap();\n            let pixels = bytemuck::cast_slice::<u8, u16>(pixels.as_slice())\n                .iter()\n                .map(|p| half::f16::from_bits(*p).to_f32())\n                .collect::<Vec<_>>();\n            assert_eq!(32 * 32 * 4, pixels.len());\n            let img: image::Rgba32FImage = image::ImageBuffer::from_vec(32, 32, pixels).unwrap();\n            let img = image::DynamicImage::from(img);\n            let img = img.to_rgba8();\n            img_diff::assert_img_eq(&format!(\"skybox/irradiance{i}.png\"), img);\n            for mip_level in 0..5 {\n                let mip_size = 128u32 >> mip_level;\n                // save out the prefiltered environment faces' mips\n                let copied_buffer = CopiedTextureBuffer::read_from(\n                    &ctx,\n                    &ibl.prefiltered_environment_cubemap.texture,\n                    mip_size as usize,\n                    mip_size as usize,\n                    4,\n                    2,\n                    mip_level,\n                    Some(wgpu::Origin3d { x: 0, y: 0, z: i }),\n                );\n                let pixels = copied_buffer.pixels(ctx.get_device()).block().unwrap();\n                let pixels = bytemuck::cast_slice::<u8, u16>(pixels.as_slice())\n                    .iter()\n                    .map(|p| half::f16::from_bits(*p).to_f32())\n                    .collect::<Vec<_>>();\n                assert_eq!((mip_size * mip_size * 4) as usize, pixels.len());\n                let img: image::Rgba32FImage =\n                    image::ImageBuffer::from_vec(mip_size, mip_size, pixels).unwrap();\n                let img = image::DynamicImage::from(img);\n                let img = img.to_rgba8();\n                img_diff::assert_img_eq(\n                    &format!(\"skybox/prefiltered_environment_face{i}_mip{mip_level}.png\"),\n                    img,\n                );\n            }\n        }\n    }\n\n    #[test]\n    /// Creates a Skybox, Ibl, and uses the Ibl to light a mirror cube.\n    fn mirror_cube_is_lit_by_environment() {\n        let ctx = Context::headless(256, 256).block();\n        let stage = ctx.new_stage();\n\n        let _camera = stage\n            .new_camera()\n            .with_default_perspective(256.0, 256.0)\n            .with_view(Mat4::look_at_rh(Vec3::ONE * 1.5, Vec3::ZERO, Vec3::Y));\n        let _model = stage.new_primitive().with_material(\n            stage\n                .new_material()\n                .with_metallic_factor(0.9)\n                .with_roughness_factor(0.1),\n        );\n\n        let skybox = stage\n            .new_skybox_from_path(workspace_dir().join(\"img/hdr/helipad.hdr\"))\n            .unwrap();\n        stage.use_skybox(&skybox);\n\n        // Render once here because we found a bug where rendering before setting\n        // ibl would cause the primitive bindgroup to *not* be invalidated when\n        // ibl was set.\n        //\n        // This essentially just ensures that `Stage::use_ibl` is invalidating the\n        // primitive bindgroup.\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        frame.present();\n\n        let ibl = stage.new_ibl(&skybox);\n        stage.use_ibl(&ibl);\n        stage.remove_skybox();\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::save(\"pbr/ibl/mirror_cube_is_lit_by_environment.png\", img);\n        frame.present();\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/pbr/ibl/diffuse_irradiance.rs",
    "content": "//! Diffuse irradiance convolution.\n\nuse glam::{Vec3, Vec4, Vec4Swizzles};\n#[cfg(target_arch = \"spirv\")]\nuse spirv_std::num_traits::Float;\nuse spirv_std::{image::Cubemap, spirv, Sampler};\n\nuse crate::math::IsVector;\n\n#[cfg(not(target_arch = \"spirv\"))]\nmod cpu;\n#[cfg(not(target_arch = \"spirv\"))]\npub use cpu::*;\n"
  },
  {
    "path": "crates/renderling/src/pbr/ibl/shader.rs",
    "content": "//! Shader side of IBL\nuse glam::{Vec3, Vec4, Vec4Swizzles};\n#[cfg(gpu)]\nuse spirv_std::num_traits::Float;\nuse spirv_std::{image::Cubemap, spirv, Sampler};\n\nuse crate::math::IsVector;\n\n/// Diffuse irradiance convolution.\n#[spirv(fragment)]\npub fn di_convolution_fragment(\n    #[spirv(descriptor_set = 0, binding = 1)] environment_texture: &Cubemap,\n    #[spirv(descriptor_set = 0, binding = 2)] environment_sampler: &Sampler,\n    local_pos: Vec3,\n    frag_color: &mut Vec4,\n) {\n    let normal = {\n        let mut n = local_pos.alt_norm_or_zero();\n        n.y *= -1.0;\n        n\n    };\n    let mut irradiance = Vec3::ZERO;\n    let right = Vec3::Y.cross(normal).alt_norm_or_zero();\n    let up = normal.cross(right).alt_norm_or_zero();\n\n    let sample_delta = 0.025;\n    let mut nr_samples = 0.0;\n    let mut phi = 0.0f32;\n    while phi < 2.0 * core::f32::consts::PI {\n        let mut theta = 0.0f32;\n        while theta < core::f32::consts::FRAC_PI_2 {\n            // spherical to cartisian tangent coords\n            let tangent_sample = Vec3::new(\n                theta.sin() * phi.cos(),\n                theta.sin() * phi.sin(),\n                theta.cos(),\n            );\n            // tangent to world coords\n            let sample_vec =\n                (tangent_sample.x * right + tangent_sample.y * up + tangent_sample.z * normal)\n                    .alt_norm_or_zero();\n            let sample = environment_texture.sample(*environment_sampler, sample_vec)\n                * theta.cos()\n                * theta.sin();\n            irradiance += sample.xyz();\n            nr_samples += 1.0;\n\n            theta += sample_delta;\n        }\n        phi += sample_delta\n    }\n\n    *frag_color = (irradiance * (core::f32::consts::PI / nr_samples)).extend(1.0);\n}\n"
  },
  {
    "path": "crates/renderling/src/pbr/ibl.rs",
    "content": "//! Image based lighting\n//!\n//! For more info on image based lighting, see <https://learnopengl.com/PBR/IBL/Diffuse-irradiance>.\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\npub mod shader;\n"
  },
  {
    "path": "crates/renderling/src/pbr/shader.rs",
    "content": "//! Physically based shader code.\nuse crabslab::{Id, Slab};\nuse glam::{Mat4, Vec2, Vec3, Vec4, Vec4Swizzles};\n\n#[allow(unused)]\nuse spirv_std::num_traits::{Float, Zero};\n\nuse crate::{\n    atlas::shader::AtlasTextureDescriptor,\n    geometry::shader::GeometryDescriptor,\n    light::shader::{\n        Candela, DirectionalLightDescriptor, LightStyle, LightingDescriptor, PointLightDescriptor,\n        ShadowCalculation, SpotLightCalculation,\n    },\n    material::shader::MaterialDescriptor,\n    math::{self, IsSampler, IsVector, Sample2d, Sample2dArray, SampleCube},\n    primitive::shader::PrimitiveDescriptor,\n    println as my_println,\n};\n\nuse super::{brdf, debug};\n\n/// Trowbridge-Reitz GGX normal distribution function (NDF).\n///\n/// The normal distribution function D statistically approximates the relative\n/// surface area of microfacets exactly aligned to the (halfway) vector h.\npub fn normal_distribution_ggx(n: Vec3, h: Vec3, roughness: f32) -> f32 {\n    let a = roughness * roughness;\n    let a2 = a * a;\n    let ndot_h = n.dot(h).max(0.0);\n    let ndot_h2 = ndot_h * ndot_h;\n\n    let num = a2;\n    let denom = (ndot_h2 * (a2 - 1.0) + 1.0).powf(2.0) * core::f32::consts::PI;\n\n    num / denom\n}\n\nfn geometry_schlick_ggx(ndot_v: f32, roughness: f32) -> f32 {\n    let r = roughness + 1.0;\n    let k = (r * r) / 8.0;\n    let num = ndot_v;\n    let denom = ndot_v * (1.0 - k) + k;\n\n    num / denom\n}\n\n/// The geometry function statistically approximates the relative surface area\n/// where its micro surface-details overshadow each other, causing light rays to\n/// be occluded.\nfn geometry_smith(n: Vec3, v: Vec3, l: Vec3, roughness: f32) -> f32 {\n    let ndot_v = n.dot(v).max(0.0);\n    let ndot_l = n.dot(l).max(0.0);\n    let ggx1 = geometry_schlick_ggx(ndot_v, roughness);\n    let ggx2 = geometry_schlick_ggx(ndot_l, roughness);\n\n    ggx1 * ggx2\n}\n\n/// Fresnel-Schlick approximation function.\n///\n/// The Fresnel equation describes the ratio of light that gets reflected over\n/// the light that gets refracted, which varies over the angle we're looking at\n/// a surface. The moment light hits a surface, based on the surface-to-view\n/// angle, the Fresnel equation tells us the percentage of light that gets\n/// reflected. From this ratio of reflection and the energy conservation\n/// principle we can directly obtain the refracted portion of light.\nfn fresnel_schlick(\n    // dot product result between the surface's normal n and the halfway h (or view v) direction.\n    cos_theta: f32,\n    // surface reflection at zero incidence (how much the surface reflects if looking directly at\n    // the surface)\n    f0: Vec3,\n) -> Vec3 {\n    f0 + (1.0 - f0) * (1.0 - cos_theta).clamp(0.0, 1.0).powf(5.0)\n}\n\nfn fresnel_schlick_roughness(cos_theta: f32, f0: Vec3, roughness: f32) -> Vec3 {\n    f0 + (Vec3::splat(1.0 - roughness).max(f0) - f0) * (1.0 - cos_theta).clamp(0.0, 1.0).powf(5.0)\n}\n\n#[allow(clippy::too_many_arguments)]\npub fn outgoing_radiance(\n    light_color: Vec4,\n    albedo: Vec3,\n    attenuation: f32,\n    v: Vec3,\n    l: Vec3,\n    n: Vec3,\n    metalness: f32,\n    roughness: f32,\n) -> Vec3 {\n    my_println!(\"outgoing_radiance\");\n    my_println!(\"    light_color: {light_color:?}\");\n    my_println!(\"    albedo: {albedo:?}\");\n    my_println!(\"    attenuation: {attenuation:?}\");\n    my_println!(\"    v: {v:?}\");\n    my_println!(\"    l: {l:?}\");\n    my_println!(\"    n: {n:?}\");\n    my_println!(\"    metalness: {metalness:?}\");\n    my_println!(\"    roughness: {roughness:?}\");\n\n    let f0 = Vec3::splat(0.4).lerp(albedo, metalness);\n    my_println!(\"    f0: {f0:?}\");\n    let radiance = light_color.xyz() * attenuation;\n    my_println!(\"    radiance: {radiance:?}\");\n    let h = (v + l).alt_norm_or_zero();\n    my_println!(\"    h: {h:?}\");\n    // cook-torrance brdf\n    let ndf: f32 = normal_distribution_ggx(n, h, roughness);\n    my_println!(\"    ndf: {ndf:?}\");\n    let g: f32 = geometry_smith(n, v, l, roughness);\n    my_println!(\"    g: {g:?}\");\n    let f: Vec3 = fresnel_schlick(h.dot(v).max(0.0), f0);\n    my_println!(\"    f: {f:?}\");\n\n    let k_s = f;\n    let k_d = (Vec3::splat(1.0) - k_s) * (1.0 - metalness);\n    my_println!(\"    k_s: {k_s:?}\");\n\n    let numerator: Vec3 = ndf * g * f;\n    my_println!(\"    numerator: {numerator:?}\");\n    let n_dot_l = n.dot(l).max(0.0);\n    my_println!(\"    n_dot_l: {n_dot_l:?}\");\n    let denominator: f32 = 4.0 * n.dot(v).max(0.0) * n_dot_l + 0.0001;\n    my_println!(\"    denominator: {denominator:?}\");\n    let specular: Vec3 = numerator / denominator;\n    my_println!(\"    specular: {specular:?}\");\n\n    (k_d * albedo / core::f32::consts::PI + specular) * radiance * n_dot_l\n}\n\npub fn sample_irradiance<T: SampleCube<Sampler = S>, S: IsSampler>(\n    irradiance: &T,\n    irradiance_sampler: &S,\n    // Normal vector\n    n: Vec3,\n) -> Vec3 {\n    irradiance.sample_by_lod(*irradiance_sampler, n, 0.0).xyz()\n}\n\npub fn sample_specular_reflection<T: SampleCube<Sampler = S>, S: IsSampler>(\n    prefiltered: &T,\n    prefiltered_sampler: &S,\n    // camera position in world space\n    camera_pos: Vec3,\n    // fragment position in world space\n    in_pos: Vec3,\n    // normal vector\n    n: Vec3,\n    roughness: f32,\n) -> Vec3 {\n    let v = (camera_pos - in_pos).alt_norm_or_zero();\n    let reflect_dir = math::reflect(-v, n);\n    prefiltered\n        .sample_by_lod(*prefiltered_sampler, reflect_dir, roughness * 4.0)\n        .xyz()\n}\n\n/// Returns the `Material` from the stage's slab.\npub fn get_material(\n    material_id: Id<MaterialDescriptor>,\n    has_lighting: bool,\n    material_slab: &[u32],\n) -> MaterialDescriptor {\n    if material_id.is_none() {\n        // without an explicit material (or if the entire render has no lighting)\n        // the entity will not participate in any lighting calculations\n        MaterialDescriptor {\n            has_lighting: false,\n            ..Default::default()\n        }\n    } else {\n        let mut material = material_slab.read_unchecked(material_id);\n        material.has_lighting &= has_lighting;\n        material\n    }\n}\n\npub fn texture_color<A: Sample2dArray<Sampler = S>, S: IsSampler>(\n    texture_id: Id<AtlasTextureDescriptor>,\n    uv: Vec2,\n    atlas: &A,\n    sampler: &S,\n    atlas_size: glam::UVec2,\n    material_slab: &[u32],\n) -> Vec4 {\n    let texture = material_slab.read(texture_id);\n    // uv is [0, 0] when texture_id is Id::NONE\n    let uv = texture.uv(uv, atlas_size);\n    crate::println!(\"uv: {uv}\");\n    let mut color: Vec4 = atlas.sample_by_lod(*sampler, uv, 0.0);\n    if texture_id.is_none() {\n        color = Vec4::splat(1.0);\n    }\n    color\n}\n\n/// PBR fragment shader capable of being run on CPU or GPU.\n#[allow(clippy::too_many_arguments)]\npub fn fragment_impl<A, T, DtA, C, S>(\n    atlas: &A,\n    atlas_sampler: &S,\n    irradiance: &C,\n    irradiance_sampler: &S,\n    prefiltered: &C,\n    prefiltered_sampler: &S,\n    brdf: &T,\n    brdf_sampler: &S,\n    shadow_map: &DtA,\n    shadow_map_sampler: &S,\n\n    geometry_slab: &[u32],\n    material_slab: &[u32],\n    lighting_slab: &[u32],\n\n    renderlet_id: Id<PrimitiveDescriptor>,\n\n    frag_coord: Vec4,\n    in_color: Vec4,\n    in_uv0: Vec2,\n    in_uv1: Vec2,\n    in_norm: Vec3,\n    in_tangent: Vec3,\n    in_bitangent: Vec3,\n    in_pos: Vec3,\n\n    output: &mut Vec4,\n) where\n    A: Sample2dArray<Sampler = S>,\n    T: Sample2d<Sampler = S>,\n    DtA: Sample2dArray<Sampler = S>,\n    C: SampleCube<Sampler = S>,\n    S: IsSampler,\n{\n    let renderlet = geometry_slab.read_unchecked(renderlet_id);\n    let geom_desc = geometry_slab.read_unchecked(renderlet.geometry_descriptor_id);\n    crate::println!(\"pbr_desc_id: {:?}\", renderlet.geometry_descriptor_id);\n    crate::println!(\"pbr_desc: {geom_desc:#?}\");\n    let GeometryDescriptor {\n        camera_id,\n        atlas_size,\n        resolution: _,\n        debug_channel,\n        has_lighting,\n        has_skinning: _,\n        perform_frustum_culling: _,\n        perform_occlusion_culling: _,\n    } = geom_desc;\n\n    let material = get_material(renderlet.material_id, has_lighting, material_slab);\n    crate::println!(\"material: {:#?}\", material);\n\n    let albedo_tex_uv = if material.albedo_tex_coord == 0 {\n        in_uv0\n    } else {\n        in_uv1\n    };\n    let albedo_tex_color = texture_color(\n        material.albedo_texture_id,\n        albedo_tex_uv,\n        atlas,\n        atlas_sampler,\n        atlas_size,\n        material_slab,\n    );\n    my_println!(\"albedo_tex_color: {:?}\", albedo_tex_color);\n\n    let metallic_roughness_uv = if material.metallic_roughness_tex_coord == 0 {\n        in_uv0\n    } else {\n        in_uv1\n    };\n    let metallic_roughness_tex_color = texture_color(\n        material.metallic_roughness_texture_id,\n        metallic_roughness_uv,\n        atlas,\n        atlas_sampler,\n        atlas_size,\n        material_slab,\n    );\n    my_println!(\n        \"metallic_roughness_tex_color: {:?}\",\n        metallic_roughness_tex_color\n    );\n\n    let normal_tex_uv = if material.normal_tex_coord == 0 {\n        in_uv0\n    } else {\n        in_uv1\n    };\n    let normal_tex_color = texture_color(\n        material.normal_texture_id,\n        normal_tex_uv,\n        atlas,\n        atlas_sampler,\n        atlas_size,\n        material_slab,\n    );\n    my_println!(\"normal_tex_color: {:?}\", normal_tex_color);\n\n    let ao_tex_uv = if material.ao_tex_coord == 0 {\n        in_uv0\n    } else {\n        in_uv1\n    };\n    let ao_tex_color = texture_color(\n        material.ao_texture_id,\n        ao_tex_uv,\n        atlas,\n        atlas_sampler,\n        atlas_size,\n        material_slab,\n    );\n\n    let emissive_tex_uv = if material.emissive_tex_coord == 0 {\n        in_uv0\n    } else {\n        in_uv1\n    };\n    let emissive_tex_color = texture_color(\n        material.emissive_texture_id,\n        emissive_tex_uv,\n        atlas,\n        atlas_sampler,\n        atlas_size,\n        material_slab,\n    );\n\n    let (norm, uv_norm) = if material.normal_texture_id.is_none() {\n        // there is no normal map, use the normal normal ;)\n        (in_norm, Vec3::ZERO)\n    } else {\n        // convert the normal from color coords to tangent space -1,1\n        let sampled_norm = (normal_tex_color.xyz() * 2.0 - Vec3::splat(1.0)).alt_norm_or_zero();\n        let tbn = glam::mat3(\n            in_tangent.alt_norm_or_zero(),\n            in_bitangent.alt_norm_or_zero(),\n            in_norm.alt_norm_or_zero(),\n        );\n        // convert the normal from tangent space to world space\n        let norm = (tbn * sampled_norm).alt_norm_or_zero();\n        (norm, sampled_norm)\n    };\n\n    let n = norm;\n    let albedo = albedo_tex_color * material.albedo_factor * in_color;\n    let roughness = metallic_roughness_tex_color.y * material.roughness_factor;\n    let metallic = metallic_roughness_tex_color.z * material.metallic_factor;\n    let ao = 1.0 + material.ao_strength * (ao_tex_color.x - 1.0);\n    let emissive =\n        emissive_tex_color.xyz() * material.emissive_factor * material.emissive_strength_multiplier;\n    let irradiance = sample_irradiance(irradiance, irradiance_sampler, n);\n    let camera = geometry_slab.read(camera_id);\n    let specular = sample_specular_reflection(\n        prefiltered,\n        prefiltered_sampler,\n        camera.position(),\n        in_pos,\n        n,\n        roughness,\n    );\n    let brdf =\n        brdf::shader::sample_brdf(brdf, brdf_sampler, camera.position(), in_pos, n, roughness);\n\n    fn colorize(u: Vec3) -> Vec4 {\n        ((u.alt_norm_or_zero() + Vec3::splat(1.0)) / 2.0).extend(1.0)\n    }\n\n    crate::println!(\"debug_mode: {debug_channel:?}\");\n    use debug::DebugChannel::*;\n    match debug_channel {\n        None => {}\n        UvCoords0 => {\n            *output = colorize(Vec3::new(in_uv0.x, in_uv0.y, 0.0));\n            return;\n        }\n        UvCoords1 => {\n            *output = colorize(Vec3::new(in_uv1.x, in_uv1.y, 0.0));\n            return;\n        }\n        Normals => {\n            *output = colorize(norm);\n            return;\n        }\n        VertexColor => {\n            *output = in_color;\n            return;\n        }\n        VertexNormals => {\n            *output = colorize(in_norm);\n            return;\n        }\n        UvNormals => {\n            *output = colorize(uv_norm);\n            return;\n        }\n        Tangents => {\n            *output = colorize(in_tangent);\n            return;\n        }\n        Bitangents => {\n            *output = colorize(in_bitangent);\n            return;\n        }\n        DiffuseIrradiance => {\n            *output = irradiance.extend(1.0);\n            return;\n        }\n        SpecularReflection => {\n            *output = specular.extend(1.0);\n            return;\n        }\n        Brdf => {\n            *output = brdf.extend(1.0).extend(1.0);\n            return;\n        }\n        Roughness => {\n            *output = Vec3::splat(roughness).extend(1.0);\n            return;\n        }\n        Metallic => {\n            *output = Vec3::splat(metallic).extend(1.0);\n            return;\n        }\n        Albedo => {\n            *output = albedo;\n            return;\n        }\n        Occlusion => {\n            *output = Vec3::splat(ao).extend(1.0);\n            return;\n        }\n        Emissive => {\n            *output = emissive.extend(1.0);\n            return;\n        }\n        UvEmissive => {\n            *output = emissive_tex_color.xyz().extend(1.0);\n            return;\n        }\n        EmissiveFactor => {\n            *output = material.emissive_factor.extend(1.0);\n            return;\n        }\n        EmissiveStrength => {\n            *output = Vec3::splat(material.emissive_strength_multiplier).extend(1.0);\n            return;\n        }\n    }\n\n    *output = if material.has_lighting {\n        shade_fragment(\n            shadow_map,\n            shadow_map_sampler,\n            camera.position(),\n            n,\n            in_pos,\n            albedo.xyz(),\n            metallic,\n            roughness,\n            ao,\n            emissive,\n            irradiance,\n            specular,\n            brdf,\n            lighting_slab,\n            frag_coord,\n        )\n    } else {\n        crate::println!(\"no shading!\");\n        in_color * albedo_tex_color * material.albedo_factor\n    };\n}\n\n#[allow(clippy::too_many_arguments)]\npub fn shade_fragment<S, T>(\n    shadow_map: &T,\n    shadow_map_sampler: &S,\n    // camera's position in world space\n    camera_pos: Vec3,\n    // normal of the fragment\n    in_norm: Vec3,\n    // position of the fragment in world space\n    in_pos: Vec3,\n    // base color of the fragment\n    albedo: Vec3,\n    metallic: f32,\n    roughness: f32,\n    ao: f32,\n    emissive: Vec3,\n    irradiance: Vec3,\n    prefiltered: Vec3,\n    brdf: Vec2,\n\n    light_slab: &[u32],\n    frag_coord: Vec4,\n) -> Vec4\nwhere\n    S: IsSampler,\n    T: Sample2dArray<Sampler = S>,\n{\n    let n = in_norm.alt_norm_or_zero();\n    let v = (camera_pos - in_pos).alt_norm_or_zero();\n    // There is always a `LightingDescriptor` stored at index `0` of the\n    // light slab.\n    let lighting_desc = light_slab.read_unchecked(Id::<LightingDescriptor>::new(0));\n    // If light tiling is enabled, use the pre-computed tile's light list\n    let analytical_lights_array = if lighting_desc.light_tiling_descriptor_id.is_none() {\n        lighting_desc.analytical_lights_array\n    } else {\n        let tiling_descriptor = light_slab.read_unchecked(lighting_desc.light_tiling_descriptor_id);\n        let tile_index = tiling_descriptor.tile_index_for_fragment(frag_coord.xy());\n        let tile = light_slab.read_unchecked(tiling_descriptor.tiles_array.at(tile_index));\n        tile.lights_array\n    };\n    my_println!(\"lights: {analytical_lights_array:?}\");\n    my_println!(\"surface normal: {n:?}\");\n    my_println!(\"vector from surface to camera: {v:?}\");\n\n    // accumulated outgoing radiance\n    let mut lo = Vec3::ZERO;\n    for light_id_id in analytical_lights_array.iter() {\n        // calculate per-light radiance\n        let light_id = light_slab.read(light_id_id);\n        if light_id.is_none() {\n            break;\n        }\n        let light = light_slab.read(light_id);\n        let transform = light_slab.read(light.transform_id);\n        crate::println!(\"transform: {transform:?}\");\n        let transform = Mat4::from(transform);\n\n        // determine the light ray and the radiance\n        let (radiance, shadow) = match light.light_type {\n            LightStyle::Point => {\n                let PointLightDescriptor {\n                    position,\n                    color,\n                    intensity: Candela(intensity_candelas),\n                } = light_slab.read(light.into_point_id());\n                // Convert candelas into radiometric\n                // TODO: write true radiometric light conversions for Lux and Candela\n                let intensity = intensity_candelas / 683.0;\n                let position = transform.transform_point3(position);\n                // This definitely is the direction pointing from fragment to the light.\n                // It needs to stay this way.\n                // For more info, see\n                // <https://renderling.xyz/articles/live/light_tiling.html#point_and_spotlight_discrepancies__fri_11_june>\n                let frag_to_light = position - in_pos;\n                let distance = frag_to_light.length();\n                if distance == 0.0 {\n                    crate::println!(\"distance between point light and surface is zero\");\n                    continue;\n                }\n                let l = frag_to_light.alt_norm_or_zero();\n                let attenuation = intensity / (distance * distance);\n                let radiance =\n                    outgoing_radiance(color, albedo, attenuation, v, l, n, metallic, roughness);\n                let shadow = if light.shadow_map_desc_id.is_some() {\n                    // Shadow is 1.0 when the fragment is in the shadow of this light,\n                    // and 0.0 in darkness\n                    ShadowCalculation::new(light_slab, light, in_pos, n, l).run_point(\n                        light_slab,\n                        shadow_map,\n                        shadow_map_sampler,\n                        position,\n                    )\n                } else {\n                    0.0\n                };\n                (radiance, shadow)\n            }\n\n            LightStyle::Spot => {\n                let spot_light_descriptor = light_slab.read(light.into_spot_id());\n                let calculation =\n                    SpotLightCalculation::new(spot_light_descriptor, transform, in_pos);\n                crate::println!(\"calculation: {calculation:#?}\");\n                if calculation.frag_to_light_distance == 0.0 {\n                    continue;\n                }\n                // Convert from candelas to a radiometric unit.\n                //\n                // TODO: verify that spot light radiometric conversion is correct.\n                let intensity =\n                // TODO: write true radiometric light conversions for Lux and Candela\n                    spot_light_descriptor.intensity.0 / (683.0 * 4.0 * core::f32::consts::PI);\n                let attenuation: f32 = intensity * calculation.contribution;\n                let radiance = outgoing_radiance(\n                    spot_light_descriptor.color,\n                    albedo,\n                    attenuation,\n                    v,\n                    calculation.frag_to_light,\n                    n,\n                    metallic,\n                    roughness,\n                );\n                let shadow = if light.shadow_map_desc_id.is_some() {\n                    // Shadow is 1.0 when the fragment is in the shadow of this light,\n                    // and 0.0 in darkness\n                    ShadowCalculation::new(light_slab, light, in_pos, n, calculation.frag_to_light)\n                        .run_directional_or_spot(light_slab, shadow_map, shadow_map_sampler)\n                } else {\n                    0.0\n                };\n                (radiance, shadow)\n            }\n\n            LightStyle::Directional => {\n                let DirectionalLightDescriptor {\n                    direction,\n                    color,\n                    intensity: intensity_lux,\n                } = light_slab.read(light.into_directional_id());\n                let direction = transform.transform_vector3(direction);\n                let l = -direction.alt_norm_or_zero();\n                // TODO: write true radiometric light conversions for Lux and Candela\n                let attenuation = intensity_lux.0 / 683.0;\n                let radiance =\n                    outgoing_radiance(color, albedo, attenuation, v, l, n, metallic, roughness);\n                let shadow =\n                    if light.shadow_map_desc_id.is_some() {\n                        // Shadow is 1.0 when the fragment is in the shadow of this light,\n                        // and 0.0 in darkness\n                        ShadowCalculation::new(light_slab, light, in_pos, n, l)\n                            .run_directional_or_spot(light_slab, shadow_map, shadow_map_sampler)\n                    } else {\n                        0.0\n                    };\n                (radiance, shadow)\n            }\n        };\n        crate::println!(\"radiance: {radiance}\");\n        crate::println!(\"shadow: {shadow}\");\n        lo += radiance * (1.0 - shadow);\n    }\n\n    my_println!(\"lo: {lo:?}\");\n    // calculate reflectance at normal incidence; if dia-electric (like plastic) use\n    // F0 of 0.04 and if it's a metal, use the albedo color as F0 (metallic\n    // workflow)\n    let f0: Vec3 = Vec3::splat(0.04).lerp(albedo, metallic);\n    let cos_theta = n.dot(v).max(0.0);\n    let fresnel = fresnel_schlick_roughness(cos_theta, f0, roughness);\n    let ks = fresnel;\n    let kd = (1.0 - ks) * (1.0 - metallic);\n    let diffuse = irradiance * albedo;\n    let specular = prefiltered * (fresnel * brdf.x + brdf.y);\n    // Global ambient light contribution, modulated by surface albedo and AO.\n    let ambient_light = lighting_desc.ambient_color.xyz() * lighting_desc.ambient_color.w;\n    let color = (kd * diffuse + specular) * ao + lo + emissive + ambient_light * albedo * ao;\n    color.extend(1.0)\n}\n"
  },
  {
    "path": "crates/renderling/src/pbr.rs",
    "content": "//! \"Physically based\" types and functions.\n//!\n//! ## References\n//! * <https://learnopengl.com/PBR/Theory>\n//! * <https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/5b1b7f48a8cb2b7aaef00d08fdba18ccc8dd331b/source/Renderer/shaders/pbr.frag>\n//! * <https://github.khronos.org/glTF-Sample-Viewer-Release/>\n\npub mod brdf;\npub mod debug;\npub mod ibl;\n\npub mod shader;\n\n#[cfg(test)]\nmod test {\n    use crate::{\n        atlas::AtlasImage,\n        geometry::Vertex,\n        glam::{Vec3, Vec4},\n        test::BlockOnFuture,\n    };\n\n    #[test]\n    // TODO: Move this over to a manual example\n    // Tests the initial implementation of pbr metallic roughness on an array of\n    // spheres with different metallic roughnesses lit by an environment map.\n    //\n    // see https://learnopengl.com/PBR/Lighting\n    fn pbr_metallic_roughness_spheres() {\n        let ss = 600;\n        let ctx = crate::context::Context::headless(ss, ss).block();\n        let stage = ctx.new_stage();\n\n        let radius = 0.5;\n        let ss = ss as f32;\n        let projection = crate::camera::perspective(ss, ss);\n        let k = 7;\n        let diameter = 2.0 * radius;\n        let spacing = radius * 0.25;\n        let len = (k - 1) as f32 * (diameter + spacing);\n        let half = len / 2.0;\n        let view = crate::camera::look_at(\n            Vec3::new(half, half, 1.6 * len),\n            Vec3::new(half, half, 0.0),\n            Vec3::Y,\n        );\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n\n        let geometry = stage.new_vertices({\n            let mut icosphere = icosahedron::Polyhedron::new_isocahedron(radius, 5);\n            icosphere.compute_triangle_normals();\n            let icosahedron::Polyhedron {\n                positions,\n                normals,\n                cells,\n                ..\n            } = icosphere;\n            log::info!(\"icosphere created on CPU\");\n\n            let to_vertex = |ndx: &usize| -> Vertex {\n                let p: [f32; 3] = positions[*ndx].0.into();\n                let n: [f32; 3] = normals[*ndx].0.into();\n                Vertex::default().with_position(p).with_normal(n)\n            };\n            cells\n                .iter()\n                .flat_map(|icosahedron::Triangle { a, b, c }| {\n                    let p0 = to_vertex(a);\n                    let p1 = to_vertex(b);\n                    let p2 = to_vertex(c);\n                    vec![p0, p1, p2]\n                })\n                .collect::<Vec<_>>()\n        });\n        let mut spheres = vec![];\n        for i in 0..k {\n            let roughness = i as f32 / (k - 1) as f32;\n            let x = (diameter + spacing) * i as f32;\n            for j in 0..k {\n                let metallic = j as f32 / (k - 1) as f32;\n                let y = (diameter + spacing) * j as f32;\n\n                let rez = stage\n                    .new_primitive()\n                    .with_material(\n                        stage\n                            .new_material()\n                            .with_albedo_factor(Vec4::new(1.0, 1.0, 1.0, 1.0))\n                            .with_metallic_factor(metallic)\n                            .with_roughness_factor(roughness),\n                    )\n                    .with_transform(stage.new_transform().with_translation(Vec3::new(x, y, 0.0)))\n                    .with_vertices(&geometry);\n                spheres.push(rez);\n            }\n        }\n\n        let hdr_image = AtlasImage::from_hdr_path(\"../../img/hdr/resting_place.hdr\").unwrap();\n        let skybox = crate::skybox::Skybox::new(&ctx, hdr_image);\n        stage.use_skybox(&skybox);\n\n        let ibl = stage.new_ibl(&skybox);\n        stage.use_ibl(&ibl);\n\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq(\"pbr/metallic_roughness_spheres.png\", img);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/primitive/cpu.rs",
    "content": "//! Mesh primitives.\n\nuse core::ops::Deref;\nuse std::sync::{Arc, Mutex};\n\nuse craballoc::value::Hybrid;\nuse crabslab::{Array, Id};\n\nuse crate::{\n    bvol::BoundingSphere,\n    geometry::{Indices, MorphTargetWeights, MorphTargets, Skin, Vertices},\n    material::Material,\n    primitive::shader::PrimitiveDescriptor,\n    stage::Stage,\n    transform::Transform,\n    types::GpuOnlyArray,\n};\n\n/// A unit of rendering.\n///\n/// A `Primitive` represents one draw call, or one mesh primitive.\npub struct Primitive {\n    pub(crate) descriptor: Hybrid<PrimitiveDescriptor>,\n\n    vertices: Arc<Mutex<Option<Vertices<GpuOnlyArray>>>>,\n    indices: Arc<Mutex<Option<Indices<GpuOnlyArray>>>>,\n\n    pub(crate) transform: Arc<Mutex<Option<Transform>>>,\n    pub(crate) material: Arc<Mutex<Option<Material>>>,\n    skin: Arc<Mutex<Option<Skin>>>,\n    morph_targets: Arc<Mutex<Option<(MorphTargets, MorphTargetWeights)>>>,\n}\n\nimpl Primitive {\n    /// Create a new [`Primitive`], automatically adding it to the\n    /// [`Stage`](crate::stage::Stage) to be drawn.\n    ///\n    /// The returned [`Primitive`] will have the stage's default [`Vertices`],\n    /// which is an all-white unit cube.\n    pub fn new(stage: &Stage) -> Self {\n        let descriptor = stage\n            .geometry\n            .slab_allocator()\n            .new_value(PrimitiveDescriptor::default());\n        let primitive = Primitive {\n            descriptor,\n            vertices: Default::default(),\n            indices: Default::default(),\n            transform: Default::default(),\n            material: Default::default(),\n            skin: Default::default(),\n            morph_targets: Default::default(),\n        }\n        .with_vertices(stage.default_vertices());\n        stage.add_primitive(&primitive);\n        primitive\n    }\n}\n\nimpl Clone for Primitive {\n    fn clone(&self) -> Self {\n        Self {\n            descriptor: self.descriptor.clone(),\n            vertices: self.vertices.clone(),\n            indices: self.indices.clone(),\n            transform: self.transform.clone(),\n            material: self.material.clone(),\n            skin: self.skin.clone(),\n            morph_targets: self.morph_targets.clone(),\n        }\n    }\n}\n\n// Vertices impls\nimpl Primitive {\n    /// Set the vertex data of this primitive.\n    pub fn set_vertices(&self, vertices: impl Into<Vertices<GpuOnlyArray>>) -> &Self {\n        let vertices = vertices.into();\n        let array = vertices.array();\n        self.descriptor.modify(|d| d.vertices_array = array);\n        *self.vertices.lock().expect(\"vertices lock\") = Some(vertices.clone());\n        self\n    }\n\n    /// Set the vertex data of this primitive and return the primitive.\n    pub fn with_vertices(self, vertices: impl Into<Vertices<GpuOnlyArray>>) -> Self {\n        self.set_vertices(vertices);\n        self\n    }\n}\n\n// Indices impls\nimpl Primitive {\n    /// Set the index data of this primitive.\n    pub fn set_indices(&self, indices: impl Into<Indices<GpuOnlyArray>>) -> &Self {\n        let indices = indices.into();\n        let array = indices.array();\n        self.descriptor.modify(|d| d.indices_array = array);\n        *self.indices.lock().expect(\"indices lock\") = Some(indices.clone());\n        self\n    }\n\n    /// Set the index data of this primitive and return the primitive.\n    pub fn with_indices(self, indices: impl Into<Indices<GpuOnlyArray>>) -> Self {\n        self.set_indices(indices);\n        self\n    }\n\n    /// Remove the indices from this primitive.\n    pub fn remove_indices(&self) -> &Self {\n        *self.indices.lock().expect(\"indices lock\") = None;\n        self.descriptor.modify(|d| d.indices_array = Array::NONE);\n        self\n    }\n}\n\n// PrimitiveDescriptor impls\nimpl Primitive {\n    /// Return a pointer to the underlying descriptor on the GPU.\n    pub fn id(&self) -> Id<PrimitiveDescriptor> {\n        self.descriptor.id()\n    }\n\n    /// Return the underlying descriptor.\n    pub fn descriptor(&self) -> PrimitiveDescriptor {\n        self.descriptor.get()\n    }\n\n    /// Set the bounds of this primitive.\n    pub fn set_bounds(&self, bounds: BoundingSphere) -> &Self {\n        self.descriptor.modify(|d| d.bounds = bounds);\n        self\n    }\n\n    /// Set the bounds and return the primitive.\n    pub fn with_bounds(self, bounds: BoundingSphere) -> Self {\n        self.set_bounds(bounds);\n        self\n    }\n\n    /// Get the bounds.\n    ///\n    /// Returns the current [`BoundingSphere`].\n    pub fn bounds(&self) -> BoundingSphere {\n        self.descriptor.get().bounds\n    }\n\n    /// Modify the bounds of the primitive.\n    ///\n    /// # Arguments\n    ///\n    /// * `f` - A closure that takes a mutable reference to the\n    ///   [`BoundingSphere`] and returns a value of type `T`.\n    pub fn modify_bounds<T: 'static>(&self, f: impl FnOnce(&mut BoundingSphere) -> T) -> T {\n        self.descriptor.modify(|d| f(&mut d.bounds))\n    }\n\n    /// Set the visibility of this primitive.\n    pub fn set_visible(&self, visible: bool) -> &Self {\n        self.descriptor.modify(|d| d.visible = visible);\n        self\n    }\n\n    /// Set the visibility and return the primitive.\n    pub fn with_visible(self, visible: bool) -> Self {\n        self.set_visible(visible);\n        self\n    }\n\n    /// Return the primitive's visibility.\n    pub fn visible(&self) -> bool {\n        self.descriptor.get().visible\n    }\n\n    /// Modify the visible of the primitive.\n    ///\n    /// # Arguments\n    ///\n    /// * `f` - A closure that takes a mutable reference to the visibility and\n    ///   returns a value of type `T`.\n    pub fn modify_visible<T: 'static>(&self, f: impl FnOnce(&mut bool) -> T) -> T {\n        self.descriptor.modify(|d| f(&mut d.visible))\n    }\n}\n\n// Transform functions\nimpl Primitive {\n    /// Set the transform.\n    ///\n    /// # Note\n    /// This can be set with [`Transform`] or\n    /// [`NestedTransform`](crate::transform::NestedTransform).\n    pub fn set_transform(&self, transform: impl Into<Transform>) -> &Self {\n        let transform = transform.into();\n        self.descriptor.modify(|d| d.transform_id = transform.id());\n        *self.transform.lock().expect(\"transform lock\") = Some(transform.clone());\n        self\n    }\n\n    /// Set the transform and return the `Primitive`.\n    ///\n    /// # Note\n    /// This can be set with [`Transform`] or\n    /// [`NestedTransform`](crate::transform::NestedTransform).\n    pub fn with_transform(self, transform: impl Into<Transform>) -> Self {\n        self.set_transform(transform);\n        self\n    }\n\n    /// Get the transform.\n    ///\n    /// Returns a reference to the current `Transform`, if any.\n    pub fn transform(&self) -> impl Deref<Target = Option<Transform>> + '_ {\n        self.transform.lock().expect(\"transform lock\")\n    }\n\n    /// Remove the transform from this primitive.\n    ///\n    /// This effectively makes the transform the identity.\n    pub fn remove_transform(&self) -> &Self {\n        self.descriptor.modify(|d| d.transform_id = Id::NONE);\n        *self.transform.lock().expect(\"transform lock\") = None;\n        self\n    }\n}\n\n// Material impls\nimpl Primitive {\n    /// Set the material of this primitive.\n    pub fn set_material(&self, material: impl Into<Material>) -> &Self {\n        let material = material.into();\n        self.descriptor.modify(|d| d.material_id = material.id());\n        *self.material.lock().expect(\"material lock\") = Some(material);\n        self\n    }\n\n    /// Set the material and return the primitive.\n    pub fn with_material(self, material: impl Into<Material>) -> Self {\n        self.set_material(material);\n        self\n    }\n\n    /// Get the material.\n    ///\n    /// Returns a reference to the current `Material`, if any.\n    pub fn material(&self) -> impl Deref<Target = Option<Material>> + '_ {\n        self.material.lock().expect(\"material lock\")\n    }\n\n    /// Remove the material from this primitive.\n    pub fn remove_material(&self) -> &Self {\n        self.descriptor.modify(|d| d.material_id = Id::NONE);\n        *self.material.lock().expect(\"material lock\") = None;\n        self\n    }\n}\n\n// Skin impls\nimpl Primitive {\n    /// Set the skin of this primitive.\n    pub fn set_skin(&self, skin: impl Into<Skin>) -> &Self {\n        let skin = skin.into();\n        self.descriptor.modify(|d| d.skin_id = skin.id());\n        *self.skin.lock().expect(\"skin lock\") = Some(skin.clone());\n        self\n    }\n\n    /// Set the skin and return the primitive.\n    pub fn with_skin(self, skin: impl Into<Skin>) -> Self {\n        self.set_skin(skin);\n        self\n    }\n\n    /// Get the skin.\n    ///\n    /// Returns a reference to the current `Skin`, if any.\n    pub fn skin(&self) -> impl Deref<Target = Option<Skin>> + '_ {\n        self.skin.lock().expect(\"skin lock\")\n    }\n\n    /// Remove the skin from this primitive.\n    pub fn remove_skin(&self) -> &Self {\n        self.descriptor.modify(|d| d.skin_id = Id::NONE);\n        *self.skin.lock().expect(\"skin lock\") = None;\n        self\n    }\n}\n\n// (MorphTargets, MorphTargetsWeights) impls\nimpl Primitive {\n    /// Set the morph targets and weights of this primitive.\n    pub fn set_morph_targets(\n        &self,\n        morph_targets: impl Into<MorphTargets>,\n        weights: impl Into<MorphTargetWeights>,\n    ) -> &Self {\n        let morph_targets = morph_targets.into();\n        let weights = weights.into();\n        self.descriptor.modify(|d| {\n            d.morph_targets = morph_targets.array();\n            d.morph_weights = weights.array();\n        });\n        *self.morph_targets.lock().expect(\"morph_targets lock\") =\n            Some((morph_targets.clone(), weights.clone()));\n        self\n    }\n\n    /// Set the morph targets and weights and return the primitive.\n    pub fn with_morph_targets(\n        self,\n        morph_targets: impl Into<MorphTargets>,\n        weights: impl Into<MorphTargetWeights>,\n    ) -> Self {\n        self.set_morph_targets(morph_targets, weights);\n        self\n    }\n\n    /// Get the morph targets and weights.\n    ///\n    /// Returns a reference to the current `MorphTargets` and\n    /// `MorphTargetsWeights`, if any.\n    pub fn morph_targets(\n        &self,\n    ) -> impl Deref<Target = Option<(MorphTargets, MorphTargetWeights)>> + '_ {\n        self.morph_targets.lock().expect(\"morph_targets lock\")\n    }\n\n    /// Remove the morph targets and weights from this primitive.\n    pub fn remove_morph_targets(&self) -> &Self {\n        self.descriptor.modify(|d| {\n            d.morph_targets = Array::NONE;\n            d.morph_weights = Array::NONE;\n        });\n        *self.morph_targets.lock().expect(\"morph_targets lock\") = None;\n        self\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/primitive/shader.rs",
    "content": "//! Shader support for rendering primitives.\nuse crabslab::{Array, Id, Slab, SlabItem};\nuse glam::{Mat4, Vec2, Vec3, Vec4, Vec4Swizzles};\nuse spirv_std::{\n    image::{Cubemap, Image2d, Image2dArray},\n    spirv, Image, Sampler,\n};\n\n// use glam::Mat4;\n// #[cfg(not(target_arch = \"spirv\"))]\n// use glam::UVec2;\n\n// #[allow(unused_imports)]\n// use spirv_std::num_traits::Float;\n\nuse crate::{\n    bvol::BoundingSphere,\n    geometry::{\n        shader::{GeometryDescriptor, SkinDescriptor},\n        MorphTarget, Vertex,\n    },\n    material::shader::MaterialDescriptor,\n    math::IsVector,\n    transform::shader::TransformDescriptor,\n};\n\n#[allow(unused_imports)]\nuse spirv_std::num_traits::Float;\n\n/// Returned by [`PrimitiveDescriptor::get_vertex_info`].\npub struct VertexInfo {\n    pub vertex: Vertex,\n    pub transform: TransformDescriptor,\n    pub model_matrix: Mat4,\n    pub world_pos: Vec3,\n}\n\n/// A draw call used to render some geometry.\n#[derive(Clone, Copy, PartialEq, SlabItem, Debug)]\n#[offsets]\npub struct PrimitiveDescriptor {\n    pub visible: bool,\n    pub vertices_array: Array<Vertex>,\n    /// Bounding sphere of the entire primitive, in local space.\n    pub bounds: BoundingSphere,\n    pub indices_array: Array<u32>,\n    pub transform_id: Id<TransformDescriptor>,\n    pub material_id: Id<MaterialDescriptor>,\n    pub skin_id: Id<SkinDescriptor>,\n    pub morph_targets: Array<Array<MorphTarget>>,\n    pub morph_weights: Array<f32>,\n    pub geometry_descriptor_id: Id<GeometryDescriptor>,\n}\n\nimpl Default for PrimitiveDescriptor {\n    fn default() -> Self {\n        PrimitiveDescriptor {\n            visible: true,\n            vertices_array: Array::default(),\n            bounds: BoundingSphere::default(),\n            indices_array: Array::default(),\n            transform_id: Id::NONE,\n            material_id: Id::NONE,\n            skin_id: Id::NONE,\n            morph_targets: Array::default(),\n            morph_weights: Array::default(),\n            geometry_descriptor_id: Id::new(0),\n        }\n    }\n}\n\nimpl PrimitiveDescriptor {\n    /// Returns the vertex at the given index and its related values.\n    ///\n    /// These values are often used in shaders, so they are grouped together.\n    pub fn get_vertex_info(&self, vertex_index: u32, geometry_slab: &[u32]) -> VertexInfo {\n        let vertex = self.get_vertex(vertex_index, geometry_slab);\n        let transform = self.get_transform(vertex, geometry_slab);\n        let model_matrix = Mat4::from(transform);\n        let world_pos = model_matrix.transform_point3(vertex.position);\n        VertexInfo {\n            vertex,\n            transform,\n            model_matrix,\n            world_pos,\n        }\n    }\n    /// Retrieve the transform of this `primitive`.\n    ///\n    /// This takes into consideration all skinning matrices.\n    pub fn get_transform(&self, vertex: Vertex, slab: &[u32]) -> TransformDescriptor {\n        let config = slab.read_unchecked(self.geometry_descriptor_id);\n        if config.has_skinning && self.skin_id.is_some() {\n            let skin = slab.read(self.skin_id);\n            TransformDescriptor::from(skin.get_skinning_matrix(vertex, slab))\n        } else {\n            slab.read(self.transform_id)\n        }\n    }\n\n    /// Retrieve the vertex from the slab, calculating any displacement due to\n    /// morph targets.\n    pub fn get_vertex(&self, vertex_index: u32, slab: &[u32]) -> Vertex {\n        let index = if self.indices_array.is_null() {\n            vertex_index as usize\n        } else {\n            slab.read(self.indices_array.at(vertex_index as usize)) as usize\n        };\n        let vertex_id = self.vertices_array.at(index);\n        let mut vertex = slab.read_unchecked(vertex_id);\n        for i in 0..self.morph_targets.len() {\n            let morph_target_array = slab.read(self.morph_targets.at(i));\n            let morph_target = slab.read(morph_target_array.at(index));\n            let weight = slab.read(self.morph_weights.at(i));\n            vertex.position += weight * morph_target.position;\n            vertex.normal += weight * morph_target.normal;\n            vertex.tangent += weight * morph_target.tangent.extend(0.0);\n        }\n        vertex\n    }\n\n    pub fn get_vertex_count(&self) -> u32 {\n        if self.indices_array.is_null() {\n            self.vertices_array.len() as u32\n        } else {\n            self.indices_array.len() as u32\n        }\n    }\n}\n\n#[cfg(test)]\n/// A helper struct that contains all outputs of the primitive's PBR vertex\n/// shader.\n#[derive(Default, Debug, Clone, Copy, PartialEq)]\npub struct PrimitivePbrVertexInfo {\n    pub primitive: PrimitiveDescriptor,\n    pub primitive_id: Id<PrimitiveDescriptor>,\n    pub vertex_index: u32,\n    pub vertex: Vertex,\n    pub transform: TransformDescriptor,\n    pub model_matrix: Mat4,\n    pub view_projection: Mat4,\n    pub out_color: Vec4,\n    pub out_uv0: Vec2,\n    pub out_uv1: Vec2,\n    pub out_norm: Vec3,\n    pub out_tangent: Vec3,\n    pub out_bitangent: Vec3,\n    pub out_pos: Vec3,\n    pub out_clip_pos: Vec4,\n}\n\n/// primitive vertex shader.\n#[spirv(vertex)]\n#[allow(clippy::too_many_arguments)]\npub fn primitive_vertex(\n    // Points at a `primitive`\n    #[spirv(instance_index)] primitive_id: Id<PrimitiveDescriptor>,\n    // Which vertex within the primitive are we rendering\n    #[spirv(vertex_index)] vertex_index: u32,\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32],\n\n    #[spirv(flat)] out_primitive: &mut Id<PrimitiveDescriptor>,\n    // TODO: Think about placing all these out values in a G-Buffer\n    // But do we have enough buffers + enough space on web?\n    // ...and can we write to buffers from vertex shaders on web?\n    out_color: &mut Vec4,\n    out_uv0: &mut Vec2,\n    out_uv1: &mut Vec2,\n    out_norm: &mut Vec3,\n    out_tangent: &mut Vec3,\n    out_bitangent: &mut Vec3,\n    out_world_pos: &mut Vec3,\n    #[spirv(position)] out_clip_pos: &mut Vec4,\n    // test-only info struct\n    #[cfg(test)] out_info: &mut PrimitivePbrVertexInfo,\n) {\n    let primitive = geometry_slab.read_unchecked(primitive_id);\n    if !primitive.visible {\n        // put it outside the clipping frustum\n        *out_clip_pos = Vec4::new(10.0, 10.0, 10.0, 1.0);\n        return;\n    }\n\n    *out_primitive = primitive_id;\n\n    let VertexInfo {\n        vertex,\n        transform,\n        model_matrix,\n        world_pos,\n    } = primitive.get_vertex_info(vertex_index, geometry_slab);\n    *out_color = vertex.color;\n    *out_uv0 = vertex.uv0;\n    *out_uv1 = vertex.uv1;\n    *out_world_pos = world_pos;\n\n    let scale2 = transform.scale * transform.scale;\n    let normal = vertex.normal.alt_norm_or_zero();\n    let tangent = vertex.tangent.xyz().alt_norm_or_zero();\n    let normal_w: Vec3 = (model_matrix * (normal / scale2).extend(0.0))\n        .xyz()\n        .alt_norm_or_zero();\n    *out_norm = normal_w;\n\n    let tangent_w: Vec3 = (model_matrix * tangent.extend(0.0))\n        .xyz()\n        .alt_norm_or_zero();\n    *out_tangent = tangent_w;\n\n    let bitangent_w = normal_w.cross(tangent_w) * if vertex.tangent.w >= 0.0 { 1.0 } else { -1.0 };\n    *out_bitangent = bitangent_w;\n\n    let camera_id = geometry_slab\n        .read_unchecked(primitive.geometry_descriptor_id + GeometryDescriptor::OFFSET_OF_CAMERA_ID);\n    let camera = geometry_slab.read(camera_id);\n    let clip_pos = camera.view_projection() * world_pos.extend(1.0);\n    *out_clip_pos = clip_pos;\n    #[cfg(test)]\n    {\n        *out_info = PrimitivePbrVertexInfo {\n            primitive_id,\n            vertex_index,\n            vertex,\n            transform,\n            model_matrix,\n            view_projection: camera.view_projection(),\n            out_clip_pos: clip_pos,\n            primitive,\n            out_color: *out_color,\n            out_uv0: *out_uv0,\n            out_uv1: *out_uv1,\n            out_norm: *out_norm,\n            out_tangent: *out_tangent,\n            out_bitangent: *out_bitangent,\n            out_pos: *out_world_pos,\n        };\n    }\n}\n\n/// primitive fragment shader\n#[allow(clippy::too_many_arguments, dead_code)]\n#[spirv(fragment)]\npub fn primitive_fragment(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] geometry_slab: &[u32],\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] material_slab: &[u32],\n    #[spirv(descriptor_set = 0, binding = 2)] atlas: &Image2dArray,\n    #[spirv(descriptor_set = 0, binding = 3)] atlas_sampler: &Sampler,\n    #[spirv(descriptor_set = 0, binding = 4)] irradiance: &Cubemap,\n    #[spirv(descriptor_set = 0, binding = 5)] irradiance_sampler: &Sampler,\n    #[spirv(descriptor_set = 0, binding = 6)] prefiltered: &Cubemap,\n    #[spirv(descriptor_set = 0, binding = 7)] prefiltered_sampler: &Sampler,\n    #[spirv(descriptor_set = 0, binding = 8)] brdf: &Image2d,\n    #[spirv(descriptor_set = 0, binding = 9)] brdf_sampler: &Sampler,\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 10)] light_slab: &[u32],\n    #[spirv(descriptor_set = 0, binding = 11)] shadow_map: &Image!(2D, type=f32, sampled, arrayed),\n    #[spirv(descriptor_set = 0, binding = 12)] shadow_map_sampler: &Sampler,\n    #[cfg(feature = \"debug-slab\")]\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 13)]\n    _debug_slab: &mut [u32],\n\n    #[spirv(flat)] primitive_id: Id<PrimitiveDescriptor>,\n    #[spirv(frag_coord)] frag_coord: Vec4,\n    in_color: Vec4,\n    in_uv0: Vec2,\n    in_uv1: Vec2,\n    in_norm: Vec3,\n    in_tangent: Vec3,\n    in_bitangent: Vec3,\n    world_pos: Vec3,\n    output: &mut Vec4,\n) {\n    // proxy to a separate impl that allows us to test on CPU\n    crate::pbr::shader::fragment_impl(\n        atlas,\n        atlas_sampler,\n        irradiance,\n        irradiance_sampler,\n        prefiltered,\n        prefiltered_sampler,\n        brdf,\n        brdf_sampler,\n        shadow_map,\n        shadow_map_sampler,\n        geometry_slab,\n        material_slab,\n        light_slab,\n        primitive_id,\n        frag_coord,\n        in_color,\n        in_uv0,\n        in_uv1,\n        in_norm,\n        in_tangent,\n        in_bitangent,\n        world_pos,\n        output,\n    );\n}\n\n#[cfg(feature = \"test_i8_16_extraction\")]\n#[spirv(compute(threads(32)))]\n/// A shader to ensure that we can extract i8 and i16 values from a storage\n/// buffer.\npub fn test_i8_i16_extraction(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &mut [u32],\n    #[spirv(global_invocation_id)] global_id: UVec3,\n) {\n    let index = global_id.x as usize;\n    let (value, _, _) = crate::bits::extract_i8(index, 2, slab);\n    if value > 0 {\n        slab[index] = value as u32;\n    }\n    let (value, _, _) = crate::bits::extract_i16(index, 2, slab);\n    if value > 0 {\n        slab[index] = value as u32;\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/primitive.rs",
    "content": "//! Mesh primitives\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\npub mod shader;\n"
  },
  {
    "path": "crates/renderling/src/sdf.rs",
    "content": "//! SDF functions for use in shaders.\n//!\n//! For more info, see these great articles:\n//! - <https://iquilezles.org/articles/distfunctions2d/>\nuse crabslab::SlabItem;\nuse glam::Vec2;\n// use spirv_std::spirv;\n\n// #[spirv(vertex)]\n// pub fn vertex_sdf_circle(\n//     #[spirv(instance_index)] circle_id: Id<CircleDescriptor>,\n//     #[spirv(vertex_index)] vertex_index: u32,\n// )\n\n#[derive(Clone, Copy, SlabItem)]\npub struct CircleDescriptor {\n    pub center: Vec2,\n    pub radius: f32,\n}\n\nimpl CircleDescriptor {\n    pub fn distance(&self, point: Vec2) -> f32 {\n        let p = point - self.center;\n        p.length() - self.radius\n    }\n}\n\n#[derive(Clone, Copy, SlabItem)]\npub struct Box {\n    pub center: Vec2,\n    pub half_extent: Vec2,\n}\n\nimpl Box {\n    pub fn distance(&self, point: Vec2) -> f32 {\n        let p = point - self.center;\n        let component_edge_distance = p.abs() - self.half_extent;\n        let outside = component_edge_distance.max(Vec2::ZERO).length();\n        let inside = component_edge_distance\n            .x\n            .max(component_edge_distance.y)\n            .min(0.0);\n        inside + outside\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn sdf_circle_sanity() {\n        let mut img = image::ImageBuffer::<image::Luma<f32>, Vec<f32>>::new(32, 32);\n\n        let circle = CircleDescriptor {\n            center: Vec2::new(12.0, 12.0),\n            radius: 4.0,\n        };\n\n        img.enumerate_pixels_mut().for_each(|(x, y, p)| {\n            let distance = circle.distance(Vec2::new(x as f32 + 0.5, y as f32 + 0.5));\n            p.0[0] = distance / circle.radius;\n        });\n\n        img_diff::assert_img_eq(\n            \"sdf/circle_sanity.png\",\n            image::DynamicImage::from(img).into_rgb8(),\n        );\n    }\n\n    #[test]\n    fn sdf_box_sanity() {\n        let mut img = image::ImageBuffer::<image::Luma<f32>, Vec<f32>>::new(32, 32);\n\n        let bx = Box {\n            center: Vec2::new(12.0, 12.0),\n            half_extent: Vec2::new(4.0, 6.0),\n        };\n\n        img.enumerate_pixels_mut().for_each(|(x, y, p)| {\n            let distance = bx.distance(Vec2::new(x as f32 + 0.5, y as f32 + 0.5));\n            p.0[0] = distance / bx.half_extent.max_element();\n        });\n\n        img_diff::assert_img_eq(\n            \"sdf/box_sanity.png\",\n            image::DynamicImage::from(img).into_rgb8(),\n        );\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/skybox/cpu.rs",
    "content": "//! CPU-side code for skybox rendering.\nuse core::sync::atomic::AtomicBool;\nuse std::sync::Arc;\n\nuse craballoc::{prelude::SlabAllocator, runtime::WgpuRuntime};\nuse glam::{Mat4, UVec2, Vec3};\n\nuse crate::{\n    atlas::AtlasImage,\n    camera::Camera,\n    cubemap::EquirectangularImageToCubemapBlitter,\n    texture::{self, Texture},\n};\n\n/// Render pipeline used to draw a skybox.\npub struct SkyboxRenderPipeline {\n    pub pipeline: wgpu::RenderPipeline,\n    msaa_sample_count: u32,\n}\n\nimpl SkyboxRenderPipeline {\n    pub fn msaa_sample_count(&self) -> u32 {\n        self.msaa_sample_count\n    }\n}\n\nfn skybox_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {\n    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n        label: Some(\"skybox bindgroup\"),\n        entries: &[\n            wgpu::BindGroupLayoutEntry {\n                binding: 0,\n                visibility: wgpu::ShaderStages::VERTEX,\n                ty: wgpu::BindingType::Buffer {\n                    ty: wgpu::BufferBindingType::Storage { read_only: true },\n                    has_dynamic_offset: false,\n                    min_binding_size: None,\n                },\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 1,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Texture {\n                    sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                    view_dimension: wgpu::TextureViewDimension::Cube,\n                    multisampled: false,\n                },\n                count: None,\n            },\n            wgpu::BindGroupLayoutEntry {\n                binding: 2,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                count: None,\n            },\n        ],\n    })\n}\n\npub(crate) fn create_skybox_bindgroup(\n    device: &wgpu::Device,\n    slab_buffer: &wgpu::Buffer,\n    texture: &Texture,\n) -> wgpu::BindGroup {\n    device.create_bind_group(&wgpu::BindGroupDescriptor {\n        label: Some(\"skybox\"),\n        layout: &skybox_bindgroup_layout(device),\n        entries: &[\n            wgpu::BindGroupEntry {\n                binding: 0,\n                resource: slab_buffer.as_entire_binding(),\n            },\n            wgpu::BindGroupEntry {\n                binding: 1,\n                resource: wgpu::BindingResource::TextureView(&texture.view),\n            },\n            wgpu::BindGroupEntry {\n                binding: 2,\n                resource: wgpu::BindingResource::Sampler(&texture.sampler),\n            },\n        ],\n    })\n}\n\n/// Create the skybox rendering pipeline.\npub(crate) fn create_skybox_render_pipeline(\n    device: &wgpu::Device,\n    format: wgpu::TextureFormat,\n    multisample_count: Option<u32>,\n) -> SkyboxRenderPipeline {\n    log::trace!(\"creating skybox render pipeline with format '{format:?}'\");\n    let vertex_linkage = crate::linkage::skybox_vertex::linkage(device);\n    let fragment_linkage = crate::linkage::skybox_cubemap_fragment::linkage(device);\n    let bg_layout = skybox_bindgroup_layout(device);\n    let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n        label: Some(\"skybox pipeline layout\"),\n        bind_group_layouts: &[&bg_layout],\n        push_constant_ranges: &[],\n    });\n    let msaa_sample_count = multisample_count.unwrap_or(1);\n    SkyboxRenderPipeline {\n        pipeline: device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n            label: Some(\"skybox render pipeline\"),\n            layout: Some(&pp_layout),\n            vertex: wgpu::VertexState {\n                module: &vertex_linkage.module,\n                entry_point: Some(vertex_linkage.entry_point),\n                buffers: &[],\n                compilation_options: Default::default(),\n            },\n            primitive: wgpu::PrimitiveState {\n                topology: wgpu::PrimitiveTopology::TriangleList,\n                strip_index_format: None,\n                front_face: wgpu::FrontFace::Ccw,\n                cull_mode: None,\n                unclipped_depth: false,\n                polygon_mode: wgpu::PolygonMode::Fill,\n                conservative: false,\n            },\n            depth_stencil: Some(wgpu::DepthStencilState {\n                format: wgpu::TextureFormat::Depth32Float,\n                depth_write_enabled: true,\n                depth_compare: wgpu::CompareFunction::LessEqual,\n                stencil: wgpu::StencilState::default(),\n                bias: wgpu::DepthBiasState::default(),\n            }),\n            multisample: wgpu::MultisampleState {\n                mask: !0,\n                alpha_to_coverage_enabled: false,\n                count: msaa_sample_count,\n            },\n            fragment: Some(wgpu::FragmentState {\n                module: &fragment_linkage.module,\n                entry_point: Some(fragment_linkage.entry_point),\n                targets: &[Some(wgpu::ColorTargetState {\n                    format,\n                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                    write_mask: wgpu::ColorWrites::ALL,\n                })],\n                compilation_options: Default::default(),\n            }),\n            multiview: None,\n            cache: None,\n        }),\n        msaa_sample_count,\n    }\n}\n\n/// An HDR skybox.\n///\n/// Skyboxes provide an environment cubemap around all your scenery\n/// that acts as a background.\n///\n/// A [`Skybox`] can also be used to create [`Ibl`], which illuminates\n/// your scene using the environment map as a light source.\n///\n/// All clones of a skybox point to the same underlying data.\n#[derive(Debug, Clone)]\npub struct Skybox {\n    is_empty: Arc<AtomicBool>,\n    // Cubemap texture of the environment cubemap\n    environment_cubemap: Texture,\n}\n\nimpl Skybox {\n    /// Create an empty, transparent skybox.\n    pub fn empty(runtime: impl AsRef<WgpuRuntime>) -> Self {\n        let runtime = runtime.as_ref();\n        log::trace!(\"creating empty skybox\");\n        let hdr_img = AtlasImage {\n            pixels: vec![0u8; 4 * 4],\n            size: UVec2::splat(1),\n            format: crate::atlas::AtlasImageFormat::R32G32B32A32FLOAT,\n            apply_linear_transfer: false,\n        };\n        let s = Self::new(runtime, hdr_img);\n        s.is_empty.store(true, std::sync::atomic::Ordering::Relaxed);\n        s\n    }\n\n    /// Create a new `Skybox`.\n    pub fn new(runtime: impl AsRef<WgpuRuntime>, hdr_img: AtlasImage) -> Self {\n        let runtime = runtime.as_ref();\n        log::trace!(\"creating skybox\");\n\n        let slab = SlabAllocator::new(runtime, \"skybox-slab\", wgpu::BufferUsages::VERTEX);\n        let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, 10.0);\n        let camera = Camera::new(&slab).with_projection(proj);\n        let buffer = slab.commit();\n        let mut buffer_upkeep = || {\n            let possibly_new_buffer = slab.commit();\n            debug_assert!(!possibly_new_buffer.is_new_this_commit());\n        };\n\n        let equirectangular_texture = Skybox::hdr_texture_from_atlas_image(runtime, hdr_img);\n        let views = [\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(1.0, 0.0, 0.0),\n                Vec3::new(0.0, -1.0, 0.0),\n            ),\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(-1.0, 0.0, 0.0),\n                Vec3::new(0.0, -1.0, 0.0),\n            ),\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(0.0, -1.0, 0.0),\n                Vec3::new(0.0, 0.0, -1.0),\n            ),\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(0.0, 1.0, 0.0),\n                Vec3::new(0.0, 0.0, 1.0),\n            ),\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(0.0, 0.0, 1.0),\n                Vec3::new(0.0, -1.0, 0.0),\n            ),\n            Mat4::look_at_rh(\n                Vec3::new(0.0, 0.0, 0.0),\n                Vec3::new(0.0, 0.0, -1.0),\n                Vec3::new(0.0, -1.0, 0.0),\n            ),\n        ];\n\n        // Create environment map.\n        let environment_cubemap = Skybox::create_environment_map_from_hdr(\n            runtime,\n            &buffer,\n            &mut buffer_upkeep,\n            &equirectangular_texture,\n            &camera,\n            views,\n        );\n\n        Skybox {\n            is_empty: Arc::new(false.into()),\n            environment_cubemap,\n        }\n    }\n\n    /// Return a reference to the environment cubemap texture.\n    pub fn environment_cubemap_texture(&self) -> &texture::Texture {\n        &self.environment_cubemap\n    }\n\n    /// Convert an HDR [`AtlasImage`] into a texture.\n    pub fn hdr_texture_from_atlas_image(\n        runtime: impl AsRef<WgpuRuntime>,\n        img: AtlasImage,\n    ) -> Texture {\n        let runtime = runtime.as_ref();\n        Texture::new_with(\n            runtime,\n            Some(\"create hdr texture\"),\n            None,\n            Some(runtime.device.create_sampler(&wgpu::SamplerDescriptor {\n                mag_filter: wgpu::FilterMode::Nearest,\n                min_filter: wgpu::FilterMode::Nearest,\n                mipmap_filter: wgpu::FilterMode::Nearest,\n                ..Default::default()\n            })),\n            wgpu::TextureFormat::Rgba32Float,\n            4,\n            4,\n            img.size.x,\n            img.size.y,\n            1,\n            &img.pixels,\n        )\n    }\n\n    /// Create an HDR equirectangular texture from bytes.\n    pub fn create_hdr_texture(runtime: impl AsRef<WgpuRuntime>, hdr_data: &[u8]) -> Texture {\n        let runtime = runtime.as_ref();\n        let img = AtlasImage::from_hdr_bytes(hdr_data).unwrap();\n        Self::hdr_texture_from_atlas_image(runtime, img)\n    }\n\n    fn create_environment_map_from_hdr(\n        runtime: impl AsRef<WgpuRuntime>,\n        buffer: &wgpu::Buffer,\n        buffer_upkeep: impl FnMut(),\n        hdr_texture: &Texture,\n        camera: &Camera,\n        views: [Mat4; 6],\n    ) -> Texture {\n        let runtime = runtime.as_ref();\n        let device = &runtime.device;\n        let queue = &runtime.queue;\n        // Create the cubemap-making pipeline.\n        let pipeline =\n            EquirectangularImageToCubemapBlitter::new(device, wgpu::TextureFormat::Rgba16Float);\n\n        let resources = (\n            device,\n            queue,\n            Some(\"hdr environment map\"),\n            wgpu::BufferUsages::VERTEX,\n        );\n        let bindgroup = EquirectangularImageToCubemapBlitter::create_bindgroup(\n            device,\n            resources.2,\n            buffer,\n            hdr_texture,\n        );\n\n        texture::Texture::render_cubemap(\n            runtime,\n            \"skybox-environment\",\n            &pipeline.0,\n            buffer_upkeep,\n            camera,\n            &bindgroup,\n            views,\n            512,\n            Some(9),\n        )\n    }\n\n    /// Returns whether this skybox is empty.\n    pub fn is_empty(&self) -> bool {\n        self.is_empty.load(std::sync::atomic::Ordering::Relaxed)\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use glam::Vec3;\n\n    use crate::{context::Context, test::BlockOnFuture};\n\n    #[test]\n    fn hdr_skybox_scene() {\n        let ctx = Context::headless(600, 400).block();\n        let proj = crate::camera::perspective(600.0, 400.0);\n        let view = crate::camera::look_at(Vec3::new(0.0, 0.0, 2.0), Vec3::ZERO, Vec3::Y);\n        let stage = ctx.new_stage();\n        let _camera = stage.new_camera().with_projection_and_view(proj, view);\n        let skybox = stage\n            .new_skybox_from_path(\"../../img/hdr/resting_place.hdr\")\n            .unwrap();\n        stage.use_skybox(&skybox);\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_linear_image().block().unwrap();\n        img_diff::assert_img_eq(\"skybox/hdr.png\", img);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/skybox/shader.rs",
    "content": "//! Skybox shaders.\n\nuse crabslab::{Id, Slab};\nuse glam::{Mat3, Mat4, Vec2, Vec3, Vec4, Vec4Swizzles};\nuse spirv_std::{\n    image::{Cubemap, Image2d},\n    spirv, Sampler,\n};\n\n#[allow(unused_imports)]\nuse spirv_std::num_traits::Float;\n\nuse crate::{\n    camera::shader::CameraDescriptor,\n    math::{self, IsVector},\n};\n\nconst INV_ATAN: Vec2 = Vec2::new(0.1591, core::f32::consts::FRAC_1_PI);\n\n/// Takes a unit direction and converts it to a uv lookup in an equirectangular\n/// map.\npub fn direction_to_equirectangular_uv(dir: Vec3) -> Vec2 {\n    let mut uv = Vec2::new(f32::atan2(dir.z, dir.x), f32::asin(dir.y));\n    uv *= INV_ATAN;\n    uv += 0.5;\n    uv\n}\n\n/// Vertex shader for a skybox.\n#[spirv(vertex)]\npub fn skybox_vertex(\n    #[spirv(instance_index)] camera_index: u32,\n    #[spirv(vertex_index)] vertex_index: u32,\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n    local_pos: &mut Vec3,\n    #[spirv(position)] clip_pos: &mut Vec4,\n) {\n    let camera_id = Id::<CameraDescriptor>::from(camera_index);\n    let camera = slab.read(camera_id);\n    let point = math::CUBE[vertex_index as usize];\n    *local_pos = point;\n    let camera_view_without_translation = Mat3::from_mat4(camera.view());\n    let rot_view = Mat4::from_mat3(camera_view_without_translation);\n    let position = camera.projection() * rot_view * point.extend(1.0);\n    *clip_pos = position.xyww();\n}\n\n/// Colors a skybox using a cubemap texture.\n#[spirv(fragment)]\npub fn skybox_cubemap_fragment(\n    #[spirv(descriptor_set = 0, binding = 1)] texture: &Cubemap,\n    #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,\n    local_pos: Vec3,\n    out_color: &mut Vec4,\n) {\n    let env_color: Vec3 = texture.sample(*sampler, local_pos.alt_norm_or_zero()).xyz();\n    *out_color = env_color.extend(1.0);\n}\n\n/// Vertex shader that draws a cubemap.\n///\n/// Uses the `instance_index` as the [`Id`] for a [`CameraDescriptor`].\n///\n/// Used to create a cubemap from an equirectangular image as well as cubemap\n/// convolutions.\n#[spirv(vertex)]\npub fn skybox_cubemap_vertex(\n    #[spirv(instance_index)] camera_id: Id<CameraDescriptor>,\n    #[spirv(vertex_index)] vertex_index: u32,\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n    local_pos: &mut Vec3,\n    #[spirv(position)] gl_pos: &mut Vec4,\n) {\n    let camera = slab.read(camera_id);\n    let pos = crate::math::CUBE[vertex_index as usize];\n    *local_pos = pos;\n    *gl_pos = camera.view_projection() * pos.extend(1.0);\n}\n\n/// Fragment shader that colors a skybox using an equirectangular texture.\n#[spirv(fragment)]\npub fn skybox_equirectangular_fragment(\n    #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,\n    #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,\n    local_pos: Vec3,\n    out_color: &mut Vec4,\n) {\n    let uv = direction_to_equirectangular_uv(local_pos.alt_norm_or_zero());\n    let env_color: Vec3 = texture.sample(*sampler, uv).xyz();\n    *out_color = env_color.extend(1.0);\n}\n"
  },
  {
    "path": "crates/renderling/src/skybox.rs",
    "content": "//! Rendering skylines at infinite distances.\n#[cfg(not(target_arch = \"spirv\"))]\nmod cpu;\n#[cfg(not(target_arch = \"spirv\"))]\npub use cpu::*;\n\npub mod shader;\n"
  },
  {
    "path": "crates/renderling/src/stage/cpu.rs",
    "content": "//! GPU staging area.\nuse core::{\n    ops::Deref,\n    sync::atomic::{AtomicU32, AtomicUsize, Ordering},\n};\nuse craballoc::prelude::*;\nuse crabslab::Id;\nuse glam::{Mat4, UVec2, Vec4};\nuse snafu::{ResultExt, Snafu};\nuse std::sync::{atomic::AtomicBool, Arc, Mutex, RwLock};\n\n#[cfg(gltf)]\nuse crate::gltf::GltfDocument;\nuse crate::{\n    atlas::{AtlasError, AtlasImage, AtlasImageError, AtlasTexture},\n    bindgroup::ManagedBindGroup,\n    bloom::Bloom,\n    camera::{shader::CameraDescriptor, Camera},\n    debug::DebugOverlay,\n    draw::DrawCalls,\n    geometry::{\n        shader::GeometryDescriptor, Geometry, Indices, MorphTarget, MorphTargetWeights,\n        MorphTargets, Skin, SkinJoint, Vertex, Vertices,\n    },\n    light::{\n        AnalyticalLight, DirectionalLight, IsLight, Light, LightTiling, LightTilingConfig,\n        Lighting, LightingBindGroupLayoutEntries, LightingError, PointLight, ShadowMap, SpotLight,\n    },\n    material::{Material, Materials},\n    pbr::{brdf::BrdfLut, debug::DebugChannel, ibl::Ibl},\n    primitive::Primitive,\n    skybox::{Skybox, SkyboxRenderPipeline},\n    texture::{DepthTexture, Texture},\n    tonemapping::Tonemapping,\n    transform::{NestedTransform, Transform},\n};\n\n/// Enumeration of errors that may be the result of [`Stage`] functions.\n#[derive(Debug, Snafu)]\npub enum StageError {\n    #[snafu(display(\"{source}\"))]\n    Atlas { source: AtlasError },\n\n    #[snafu(display(\"{source}\"))]\n    Lighting { source: LightingError },\n\n    #[cfg(gltf)]\n    #[snafu(display(\"{source}\"))]\n    Gltf { source: crate::gltf::StageGltfError },\n}\n\nimpl From<AtlasError> for StageError {\n    fn from(source: AtlasError) -> Self {\n        Self::Atlas { source }\n    }\n}\n\nimpl From<LightingError> for StageError {\n    fn from(source: LightingError) -> Self {\n        Self::Lighting { source }\n    }\n}\n\n#[cfg(gltf)]\nimpl From<crate::gltf::StageGltfError> for StageError {\n    fn from(source: crate::gltf::StageGltfError) -> Self {\n        Self::Gltf { source }\n    }\n}\n\nfn create_msaa_textureview(\n    device: &wgpu::Device,\n    width: u32,\n    height: u32,\n    format: wgpu::TextureFormat,\n    sample_count: u32,\n) -> wgpu::TextureView {\n    device\n        .create_texture(&wgpu::TextureDescriptor {\n            label: Some(\"stage msaa render target\"),\n            size: wgpu::Extent3d {\n                width,\n                height,\n                depth_or_array_layers: 1,\n            },\n            mip_level_count: 1,\n            sample_count,\n            dimension: wgpu::TextureDimension::D2,\n            format,\n            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,\n            view_formats: &[],\n        })\n        .create_view(&wgpu::TextureViewDescriptor::default())\n}\n\n/// Result of calling [`Stage::commit`].\npub struct StageCommitResult {\n    pub(crate) geometry_buffer: SlabBuffer<wgpu::Buffer>,\n    pub(crate) lighting_buffer: SlabBuffer<wgpu::Buffer>,\n    pub(crate) materials_buffer: SlabBuffer<wgpu::Buffer>,\n}\n\nimpl StageCommitResult {\n    /// Timestamp of the most recently created buffer used by the stage.\n    pub(crate) fn latest_creation_time(&self) -> usize {\n        [\n            &self.geometry_buffer,\n            &self.materials_buffer,\n            &self.lighting_buffer,\n        ]\n        .iter()\n        .map(|buffer| buffer.creation_time())\n        .max()\n        .unwrap_or_default()\n    }\n\n    /// Whether or not the stage's bindgroups need to be invalidated as a result\n    /// of the call to [`Stage::commit`] that produced this `StageCommitResult`.\n    pub(crate) fn should_invalidate(&self, previous_creation_time: usize) -> bool {\n        let mut should = false;\n        if self.geometry_buffer.is_new_this_commit() {\n            log::trace!(\"geometry buffer is new this frame\");\n            should = true;\n        }\n        if self.materials_buffer.is_new_this_commit() {\n            log::trace!(\"materials buffer is new this frame\");\n            should = true;\n        }\n        if self.lighting_buffer.is_new_this_commit() {\n            log::trace!(\"lighting buffer is new this frame\");\n            should = true;\n        }\n        let current = self.latest_creation_time();\n        if current > previous_creation_time {\n            log::trace!(\n                \"current latest buffer creation time {current} > previous {previous_creation_time}\"\n            );\n            should = true;\n        }\n        should\n    }\n}\n\n/// Bindgroup used to render primitives.\n///\n/// This is the bindgroup that occupies `descriptor_set = 0` in\n/// [crate::primitive::shader::primitive_vertex] and\n/// [crate::primitive::shader::primitive_fragment].\nstruct PrimitiveBindGroup<'a> {\n    device: &'a wgpu::Device,\n    layout: &'a wgpu::BindGroupLayout,\n    geometry_buffer: &'a wgpu::Buffer,\n    material_buffer: &'a wgpu::Buffer,\n    light_buffer: &'a wgpu::Buffer,\n    atlas_texture_view: &'a wgpu::TextureView,\n    atlas_texture_sampler: &'a wgpu::Sampler,\n    irradiance_texture_view: &'a wgpu::TextureView,\n    irradiance_texture_sampler: &'a wgpu::Sampler,\n    prefiltered_texture_view: &'a wgpu::TextureView,\n    prefiltered_texture_sampler: &'a wgpu::Sampler,\n    brdf_texture_view: &'a wgpu::TextureView,\n    brdf_texture_sampler: &'a wgpu::Sampler,\n    shadow_map_texture_view: &'a wgpu::TextureView,\n    shadow_map_texture_sampler: &'a wgpu::Sampler,\n}\n\nimpl PrimitiveBindGroup<'_> {\n    pub fn create(self) -> wgpu::BindGroup {\n        self.device.create_bind_group(&wgpu::BindGroupDescriptor {\n            label: Some(\"primitive\"),\n            layout: self.layout,\n            entries: &[\n                wgpu::BindGroupEntry {\n                    binding: 0,\n                    resource: self.geometry_buffer.as_entire_binding(),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 1,\n                    resource: self.material_buffer.as_entire_binding(),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 2,\n                    resource: wgpu::BindingResource::TextureView(self.atlas_texture_view),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 3,\n                    resource: wgpu::BindingResource::Sampler(self.atlas_texture_sampler),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 4,\n                    resource: wgpu::BindingResource::TextureView(self.irradiance_texture_view),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 5,\n                    resource: wgpu::BindingResource::Sampler(self.irradiance_texture_sampler),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 6,\n                    resource: wgpu::BindingResource::TextureView(self.prefiltered_texture_view),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 7,\n                    resource: wgpu::BindingResource::Sampler(self.prefiltered_texture_sampler),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 8,\n                    resource: wgpu::BindingResource::TextureView(self.brdf_texture_view),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 9,\n                    resource: wgpu::BindingResource::Sampler(self.brdf_texture_sampler),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 10,\n                    resource: self.light_buffer.as_entire_binding(),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 11,\n                    resource: wgpu::BindingResource::TextureView(self.shadow_map_texture_view),\n                },\n                wgpu::BindGroupEntry {\n                    binding: 12,\n                    resource: wgpu::BindingResource::Sampler(self.shadow_map_texture_sampler),\n                },\n            ],\n        })\n    }\n}\n\n/// Performs a rendering of an entire scene, given the resources at hand.\npub(crate) struct StageRendering<'a> {\n    // TODO: include the rest of the needed paramaters from `stage`, and then remove `stage`\n    pub stage: &'a Stage,\n    pub pipeline: &'a wgpu::RenderPipeline,\n    pub color_attachment: wgpu::RenderPassColorAttachment<'a>,\n    pub depth_stencil_attachment: wgpu::RenderPassDepthStencilAttachment<'a>,\n}\n\nimpl StageRendering<'_> {\n    /// Run the stage rendering.\n    ///\n    /// Returns the queue submission index and the indirect draw buffer, if\n    /// available.\n    pub fn run(self) -> (wgpu::SubmissionIndex, Option<SlabBuffer<wgpu::Buffer>>) {\n        let commit_result = self.stage.commit();\n        let current_primitive_bind_group_creation_time = commit_result.latest_creation_time();\n        log::trace!(\n            \"current_primitive_bind_group_creation_time: \\\n             {current_primitive_bind_group_creation_time}\"\n        );\n        let previous_primitive_bind_group_creation_time =\n            self.stage.primitive_bind_group_created.swap(\n                current_primitive_bind_group_creation_time,\n                std::sync::atomic::Ordering::Relaxed,\n            );\n        let should_invalidate_primitive_bind_group =\n            commit_result.should_invalidate(previous_primitive_bind_group_creation_time);\n        log::trace!(\n            \"should_invalidate_primitive_bind_group: {should_invalidate_primitive_bind_group}\"\n        );\n        let primitive_bind_group =\n            self.stage\n                .primitive_bind_group\n                .get(should_invalidate_primitive_bind_group, || {\n                    log::trace!(\"recreating primitive bind group\");\n                    let atlas_texture = self.stage.materials.atlas().get_texture();\n                    let ibl = self.stage.ibl.read().expect(\"ibl read\");\n                    let shadow_map = self.stage.lighting.shadow_map_atlas.get_texture();\n                    PrimitiveBindGroup {\n                        device: self.stage.device(),\n                        layout: &Stage::primitive_pipeline_bindgroup_layout(self.stage.device()),\n                        geometry_buffer: &commit_result.geometry_buffer,\n                        material_buffer: &commit_result.materials_buffer,\n                        light_buffer: &commit_result.lighting_buffer,\n                        atlas_texture_view: &atlas_texture.view,\n                        atlas_texture_sampler: &atlas_texture.sampler,\n                        irradiance_texture_view: &ibl.irradiance_cubemap.view,\n                        irradiance_texture_sampler: &ibl.irradiance_cubemap.sampler,\n                        prefiltered_texture_view: &ibl.prefiltered_environment_cubemap.view,\n                        prefiltered_texture_sampler: &ibl.prefiltered_environment_cubemap.sampler,\n                        brdf_texture_view: &self.stage.brdf_lut.inner.view,\n                        brdf_texture_sampler: &self.stage.brdf_lut.inner.sampler,\n                        shadow_map_texture_view: &shadow_map.view,\n                        shadow_map_texture_sampler: &shadow_map.sampler,\n                    }\n                    .create()\n                });\n\n        let mut draw_calls = self.stage.draw_calls.write().expect(\"draw_calls write\");\n        let depth_texture = self.stage.depth_texture.read().expect(\"depth_texture read\");\n        // UNWRAP: safe because we know the depth texture format will always match\n        let maybe_indirect_buffer = draw_calls.pre_draw(&depth_texture).unwrap();\n\n        log::trace!(\"rendering\");\n        let label = Some(\"stage render\");\n\n        let mut encoder = self\n            .stage\n            .device()\n            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label });\n        {\n            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                label,\n                color_attachments: &[Some(self.color_attachment)],\n                depth_stencil_attachment: Some(self.depth_stencil_attachment),\n                ..Default::default()\n            });\n\n            render_pass.set_pipeline(self.pipeline);\n            render_pass.set_bind_group(0, Some(primitive_bind_group.as_ref()), &[]);\n            draw_calls.draw(&mut render_pass);\n\n            let has_skybox = self.stage.has_skybox.load(Ordering::Relaxed);\n            if has_skybox {\n                let (pipeline, bindgroup) = self\n                    .stage\n                    .get_skybox_pipeline_and_bindgroup(&commit_result.geometry_buffer);\n                render_pass.set_pipeline(&pipeline.pipeline);\n                render_pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]);\n                let camera_id = self.stage.geometry.descriptor().get().camera_id.inner();\n                render_pass.draw(0..36, camera_id..camera_id + 1);\n            }\n        }\n        let sindex = self.stage.queue().submit(std::iter::once(encoder.finish()));\n        (sindex, maybe_indirect_buffer)\n    }\n}\n\n/// Entrypoint for staging data on the GPU and interacting with lighting.\n///\n/// # Design\n///\n/// The `Stage` struct serves as the central hub for managing and staging data\n/// on the GPU. It provides a consistent API for creating resources, applying\n/// effects, and customizing parameters.\n///\n/// The `Stage` uses a combination of `new_*`, `with_*`, `set_*`, and getter\n/// functions to facilitate resource management and customization.\n///\n/// Resources are managed internally, requiring no additional lifecycle work\n/// from the user. This design simplifies the process of resource management,\n/// allowing developers to focus on creating and rendering their scenes without\n/// worrying about the underlying GPU resource management.\n///\n/// # Resources\n///\n/// The `Stage` is responsible for creating various resources and staging them\n/// on the GPU. It handles the setup and management of the following resources:\n///\n/// * [`Camera`]: Manages the view and projection matrices for rendering scenes.\n///   - [`Stage::new_camera`] creates a new [`Camera`].\n///   - [`Stage::use_camera`] tells the `Stage` to use a camera.\n/// * [`Transform`]: Represents the position, rotation, and scale of objects.\n///   - [`Stage::new_transform`] creates a new [`Transform`].\n/// * [`NestedTransform`]: Allows for hierarchical transformations, useful for\n///   complex object hierarchies.\n///   - [`Stage::new_nested_transform`] creates a new [`NestedTransform`]\n/// * [`Vertices`]: Manages vertex data for rendering meshes.\n///   - [`Stage::new_vertices`]\n/// * [`Indices`]: Manages index data for rendering meshes with indexed drawing.\n///   - [`Stage::new_indices`]\n/// * [`Primitive`]: Represents a drawable object in the scene.\n///   - [`Stage::new_primitive`]\n/// * [`GltfDocument`]: Handles loading and managing GLTF assets.\n///   - [`Stage::load_gltf_document_from_path`] loads a new GLTF document from\n///     the local filesystem.\n///   - [`Stage::load_gltf_document_from_bytes`] parses a new GLTF document from\n///     pre-loaded bytes.\n/// * [`Skin`]: Animation and rigging information.\n///   - [`Stage::new_skin`]\n///\n/// # Lighting effects\n///\n/// The `Stage` also manages various lighting effects, which enhance the visual\n/// quality of the scene:\n///\n/// * [`AnalyticalLight`]: Simulates a single light source, with three flavors:\n///   - [`DirectionalLight`]: Represents sunlight or other distant light\n///     sources.\n///   - [`PointLight`]: Represents a light source that emits light in all\n///     directions from a single point.\n///   - [`SpotLight`]: Represents a light source that emits light in a cone\n///     shape.\n/// * [`Skybox`]: Provides image-based lighting (IBL) for realistic\n///   environmental reflections and ambient lighting.\n/// * [`Bloom`]: Adds a glow effect to bright areas of the scene, enhancing\n///   visual appeal.\n/// * [`ShadowMap`]: Manages shadow mapping for realistic shadow rendering.\n/// * [`LightTiling`]: Optimizes lighting calculations by dividing the scene\n///   into tiles for efficient processing.\n///\n/// # Note\n///\n/// Clones of [`Stage`] all point to the same underlying data.\n#[derive(Clone)]\npub struct Stage {\n    pub(crate) geometry: Geometry,\n    pub(crate) materials: Materials,\n    pub(crate) lighting: Lighting,\n\n    pub(crate) primitive_pipeline: Arc<RwLock<wgpu::RenderPipeline>>,\n    pub(crate) primitive_bind_group: ManagedBindGroup,\n    pub(crate) primitive_bind_group_created: Arc<AtomicUsize>,\n\n    pub(crate) skybox_pipeline: Arc<RwLock<Option<Arc<SkyboxRenderPipeline>>>>,\n\n    pub(crate) hdr_texture: Arc<RwLock<Texture>>,\n    pub(crate) depth_texture: Arc<RwLock<Texture>>,\n    pub(crate) msaa_render_target: Arc<RwLock<Option<wgpu::TextureView>>>,\n    pub(crate) msaa_sample_count: Arc<AtomicU32>,\n    pub(crate) clear_color_attachments: Arc<AtomicBool>,\n    pub(crate) clear_depth_attachments: Arc<AtomicBool>,\n\n    pub(crate) bloom: Bloom,\n\n    pub(crate) tonemapping: Tonemapping,\n    pub(crate) debug_overlay: DebugOverlay,\n    pub(crate) background_color: Arc<RwLock<wgpu::Color>>,\n\n    pub(crate) brdf_lut: BrdfLut,\n\n    pub(crate) ibl: Arc<RwLock<Ibl>>,\n\n    pub(crate) skybox: Arc<RwLock<Skybox>>,\n    pub(crate) skybox_bindgroup: Arc<Mutex<Option<Arc<wgpu::BindGroup>>>>,\n    // TODO: remove Stage.has_skybox, replace with Skybox::is_empty\n    pub(crate) has_skybox: Arc<AtomicBool>,\n\n    pub(crate) has_bloom: Arc<AtomicBool>,\n    pub(crate) has_debug_overlay: Arc<AtomicBool>,\n\n    pub(crate) stage_slab_buffer: Arc<RwLock<SlabBuffer<wgpu::Buffer>>>,\n\n    pub(crate) textures_bindgroup: Arc<Mutex<Option<Arc<wgpu::BindGroup>>>>,\n\n    pub(crate) draw_calls: Arc<RwLock<DrawCalls>>,\n}\n\nimpl AsRef<WgpuRuntime> for Stage {\n    fn as_ref(&self) -> &WgpuRuntime {\n        self.geometry.as_ref()\n    }\n}\n\nimpl AsRef<Geometry> for Stage {\n    fn as_ref(&self) -> &Geometry {\n        &self.geometry\n    }\n}\n\nimpl AsRef<Materials> for Stage {\n    fn as_ref(&self) -> &Materials {\n        &self.materials\n    }\n}\n\nimpl AsRef<Lighting> for Stage {\n    fn as_ref(&self) -> &Lighting {\n        &self.lighting\n    }\n}\n\n#[cfg(gltf)]\n/// GLTF functions\nimpl Stage {\n    pub fn load_gltf_document_from_path(\n        &self,\n        path: impl AsRef<std::path::Path>,\n    ) -> Result<GltfDocument, StageError> {\n        use snafu::ResultExt;\n\n        let (document, buffers, images) =\n            gltf::import(&path).with_context(|_| crate::gltf::GltfSnafu {\n                path: Some(path.as_ref().to_path_buf()),\n            })?;\n        GltfDocument::from_gltf(self, &document, buffers, images)\n    }\n\n    pub fn load_gltf_document_from_bytes(\n        &self,\n        bytes: impl AsRef<[u8]>,\n    ) -> Result<GltfDocument, StageError> {\n        let (document, buffers, images) =\n            gltf::import_slice(bytes).context(crate::gltf::GltfSnafu { path: None })?;\n        GltfDocument::from_gltf(self, &document, buffers, images)\n    }\n}\n\n/// Geometry functions\nimpl Stage {\n    /// Returns the vertices of a white unit cube.\n    ///\n    /// This is the mesh of every [`Primitive`] that has not had its vertices\n    /// set.\n    pub fn default_vertices(&self) -> &Vertices {\n        self.geometry.default_vertices()\n    }\n\n    /// Stage a new [`Camera`] on the GPU.\n    ///\n    /// If no camera is currently in use on the [`Stage`] through\n    /// [`Stage::use_camera`], this new camera will be used automatically.\n    pub fn new_camera(&self) -> Camera {\n        self.geometry.new_camera()\n    }\n\n    /// Use the given camera when rendering.\n    pub fn use_camera(&self, camera: impl AsRef<Camera>) {\n        self.geometry.use_camera(camera.as_ref());\n    }\n\n    /// Return the `Id` of the camera currently in use.\n    pub fn used_camera_id(&self) -> Id<CameraDescriptor> {\n        self.geometry.descriptor().get().camera_id\n    }\n\n    /// Set the default camera `Id`.\n    pub fn use_camera_id(&self, camera_id: Id<CameraDescriptor>) {\n        self.geometry\n            .descriptor()\n            .modify(|desc| desc.camera_id = camera_id);\n    }\n\n    /// Stage a [`Transform`] on the GPU.\n    pub fn new_transform(&self) -> Transform {\n        self.geometry.new_transform()\n    }\n\n    /// Stage some vertex geometry data.\n    pub fn new_vertices(&self, data: impl IntoIterator<Item = Vertex>) -> Vertices {\n        self.geometry.new_vertices(data)\n    }\n\n    /// Stage some vertex index data.\n    pub fn new_indices(&self, data: impl IntoIterator<Item = u32>) -> Indices {\n        self.geometry.new_indices(data)\n    }\n\n    /// Stage new morph targets.\n    pub fn new_morph_targets(\n        &self,\n        data: impl IntoIterator<Item = Vec<MorphTarget>>,\n    ) -> MorphTargets {\n        self.geometry.new_morph_targets(data)\n    }\n\n    /// Stage new morph target weights.\n    pub fn new_morph_target_weights(\n        &self,\n        data: impl IntoIterator<Item = f32>,\n    ) -> MorphTargetWeights {\n        self.geometry.new_morph_target_weights(data)\n    }\n\n    /// Stage a new skin.\n    pub fn new_skin(\n        &self,\n        joints: impl IntoIterator<Item = impl Into<SkinJoint>>,\n        inverse_bind_matrices: impl IntoIterator<Item = impl Into<Mat4>>,\n    ) -> Skin {\n        self.geometry.new_skin(joints, inverse_bind_matrices)\n    }\n\n    /// Stage a new [`Primitive`] on the GPU.\n    ///\n    /// The returned [`Primitive`] will automatically be added to this\n    /// [`Stage`].\n    ///\n    /// The returned [`Primitive`] will have the stage's default [`Vertices`],\n    /// which is an all-white unit cube.\n    ///\n    /// The returned [`Primitive`] uses the stage's default [`Material`], which\n    /// is white and **does not** participate in lighting. To change this,\n    /// first create a [`Material`] with [`Stage::new_material`] and then\n    /// call [`Primitive::set_material`] with the new material.\n    pub fn new_primitive(&self) -> Primitive {\n        Primitive::new(self)\n    }\n\n    /// Returns a reference to the descriptor stored at the root of the\n    /// geometry slab.\n    pub fn geometry_descriptor(&self) -> &Hybrid<GeometryDescriptor> {\n        self.geometry.descriptor()\n    }\n}\n\n/// Materials methods.\nimpl Stage {\n    /// Returns the default [`Material`].\n    ///\n    /// The default is an all-white matte material.\n    pub fn default_material(&self) -> &Material {\n        self.materials.default_material()\n    }\n\n    /// Stage a new [`Material`] on the GPU.\n    ///\n    /// The returned [`Material`] can be customized using the builder pattern.\n    pub fn new_material(&self) -> Material {\n        self.materials.new_material()\n    }\n\n    /// Set the size of the atlas.\n    ///\n    /// This will cause a repacking.\n    pub fn set_atlas_size(&self, size: wgpu::Extent3d) -> Result<(), StageError> {\n        log::info!(\"resizing atlas to {size:?}\");\n        self.materials.atlas().resize(self.runtime(), size)?;\n        Ok(())\n    }\n\n    /// Add images to the set of atlas images.\n    ///\n    /// This returns a vector of [`Hybrid<AtlasTexture>`], which\n    /// is a descriptor of each image on the GPU. Dropping these entries\n    /// will invalidate those images and cause the atlas to be repacked, and any\n    /// raw GPU references to the underlying [`AtlasTexture`] will also be\n    /// invalidated.     \n    /// Adding an image can be quite expensive, as it requires creating a new\n    /// texture array for the atlas and repacking all previous images. For\n    /// that reason it is good to batch images to reduce the number of\n    /// calls.\n    pub fn add_images(\n        &self,\n        images: impl IntoIterator<Item = impl Into<AtlasImage>>,\n    ) -> Result<Vec<AtlasTexture>, StageError> {\n        let images = images.into_iter().map(|i| i.into()).collect::<Vec<_>>();\n        let frames = self.materials.atlas().add_images(&images)?;\n\n        // The textures bindgroup will have to be remade\n        let _ = self\n            .textures_bindgroup\n            .lock()\n            .expect(\"textures_bindgroup lock\")\n            .take();\n\n        Ok(frames)\n    }\n\n    /// Clear all images from the atlas.\n    ///\n    /// ## WARNING\n    /// This invalidates any previously staged [`AtlasTexture`]s.\n    pub fn clear_images(&self) -> Result<(), StageError> {\n        let none = Option::<AtlasImage>::None;\n        let _ = self.set_images(none)?;\n        Ok(())\n    }\n\n    /// Set the images to use for the atlas.\n    ///\n    /// Resets the atlas, packing it with the given images and returning a\n    /// vector of the frames already staged.\n    ///\n    /// ## WARNING\n    /// This invalidates any previously staged [`AtlasTexture`]s.\n    pub fn set_images(\n        &self,\n        images: impl IntoIterator<Item = impl Into<AtlasImage>>,\n    ) -> Result<Vec<AtlasTexture>, StageError> {\n        let images = images.into_iter().map(|i| i.into()).collect::<Vec<_>>();\n        let frames = self.materials.atlas().set_images(&images)?;\n\n        // The textures bindgroup will have to be remade\n        let _ = self\n            .textures_bindgroup\n            .lock()\n            .expect(\"textures_bindgroup lock\")\n            .take();\n\n        Ok(frames)\n    }\n}\n\n/// Lighting methods.\nimpl Stage {\n    /// Stage a new directional light.\n    pub fn new_directional_light(&self) -> AnalyticalLight<DirectionalLight> {\n        self.lighting.new_directional_light()\n    }\n\n    /// Stage a new point light.\n    pub fn new_point_light(&self) -> AnalyticalLight<PointLight> {\n        self.lighting.new_point_light()\n    }\n\n    /// Stage a new spot light.\n    pub fn new_spot_light(&self) -> AnalyticalLight<SpotLight> {\n        self.lighting.new_spot_light()\n    }\n\n    /// Add an [`AnalyticalLight`] to the internal list of lights.\n    ///\n    /// This is called implicitly by `Stage::new_*_light`.\n    ///\n    /// This can be used to add the light back to the scene after using\n    /// [`Stage::remove_light`].\n    pub fn add_light<T>(&self, bundle: &AnalyticalLight<T>)\n    where\n        T: IsLight,\n        Light: From<T>,\n    {\n        self.lighting.add_light(bundle)\n    }\n\n    /// Remove an [`AnalyticalLight`] from the internal list of lights.\n    ///\n    /// Use this to exclude a light from rendering, without dropping the light.\n    ///\n    /// After calling this function you can include the light again using\n    /// [`Stage::add_light`].\n    pub fn remove_light<T: IsLight>(&self, bundle: &AnalyticalLight<T>) {\n        self.lighting.remove_light(bundle);\n    }\n\n    /// Set the global ambient light color and intensity.\n    ///\n    /// XYZ components are the RGB color, W is the intensity.\n    /// The ambient term is added to the final shaded color, modulated by\n    /// the surface albedo and ambient occlusion.\n    ///\n    /// Defaults to `Vec4::ZERO` (no ambient contribution).\n    pub fn set_ambient_color(&self, color: Vec4) -> &Self {\n        self.lighting.set_ambient_color(color);\n        self\n    }\n\n    /// Set the global ambient light color and intensity (builder pattern).\n    pub fn with_ambient_color(self, color: Vec4) -> Self {\n        self.set_ambient_color(color);\n        self\n    }\n\n    /// Get the current global ambient light color and intensity.\n    pub fn ambient_color(&self) -> Vec4 {\n        self.lighting.ambient_color()\n    }\n\n    /// Enable shadow mapping for the given [`AnalyticalLight`], creating\n    /// a new [`ShadowMap`].\n    ///\n    /// ## Tips for making a good shadow map\n    ///\n    /// 1. Make sure the map is big enough. Using a big map can fix some peter\n    ///    panning issues, even before playing with bias in the returned\n    ///    [`ShadowMap`]. The bigger the map, the cleaner the shadows will be.\n    ///    This can also solve PCF problems.\n    /// 2. Don't set PCF samples too high in the returned [`ShadowMap`], as this\n    ///    can _cause_ peter panning.\n    /// 3. Ensure the **znear** and **zfar** parameters make sense, as the\n    ///    shadow map uses these to determine how much of the scene to cover. If\n    ///    you find that shadows are cut off in a straight line, it's likely\n    ///    `znear` or `zfar` needs adjustment.\n    pub fn new_shadow_map<T>(\n        &self,\n        analytical_light: &AnalyticalLight<T>,\n        // Size of the shadow map\n        size: UVec2,\n        // Distance to the near plane of the shadow map's frustum.\n        //\n        // Only objects within the shadow map's frustum will cast shadows.\n        z_near: f32,\n        // Distance to the far plane of the shadow map's frustum\n        //\n        // Only objects within the shadow map's frustum will cast shadows.\n        z_far: f32,\n    ) -> Result<ShadowMap, StageError>\n    where\n        T: IsLight,\n        Light: From<T>,\n    {\n        Ok(self\n            .lighting\n            .new_shadow_map(analytical_light, size, z_near, z_far)?)\n    }\n\n    /// Enable light tiling, creating a new [`LightTiling`].\n    pub fn new_light_tiling(&self, config: LightTilingConfig) -> LightTiling {\n        let lighting = self.as_ref();\n        let multisampled = self.get_msaa_sample_count() > 1;\n        let depth_texture_size = self.get_depth_texture().size();\n        LightTiling::new(\n            lighting,\n            multisampled,\n            UVec2::new(depth_texture_size.width, depth_texture_size.height),\n            config,\n        )\n    }\n}\n\n/// Skybox methods\nimpl Stage {\n    /// Return the cached skybox render pipeline, creating it if necessary.\n    fn get_skybox_pipeline_and_bindgroup(\n        &self,\n        geometry_slab_buffer: &wgpu::Buffer,\n    ) -> (Arc<SkyboxRenderPipeline>, Arc<wgpu::BindGroup>) {\n        let msaa_sample_count = self.msaa_sample_count.load(Ordering::Relaxed);\n        // UNWRAP: safe because we're only ever called from the render thread.\n        let mut pipeline_guard = self.skybox_pipeline.write().expect(\"skybox_pipeline write\");\n        let pipeline = if let Some(pipeline) = pipeline_guard.as_mut() {\n            if pipeline.msaa_sample_count() != msaa_sample_count {\n                *pipeline = Arc::new(crate::skybox::create_skybox_render_pipeline(\n                    self.device(),\n                    Texture::HDR_TEXTURE_FORMAT,\n                    Some(msaa_sample_count),\n                ));\n            }\n            pipeline.clone()\n        } else {\n            let pipeline = Arc::new(crate::skybox::create_skybox_render_pipeline(\n                self.device(),\n                Texture::HDR_TEXTURE_FORMAT,\n                Some(msaa_sample_count),\n            ));\n            *pipeline_guard = Some(pipeline.clone());\n            pipeline\n        };\n        // UNWRAP: safe because we're only ever called from the render thread.\n        let mut bindgroup = self.skybox_bindgroup.lock().expect(\"skybox_bindgroup lock\");\n        let bindgroup = if let Some(bindgroup) = bindgroup.as_ref() {\n            bindgroup.clone()\n        } else {\n            let bg = Arc::new(crate::skybox::create_skybox_bindgroup(\n                self.device(),\n                geometry_slab_buffer,\n                self.skybox\n                    .read()\n                    .expect(\"skybox read\")\n                    .environment_cubemap_texture(),\n            ));\n            *bindgroup = Some(bg.clone());\n            bg\n        };\n        (pipeline, bindgroup)\n    }\n\n    /// Used the given [`Skybox`].\n    ///\n    /// To remove the currently used [`Skybox`], call [`Skybox::remove_skybox`].\n    pub fn use_skybox(&self, skybox: &Skybox) -> &Self {\n        // UNWRAP: if we can't acquire the lock we want to panic.\n        let mut guard = self.skybox.write().expect(\"skybox write\");\n        *guard = skybox.clone();\n        self.has_skybox\n            .store(true, std::sync::atomic::Ordering::Relaxed);\n        *self.skybox_bindgroup.lock().expect(\"skybox_bindgroup lock\") = None;\n        *self\n            .textures_bindgroup\n            .lock()\n            .expect(\"textures_bindgroup lock\") = None;\n        self\n    }\n\n    /// Removes the currently used [`Skybox`].\n    ///\n    /// Returns the currently used [`Skybox`], if any.\n    ///\n    /// After calling this the [`Stage`] will not render with any [`Skybox`],\n    /// until [`Skybox::use_skybox`] is called with another [`Skybox`].\n    pub fn remove_skybox(&self) -> Option<Skybox> {\n        let mut guard = self.skybox.write().expect(\"skybox write\");\n        if guard.is_empty() {\n            // Do nothing, the skybox is already empty\n            None\n        } else {\n            let skybox = guard.clone();\n            *guard = Skybox::empty(self.runtime());\n            self.skybox_bindgroup\n                .lock()\n                .expect(\"skybox_bindgroup lock\")\n                .take();\n            self.textures_bindgroup\n                .lock()\n                .expect(\"textures_bindgroup lock\")\n                .take();\n            Some(skybox)\n        }\n    }\n\n    /// Returns a new [`Skybox`] using the HDR image at the given path, if\n    /// possible.\n    ///\n    /// The returned [`Skybox`] must be **used** with [`Stage::use_skybox`].\n    pub fn new_skybox_from_path(\n        &self,\n        path: impl AsRef<std::path::Path>,\n    ) -> Result<Skybox, AtlasImageError> {\n        let hdr = AtlasImage::from_hdr_path(path)?;\n        Ok(Skybox::new(self.runtime(), hdr))\n    }\n\n    /// Returns a new [`Skybox`] using the bytes of an HDR image, if possible.\n    ///\n    /// The returned [`Skybox`] must be **used** with [`Stage::use_skybox`].\n    pub fn new_skybox_from_bytes(&self, bytes: &[u8]) -> Result<Skybox, AtlasImageError> {\n        let hdr = AtlasImage::from_hdr_bytes(bytes)?;\n        Ok(Skybox::new(self.runtime(), hdr))\n    }\n}\n\n/// Image based lighting methods\nimpl Stage {\n    /// Crate a new [`Ibl`] from the given [`Skybox`].\n    pub fn new_ibl(&self, skybox: &Skybox) -> Ibl {\n        Ibl::new(self.runtime(), skybox)\n    }\n\n    /// Use the given image based lighting.\n    ///\n    /// Use [`Stage::new_ibl`] to create a new [`Ibl`].\n    pub fn use_ibl(&self, ibl: &Ibl) -> &Self {\n        let mut guard = self.ibl.write().expect(\"ibl write\");\n        *guard = ibl.clone();\n        self.primitive_bind_group.invalidate();\n        self\n    }\n\n    /// Remove the current image based lighting from the stage and return it, if\n    /// any.\n    pub fn remove_ibl(&self) -> Option<Ibl> {\n        let mut guard = self.ibl.write().expect(\"ibl write\");\n        if guard.is_empty() {\n            // Do nothing, we're already not using IBL\n            None\n        } else {\n            let ibl = guard.clone();\n            *guard = Ibl::new(self.runtime(), &Skybox::empty(self.runtime()));\n            self.primitive_bind_group.invalidate();\n            Some(ibl)\n        }\n    }\n}\n\nimpl Stage {\n    /// Returns the runtime.\n    pub fn runtime(&self) -> &WgpuRuntime {\n        self.as_ref()\n    }\n\n    pub fn device(&self) -> &wgpu::Device {\n        &self.runtime().device\n    }\n\n    pub fn queue(&self) -> &wgpu::Queue {\n        &self.runtime().queue\n    }\n\n    /// Returns a reference to the [`BrdfLut`].\n    ///\n    /// This is used for creating skyboxes used in image based lighting.\n    pub fn brdf_lut(&self) -> &BrdfLut {\n        &self.brdf_lut\n    }\n\n    /// Sum the byte size of all used GPU memory.\n    ///\n    /// Adds together the byte size of all underlying slab buffers.\n    ///\n    /// ## Note\n    /// This does not take into consideration staged data that has not yet\n    /// been committed with either [`Stage::commit`] or [`Stage::render`].\n    pub fn used_gpu_buffer_byte_size(&self) -> usize {\n        let num_u32s = self.geometry.slab_allocator().len()\n            + self.lighting.slab_allocator().len()\n            + self.materials.slab_allocator().len()\n            + self.bloom.slab_allocator().len()\n            + self.tonemapping.slab_allocator().len()\n            + self\n                .draw_calls\n                .read()\n                .unwrap()\n                .drawing_strategy()\n                .as_indirect()\n                .map(|draws| draws.slab_allocator().len())\n                .unwrap_or_default();\n        4 * num_u32s\n    }\n\n    pub fn hdr_texture(&self) -> impl Deref<Target = crate::texture::Texture> + '_ {\n        self.hdr_texture.read().expect(\"hdr_texture read\")\n    }\n\n    /// Run all upkeep and commit all staged changes to the GPU.\n    ///\n    /// This is done implicitly in [`Stage::render`].\n    ///\n    /// This can be used after dropping resources to reclaim those resources on\n    /// the GPU.\n    #[must_use]\n    pub fn commit(&self) -> StageCommitResult {\n        let (materials_atlas_texture_was_recreated, materials_buffer) = self.materials.commit();\n        if materials_atlas_texture_was_recreated {\n            self.primitive_bind_group.invalidate();\n        }\n        let geometry_buffer = self.geometry.commit();\n        let lighting_buffer = self.lighting.commit();\n        StageCommitResult {\n            geometry_buffer,\n            lighting_buffer,\n            materials_buffer,\n        }\n    }\n\n    fn primitive_pipeline_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {\n        let geometry_slab = wgpu::BindGroupLayoutEntry {\n            binding: 0,\n            visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,\n            ty: wgpu::BindingType::Buffer {\n                ty: wgpu::BufferBindingType::Storage { read_only: true },\n                has_dynamic_offset: false,\n                min_binding_size: None,\n            },\n            count: None,\n        };\n        let material_slab = wgpu::BindGroupLayoutEntry {\n            binding: 1,\n            visibility: wgpu::ShaderStages::FRAGMENT,\n            ty: wgpu::BindingType::Buffer {\n                ty: wgpu::BufferBindingType::Storage { read_only: true },\n                has_dynamic_offset: false,\n                min_binding_size: None,\n            },\n            count: None,\n        };\n\n        fn image2d_entry(binding: u32) -> (wgpu::BindGroupLayoutEntry, wgpu::BindGroupLayoutEntry) {\n            let img = wgpu::BindGroupLayoutEntry {\n                binding,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Texture {\n                    sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                    view_dimension: wgpu::TextureViewDimension::D2,\n                    multisampled: false,\n                },\n                count: None,\n            };\n            let sampler = wgpu::BindGroupLayoutEntry {\n                binding: binding + 1,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                count: None,\n            };\n            (img, sampler)\n        }\n\n        fn cubemap_entry(binding: u32) -> (wgpu::BindGroupLayoutEntry, wgpu::BindGroupLayoutEntry) {\n            let img = wgpu::BindGroupLayoutEntry {\n                binding,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Texture {\n                    sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                    view_dimension: wgpu::TextureViewDimension::Cube,\n                    multisampled: false,\n                },\n                count: None,\n            };\n            let sampler = wgpu::BindGroupLayoutEntry {\n                binding: binding + 1,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                count: None,\n            };\n            (img, sampler)\n        }\n\n        let atlas = wgpu::BindGroupLayoutEntry {\n            binding: 2,\n            visibility: wgpu::ShaderStages::FRAGMENT,\n            ty: wgpu::BindingType::Texture {\n                sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                view_dimension: wgpu::TextureViewDimension::D2Array,\n                multisampled: false,\n            },\n            count: None,\n        };\n        let atlas_sampler = wgpu::BindGroupLayoutEntry {\n            binding: 3,\n            visibility: wgpu::ShaderStages::FRAGMENT,\n            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n            count: None,\n        };\n        let (irradiance, irradiance_sampler) = cubemap_entry(4);\n        let (prefilter, prefilter_sampler) = cubemap_entry(6);\n        let (brdf, brdf_sampler) = image2d_entry(8);\n\n        let LightingBindGroupLayoutEntries {\n            light_slab,\n            shadow_map_image,\n            shadow_map_sampler,\n        } = LightingBindGroupLayoutEntries::new(10);\n\n        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: Some(\"primitive\"),\n            entries: &[\n                geometry_slab,\n                material_slab,\n                atlas,\n                atlas_sampler,\n                irradiance,\n                irradiance_sampler,\n                prefilter,\n                prefilter_sampler,\n                brdf,\n                brdf_sampler,\n                light_slab,\n                shadow_map_image,\n                shadow_map_sampler,\n            ],\n        })\n    }\n\n    pub fn create_primitive_pipeline(\n        device: &wgpu::Device,\n        fragment_color_format: wgpu::TextureFormat,\n        multisample_count: u32,\n    ) -> wgpu::RenderPipeline {\n        log::trace!(\"creating stage render pipeline\");\n        let label = Some(\"primitive\");\n        let vertex_linkage = crate::linkage::primitive_vertex::linkage(device);\n        let fragment_linkage = crate::linkage::primitive_fragment::linkage(device);\n\n        let bind_group_layout = Self::primitive_pipeline_bindgroup_layout(device);\n        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label,\n            bind_group_layouts: &[&bind_group_layout],\n            push_constant_ranges: &[],\n        });\n\n        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n            label,\n            layout: Some(&layout),\n            vertex: wgpu::VertexState {\n                module: &vertex_linkage.module,\n                entry_point: Some(vertex_linkage.entry_point),\n                buffers: &[],\n                compilation_options: Default::default(),\n            },\n            primitive: wgpu::PrimitiveState {\n                topology: wgpu::PrimitiveTopology::TriangleList,\n                strip_index_format: None,\n                front_face: wgpu::FrontFace::Ccw,\n                cull_mode: None,\n                unclipped_depth: false,\n                polygon_mode: wgpu::PolygonMode::Fill,\n                conservative: false,\n            },\n            depth_stencil: Some(wgpu::DepthStencilState {\n                format: wgpu::TextureFormat::Depth32Float,\n                depth_write_enabled: true,\n                depth_compare: wgpu::CompareFunction::Less,\n                stencil: wgpu::StencilState::default(),\n                bias: wgpu::DepthBiasState::default(),\n            }),\n            multisample: wgpu::MultisampleState {\n                mask: !0,\n                alpha_to_coverage_enabled: false,\n                count: multisample_count,\n            },\n            fragment: Some(wgpu::FragmentState {\n                module: &fragment_linkage.module,\n                entry_point: Some(fragment_linkage.entry_point),\n                targets: &[Some(wgpu::ColorTargetState {\n                    format: fragment_color_format,\n                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                    write_mask: wgpu::ColorWrites::ALL,\n                })],\n                compilation_options: Default::default(),\n            }),\n            multiview: None,\n            cache: None,\n        })\n    }\n\n    /// Create a new stage.\n    pub fn new(ctx: &crate::context::Context) -> Self {\n        let runtime = ctx.runtime();\n        let device = &runtime.device;\n        let resolution @ UVec2 { x: w, y: h } = ctx.get_size();\n        let stage_config = *ctx.stage_config.read().expect(\"stage_config read\");\n        let geometry = Geometry::new(\n            ctx,\n            resolution,\n            UVec2::new(\n                stage_config.atlas_size.width,\n                stage_config.atlas_size.height,\n            ),\n        );\n        let materials = Materials::new(runtime, stage_config.atlas_size);\n        let multisample_count = 1;\n        let hdr_texture = Arc::new(RwLock::new(Texture::create_hdr_texture(\n            device,\n            w,\n            h,\n            multisample_count,\n        )));\n        let depth_texture =\n            Texture::create_depth_texture(device, w, h, multisample_count, Some(\"stage-depth\"));\n        let msaa_render_target = Default::default();\n        // UNWRAP: safe because no other references at this point (created above^)\n        let bloom = Bloom::new(ctx, &hdr_texture.read().expect(\"hdr_texture read\"));\n        let tonemapping = Tonemapping::new(\n            runtime,\n            ctx.get_render_target().format().add_srgb_suffix(),\n            &bloom.get_mix_texture(),\n        );\n        let stage_pipeline = Self::create_primitive_pipeline(\n            device,\n            wgpu::TextureFormat::Rgba16Float,\n            multisample_count,\n        );\n        let geometry_buffer = geometry.slab_allocator().commit();\n        let lighting = Lighting::new(stage_config.shadow_map_atlas_size, &geometry);\n\n        let brdf_lut = BrdfLut::new(runtime);\n        let skybox = Skybox::empty(runtime);\n        let ibl = Ibl::new(runtime, &skybox);\n\n        Self {\n            materials,\n            draw_calls: Arc::new(RwLock::new(DrawCalls::new(\n                ctx,\n                ctx.get_use_direct_draw(),\n                &geometry_buffer,\n                &depth_texture,\n            ))),\n            lighting,\n            depth_texture: Arc::new(RwLock::new(depth_texture)),\n            stage_slab_buffer: Arc::new(RwLock::new(geometry_buffer)),\n            geometry,\n\n            primitive_pipeline: Arc::new(RwLock::new(stage_pipeline)),\n            primitive_bind_group: ManagedBindGroup::default(),\n            primitive_bind_group_created: Arc::new(0.into()),\n\n            ibl: Arc::new(RwLock::new(ibl)),\n            skybox: Arc::new(RwLock::new(skybox)),\n            skybox_bindgroup: Default::default(),\n            skybox_pipeline: Default::default(),\n            has_skybox: Arc::new(AtomicBool::new(false)),\n            brdf_lut,\n            bloom,\n            tonemapping,\n            has_bloom: AtomicBool::from(true).into(),\n            textures_bindgroup: Default::default(),\n            debug_overlay: DebugOverlay::new(device, ctx.get_render_target().format()),\n            has_debug_overlay: Arc::new(false.into()),\n            hdr_texture,\n            msaa_render_target,\n            msaa_sample_count: Arc::new(multisample_count.into()),\n            clear_color_attachments: Arc::new(true.into()),\n            clear_depth_attachments: Arc::new(true.into()),\n            background_color: Arc::new(RwLock::new(wgpu::Color::TRANSPARENT)),\n        }\n    }\n\n    pub fn set_background_color(&self, color: impl Into<Vec4>) {\n        let color = color.into();\n        *self\n            .background_color\n            .write()\n            .expect(\"background_color write\") = wgpu::Color {\n            r: color.x as f64,\n            g: color.y as f64,\n            b: color.z as f64,\n            a: color.w as f64,\n        };\n    }\n\n    pub fn with_background_color(self, color: impl Into<Vec4>) -> Self {\n        self.set_background_color(color);\n        self\n    }\n\n    /// Return the multisample count.\n    pub fn get_msaa_sample_count(&self) -> u32 {\n        self.msaa_sample_count\n            .load(std::sync::atomic::Ordering::Relaxed)\n    }\n\n    /// Set the MSAA multisample count.\n    ///\n    /// Set to `1` to disable MSAA. Setting to `0` will be treated the same as\n    /// setting to `1`.\n    pub fn set_msaa_sample_count(&self, multisample_count: u32) {\n        let multisample_count = multisample_count.max(1);\n        let prev_multisample_count = self\n            .msaa_sample_count\n            .swap(multisample_count, Ordering::Relaxed);\n        if prev_multisample_count == multisample_count {\n            log::warn!(\"set_multisample_count: multisample count is unchanged, noop\");\n            return;\n        }\n\n        log::debug!(\"setting multisample count to {multisample_count}\");\n        // UNWRAP: POP\n        *self\n            .primitive_pipeline\n            .write()\n            .expect(\"primitive_pipeline write\") = Self::create_primitive_pipeline(\n            self.device(),\n            wgpu::TextureFormat::Rgba16Float,\n            multisample_count,\n        );\n        let size = self.get_size();\n        // UNWRAP: POP\n        *self.depth_texture.write().expect(\"depth_texture write\") = Texture::create_depth_texture(\n            self.device(),\n            size.x,\n            size.y,\n            multisample_count,\n            Some(\"stage-depth\"),\n        );\n        let size = self.get_size();\n        // UNWRAP: POP\n        let format = self\n            .hdr_texture\n            .read()\n            .expect(\"hdr_texture read\")\n            .texture\n            .format();\n        *self\n            .msaa_render_target\n            .write()\n            .expect(\"msaa_render_target write\") = if multisample_count == 1 {\n            None\n        } else {\n            Some(create_msaa_textureview(\n                self.device(),\n                size.x,\n                size.y,\n                format,\n                multisample_count,\n            ))\n        };\n\n        // Invalidate the textures bindgroup - it must be recreated\n        let _ = self\n            .textures_bindgroup\n            .lock()\n            .expect(\"textures_bindgroup lock\")\n            .take();\n    }\n\n    /// Set the MSAA multisample count.\n    ///\n    /// Set to `1` to disable MSAA. Setting to `0` will be treated the same as\n    /// setting to `1`.\n    pub fn with_msaa_sample_count(self, multisample_count: u32) -> Self {\n        self.set_msaa_sample_count(multisample_count);\n        self\n    }\n\n    /// Set whether color attachments are cleared before rendering.\n    pub fn set_clear_color_attachments(&self, should_clear: bool) {\n        self.clear_color_attachments\n            .store(should_clear, Ordering::Relaxed);\n    }\n\n    /// Set whether color attachments are cleared before rendering.\n    pub fn with_clear_color_attachments(self, should_clear: bool) -> Self {\n        self.set_clear_color_attachments(should_clear);\n        self\n    }\n\n    /// Set whether color attachments are cleared before rendering.\n    pub fn set_clear_depth_attachments(&self, should_clear: bool) {\n        self.clear_depth_attachments\n            .store(should_clear, Ordering::Relaxed);\n    }\n\n    /// Set whether color attachments are cleared before rendering.\n    pub fn with_clear_depth_attachments(self, should_clear: bool) -> Self {\n        self.set_clear_depth_attachments(should_clear);\n        self\n    }\n\n    /// Set the debug mode.\n    pub fn set_debug_mode(&self, debug_mode: DebugChannel) {\n        self.geometry\n            .descriptor()\n            .modify(|cfg| cfg.debug_channel = debug_mode);\n    }\n\n    /// Set the debug mode.\n    pub fn with_debug_mode(self, debug_mode: DebugChannel) -> Self {\n        self.set_debug_mode(debug_mode);\n        self\n    }\n\n    /// Set whether to render the debug overlay.\n    pub fn set_use_debug_overlay(&self, use_debug_overlay: bool) {\n        self.has_debug_overlay\n            .store(use_debug_overlay, std::sync::atomic::Ordering::Relaxed);\n    }\n\n    /// Set whether to render the debug overlay.\n    pub fn with_debug_overlay(self, use_debug_overlay: bool) -> Self {\n        self.set_use_debug_overlay(use_debug_overlay);\n        self\n    }\n\n    /// Set whether to use frustum culling on GPU before drawing.\n    ///\n    /// This defaults to `true`.\n    pub fn set_use_frustum_culling(&self, use_frustum_culling: bool) {\n        self.geometry\n            .descriptor()\n            .modify(|cfg| cfg.perform_frustum_culling = use_frustum_culling);\n    }\n\n    /// Set whether to render the debug overlay.\n    pub fn with_frustum_culling(self, use_frustum_culling: bool) -> Self {\n        self.set_use_frustum_culling(use_frustum_culling);\n        self\n    }\n\n    /// Set whether to use occlusion culling on GPU before drawing.\n    ///\n    /// This defaults to `false`.\n    ///\n    /// ## Warning\n    ///\n    /// Occlusion culling is a feature in development. YMMV.\n    pub fn set_use_occlusion_culling(&self, use_occlusion_culling: bool) {\n        self.geometry\n            .descriptor()\n            .modify(|cfg| cfg.perform_occlusion_culling = use_occlusion_culling);\n    }\n\n    /// Set whether to render the debug overlay.\n    pub fn with_occlusion_culling(self, use_occlusion_culling: bool) -> Self {\n        self.set_use_occlusion_culling(use_occlusion_culling);\n        self\n    }\n\n    /// Set whether the stage uses lighting.\n    pub fn set_has_lighting(&self, use_lighting: bool) {\n        self.geometry\n            .descriptor()\n            .modify(|cfg| cfg.has_lighting = use_lighting);\n    }\n\n    /// Set whether the stage uses lighting.\n    pub fn with_lighting(self, use_lighting: bool) -> Self {\n        self.set_has_lighting(use_lighting);\n        self\n    }\n\n    /// Set whether to use vertex skinning.\n    pub fn set_has_vertex_skinning(&self, use_skinning: bool) {\n        self.geometry\n            .descriptor()\n            .modify(|cfg| cfg.has_skinning = use_skinning);\n    }\n\n    /// Set whether to use vertex skinning.\n    pub fn with_vertex_skinning(self, use_skinning: bool) -> Self {\n        self.set_has_vertex_skinning(use_skinning);\n        self\n    }\n\n    pub fn get_size(&self) -> UVec2 {\n        // UNWRAP: panic on purpose\n        let hdr = self.hdr_texture.read().expect(\"hdr_texture read\");\n        let w = hdr.width();\n        let h = hdr.height();\n        UVec2::new(w, h)\n    }\n\n    pub fn set_size(&self, size: UVec2) {\n        if size == self.get_size() {\n            return;\n        }\n\n        self.geometry\n            .descriptor()\n            .modify(|cfg| cfg.resolution = size);\n        let hdr_texture = Texture::create_hdr_texture(self.device(), size.x, size.y, 1);\n        let sample_count = self.msaa_sample_count.load(Ordering::Relaxed);\n        if let Some(msaa_view) = self\n            .msaa_render_target\n            .write()\n            .expect(\"msaa_render_target write\")\n            .as_mut()\n        {\n            *msaa_view = create_msaa_textureview(\n                self.device(),\n                size.x,\n                size.y,\n                hdr_texture.texture.format(),\n                sample_count,\n            );\n        }\n\n        // UNWRAP: panic on purpose\n        *self.depth_texture.write().expect(\"depth_texture write\") = Texture::create_depth_texture(\n            self.device(),\n            size.x,\n            size.y,\n            sample_count,\n            Some(\"stage-depth\"),\n        );\n        self.bloom.set_hdr_texture(self.runtime(), &hdr_texture);\n        self.tonemapping\n            .set_hdr_texture(self.device(), &hdr_texture);\n        *self.hdr_texture.write().expect(\"hdr_texture write\") = hdr_texture;\n\n        let _ = self\n            .skybox_bindgroup\n            .lock()\n            .expect(\"skybox_bindgroup lock\")\n            .take();\n        let _ = self\n            .textures_bindgroup\n            .lock()\n            .expect(\"textures_bindgroup lock\")\n            .take();\n    }\n\n    pub fn with_size(self, size: UVec2) -> Self {\n        self.set_size(size);\n        self\n    }\n\n    /// Turn the bloom effect on or off.\n    pub fn set_has_bloom(&self, has_bloom: bool) {\n        self.has_bloom\n            .store(has_bloom, std::sync::atomic::Ordering::Relaxed);\n    }\n\n    /// Turn the bloom effect on or off.\n    pub fn with_bloom(self, has_bloom: bool) -> Self {\n        self.set_has_bloom(has_bloom);\n        self\n    }\n\n    /// Set the amount of bloom that is mixed in with the input image.\n    ///\n    /// Defaults to `0.04`.\n    pub fn set_bloom_mix_strength(&self, strength: f32) {\n        self.bloom.set_mix_strength(strength);\n    }\n\n    pub fn with_bloom_mix_strength(self, strength: f32) -> Self {\n        self.set_bloom_mix_strength(strength);\n        self\n    }\n\n    /// Sets the bloom filter radius, in pixels.\n    ///\n    /// Default is `1.0`.\n    pub fn set_bloom_filter_radius(&self, filter_radius: f32) {\n        self.bloom.set_filter_radius(filter_radius);\n    }\n\n    /// Sets the bloom filter radius, in pixels.\n    ///\n    /// Default is `1.0`.\n    pub fn with_bloom_filter_radius(self, filter_radius: f32) -> Self {\n        self.set_bloom_filter_radius(filter_radius);\n        self\n    }\n\n    /// Adds a primitive to the internal list of primitives to be drawn each\n    /// frame.\n    ///\n    /// Returns the number of primitives added.\n    ///\n    /// If you drop the primitive and no other references are kept, it will be\n    /// removed automatically from the internal list and will cease to be\n    /// drawn each frame.\n    pub fn add_primitive(&self, primitive: &Primitive) -> usize {\n        // UNWRAP: if we can't acquire the lock we want to panic.\n        let mut draws = self.draw_calls.write().expect(\"draw_calls write\");\n        draws.add_primitive(primitive)\n    }\n\n    /// Erase the given primitive from the internal list of primitives to be\n    /// drawn each frame.\n    ///\n    /// Returns the number of primitives added.\n    pub fn remove_primitive(&self, primitive: &Primitive) -> usize {\n        let mut draws = self.draw_calls.write().expect(\"draw_calls write\");\n        draws.remove_primitive(primitive)\n    }\n\n    /// Sort the drawing order of primitives.\n    ///\n    /// This determines the order in which [`Primitive`]s are drawn each frame.\n    pub fn sort_primitive(&self, f: impl Fn(&Primitive, &Primitive) -> std::cmp::Ordering) {\n        // UNWRAP: panic on purpose\n        let mut guard = self.draw_calls.write().expect(\"draw_calls write\");\n        guard.sort_primitives(f);\n    }\n\n    /// Returns a clone of the current depth texture.\n    pub fn get_depth_texture(&self) -> DepthTexture {\n        DepthTexture {\n            runtime: self.runtime().clone(),\n            texture: self\n                .depth_texture\n                .read()\n                .expect(\"depth_texture read\")\n                .texture\n                .clone(),\n        }\n    }\n\n    /// Create a new [`NestedTransform`].\n    pub fn new_nested_transform(&self) -> NestedTransform {\n        NestedTransform::new(self.geometry.slab_allocator())\n    }\n\n    /// Render the staged scene into the given view.\n    pub fn render(&self, view: &wgpu::TextureView) {\n        // UNWRAP: POP\n        let background_color = *self.background_color.read().expect(\"background_color read\");\n        // UNWRAP: POP\n        let msaa_target = self\n            .msaa_render_target\n            .read()\n            .expect(\"msaa_render_target read\");\n        let clear_colors = self.clear_color_attachments.load(Ordering::Relaxed);\n        let hdr_texture = self.hdr_texture.read().expect(\"hdr_texture read\");\n\n        let mk_ops = |store| wgpu::Operations {\n            load: if clear_colors {\n                wgpu::LoadOp::Clear(background_color)\n            } else {\n                wgpu::LoadOp::Load\n            },\n            store,\n        };\n        let render_pass_color_attachment = if let Some(msaa_view) = msaa_target.as_ref() {\n            wgpu::RenderPassColorAttachment {\n                ops: mk_ops(wgpu::StoreOp::Discard),\n                view: msaa_view,\n                resolve_target: Some(&hdr_texture.view),\n                depth_slice: None,\n            }\n        } else {\n            wgpu::RenderPassColorAttachment {\n                ops: mk_ops(wgpu::StoreOp::Store),\n                view: &hdr_texture.view,\n                resolve_target: None,\n                depth_slice: None,\n            }\n        };\n\n        let depth_texture = self.depth_texture.read().expect(\"depth_texture read\");\n        let clear_depth = self.clear_depth_attachments.load(Ordering::Relaxed);\n        let render_pass_depth_attachment = wgpu::RenderPassDepthStencilAttachment {\n            view: &depth_texture.view,\n            depth_ops: Some(wgpu::Operations {\n                load: if clear_depth {\n                    wgpu::LoadOp::Clear(1.0)\n                } else {\n                    wgpu::LoadOp::Load\n                },\n                store: wgpu::StoreOp::Store,\n            }),\n            stencil_ops: None,\n        };\n        let pipeline_guard = self\n            .primitive_pipeline\n            .read()\n            .expect(\"primitive_pipeline read\");\n        let (_submission_index, maybe_indirect_buffer) = StageRendering {\n            pipeline: &pipeline_guard,\n            stage: self,\n            color_attachment: render_pass_color_attachment,\n            depth_stencil_attachment: render_pass_depth_attachment,\n        }\n        .run();\n\n        // then render bloom\n        if self.has_bloom.load(Ordering::Relaxed) {\n            self.bloom.bloom(self.device(), self.queue());\n        } else {\n            // copy the input hdr texture to the bloom mix texture\n            let mut encoder =\n                self.device()\n                    .create_command_encoder(&wgpu::CommandEncoderDescriptor {\n                        label: Some(\"no bloom copy\"),\n                    });\n            let bloom_mix_texture = self.bloom.get_mix_texture();\n            encoder.copy_texture_to_texture(\n                wgpu::TexelCopyTextureInfo {\n                    texture: &self.hdr_texture.read().expect(\"hdr_texture read\").texture,\n                    mip_level: 0,\n                    origin: wgpu::Origin3d { x: 0, y: 0, z: 0 },\n                    aspect: wgpu::TextureAspect::All,\n                },\n                wgpu::TexelCopyTextureInfo {\n                    texture: &bloom_mix_texture.texture,\n                    mip_level: 0,\n                    origin: wgpu::Origin3d { x: 0, y: 0, z: 0 },\n                    aspect: wgpu::TextureAspect::All,\n                },\n                wgpu::Extent3d {\n                    width: bloom_mix_texture.width(),\n                    height: bloom_mix_texture.height(),\n                    depth_or_array_layers: 1,\n                },\n            );\n            self.queue().submit(std::iter::once(encoder.finish()));\n        }\n\n        // then render tonemapping\n        self.tonemapping.render(self.device(), self.queue(), view);\n\n        // then render the debug overlay\n        if self.has_debug_overlay.load(Ordering::Relaxed) {\n            if let Some(indirect_draw_buffer) = maybe_indirect_buffer {\n                self.debug_overlay.render(\n                    self.device(),\n                    self.queue(),\n                    view,\n                    &self\n                        .stage_slab_buffer\n                        .read()\n                        .expect(\"stage_slab_buffer read\"),\n                    &indirect_draw_buffer,\n                );\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use craballoc::{runtime::CpuRuntime, slab::SlabAllocator};\n    use crabslab::{Array, Id, Slab};\n    use glam::{Mat4, Vec2, Vec3, Vec4};\n\n    use crate::{\n        context::Context,\n        geometry::{shader::GeometryDescriptor, Geometry, Vertex},\n        test::BlockOnFuture,\n        transform::NestedTransform,\n    };\n\n    #[test]\n    fn vertex_slab_roundtrip() {\n        let initial_vertices = {\n            let tl = Vertex::default()\n                .with_position(Vec3::ZERO)\n                .with_uv0(Vec2::ZERO);\n            let tr = Vertex::default()\n                .with_position(Vec3::new(1.0, 0.0, 0.0))\n                .with_uv0(Vec2::new(1.0, 0.0));\n            let bl = Vertex::default()\n                .with_position(Vec3::new(0.0, 1.0, 0.0))\n                .with_uv0(Vec2::new(0.0, 1.0));\n            let br = Vertex::default()\n                .with_position(Vec3::new(1.0, 1.0, 0.0))\n                .with_uv0(Vec2::splat(1.0));\n            vec![tl, bl, br, tl, br, tr]\n        };\n        let mut slab = [0u32; 256];\n        slab.write_indexed_slice(&initial_vertices, 0);\n        let vertices = slab.read_vec(Array::<Vertex>::new(0, initial_vertices.len() as u32));\n        pretty_assertions::assert_eq!(initial_vertices, vertices);\n    }\n\n    #[test]\n    fn matrix_subtraction_sanity() {\n        let m = Mat4::IDENTITY - Mat4::IDENTITY;\n        assert_eq!(Mat4::ZERO, m);\n    }\n\n    #[test]\n    fn can_global_transform_calculation() {\n        #[expect(\n            clippy::needless_borrows_for_generic_args,\n            reason = \"This is just riff-raff, as it doesn't compile without the borrow.\"\n        )]\n        let slab = SlabAllocator::<CpuRuntime>::new(&CpuRuntime, \"transform\", ());\n        // Setup a hierarchy of transforms\n        let root = NestedTransform::new(&slab);\n        let child = NestedTransform::new(&slab).with_local_translation(Vec3::new(1.0, 0.0, 0.0));\n        let grandchild =\n            NestedTransform::new(&slab).with_local_translation(Vec3::new(1.0, 0.0, 0.0));\n        log::info!(\"hierarchy\");\n        // Build the hierarchy\n        root.add_child(&child);\n        child.add_child(&grandchild);\n\n        log::info!(\"get_global_transform\");\n        // Calculate global transforms\n        let grandchild_global_transform = grandchild.global_descriptor();\n\n        // Assert that the global transform is as expected\n        assert_eq!(\n            grandchild_global_transform.translation.x, 2.0,\n            \"Grandchild's global translation should   2.0 along the x-axis\"\n        );\n    }\n\n    #[test]\n    fn can_msaa() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx\n            .new_stage()\n            .with_background_color([1.0, 1.0, 1.0, 1.0])\n            .with_lighting(false);\n        let (projection, view) = crate::camera::default_ortho2d(100.0, 100.0);\n        let _camera = stage\n            .new_camera()\n            .with_projection_and_view(projection, view);\n        let _triangle_rez = stage.new_primitive().with_vertices(\n            stage.new_vertices([\n                Vertex::default()\n                    .with_position([10.0, 10.0, 0.0])\n                    .with_color([0.0, 1.0, 1.0, 1.0]),\n                Vertex::default()\n                    .with_position([10.0, 90.0, 0.0])\n                    .with_color([1.0, 1.0, 0.0, 1.0]),\n                Vertex::default()\n                    .with_position([90.0, 10.0, 0.0])\n                    .with_color([1.0, 0.0, 1.0, 1.0]),\n            ]),\n        );\n\n        log::debug!(\"rendering without msaa\");\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq_cfg(\n            \"msaa/without.png\",\n            img,\n            img_diff::DiffCfg {\n                pixel_threshold: img_diff::LOW_PIXEL_THRESHOLD,\n                ..Default::default()\n            },\n        );\n        frame.present();\n        log::debug!(\"  all good!\");\n\n        stage.set_msaa_sample_count(4);\n        log::debug!(\"rendering with msaa\");\n        let frame = ctx.get_next_frame().unwrap();\n        stage.render(&frame.view());\n        let img = frame.read_image().block().unwrap();\n        img_diff::assert_img_eq_cfg(\n            \"msaa/with.png\",\n            img,\n            img_diff::DiffCfg {\n                pixel_threshold: img_diff::LOW_PIXEL_THRESHOLD,\n                ..Default::default()\n            },\n        );\n        frame.present();\n    }\n\n    #[test]\n    /// Tests that the PBR descriptor is written to slot 0 of the geometry\n    /// buffer, and that it contains what we think it contains.\n    fn stage_geometry_desc_sanity() {\n        let ctx = Context::headless(100, 100).block();\n        let stage = ctx.new_stage();\n        let _ = stage.commit();\n\n        let slab = futures_lite::future::block_on({\n            let geometry: &Geometry = stage.as_ref();\n            geometry.slab_allocator().read(..)\n        })\n        .unwrap();\n        let pbr_desc = slab.read_unchecked(Id::<GeometryDescriptor>::new(0));\n        pretty_assertions::assert_eq!(stage.geometry_descriptor().get(), pbr_desc);\n    }\n\n    #[test]\n    fn slabbed_vertices_native() {\n        let ctx = Context::headless(100, 100).block();\n        let runtime = ctx.as_ref();\n\n        // Create our geometry on the slab.\n        let slab = SlabAllocator::new(\n            runtime,\n            \"slabbed_isosceles_triangle\",\n            wgpu::BufferUsages::empty(),\n        );\n\n        let geometry = vec![\n            (Vec3::new(0.5, -0.5, 0.0), Vec4::new(1.0, 0.0, 0.0, 1.0)),\n            (Vec3::new(0.0, 0.5, 0.0), Vec4::new(0.0, 1.0, 0.0, 1.0)),\n            (Vec3::new(-0.5, -0.5, 0.0), Vec4::new(0.0, 0.0, 1.0, 1.0)),\n            (Vec3::new(-1.0, 1.0, 0.0), Vec4::new(1.0, 0.0, 0.0, 1.0)),\n            (Vec3::new(-1.0, 0.0, 0.0), Vec4::new(0.0, 1.0, 0.0, 1.0)),\n            (Vec3::new(0.0, 1.0, 0.0), Vec4::new(0.0, 0.0, 1.0, 1.0)),\n        ];\n        let vertices = slab.new_array(geometry);\n        let array = slab.new_value(vertices.array());\n\n        // Create a bindgroup for the slab so our shader can read out the types.\n        let bindgroup_layout =\n            runtime\n                .device\n                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n                    label: None,\n                    entries: &[wgpu::BindGroupLayoutEntry {\n                        binding: 0,\n                        visibility: wgpu::ShaderStages::VERTEX,\n                        ty: wgpu::BindingType::Buffer {\n                            ty: wgpu::BufferBindingType::Storage { read_only: true },\n                            has_dynamic_offset: false,\n                            min_binding_size: None,\n                        },\n                        count: None,\n                    }],\n                });\n        let pipeline_layout =\n            runtime\n                .device\n                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n                    label: None,\n                    bind_group_layouts: &[&bindgroup_layout],\n                    push_constant_ranges: &[],\n                });\n\n        let vertex = crate::linkage::slabbed_vertices::linkage(&runtime.device);\n        let fragment = crate::linkage::passthru_fragment::linkage(&runtime.device);\n        let pipeline = runtime\n            .device\n            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n                label: None,\n                cache: None,\n                layout: Some(&pipeline_layout),\n                vertex: wgpu::VertexState {\n                    compilation_options: wgpu::PipelineCompilationOptions::default(),\n                    module: &vertex.module,\n                    entry_point: Some(vertex.entry_point),\n                    buffers: &[],\n                },\n                primitive: wgpu::PrimitiveState {\n                    topology: wgpu::PrimitiveTopology::TriangleList,\n                    strip_index_format: None,\n                    front_face: wgpu::FrontFace::Ccw,\n                    cull_mode: None,\n                    unclipped_depth: false,\n                    polygon_mode: wgpu::PolygonMode::Fill,\n                    conservative: false,\n                },\n                depth_stencil: None,\n                multisample: wgpu::MultisampleState {\n                    mask: !0,\n                    alpha_to_coverage_enabled: false,\n                    count: 1,\n                },\n                fragment: Some(wgpu::FragmentState {\n                    compilation_options: Default::default(),\n                    module: &fragment.module,\n                    entry_point: Some(fragment.entry_point),\n                    targets: &[Some(wgpu::ColorTargetState {\n                        format: wgpu::TextureFormat::Rgba8UnormSrgb,\n                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                        write_mask: wgpu::ColorWrites::ALL,\n                    })],\n                }),\n                multiview: None,\n            });\n        let slab_buffer = slab.commit();\n\n        let bindgroup = runtime\n            .device\n            .create_bind_group(&wgpu::BindGroupDescriptor {\n                label: None,\n                layout: &bindgroup_layout,\n                entries: &[wgpu::BindGroupEntry {\n                    binding: 0,\n                    resource: slab_buffer.as_entire_binding(),\n                }],\n            });\n\n        let frame = ctx.get_next_frame().unwrap();\n        let mut encoder = runtime.device.create_command_encoder(&Default::default());\n        {\n            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                    view: &frame.view(),\n                    resolve_target: None,\n                    ops: wgpu::Operations {\n                        load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),\n                        store: wgpu::StoreOp::Store,\n                    },\n                    depth_slice: None,\n                })],\n                ..Default::default()\n            });\n            render_pass.set_pipeline(&pipeline);\n            render_pass.set_bind_group(0, &bindgroup, &[]);\n            let id = array.id().inner();\n            render_pass.draw(0..vertices.len() as u32, id..id + 1);\n        }\n        runtime.queue.submit(std::iter::once(encoder.finish()));\n\n        let img = frame\n            .read_linear_image()\n            .block()\n            .expect(\"could not read frame\");\n        img_diff::assert_img_eq(\"tutorial/slabbed_isosceles_triangle.png\", img);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/stage.rs",
    "content": "//! Scene staging.\n//!\n//! The [`Stage`] is the entrypoint for staging data on the GPU and\n//! interacting with lighting.\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\n#[cfg(test)]\nmod test {\n    use craballoc::{prelude::SlabAllocator, runtime::CpuRuntime};\n    use glam::{Mat4, Quat, Vec3};\n\n    use crate::{\n        math::IsMatrix,\n        transform::{shader::TransformDescriptor, NestedTransform},\n    };\n\n    #[test]\n    fn matrix_hierarchy_sanity() {\n        let a: Mat4 = TransformDescriptor {\n            translation: Vec3::new(100.0, 100.0, 0.0),\n            ..Default::default()\n        }\n        .into();\n        let b: Mat4 = TransformDescriptor {\n            scale: Vec3::splat(0.5),\n            ..Default::default()\n        }\n        .into();\n        let c1 = a * b;\n        let c2 = b * a;\n        assert_ne!(c1, c2);\n    }\n\n    #[test]\n    fn nested_transform_fox_rigging() {\n        pub fn legacy_get_world_transform(tfrm: &NestedTransform) -> (Vec3, Quat, Vec3) {\n            let mut mat = Mat4::IDENTITY;\n            let mut local = Some(tfrm.clone());\n            while let Some(t) = local.take() {\n                let transform = t.local_descriptor();\n                mat = Mat4::from_scale_rotation_translation(\n                    transform.scale,\n                    transform.rotation,\n                    transform.translation,\n                ) * mat;\n                local = t.parent();\n            }\n            let (s, r, t) = mat.to_scale_rotation_translation_or_id();\n            (t, r, s)\n        }\n\n        let slab = SlabAllocator::new(CpuRuntime, \"transform\", ());\n        let a = NestedTransform::new(&slab);\n        a.set_local_translation(Vec3::splat(100.0));\n        let b = NestedTransform::new(&slab);\n        b.set_local_rotation(Quat::from_scaled_axis(Vec3::Z));\n        let c = NestedTransform::new(&slab);\n        c.set_local_scale(Vec3::splat(2.0));\n\n        a.add_child(&b);\n        b.add_child(&c);\n\n        let TransformDescriptor {\n            translation,\n            rotation,\n            scale,\n        } = c.global_descriptor();\n        let global_transform = (translation, rotation, scale);\n        let legacy_transform = legacy_get_world_transform(&c);\n        assert_eq!(legacy_transform, global_transform);\n\n        c.set_local_translation(Vec3::ONE);\n\n        let all_updates = slab.get_updated_source_ids();\n        assert_eq!(\n            std::collections::HashSet::from_iter([\n                a.global_transform.descriptor.notifier_index(),\n                b.global_transform.descriptor.notifier_index(),\n                c.global_transform.descriptor.notifier_index()\n            ]),\n            all_updates\n        );\n\n        let TransformDescriptor {\n            translation,\n            rotation,\n            scale,\n        } = c.global_descriptor();\n        let global_transform = (translation, rotation, scale);\n        let legacy_transform = legacy_get_world_transform(&c);\n        assert_eq!(legacy_transform, global_transform);\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/sync.rs",
    "content": "//! GPU locks and atomics.\n\nuse crabslab::Id;\n\n/// Perform an [atomic_i_increment](spirv_std::arch::atomic_i_increment)\n/// operation.\n///\n/// ## Note\n/// This is **not** atomic on CPU.\npub fn atomic_i_increment<const SCOPE: u32, const SEMANTICS: u32>(\n    slab: &mut [u32],\n    id: Id<u32>,\n) -> u32 {\n    #[cfg(gpu)]\n    {\n        let ptr = &mut slab[id.index()];\n        unsafe { spirv_std::arch::atomic_i_increment::<u32, SCOPE, SEMANTICS>(ptr) }\n    }\n    #[cfg(cpu)]\n    {\n        let prev = slab[id.index()];\n        slab[id.index()] = prev + 1;\n        prev\n    }\n}\n\n/// Perform an [atomic_u_min](spirv_std::arch::atomic_u_min) operation.\n///\n/// ## Note\n/// This is **not** atomic on CPU.\npub fn atomic_u_min<const SCOPE: u32, const SEMANTICS: u32>(\n    slab: &mut [u32],\n    id: Id<u32>,\n    val: u32,\n) -> u32 {\n    #[cfg(gpu)]\n    {\n        let ptr = &mut slab[id.index()];\n        unsafe { spirv_std::arch::atomic_u_min::<u32, SCOPE, SEMANTICS>(ptr, val) }\n    }\n    #[cfg(cpu)]\n    {\n        let prev = slab[id.index()];\n        let new = prev.min(val);\n        slab[id.index()] = new;\n        prev\n    }\n}\n\n/// Perform an [atomic_u_max](spirv_std::arch::atomic_u_max) operation.\n///\n/// ## Note\n/// This is **not** atomic on CPU.\npub fn atomic_u_max<const SCOPE: u32, const SEMANTICS: u32>(\n    slab: &mut [u32],\n    id: Id<u32>,\n    val: u32,\n) -> u32 {\n    #[cfg(gpu)]\n    {\n        let ptr = &mut slab[id.index()];\n        unsafe { spirv_std::arch::atomic_u_max::<u32, SCOPE, SEMANTICS>(ptr, val) }\n    }\n    #[cfg(cpu)]\n    {\n        let prev = slab[id.index()];\n        let new = prev.max(val);\n        slab[id.index()] = new;\n        prev\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/texture/mips.rs",
    "content": "//! Mip-map generation.\n\nuse crate::texture::Texture;\nuse craballoc::runtime::WgpuRuntime;\nuse snafu::Snafu;\n\nuse super::wgpu_texture_format_channels_and_subpixel_bytes_todo;\n\nconst LABEL: Option<&str> = Some(\"mip-map-generator\");\n\n#[derive(Debug, Snafu)]\npub enum MipMapError {\n    #[snafu(display(\"Texture format does not match, expected '{expected:?}' but saw '{seen:?}'\"))]\n    TextureMismatch {\n        expected: wgpu::TextureFormat,\n        seen: wgpu::TextureFormat,\n    },\n}\n\nfn create_pipeline(\n    device: &wgpu::Device,\n    format: wgpu::TextureFormat,\n    pp_layout: &wgpu::PipelineLayout,\n) -> wgpu::RenderPipeline {\n    let vertex_linkage = crate::linkage::generate_mipmap_vertex::linkage(device);\n    let fragment_linkage = crate::linkage::generate_mipmap_fragment::linkage(device);\n    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n        label: LABEL,\n        layout: Some(pp_layout),\n        vertex: wgpu::VertexState {\n            module: &vertex_linkage.module,\n            entry_point: Some(vertex_linkage.entry_point),\n            buffers: &[],\n            compilation_options: Default::default(),\n        },\n        primitive: wgpu::PrimitiveState {\n            topology: wgpu::PrimitiveTopology::TriangleList,\n            front_face: wgpu::FrontFace::Cw,\n            polygon_mode: wgpu::PolygonMode::Fill,\n            ..Default::default()\n        },\n        fragment: Some(wgpu::FragmentState {\n            module: &fragment_linkage.module,\n            entry_point: Some(fragment_linkage.entry_point),\n            targets: &[Some(wgpu::ColorTargetState {\n                format,\n                blend: None,\n                write_mask: wgpu::ColorWrites::all(),\n            })],\n            compilation_options: Default::default(),\n        }),\n        depth_stencil: None,\n        multisample: wgpu::MultisampleState::default(),\n        multiview: None,\n        cache: None,\n    })\n}\n\npub struct MipMapGenerator {\n    format: wgpu::TextureFormat,\n    pipeline: wgpu::RenderPipeline,\n    bindgroup_layout: wgpu::BindGroupLayout,\n}\n\nimpl MipMapGenerator {\n    pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {\n        let bg_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: LABEL,\n            entries: &[\n                wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Texture {\n                        sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                        view_dimension: wgpu::TextureViewDimension::D2,\n                        multisampled: false,\n                    },\n                    count: None,\n                },\n                wgpu::BindGroupLayoutEntry {\n                    binding: 1,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                    count: None,\n                },\n            ],\n        });\n        let pp_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label: LABEL,\n            bind_group_layouts: &[&bg_layout],\n            push_constant_ranges: &[],\n        });\n        let pipeline = create_pipeline(device, format, &pp_layout);\n        Self {\n            format,\n            pipeline,\n            bindgroup_layout: bg_layout,\n        }\n    }\n\n    /// Generate mip maps.\n    ///\n    /// # Errs\n    /// Errors if the texture's format doesn't match the generator format.\n    pub fn generate(\n        &self,\n        runtime: impl AsRef<WgpuRuntime>,\n        texture: &Texture,\n        mip_levels: u32,\n    ) -> Result<Vec<Texture>, MipMapError> {\n        snafu::ensure!(\n            texture.texture.format() == self.format,\n            TextureMismatchSnafu {\n                expected: self.format,\n                seen: texture.texture.format()\n            }\n        );\n\n        let mip_levels = 1.max(mip_levels);\n        let (color_channels, subpixel_bytes) =\n            wgpu_texture_format_channels_and_subpixel_bytes_todo(self.format);\n\n        let size = texture.texture.size();\n        let mut mips: Vec<Texture> = vec![];\n\n        for mip_level in 1..mip_levels {\n            let mip_width = size.width >> mip_level;\n            let mip_height = size.height >> mip_level;\n            let mip_texture = Texture::new_with(\n                runtime.as_ref(),\n                Some(&format!(\"mip{mip_level}\")),\n                Some(\n                    wgpu::TextureUsages::COPY_SRC\n                        | wgpu::TextureUsages::RENDER_ATTACHMENT\n                        | wgpu::TextureUsages::TEXTURE_BINDING,\n                ),\n                None,\n                self.format,\n                color_channels,\n                subpixel_bytes,\n                mip_width,\n                mip_height,\n                1,\n                &[],\n            );\n            let prev_texture = if mip_level == 1 {\n                texture\n            } else {\n                &mips[(mip_level - 2) as usize]\n            };\n            let bindgroup = runtime\n                .as_ref()\n                .device\n                .create_bind_group(&wgpu::BindGroupDescriptor {\n                    label: LABEL,\n                    layout: &self.bindgroup_layout,\n                    entries: &[\n                        wgpu::BindGroupEntry {\n                            binding: 0,\n                            resource: wgpu::BindingResource::TextureView(&prev_texture.view),\n                        },\n                        wgpu::BindGroupEntry {\n                            binding: 1,\n                            resource: wgpu::BindingResource::Sampler(&prev_texture.sampler),\n                        },\n                    ],\n                });\n\n            let mut encoder = runtime\n                .as_ref()\n                .device\n                .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());\n\n            {\n                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                    label: Some(&format!(\"mip{mip_level}\")),\n                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                        view: &mip_texture.view,\n                        resolve_target: None,\n                        ops: wgpu::Operations {\n                            load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),\n                            store: wgpu::StoreOp::Store,\n                        },\n                        depth_slice: None,\n                    })],\n                    depth_stencil_attachment: None,\n                    ..Default::default()\n                });\n\n                render_pass.set_pipeline(&self.pipeline);\n                render_pass.set_bind_group(0, Some(&bindgroup), &[]);\n                render_pass.draw(0..6, 0..1);\n            }\n\n            runtime\n                .as_ref()\n                .queue\n                .submit(std::iter::once(encoder.finish()));\n\n            mips.push(mip_texture);\n        }\n        Ok(mips)\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/texture.rs",
    "content": "//! Wrapper around [`wgpu::Texture`].\nuse core::sync::atomic::AtomicUsize;\nuse std::{\n    ops::Deref,\n    sync::{Arc, LazyLock, Mutex},\n};\n\nuse craballoc::runtime::WgpuRuntime;\nuse glam::{Mat4, UVec2};\nuse image::{\n    load_from_memory, DynamicImage, GenericImage, GenericImageView, ImageBuffer, ImageError, Luma,\n    PixelWithColorType, Rgba32FImage,\n};\nuse mips::MipMapGenerator;\nuse snafu::prelude::*;\n\nuse crate::{\n    atlas::{AtlasImage, AtlasImageFormat},\n    camera::Camera,\n};\n\npub mod mips;\n\n#[derive(Debug, Snafu)]\n/// Enumeration of errors produced by [`Texture`].\npub enum TextureError {\n    #[snafu(display(\"Unable to load '{}' image from memory: {}\", label, source))]\n    Loading { source: ImageError, label: String },\n\n    #[snafu(display(\"Image buffer '{}' unsupported color type: {:?}\", label, color_type))]\n    UnsupportedColorType {\n        color_type: image::ExtendedColorType,\n        label: String,\n    },\n\n    #[snafu(display(\"Could not map buffer\"))]\n    CouldNotMapBuffer { source: wgpu::BufferAsyncError },\n\n    #[snafu(display(\"Could not convert image buffer\"))]\n    CouldNotConvertImageBuffer,\n\n    #[snafu(display(\"Could not create an image buffer\"))]\n    CouldNotCreateImageBuffer,\n\n    #[snafu(display(\"Unsupported format: {format:#?}\"))]\n    UnsupportedFormat { format: wgpu::TextureFormat },\n\n    #[snafu(display(\"Buffer async error: {source}\"))]\n    BufferAsync { source: wgpu::BufferAsyncError },\n\n    #[snafu(display(\"Driver poll error: {source}\"))]\n    Poll { source: wgpu::PollError },\n}\n\ntype Result<T, E = TextureError> = std::result::Result<T, E>;\n\npub fn wgpu_texture_format_channels_and_subpixel_bytes(\n    format: wgpu::TextureFormat,\n) -> Result<(u32, u32)> {\n    Ok(match format {\n        wgpu::TextureFormat::Depth32Float => (1, 4),\n        wgpu::TextureFormat::R32Float => (1, 4),\n        wgpu::TextureFormat::Rg16Float => (2, 2),\n        wgpu::TextureFormat::Rgba8Unorm => (4, 1),\n        wgpu::TextureFormat::Rgba16Float => (4, 2),\n        wgpu::TextureFormat::Rgba32Float => (4, 4),\n        wgpu::TextureFormat::Rgba8UnormSrgb => (4, 1),\n        wgpu::TextureFormat::R8Unorm => (1, 1),\n        f => UnsupportedFormatSnafu { format: f }.fail()?,\n    })\n}\n\n/// ## Panics\npub fn wgpu_texture_format_channels_and_subpixel_bytes_todo(\n    format: wgpu::TextureFormat,\n) -> (u32, u32) {\n    wgpu_texture_format_channels_and_subpixel_bytes(format).unwrap()\n}\n\nstatic NEXT_TEXTURE_ID: LazyLock<Arc<AtomicUsize>> = LazyLock::new(|| Arc::new(0.into()));\n\npub(crate) fn get_next_texture_id() -> usize {\n    NEXT_TEXTURE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)\n}\n\n/// A texture living on the GPU.\n#[derive(Debug, Clone)]\npub struct Texture {\n    pub texture: Arc<wgpu::Texture>,\n    // TODO: revisit whether we really need to create view and sampler for textures\n    // automatically\n    pub view: Arc<wgpu::TextureView>,\n    pub sampler: Arc<wgpu::Sampler>,\n    pub(crate) id: usize,\n}\n\nimpl Texture {\n    /// Returns the id of this texture.\n    ///\n    /// The id is a monotonically increasing count of all textures created.\n    ///\n    /// This can be used to determine if a texture has been\n    /// replaced by another, which can be used, for example, to invalidate\n    /// a [`wgpu::BindGroup`].\n    pub fn id(&self) -> usize {\n        self.id\n    }\n\n    pub fn width(&self) -> u32 {\n        self.texture.width()\n    }\n\n    pub fn height(&self) -> u32 {\n        self.texture.height()\n    }\n\n    pub fn size(&self) -> UVec2 {\n        UVec2::new(self.width(), self.height())\n    }\n\n    /// Create a cubemap texture from 6 faces.\n    pub fn new_cubemap_texture(\n        runtime: impl AsRef<WgpuRuntime>,\n        label: Option<impl AsRef<str>>,\n        texture_size: u32,\n        face_textures: &[Texture],\n        image_format: wgpu::TextureFormat,\n        mip_levels: u32,\n    ) -> Self {\n        let label = label.as_ref().map(|s| s.as_ref());\n        let WgpuRuntime { device, queue } = runtime.as_ref();\n        let size = wgpu::Extent3d {\n            width: texture_size,\n            height: texture_size,\n            depth_or_array_layers: 6,\n        };\n        let cubemap_texture = device.create_texture(&wgpu::TextureDescriptor {\n            label: None,\n            size,\n            mip_level_count: mip_levels,\n            sample_count: 1,\n            dimension: wgpu::TextureDimension::D2,\n            format: image_format,\n            usage: wgpu::TextureUsages::TEXTURE_BINDING\n                | wgpu::TextureUsages::COPY_DST\n                | wgpu::TextureUsages::COPY_SRC,\n            view_formats: &[],\n        });\n\n        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {\n            label: Some(\"texture_buffer_copy_encoder\"),\n        });\n\n        for i in 0..6 {\n            for mip_level in 0..mip_levels as usize {\n                let mip_size = texture_size >> mip_level;\n                let index = i * mip_levels as usize + mip_level;\n                let texture = &face_textures[index].texture;\n                encoder.copy_texture_to_texture(\n                    wgpu::TexelCopyTextureInfo {\n                        texture,\n                        mip_level: 0,\n                        origin: wgpu::Origin3d::ZERO,\n                        aspect: wgpu::TextureAspect::All,\n                    },\n                    wgpu::TexelCopyTextureInfo {\n                        texture: &cubemap_texture,\n                        mip_level: mip_level as u32,\n                        origin: wgpu::Origin3d {\n                            x: 0,\n                            y: 0,\n                            z: i as u32,\n                        },\n                        aspect: wgpu::TextureAspect::All,\n                    },\n                    wgpu::Extent3d {\n                        width: mip_size,\n                        height: mip_size,\n                        depth_or_array_layers: 1,\n                    },\n                );\n            }\n        }\n        queue.submit([encoder.finish()]);\n\n        let view = cubemap_texture.create_view(&wgpu::TextureViewDescriptor {\n            dimension: Some(wgpu::TextureViewDimension::Cube),\n            label,\n            ..Default::default()\n        });\n\n        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {\n            address_mode_u: wgpu::AddressMode::ClampToEdge,\n            address_mode_v: wgpu::AddressMode::ClampToEdge,\n            address_mode_w: wgpu::AddressMode::ClampToEdge,\n            mag_filter: wgpu::FilterMode::Linear,\n            min_filter: wgpu::FilterMode::Linear,\n            mipmap_filter: wgpu::FilterMode::Linear,\n            label,\n            ..Default::default()\n        });\n\n        Texture {\n            texture: cubemap_texture.into(),\n            view: view.into(),\n            sampler: sampler.into(),\n            id: get_next_texture_id(),\n        }\n    }\n\n    /// Create a new texture.\n    #[allow(clippy::too_many_arguments)]\n    pub fn new_with(\n        runtime: impl AsRef<WgpuRuntime>,\n        label: Option<&str>,\n        usage: Option<wgpu::TextureUsages>,\n        sampler: Option<wgpu::Sampler>,\n        format: wgpu::TextureFormat,\n        color_channels: u32,\n        color_channel_bytes: u32,\n        width: u32,\n        height: u32,\n        mip_level_count: u32,\n        data: &[u8],\n    ) -> Self {\n        let runtime = runtime.as_ref();\n        let device = &runtime.device;\n        let queue = &runtime.queue;\n        let mip_level_count = 1.max(mip_level_count);\n        let size = wgpu::Extent3d {\n            width,\n            height,\n            depth_or_array_layers: 1,\n        };\n\n        let texture = device.create_texture(&wgpu::TextureDescriptor {\n            label,\n            size,\n            mip_level_count,\n            sample_count: 1,\n            dimension: wgpu::TextureDimension::D2,\n            format,\n            usage: usage\n                .unwrap_or(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST),\n            view_formats: &[],\n        });\n\n        if !data.is_empty() {\n            queue.write_texture(\n                wgpu::TexelCopyTextureInfo {\n                    texture: &texture,\n                    mip_level: 0,\n                    origin: wgpu::Origin3d::ZERO,\n                    aspect: wgpu::TextureAspect::All,\n                },\n                data,\n                wgpu::TexelCopyBufferLayout {\n                    offset: 0,\n                    bytes_per_row: Some(color_channels * color_channel_bytes * width),\n                    rows_per_image: None,\n                },\n                size,\n            );\n        }\n\n        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());\n        let sampler = sampler.unwrap_or_else(|| {\n            device.create_sampler(&wgpu::SamplerDescriptor {\n                address_mode_u: wgpu::AddressMode::ClampToEdge,\n                address_mode_v: wgpu::AddressMode::ClampToEdge,\n                address_mode_w: wgpu::AddressMode::ClampToEdge,\n                mag_filter: wgpu::FilterMode::Linear,\n                min_filter: wgpu::FilterMode::Linear,\n                mipmap_filter: wgpu::FilterMode::Linear,\n                ..Default::default()\n            })\n        });\n\n        Texture {\n            texture: Arc::new(texture),\n            view: Arc::new(view),\n            sampler: Arc::new(sampler),\n            id: get_next_texture_id(),\n        }\n    }\n\n    /// Create a new texture.\n    ///\n    /// This defaults the format to `Rgba8UnormSrgb` and assumes a pixel is 1\n    /// byte per channel.\n    #[allow(clippy::too_many_arguments)]\n    pub fn new(\n        runtime: impl AsRef<WgpuRuntime>,\n        label: Option<&str>,\n        usage: Option<wgpu::TextureUsages>,\n        color_channels: u32,\n        width: u32,\n        height: u32,\n        data: &[u8],\n    ) -> Self {\n        let runtime = runtime.as_ref();\n        Self::new_with(\n            runtime,\n            label,\n            usage,\n            None,\n            wgpu::TextureFormat::Rgba8UnormSrgb,\n            color_channels,\n            1,\n            width,\n            height,\n            1,\n            data,\n        )\n    }\n\n    pub fn from_image_bytes(\n        runtime: impl AsRef<WgpuRuntime>,\n        bytes: &[u8],\n        label: &str,\n    ) -> Result<Self> {\n        let img = load_from_memory(bytes).with_context(|_| LoadingSnafu {\n            label: label.to_string(),\n        })?;\n\n        match img {\n            DynamicImage::ImageLuma8(b) => {\n                Self::from_image_buffer(runtime, &b, Some(label), None, None)\n            }\n            DynamicImage::ImageLumaA8(b) => {\n                Self::from_image_buffer(runtime, &b, Some(label), None, None)\n            }\n            DynamicImage::ImageRgb8(b) => {\n                Self::from_image_buffer(runtime, &b, Some(label), None, None)\n            }\n            DynamicImage::ImageRgba8(b) => {\n                Self::from_image_buffer(runtime, &b, Some(label), None, None)\n            }\n            img => Self::from_image_buffer(runtime, &img.to_rgba8(), Some(label), None, None),\n        }\n    }\n\n    pub fn from_dynamic_image(\n        runtime: impl AsRef<WgpuRuntime>,\n        dyn_img: image::DynamicImage,\n        label: Option<&str>,\n        usage: Option<wgpu::TextureUsages>,\n        mip_level_count: u32,\n    ) -> Self {\n        let runtime = runtime.as_ref();\n        let device = &runtime.device;\n        let queue = &runtime.queue;\n        let mip_level_count = mip_level_count.max(1);\n        let dimensions = dyn_img.dimensions();\n\n        let size = wgpu::Extent3d {\n            width: dimensions.0,\n            height: dimensions.1,\n            depth_or_array_layers: 1,\n        };\n\n        let (img, format, channels) = match dyn_img {\n            img @ DynamicImage::ImageLuma8(_) => (img, wgpu::TextureFormat::R8Unorm, 1),\n            img @ DynamicImage::ImageRgba8(_) => (img, wgpu::TextureFormat::Rgba8UnormSrgb, 4),\n            img @ DynamicImage::ImageLuma16(_) => (img, wgpu::TextureFormat::R16Unorm, 1),\n            img @ DynamicImage::ImageRgba16(_) => (img, wgpu::TextureFormat::Rgba16Unorm, 4),\n            img @ DynamicImage::ImageRgba32F(_) => (img, wgpu::TextureFormat::Rgba32Float, 4),\n            img => {\n                let rgba8 = DynamicImage::ImageRgba8(img.into_rgba8());\n                (rgba8, wgpu::TextureFormat::Rgba8UnormSrgb, 4)\n            }\n        };\n\n        let texture = device.create_texture(&wgpu::TextureDescriptor {\n            label,\n            size,\n            mip_level_count,\n            sample_count: 1,\n            dimension: wgpu::TextureDimension::D2,\n            format,\n            usage: usage\n                .unwrap_or(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST),\n            view_formats: &[],\n        });\n\n        queue.write_texture(\n            wgpu::TexelCopyTextureInfo {\n                texture: &texture,\n                mip_level: 0,\n                origin: wgpu::Origin3d::ZERO,\n                aspect: wgpu::TextureAspect::All,\n            },\n            img.as_bytes(),\n            wgpu::TexelCopyBufferLayout {\n                offset: 0,\n                bytes_per_row: Some(channels * dimensions.0),\n                rows_per_image: Some(dimensions.1),\n            },\n            size,\n        );\n\n        Self::from_wgpu_tex(device, texture, None, None)\n    }\n\n    pub fn from_image_buffer<P>(\n        runtime: impl AsRef<WgpuRuntime>,\n        img: &ImageBuffer<P, Vec<u8>>,\n        label: Option<&str>,\n        usage: Option<wgpu::TextureUsages>,\n        mip_level_count: Option<u32>,\n    ) -> Result<Self>\n    where\n        P: PixelWithColorType,\n        ImageBuffer<P, Vec<u8>>: GenericImage + Deref<Target = [u8]>,\n    {\n        let runtime = runtime.as_ref();\n        let dimensions = img.dimensions();\n\n        let size = wgpu::Extent3d {\n            width: dimensions.0,\n            height: dimensions.1,\n            depth_or_array_layers: 1,\n        };\n\n        let texture = runtime.device.create_texture(&wgpu::TextureDescriptor {\n            label,\n            size,\n            mip_level_count: 1,\n            sample_count: 1,\n            dimension: wgpu::TextureDimension::D2,\n            format: {\n                ensure!(\n                    P::COLOR_TYPE == image::ExtendedColorType::Rgba8,\n                    UnsupportedColorTypeSnafu {\n                        color_type: P::COLOR_TYPE,\n                        label: label\n                            .map(ToString::to_string)\n                            .unwrap_or_else(|| \"unknown\".to_string()),\n                    }\n                );\n                wgpu::TextureFormat::Rgba8UnormSrgb\n            },\n            usage: usage\n                .unwrap_or(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST),\n            view_formats: &[],\n        });\n\n        runtime.queue.write_texture(\n            wgpu::TexelCopyTextureInfo {\n                texture: &texture,\n                mip_level: 0,\n                origin: wgpu::Origin3d::ZERO,\n                aspect: wgpu::TextureAspect::All,\n            },\n            img.deref(),\n            wgpu::TexelCopyBufferLayout {\n                offset: 0,\n                bytes_per_row: Some(P::CHANNEL_COUNT as u32 * dimensions.0),\n                rows_per_image: Some(dimensions.1),\n            },\n            size,\n        );\n\n        Ok(Self::from_wgpu_tex(\n            &runtime.device,\n            texture,\n            None,\n            mip_level_count,\n        ))\n    }\n\n    pub fn from_wgpu_tex(\n        device: &wgpu::Device,\n        texture: impl Into<Arc<wgpu::Texture>>,\n        sampler: Option<wgpu::SamplerDescriptor>,\n        mip_level_count: Option<u32>,\n    ) -> Self {\n        let texture = texture.into();\n        let view = Arc::new(texture.create_view(&wgpu::TextureViewDescriptor {\n            mip_level_count,\n            ..Default::default()\n        }));\n        let sampler_descriptor = sampler.unwrap_or_else(|| wgpu::SamplerDescriptor {\n            address_mode_u: wgpu::AddressMode::ClampToEdge,\n            address_mode_v: wgpu::AddressMode::ClampToEdge,\n            address_mode_w: wgpu::AddressMode::ClampToEdge,\n            mag_filter: wgpu::FilterMode::Linear,\n            min_filter: wgpu::FilterMode::Linear,\n            mipmap_filter: wgpu::FilterMode::Linear,\n            ..Default::default()\n        });\n        let sampler = Arc::new(device.create_sampler(&sampler_descriptor));\n\n        Self {\n            texture,\n            view,\n            sampler,\n            id: get_next_texture_id(),\n        }\n    }\n\n    pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;\n\n    pub fn create_depth_texture(\n        device: &wgpu::Device,\n        width: u32,\n        height: u32,\n        multisample_count: u32,\n        label: Option<&str>,\n    ) -> Self {\n        let size = wgpu::Extent3d {\n            width,\n            height,\n            depth_or_array_layers: 1,\n        };\n        let desc = wgpu::TextureDescriptor {\n            label,\n            size,\n            mip_level_count: 1,\n            sample_count: multisample_count,\n            dimension: wgpu::TextureDimension::D2,\n            format: Self::DEPTH_FORMAT,\n            usage: wgpu::TextureUsages::RENDER_ATTACHMENT\n                | wgpu::TextureUsages::TEXTURE_BINDING\n                | wgpu::TextureUsages::COPY_SRC,\n            view_formats: &[],\n        };\n        let texture = device.create_texture(&desc);\n\n        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());\n        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {\n            address_mode_u: wgpu::AddressMode::ClampToEdge,\n            address_mode_v: wgpu::AddressMode::ClampToEdge,\n            address_mode_w: wgpu::AddressMode::ClampToEdge,\n            mag_filter: wgpu::FilterMode::Linear,\n            min_filter: wgpu::FilterMode::Linear,\n            mipmap_filter: wgpu::FilterMode::Nearest,\n            compare: Some(wgpu::CompareFunction::LessEqual),\n            lod_min_clamp: 0.0,\n            lod_max_clamp: 100.0,\n            ..Default::default()\n        });\n\n        Self {\n            texture: Arc::new(texture),\n            view: Arc::new(view),\n            sampler: Arc::new(sampler),\n            id: get_next_texture_id(),\n        }\n    }\n\n    pub fn create_depth_texture_for_shadow_map(\n        device: &wgpu::Device,\n        width: u32,\n        height: u32,\n        multisample_count: u32,\n        label: Option<&str>,\n        is_point_light: bool,\n    ) -> Self {\n        let size = wgpu::Extent3d {\n            width,\n            height,\n            depth_or_array_layers: if is_point_light { 6 } else { 1 },\n        };\n        let desc = wgpu::TextureDescriptor {\n            label,\n            size,\n            mip_level_count: 1,\n            sample_count: multisample_count,\n            dimension: wgpu::TextureDimension::D2,\n            format: Self::DEPTH_FORMAT,\n            usage: wgpu::TextureUsages::RENDER_ATTACHMENT\n                | wgpu::TextureUsages::TEXTURE_BINDING\n                | wgpu::TextureUsages::COPY_SRC,\n            view_formats: &[],\n        };\n        let texture = device.create_texture(&desc);\n\n        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());\n        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {\n            address_mode_u: wgpu::AddressMode::ClampToEdge,\n            address_mode_v: wgpu::AddressMode::ClampToEdge,\n            address_mode_w: wgpu::AddressMode::ClampToEdge,\n            mag_filter: wgpu::FilterMode::Linear,\n            min_filter: wgpu::FilterMode::Linear,\n            mipmap_filter: wgpu::FilterMode::Nearest,\n            compare: Some(wgpu::CompareFunction::LessEqual),\n            lod_min_clamp: 0.0,\n            lod_max_clamp: 100.0,\n            ..Default::default()\n        });\n\n        Self {\n            texture: Arc::new(texture),\n            view: Arc::new(view),\n            sampler: Arc::new(sampler),\n            id: get_next_texture_id(),\n        }\n    }\n\n    /// Read the texture from the GPU.\n    ///\n    /// To read the texture you must provide the width, height, the number of\n    /// color/alpha channels and the number of bytes in the underlying\n    /// subpixel type (usually u8=1, u16=2 or f32=4).\n    // TODO: remove width and height from these calls, as they can be obtained\n    // from Texture::size()\n    pub fn read(\n        runtime: impl AsRef<WgpuRuntime>,\n        texture: &wgpu::Texture,\n        width: usize,\n        height: usize,\n        channels: usize,\n        subpixel_bytes: usize,\n    ) -> CopiedTextureBuffer {\n        CopiedTextureBuffer::read_from(\n            runtime,\n            texture,\n            width,\n            height,\n            channels,\n            subpixel_bytes,\n            0,\n            None,\n        )\n    }\n\n    pub async fn read_hdr_image(\n        &self,\n        runtime: impl AsRef<WgpuRuntime>,\n    ) -> Result<Rgba32FImage, TextureError> {\n        let runtime = runtime.as_ref();\n        let width = self.width();\n        let height = self.height();\n        let copied = Texture::read(\n            runtime,\n            &self.texture,\n            width as usize,\n            height as usize,\n            4,\n            2,\n        );\n\n        let pixels = copied.pixels(&runtime.device).await?;\n        let pixels = bytemuck::cast_slice::<u8, u16>(pixels.as_slice())\n            .iter()\n            .map(|p| half::f16::from_bits(*p).to_f32())\n            .collect::<Vec<_>>();\n        assert_eq!((width * height * 4) as usize, pixels.len());\n        let img: image::Rgba32FImage = image::ImageBuffer::from_vec(width, height, pixels)\n            .context(CouldNotCreateImageBufferSnafu)?;\n        Ok(img)\n    }\n\n    /// Generate `mipmap_levels - 1` mipmaps for the given texture.\n    ///\n    /// ## Note\n    /// Ensure that `self` only has one mip level. If not it will try to sample\n    /// from an empty mip.\n    pub fn generate_mips(\n        &mut self,\n        runtime: impl AsRef<WgpuRuntime>,\n        _label: Option<&str>,\n        mip_levels: u32,\n    ) -> Vec<Self> {\n        let runtime = runtime.as_ref();\n        let generator = MipMapGenerator::new(&runtime.device, self.texture.format());\n        // UNWRAP: safe because we know the formats match.\n        generator.generate(runtime, self, mip_levels).unwrap()\n    }\n\n    pub const HDR_TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;\n\n    /// Create a new HDR texture.\n    pub fn create_hdr_texture(\n        device: &wgpu::Device,\n        width: u32,\n        height: u32,\n        multisample_count: u32,\n    ) -> Texture {\n        // * The hdr texture is what we render to in most cases\n        // * we also read from it to calculate bloom\n        // * we also write the bloom mix result back to it\n        // * we also read the texture in tests\n        let usage = wgpu::TextureUsages::RENDER_ATTACHMENT\n            | wgpu::TextureUsages::TEXTURE_BINDING\n            | wgpu::TextureUsages::COPY_DST\n            | wgpu::TextureUsages::COPY_SRC;\n        let texture = Arc::new(device.create_texture(&wgpu::TextureDescriptor {\n            label: Some(\"hdr\"),\n            size: wgpu::Extent3d {\n                width,\n                height,\n                depth_or_array_layers: 1,\n            },\n            mip_level_count: 1,\n            sample_count: multisample_count,\n            dimension: wgpu::TextureDimension::D2,\n            format: Self::HDR_TEXTURE_FORMAT,\n            usage,\n            view_formats: &[],\n        }));\n        let sampler = Arc::new(device.create_sampler(&wgpu::SamplerDescriptor {\n            address_mode_u: wgpu::AddressMode::ClampToEdge,\n            address_mode_v: wgpu::AddressMode::ClampToEdge,\n            address_mode_w: wgpu::AddressMode::ClampToEdge,\n            mag_filter: wgpu::FilterMode::Nearest,\n            min_filter: wgpu::FilterMode::Nearest,\n            mipmap_filter: wgpu::FilterMode::Nearest,\n            ..Default::default()\n        }));\n        let view = Arc::new(texture.create_view(&wgpu::TextureViewDescriptor::default()));\n        Texture {\n            texture,\n            view,\n            sampler,\n            id: get_next_texture_id(),\n        }\n    }\n\n    #[allow(clippy::too_many_arguments)]\n    pub(crate) fn render_cubemap(\n        runtime: impl AsRef<WgpuRuntime>,\n        label: &str,\n        pipeline: &wgpu::RenderPipeline,\n        mut buffer_upkeep: impl FnMut(),\n        camera: &Camera,\n        bindgroup: &wgpu::BindGroup,\n        views: [Mat4; 6],\n        texture_size: u32,\n        mip_levels: Option<u32>,\n    ) -> Self {\n        let runtime = runtime.as_ref();\n        let device = &runtime.device;\n        let queue = &runtime.queue;\n        let mut cubemap_faces = Vec::new();\n        let mip_levels = mip_levels.unwrap_or(1);\n\n        // Render every cube face.\n        for (i, view) in views.iter().enumerate() {\n            let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {\n                label: Some(&format!(\"{label}-cubemap{i}\")),\n            });\n\n            let mut cubemap_face = Texture::new_with(\n                runtime,\n                Some(&format!(\"{label}-cubemap{i}\")),\n                Some(\n                    wgpu::TextureUsages::RENDER_ATTACHMENT\n                        | wgpu::TextureUsages::COPY_SRC\n                        | wgpu::TextureUsages::COPY_DST\n                        | wgpu::TextureUsages::TEXTURE_BINDING,\n                ),\n                None,\n                wgpu::TextureFormat::Rgba16Float,\n                4,\n                2,\n                texture_size,\n                texture_size,\n                1,\n                &[],\n            );\n\n            // update the view to point at one of the cube faces\n            camera.set_view(*view);\n            buffer_upkeep();\n\n            {\n                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                    label: Some(&format!(\"{label}-cubemap{i}\")),\n                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                        view: &cubemap_face.view,\n                        resolve_target: None,\n                        ops: wgpu::Operations {\n                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),\n                            store: wgpu::StoreOp::Store,\n                        },\n                        depth_slice: None,\n                    })],\n                    depth_stencil_attachment: None,\n                    ..Default::default()\n                });\n\n                render_pass.set_pipeline(pipeline);\n                render_pass.set_bind_group(0, Some(bindgroup), &[]);\n                render_pass.draw(0..36, 0..1);\n            }\n\n            queue.submit([encoder.finish()]);\n            let mips = cubemap_face.generate_mips(\n                runtime,\n                Some(&format!(\"{label}-cubemap mips\")),\n                mip_levels,\n            );\n            cubemap_faces.push(cubemap_face);\n            cubemap_faces.extend(mips);\n        }\n\n        Texture::new_cubemap_texture(\n            runtime,\n            Some(format!(\"{label}-cubemap\")),\n            texture_size,\n            cubemap_faces.as_slice(),\n            wgpu::TextureFormat::Rgba16Float,\n            mip_levels,\n        )\n    }\n}\n\npub async fn read_depth_texture_to_image(\n    runtime: impl AsRef<WgpuRuntime>,\n    width: usize,\n    height: usize,\n    texture: &wgpu::Texture,\n) -> Result<Option<image::GrayImage>> {\n    let depth_copied_buffer = Texture::read(runtime.as_ref(), texture, width, height, 1, 4);\n    let pixels = depth_copied_buffer.pixels(&runtime.as_ref().device).await?;\n    let pixels = bytemuck::cast_slice::<u8, f32>(&pixels)\n        .iter()\n        .copied()\n        .map(|f| {\n            // Depth texture is stored as Depth32Float, but the values are normalized\n            // 0.0-1.0\n            (255.0 * f) as u8\n        })\n        .collect::<Vec<u8>>();\n    Ok(image::GrayImage::from_raw(\n        width as u32,\n        height as u32,\n        pixels,\n    ))\n}\n\npub async fn read_depth_texture_f32(\n    runtime: impl AsRef<WgpuRuntime>,\n    width: usize,\n    height: usize,\n    texture: &wgpu::Texture,\n) -> Result<Option<image::ImageBuffer<Luma<f32>, Vec<f32>>>> {\n    let depth_copied_buffer = Texture::read(runtime.as_ref(), texture, width, height, 1, 4);\n    let pixels = depth_copied_buffer.pixels(&runtime.as_ref().device).await?;\n    let pixels = bytemuck::cast_slice::<u8, f32>(&pixels).to_vec();\n    Ok(image::ImageBuffer::from_raw(\n        width as u32,\n        height as u32,\n        pixels,\n    ))\n}\n\n/// A depth texture.\npub struct DepthTexture {\n    pub(crate) runtime: WgpuRuntime,\n    pub(crate) texture: Arc<wgpu::Texture>,\n}\n\nimpl Deref for DepthTexture {\n    type Target = wgpu::Texture;\n\n    fn deref(&self) -> &Self::Target {\n        &self.texture\n    }\n}\n\nimpl DepthTexture {\n    pub fn new(runtime: impl AsRef<WgpuRuntime>, texture: impl Into<Arc<wgpu::Texture>>) -> Self {\n        Self {\n            runtime: runtime.as_ref().clone(),\n            texture: texture.into(),\n        }\n    }\n\n    pub fn try_new_from(\n        runtime: impl AsRef<WgpuRuntime>,\n        value: Texture,\n    ) -> Result<Self, TextureError> {\n        let format = value.texture.format();\n        if format != wgpu::TextureFormat::Depth32Float {\n            return UnsupportedFormatSnafu { format }.fail();\n        }\n\n        Ok(Self {\n            runtime: runtime.as_ref().clone(),\n            texture: value.texture,\n        })\n    }\n\n    /// Converts the depth texture into an image.\n    ///\n    /// Assumes the format is single channel 32bit.\n    ///\n    /// ## Panics\n    /// This may panic if the depth texture has a multisample count greater than\n    /// 1.\n    pub async fn read_image(&self) -> Result<Option<image::GrayImage>> {\n        read_depth_texture_to_image(\n            &self.runtime,\n            self.width() as usize,\n            self.height() as usize,\n            &self.texture,\n        )\n        .await\n    }\n}\n\n/// Helper for retreiving an image from a texture.\n#[derive(Clone, Copy, Debug)]\npub struct BufferDimensions {\n    pub width: usize,\n    pub height: usize,\n    pub unpadded_bytes_per_row: usize,\n    pub padded_bytes_per_row: usize,\n}\n\nimpl BufferDimensions {\n    pub fn new(channels: usize, subpixel_bytes: usize, width: usize, height: usize) -> Self {\n        let bytes_per_pixel = channels * subpixel_bytes;\n        let unpadded_bytes_per_row = width * bytes_per_pixel;\n        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;\n        let padded_bytes_per_row_padding = (align - unpadded_bytes_per_row % align) % align;\n        let padded_bytes_per_row = unpadded_bytes_per_row + padded_bytes_per_row_padding;\n        Self {\n            width,\n            height,\n            unpadded_bytes_per_row,\n            padded_bytes_per_row,\n        }\n    }\n}\n\n/// A buffer that is being mapped.\n///\n/// This implements `Future<Output = Vec<u8>>`.\npub struct MappedBuffer<'a> {\n    waker: Arc<Mutex<Option<std::task::Waker>>>,\n    result: Arc<Mutex<Option<Result<(), wgpu::BufferAsyncError>>>>,\n    dimensions: BufferDimensions,\n    buffer_slice: wgpu::BufferSlice<'a>,\n}\n\nimpl std::future::Future for MappedBuffer<'_> {\n    type Output = Result<Vec<u8>, wgpu::BufferAsyncError>;\n\n    fn poll(\n        self: core::pin::Pin<&mut Self>,\n        cx: &mut core::task::Context<'_>,\n    ) -> core::task::Poll<Self::Output> {\n        let this = self.deref();\n        if let Some(result) = this.result.lock().expect(\"texture result lock\").take() {\n            std::task::Poll::Ready(result.map(|()| {\n                let padded_buffer = this.buffer_slice.get_mapped_range();\n                let mut unpadded_buffer = vec![];\n                // from the padded_buffer we write just the unpadded bytes into the\n                // unpadded_buffer\n                for chunk in padded_buffer.chunks(self.dimensions.padded_bytes_per_row) {\n                    unpadded_buffer\n                        .extend_from_slice(&chunk[..self.dimensions.unpadded_bytes_per_row]);\n                }\n                unpadded_buffer\n            }))\n        } else {\n            let waker = cx.waker().clone();\n            *this.waker.lock().expect(\"texture waker lock\") = Some(waker);\n            std::task::Poll::Pending\n        }\n    }\n}\n\n/// Helper for retreiving a rendered frame.\npub struct CopiedTextureBuffer {\n    pub format: wgpu::TextureFormat,\n    pub dimensions: BufferDimensions,\n    pub buffer: wgpu::Buffer,\n}\n\nimpl CopiedTextureBuffer {\n    /// Return a mapped buffer that can be `await`ed for data from the GPU.\n    fn get_mapped_buffer(&self) -> MappedBuffer<'_> {\n        let buffer_slice = self.buffer.slice(..);\n        let waker: Arc<Mutex<Option<std::task::Waker>>> = Default::default();\n        let result = Arc::new(Mutex::new(None));\n        buffer_slice.map_async(wgpu::MapMode::Read, {\n            let waker = waker.clone();\n            let result = result.clone();\n            move |res| {\n                let mut result = result.lock().expect(\"texture result lock\");\n                *result = Some(res);\n                if let Some(waker) = waker.lock().expect(\"texture waker lock\").take() {\n                    waker.wake();\n                }\n            }\n        });\n        MappedBuffer {\n            result,\n            waker,\n            buffer_slice,\n            dimensions: self.dimensions,\n        }\n    }\n\n    /// Access the raw unpadded pixels of the buffer.\n    ///\n    /// This calls `wgpu::Device::poll`.\n    pub async fn pixels(&self, device: &wgpu::Device) -> Result<Vec<u8>> {\n        let buffer = self.get_mapped_buffer();\n        device.poll(wgpu::PollType::Wait).context(PollSnafu)?;\n        buffer.await.context(BufferAsyncSnafu)\n    }\n\n    /// Convert the post render buffer into an RgbaImage.\n    pub async fn convert_to_rgba(self) -> Result<image::RgbaImage, TextureError> {\n        let fut_buffer = self.get_mapped_buffer();\n        let pixels = fut_buffer.await.context(BufferAsyncSnafu)?;\n        let mut img_buffer: image::ImageBuffer<image::Rgba<u8>, Vec<u8>> =\n            image::ImageBuffer::from_raw(\n                self.dimensions.width as u32,\n                self.dimensions.height as u32,\n                pixels,\n            )\n            .context(CouldNotConvertImageBufferSnafu)?;\n        if self.format.is_srgb() {\n            log::trace!(\"converting applying linear transfer to srgb pixels\");\n            // Convert back to linear\n            img_buffer.pixels_mut().for_each(|p| {\n                crate::color::linear_xfer_u8(&mut p.0[0]);\n                crate::color::linear_xfer_u8(&mut p.0[1]);\n                crate::color::linear_xfer_u8(&mut p.0[2]);\n                crate::color::linear_xfer_u8(&mut p.0[3]);\n            });\n        }\n        Ok(image::DynamicImage::ImageRgba8(img_buffer).to_rgba8())\n    }\n\n    /// Convert the post render buffer into an image.\n    ///\n    /// `Sp` is the sub-pixel type. eg, `u8` or `f32`\n    ///\n    /// `P` is the pixel type. eg, `Rgba<u8>` or `Luma<f32>`\n    pub async fn into_image<Sp, P>(\n        self,\n        device: &wgpu::Device,\n    ) -> Result<image::DynamicImage, TextureError>\n    where\n        Sp: bytemuck::AnyBitPattern,\n        P: image::Pixel<Subpixel = Sp>,\n        image::DynamicImage: From<image::ImageBuffer<P, Vec<Sp>>>,\n    {\n        let pixels = self.pixels(device).await?;\n        let coerced_pixels: &[Sp] = bytemuck::cast_slice(&pixels);\n        let img_buffer: image::ImageBuffer<P, Vec<Sp>> = image::ImageBuffer::from_raw(\n            self.dimensions.width as u32,\n            self.dimensions.height as u32,\n            coerced_pixels.to_vec(),\n        )\n        .context(CouldNotConvertImageBufferSnafu)?;\n        Ok(image::DynamicImage::from(img_buffer))\n    }\n\n    /// Convert the post render buffer into an internal-format [`AtlasImage`].\n    pub async fn into_atlas_image(self, device: &wgpu::Device) -> Result<AtlasImage, TextureError> {\n        let pixels = self.pixels(device).await?;\n        let img = AtlasImage {\n            pixels,\n            size: UVec2::new(self.dimensions.width as u32, self.dimensions.height as u32),\n            format: AtlasImageFormat::from_wgpu_texture_format(self.format).context(\n                UnsupportedFormatSnafu {\n                    format: self.format,\n                },\n            )?,\n            apply_linear_transfer: false,\n        };\n        Ok(img)\n    }\n\n    /// Convert the post render buffer into an RgbaImage.\n    ///\n    /// Ensures that the pixels are in the given color space by applying the\n    /// correct transfer function if needed.\n    ///\n    /// Assumes the texture is in `Rgba8` format.\n    pub async fn into_rgba(\n        self,\n        device: &wgpu::Device,\n        // `true` - the resulting image will be in a linear color space\n        // `false` - the resulting image will be in an sRGB color space\n        linear: bool,\n    ) -> Result<image::RgbaImage, TextureError> {\n        let format = self.format;\n        let mut img_buffer = self\n            .into_image::<u8, image::Rgba<u8>>(device)\n            .await?\n            .into_rgba8();\n        let linear_xfer = format.is_srgb() && linear;\n        let opto_xfer = !format.is_srgb() && !linear;\n        let should_xfer = linear_xfer || opto_xfer;\n\n        if should_xfer {\n            let f = if linear_xfer {\n                log::trace!(\n                    \"converting by applying linear transfer fn to srgb pixels (sRGB -> linear)\"\n                );\n                crate::color::linear_xfer_u8\n            } else {\n                log::trace!(\n                    \"converting by applying opto transfer fn to linear pixels (linear -> sRGB)\"\n                );\n                crate::color::opto_xfer_u8\n            };\n            // Convert back to linear\n            img_buffer.pixels_mut().for_each(|p| {\n                f(&mut p.0[0]);\n                f(&mut p.0[1]);\n                f(&mut p.0[2]);\n                f(&mut p.0[3]);\n            });\n        }\n\n        Ok(img_buffer)\n    }\n\n    /// Convert the post render buffer into an RgbaImage.\n    ///\n    /// Ensures that the pixels are in a linear color space by applying the\n    /// linear transfer if the texture this buffer was copied from was sRGB.\n    pub async fn into_linear_rgba(\n        self,\n        device: &wgpu::Device,\n    ) -> Result<image::RgbaImage, TextureError> {\n        let format = self.format;\n        let mut img_buffer = self\n            .into_image::<u8, image::Rgba<u8>>(device)\n            .await?\n            .into_rgba8();\n        if format.is_srgb() {\n            log::trace!(\n                \"converting by applying linear transfer fn to srgb pixels (sRGB -> linear)\"\n            );\n            // Convert back to linear\n            img_buffer.pixels_mut().for_each(|p| {\n                crate::color::linear_xfer_u8(&mut p.0[0]);\n                crate::color::linear_xfer_u8(&mut p.0[1]);\n                crate::color::linear_xfer_u8(&mut p.0[2]);\n                crate::color::linear_xfer_u8(&mut p.0[3]);\n            });\n        }\n\n        Ok(img_buffer)\n    }\n\n    /// Convert the post render buffer into an RgbaImage.\n    ///\n    /// Ensures that the pixels are in a sRGB color space by applying the\n    /// opto transfer function if the texture this buffer was copied from was\n    /// linear.\n    pub async fn into_srgba(self, device: &wgpu::Device) -> Result<image::RgbaImage, TextureError> {\n        let format = self.format;\n        let mut img_buffer = self\n            .into_image::<u8, image::Rgba<u8>>(device)\n            .await?\n            .into_rgba8();\n        if !format.is_srgb() {\n            log::warn!(\"converting by applying opto transfer fn to linear pixels (linear -> sRGB)\");\n            // Convert back to linear\n            img_buffer.pixels_mut().for_each(|p| {\n                crate::color::opto_xfer_u8(&mut p.0[0]);\n                crate::color::opto_xfer_u8(&mut p.0[1]);\n                crate::color::opto_xfer_u8(&mut p.0[2]);\n                crate::color::opto_xfer_u8(&mut p.0[3]);\n            });\n        }\n\n        Ok(img_buffer)\n    }\n\n    /// Read the texture from the GPU.\n    ///\n    /// To read the texture you must provide the width, height, the number of\n    /// color/alpha channels and the number of bytes in the underlying\n    /// subpixel type (usually u8=1, u16=2 or f32=4).\n    #[allow(clippy::too_many_arguments)]\n    pub fn read_from(\n        runtime: impl AsRef<WgpuRuntime>,\n        texture: &wgpu::Texture,\n        width: usize,\n        height: usize,\n        channels: usize,\n        subpixel_bytes: usize,\n        mip_level: u32,\n        origin: Option<wgpu::Origin3d>,\n    ) -> CopiedTextureBuffer {\n        let runtime = runtime.as_ref();\n        let device = &runtime.device;\n        let queue = &runtime.queue;\n        let dimensions = BufferDimensions::new(channels, subpixel_bytes, width, height);\n        // The output buffer lets us retrieve the self as an array\n        let buffer = device.create_buffer(&wgpu::BufferDescriptor {\n            label: Some(\"Texture::read buffer\"),\n            size: (dimensions.padded_bytes_per_row * dimensions.height) as u64,\n            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,\n            mapped_at_creation: false,\n        });\n        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {\n            label: Some(\"post render screen capture encoder\"),\n        });\n        let mut source = texture.as_image_copy();\n        source.mip_level = mip_level;\n        if let Some(origin) = origin {\n            source.origin = origin;\n        }\n        // Copy the data from the surface texture to the buffer\n        encoder.copy_texture_to_buffer(\n            source,\n            wgpu::TexelCopyBufferInfo {\n                buffer: &buffer,\n                layout: wgpu::TexelCopyBufferLayout {\n                    offset: 0,\n                    bytes_per_row: Some(dimensions.padded_bytes_per_row as u32),\n                    rows_per_image: None,\n                },\n            },\n            wgpu::Extent3d {\n                width: dimensions.width as u32,\n                height: dimensions.height as u32,\n                depth_or_array_layers: 1,\n            },\n        );\n        queue.submit(std::iter::once(encoder.finish()));\n\n        CopiedTextureBuffer {\n            dimensions,\n            buffer,\n            format: texture.format(),\n        }\n    }\n\n    /// Copy the entire texture into a buffer, at mip `0`.\n    ///\n    /// Attempts to figure out the parameters to\n    /// [`CopiedTextureBuffer::read_from`].\n    pub fn new(runtime: impl AsRef<WgpuRuntime>, texture: &wgpu::Texture) -> Result<Self> {\n        let (channels, subpixel_bytes) =\n            wgpu_texture_format_channels_and_subpixel_bytes(texture.format())?;\n        Ok(Self::read_from(\n            runtime,\n            texture,\n            texture.width() as usize,\n            texture.height() as usize,\n            channels as usize,\n            subpixel_bytes as usize,\n            0,\n            None,\n        ))\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use crate::{context::Context, test::BlockOnFuture, texture::CopiedTextureBuffer};\n\n    use super::Texture;\n\n    #[test]\n    fn generate_mipmaps() {\n        let r = Context::headless(10, 10).block();\n        let img = image::open(\"../../img/sandstone.png\").unwrap();\n        let width = img.width();\n        let height = img.height();\n        let mip_level_count = 5;\n        let mut texture = Texture::from_dynamic_image(\n            &r,\n            img,\n            Some(\"sandstone\"),\n            Some(\n                wgpu::TextureUsages::COPY_SRC\n                    | wgpu::TextureUsages::TEXTURE_BINDING\n                    | wgpu::TextureUsages::COPY_DST,\n            ),\n            1,\n        );\n        let mips = texture.generate_mips(&r, None, mip_level_count);\n\n        let (channels, subpixel_bytes) =\n            super::wgpu_texture_format_channels_and_subpixel_bytes_todo(texture.texture.format());\n        for (level, mip) in mips.into_iter().enumerate() {\n            let mip_level = level + 1;\n            let mip_width = width >> mip_level;\n            let mip_height = height >> mip_level;\n            // save out the mips\n            let copied_buffer = CopiedTextureBuffer::read_from(\n                &r,\n                &mip.texture,\n                mip_width as usize,\n                mip_height as usize,\n                channels as usize,\n                subpixel_bytes as usize,\n                0,\n                None,\n            );\n            let pixels = copied_buffer.pixels(r.get_device()).block().unwrap();\n            assert_eq!((mip_width * mip_height * 4) as usize, pixels.len());\n            let img: image::RgbaImage =\n                image::ImageBuffer::from_vec(mip_width, mip_height, pixels).unwrap();\n            let img = image::DynamicImage::from(img);\n            let img = img.to_rgba8();\n            img_diff::assert_img_eq(&format!(\"texture/sandstone_mip{mip_level}.png\"), img);\n        }\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/tonemapping/cpu.rs",
    "content": "//! Tonemapping.\nuse core::ops::Deref;\nuse craballoc::{\n    prelude::{Hybrid, SlabAllocator},\n    runtime::WgpuRuntime,\n};\nuse std::sync::{Arc, RwLock};\n\nuse crate::texture::Texture;\n\nuse super::TonemapConstants;\n\npub fn bindgroup_layout(device: &wgpu::Device, label: Option<&str>) -> wgpu::BindGroupLayout {\n    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n        label,\n        entries: &[\n            // slab\n            wgpu::BindGroupLayoutEntry {\n                binding: 0,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Buffer {\n                    ty: wgpu::BufferBindingType::Storage { read_only: true },\n                    has_dynamic_offset: false,\n                    min_binding_size: None,\n                },\n                count: None,\n            },\n            // hdr texture\n            wgpu::BindGroupLayoutEntry {\n                binding: 1,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Texture {\n                    sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                    view_dimension: wgpu::TextureViewDimension::D2,\n                    multisampled: false,\n                },\n                count: None,\n            },\n            // hdr sampler\n            wgpu::BindGroupLayoutEntry {\n                binding: 2,\n                visibility: wgpu::ShaderStages::FRAGMENT,\n                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                count: None,\n            },\n        ],\n    })\n}\n\npub fn create_bindgroup(\n    device: &wgpu::Device,\n    label: Option<&str>,\n    hdr_texture: &Texture,\n    slab_buffer: &wgpu::Buffer,\n) -> wgpu::BindGroup {\n    device.create_bind_group(&wgpu::BindGroupDescriptor {\n        label,\n        layout: &bindgroup_layout(device, label),\n        entries: &[\n            wgpu::BindGroupEntry {\n                binding: 0,\n                resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {\n                    buffer: slab_buffer,\n                    offset: 0,\n                    size: None,\n                }),\n            },\n            wgpu::BindGroupEntry {\n                binding: 1,\n                resource: wgpu::BindingResource::TextureView(&hdr_texture.view),\n            },\n            wgpu::BindGroupEntry {\n                binding: 2,\n                resource: wgpu::BindingResource::Sampler(&hdr_texture.sampler),\n            },\n        ],\n    })\n}\n\n/// Conducts HDR tone mapping.\n///\n/// Writes the HDR surface texture to the (most likely) sRGB window surface.\n///\n/// Clones of [`Tonemapping`] all reference the same internal data.\n///\n/// ## Note\n/// Only available on CPU. Not Available in shaders.\n#[derive(Clone)]\npub struct Tonemapping {\n    slab: SlabAllocator<WgpuRuntime>,\n    config: Hybrid<TonemapConstants>,\n    hdr_texture: Arc<RwLock<Texture>>,\n    bindgroup: Arc<RwLock<wgpu::BindGroup>>,\n    pipeline: Arc<wgpu::RenderPipeline>,\n}\n\nimpl Tonemapping {\n    pub fn new(\n        runtime: &WgpuRuntime,\n        frame_texture_format: wgpu::TextureFormat,\n        hdr_texture: &Texture,\n    ) -> Self {\n        let slab = SlabAllocator::new(runtime, \"tonemapping-slab\", wgpu::BufferUsages::empty());\n        let config = slab.new_value(TonemapConstants::default());\n\n        let label = Some(\"tonemapping\");\n        let slab_buffer = slab.commit();\n        let bindgroup = Arc::new(RwLock::new(create_bindgroup(\n            &runtime.device,\n            label,\n            hdr_texture,\n            &slab_buffer,\n        )));\n\n        let device = &runtime.device;\n        let vertex_linkage = crate::linkage::tonemapping_vertex::linkage(device);\n        let fragment_linkage = crate::linkage::tonemapping_fragment::linkage(device);\n        let hdr_layout = bindgroup_layout(device, label);\n        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label,\n            bind_group_layouts: &[&hdr_layout],\n            push_constant_ranges: &[],\n        });\n        let pipeline = Arc::new(\n            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n                label,\n                layout: Some(&layout),\n                vertex: wgpu::VertexState {\n                    module: &vertex_linkage.module,\n                    entry_point: Some(vertex_linkage.entry_point),\n                    buffers: &[],\n                    compilation_options: Default::default(),\n                },\n                primitive: wgpu::PrimitiveState {\n                    topology: wgpu::PrimitiveTopology::TriangleList,\n                    strip_index_format: None,\n                    front_face: wgpu::FrontFace::Ccw,\n                    cull_mode: Some(wgpu::Face::Back),\n                    unclipped_depth: false,\n                    polygon_mode: wgpu::PolygonMode::Fill,\n                    conservative: false,\n                },\n                depth_stencil: None,\n                fragment: Some(wgpu::FragmentState {\n                    module: &fragment_linkage.module,\n                    entry_point: Some(fragment_linkage.entry_point),\n                    targets: &[Some(wgpu::ColorTargetState {\n                        format: frame_texture_format,\n                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                        write_mask: wgpu::ColorWrites::ALL,\n                    })],\n                    compilation_options: Default::default(),\n                }),\n                multisample: wgpu::MultisampleState::default(),\n                multiview: None,\n                cache: None,\n            }),\n        );\n        Self {\n            slab,\n            config,\n            hdr_texture: Arc::new(RwLock::new(hdr_texture.clone())),\n            bindgroup,\n            pipeline,\n        }\n    }\n\n    pub(crate) fn slab_allocator(&self) -> &SlabAllocator<WgpuRuntime> {\n        &self.slab\n    }\n\n    pub fn set_hdr_texture(&self, device: &wgpu::Device, hdr_texture: &Texture) {\n        // UNWRAP: safe because the buffer is created in `Self::new` and guaranteed to\n        // exist\n        let slab_buffer = self.slab.get_buffer().unwrap();\n        let bindgroup = create_bindgroup(device, Some(\"tonemapping\"), hdr_texture, &slab_buffer);\n        // UNWRAP: not safe but we want to panic\n        *self.bindgroup.write().expect(\"tonemapping bindgroup write\") = bindgroup;\n        *self\n            .hdr_texture\n            .write()\n            .expect(\"tonemapping hdr_texture write\") = hdr_texture.clone();\n    }\n\n    pub fn get_tonemapping_config(&self) -> TonemapConstants {\n        self.config.get()\n    }\n\n    pub fn set_tonemapping_config(&self, config: TonemapConstants) {\n        self.config.set(config);\n    }\n\n    pub fn render(&self, device: &wgpu::Device, queue: &wgpu::Queue, view: &wgpu::TextureView) {\n        let label = Some(\"tonemapping render\");\n        assert!(!self.slab.commit().is_new_this_commit());\n\n        // UNWRAP: not safe but we want to panic\n        let bindgroup = self.bindgroup.read().expect(\"tonemapping bindgroup read\");\n        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label });\n        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n            label,\n            color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                view,\n                resolve_target: None,\n                ops: wgpu::Operations {\n                    load: wgpu::LoadOp::Load,\n                    store: wgpu::StoreOp::Store,\n                },\n                depth_slice: None,\n            })],\n            depth_stencil_attachment: None,\n            ..Default::default()\n        });\n        render_pass.set_pipeline(&self.pipeline);\n        render_pass.set_bind_group(0, Some(bindgroup.deref()), &[]);\n        let id = self.config.id().into();\n        render_pass.draw(0..6, id..id + 1);\n        drop(render_pass);\n\n        queue.submit(std::iter::once(encoder.finish()));\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/tonemapping.rs",
    "content": "//! Tonemapping from an HDR texture to SDR.\n//!\n//! ## References\n//! * <https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/5b1b7f48a8cb2b7aaef00d08fdba18ccc8dd331b/source/Renderer/shaders/tonemapping.glsl>\n//! * <https://64.github.io/tonemapping>\n\nuse crabslab::{Slab, SlabItem};\nuse glam::{mat3, Mat3, Vec2, Vec3, Vec4, Vec4Swizzles};\nuse spirv_std::{image::Image2d, spirv, Sampler};\n\n#[cfg(not(target_arch = \"spirv\"))]\nmod cpu;\n#[cfg(not(target_arch = \"spirv\"))]\npub use cpu::*;\n\nconst GAMMA: f32 = 2.2;\nconst INV_GAMMA: f32 = 1.0 / GAMMA;\n\n/// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT\nconst ACESINPUT_MAT: Mat3 = mat3(\n    Vec3::new(0.59719, 0.07600, 0.02840),\n    Vec3::new(0.35458, 0.90834, 0.13383),\n    Vec3::new(0.04823, 0.01566, 0.83777),\n);\n\n/// ODT_SAT => XYZ => D60_2_D65 => sRGB\nconst ACESOUTPUT_MAT: Mat3 = mat3(\n    Vec3::new(1.60475, -0.10208, -0.00327),\n    Vec3::new(-0.53108, 1.10813, -0.07276),\n    Vec3::new(-0.07367, -0.00605, 1.07602),\n);\n\n/// Linear to sRGB approximation.\n/// See <http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html>\npub fn linear_to_srgb(color: Vec3) -> Vec3 {\n    color.powf(INV_GAMMA)\n}\n\n/// sRGB to linear approximation.\n/// See <http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html>\npub fn srgb_to_linear(srgb_in: Vec3) -> Vec3 {\n    srgb_in.powf(GAMMA)\n}\n\n/// sRGB to linear approximation.\n/// See <http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html>\npub fn srgba_to_linear(srgb_in: Vec4) -> Vec4 {\n    srgb_to_linear(srgb_in.xyz()).extend(srgb_in.w)\n}\n\n/// ACES tone map (faster approximation)\n/// see: <https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve>\npub fn tone_map_aces_narkowicz(color: Vec3) -> Vec3 {\n    const A: f32 = 2.51;\n    const B: f32 = 0.03;\n    const C: f32 = 2.43;\n    const D: f32 = 0.59;\n    const E: f32 = 0.14;\n    let c = (color * (A * color + B)) / (color * (C * color + D) + E);\n    c.clamp(Vec3::ZERO, Vec3::ONE)\n}\n\n/// ACES filmic tone map approximation\n/// see <https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl>\nfn rrt_and_odtfit(color: Vec3) -> Vec3 {\n    let a: Vec3 = color * (color + 0.0245786) - 0.000090537;\n    let b: Vec3 = color * (0.983729 * color + 0.432951) + 0.238081;\n    a / b\n}\n\npub fn tone_map_aces_hill(mut color: Vec3) -> Vec3 {\n    color = ACESINPUT_MAT * color;\n    // Apply RRT and ODT\n    color = rrt_and_odtfit(color);\n    color = ACESOUTPUT_MAT * color;\n    // Clamp to [0, 1]\n    color = color.clamp(Vec3::ZERO, Vec3::ONE);\n\n    color\n}\n\npub fn tone_map_reinhard(color: Vec3) -> Vec3 {\n    color / (color + Vec3::ONE)\n}\n\n#[repr(transparent)]\n#[derive(Clone, Copy, Default, PartialEq, Eq, SlabItem, core::fmt::Debug)]\npub struct Tonemap(u32);\n\nimpl Tonemap {\n    pub const NONE: Self = Tonemap(0);\n    pub const ACES_NARKOWICZ: Self = Tonemap(1);\n    pub const ACES_HILL: Self = Tonemap(2);\n    pub const ACES_HILL_EXPOSURE_BOOST: Self = Tonemap(3);\n    pub const REINHARD: Self = Tonemap(4);\n}\n\n#[repr(C)]\n#[derive(Clone, Copy, PartialEq, SlabItem)]\npub struct TonemapConstants {\n    pub tonemap: Tonemap,\n    pub exposure: f32,\n}\n\nimpl Default for TonemapConstants {\n    fn default() -> Self {\n        Self {\n            tonemap: Tonemap::NONE,\n            exposure: 1.0,\n        }\n    }\n}\n\npub fn tonemap(mut color: Vec4, slab: &[u32]) -> Vec4 {\n    let constants = slab.read::<TonemapConstants>(0u32.into());\n    color *= constants.exposure;\n\n    match constants.tonemap {\n        Tonemap::ACES_NARKOWICZ => tone_map_aces_narkowicz(color.xyz()).extend(color.w),\n        Tonemap::ACES_HILL => tone_map_aces_hill(color.xyz()).extend(color.w),\n        Tonemap::ACES_HILL_EXPOSURE_BOOST => {\n            // boost exposure as discussed in https://github.com/mrdoob/three.js/pull/19621\n            // this factor is based on the exposure correction of Krzysztof Narkowicz in his\n            // implemetation of ACES tone mapping\n            tone_map_aces_hill(color.xyz() / 0.6).extend(color.w)\n        }\n        Tonemap::REINHARD => {\n            // Use Reinhard tone mapping\n            tone_map_reinhard(color.xyz()).extend(color.w)\n        }\n        _ => color,\n    }\n}\n\nconst QUAD_2D_POINTS: [(Vec2, Vec2); 6] = {\n    let tl = (Vec2::new(-1.0, 1.0), Vec2::new(0.0, 0.0));\n    let tr = (Vec2::new(1.0, 1.0), Vec2::new(1.0, 0.0));\n    let bl = (Vec2::new(-1.0, -1.0), Vec2::new(0.0, 1.0));\n    let br = (Vec2::new(1.0, -1.0), Vec2::new(1.0, 1.0));\n    [tl, bl, br, tl, br, tr]\n};\n\n#[spirv(vertex)]\npub fn tonemapping_vertex(\n    #[spirv(vertex_index)] vertex_id: u32,\n    out_uv: &mut glam::Vec2,\n    #[spirv(position)] gl_pos: &mut glam::Vec4,\n) {\n    let (pos, uv) = QUAD_2D_POINTS[vertex_id as usize];\n    *out_uv = uv;\n    *gl_pos = pos.extend(0.0).extend(1.0);\n}\n\n#[spirv(fragment)]\npub fn tonemapping_fragment(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n    #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,\n    #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,\n    in_uv: glam::Vec2,\n    output: &mut glam::Vec4,\n) {\n    let color: Vec4 = texture.sample(*sampler, in_uv);\n    let color = tonemap(color, slab);\n    *output = color;\n}\n"
  },
  {
    "path": "crates/renderling/src/transform/cpu.rs",
    "content": "//! CPU side of transform.\n\nuse std::sync::{Arc, RwLock};\n\nuse craballoc::{runtime::IsRuntime, slab::SlabAllocator, value::Hybrid};\nuse crabslab::Id;\nuse glam::{Mat4, Quat, Vec3};\n\nuse super::shader::TransformDescriptor;\n\n/// A decomposed 3d transformation.\n#[derive(Clone, Debug)]\npub struct Transform {\n    pub(crate) descriptor: Hybrid<TransformDescriptor>,\n}\n\nimpl From<&Transform> for Transform {\n    fn from(value: &Transform) -> Self {\n        value.clone()\n    }\n}\n\nimpl Transform {\n    /// Stage a new transform on the GPU.\n    pub(crate) fn new(slab: &SlabAllocator<impl IsRuntime>) -> Self {\n        let descriptor = slab.new_value(TransformDescriptor::default());\n        Self { descriptor }\n    }\n\n    /// Return a pointer to the underlying descriptor data on the GPU.\n    pub fn id(&self) -> Id<TransformDescriptor> {\n        self.descriptor.id()\n    }\n\n    /// Return the a copy of the underlying descriptor.\n    pub fn descriptor(&self) -> TransformDescriptor {\n        self.descriptor.get()\n    }\n\n    /// Set the descriptor.\n    pub fn set_descriptor(&self, descriptor: TransformDescriptor) -> &Self {\n        self.descriptor.set(descriptor);\n        self\n    }\n\n    /// Set the descriptor and return the `Transform`.\n    pub fn with_descriptor(self, descriptor: TransformDescriptor) -> Self {\n        self.set_descriptor(descriptor);\n        self\n    }\n\n    /// Return the transform in combined matrix format;\n    pub fn as_mat4(&self) -> Mat4 {\n        self.descriptor().into()\n    }\n\n    /// Get the translation of the transform.\n    pub fn translation(&self) -> Vec3 {\n        self.descriptor.get().translation\n    }\n\n    /// Modify the translation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `f`: A closure that takes a mutable reference to the translation\n    ///   vector and returns a value of type `T`.\n    pub fn modify_translation<T: 'static>(&self, f: impl FnOnce(&mut Vec3) -> T) -> T {\n        self.descriptor.modify(|t| f(&mut t.translation))\n    }\n\n    /// Set the translation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `translation`: A 3d translation vector `Vec3`.\n    pub fn set_translation(&self, translation: impl Into<Vec3>) -> &Self {\n        self.descriptor\n            .modify(|t| t.translation = translation.into());\n        self\n    }\n\n    /// Set the translation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `translation`: A 3d translation vector `Vec3`.\n    pub fn with_translation(self, translation: impl Into<Vec3>) -> Self {\n        self.set_translation(translation);\n        self\n    }\n\n    /// Get the rotation of the transform.\n    pub fn rotation(&self) -> Quat {\n        self.descriptor.get().rotation\n    }\n\n    /// Modify the rotation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `f`: A closure that takes a mutable reference to the rotation\n    ///   quaternion and returns a value of type `T`.\n    pub fn modify_rotation<T: 'static>(&self, f: impl FnOnce(&mut Quat) -> T) -> T {\n        self.descriptor.modify(|t| f(&mut t.rotation))\n    }\n\n    /// Set the rotation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `rotation`: A quaternion representing the rotation.\n    pub fn set_rotation(&self, rotation: impl Into<Quat>) -> &Self {\n        self.descriptor.modify(|t| t.rotation = rotation.into());\n        self\n    }\n\n    /// Set the rotation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `rotation`: A quaternion representing the rotation.\n    pub fn with_rotation(self, rotation: impl Into<Quat>) -> Self {\n        self.set_rotation(rotation);\n        self\n    }\n\n    /// Get the scale of the transform.\n    pub fn scale(&self) -> Vec3 {\n        self.descriptor.get().scale\n    }\n\n    /// Modify the scale of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `f`: A closure that takes a mutable reference to the scale vector and\n    ///   returns a value of type `T`.\n    pub fn modify_scale<T: 'static>(&self, f: impl FnOnce(&mut Vec3) -> T) -> T {\n        self.descriptor.modify(|t| f(&mut t.scale))\n    }\n\n    /// Set the scale of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `scale`: A 3d scale vector `Vec3`.\n    pub fn set_scale(&self, scale: impl Into<Vec3>) -> &Self {\n        self.descriptor.modify(|t| t.scale = scale.into());\n        self\n    }\n\n    /// Set the scale of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `scale`: A 3d scale vector `Vec3`.\n    pub fn with_scale(self, scale: impl Into<Vec3>) -> Self {\n        self.set_scale(scale);\n        self\n    }\n}\n\n/// Manages scene heirarchy on the [`Stage`](crate::stage::Stage).\n///\n/// Can be created with\n/// [`Stage::new_nested_transform`](crate::stage::Stage::new_nested_transform).\n///\n/// Clones all reference the same nested transform.\n#[derive(Clone)]\npub struct NestedTransform {\n    pub(crate) global_transform: Transform,\n    local_transform: Arc<RwLock<TransformDescriptor>>,\n    children: Arc<RwLock<Vec<NestedTransform>>>,\n    parent: Arc<RwLock<Option<NestedTransform>>>,\n}\n\nimpl core::fmt::Debug for NestedTransform {\n    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n        let children = self\n            .children\n            .read()\n            .unwrap()\n            .iter()\n            .map(|nt| nt.global_transform.id())\n            .collect::<Vec<_>>();\n        let parent = self\n            .parent\n            .read()\n            .unwrap()\n            .as_ref()\n            .map(|nt| nt.global_transform.id());\n        f.debug_struct(\"NestedTransform\")\n            .field(\"local_transform\", &self.local_transform)\n            .field(\"children\", &children)\n            .field(\"parent\", &parent)\n            .finish()\n    }\n}\n\nimpl From<&NestedTransform> for Transform {\n    fn from(value: &NestedTransform) -> Self {\n        value.global_transform.clone()\n    }\n}\n\nimpl From<NestedTransform> for Transform {\n    fn from(value: NestedTransform) -> Self {\n        value.global_transform\n    }\n}\n\nimpl NestedTransform {\n    /// Stage a new hierarchical transform on the GPU.\n    pub(crate) fn new(slab: &SlabAllocator<impl IsRuntime>) -> Self {\n        let nested = NestedTransform {\n            local_transform: Arc::new(RwLock::new(TransformDescriptor::default())),\n            global_transform: Transform::new(slab),\n            children: Default::default(),\n            parent: Default::default(),\n        };\n        nested.mark_dirty();\n        nested\n    }\n\n    /// Get the _local_ translation of the transform.\n    pub fn local_translation(&self) -> Vec3 {\n        self.local_transform\n            .read()\n            .expect(\"local_transform read\")\n            .translation\n    }\n\n    /// Modify the _local_ translation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `f`: A closure that takes a mutable reference to the translation\n    ///   vector and returns a value of type `T`.\n    pub fn modify_local_translation<T>(&self, f: impl FnOnce(&mut Vec3) -> T) -> T {\n        let t = {\n            let mut local_transform = self.local_transform.write().expect(\"local_transform write\");\n            f(&mut local_transform.translation)\n        };\n        self.mark_dirty();\n        t\n    }\n\n    /// Set the _local_ translation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `translation`: A 3d translation vector `Vec3`.\n    pub fn set_local_translation(&self, translation: impl Into<Vec3>) -> &Self {\n        self.local_transform\n            .write()\n            .expect(\"local_transform write\")\n            .translation = translation.into();\n        self.mark_dirty();\n        self\n    }\n\n    /// Set the _local_ translation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `translation`: A 3d translation vector `Vec3`.\n    pub fn with_local_translation(self, translation: impl Into<Vec3>) -> Self {\n        self.set_local_translation(translation);\n        self\n    }\n\n    /// Get the _local_ rotation of the transform.\n    pub fn local_rotation(&self) -> Quat {\n        self.local_transform\n            .read()\n            .expect(\"local_transform read\")\n            .rotation\n    }\n\n    /// Modify the _local_ rotation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `f`: A closure that takes a mutable reference to the rotation\n    ///   quaternion and returns a value of type `T`.\n    pub fn modify_local_rotation<T>(&self, f: impl FnOnce(&mut Quat) -> T) -> T {\n        let t = {\n            let mut local_transform = self.local_transform.write().expect(\"local_transform write\");\n            f(&mut local_transform.rotation)\n        };\n        self.mark_dirty();\n        t\n    }\n\n    /// Set the _local_ rotation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `rotation`: A quaternion representing the rotation.\n    pub fn set_local_rotation(&self, rotation: impl Into<Quat>) -> &Self {\n        self.local_transform\n            .write()\n            .expect(\"local_transform write\")\n            .rotation = rotation.into();\n        self.mark_dirty();\n        self\n    }\n\n    /// Set the _local_ rotation of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `rotation`: A quaternion representing the rotation.\n    pub fn with_local_rotation(self, rotation: impl Into<Quat>) -> Self {\n        self.set_local_rotation(rotation);\n        self\n    }\n\n    /// Get the _local_ scale of the transform.\n    pub fn local_scale(&self) -> Vec3 {\n        self.local_transform\n            .read()\n            .expect(\"local_transform read\")\n            .scale\n    }\n\n    /// Modify the _local_ scale of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `f`: A closure that takes a mutable reference to the scale vector and\n    ///   returns a value of type `T`.\n    pub fn modify_local_scale<T>(&self, f: impl FnOnce(&mut Vec3) -> T) -> T {\n        let t = {\n            let mut local_transform = self.local_transform.write().expect(\"local_transform write\");\n            f(&mut local_transform.scale)\n        };\n        self.mark_dirty();\n        t\n    }\n\n    /// Set the _local_ scale of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `scale`: A 3d scale vector `Vec3`.\n    pub fn set_local_scale(&self, scale: impl Into<Vec3>) -> &Self {\n        self.local_transform\n            .write()\n            .expect(\"local_transform write\")\n            .scale = scale.into();\n        self.mark_dirty();\n        self\n    }\n\n    /// Set the _local_ scale of the transform.\n    ///\n    /// # Arguments\n    ///\n    /// - `scale`: A 3d scale vector `Vec3`.\n    pub fn with_local_scale(self, scale: impl Into<Vec3>) -> Self {\n        self.set_local_scale(scale);\n        self\n    }\n\n    /// Return a pointer to the underlying descriptor data on the GPU.\n    ///\n    /// The descriptor is the descriptor that describes the _global_ transform.\n    pub fn global_id(&self) -> Id<TransformDescriptor> {\n        self.global_transform.id()\n    }\n\n    /// Return the descriptor of the _global_ transform.\n    ///\n    /// This traverses the heirarchy and computes the result.\n    pub fn global_descriptor(&self) -> TransformDescriptor {\n        let maybe_parent_guard = self.parent.read().expect(\"parent read\");\n        let transform = self.local_descriptor();\n        let parent_transform = maybe_parent_guard\n            .as_ref()\n            .map(|parent| parent.global_descriptor())\n            .unwrap_or_default();\n        TransformDescriptor::from(Mat4::from(parent_transform) * Mat4::from(transform))\n    }\n\n    /// Return the descriptor of the _local_ tarnsform.\n    pub fn local_descriptor(&self) -> TransformDescriptor {\n        *self.local_transform.read().expect(\"local_transform read\")\n    }\n\n    fn mark_dirty(&self) {\n        self.global_transform\n            .descriptor\n            .set(self.global_descriptor());\n        for child in self.children.read().expect(\"children read\").iter() {\n            child.mark_dirty();\n        }\n    }\n\n    /// Get a vector containing all the hierarchy's transforms.\n    ///\n    /// Starts with the root transform and ends with the local transform.\n    pub fn hierarchy(&self) -> Vec<TransformDescriptor> {\n        let mut transforms = vec![];\n        if let Some(parent) = self.parent() {\n            transforms.extend(parent.hierarchy());\n        }\n        transforms.push(self.local_descriptor());\n        transforms\n    }\n\n    pub fn add_child(&self, node: &NestedTransform) {\n        *node.parent.write().expect(\"parent write\") = Some(self.clone());\n        node.mark_dirty();\n        self.children\n            .write()\n            .expect(\"children write\")\n            .push(node.clone());\n    }\n\n    pub fn remove_child(&self, node: &NestedTransform) {\n        self.children\n            .write()\n            .expect(\"children write\")\n            .retain_mut(|child| {\n                if child.global_transform.id() == node.global_transform.id() {\n                    node.mark_dirty();\n                    let _ = node.parent.write().expect(\"parent write\").take();\n                    false\n                } else {\n                    true\n                }\n            });\n    }\n\n    /// Return a clone of the parent `NestedTransform`, if any.\n    pub fn parent(&self) -> Option<NestedTransform> {\n        self.parent.read().expect(\"parent read\").clone()\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/transform.rs",
    "content": "//! Decomposed 3d transforms and hierarchies.\n\n#[cfg(cpu)]\nmod cpu;\n#[cfg(cpu)]\npub use cpu::*;\n\npub mod shader {\n    use crabslab::SlabItem;\n    use glam::{Mat4, Quat, Vec3};\n\n    use crate::math::IsMatrix;\n\n    #[derive(Clone, Copy, PartialEq, SlabItem, core::fmt::Debug)]\n    /// A GPU descriptor of a decomposed transformation.\n    ///\n    /// `TransformDescriptor` can be converted to/from [`Mat4`].\n    pub struct TransformDescriptor {\n        pub translation: Vec3,\n        pub rotation: Quat,\n        pub scale: Vec3,\n    }\n\n    impl Default for TransformDescriptor {\n        fn default() -> Self {\n            Self::IDENTITY\n        }\n    }\n\n    impl From<Mat4> for TransformDescriptor {\n        fn from(value: Mat4) -> Self {\n            let (scale, rotation, translation) = value.to_scale_rotation_translation_or_id();\n            TransformDescriptor {\n                translation,\n                rotation,\n                scale,\n            }\n        }\n    }\n\n    impl From<TransformDescriptor> for Mat4 {\n        fn from(\n            TransformDescriptor {\n                translation,\n                rotation,\n                scale,\n            }: TransformDescriptor,\n        ) -> Self {\n            Mat4::from_scale_rotation_translation(scale, rotation, translation)\n        }\n    }\n\n    impl TransformDescriptor {\n        pub const IDENTITY: Self = TransformDescriptor {\n            translation: Vec3::ZERO,\n            rotation: Quat::IDENTITY,\n            scale: Vec3::ONE,\n        };\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use crabslab::*;\n    use glam::{Quat, Vec3};\n\n    use crate::transform::shader::TransformDescriptor;\n\n    #[test]\n    fn transform_roundtrip() {\n        assert_eq!(3, Vec3::SLAB_SIZE, \"unexpected Vec3 slab size\");\n        assert_eq!(4, Quat::SLAB_SIZE, \"unexpected Quat slab size\");\n        assert_eq!(10, TransformDescriptor::SLAB_SIZE);\n        let t = TransformDescriptor::default();\n        let mut slab = CpuSlab::new(vec![]);\n        let t_id = slab.append(&t);\n        pretty_assertions::assert_eq!(\n            &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],\n            bytemuck::cast_slice::<u32, f32>(slab.as_ref().as_slice())\n        );\n        pretty_assertions::assert_eq!(t, slab.read(t_id));\n    }\n}\n"
  },
  {
    "path": "crates/renderling/src/tutorial/implicit_isosceles_vertex.wgsl",
    "content": "struct VertexOutput {\n    @location(0) color: vec4<f32>,\n    @builtin(position) clip_pos: vec4<f32>,\n}\n@vertex \nfn main(@builtin(vertex_index) index: u32) -> VertexOutput {\n    let x = f32(1i - bitcast<i32>(index)) * 0.5f;\n    let y = (f32(index & 1u) * 2f - 1f) * 0.5f;\n    let position = vec4<f32>(x, y, 0f, 1f);\n\n    let color = vec4<f32>(1f, 0f, 0f, 1f);\n    return VertexOutput(color, position);\n}\n"
  },
  {
    "path": "crates/renderling/src/tutorial/passthru.wgsl",
    "content": "// Pass-through fragment shader that copies in color to out.\n@fragment\nfn main(@location(0) color:vec4<f32>) -> @location(0) vec4<f32> {\n    return color;\n}\n"
  },
  {
    "path": "crates/renderling/src/tutorial.rs",
    "content": "//! Shaders used in the contributor intro tutorial and in WASM tests.\n\nuse crabslab::{Array, Id, Slab, SlabItem};\nuse glam::{Vec3, Vec3Swizzles, Vec4};\nuse spirv_std::spirv;\n\nuse crate::{\n    geometry::{shader::GeometryDescriptor, Vertex},\n    primitive::shader::{PrimitiveDescriptor, VertexInfo},\n};\n\n/// Simple fragment shader that writes the input color to the output color.\n// Inline pragma needed so this shader doesn't get optimized away:\n// See <https://github.com/Rust-GPU/rust-gpu/issues/185#issuecomment-2661663722>\n#[inline(never)]\n#[spirv(fragment)]\npub fn passthru_fragment(in_color: Vec4, output: &mut Vec4) {\n    *output = in_color;\n}\n\n/// Simple vertex shader with an implicit isosceles triangle.\n///\n/// This shader gets run with three indices and draws a triangle without\n/// using any other data from the CPU.\n#[spirv(vertex)]\npub fn implicit_isosceles_vertex(\n    // Which vertex within the render unit are we rendering\n    #[spirv(vertex_index)] vertex_index: u32,\n\n    out_color: &mut Vec4,\n    #[spirv(position)] clip_pos: &mut Vec4,\n) {\n    let pos = {\n        let x = (1 - vertex_index as i32) as f32 * 0.5;\n        let y = (((vertex_index & 1) as f32 * 2.0) - 1.0) * 0.5;\n        Vec4::new(x, y, 0.0, 1.0)\n    };\n    *out_color = Vec4::new(1.0, 0.0, 0.0, 1.0);\n    *clip_pos = pos;\n}\n\n/// This shader uses the vertex index as a slab [`Id`]. The [`Id`] is used to\n/// read the vertex from the slab. The vertex's position and color are written\n/// to the output.\n#[spirv(vertex)]\npub fn slabbed_vertices_no_instance(\n    // Which vertex within the render unit are we rendering\n    #[spirv(vertex_index)] vertex_index: u32,\n\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n\n    out_color: &mut Vec4,\n    #[spirv(position)] clip_pos: &mut Vec4,\n) {\n    let vertex_id = Id::<Vertex>::from(vertex_index as usize * Vertex::SLAB_SIZE);\n    let vertex = slab.read(vertex_id);\n    *clip_pos = vertex.position.extend(1.0);\n    *out_color = vertex.color;\n}\n\n/// This shader uses the `instance_index` as a slab [`Id`].\n/// The `instance_index` is the [`Id`] of an [`Array`] of [`Vertex`]s. The\n/// `vertex_index` is the index of a [`Vertex`] within the [`Array`].\n#[spirv(vertex)]\npub fn slabbed_vertices(\n    // Id of the array of vertices we are rendering\n    #[spirv(instance_index)] array_id: Id<Array<(Vec3, Vec4)>>,\n    // Which vertex within the render unit are we rendering\n    #[spirv(vertex_index)] vertex_index: u32,\n\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n\n    out_color: &mut Vec4,\n    #[spirv(position)] clip_pos: &mut Vec4,\n) {\n    let array = slab.read(array_id);\n    let vertex_id = array.at(vertex_index as usize);\n    let (position, color) = slab.read(vertex_id);\n    *clip_pos = position.extend(1.0);\n    *out_color = color;\n}\n\n/// This shader uses the `instance_index` as a slab id.\n/// The `instance_index` is the `id` of a [`PrimitiveDescriptor`].\n/// The [`PrimitiveDescriptor`] contains an [`Array`] of [`Vertex`]s\n/// as its mesh, the [`Id`]s of a\n/// [`MaterialDescriptor`](crate::material::shader::MaterialDescriptor) and\n///[`CameraDescriptor`](crate::camera::shader::CameraDescriptor),\n/// and TRS transforms.\n/// The `vertex_index` is the index of a [`Vertex`] within the\n/// [`PrimitiveDescriptor`]'s `vertices` [`Array`].\n#[spirv(vertex)]\npub fn slabbed_renderlet(\n    // Id of the array of vertices we are rendering\n    #[spirv(instance_index)] primitive_id: Id<PrimitiveDescriptor>,\n    // Which vertex within the render unit are we rendering\n    #[spirv(vertex_index)] vertex_index: u32,\n\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n\n    out_color: &mut Vec4,\n    #[spirv(position)] clip_pos: &mut Vec4,\n) {\n    let prim = slab.read(primitive_id);\n    let VertexInfo {\n        vertex,\n        model_matrix,\n        ..\n    } = prim.get_vertex_info(vertex_index, slab);\n    let camera_id =\n        slab.read_unchecked(prim.geometry_descriptor_id + GeometryDescriptor::OFFSET_OF_CAMERA_ID);\n    let camera = slab.read(camera_id);\n    *clip_pos = camera.view_projection() * model_matrix * vertex.position.xyz().extend(1.0);\n    *out_color = vertex.color;\n}\n"
  },
  {
    "path": "crates/renderling/src/types.rs",
    "content": "//! Type level machinery.\n\nuse craballoc::value::{GpuArrayContainer, GpuContainer, HybridArrayContainer, HybridContainer};\n\n/// Specifies that a staged value has been unloaded from the CPU\n/// and now lives solely on the GPU.\npub type GpuOnly = GpuContainer;\n\n/// Specifies that a contiguous array of staged values has been\n/// unloaded from the CPU and now lives solely on the GPU.\npub type GpuOnlyArray = GpuArrayContainer;\n\n/// Specifies that a staged value lives on both the CPU and GPU,\n/// with the CPU value being a synchronized copy of the GPU value.\n///\n/// Currently updates flow from the CPU to the GPU, but not back.\npub type GpuCpu = HybridContainer;\n\n/// Specifies that a contiguous array of staged values lives on both\n/// the CPU and GPU, with the CPU values being synchronized copies\n/// of the GPU values.\n///\n/// Currently updates flow from the CPU to the GPU, but not back.\npub type GpuCpuArray = HybridArrayContainer;\n"
  },
  {
    "path": "crates/renderling/src/ui_slab/mod.rs",
    "content": "//! Shared types for the 2D/UI rendering pipeline.\n//!\n//! These types are used by both the CPU (renderling-ui crate) and the GPU\n//! (shader entry points in this crate). They are stored in a GPU slab buffer\n//! and read by the UI vertex and fragment shaders.\n\nuse crabslab::{Id, SlabItem};\nuse glam::{Mat4, UVec2, Vec2, Vec4};\n\nuse crate::atlas::shader::{AtlasDescriptor, AtlasTextureDescriptor};\n\npub mod shader;\n\n\n/// Identifies what kind of UI element is being rendered.\n///\n/// Used by the fragment shader to select the appropriate SDF / sampling logic.\n#[derive(Clone, Copy, Default, PartialEq, SlabItem, core::fmt::Debug)]\n#[repr(u32)]\npub enum UiElementType {\n    /// A rectangle (optionally rounded).\n    #[default]\n    Rectangle = 0,\n    /// A circle.\n    Circle = 1,\n    /// An ellipse.\n    Ellipse = 2,\n    /// A textured quad (atlas texture sampling).\n    Image = 3,\n    /// A text glyph quad (glyph atlas sampling).\n    TextGlyph = 4,\n    /// A pre-tessellated path triangle (uses vertex color directly).\n    Path = 5,\n}\n\nimpl UiElementType {\n    pub fn from_u32(v: u32) -> Self {\n        match v {\n            0 => Self::Rectangle,\n            1 => Self::Circle,\n            2 => Self::Ellipse,\n            3 => Self::Image,\n            4 => Self::TextGlyph,\n            5 => Self::Path,\n            _ => Self::Rectangle,\n        }\n    }\n}\n\n/// Identifies the type of gradient fill.\n#[derive(Clone, Copy, Default, PartialEq, core::fmt::Debug)]\n#[repr(u32)]\npub enum GradientType {\n    /// No gradient; use solid fill color.\n    #[default]\n    None = 0,\n    /// Linear gradient from `start` to `end`.\n    Linear = 1,\n    /// Radial gradient from `center` outward.\n    Radial = 2,\n}\n\nimpl GradientType {\n    pub fn from_u32(v: u32) -> Self {\n        match v {\n            0 => Self::None,\n            1 => Self::Linear,\n            2 => Self::Radial,\n            _ => Self::None,\n        }\n    }\n}\n\n/// Describes a gradient fill for a UI element.\n///\n/// Stored on the GPU slab.\n#[derive(Clone, Copy, Default, PartialEq, SlabItem, core::fmt::Debug)]\npub struct GradientDescriptor {\n    /// The type of gradient (None, Linear, Radial).\n    pub gradient_type: u32,\n    /// For linear: start point (in element-local 0..1 space).\n    /// For radial: center point.\n    pub start: Vec2,\n    /// For linear: end point.\n    /// For radial: unused.\n    pub end: Vec2,\n    /// For radial: the radius. For linear: unused.\n    pub radius: f32,\n    /// Color at the start (or center for radial).\n    pub color_start: Vec4,\n    /// Color at the end (or edge for radial).\n    pub color_end: Vec4,\n}\n\n/// Per-vertex data for the 2D/UI pipeline.\n///\n/// This is a lightweight vertex type (32 bytes) compared to the 3D\n/// `Vertex` (~160 bytes).\n#[derive(Clone, Copy, Default, PartialEq, SlabItem, core::fmt::Debug)]\npub struct UiVertex {\n    /// Screen-space position (x, y).\n    pub position: Vec2,\n    /// UV coordinates (for texture sampling or SDF evaluation).\n    pub uv: Vec2,\n    /// Per-vertex RGBA color.\n    pub color: Vec4,\n}\n\nimpl UiVertex {\n    pub fn with_position(mut self, position: impl Into<Vec2>) -> Self {\n        self.position = position.into();\n        self\n    }\n\n    pub fn with_uv(mut self, uv: impl Into<Vec2>) -> Self {\n        self.uv = uv.into();\n        self\n    }\n\n    pub fn with_color(mut self, color: impl Into<Vec4>) -> Self {\n        self.color = color.into();\n        self\n    }\n}\n\n/// Describes a single 2D UI element on the GPU.\n///\n/// This is the per-instance data stored in the GPU slab.\n/// The vertex shader reads this to generate quad corners,\n/// and the fragment shader reads it to evaluate SDF shapes,\n/// gradients, textures, etc.\n#[derive(Clone, Copy, Default, PartialEq, SlabItem, core::fmt::Debug)]\npub struct UiDrawCallDescriptor {\n    /// The type of element (Rectangle, Circle, Ellipse, Image, TextGlyph,\n    /// Path).\n    pub element_type: UiElementType,\n    /// Position of the element's top-left corner in screen space.\n    pub position: Vec2,\n    /// Size of the element in screen pixels (width, height).\n    pub size: Vec2,\n    /// Per-corner radii for rounded rectangles (top-left, top-right,\n    /// bottom-right, bottom-left).\n    pub corner_radii: Vec4,\n    /// Border width in pixels. 0 means no border.\n    pub border_width: f32,\n    /// Border color (RGBA).\n    pub border_color: Vec4,\n    /// Fill color (RGBA). Used when gradient_type is None.\n    pub fill_color: Vec4,\n    /// Gradient fill descriptor.\n    pub gradient: GradientDescriptor,\n    /// ID of the atlas texture descriptor on the slab.\n    ///\n    /// For `Image` and `TextGlyph` elements: points to an\n    /// `AtlasTextureDescriptor`.\n    /// For `Path` elements: when not `Id::NONE`, points to an\n    /// `AtlasTextureDescriptor` for image-filled paths.\n    /// Set to `Id::NONE` when unused.\n    pub atlas_texture_id: Id<AtlasTextureDescriptor>,\n    /// ID of the atlas descriptor on the slab.\n    ///\n    /// For `Path` elements: repurposed to store the slab offset of\n    /// the first `UiVertex` (via `Id::new(offset)`).\n    /// Set to `Id::NONE` when unused.\n    pub atlas_descriptor_id: Id<AtlasDescriptor>,\n    /// Scissor/clip rectangle (x, y, width, height).\n    /// Reserved for future use — not currently enforced by the shader\n    /// or renderer. Set to (0, 0, viewport_w, viewport_h) by default.\n    pub clip_rect: Vec4,\n    /// Element opacity (0.0 = fully transparent, 1.0 = fully opaque).\n    /// Multiplied with the final alpha.\n    pub opacity: f32,\n    /// Z-depth for sorting (painter's algorithm). Lower values are drawn\n    /// first (further back).\n    pub z: f32,\n}\n\n/// Camera/viewport descriptor for the 2D UI pipeline.\n///\n/// Contains the orthographic projection matrix, viewport dimensions,\n/// and atlas texture dimensions.\n#[derive(Clone, Copy, Default, PartialEq, SlabItem, core::fmt::Debug)]\npub struct UiViewport {\n    /// Orthographic projection matrix (typically top-left origin, +Y down).\n    pub projection: Mat4,\n    /// Viewport size in pixels.\n    pub size: UVec2,\n    /// Atlas texture size in pixels.\n    pub atlas_size: UVec2,\n}\n"
  },
  {
    "path": "crates/renderling/src/ui_slab/shader.rs",
    "content": "//! GPU shader entry points for the 2D/UI rendering pipeline.\n//!\n//! These shaders are compiled via rust-gpu and used by the `renderling-ui`\n//! crate's `UiRenderer`.\n\nuse crabslab::{Id, Slab, SlabItem};\nuse glam::{Vec2, Vec4, Vec4Swizzles};\nuse spirv_std::{image::Image2dArray, spirv, Sampler};\n\nuse super::{GradientType, UiDrawCallDescriptor, UiElementType, UiVertex, UiViewport};\nuse crate::atlas::shader::AtlasTextureDescriptor;\n\n/// SDF for a rounded rectangle.\n///\n/// `p` is the point relative to the rectangle center.\n/// `half_ext` is the half-extents of the rectangle.\n/// `radii` are the corner radii: (top-left, top-right, bottom-right,\n/// bottom-left).\nfn sdf_rounded_rect(p: Vec2, half_ext: Vec2, radii: Vec4) -> f32 {\n    // Select the appropriate corner radius based on quadrant.\n    let r = if p.x > 0.0 {\n        if p.y > 0.0 {\n            // bottom-right (in screen coords, +Y is down)\n            radii.z\n        } else {\n            // top-right\n            radii.y\n        }\n    } else if p.y > 0.0 {\n        // bottom-left\n        radii.w\n    } else {\n        // top-left\n        radii.x\n    };\n    let q = p.abs() - half_ext + Vec2::splat(r);\n    let outside = q.max(Vec2::ZERO).length();\n    let inside = q.x.max(q.y).min(0.0);\n    outside + inside - r\n}\n\n/// SDF for a circle.\nfn sdf_circle(p: Vec2, radius: f32) -> f32 {\n    p.length() - radius\n}\n\n/// SDF for an ellipse (approximation using the Iq formula).\nfn sdf_ellipse(p: Vec2, radii: Vec2) -> f32 {\n    // Simplified ellipse SDF (not exact but good for UI).\n    let p_norm = p / radii;\n    let d = p_norm.length() - 1.0;\n    d * radii.x.min(radii.y)\n}\n\n/// Evaluate a gradient at the given local UV coordinate.\nfn eval_gradient(\n    gradient_type: u32,\n    start: Vec2,\n    end: Vec2,\n    radius: f32,\n    color_start: Vec4,\n    color_end: Vec4,\n    local_uv: Vec2,\n) -> Vec4 {\n    let gt = GradientType::from_u32(gradient_type);\n    match gt {\n        GradientType::None => color_start,\n        GradientType::Linear => {\n            let dir = end - start;\n            let len_sq = dir.dot(dir);\n            let t = if len_sq > 0.0 {\n                let t = (local_uv - start).dot(dir) / len_sq;\n                t.clamp(0.0, 1.0)\n            } else {\n                0.0\n            };\n            color_start + (color_end - color_start) * t\n        }\n        GradientType::Radial => {\n            let d = (local_uv - start).length();\n            let t = if radius > 0.0 {\n                (d / radius).clamp(0.0, 1.0)\n            } else {\n                0.0\n            };\n            color_start + (color_end - color_start) * t\n        }\n    }\n}\n\n/// 2D UI vertex shader.\n///\n/// For SDF-based elements (Rectangle, Circle, Ellipse), this generates\n/// 6 vertices (2 triangles) per instance from the element's position and\n/// size, reading from the slab. The vertex index (0..5) selects which\n/// corner of the quad.\n///\n/// For Path elements, the vertex data is read directly from the slab\n/// (pre-tessellated vertices).\n#[spirv(vertex)]\npub fn ui_vertex(\n    #[spirv(vertex_index)] vertex_index: u32,\n    #[spirv(instance_index)] draw_call_id: Id<UiDrawCallDescriptor>,\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n    out_uv: &mut Vec2,\n    out_color: &mut Vec4,\n    #[spirv(flat)] out_draw_call_id: &mut u32,\n    #[spirv(position)] out_clip_pos: &mut Vec4,\n) {\n    let viewport: UiViewport = slab.read_unchecked(Id::new(0));\n    let draw_call: UiDrawCallDescriptor = slab.read_unchecked(draw_call_id);\n\n    *out_draw_call_id = draw_call_id.inner();\n\n    match draw_call.element_type {\n        UiElementType::Path => {\n            // For path elements, the draw_call stores an offset into the\n            // slab where UiVertex data lives. We read the vertex directly.\n            // The atlas_descriptor_id field stores the vertex slab offset.\n            let vertex_offset = draw_call.atlas_descriptor_id.inner();\n            let vertex_id =\n                Id::<UiVertex>::new(vertex_offset + vertex_index * UiVertex::SLAB_SIZE as u32);\n            let vertex: UiVertex = slab.read_unchecked(vertex_id);\n            *out_uv = vertex.uv;\n            *out_color = vertex.color;\n\n            let pos4 = viewport.projection\n                * Vec4::new(vertex.position.x, vertex.position.y, draw_call.z, 1.0);\n            *out_clip_pos = pos4;\n        }\n        _ => {\n            // SDF-based element: generate quad vertices.\n            // Quad corners in CCW order for two triangles:\n            //   0: top-left, 1: bottom-left, 2: bottom-right,\n            //   3: bottom-right, 4: top-right, 5: top-left\n            let vi = vertex_index % 6;\n            let (corner_x, corner_y) = match vi {\n                0 => (0.0f32, 0.0f32), // top-left\n                1 => (0.0, 1.0),       // bottom-left\n                2 => (1.0, 1.0),       // bottom-right\n                3 => (1.0, 1.0),       // bottom-right\n                4 => (1.0, 0.0),       // top-right\n                _ => (0.0, 0.0),       // top-left\n            };\n\n            let local_uv = Vec2::new(corner_x, corner_y);\n            *out_uv = local_uv;\n            *out_color = draw_call.fill_color;\n\n            let screen_pos = draw_call.position\n                + Vec2::new(corner_x * draw_call.size.x, corner_y * draw_call.size.y);\n\n            let pos4 =\n                viewport.projection * Vec4::new(screen_pos.x, screen_pos.y, draw_call.z, 1.0);\n            *out_clip_pos = pos4;\n        }\n    }\n}\n\n/// 2D UI fragment shader.\n///\n/// Evaluates SDF shapes, gradients, textures, and text glyphs depending\n/// on the element type.\n#[spirv(fragment)]\npub fn ui_fragment(\n    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],\n    #[spirv(descriptor_set = 0, binding = 1)] atlas: &Image2dArray,\n    #[spirv(descriptor_set = 0, binding = 2)] atlas_sampler: &Sampler,\n    in_uv: Vec2,\n    in_color: Vec4,\n    #[spirv(flat)] in_draw_call_id: u32,\n    frag_color: &mut Vec4,\n) {\n    let draw_call_id = Id::<UiDrawCallDescriptor>::new(in_draw_call_id);\n    let draw_call: UiDrawCallDescriptor = slab.read_unchecked(draw_call_id);\n    #[allow(unused_assignments)]\n    let mut color = Vec4::ZERO;\n\n    match draw_call.element_type {\n        UiElementType::Path => {\n            // Pre-tessellated path: start with vertex color.\n            color = in_color;\n            // If an atlas texture is set, sample it and multiply.\n            if !draw_call.atlas_texture_id.is_none() {\n                let atlas_tex: AtlasTextureDescriptor =\n                    slab.read_unchecked(draw_call.atlas_texture_id);\n                let viewport: UiViewport = slab.read_unchecked(Id::new(0));\n                let atlas_uv = atlas_tex.uv(in_uv, viewport.atlas_size);\n                let sample: Vec4 = atlas.sample_by_lod(*atlas_sampler, atlas_uv, 0.0);\n                color *= sample;\n            }\n        }\n        UiElementType::TextGlyph => {\n            // Text glyph: sample the glyph texture and multiply by color.\n            let atlas_tex: AtlasTextureDescriptor = slab.read_unchecked(draw_call.atlas_texture_id);\n            let viewport: UiViewport = slab.read_unchecked(Id::new(0));\n            let atlas_uv = atlas_tex.uv(in_uv, viewport.atlas_size);\n            let sample: Vec4 = atlas.sample_by_lod(*atlas_sampler, atlas_uv, 0.0);\n            color = draw_call.fill_color;\n            color.w *= sample.w;\n        }\n        UiElementType::Image => {\n            // Textured quad: sample the atlas texture.\n            let atlas_tex: AtlasTextureDescriptor = slab.read_unchecked(draw_call.atlas_texture_id);\n            let viewport: UiViewport = slab.read_unchecked(Id::new(0));\n            let atlas_uv = atlas_tex.uv(in_uv, viewport.atlas_size);\n            color = atlas.sample_by_lod(*atlas_sampler, atlas_uv, 0.0);\n            // Modulate with fill color (tint).\n            color *= draw_call.fill_color;\n        }\n        _ => {\n            // SDF-based element (Rectangle, Circle, Ellipse).\n            let half_size = draw_call.size * 0.5;\n            // Convert UV (0..1) to element-local coords centered on element\n            // center.\n            let local_pos = (in_uv - Vec2::splat(0.5)) * draw_call.size;\n\n            let distance = match draw_call.element_type {\n                UiElementType::Rectangle => {\n                    sdf_rounded_rect(local_pos, half_size, draw_call.corner_radii)\n                }\n                UiElementType::Circle => {\n                    let radius = half_size.x.min(half_size.y);\n                    sdf_circle(local_pos, radius)\n                }\n                UiElementType::Ellipse => sdf_ellipse(local_pos, half_size),\n                _ => 0.0,\n            };\n\n            // Evaluate fill color (possibly gradient).\n            let fill = eval_gradient(\n                draw_call.gradient.gradient_type,\n                draw_call.gradient.start,\n                draw_call.gradient.end,\n                draw_call.gradient.radius,\n                draw_call.gradient.color_start,\n                draw_call.gradient.color_end,\n                in_uv,\n            );\n            // If gradient is None, use the solid fill color.\n            let fill = if draw_call.gradient.gradient_type == 0 {\n                draw_call.fill_color\n            } else {\n                fill\n            };\n\n            // Anti-aliased edge using smoothstep.\n            let aa_width = 1.0; // 1 pixel of anti-aliasing\n            let fill_alpha = 1.0 - crate::math::smoothstep(-aa_width, aa_width, distance);\n\n            if draw_call.border_width > 0.0 {\n                // Border: the border region is between the outer edge and\n                // (outer edge - border_width).\n                let inner_distance = distance + draw_call.border_width;\n                let border_alpha =\n                    1.0 - crate::math::smoothstep(-aa_width, aa_width, inner_distance);\n                // Coverage weights.\n                let border_weight = fill_alpha - border_alpha;\n                let fill_weight = border_alpha;\n                let total = fill_alpha;\n                // Straight-alpha RGB: weighted blend of border and fill.\n                if total > 0.0 {\n                    let rgb = (draw_call.border_color.xyz() * border_weight\n                        + fill.xyz() * fill_weight)\n                        / total;\n                    let a =\n                        (draw_call.border_color.w * border_weight + fill.w * fill_weight) / total;\n                    color = rgb.extend(a * total);\n                } else {\n                    color = Vec4::ZERO;\n                }\n            } else {\n                color = fill;\n                color.w *= fill_alpha;\n            }\n        }\n    }\n\n    // Apply element opacity.\n    color.w *= draw_call.opacity;\n\n    // Premultiply RGB by final alpha for premultiplied-alpha blending.\n    color = (color.xyz() * color.w).extend(color.w);\n\n    *frag_color = color;\n}\n"
  },
  {
    "path": "crates/renderling/tests/wasm.rs",
    "content": "//! WASM tests.\n#![allow(dead_code)]\n\nuse craballoc::{\n    runtime::WgpuRuntime,\n    slab::{SlabAllocator, SlabBuffer},\n};\nuse glam::{Vec3, Vec4};\nuse image::DynamicImage;\nuse renderling::{\n    context::{Context, Frame},\n    geometry::Vertex,\n    texture::CopiedTextureBuffer,\n};\nuse wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};\nuse web_sys::wasm_bindgen::prelude::*;\nuse wire_types::{Error, PixelType};\n\nwasm_bindgen_test_configure!(run_in_browser);\n\n#[wasm_bindgen_test]\n/// Writes a textfile containing some system info.\n///\n/// If you need more info on CI etc, add it here.\nasync fn can_write_system_info_artifact() {\n    let _ = console_log::init();\n\n    let user_agent = web_sys::window()\n        .expect_throw(\"no window\")\n        .navigator()\n        .user_agent()\n        .expect_throw(\"no user agent\");\n    log::info!(\"user_agent: {user_agent}\");\n\n    let table = std::collections::HashMap::<String, String>::from_iter(Some((\n        \"user_agent\".to_owned(),\n        user_agent,\n    )));\n    let file = format!(\"{table:#?}\");\n    loading_bytes::post_bin_wasm::<Result<(), wire_types::Error>>(\n        \"http://127.0.0.1:4000/artifact/info.txt\",\n        file.as_bytes(),\n    )\n    .await\n    .unwrap_throw()\n    .unwrap_throw();\n}\n\n#[wasm_bindgen_test]\nasync fn can_create_headless_ctx() {\n    let _ctx = Context::try_new_headless(256, 256, None)\n        .await\n        .unwrap_throw();\n}\n\n#[wasm_bindgen_test]\nasync fn stage_creation() {\n    let ctx = Context::try_new_headless(256, 256, None)\n        .await\n        .unwrap_throw();\n    let _stage = ctx.new_stage();\n}\n\nfn image_from_bytes(bytes: &[u8]) -> image::DynamicImage {\n    image::ImageReader::new(std::io::Cursor::new(bytes))\n        .with_guessed_format()\n        .expect_throw(\"could not guess format\")\n        .decode()\n        .expect_throw(\"could not decode\")\n}\n\nasync fn load_test_img(path: &str) -> image::DynamicImage {\n    let result = loading_bytes::load(&format!(\"http://127.0.0.1:4000/test_img/{path}\")).await;\n    let bytes = match result {\n        Ok(bytes) => bytes,\n        Err(e) => panic!(\"{e}\"),\n    };\n    image_from_bytes(&bytes)\n}\n\nfn image_to_wire(seen: impl Into<DynamicImage>) -> wire_types::Image {\n    let img: DynamicImage = seen.into();\n    let width = img.width();\n    let height = img.height();\n    let (pixel, bytes) = match img {\n        DynamicImage::ImageRgb8(image_buffer) => (PixelType::Rgb8, image_buffer.to_vec()),\n        DynamicImage::ImageRgba8(image_buffer) => (PixelType::Rgba8, image_buffer.to_vec()),\n        _ => panic!(\"Image type is not yet supported in the WASM tests\"),\n    };\n    wire_types::Image {\n        width,\n        height,\n        bytes,\n        pixel,\n    }\n}\n\nasync fn assert_img_eq(filename: &str, seen: impl Into<image::DynamicImage>) {\n    let wire_data = image_to_wire(seen);\n    let data = serde_json::to_string(&wire_data).unwrap();\n    let result = loading_bytes::post_json_wasm::<Result<(), wire_types::Error>>(\n        &format!(\"http://127.0.0.1:4000/assert_img_eq/{filename}\"),\n        &data,\n    )\n    .await\n    .unwrap();\n\n    if let Err(Error { description }) = result {\n        panic!(\"{description}\");\n    }\n}\n\nasync fn save(filename: &str, seen: impl Into<image::DynamicImage>) {\n    let wire_data = image_to_wire(seen);\n    let data = serde_json::to_string(&wire_data).unwrap();\n    let result = loading_bytes::post_json_wasm::<Result<(), wire_types::Error>>(\n        &format!(\"http://127.0.0.1:4000/save/{filename}\"),\n        &data,\n    )\n    .await\n    .unwrap();\n\n    if let Err(Error { description }) = result {\n        panic!(\"{description}\");\n    }\n}\n\n#[wasm_bindgen_test]\nasync fn can_load_image() {\n    let _img = load_test_img(\"jolt.png\").await;\n}\n\n#[wasm_bindgen_test]\nasync fn can_img_diff() {\n    let a = load_test_img(\"jolt.png\").await;\n    assert_img_eq(\"jolt.png\", a).await;\n\n    let b = load_test_img(\"cmy_triangle/hdr.png\").await;\n    assert_img_eq(\"cmy_triangle/hdr.png\", b).await;\n}\n\n/// Performs a clearing render pass with internal context machinery.\n///\n/// This tests that the context setup is correct.\n#[wasm_bindgen_test]\nasync fn can_clear_background_sanity() {\n    let instance = renderling::internal::new_instance(None);\n    let (_adapter, device, queue, target) =\n        renderling::internal::new_headless_device_queue_and_target(2, 2, &instance)\n            .await\n            .unwrap();\n    let texture = target.as_texture().expect(\"unexpected RenderTarget\");\n    let view = texture.create_view(&Default::default());\n\n    let mut encoder = device.create_command_encoder(&Default::default());\n    {\n        let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n            color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                view: &view,\n                ops: wgpu::Operations {\n                    load: wgpu::LoadOp::Clear(wgpu::Color::RED),\n                    store: wgpu::StoreOp::Store,\n                },\n                depth_slice: None,\n                resolve_target: None,\n            })],\n            ..Default::default()\n        });\n    }\n    let _index = queue.submit(Some(encoder.finish()));\n\n    let runtime = WgpuRuntime {\n        device: device.into(),\n        queue: queue.into(),\n    };\n    let buffer = CopiedTextureBuffer::new(&runtime, texture).unwrap();\n    let img = buffer.convert_to_rgba().await.unwrap();\n    assert_img_eq(\"clear.png\", img).await;\n}\n\n/// Test rendering a triangle using no mesh geometry.\n#[wasm_bindgen_test]\nasync fn implicit_isosceles_triangle() {\n    let ctx = Context::headless(100, 100).await;\n    let runtime = ctx.as_ref();\n\n    fn create_pipeline(\n        runtime: &WgpuRuntime,\n        vmodule: &wgpu::ShaderModule,\n        ventry_point: &str,\n        fmodule: &wgpu::ShaderModule,\n        fentry_point: &str,\n    ) -> wgpu::RenderPipeline {\n        runtime\n            .device\n            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n                label: None,\n                layout: None,\n                vertex: wgpu::VertexState {\n                    module: vmodule,\n                    entry_point: Some(ventry_point),\n                    compilation_options: wgpu::PipelineCompilationOptions::default(),\n                    buffers: &[],\n                },\n                primitive: wgpu::PrimitiveState {\n                    topology: wgpu::PrimitiveTopology::TriangleList,\n                    strip_index_format: None,\n                    front_face: wgpu::FrontFace::Ccw,\n                    cull_mode: None,\n                    unclipped_depth: false,\n                    polygon_mode: wgpu::PolygonMode::Fill,\n                    conservative: false,\n                },\n                depth_stencil: None,\n                multisample: wgpu::MultisampleState {\n                    mask: !0,\n                    alpha_to_coverage_enabled: false,\n                    count: 1,\n                },\n                fragment: Some(wgpu::FragmentState {\n                    module: fmodule,\n                    entry_point: Some(fentry_point),\n                    targets: &[Some(wgpu::ColorTargetState {\n                        format: wgpu::TextureFormat::Rgba8UnormSrgb,\n                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                        write_mask: wgpu::ColorWrites::ALL,\n                    })],\n                    compilation_options: wgpu::PipelineCompilationOptions::default(),\n                }),\n                multiview: None,\n                cache: None,\n            })\n    }\n    // The first time through render with handwritten WGSL to ensure the setup works\n    let hand_written_wgsl_pipeline = {\n        let vertex = runtime.device.create_shader_module(wgpu::include_wgsl!(\n            \"../src/tutorial/implicit_isosceles_vertex.wgsl\"\n        ));\n        let fragment = runtime\n            .device\n            .create_shader_module(wgpu::include_wgsl!(\"../src/tutorial/passthru.wgsl\"));\n        create_pipeline(runtime, &vertex, \"main\", &fragment, \"main\")\n    };\n    // The second time render with WGSL that is transpiled from Rust code and pulled\n    // in through the renderling linkage machinery.\n    let linkage_pipeline = {\n        let vertex = renderling::linkage::implicit_isosceles_vertex::linkage(&runtime.device);\n        let fragment = renderling::linkage::passthru_fragment::linkage(&runtime.device);\n        create_pipeline(\n            runtime,\n            &vertex.module,\n            vertex.entry_point,\n            &fragment.module,\n            fragment.entry_point,\n        )\n    };\n\n    async fn render(runtime: &WgpuRuntime, frame: &Frame, pipeline: wgpu::RenderPipeline) {\n        let mut encoder = runtime\n            .device\n            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });\n        {\n            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                    view: &frame.view(),\n                    resolve_target: None,\n                    depth_slice: None,\n                    ops: wgpu::Operations {\n                        load: wgpu::LoadOp::Clear(wgpu::Color::GREEN),\n                        store: wgpu::StoreOp::Store,\n                    },\n                })],\n                ..Default::default()\n            });\n            render_pass.set_pipeline(&pipeline);\n            render_pass.draw(0..3, 0..1);\n        }\n        let _index = runtime.queue.submit(std::iter::once(encoder.finish()));\n\n        let img = frame\n            .read_image()\n            .await\n            .expect_throw(\"could not read frame\");\n        assert_img_eq(\"tutorial/implicit_isosceles_triangle.png\", img).await;\n    }\n\n    let frame = ctx.get_next_frame().unwrap();\n    render(runtime, &frame, hand_written_wgsl_pipeline).await;\n    frame.present();\n    let frame = ctx.get_next_frame().unwrap();\n    render(runtime, &frame, linkage_pipeline).await;\n}\n\n/// Test rendering a triangle from vertices on a slab, without an\n/// instance_index.\n#[wasm_bindgen_test]\nasync fn slabbed_vertices_no_instance() {\n    let _ = console_log::init_with_level(log::Level::Debug);\n\n    let instance = renderling::internal::new_instance(None);\n    let (_adapter, device, queue, target) =\n        renderling::internal::new_headless_device_queue_and_target(100, 100, &instance)\n            .await\n            .unwrap();\n    let runtime = WgpuRuntime {\n        device: device.into(),\n        queue: queue.into(),\n    };\n\n    // Create our geometry on the slab.\n    let slab = SlabAllocator::new(\n        &runtime,\n        \"isosceles-triangle-no-instance\",\n        wgpu::BufferUsages::empty(),\n    );\n\n    let initial_vertices = [\n        Vertex {\n            position: Vec3::new(0.5, -0.5, 0.0),\n            color: Vec4::new(1.0, 0.0, 0.0, 1.0),\n            ..Default::default()\n        },\n        Vertex {\n            position: Vec3::new(0.0, 0.5, 0.0),\n            color: Vec4::new(0.0, 1.0, 0.0, 1.0),\n            ..Default::default()\n        },\n        Vertex {\n            position: Vec3::new(-0.5, -0.5, 0.0),\n            color: Vec4::new(0.0, 0.0, 1.0, 1.0),\n            ..Default::default()\n        },\n    ];\n\n    let vertices = slab.new_array(initial_vertices);\n\n    assert_eq!(3, vertices.len());\n\n    // Create a bindgroup for the slab so our shader can read out the types.\n\n    let bindgroup_layout =\n        runtime\n            .device\n            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n                label: None,\n                entries: &[wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::VERTEX,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                }],\n            });\n    let pipeline_layout = runtime\n        .device\n        .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label: None,\n            bind_group_layouts: &[&bindgroup_layout],\n            push_constant_ranges: &[],\n        });\n    let pipeline = {\n        let vertex = renderling::linkage::slabbed_vertices_no_instance::linkage(&runtime.device);\n        let fragment = renderling::linkage::passthru_fragment::linkage(&runtime.device);\n        runtime\n            .device\n            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n                label: None,\n                layout: Some(&pipeline_layout),\n                vertex: wgpu::VertexState {\n                    module: &vertex.module,\n                    entry_point: Some(vertex.entry_point),\n                    compilation_options: wgpu::PipelineCompilationOptions::default(),\n                    buffers: &[],\n                },\n                primitive: wgpu::PrimitiveState {\n                    topology: wgpu::PrimitiveTopology::TriangleList,\n                    strip_index_format: None,\n                    front_face: wgpu::FrontFace::Ccw,\n                    cull_mode: None,\n                    unclipped_depth: false,\n                    polygon_mode: wgpu::PolygonMode::Fill,\n                    conservative: false,\n                },\n                depth_stencil: None,\n                multisample: wgpu::MultisampleState {\n                    mask: !0,\n                    alpha_to_coverage_enabled: false,\n                    count: 1,\n                },\n                fragment: Some(wgpu::FragmentState {\n                    module: &fragment.module,\n                    entry_point: Some(fragment.entry_point),\n                    targets: &[Some(wgpu::ColorTargetState {\n                        format: wgpu::TextureFormat::Rgba8UnormSrgb,\n                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                        write_mask: wgpu::ColorWrites::ALL,\n                    })],\n                    compilation_options: wgpu::PipelineCompilationOptions::default(),\n                }),\n                multiview: None,\n                cache: None,\n            })\n    };\n\n    let slab_buffer: SlabBuffer<wgpu::Buffer> = slab.commit();\n\n    let bindgroup = runtime\n        .device\n        .create_bind_group(&wgpu::BindGroupDescriptor {\n            label: None,\n            layout: &bindgroup_layout,\n            entries: &[wgpu::BindGroupEntry {\n                binding: 0,\n\n                resource: slab_buffer.as_entire_binding(),\n            }],\n        });\n\n    let texture = target.as_texture().expect(\"unexpected RenderTarget\");\n    let view = texture.create_view(&Default::default());\n    let mut encoder = runtime\n        .device\n        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });\n    {\n        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n            color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                view: &view,\n                resolve_target: None,\n                depth_slice: None,\n                ops: wgpu::Operations {\n                    load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),\n                    store: wgpu::StoreOp::Store,\n                },\n            })],\n            ..Default::default()\n        });\n        render_pass.set_pipeline(&pipeline);\n        render_pass.set_bind_group(0, &bindgroup, &[]);\n        render_pass.draw(0..3, 0..1);\n    }\n    let _index = runtime.queue.submit(std::iter::once(encoder.finish()));\n\n    let buffer = CopiedTextureBuffer::new(runtime, texture).unwrap();\n    let img = buffer.convert_to_rgba().await.unwrap();\n    assert_img_eq(\"tutorial/slabbed_isosceles_triangle_no_instance.png\", img).await;\n}\n\n#[wasm_bindgen_test]\nasync fn slabbed_isosceles_triangle() {\n    let ctx = Context::headless(100, 100).await;\n    let runtime = ctx.as_ref();\n\n    // Create our geometry on the slab.\n    let slab = SlabAllocator::new(\n        runtime,\n        \"slabbed_isosceles_triangle\",\n        wgpu::BufferUsages::empty(),\n    );\n\n    let geometry = vec![\n        (Vec3::new(0.5, -0.5, 0.0), Vec4::new(1.0, 0.0, 0.0, 1.0)),\n        (Vec3::new(0.0, 0.5, 0.0), Vec4::new(0.0, 1.0, 0.0, 1.0)),\n        (Vec3::new(-0.5, -0.5, 0.0), Vec4::new(0.0, 0.0, 1.0, 1.0)),\n        (Vec3::new(-1.0, 1.0, 0.0), Vec4::new(1.0, 0.0, 0.0, 1.0)),\n        (Vec3::new(-1.0, 0.0, 0.0), Vec4::new(0.0, 1.0, 0.0, 1.0)),\n        (Vec3::new(0.0, 1.0, 0.0), Vec4::new(0.0, 0.0, 1.0, 1.0)),\n    ];\n    let vertices = slab.new_array(geometry);\n    let array = slab.new_value(vertices.array());\n\n    // Create a bindgroup for the slab so our shader can read out the types.\n    let bindgroup_layout =\n        runtime\n            .device\n            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n                label: None,\n                entries: &[wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::VERTEX,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                }],\n            });\n    let pipeline_layout = runtime\n        .device\n        .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label: None,\n            bind_group_layouts: &[&bindgroup_layout],\n            push_constant_ranges: &[],\n        });\n\n    let vertex = renderling::linkage::slabbed_vertices::linkage(&runtime.device);\n    let fragment = renderling::linkage::passthru_fragment::linkage(&runtime.device);\n    let pipeline = runtime\n        .device\n        .create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n            label: None,\n            cache: None,\n            layout: Some(&pipeline_layout),\n            vertex: wgpu::VertexState {\n                compilation_options: wgpu::PipelineCompilationOptions::default(),\n                module: &vertex.module,\n                entry_point: Some(vertex.entry_point),\n                buffers: &[],\n            },\n            primitive: wgpu::PrimitiveState {\n                topology: wgpu::PrimitiveTopology::TriangleList,\n                strip_index_format: None,\n                front_face: wgpu::FrontFace::Ccw,\n                cull_mode: None,\n                unclipped_depth: false,\n                polygon_mode: wgpu::PolygonMode::Fill,\n                conservative: false,\n            },\n            depth_stencil: None,\n            multisample: wgpu::MultisampleState {\n                mask: !0,\n                alpha_to_coverage_enabled: false,\n                count: 1,\n            },\n            fragment: Some(wgpu::FragmentState {\n                compilation_options: Default::default(),\n                module: &fragment.module,\n                entry_point: Some(fragment.entry_point),\n                targets: &[Some(wgpu::ColorTargetState {\n                    format: wgpu::TextureFormat::Rgba8UnormSrgb,\n                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n                    write_mask: wgpu::ColorWrites::ALL,\n                })],\n            }),\n            multiview: None,\n        });\n    let slab_buffer = slab.commit();\n\n    let bindgroup = runtime\n        .device\n        .create_bind_group(&wgpu::BindGroupDescriptor {\n            label: None,\n            layout: &bindgroup_layout,\n            entries: &[wgpu::BindGroupEntry {\n                binding: 0,\n                resource: slab_buffer.as_entire_binding(),\n            }],\n        });\n\n    let frame = ctx.get_next_frame().unwrap();\n    let mut encoder = runtime.device.create_command_encoder(&Default::default());\n    {\n        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n            color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                view: &frame.view(),\n                resolve_target: None,\n                ops: wgpu::Operations {\n                    load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),\n                    store: wgpu::StoreOp::Store,\n                },\n                depth_slice: None,\n            })],\n            ..Default::default()\n        });\n        render_pass.set_pipeline(&pipeline);\n        render_pass.set_bind_group(0, &bindgroup, &[]);\n        let id = array.id().inner();\n        render_pass.draw(0..vertices.len() as u32, id..id + 1);\n    }\n    runtime.queue.submit(std::iter::once(encoder.finish()));\n\n    let img = frame\n        .read_linear_image()\n        .await\n        .expect_throw(\"could not read frame\");\n    assert_img_eq(\"tutorial/slabbed_isosceles_triangle.png\", img).await;\n}\n\n// #[test]\n// fn slabbed_render_unit() {\n//     let mut r = Renderling::headless(100, 100).unwrap();\n//     let (device, queue) = r.get_device_and_queue_owned();\n\n//     // Create our geometry on the slab.\n//     // Don't worry too much about capacity, it can grow.\n//     let slab = crate::slab::SlabBuffer::new(&device, 16);\n//     let geometry = vec![\n//         Vertex {\n//             position: Vec4::new(0.5, -0.5, 0.0, 1.0),\n//             color: Vec4::new(1.0, 0.0, 0.0, 1.0),\n//             ..Default::default()\n//         },\n//         Vertex {\n//             position: Vec4::new(0.0, 0.5, 0.0, 1.0),\n//             color: Vec4::new(0.0, 1.0, 0.0, 1.0),\n//             ..Default::default()\n//         },\n//         Vertex {\n//             position: Vec4::new(-0.5, -0.5, 0.0, 1.0),\n//             color: Vec4::new(0.0, 0.0, 1.0, 1.0),\n//             ..Default::default()\n//         },\n//         Vertex {\n//             position: Vec4::new(-1.0, 1.0, 0.0, 1.0),\n//             color: Vec4::new(1.0, 0.0, 0.0, 1.0),\n//             ..Default::default()\n//         },\n//         Vertex {\n//             position: Vec4::new(-1.0, 0.0, 0.0, 1.0),\n//             color: Vec4::new(0.0, 1.0, 0.0, 1.0),\n//             ..Default::default()\n//         },\n//         Vertex {\n//             position: Vec4::new(0.0, 1.0, 0.0, 1.0),\n//             color: Vec4::new(0.0, 0.0, 1.0, 1.0),\n//             ..Default::default()\n//         },\n//     ];\n//     let vertices = slab.append_slice(&device, &queue, &geometry);\n//     let unit = RenderUnit {\n//         vertices,\n//         ..Default::default()\n//     };\n//     let unit_id = slab.append(&device, &queue, &unit);\n\n//     // Create a bindgroup for the slab so our shader can read out the types.\n//     let label = Some(\"slabbed isosceles triangle\");\n//     let bindgroup_layout =\n// device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n//         label,\n//         entries: &[wgpu::BindGroupLayoutEntry {\n//             binding: 0,\n//             visibility: wgpu::ShaderStages::VERTEX,\n//             ty: wgpu::BindingType::Buffer {\n//                 ty: wgpu::BufferBindingType::Storage { read_only: true },\n//                 has_dynamic_offset: false,\n//                 min_binding_size: None,\n//             },\n//             count: None,\n//         }],\n//     });\n//     let pipeline_layout =\n// device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n//         label,\n//         bind_group_layouts: &[&bindgroup_layout],\n//         push_constant_ranges: &[],\n//     });\n\n//     let pipeline =\n// device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n//         label,\n//         layout: Some(&pipeline_layout),\n//         vertex: wgpu::VertexState {\n//             module: &device.create_shader_module(wgpu::include_spirv!(\n//                 \"linkage/tutorial-slabbed_render_unit.spv\"\n//             )),\n//             entry_point: \"tutorial::slabbed_render_unit\",\n//             buffers: &[],\n//         },\n//         primitive: wgpu::PrimitiveState {\n//             topology: wgpu::PrimitiveTopology::TriangleList,\n//             strip_index_format: None,\n//             front_face: wgpu::FrontFace::Ccw,\n//             cull_mode: None,\n//             unclipped_depth: false,\n//             polygon_mode: wgpu::PolygonMode::Fill,\n//             conservative: false,\n//         },\n//         depth_stencil: Some(wgpu::DepthStencilState {\n//             format: wgpu::TextureFormat::Depth32Float,\n//             depth_write_enabled: true,\n//             depth_compare: wgpu::CompareFunction::Less,\n//             stencil: wgpu::StencilState::default(),\n//             bias: wgpu::DepthBiasState::default(),\n//         }),\n//         multisample: wgpu::MultisampleState {\n//             mask: !0,\n//             alpha_to_coverage_enabled: false,\n//             count: 1,\n//         },\n//         fragment: Some(wgpu::FragmentState {\n//             module: &device.create_shader_module(wgpu::include_spirv!(\n//                 \"linkage/tutorial-passthru_fragment.spv\"\n//             )),\n//             entry_point: \"tutorial::passthru_fragment\",\n//             targets: &[Some(wgpu::ColorTargetState {\n//                 format: wgpu::TextureFormat::Rgba8UnormSrgb,\n//                 blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n//                 write_mask: wgpu::ColorWrites::ALL,\n//             })],\n//         }),\n//         multiview: None,\n//     });\n\n//     let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor {\n//         label,\n//         layout: &bindgroup_layout,\n//         entries: &[wgpu::BindGroupEntry {\n//             binding: 0,\n//             resource: slab.get_buffer().as_entire_binding(),\n//         }],\n//     });\n\n//     struct App {\n//         pipeline: wgpu::RenderPipeline,\n//         bindgroup: wgpu::BindGroup,\n//         unit_id: Id<RenderUnit>,\n//         unit: RenderUnit,\n//     }\n\n//     let app = App {\n//         pipeline,\n//         bindgroup,\n//         unit_id,\n//         unit,\n//     };\n//     r.graph.add_resource(app);\n\n//     fn render(\n//         (device, queue, app, frame, depth): (\n//             View<Device>,\n//             View<Queue>,\n//             View<App>,\n//             View<FrameTextureView>,\n//             View<DepthTexture>,\n//         ),\n//     ) -> Result<(), GraphError> {\n//         let label = Some(\"slabbed isosceles triangle\");\n//         let mut encoder =\n//             device.create_command_encoder(&wgpu::CommandEncoderDescriptor {\n// label });         {\n//             let mut render_pass =\n// encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n// label,                 color_attachments:\n// &[Some(wgpu::RenderPassColorAttachment {                     view:\n// &frame.view,                     resolve_target: None,\n//                     ops: wgpu::Operations {\n//                         load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),\n//                         store: true,\n//                     },\n//                 })],\n//                 depth_stencil_attachment:\n// Some(wgpu::RenderPassDepthStencilAttachment {                     view:\n// &depth.view,                     depth_ops: Some(wgpu::Operations {\n//                         load: wgpu::LoadOp::Load,\n//                         store: true,\n//                     }),\n//                     stencil_ops: None,\n//                 }),\n//             });\n//             render_pass.set_pipeline(&app.pipeline);\n//             render_pass.set_bind_group(0, &app.bindgroup, &[]);\n//             render_pass.draw(\n//                 0..app.unit.vertices.len() as u32,\n//                 app.unit_id.inner()..app.unit_id.inner() + 1,\n//             );\n//         }\n//         queue.submit(std::iter::once(encoder.finish()));\n//         Ok(())\n//     }\n\n//     use crate::frame::{clear_frame_and_depth, copy_frame_to_post,\n// create_frame, present};     r.graph.add_subgraph(graph!(\n//         create_frame\n//             < clear_frame_and_depth\n//             < render\n//             < copy_frame_to_post\n//             < present\n//     ));\n\n//     let img = r.render_image().unwrap();\n//     img_diff::assert_img_eq(\"tutorial/slabbed_render_unit.png\", img);\n// }\n\n//     #[test]\n//     fn slabbed_render_unit_camera() {\n//         let mut r = Renderling::headless(100, 100).unwrap();\n//         let (device, queue) = r.get_device_and_queue_owned();\n\n//         // Create our geometry on the slab.\n//         // Don't worry too much about capacity, it can grow.\n//         let slab = crate::slab::SlabBuffer::new(&device, 16);\n//         let geometry = vec![\n//             Vertex {\n//                 position: Vec4::new(0.5, -0.5, 0.0, 1.0),\n//                 color: Vec4::new(1.0, 0.0, 0.0, 1.0),\n//                 ..Default::default()\n//             },\n//             Vertex {\n//                 position: Vec4::new(0.0, 0.5, 0.0, 1.0),\n//                 color: Vec4::new(0.0, 1.0, 0.0, 1.0),\n//                 ..Default::default()\n//             },\n//             Vertex {\n//                 position: Vec4::new(-0.5, -0.5, 0.0, 1.0),\n//                 color: Vec4::new(0.0, 0.0, 1.0, 1.0),\n//                 ..Default::default()\n//             },\n//             Vertex {\n//                 position: Vec4::new(-1.0, 1.0, 0.0, 1.0),\n//                 color: Vec4::new(1.0, 0.0, 0.0, 1.0),\n//                 ..Default::default()\n//             },\n//             Vertex {\n//                 position: Vec4::new(-1.0, 0.0, 0.0, 1.0),\n//                 color: Vec4::new(0.0, 1.0, 0.0, 1.0),\n//                 ..Default::default()\n//             },\n//             Vertex {\n//                 position: Vec4::new(0.0, 1.0, 0.0, 1.0),\n//                 color: Vec4::new(0.0, 0.0, 1.0, 1.0),\n//                 ..Default::default()\n//             },\n//         ];\n//         let vertices = slab.append_slice(&device, &queue, &geometry);\n//         let (projection, view) = crate::default_ortho2d(100.0, 100.0);\n//         let camera_id = slab.append(\n//             &device,\n//             &queue,\n//             &Camera {\n//                 projection,\n//                 view,\n//                 ..Default::default()\n//             },\n//         );\n//         let unit = RenderUnit {\n//             vertices,\n//             camera: camera_id,\n//             position: Vec3::new(50.0, 50.0, 0.0),\n//             scale: Vec3::new(50.0, 50.0, 1.0),\n//             ..Default::default()\n//         };\n//         let unit_id = slab.append(&device, &queue, &unit);\n\n//         // Create a bindgroup for the slab so our shader can read out the\n// types.         let label = Some(\"slabbed isosceles triangle\");\n//         let bindgroup_layout =\n// device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n//             label,\n//             entries: &[wgpu::BindGroupLayoutEntry {\n//                 binding: 0,\n//                 visibility: wgpu::ShaderStages::VERTEX,\n//                 ty: wgpu::BindingType::Buffer {\n//                     ty: wgpu::BufferBindingType::Storage { read_only: true },\n//                     has_dynamic_offset: false,\n//                     min_binding_size: None,\n//                 },\n//                 count: None,\n//             }],\n//         });\n//         let pipeline_layout =\n// device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n// label,             bind_group_layouts: &[&bindgroup_layout],\n//             push_constant_ranges: &[],\n//         });\n\n//         let pipeline =\n// device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n// label,             layout: Some(&pipeline_layout),\n//             vertex: wgpu::VertexState {\n//                 module: &device.create_shader_module(wgpu::include_spirv!(\n//                     \"linkage/tutorial-slabbed_render_unit.spv\"\n//                 )),\n//                 entry_point: \"tutorial::slabbed_render_unit\",\n//                 buffers: &[],\n//             },\n//             primitive: wgpu::PrimitiveState {\n//                 topology: wgpu::PrimitiveTopology::TriangleList,\n//                 strip_index_format: None,\n//                 front_face: wgpu::FrontFace::Ccw,\n//                 cull_mode: None,\n//                 unclipped_depth: false,\n//                 polygon_mode: wgpu::PolygonMode::Fill,\n//                 conservative: false,\n//             },\n//             depth_stencil: Some(wgpu::DepthStencilState {\n//                 format: wgpu::TextureFormat::Depth32Float,\n//                 depth_write_enabled: true,\n//                 depth_compare: wgpu::CompareFunction::Less,\n//                 stencil: wgpu::StencilState::default(),\n//                 bias: wgpu::DepthBiasState::default(),\n//             }),\n//             multisample: wgpu::MultisampleState {\n//                 mask: !0,\n//                 alpha_to_coverage_enabled: false,\n//                 count: 1,\n//             },\n//             fragment: Some(wgpu::FragmentState {\n//                 module: &device.create_shader_module(wgpu::include_spirv!(\n//                     \"linkage/tutorial-passthru_fragment.spv\"\n//                 )),\n//                 entry_point: \"tutorial::passthru_fragment\",\n//                 targets: &[Some(wgpu::ColorTargetState {\n//                     format: wgpu::TextureFormat::Rgba8UnormSrgb,\n//                     blend: Some(wgpu::BlendState::ALPHA_BLENDING),\n//                     write_mask: wgpu::ColorWrites::ALL,\n//                 })],\n//             }),\n//             multiview: None,\n//         });\n\n//         let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor {\n//             label,\n//             layout: &bindgroup_layout,\n//             entries: &[wgpu::BindGroupEntry {\n//                 binding: 0,\n//                 resource: slab.get_buffer().as_entire_binding(),\n//             }],\n//         });\n\n//         struct App {\n//             pipeline: wgpu::RenderPipeline,\n//             bindgroup: wgpu::BindGroup,\n//             unit_id: Id<RenderUnit>,\n//             unit: RenderUnit,\n//         }\n\n//         let app = App {\n//             pipeline,\n//             bindgroup,\n//             unit_id,\n//             unit,\n//         };\n//         r.graph.add_resource(app);\n\n//         fn render(\n//             (device, queue, app, frame, depth): (\n//                 View<Device>,\n//                 View<Queue>,\n//                 View<App>,\n//                 View<FrameTextureView>,\n//                 View<DepthTexture>,\n//             ),\n//         ) -> Result<(), GraphError> {\n//             let label = Some(\"slabbed isosceles triangle\");\n//             let mut encoder =\n//                 device.create_command_encoder(&wgpu::CommandEncoderDescriptor\n// { label });             {\n//                 let mut render_pass =\n// encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n// label,                     color_attachments:\n// &[Some(wgpu::RenderPassColorAttachment {                         view:\n// &frame.view,                         resolve_target: None,\n//                         ops: wgpu::Operations {\n//                             load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),\n//                             store: true,\n//                         },\n//                     })],\n//                     depth_stencil_attachment:\n// Some(wgpu::RenderPassDepthStencilAttachment {                         view:\n// &depth.view,                         depth_ops: Some(wgpu::Operations {\n//                             load: wgpu::LoadOp::Load,\n//                             store: true,\n//                         }),\n//                         stencil_ops: None,\n//                     }),\n//                 });\n//                 render_pass.set_pipeline(&app.pipeline);\n//                 render_pass.set_bind_group(0, &app.bindgroup, &[]);\n//                 render_pass.draw(\n//                     0..app.unit.vertices.len() as u32,\n//                     app.unit_id.inner()..app.unit_id.inner() + 1,\n//                 );\n//             }\n//             queue.submit(std::iter::once(encoder.finish()));\n//             Ok(())\n//         }\n\n//         use crate::frame::{clear_frame_and_depth, copy_frame_to_post,\n// create_frame, present};         r.graph.add_subgraph(graph!(\n//             create_frame\n//                 < clear_frame_and_depth\n//                 < render\n//                 < copy_frame_to_post\n//                 < present\n//         ));\n\n//         let img = r.render_image().unwrap();\n//         img_diff::assert_img_eq(\"tutorial/slabbed_render_unit_camera.png\",\n// img);     }\n// }\n\n#[wasm_bindgen_test]\nasync fn can_clear_background() {\n    let ctx = Context::try_new_headless(2, 2, None).await.unwrap();\n    let stage = ctx\n        .new_stage()\n        .with_background_color(Vec4::new(1.0, 0.0, 0.0, 1.0));\n    // ANCHOR: manual_context_frame\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let seen = frame.read_image().await.unwrap();\n    assert_img_eq(\"clear.png\", seen).await;\n    frame.present();\n    // ANCHOR_END: manual_context_frame\n}\n\n// #[wasm_bindgen_test]\n// #[should_panic]\n// async fn can_save_wrong_diffs() {\n//     let img = load_test_img(\"jolt.png\").await;\n//     assert_img_eq(\"cmy_triangle/hdr.png\", img).await;\n// }\n\nfn right_tri_vertices() -> Vec<Vertex> {\n    vec![\n        Vertex::default()\n            .with_position([0.0, 0.0, 0.0])\n            .with_color([0.0, 1.0, 1.0, 1.0]),\n        Vertex::default()\n            .with_position([0.0, 100.0, 0.0])\n            .with_color([1.0, 1.0, 0.0, 1.0]),\n        Vertex::default()\n            .with_position([100.0, 0.0, 0.0])\n            .with_color([1.0, 0.0, 1.0, 1.0]),\n    ]\n}\n\n#[wasm_bindgen_test]\nasync fn can_render_hello_triangle() {\n    // This is a wasm version of cmy_triangle_sanity\n    let ctx = Context::try_new_headless(100, 100, None).await.unwrap();\n    let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0));\n    let (projection, view) = renderling::camera::default_ortho2d(100.0, 100.0);\n    let _camera = stage\n        .new_camera()\n        .with_projection_and_view(projection, view);\n    let _rez = stage\n        .new_primitive()\n        .with_vertices(stage.new_vertices(right_tri_vertices()));\n\n    let frame = ctx.get_next_frame().unwrap();\n    stage.render(&frame.view());\n    let img = frame\n        .read_linear_image()\n        .await\n        .expect_throw(\"could not read frame\");\n    assert_img_eq(\"cmy_triangle/hdr.png\", img).await;\n}\n"
  },
  {
    "path": "crates/renderling/webdriver.json",
    "content": "{\n  \"moz:firefoxOptions\": {\n    \"prefs\": {\n      \"dom.webgpu.enabled\": true\n    },\n    \"args\": []\n  },\n  \"goog:chromeOptions\": {\n    \"args\": [\n      \"--enable-unsafe-webgpu\"\n    ]\n  }\n}\n"
  },
  {
    "path": "crates/renderling-build/Cargo.toml",
    "content": "[package]\nname = \"renderling_build\"\nversion = \"0.1.0\"\nedition = \"2021\"\ndescription = \"Builds shader linkage for the Renderling project\"\nrepository = \"https://github.com/schell/renderling\"\nlicense = \"MIT OR Apache-2.0\"\nkeywords = [\"game\", \"graphics\", \"shader\", \"rendering\"]\ncategories = [\"rendering\", \"game-development\", \"graphics\"]\n\n[dependencies]\nlog.workspace = true\nnaga.workspace = true\nquote.workspace = true\nserde.workspace = true\nserde_json.workspace = true\n"
  },
  {
    "path": "crates/renderling-build/src/lib.rs",
    "content": "#![allow(unexpected_cfgs)]\nuse naga::{\n    back::wgsl::WriterFlags,\n    valid::{ValidationFlags, Validator},\n};\nuse quote::quote;\n\n#[derive(Debug, serde::Deserialize)]\nstruct Linkage {\n    source_path: std::path::PathBuf,\n    entry_point: String,\n    wgsl_entry_point: String,\n}\n\nimpl core::fmt::Display for Linkage {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let spv_source_path = self.source_path.clone();\n        let spv_source_filename = spv_source_path.file_name().unwrap().to_str().unwrap();\n        let spv_include_source_path = format!(\"../../shaders/{spv_source_filename}\");\n\n        let wgsl_source_path = self.source_path.with_extension(\"wgsl\");\n        let wgsl_source_filename = wgsl_source_path.file_name().unwrap().to_str().unwrap();\n        let wgsl_include_source_path = format!(\"../../shaders/{wgsl_source_filename}\");\n\n        let Linkage {\n            source_path: _,\n            entry_point,\n            wgsl_entry_point,\n        } = self;\n\n        let fn_name = self.fn_name();\n\n        let quote = quote! {\n            use crate::linkage::ShaderLinkage;\n\n            #[cfg(not(target_arch = \"wasm32\"))]\n            mod target {\n                pub const ENTRY_POINT: &str = #entry_point;\n\n                pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n                    wgpu::include_spirv!(#spv_include_source_path)\n                }\n\n                pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n                    log::debug!(\"creating native linkage for {}\", #fn_name);\n                    super::ShaderLinkage {\n                        entry_point: ENTRY_POINT,\n                        module: device.create_shader_module(descriptor()).into()\n                    }\n                }\n            }\n            #[cfg(target_arch = \"wasm32\")]\n            mod target {\n                pub const ENTRY_POINT: &str = #wgsl_entry_point;\n\n                pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> {\n                    wgpu::include_wgsl!(#wgsl_include_source_path)\n                }\n\n                pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage {\n                    log::debug!(\"creating web linkage for {}\", #fn_name);\n                    super::ShaderLinkage {\n                        entry_point: ENTRY_POINT,\n                        module: device.create_shader_module(descriptor()).into()\n                    }\n                }\n            }\n\n            pub fn linkage(device: &wgpu::Device) -> ShaderLinkage {\n                target::linkage(device)\n            }\n        };\n\n        f.write_fmt(format_args!(\n            r#\"#![allow(dead_code)]\n            //! Automatically generated by Renderling's `build.rs`.\n            {quote}\n            \"#,\n        ))\n    }\n}\n\nimpl Linkage {\n    pub fn fn_name(&self) -> &str {\n        self.entry_point.split(\"::\").last().unwrap()\n    }\n}\n\nfn wgsl(spv_filepath: impl AsRef<std::path::Path>, destination: impl AsRef<std::path::Path>) {\n    log::trace!(\"generating WGSL for {}\", spv_filepath.as_ref().display());\n    let bytes = std::fs::read(spv_filepath.as_ref()).unwrap_or_else(|e| {\n        panic!(\n            \"could not read spv filepath '{}' while attempting to translate to wgsl: {e}\",\n            spv_filepath.as_ref().display()\n        );\n    });\n    let opts = naga::front::spv::Options {\n        adjust_coordinate_space: false,\n        ..Default::default()\n    };\n    let module = naga::front::spv::parse_u8_slice(&bytes, &opts)\n        .unwrap_or_else(|e| panic!(\"could not parse {}: {e}\", spv_filepath.as_ref().display()));\n    let mut wgsl = String::new();\n    let mut panic_msg: Option<String> = None;\n    for (vflags, name) in [\n        (ValidationFlags::empty(), \"empty\"),\n        (ValidationFlags::all(), \"all\"),\n    ] {\n        let mut validator = Validator::new(vflags, Default::default());\n        match validator.validate(&module) {\n            Err(e) => {\n                panic_msg = Some(format!(\n                    \"Could not validate '{}' with WGSL validation flags {name}: {}\",\n                    spv_filepath.as_ref().display(),\n                    e.emit_to_string(&wgsl)\n                ));\n            }\n            Ok(i) => {\n                wgsl = naga::back::wgsl::write_string(&module, &i, WriterFlags::empty())\n                    .unwrap_or_else(|e| {\n                        panic!(\n                            \"could not generate WGSL code for {}: {e}\",\n                            spv_filepath.as_ref().display()\n                        );\n                    });\n            }\n        };\n    }\n\n    let destination = destination.as_ref().with_extension(\"wgsl\");\n    std::fs::write(destination, wgsl).unwrap();\n    if let Some(msg) = panic_msg {\n        panic!(\n            \"{msg}\\nWGSL was written to {}\",\n            spv_filepath.as_ref().display()\n        );\n    }\n}\n\n/// The cargo workspace directory.\n///\n/// ## Panics\n/// Panics if not called from a checkout of the renderling repo.\npub fn workspace_dir() -> std::path::PathBuf {\n    std::path::PathBuf::from(std::env::var(\"CARGO_WORKSPACE_DIR\").unwrap())\n}\n\n/// The test_img directory.\n///\n/// ## Panics\n/// Panics if not called from a checkout of the renderling repo.\npub fn test_img_dir() -> std::path::PathBuf {\n    workspace_dir().join(\"test_img\")\n}\n\n/// The test_output directory.\n///\n/// ## Panics\n/// Panics if not called from a checkout of the renderling repo.\npub fn test_output_dir() -> std::path::PathBuf {\n    workspace_dir().join(\"test_output\")\n}\n\n/// The WASM test_output directory.\n///\n/// ## Panics\n/// Panics if not called from a checkout of the renderling repo.\npub fn wasm_test_output_dir() -> std::path::PathBuf {\n    test_output_dir().join(\"wasm\")\n}\n\n#[derive(Debug)]\npub struct RenderlingPaths {\n    /// `cargo_workspace` is not available when building outside of the project\n    /// directory.\n    pub cargo_workspace: Option<std::path::PathBuf>,\n    pub renderling_crate: std::path::PathBuf,\n    pub shader_dir: std::path::PathBuf,\n    pub shader_manifest: std::path::PathBuf,\n    pub linkage_dir: std::path::PathBuf,\n}\n\nimpl RenderlingPaths {\n    /// Create a new `RenderlingPaths`.\n    ///\n    /// If the `CARGO_WORKSPACE_DIR` and subsequently the `cargo_workspace` is\n    /// _not_ available, this most likely means we're building renderling\n    /// outside of its own source tree, which means we **don't want to compile\n    /// shaders**.\n    ///\n    /// But we may still need to transpile the packaged SPIR-V into WGSL for\n    /// WASM, and so `cargo_workspace` is `Option` and the entire function\n    /// also returns `Option`.\n    pub fn new() -> Option<Self> {\n        let cargo_workspace = std::env::var(\"CARGO_WORKSPACE_DIR\")\n            .map(std::path::PathBuf::from)\n            .ok();\n        let renderling_crate = if let Some(workspace) = cargo_workspace.as_ref() {\n            workspace.join(\"crates\").join(\"renderling\")\n        } else {\n            std::env::var(\"CARGO_MANIFEST_DIR\")\n                .map(std::path::PathBuf::from)\n                .ok()?\n        };\n        log::debug!(\"cargo_manifest_dir: {renderling_crate:#?}\");\n        let shader_dir = renderling_crate.join(\"shaders\");\n\n        let shader_manifest = shader_dir.join(\"manifest.json\");\n        let linkage_dir = renderling_crate.join(\"src\").join(\"linkage\");\n\n        Some(Self {\n            cargo_workspace,\n            renderling_crate,\n            shader_dir,\n            shader_manifest,\n            linkage_dir,\n        })\n    }\n\n    /// Generate linkage (Rust source) files for each shader in the manifest.\n    pub fn generate_linkage(\n        &self,\n        from_cargo: bool,\n        with_wgsl: bool,\n        just_fn_name: Option<String>,\n    ) {\n        log::trace!(\"{:#?}\", std::env::vars().collect::<Vec<_>>());\n        assert!(\n            self.shader_manifest.is_file(),\n            \"missing file '{}', you must first compile the shaders\",\n            self.shader_manifest.display()\n        );\n\n        if from_cargo {\n            println!(\"cargo::rerun-if-changed={}\", self.shader_manifest.display());\n        }\n\n        if !self.linkage_dir.is_dir() {\n            log::info!(\"creating linkage directory\");\n            std::fs::create_dir_all(&self.linkage_dir).unwrap();\n        }\n\n        log::debug!(\"cwd: {:?}\", std::env::current_dir().unwrap());\n\n        let manifest_file = std::fs::File::open(&self.shader_manifest).unwrap();\n        let manifest: Vec<Linkage> = serde_json::from_reader(manifest_file).unwrap();\n        let mut set = std::collections::HashSet::new();\n        for linkage in manifest.into_iter() {\n            log::debug!(\"linkage: {linkage:#?}\");\n            let fn_name = linkage.fn_name();\n            if let Some(fn_name_match) = just_fn_name.as_ref() {\n                if fn_name != fn_name_match {\n                    log::debug!(\"  skipping {fn_name} != {fn_name_match}\");\n                    continue;\n                }\n            }\n\n            if set.contains(fn_name) {\n                panic!(\"Shader name '{fn_name}' is used for two or more shaders, aborting!\");\n            }\n            set.insert(fn_name.to_string());\n\n            let absolute_source_path = self\n                .shader_dir\n                .join(linkage.source_path.file_name().unwrap());\n\n            if from_cargo {\n                println!(\"cargo::rerun-if-changed={}\", absolute_source_path.display());\n            }\n            if with_wgsl {\n                let wgsl_source_path = linkage.source_path.with_extension(\"wgsl\");\n                let absolute_wgsl_source_path =\n                    self.shader_dir.join(wgsl_source_path.file_name().unwrap());\n                wgsl(absolute_source_path, absolute_wgsl_source_path);\n            }\n\n            let filepath = self.linkage_dir.join(fn_name).with_extension(\"rs\");\n            log::info!(\"generating: {}\", linkage.entry_point,);\n\n            let contents = linkage.to_string();\n            std::fs::write(&filepath, contents).unwrap();\n        }\n        // Just format the whole project. I know this is less than ideal,\n        // but people should be running with a formatter in their editor, and all of\n        // this is temporary given the wgsl-rs re-stacking happening this year (2026)\n        std::process::Command::new(\"cargo\")\n            .args([\"+nightly\", \"fmt\"])\n            .output()\n            .expect(\"could not format generated code\");\n        log::info!(\"...done!\")\n    }\n}\n"
  },
  {
    "path": "crates/renderling-ui/Cargo.toml",
    "content": "[package]\nname = \"renderling-ui\"\nversion = \"0.1.0\"\nedition = \"2021\"\ndescription = \"Lightweight 2D/UI renderer for renderling.\"\nrepository = \"https://github.com/schell/renderling\"\nlicense = \"MIT OR Apache-2.0\"\n\n[features]\ndefault = [\"text\", \"path\"]\ntext = [\"dep:glyph_brush\", \"dep:image\", \"dep:loading-bytes\"]\npath = [\"dep:lyon\"]\ntest-utils = [\"renderling/test-utils\"]\n\n[dependencies]\nbytemuck = { workspace = true }\ncraballoc = { workspace = true }\ncrabslab = { workspace = true, features = [\"default\"] }\nglam = { workspace = true, features = [\"std\"] }\nglyph_brush = { workspace = true, optional = true }\nimage = { workspace = true, optional = true }\nloading-bytes = { workspace = true, optional = true }\nlog = { workspace = true }\nlyon = { workspace = true, optional = true }\nrenderling = { path = \"../renderling\", default-features = false }\nrustc-hash = { workspace = true }\nsnafu = { workspace = true }\nwgpu = { workspace = true, features = [\"spirv\"] }\n\n[dev-dependencies]\nenv_logger = { workspace = true }\nfutures-lite = { workspace = true }\nimg-diff = { path = \"../img-diff\" }\nimage = { workspace = true }\nrenderling = { path = \"../renderling\", features = [\"test-utils\"] }\nrenderling_build = { path = \"../renderling-build\" }\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/renderling-ui/src/lib.rs",
    "content": "//! Lightweight 2D/UI renderer for renderling.\n//!\n//! This crate provides a dedicated 2D rendering pipeline that is separate\n//! from renderling's 3D PBR pipeline. It features:\n//!\n//! - SDF-based shape rendering (rectangles, rounded rectangles, circles,\n//!   ellipses) with anti-aliased edges\n//! - Gradient fills (linear and radial)\n//! - Texture/image rendering via the renderling atlas system\n//! - Text rendering via `glyph_brush` (behind the `text` feature)\n//! - Vector path rendering via `lyon` tessellation (behind the `path` feature)\n//! - A lightweight vertex format (32 bytes vs ~160 bytes for 3D)\n//! - Minimal GPU bindings (3 vs 13 for 3D)\n//!\n//! # Quick Start\n//!\n//! ```ignore\n//! use renderling::context::Context;\n//! use renderling_ui::UiRenderer;\n//!\n//! let ctx = futures_lite::future::block_on(Context::headless(800, 600));\n//! let mut ui = UiRenderer::new(&ctx);\n//!\n//! // Add a rounded rectangle\n//! let _rect = ui.add_rect()\n//!     .with_position(glam::Vec2::new(10.0, 10.0))\n//!     .with_size(glam::Vec2::new(200.0, 100.0))\n//!     .with_corner_radii(glam::Vec4::splat(8.0))\n//!     .with_fill_color(glam::Vec4::new(0.2, 0.3, 0.8, 1.0));\n//!\n//! let frame = ctx.get_next_frame().unwrap();\n//! ui.render(&frame.view());\n//! frame.present();\n//! ```\n\nmod renderer;\n#[cfg(test)]\nmod test;\n\n// Re-export key types from renderling that users will need.\npub use renderling::{\n    atlas::{AtlasImage, AtlasTexture},\n    context::Context,\n    glam,\n    ui_slab::{\n        GradientDescriptor, GradientType, UiDrawCallDescriptor, UiElementType, UiVertex, UiViewport,\n    },\n};\n\n// Re-export our own types.\npub use renderer::{UiCircle, UiEllipse, UiImage, UiRect, UiRenderer};\n\n// Re-export text types (behind \"text\" feature).\n#[cfg(feature = \"text\")]\npub use renderer::{FontArc, FontId, Section, Text, UiText};\n\n// Re-export path types (behind \"path\" feature).\n#[cfg(feature = \"path\")]\npub use renderer::{StrokeConfig, UiPath, UiPathBuilder};\n"
  },
  {
    "path": "crates/renderling-ui/src/renderer.rs",
    "content": "//! Core `UiRenderer` implementation.\n//!\n//! This module contains the GPU pipeline setup, element management,\n//! and rendering logic for the 2D/UI renderer.\n//!\n//! ## Architecture\n//!\n//! The renderer uses a [`SlabAllocator`] from `craballoc` to manage GPU\n//! memory. Each UI element is backed by a [`Hybrid<UiDrawCallDescriptor>`]\n//! which keeps a CPU copy in sync with a GPU slab allocation. Calling\n//! [`SlabAllocator::commit`] flushes all pending changes to the GPU buffer.\n//!\n//! Element wrapper types ([`UiRect`], [`UiCircle`],\n//! [`UiEllipse`]) follow the same pattern as\n//! [`renderling::camera::Camera`] — each wraps a `Hybrid` and provides\n//! typed setter methods that queue GPU updates automatically.\n\nuse craballoc::{\n    prelude::*,\n    slab::{SlabAllocator, SlabBuffer},\n    value::Hybrid,\n};\nuse crabslab::Id;\nuse glam::{Mat4, UVec2, Vec2, Vec4};\nuse renderling::{\n    atlas::{Atlas, AtlasImage, AtlasTexture},\n    compositor::Compositor,\n    context::Context,\n    ui_slab::{GradientDescriptor, UiDrawCallDescriptor, UiElementType, UiViewport},\n};\n\n// ---------------------------------------------------------------------------\n// Element wrapper types (follow the Camera pattern from camera/cpu.rs)\n// ---------------------------------------------------------------------------\n\n/// A live handle to a rectangle element in the renderer.\n///\n/// Modifications via the `set_*` methods are reflected on the GPU after\n/// the next call to [`UiRenderer::render`].\n///\n/// Clones of this type all point to the same underlying GPU data.\n///\n/// **Dropping this handle does NOT remove the element** — call\n/// [`UiRenderer::remove_rect`] explicitly.\n#[derive(Clone, Debug)]\npub struct UiRect {\n    inner: Hybrid<UiDrawCallDescriptor>,\n}\n\nimpl UiRect {\n    /// Returns the slab [`Id`] of the underlying descriptor.\n    pub fn id(&self) -> Id<UiDrawCallDescriptor> {\n        self.inner.id()\n    }\n\n    /// Returns a copy of the underlying descriptor.\n    pub fn descriptor(&self) -> UiDrawCallDescriptor {\n        self.inner.get()\n    }\n\n    /// Set the top-left position in screen pixels.\n    pub fn set_position(&self, position: Vec2) -> &Self {\n        self.inner.modify(|d| d.position = position);\n        self\n    }\n\n    /// Set the top-left position in screen pixels (builder).\n    pub fn with_position(self, position: Vec2) -> Self {\n        self.set_position(position);\n        self\n    }\n\n    /// Set the size in screen pixels.\n    pub fn set_size(&self, size: Vec2) -> &Self {\n        self.inner.modify(|d| d.size = size);\n        self\n    }\n\n    /// Set the size in screen pixels (builder).\n    pub fn with_size(self, size: Vec2) -> Self {\n        self.set_size(size);\n        self\n    }\n\n    /// Set the fill color (RGBA).\n    pub fn set_fill_color(&self, color: Vec4) -> &Self {\n        self.inner.modify(|d| d.fill_color = color);\n        self\n    }\n\n    /// Set the fill color (builder).\n    pub fn with_fill_color(self, color: Vec4) -> Self {\n        self.set_fill_color(color);\n        self\n    }\n\n    /// Set per-corner radii (top-left, top-right, bottom-right,\n    /// bottom-left).\n    pub fn set_corner_radii(&self, radii: Vec4) -> &Self {\n        self.inner.modify(|d| d.corner_radii = radii);\n        self\n    }\n\n    /// Set per-corner radii (builder).\n    pub fn with_corner_radii(self, radii: Vec4) -> Self {\n        self.set_corner_radii(radii);\n        self\n    }\n\n    /// Set the border width and color.\n    pub fn set_border(&self, width: f32, color: Vec4) -> &Self {\n        self.inner.modify(|d| {\n            d.border_width = width;\n            d.border_color = color;\n        });\n        self\n    }\n\n    /// Set the border width and color (builder).\n    pub fn with_border(self, width: f32, color: Vec4) -> Self {\n        self.set_border(width, color);\n        self\n    }\n\n    /// Set the gradient fill. Pass `None` to remove the gradient.\n    pub fn set_gradient(&self, gradient: Option<GradientDescriptor>) -> &Self {\n        self.inner\n            .modify(|d| d.gradient = gradient.unwrap_or_default());\n        self\n    }\n\n    /// Set the gradient fill (builder).\n    pub fn with_gradient(self, gradient: Option<GradientDescriptor>) -> Self {\n        self.set_gradient(gradient);\n        self\n    }\n\n    /// Set the opacity (0.0 = transparent, 1.0 = opaque).\n    pub fn set_opacity(&self, opacity: f32) -> &Self {\n        self.inner.modify(|d| d.opacity = opacity);\n        self\n    }\n\n    /// Set the opacity (builder).\n    pub fn with_opacity(self, opacity: f32) -> Self {\n        self.set_opacity(opacity);\n        self\n    }\n\n    /// Set the z-depth for sorting. Lower values are drawn first.\n    pub fn set_z(&self, z: f32) -> &Self {\n        self.inner.modify(|d| d.z = z);\n        self\n    }\n\n    /// Set the z-depth for sorting (builder).\n    pub fn with_z(self, z: f32) -> Self {\n        self.set_z(z);\n        self\n    }\n\n    // --- Getters ---\n\n    /// Returns the top-left position in screen pixels.\n    pub fn position(&self) -> Vec2 {\n        self.inner.get().position\n    }\n\n    /// Returns the size in screen pixels.\n    pub fn size(&self) -> Vec2 {\n        self.inner.get().size\n    }\n\n    /// Returns the fill color (RGBA).\n    pub fn fill_color(&self) -> Vec4 {\n        self.inner.get().fill_color\n    }\n\n    /// Returns the per-corner radii.\n    pub fn corner_radii(&self) -> Vec4 {\n        self.inner.get().corner_radii\n    }\n\n    /// Returns the border width in pixels.\n    pub fn border_width(&self) -> f32 {\n        self.inner.get().border_width\n    }\n\n    /// Returns the border color (RGBA).\n    pub fn border_color(&self) -> Vec4 {\n        self.inner.get().border_color\n    }\n\n    /// Returns the gradient descriptor.\n    pub fn gradient(&self) -> GradientDescriptor {\n        self.inner.get().gradient\n    }\n\n    /// Returns the opacity.\n    pub fn opacity(&self) -> f32 {\n        self.inner.get().opacity\n    }\n\n    /// Returns the z-depth.\n    pub fn z(&self) -> f32 {\n        self.inner.get().z\n    }\n}\n\n/// A live handle to a circle element in the renderer.\n///\n/// See [`UiRect`] for general usage notes.\n#[derive(Clone, Debug)]\npub struct UiCircle {\n    inner: Hybrid<UiDrawCallDescriptor>,\n}\n\nimpl UiCircle {\n    /// Returns the slab [`Id`] of the underlying descriptor.\n    pub fn id(&self) -> Id<UiDrawCallDescriptor> {\n        self.inner.id()\n    }\n\n    /// Returns a copy of the underlying descriptor.\n    pub fn descriptor(&self) -> UiDrawCallDescriptor {\n        self.inner.get()\n    }\n\n    /// Set the center position in screen pixels.\n    pub fn set_center(&self, center: Vec2) -> &Self {\n        self.inner.modify(|d| {\n            let radius = d.size.x / 2.0;\n            d.position = center - Vec2::splat(radius);\n        });\n        self\n    }\n\n    /// Set the center position in screen pixels (builder).\n    pub fn with_center(self, center: Vec2) -> Self {\n        self.set_center(center);\n        self\n    }\n\n    /// Set the radius in screen pixels.\n    pub fn set_radius(&self, radius: f32) -> &Self {\n        self.inner.modify(|d| {\n            let center = d.position + d.size / 2.0;\n            d.size = Vec2::splat(radius * 2.0);\n            d.position = center - Vec2::splat(radius);\n        });\n        self\n    }\n\n    /// Set the radius in screen pixels (builder).\n    pub fn with_radius(self, radius: f32) -> Self {\n        self.set_radius(radius);\n        self\n    }\n\n    /// Set the fill color (RGBA).\n    pub fn set_fill_color(&self, color: Vec4) -> &Self {\n        self.inner.modify(|d| d.fill_color = color);\n        self\n    }\n\n    /// Set the fill color (builder).\n    pub fn with_fill_color(self, color: Vec4) -> Self {\n        self.set_fill_color(color);\n        self\n    }\n\n    /// Set the border width and color.\n    pub fn set_border(&self, width: f32, color: Vec4) -> &Self {\n        self.inner.modify(|d| {\n            d.border_width = width;\n            d.border_color = color;\n        });\n        self\n    }\n\n    /// Set the border width and color (builder).\n    pub fn with_border(self, width: f32, color: Vec4) -> Self {\n        self.set_border(width, color);\n        self\n    }\n\n    /// Set the gradient fill. Pass `None` to remove the gradient.\n    pub fn set_gradient(&self, gradient: Option<GradientDescriptor>) -> &Self {\n        self.inner\n            .modify(|d| d.gradient = gradient.unwrap_or_default());\n        self\n    }\n\n    /// Set the gradient fill (builder).\n    pub fn with_gradient(self, gradient: Option<GradientDescriptor>) -> Self {\n        self.set_gradient(gradient);\n        self\n    }\n\n    /// Set the opacity.\n    pub fn set_opacity(&self, opacity: f32) -> &Self {\n        self.inner.modify(|d| d.opacity = opacity);\n        self\n    }\n\n    /// Set the opacity (builder).\n    pub fn with_opacity(self, opacity: f32) -> Self {\n        self.set_opacity(opacity);\n        self\n    }\n\n    /// Set the z-depth for sorting.\n    pub fn set_z(&self, z: f32) -> &Self {\n        self.inner.modify(|d| d.z = z);\n        self\n    }\n\n    /// Set the z-depth for sorting (builder).\n    pub fn with_z(self, z: f32) -> Self {\n        self.set_z(z);\n        self\n    }\n\n    // --- Getters ---\n\n    /// Returns the center position in screen pixels.\n    pub fn center(&self) -> Vec2 {\n        let d = self.inner.get();\n        d.position + d.size / 2.0\n    }\n\n    /// Returns the radius in screen pixels.\n    pub fn radius(&self) -> f32 {\n        self.inner.get().size.x / 2.0\n    }\n\n    /// Returns the fill color (RGBA).\n    pub fn fill_color(&self) -> Vec4 {\n        self.inner.get().fill_color\n    }\n\n    /// Returns the border width in pixels.\n    pub fn border_width(&self) -> f32 {\n        self.inner.get().border_width\n    }\n\n    /// Returns the border color (RGBA).\n    pub fn border_color(&self) -> Vec4 {\n        self.inner.get().border_color\n    }\n\n    /// Returns the gradient descriptor.\n    pub fn gradient(&self) -> GradientDescriptor {\n        self.inner.get().gradient\n    }\n\n    /// Returns the opacity.\n    pub fn opacity(&self) -> f32 {\n        self.inner.get().opacity\n    }\n\n    /// Returns the z-depth.\n    pub fn z(&self) -> f32 {\n        self.inner.get().z\n    }\n}\n\n/// A live handle to an ellipse element in the renderer.\n///\n/// See [`UiRect`] for general usage notes.\n#[derive(Clone, Debug)]\npub struct UiEllipse {\n    inner: Hybrid<UiDrawCallDescriptor>,\n}\n\nimpl UiEllipse {\n    /// Returns the slab [`Id`] of the underlying descriptor.\n    pub fn id(&self) -> Id<UiDrawCallDescriptor> {\n        self.inner.id()\n    }\n\n    /// Returns a copy of the underlying descriptor.\n    pub fn descriptor(&self) -> UiDrawCallDescriptor {\n        self.inner.get()\n    }\n\n    /// Set the center position in screen pixels.\n    pub fn set_center(&self, center: Vec2) -> &Self {\n        self.inner.modify(|d| {\n            let radii = d.size / 2.0;\n            d.position = center - radii;\n        });\n        self\n    }\n\n    /// Set the center position in screen pixels (builder).\n    pub fn with_center(self, center: Vec2) -> Self {\n        self.set_center(center);\n        self\n    }\n\n    /// Set the radii (horizontal, vertical) in screen pixels.\n    pub fn set_radii(&self, radii: Vec2) -> &Self {\n        self.inner.modify(|d| {\n            let center = d.position + d.size / 2.0;\n            d.size = radii * 2.0;\n            d.position = center - radii;\n        });\n        self\n    }\n\n    /// Set the radii (builder).\n    pub fn with_radii(self, radii: Vec2) -> Self {\n        self.set_radii(radii);\n        self\n    }\n\n    /// Set the fill color (RGBA).\n    pub fn set_fill_color(&self, color: Vec4) -> &Self {\n        self.inner.modify(|d| d.fill_color = color);\n        self\n    }\n\n    /// Set the fill color (builder).\n    pub fn with_fill_color(self, color: Vec4) -> Self {\n        self.set_fill_color(color);\n        self\n    }\n\n    /// Set the border width and color.\n    pub fn set_border(&self, width: f32, color: Vec4) -> &Self {\n        self.inner.modify(|d| {\n            d.border_width = width;\n            d.border_color = color;\n        });\n        self\n    }\n\n    /// Set the border width and color (builder).\n    pub fn with_border(self, width: f32, color: Vec4) -> Self {\n        self.set_border(width, color);\n        self\n    }\n\n    /// Set the gradient fill. Pass `None` to remove the gradient.\n    pub fn set_gradient(&self, gradient: Option<GradientDescriptor>) -> &Self {\n        self.inner\n            .modify(|d| d.gradient = gradient.unwrap_or_default());\n        self\n    }\n\n    /// Set the gradient fill (builder).\n    pub fn with_gradient(self, gradient: Option<GradientDescriptor>) -> Self {\n        self.set_gradient(gradient);\n        self\n    }\n\n    /// Set the opacity.\n    pub fn set_opacity(&self, opacity: f32) -> &Self {\n        self.inner.modify(|d| d.opacity = opacity);\n        self\n    }\n\n    /// Set the opacity (builder).\n    pub fn with_opacity(self, opacity: f32) -> Self {\n        self.set_opacity(opacity);\n        self\n    }\n\n    /// Set the z-depth for sorting.\n    pub fn set_z(&self, z: f32) -> &Self {\n        self.inner.modify(|d| d.z = z);\n        self\n    }\n\n    /// Set the z-depth for sorting (builder).\n    pub fn with_z(self, z: f32) -> Self {\n        self.set_z(z);\n        self\n    }\n\n    // --- Getters ---\n\n    /// Returns the center position in screen pixels.\n    pub fn center(&self) -> Vec2 {\n        let d = self.inner.get();\n        d.position + d.size / 2.0\n    }\n\n    /// Returns the radii (horizontal, vertical) in screen pixels.\n    pub fn radii(&self) -> Vec2 {\n        self.inner.get().size / 2.0\n    }\n\n    /// Returns the fill color (RGBA).\n    pub fn fill_color(&self) -> Vec4 {\n        self.inner.get().fill_color\n    }\n\n    /// Returns the border width in pixels.\n    pub fn border_width(&self) -> f32 {\n        self.inner.get().border_width\n    }\n\n    /// Returns the border color (RGBA).\n    pub fn border_color(&self) -> Vec4 {\n        self.inner.get().border_color\n    }\n\n    /// Returns the gradient descriptor.\n    pub fn gradient(&self) -> GradientDescriptor {\n        self.inner.get().gradient\n    }\n\n    /// Returns the opacity.\n    pub fn opacity(&self) -> f32 {\n        self.inner.get().opacity\n    }\n\n    /// Returns the z-depth.\n    pub fn z(&self) -> f32 {\n        self.inner.get().z\n    }\n}\n\n/// A live handle to an image element in the renderer.\n///\n/// See [`UiRect`] for general usage notes.\n#[derive(Clone)]\npub struct UiImage {\n    inner: Hybrid<UiDrawCallDescriptor>,\n    /// Kept alive to prevent the atlas from garbage-collecting the texture.\n    #[allow(dead_code)]\n    atlas_texture: AtlasTexture,\n}\n\nimpl std::fmt::Debug for UiImage {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"UiImage\")\n            .field(\"inner\", &self.inner)\n            .finish_non_exhaustive()\n    }\n}\n\nimpl UiImage {\n    /// Returns the slab [`Id`] of the underlying descriptor.\n    pub fn id(&self) -> Id<UiDrawCallDescriptor> {\n        self.inner.id()\n    }\n\n    /// Returns a copy of the underlying descriptor.\n    pub fn descriptor(&self) -> UiDrawCallDescriptor {\n        self.inner.get()\n    }\n\n    /// Set the top-left position in screen pixels.\n    pub fn set_position(&self, position: Vec2) -> &Self {\n        self.inner.modify(|d| d.position = position);\n        self\n    }\n\n    /// Set the top-left position in screen pixels (builder).\n    pub fn with_position(self, position: Vec2) -> Self {\n        self.set_position(position);\n        self\n    }\n\n    /// Set the size in screen pixels.\n    pub fn set_size(&self, size: Vec2) -> &Self {\n        self.inner.modify(|d| d.size = size);\n        self\n    }\n\n    /// Set the size in screen pixels (builder).\n    pub fn with_size(self, size: Vec2) -> Self {\n        self.set_size(size);\n        self\n    }\n\n    /// Set a tint color (multiplied with the texture color).\n    /// Use `Vec4::ONE` for no tint.\n    pub fn set_tint(&self, color: Vec4) -> &Self {\n        self.inner.modify(|d| d.fill_color = color);\n        self\n    }\n\n    /// Set a tint color (builder).\n    pub fn with_tint(self, color: Vec4) -> Self {\n        self.set_tint(color);\n        self\n    }\n\n    /// Set the opacity (0.0 = transparent, 1.0 = opaque).\n    pub fn set_opacity(&self, opacity: f32) -> &Self {\n        self.inner.modify(|d| d.opacity = opacity);\n        self\n    }\n\n    /// Set the opacity (builder).\n    pub fn with_opacity(self, opacity: f32) -> Self {\n        self.set_opacity(opacity);\n        self\n    }\n\n    /// Set the z-depth for sorting.\n    pub fn set_z(&self, z: f32) -> &Self {\n        self.inner.modify(|d| d.z = z);\n        self\n    }\n\n    /// Set the z-depth for sorting (builder).\n    pub fn with_z(self, z: f32) -> Self {\n        self.set_z(z);\n        self\n    }\n\n    // --- Getters ---\n\n    /// Returns the top-left position in screen pixels.\n    pub fn position(&self) -> Vec2 {\n        self.inner.get().position\n    }\n\n    /// Returns the size in screen pixels.\n    pub fn size(&self) -> Vec2 {\n        self.inner.get().size\n    }\n\n    /// Returns the tint color (RGBA).\n    pub fn tint(&self) -> Vec4 {\n        self.inner.get().fill_color\n    }\n\n    /// Returns the opacity.\n    pub fn opacity(&self) -> f32 {\n        self.inner.get().opacity\n    }\n\n    /// Returns the z-depth.\n    pub fn z(&self) -> f32 {\n        self.inner.get().z\n    }\n}\n\n// ---------------------------------------------------------------------------\n// Path types (behind \"path\" feature)\n// ---------------------------------------------------------------------------\n\n#[cfg(feature = \"path\")]\nmod path {\n    use super::*;\n    use craballoc::value::HybridArray;\n    use lyon::{\n        geom,\n        math::Angle,\n        path::{builder::BorderRadii, traits::PathBuilder, Winding},\n        tessellation::{\n            BuffersBuilder, FillTessellator, FillVertex, LineCap, LineJoin, StrokeTessellator,\n            StrokeVertex, VertexBuffers,\n        },\n    };\n    use renderling::{atlas::shader::AtlasTextureDescriptor, ui_slab::UiVertex};\n\n    fn vec2_to_point(v: impl Into<Vec2>) -> geom::Point<f32> {\n        let v = v.into();\n        geom::point(v.x, v.y)\n    }\n\n    fn vec2_to_vec(v: impl Into<Vec2>) -> geom::Vector<f32> {\n        let v = v.into();\n        geom::Vector::new(v.x, v.y)\n    }\n\n    /// Number of per-vertex attributes (stroke_color[4] + fill_color[4]).\n    const NUM_ATTRIBUTES: usize = 8;\n\n    /// Per-vertex attributes passed through lyon's attribute system.\n    #[derive(Clone, Copy)]\n    struct PathAttributes {\n        stroke_color: Vec4,\n        fill_color: Vec4,\n    }\n\n    impl PathAttributes {\n        fn to_array(self) -> [f32; NUM_ATTRIBUTES] {\n            let s = self.stroke_color;\n            let f = self.fill_color;\n            [s.x, s.y, s.z, s.w, f.x, f.y, f.z, f.w]\n        }\n\n        fn from_slice(s: &[f32]) -> Self {\n            Self {\n                stroke_color: Vec4::new(s[0], s[1], s[2], s[3]),\n                fill_color: Vec4::new(s[4], s[5], s[6], s[7]),\n            }\n        }\n    }\n\n    /// Stroke rendering options.\n    pub struct StrokeConfig {\n        /// Line width in pixels.\n        pub line_width: f32,\n        /// Line cap style.\n        pub line_cap: LineCap,\n        /// Line join style.\n        pub line_join: LineJoin,\n    }\n\n    impl Default for StrokeConfig {\n        fn default() -> Self {\n            Self {\n                line_width: 2.0,\n                line_cap: LineCap::Round,\n                line_join: LineJoin::Round,\n            }\n        }\n    }\n\n    /// A builder for constructing 2D vector paths.\n    ///\n    /// Uses lyon for tessellation. Build a path with `begin`/`line_to`/\n    /// `end` commands (or convenience methods like `add_rectangle`,\n    /// `add_circle`, etc.), then call `fill()` or `stroke()` to\n    /// tessellate and register the result with the renderer.\n    ///\n    /// ```ignore\n    /// let path = ui.path_builder()\n    ///     .with_fill_color(Vec4::new(1.0, 0.0, 0.0, 1.0))\n    ///     .with_begin(Vec2::new(10.0, 10.0))\n    ///     .with_line_to(Vec2::new(100.0, 10.0))\n    ///     .with_line_to(Vec2::new(55.0, 80.0))\n    ///     .with_end(true)\n    ///     .fill(&mut ui);\n    /// ```\n    pub struct UiPathBuilder {\n        inner: lyon::path::BuilderWithAttributes,\n        attrs: PathAttributes,\n        stroke_config: StrokeConfig,\n        /// Atlas texture descriptor ID for image-filled paths.\n        fill_image_id: Id<AtlasTextureDescriptor>,\n    }\n\n    impl UiPathBuilder {\n        pub(crate) fn new() -> Self {\n            Self {\n                inner: lyon::path::Path::builder_with_attributes(NUM_ATTRIBUTES),\n                attrs: PathAttributes {\n                    stroke_color: Vec4::ZERO,\n                    fill_color: Vec4::ONE,\n                },\n                stroke_config: StrokeConfig::default(),\n                fill_image_id: Id::NONE,\n            }\n        }\n\n        // --- Color setters ---\n\n        /// Set the fill color for subsequent path commands.\n        pub fn set_fill_color(&mut self, color: impl Into<Vec4>) -> &mut Self {\n            self.attrs.fill_color = color.into();\n            self\n        }\n\n        /// Set the fill color (builder).\n        pub fn with_fill_color(mut self, color: impl Into<Vec4>) -> Self {\n            self.set_fill_color(color);\n            self\n        }\n\n        /// Set the stroke color for subsequent path commands.\n        pub fn set_stroke_color(&mut self, color: impl Into<Vec4>) -> &mut Self {\n            self.attrs.stroke_color = color.into();\n            self\n        }\n\n        /// Set the stroke color (builder).\n        pub fn with_stroke_color(mut self, color: impl Into<Vec4>) -> Self {\n            self.set_stroke_color(color);\n            self\n        }\n\n        /// Set stroke options.\n        pub fn set_stroke_config(&mut self, config: StrokeConfig) -> &mut Self {\n            self.stroke_config = config;\n            self\n        }\n\n        /// Set stroke options (builder).\n        pub fn with_stroke_config(mut self, config: StrokeConfig) -> Self {\n            self.stroke_config = config;\n            self\n        }\n\n        /// Set an image to fill the path with.\n        ///\n        /// The image is sampled using UVs computed from each vertex's\n        /// position relative to the path's bounding box (0..1 range).\n        /// The vertex color acts as a tint/modulator.\n        ///\n        /// The `AtlasTexture` should be obtained from\n        /// [`UiRenderer::upload_image`].\n        pub fn set_fill_image(&mut self, texture: &AtlasTexture) -> &mut Self {\n            self.fill_image_id = texture.id();\n            self\n        }\n\n        /// Set an image to fill the path with (builder).\n        pub fn with_fill_image(mut self, texture: &AtlasTexture) -> Self {\n            self.set_fill_image(texture);\n            self\n        }\n\n        // --- Path commands ---\n\n        /// Begin a new sub-path at the given point.\n        pub fn begin(&mut self, at: impl Into<Vec2>) -> &mut Self {\n            let _ = self.inner.begin(vec2_to_point(at), &self.attrs.to_array());\n            self\n        }\n\n        /// Begin a new sub-path (builder).\n        pub fn with_begin(mut self, at: impl Into<Vec2>) -> Self {\n            self.begin(at);\n            self\n        }\n\n        /// End the current sub-path, optionally closing it.\n        pub fn end(&mut self, close: bool) -> &mut Self {\n            self.inner.end(close);\n            self\n        }\n\n        /// End the current sub-path (builder).\n        pub fn with_end(mut self, close: bool) -> Self {\n            self.end(close);\n            self\n        }\n\n        /// Add a line segment to the given point.\n        pub fn line_to(&mut self, to: impl Into<Vec2>) -> &mut Self {\n            let _ = self\n                .inner\n                .line_to(vec2_to_point(to), &self.attrs.to_array());\n            self\n        }\n\n        /// Add a line segment (builder).\n        pub fn with_line_to(mut self, to: impl Into<Vec2>) -> Self {\n            self.line_to(to);\n            self\n        }\n\n        /// Add a quadratic Bezier curve.\n        pub fn quadratic_bezier_to(\n            &mut self,\n            ctrl: impl Into<Vec2>,\n            to: impl Into<Vec2>,\n        ) -> &mut Self {\n            let _ = self.inner.quadratic_bezier_to(\n                vec2_to_point(ctrl),\n                vec2_to_point(to),\n                &self.attrs.to_array(),\n            );\n            self\n        }\n\n        /// Add a quadratic Bezier curve (builder).\n        pub fn with_quadratic_bezier_to(\n            mut self,\n            ctrl: impl Into<Vec2>,\n            to: impl Into<Vec2>,\n        ) -> Self {\n            self.quadratic_bezier_to(ctrl, to);\n            self\n        }\n\n        /// Add a cubic Bezier curve.\n        pub fn cubic_bezier_to(\n            &mut self,\n            ctrl1: impl Into<Vec2>,\n            ctrl2: impl Into<Vec2>,\n            to: impl Into<Vec2>,\n        ) -> &mut Self {\n            let _ = self.inner.cubic_bezier_to(\n                vec2_to_point(ctrl1),\n                vec2_to_point(ctrl2),\n                vec2_to_point(to),\n                &self.attrs.to_array(),\n            );\n            self\n        }\n\n        /// Add a cubic Bezier curve (builder).\n        pub fn with_cubic_bezier_to(\n            mut self,\n            ctrl1: impl Into<Vec2>,\n            ctrl2: impl Into<Vec2>,\n            to: impl Into<Vec2>,\n        ) -> Self {\n            self.cubic_bezier_to(ctrl1, ctrl2, to);\n            self\n        }\n\n        // --- Convenience shapes ---\n\n        /// Add an axis-aligned rectangle.\n        pub fn add_rectangle(&mut self, min: impl Into<Vec2>, max: impl Into<Vec2>) -> &mut Self {\n            let min = min.into();\n            let max = max.into();\n            let rect = lyon::geom::Box2D::new(vec2_to_point(min), vec2_to_point(max));\n            self.inner\n                .add_rectangle(&rect, Winding::Positive, &self.attrs.to_array());\n            self\n        }\n\n        /// Add a rectangle (builder).\n        pub fn with_rectangle(mut self, min: impl Into<Vec2>, max: impl Into<Vec2>) -> Self {\n            self.add_rectangle(min, max);\n            self\n        }\n\n        /// Add a rounded rectangle.\n        pub fn add_rounded_rectangle(\n            &mut self,\n            min: impl Into<Vec2>,\n            max: impl Into<Vec2>,\n            top_left: f32,\n            top_right: f32,\n            bottom_left: f32,\n            bottom_right: f32,\n        ) -> &mut Self {\n            let min = min.into();\n            let max = max.into();\n            let rect = lyon::geom::Box2D::new(vec2_to_point(min), vec2_to_point(max));\n            let radii = BorderRadii {\n                top_left,\n                top_right,\n                bottom_left,\n                bottom_right,\n            };\n            self.inner.add_rounded_rectangle(\n                &rect,\n                &radii,\n                Winding::Positive,\n                &self.attrs.to_array(),\n            );\n            self\n        }\n\n        /// Add a rounded rectangle (builder).\n        pub fn with_rounded_rectangle(\n            mut self,\n            min: impl Into<Vec2>,\n            max: impl Into<Vec2>,\n            top_left: f32,\n            top_right: f32,\n            bottom_left: f32,\n            bottom_right: f32,\n        ) -> Self {\n            self.add_rounded_rectangle(min, max, top_left, top_right, bottom_left, bottom_right);\n            self\n        }\n\n        /// Add a circle.\n        pub fn add_circle(&mut self, center: impl Into<Vec2>, radius: f32) -> &mut Self {\n            self.inner.add_circle(\n                vec2_to_point(center),\n                radius,\n                Winding::Positive,\n                &self.attrs.to_array(),\n            );\n            self\n        }\n\n        /// Add a circle (builder).\n        pub fn with_circle(mut self, center: impl Into<Vec2>, radius: f32) -> Self {\n            self.add_circle(center, radius);\n            self\n        }\n\n        /// Add an ellipse.\n        pub fn add_ellipse(\n            &mut self,\n            center: impl Into<Vec2>,\n            radii: impl Into<Vec2>,\n            rotation: f32,\n        ) -> &mut Self {\n            let radii = radii.into();\n            self.inner.add_ellipse(\n                vec2_to_point(center),\n                vec2_to_vec(radii),\n                Angle::radians(rotation),\n                Winding::Positive,\n                &self.attrs.to_array(),\n            );\n            self\n        }\n\n        /// Add an ellipse (builder).\n        pub fn with_ellipse(\n            mut self,\n            center: impl Into<Vec2>,\n            radii: impl Into<Vec2>,\n            rotation: f32,\n        ) -> Self {\n            self.add_ellipse(center, radii, rotation);\n            self\n        }\n\n        /// Add a closed polygon from a series of points.\n        pub fn add_polygon(&mut self, points: &[Vec2]) -> &mut Self {\n            let pts: Vec<geom::Point<f32>> = points.iter().map(|p| vec2_to_point(*p)).collect();\n            self.inner.add_polygon(\n                lyon::path::Polygon {\n                    points: &pts,\n                    closed: true,\n                },\n                &self.attrs.to_array(),\n            );\n            self\n        }\n\n        /// Add a polygon (builder).\n        pub fn with_polygon(mut self, points: &[Vec2]) -> Self {\n            self.add_polygon(points);\n            self\n        }\n\n        // --- Tessellation ---\n\n        /// Tessellate the path as a filled shape and register it with the\n        /// renderer. Consumes the builder.\n        pub fn fill(self, renderer: &mut UiRenderer) -> UiPath {\n            let fill_image_id = self.fill_image_id;\n            let path = self.inner.build();\n            let mut geometry = VertexBuffers::<UiVertex, u32>::new();\n            let mut tessellator = FillTessellator::new();\n\n            tessellator\n                .tessellate_path(\n                    path.as_slice(),\n                    &Default::default(),\n                    &mut BuffersBuilder::new(&mut geometry, |mut vertex: FillVertex| {\n                        let p = vertex.position();\n                        let attrs = PathAttributes::from_slice(vertex.interpolated_attributes());\n                        UiVertex {\n                            position: Vec2::new(p.x, p.y),\n                            uv: Vec2::ZERO,\n                            color: attrs.fill_color,\n                        }\n                    }),\n                )\n                .expect(\"fill tessellation failed\");\n\n            // If an image fill is set, compute UVs from the bounding box.\n            if !fill_image_id.is_none() {\n                Self::compute_bounding_box_uvs(&mut geometry);\n            }\n\n            Self::upload(renderer, &geometry, fill_image_id)\n        }\n\n        /// Tessellate the path as a stroked outline and register it with\n        /// the renderer. Consumes the builder.\n        pub fn stroke(self, renderer: &mut UiRenderer) -> UiPath {\n            let fill_image_id = self.fill_image_id;\n            let path = self.inner.build();\n            let mut geometry = VertexBuffers::<UiVertex, u32>::new();\n            let mut tessellator = StrokeTessellator::new();\n\n            let opts = lyon::tessellation::StrokeOptions::default()\n                .with_line_width(self.stroke_config.line_width)\n                .with_line_cap(self.stroke_config.line_cap)\n                .with_line_join(self.stroke_config.line_join);\n\n            tessellator\n                .tessellate_path(\n                    path.as_slice(),\n                    &opts,\n                    &mut BuffersBuilder::new(&mut geometry, |mut vertex: StrokeVertex| {\n                        let p = vertex.position();\n                        let attrs = PathAttributes::from_slice(vertex.interpolated_attributes());\n                        UiVertex {\n                            position: Vec2::new(p.x, p.y),\n                            uv: Vec2::ZERO,\n                            color: attrs.stroke_color,\n                        }\n                    }),\n                )\n                .expect(\"stroke tessellation failed\");\n\n            // If an image fill is set, compute UVs from the bounding box.\n            if !fill_image_id.is_none() {\n                Self::compute_bounding_box_uvs(&mut geometry);\n            }\n\n            Self::upload(renderer, &geometry, fill_image_id)\n        }\n\n        /// Compute UVs from the bounding box of the tessellated vertices.\n        ///\n        /// Maps each vertex position into 0..1 UV space relative to the\n        /// axis-aligned bounding box of all vertices.\n        fn compute_bounding_box_uvs(geometry: &mut VertexBuffers<UiVertex, u32>) {\n            if geometry.vertices.is_empty() {\n                return;\n            }\n            let mut min = Vec2::splat(f32::INFINITY);\n            let mut max = Vec2::splat(f32::NEG_INFINITY);\n            for v in &geometry.vertices {\n                min = min.min(v.position);\n                max = max.max(v.position);\n            }\n            let extent = max - min;\n            let inv_extent = Vec2::new(\n                if extent.x > 0.0 { 1.0 / extent.x } else { 0.0 },\n                if extent.y > 0.0 { 1.0 / extent.y } else { 0.0 },\n            );\n            for v in &mut geometry.vertices {\n                v.uv = (v.position - min) * inv_extent;\n            }\n        }\n\n        /// De-index the tessellated geometry, write vertices to the slab,\n        /// and create a draw call.\n        fn upload(\n            renderer: &mut UiRenderer,\n            geometry: &VertexBuffers<UiVertex, u32>,\n            atlas_texture_id: Id<AtlasTextureDescriptor>,\n        ) -> UiPath {\n            // De-index: expand indexed triangles to flat vertex list.\n            let expanded: Vec<UiVertex> = geometry\n                .indices\n                .iter()\n                .map(|&i| geometry.vertices[i as usize])\n                .collect();\n\n            let vertex_count = expanded.len() as u32;\n            let vertex_array = renderer.slab.new_array(expanded);\n            let vertex_offset = vertex_array.array().starting_index() as u32;\n\n            let mut desc = renderer.default_descriptor(UiElementType::Path);\n            desc.atlas_descriptor_id = Id::new(vertex_offset);\n            desc.atlas_texture_id = atlas_texture_id;\n            let hybrid = renderer.slab.new_value(desc);\n            renderer.draw_calls.push(DrawCall {\n                descriptor: hybrid.clone(),\n                vertex_count,\n            });\n\n            UiPath {\n                inner: hybrid,\n                _vertices: vertex_array,\n            }\n        }\n    }\n\n    /// A live handle to a tessellated path element in the renderer.\n    ///\n    /// **Dropping this handle does NOT remove the path** — call\n    /// [`UiRenderer::remove_path`] explicitly.\n    pub struct UiPath {\n        inner: Hybrid<UiDrawCallDescriptor>,\n        /// Kept alive so the slab doesn't reclaim the vertex data.\n        _vertices: HybridArray<UiVertex>,\n    }\n\n    impl std::fmt::Debug for UiPath {\n        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"UiPath\")\n                .field(\"inner\", &self.inner)\n                .finish_non_exhaustive()\n        }\n    }\n\n    impl UiPath {\n        /// Returns the slab [`Id`] of the underlying descriptor.\n        pub fn id(&self) -> Id<UiDrawCallDescriptor> {\n            self.inner.id()\n        }\n\n        /// Returns a copy of the underlying descriptor.\n        pub fn descriptor(&self) -> UiDrawCallDescriptor {\n            self.inner.get()\n        }\n\n        /// Set the z-depth for sorting.\n        pub fn set_z(&self, z: f32) -> &Self {\n            self.inner.modify(|d| d.z = z);\n            self\n        }\n\n        /// Set the z-depth for sorting (builder).\n        pub fn with_z(self, z: f32) -> Self {\n            self.set_z(z);\n            self\n        }\n\n        /// Set the opacity.\n        pub fn set_opacity(&self, opacity: f32) -> &Self {\n            self.inner.modify(|d| d.opacity = opacity);\n            self\n        }\n\n        /// Set the opacity (builder).\n        pub fn with_opacity(self, opacity: f32) -> Self {\n            self.set_opacity(opacity);\n            self\n        }\n\n        // --- Getters ---\n\n        /// Returns the opacity.\n        pub fn opacity(&self) -> f32 {\n            self.inner.get().opacity\n        }\n\n        /// Returns the z-depth.\n        pub fn z(&self) -> f32 {\n            self.inner.get().z\n        }\n    }\n}\n\n#[cfg(feature = \"path\")]\npub use path::{StrokeConfig, UiPath, UiPathBuilder};\n\n// ---------------------------------------------------------------------------\n// Text types (behind \"text\" feature)\n// ---------------------------------------------------------------------------\n\n#[cfg(feature = \"text\")]\nmod text {\n    use super::*;\n    use glyph_brush::ab_glyph;\n\n    /// Re-export common glyph_brush types for convenience.\n    pub use ab_glyph::FontArc;\n    use glyph_brush::GlyphCruncher as _;\n    pub use glyph_brush::{FontId, Section, Text};\n\n    /// A CPU-side glyph rasterization cache.\n    ///\n    /// Wraps a `GlyphBrush` and maintains a single-channel (Luma8) image\n    /// that accumulates rasterized glyph bitmaps.\n    pub(crate) struct GlyphCache {\n        brush: glyph_brush::GlyphBrush<GlyphQuad>,\n        cache_img: image::ImageBuffer<image::Luma<u8>, Vec<u8>>,\n        /// Cached dimensions (updated whenever cache_img is replaced).\n        cache_w: f32,\n        cache_h: f32,\n        dirty: bool,\n    }\n\n    /// Intermediate representation of one glyph quad produced by the brush.\n    #[derive(Clone, Debug)]\n    pub(crate) struct GlyphQuad {\n        /// Top-left position in screen pixels.\n        pub position: Vec2,\n        /// Size in screen pixels.\n        pub size: Vec2,\n        /// UV rect within the glyph cache image (in pixels).\n        pub tex_offset_px: UVec2,\n        /// UV rect size within the glyph cache image (in pixels).\n        pub tex_size_px: UVec2,\n        /// Text color from the section.\n        pub color: Vec4,\n    }\n\n    impl GlyphCache {\n        /// Create a new glyph cache with the given fonts.\n        pub fn new(fonts: Vec<FontArc>) -> Self {\n            let brush = glyph_brush::GlyphBrushBuilder::using_fonts(fonts).build();\n            let (w, h) = brush.texture_dimensions();\n            Self {\n                brush,\n                cache_img: image::ImageBuffer::from_pixel(w, h, image::Luma([0])),\n                cache_w: w as f32,\n                cache_h: h as f32,\n                dirty: false,\n            }\n        }\n\n        /// Rebuild the brush with the current font set (after adding fonts).\n        pub fn rebuild_with_fonts(&mut self, fonts: Vec<FontArc>) {\n            self.brush = self.brush.to_builder().replace_fonts(|_| fonts).build();\n            let (w, h) = self.brush.texture_dimensions();\n            self.cache_img = image::ImageBuffer::from_pixel(w, h, image::Luma([0]));\n            self.cache_w = w as f32;\n            self.cache_h = h as f32;\n            self.dirty = false;\n        }\n\n        /// Queue a section for layout and rasterization.\n        pub fn queue<'a>(&mut self, section: impl Into<std::borrow::Cow<'a, Section<'a>>>) {\n            self.brush.queue(section);\n        }\n\n        /// Compute the bounding rectangle for a section.\n        pub fn glyph_bounds<'a>(\n            &mut self,\n            section: impl Into<std::borrow::Cow<'a, Section<'a>>>,\n        ) -> Option<ab_glyph::Rect> {\n            self.brush.glyph_bounds(section)\n        }\n\n        /// Process queued sections, rasterizing glyphs and producing quad\n        /// data. Returns `Some(quads)` if new vertices need to be drawn,\n        /// or `None` if the previous frame's data can be reused.\n        ///\n        /// Also marks whether the cache image is dirty (needs re-upload).\n        pub fn process(&mut self) -> Option<Vec<GlyphQuad>> {\n            let cache_img = &mut self.cache_img;\n            let dirty = &mut self.dirty;\n\n            let mut result;\n            loop {\n                // Capture dimensions each iteration (they change on resize).\n                let cw = cache_img.width() as f32;\n                let ch = cache_img.height() as f32;\n                result = self.brush.process_queued(\n                    // Callback: write rasterized glyph data into cache image.\n                    |rect, tex_data| {\n                        let src = image::ImageBuffer::<image::Luma<u8>, Vec<u8>>::from_vec(\n                            rect.width(),\n                            rect.height(),\n                            tex_data.to_vec(),\n                        )\n                        .expect(\"glyph rasterization buffer size mismatch\");\n                        image::imageops::replace(\n                            cache_img,\n                            &src,\n                            rect.min[0] as i64,\n                            rect.min[1] as i64,\n                        );\n                        *dirty = true;\n                    },\n                    // Callback: convert GlyphVertex -> GlyphQuad.\n                    |gv| {\n                        let mut tex_coords = gv.tex_coords;\n                        let pixel_coords = gv.pixel_coords;\n                        let bounds = gv.bounds;\n\n                        // Clip glyph rect to section bounds.\n                        let mut gl_rect = ab_glyph::Rect {\n                            min: ab_glyph::point(pixel_coords.min.x, pixel_coords.min.y),\n                            max: ab_glyph::point(pixel_coords.max.x, pixel_coords.max.y),\n                        };\n\n                        if gl_rect.max.x > bounds.max.x {\n                            let old_width = gl_rect.width();\n                            gl_rect.max.x = bounds.max.x;\n                            tex_coords.max.x =\n                                tex_coords.min.x + tex_coords.width() * gl_rect.width() / old_width;\n                        }\n                        if gl_rect.min.x < bounds.min.x {\n                            let old_width = gl_rect.width();\n                            gl_rect.min.x = bounds.min.x;\n                            tex_coords.min.x =\n                                tex_coords.max.x - tex_coords.width() * gl_rect.width() / old_width;\n                        }\n                        if gl_rect.max.y > bounds.max.y {\n                            let old_height = gl_rect.height();\n                            gl_rect.max.y = bounds.max.y;\n                            tex_coords.max.y = tex_coords.min.y\n                                + tex_coords.height() * gl_rect.height() / old_height;\n                        }\n                        if gl_rect.min.y < bounds.min.y {\n                            let old_height = gl_rect.height();\n                            gl_rect.min.y = bounds.min.y;\n                            tex_coords.min.y = tex_coords.max.y\n                                - tex_coords.height() * gl_rect.height() / old_height;\n                        }\n\n                        // tex_coords are in normalized 0..1 space of the\n                        // glyph cache image. Convert to pixel coordinates.\n                        let tex_offset_px = UVec2::new(\n                            (tex_coords.min.x * cw) as u32,\n                            (tex_coords.min.y * ch) as u32,\n                        );\n                        let tex_size_px = UVec2::new(\n                            ((tex_coords.max.x - tex_coords.min.x) * cw) as u32,\n                            ((tex_coords.max.y - tex_coords.min.y) * ch) as u32,\n                        );\n\n                        GlyphQuad {\n                            position: Vec2::new(gl_rect.min.x, gl_rect.min.y),\n                            size: Vec2::new(gl_rect.width(), gl_rect.height()),\n                            tex_offset_px,\n                            tex_size_px,\n                            color: Vec4::new(\n                                gv.extra.color[0],\n                                gv.extra.color[1],\n                                gv.extra.color[2],\n                                gv.extra.color[3],\n                            ),\n                        }\n                    },\n                );\n\n                match &result {\n                    Err(glyph_brush::BrushError::TextureTooSmall { suggested, .. }) => {\n                        let (new_w, new_h) = *suggested;\n                        let max_dim = 2048;\n                        let (new_w, new_h) = if (new_w > max_dim || new_h > max_dim)\n                            && (cache_img.width() < max_dim || cache_img.height() < max_dim)\n                        {\n                            (max_dim, max_dim)\n                        } else {\n                            (new_w, new_h)\n                        };\n                        *cache_img = image::ImageBuffer::from_pixel(new_w, new_h, image::Luma([0]));\n                        self.brush.resize_texture(new_w, new_h);\n                        *dirty = true;\n                    }\n                    Ok(_) => break,\n                }\n            }\n\n            match result.unwrap() {\n                glyph_brush::BrushAction::Draw(quads) => Some(quads),\n                glyph_brush::BrushAction::ReDraw => None,\n            }\n        }\n\n        /// Returns the cache image if it has been modified since the last\n        /// call to `take_image()`, converting from Luma8 to RGBA8 (white +\n        /// alpha).\n        pub fn take_image(&mut self) -> Option<image::RgbaImage> {\n            if !self.dirty {\n                return None;\n            }\n            self.dirty = false;\n            let (w, h) = (self.cache_img.width(), self.cache_img.height());\n            let rgba = image::RgbaImage::from_fn(w, h, |x, y| {\n                let luma = self.cache_img.get_pixel(x, y).0[0];\n                image::Rgba([255, 255, 255, luma])\n            });\n            Some(rgba)\n        }\n    }\n\n    /// A live handle to a text element in the renderer.\n    ///\n    /// This represents a block of text rendered as a set of glyph quads.\n    /// Each glyph is a separate draw call internally, but they are all\n    /// managed as a single logical element.\n    ///\n    /// **Dropping this handle does NOT remove the text** — call\n    /// [`UiRenderer::remove_text`] explicitly.\n    #[derive(Clone, Debug)]\n    pub struct UiText {\n        /// The descriptors for each glyph quad (one per visible glyph).\n        pub(crate) glyph_descriptors: Vec<Hybrid<UiDrawCallDescriptor>>,\n        /// Per-glyph atlas texture descriptors (kept alive for slab lifetime).\n        #[allow(dead_code)]\n        pub(crate) glyph_atlas_descriptors:\n            Vec<Hybrid<renderling::atlas::shader::AtlasTextureDescriptor>>,\n        /// Bounding box of the text (min, max) in screen pixels.\n        pub(crate) bounds: (Vec2, Vec2),\n        /// Unique identifier for this text block.\n        #[allow(dead_code)]\n        pub(crate) text_id: u64,\n    }\n\n    impl UiText {\n        /// Returns the bounding box of the laid-out text (min, max) in\n        /// screen pixels.\n        pub fn bounds(&self) -> (Vec2, Vec2) {\n            self.bounds\n        }\n\n        /// Set the z-depth for all glyphs in this text block.\n        pub fn set_z(&self, z: f32) -> &Self {\n            for desc in &self.glyph_descriptors {\n                desc.modify(|d| d.z = z);\n            }\n            self\n        }\n\n        /// Set the z-depth for all glyphs (builder).\n        pub fn with_z(self, z: f32) -> Self {\n            self.set_z(z);\n            self\n        }\n\n        /// Set the opacity for all glyphs in this text block.\n        pub fn set_opacity(&self, opacity: f32) -> &Self {\n            for desc in &self.glyph_descriptors {\n                desc.modify(|d| d.opacity = opacity);\n            }\n            self\n        }\n\n        /// Set the opacity for all glyphs (builder).\n        pub fn with_opacity(self, opacity: f32) -> Self {\n            self.set_opacity(opacity);\n            self\n        }\n\n        // --- Getters ---\n\n        /// Returns the opacity (reads from the first glyph, or 1.0 if\n        /// empty).\n        pub fn opacity(&self) -> f32 {\n            self.glyph_descriptors\n                .first()\n                .map(|h| h.get().opacity)\n                .unwrap_or(1.0)\n        }\n\n        /// Returns the z-depth (reads from the first glyph, or 0.0 if\n        /// empty).\n        pub fn z(&self) -> f32 {\n            self.glyph_descriptors\n                .first()\n                .map(|h| h.get().z)\n                .unwrap_or(0.0)\n        }\n    }\n}\n\n#[cfg(feature = \"text\")]\nuse text::GlyphCache;\n#[cfg(feature = \"text\")]\npub use text::{FontArc, FontId, Section, Text, UiText};\n\n// ---------------------------------------------------------------------------\n// Internal draw call entry\n// ---------------------------------------------------------------------------\n\n/// Internal representation of a draw call for the renderer.\nstruct DrawCall {\n    /// The hybrid descriptor (shared with the element wrapper).\n    descriptor: Hybrid<UiDrawCallDescriptor>,\n    /// Number of vertices (6 for quads, variable for paths).\n    vertex_count: u32,\n}\n\n// ---------------------------------------------------------------------------\n// UiRenderer\n// ---------------------------------------------------------------------------\n\n/// The 2D/UI renderer.\n///\n/// This renderer maintains its own lightweight GPU pipeline separate from\n/// renderling's 3D PBR pipeline. It renders directly to a provided\n/// `TextureView` with no intermediate HDR buffer, bloom, or tonemapping.\n///\n/// GPU memory is managed via a [`SlabAllocator`]. Each element is a\n/// [`Hybrid<UiDrawCallDescriptor>`] — modifications via the element\n/// wrapper types are automatically synced to the GPU on the next\n/// [`render`](Self::render) call.\npub struct UiRenderer {\n    slab: SlabAllocator<WgpuRuntime>,\n    viewport: Hybrid<UiViewport>,\n    atlas: Atlas,\n    pipeline: wgpu::RenderPipeline,\n    bindgroup_layout: wgpu::BindGroupLayout,\n    /// Cached slab buffer from the last commit.\n    slab_buffer: Option<SlabBuffer<wgpu::Buffer>>,\n    /// Cached bind group (recreated when slab buffer changes).\n    bindgroup: Option<wgpu::BindGroup>,\n    /// ID of the atlas texture at the time the bind group was created.\n    /// Used to detect when the atlas is recreated and the bind group\n    /// needs rebuilding.\n    bindgroup_atlas_texture_id: Option<usize>,\n    /// All active draw calls, sorted by z before rendering.\n    draw_calls: Vec<DrawCall>,\n    /// Viewport size.\n    viewport_size: UVec2,\n    /// Background clear color.\n    background_color: Option<Vec4>,\n    /// MSAA sample count.\n    msaa_sample_count: u32,\n    /// The texture format of the render target.\n    format: wgpu::TextureFormat,\n    /// MSAA resolve texture (if msaa_sample_count > 1).\n    msaa_texture: Option<wgpu::TextureView>,\n    /// Non-MSAA intermediate texture for overlay compositing.\n    /// Used when `background_color` is `None` and MSAA is active:\n    /// the MSAA texture resolves here, then the compositor blends\n    /// this onto the caller's target view.\n    overlay_texture: Option<wgpu::TextureView>,\n    /// Compositor for alpha-blending the overlay texture onto the\n    /// final target.\n    compositor: Compositor,\n\n    // --- Text support (behind \"text\" feature) ---\n    #[cfg(feature = \"text\")]\n    fonts: Vec<glyph_brush::ab_glyph::FontArc>,\n    #[cfg(feature = \"text\")]\n    glyph_cache: GlyphCache,\n    /// Atlas texture entry for the glyph cache image. Replaced when the\n    /// cache image is re-uploaded.\n    #[cfg(feature = \"text\")]\n    glyph_cache_atlas_texture: Option<AtlasTexture>,\n    /// Monotonic counter for assigning unique text block IDs.\n    #[cfg(feature = \"text\")]\n    next_text_id: u64,\n}\n\nimpl UiRenderer {\n    const LABEL: Option<&'static str> = Some(\"renderling-ui\");\n\n    /// Default atlas texture size.\n    const DEFAULT_ATLAS_SIZE: wgpu::Extent3d = wgpu::Extent3d {\n        width: 512,\n        height: 512,\n        depth_or_array_layers: 2,\n    };\n\n    /// Create a new `UiRenderer` from a renderling `Context`.\n    pub fn new(ctx: &Context) -> Self {\n        let device = ctx.get_device();\n        let size = ctx.get_size();\n        let format = ctx.get_render_target().format();\n\n        let slab = SlabAllocator::new(ctx.runtime(), \"ui-slab\", wgpu::BufferUsages::empty());\n\n        // IMPORTANT: The viewport must be the first slab allocation so it\n        // lands at offset 0. The vertex/fragment shaders read UiViewport\n        // via `Id::new(0)`.\n        let viewport = slab.new_value(UiViewport {\n            projection: Self::ortho2d(size.x as f32, size.y as f32),\n            size,\n            atlas_size: UVec2::new(\n                Self::DEFAULT_ATLAS_SIZE.width,\n                Self::DEFAULT_ATLAS_SIZE.height,\n            ),\n        });\n\n        let atlas = Atlas::new(\n            &slab,\n            Self::DEFAULT_ATLAS_SIZE,\n            None,\n            Some(\"ui-atlas\"),\n            None,\n        );\n\n        let bindgroup_layout = Self::create_bindgroup_layout(device);\n        let default_msaa = 4;\n        let pipeline = Self::create_pipeline(device, &bindgroup_layout, format, default_msaa);\n        let msaa_texture = Some(Self::create_msaa_texture(\n            device,\n            format,\n            size,\n            default_msaa,\n        ));\n        let overlay_texture = Some(Self::create_overlay_texture(device, format, size));\n        let compositor = Compositor::new(device, format);\n\n        Self {\n            slab,\n            viewport,\n            atlas,\n            pipeline,\n            bindgroup_layout,\n            slab_buffer: None,\n            bindgroup: None,\n            bindgroup_atlas_texture_id: None,\n            draw_calls: Vec::new(),\n            viewport_size: size,\n            background_color: None,\n            msaa_sample_count: default_msaa,\n            format,\n            msaa_texture,\n            overlay_texture,\n            compositor,\n            #[cfg(feature = \"text\")]\n            fonts: Vec::new(),\n            #[cfg(feature = \"text\")]\n            glyph_cache: GlyphCache::new(Vec::new()),\n            #[cfg(feature = \"text\")]\n            glyph_cache_atlas_texture: None,\n            #[cfg(feature = \"text\")]\n            next_text_id: 0,\n        }\n    }\n\n    /// Set the background clear color. `None` means don't clear\n    /// (load existing content).\n    pub fn set_background_color(&mut self, color: Option<Vec4>) -> &mut Self {\n        self.background_color = color;\n        self\n    }\n\n    /// Builder-style background color setter.\n    pub fn with_background_color(mut self, color: Vec4) -> Self {\n        self.background_color = Some(color);\n        self\n    }\n\n    /// Set the MSAA sample count (builder).\n    ///\n    /// Higher values produce smoother edges. Common values are 1 (off)\n    /// and 4 (default). The pipeline and MSAA texture are recreated.\n    pub fn with_msaa_sample_count(mut self, count: u32) -> Self {\n        self.msaa_sample_count = count;\n        let device = self.slab.device();\n        self.pipeline = Self::create_pipeline(device, &self.bindgroup_layout, self.format, count);\n        if count > 1 {\n            self.msaa_texture = Some(Self::create_msaa_texture(\n                device,\n                self.format,\n                self.viewport_size,\n                count,\n            ));\n            self.overlay_texture = Some(Self::create_overlay_texture(\n                device,\n                self.format,\n                self.viewport_size,\n            ));\n        } else {\n            self.msaa_texture = None;\n            self.overlay_texture = None;\n        }\n        self\n    }\n\n    /// Set the viewport size (typically matches the render target size).\n    pub fn set_size(&mut self, size: UVec2) {\n        if self.viewport_size != size {\n            self.viewport_size = size;\n            self.viewport.modify(|v| {\n                v.projection = Self::ortho2d(size.x as f32, size.y as f32);\n                v.size = size;\n            });\n\n            // Recreate MSAA texture if needed.\n            if self.msaa_sample_count > 1 {\n                self.msaa_texture = Some(Self::create_msaa_texture(\n                    self.slab.device(),\n                    self.format,\n                    size,\n                    self.msaa_sample_count,\n                ));\n                self.overlay_texture = Some(Self::create_overlay_texture(\n                    self.slab.device(),\n                    self.format,\n                    size,\n                ));\n            }\n        }\n    }\n\n    /// Add a rectangle element and return a live handle.\n    ///\n    /// The element starts with sensible defaults (100x100 white rect\n    /// at the origin). Use the `with_*` builder methods or `set_*`\n    /// methods to configure it.\n    ///\n    /// ```ignore\n    /// let rect = ui.add_rect()\n    ///     .with_position(Vec2::new(10.0, 10.0))\n    ///     .with_size(Vec2::new(200.0, 100.0))\n    ///     .with_fill_color(Vec4::new(0.2, 0.4, 0.8, 1.0));\n    /// ```\n    pub fn add_rect(&mut self) -> UiRect {\n        let desc = self.default_descriptor(UiElementType::Rectangle);\n        let hybrid = self.slab.new_value(desc);\n        let element = UiRect {\n            inner: hybrid.clone(),\n        };\n        self.draw_calls.push(DrawCall {\n            descriptor: hybrid,\n            vertex_count: 6,\n        });\n        element\n    }\n\n    /// Add a circle element and return a live handle.\n    ///\n    /// The element starts centered at (0, 0) with radius 50 and\n    /// white fill. Use `with_center`, `with_radius`, etc. to\n    /// configure.\n    pub fn add_circle(&mut self) -> UiCircle {\n        let desc = self.default_descriptor(UiElementType::Circle);\n        let hybrid = self.slab.new_value(desc);\n        let element = UiCircle {\n            inner: hybrid.clone(),\n        };\n        self.draw_calls.push(DrawCall {\n            descriptor: hybrid,\n            vertex_count: 6,\n        });\n        element\n    }\n\n    /// Add an ellipse element and return a live handle.\n    ///\n    /// The element starts centered at (0, 0) with size 100x100 and\n    /// white fill. Use `with_center`, `with_radii`, etc. to\n    /// configure.\n    pub fn add_ellipse(&mut self) -> UiEllipse {\n        let desc = self.default_descriptor(UiElementType::Ellipse);\n        let hybrid = self.slab.new_value(desc);\n        let element = UiEllipse {\n            inner: hybrid.clone(),\n        };\n        self.draw_calls.push(DrawCall {\n            descriptor: hybrid,\n            vertex_count: 6,\n        });\n        element\n    }\n\n    /// Add an image element and return a live handle.\n    ///\n    /// The image is loaded into the atlas from an [`AtlasImage`]\n    /// (CPU-side pixel data). The element is sized to match the\n    /// image dimensions by default.\n    ///\n    /// ```ignore\n    /// let img = image::open(\"icon.png\").unwrap();\n    /// let _icon = ui.add_image(img.into())\n    ///     .with_position(Vec2::new(10.0, 10.0));\n    /// ```\n    pub fn add_image(&mut self, image: impl Into<AtlasImage>) -> UiImage {\n        let image = image.into();\n        let image_size = image.size;\n        let atlas_texture = self\n            .atlas\n            .add_image(&image)\n            .expect(\"failed to add image to atlas\");\n\n        // Update the viewport with the (possibly new) atlas size.\n        let atlas_extent = self.atlas.get_size();\n        self.viewport.modify(|v| {\n            v.atlas_size = UVec2::new(atlas_extent.width, atlas_extent.height);\n        });\n\n        let mut desc = self.default_descriptor(UiElementType::Image);\n        desc.size = Vec2::new(image_size.x as f32, image_size.y as f32);\n        desc.atlas_texture_id = atlas_texture.id();\n        desc.fill_color = Vec4::ONE; // no tint\n\n        let hybrid = self.slab.new_value(desc);\n        let element = UiImage {\n            inner: hybrid.clone(),\n            atlas_texture,\n        };\n        self.draw_calls.push(DrawCall {\n            descriptor: hybrid,\n            vertex_count: 6,\n        });\n        element\n    }\n\n    /// Upload an image to the atlas without creating a draw call.\n    ///\n    /// Returns the [`AtlasTexture`] handle, which can be passed to\n    /// [`UiPathBuilder::with_fill_image`] for image-filled paths\n    /// or used for other custom purposes.\n    ///\n    /// ```ignore\n    /// let atlas_img = AtlasImage::from_path(\"icon.png\").unwrap();\n    /// let tex = ui.upload_image(atlas_img);\n    /// let _path = ui.path_builder()\n    ///     .with_fill_image(&tex)\n    ///     .with_fill_color(Vec4::ONE)\n    ///     .with_circle(Vec2::new(50.0, 50.0), 30.0)\n    ///     .fill(&mut ui);\n    /// ```\n    pub fn upload_image(&mut self, image: impl Into<AtlasImage>) -> AtlasTexture {\n        let image = image.into();\n        let atlas_texture = self\n            .atlas\n            .add_image(&image)\n            .expect(\"failed to add image to atlas\");\n\n        // Update the viewport with the (possibly new) atlas size.\n        let atlas_extent = self.atlas.get_size();\n        self.viewport.modify(|v| {\n            v.atlas_size = UVec2::new(atlas_extent.width, atlas_extent.height);\n        });\n\n        atlas_texture\n    }\n\n    /// Register a font and return its [`FontId`].\n    ///\n    /// Fonts must be registered before they can be used in\n    /// [`Section`]/[`Text`] for [`add_text`](Self::add_text).\n    ///\n    /// ```ignore\n    /// let bytes = std::fs::read(\"fonts/MyFont.ttf\").unwrap();\n    /// let font = FontArc::try_from_vec(bytes).unwrap();\n    /// let font_id = ui.add_font(font);\n    /// ```\n    #[cfg(feature = \"text\")]\n    pub fn add_font(&mut self, font: FontArc) -> FontId {\n        let id = self.fonts.len();\n        self.fonts.push(font);\n        self.glyph_cache.rebuild_with_fonts(self.fonts.clone());\n        FontId(id)\n    }\n\n    /// Add a text element from a glyph_brush [`Section`].\n    ///\n    /// This rasterizes the glyphs, uploads the cache image to the atlas,\n    /// and creates one draw call per visible glyph.\n    ///\n    /// ```ignore\n    /// use glyph_brush::{Section, Text};\n    /// let font_id = ui.add_font(my_font);\n    /// let _text = ui.add_text(\n    ///     Section::default()\n    ///         .add_text(\n    ///             Text::new(\"Hello, UI!\")\n    ///                 .with_scale(32.0)\n    ///                 .with_color([0.0, 0.0, 0.0, 1.0])\n    ///         )\n    ///         .with_screen_position((10.0, 10.0))\n    /// );\n    /// ```\n    #[cfg(feature = \"text\")]\n    pub fn add_text<'a>(\n        &mut self,\n        section: impl Into<std::borrow::Cow<'a, Section<'a>>>,\n    ) -> UiText {\n        use renderling::atlas::shader::AtlasTextureDescriptor;\n\n        let section = section.into();\n\n        // Compute text bounds.\n        let bounds = self\n            .glyph_cache\n            .glyph_bounds(section.clone())\n            .map(|r| (Vec2::new(r.min.x, r.min.y), Vec2::new(r.max.x, r.max.y)))\n            .unwrap_or((Vec2::ZERO, Vec2::ZERO));\n\n        // Queue and process.\n        self.glyph_cache.queue(section);\n        let quads = self.glyph_cache.process().unwrap_or_default();\n\n        // Upload the glyph cache image to the atlas (if dirty).\n        if let Some(rgba_img) = self.glyph_cache.take_image() {\n            // Drop old atlas entry (if any) so the atlas can reclaim space.\n            self.glyph_cache_atlas_texture = None;\n\n            let atlas_img = AtlasImage::from(image::DynamicImage::ImageRgba8(rgba_img));\n            let atlas_tex = self\n                .atlas\n                .add_image(&atlas_img)\n                .expect(\"failed to upload glyph cache to atlas\");\n\n            // Update the viewport with the (possibly new) atlas size.\n            let atlas_extent = self.atlas.get_size();\n            self.viewport.modify(|v| {\n                v.atlas_size = UVec2::new(atlas_extent.width, atlas_extent.height);\n            });\n\n            self.glyph_cache_atlas_texture = Some(atlas_tex);\n        }\n\n        // Get the atlas placement of the glyph cache image.\n        let cache_atlas_desc = self\n            .glyph_cache_atlas_texture\n            .as_ref()\n            .expect(\"glyph cache not uploaded\")\n            .descriptor();\n\n        let text_id = self.next_text_id;\n        self.next_text_id += 1;\n\n        let mut glyph_descriptors = Vec::with_capacity(quads.len());\n        let mut glyph_atlas_descriptors = Vec::with_capacity(quads.len());\n\n        for quad in &quads {\n            // Create an AtlasTextureDescriptor for this specific glyph's\n            // sub-region within the glyph cache, which itself is a sub-\n            // region of the atlas.\n            let glyph_atlas_desc = AtlasTextureDescriptor {\n                offset_px: cache_atlas_desc.offset_px + quad.tex_offset_px,\n                size_px: quad.tex_size_px,\n                layer_index: cache_atlas_desc.layer_index,\n                frame_index: 0,\n                ..Default::default()\n            };\n            let glyph_atlas_hybrid = self.slab.new_value(glyph_atlas_desc);\n\n            let mut desc = self.default_descriptor(UiElementType::TextGlyph);\n            desc.position = quad.position;\n            desc.size = quad.size;\n            desc.fill_color = quad.color;\n            desc.atlas_texture_id = glyph_atlas_hybrid.id();\n\n            let hybrid = self.slab.new_value(desc);\n            self.draw_calls.push(DrawCall {\n                descriptor: hybrid.clone(),\n                vertex_count: 6,\n            });\n\n            glyph_descriptors.push(hybrid);\n            glyph_atlas_descriptors.push(glyph_atlas_hybrid);\n        }\n\n        UiText {\n            glyph_descriptors,\n            glyph_atlas_descriptors,\n            bounds,\n            text_id,\n        }\n    }\n\n    /// Remove a rectangle element by its handle.\n    pub fn remove_rect(&mut self, element: &UiRect) {\n        self.remove_by_id(element.id());\n    }\n\n    /// Remove a circle element by its handle.\n    pub fn remove_circle(&mut self, element: &UiCircle) {\n        self.remove_by_id(element.id());\n    }\n\n    /// Remove an ellipse element by its handle.\n    pub fn remove_ellipse(&mut self, element: &UiEllipse) {\n        self.remove_by_id(element.id());\n    }\n\n    /// Remove an image element by its handle.\n    pub fn remove_image(&mut self, element: &UiImage) {\n        self.remove_by_id(element.id());\n    }\n\n    /// Remove a text element by its handle.\n    #[cfg(feature = \"text\")]\n    pub fn remove_text(&mut self, element: &UiText) {\n        for desc in &element.glyph_descriptors {\n            self.remove_by_id(desc.id());\n        }\n    }\n\n    /// Create a new path builder for constructing vector paths.\n    ///\n    /// Use the builder's methods to define shapes, then call `.fill()`\n    /// or `.stroke()` to tessellate and register the path.\n    ///\n    /// ```ignore\n    /// let path = ui.path_builder()\n    ///     .with_fill_color(Vec4::new(1.0, 0.0, 0.0, 1.0))\n    ///     .with_circle(Vec2::new(50.0, 50.0), 30.0)\n    ///     .fill(&mut ui);\n    /// ```\n    #[cfg(feature = \"path\")]\n    pub fn path_builder(&self) -> UiPathBuilder {\n        UiPathBuilder::new()\n    }\n\n    /// Remove a path element by its handle.\n    #[cfg(feature = \"path\")]\n    pub fn remove_path(&mut self, element: &UiPath) {\n        self.remove_by_id(element.id());\n    }\n\n    /// Remove all elements.\n    pub fn clear(&mut self) {\n        self.draw_calls.clear();\n        // Dropping the Hybrid values reclaims slab memory on next\n        // commit.\n    }\n\n    /// Render all UI elements to the given texture view.\n    pub fn render(&mut self, view: &wgpu::TextureView) {\n        if self.draw_calls.is_empty() {\n            return;\n        }\n\n        // Sort draw calls by z (painter's algorithm).\n        // We read z from the CPU-side Hybrid each frame.\n        let mut sorted_indices: Vec<usize> = (0..self.draw_calls.len()).collect();\n        sorted_indices.sort_by(|a, b| {\n            let z_a = self.draw_calls[*a].descriptor.get().z;\n            let z_b = self.draw_calls[*b].descriptor.get().z;\n            z_a.partial_cmp(&z_b).unwrap_or(core::cmp::Ordering::Equal)\n        });\n\n        // Run atlas upkeep (garbage-collect dropped textures).\n        let atlas_texture_recreated = self.atlas.upkeep(self.slab.runtime());\n        if atlas_texture_recreated {\n            // Update viewport with new atlas size.\n            let extent = self.atlas.get_size();\n            self.viewport.modify(|v| {\n                v.atlas_size = UVec2::new(extent.width, extent.height);\n            });\n        }\n\n        // Commit slab changes to the GPU.\n        let buffer = self.slab.commit();\n\n        // Check if bind group needs recreation: slab buffer changed,\n        // atlas texture changed, or first render.\n        let atlas_tex = self.atlas.get_texture();\n        let atlas_tex_id = atlas_tex.id();\n        let atlas_changed = self.bindgroup_atlas_texture_id != Some(atlas_tex_id);\n        let should_recreate_bindgroup =\n            buffer.is_new_this_commit() || atlas_changed || self.bindgroup.is_none();\n\n        if should_recreate_bindgroup {\n            self.bindgroup = Some(self.create_bindgroup(&buffer, &atlas_tex));\n            self.bindgroup_atlas_texture_id = Some(atlas_tex_id);\n        }\n        drop(atlas_tex);\n        self.slab_buffer = Some(buffer);\n\n        let device = self.slab.device();\n        let queue = self.slab.queue();\n\n        // Create command encoder.\n        let mut encoder =\n            device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Self::LABEL });\n\n        let is_overlay = self.background_color.is_none();\n        let use_msaa = self.msaa_sample_count > 1;\n\n        // Determine load op, color attachment, and resolve target.\n        //\n        // Overlay + MSAA: clear MSAA to transparent, resolve to\n        //   intermediate overlay texture, then compositor blends onto\n        //   the caller's view.\n        // Overlay + no MSAA: load existing view content, render\n        //   directly (alpha blending preserves the scene).\n        // Standalone: clear to background color, resolve (or render)\n        //   directly to the caller's view.\n        let load_op = if let Some(bg) = self.background_color {\n            wgpu::LoadOp::Clear(wgpu::Color {\n                r: bg.x as f64,\n                g: bg.y as f64,\n                b: bg.z as f64,\n                a: bg.w as f64,\n            })\n        } else if use_msaa {\n            // Overlay + MSAA: clear the MSAA texture to transparent\n            // so non-UI pixels resolve as fully transparent.\n            wgpu::LoadOp::Clear(wgpu::Color {\n                r: 0.0,\n                g: 0.0,\n                b: 0.0,\n                a: 0.0,\n            })\n        } else {\n            wgpu::LoadOp::Load\n        };\n\n        let (color_view, resolve_target) = if use_msaa {\n            if let Some(msaa_view) = &self.msaa_texture {\n                if is_overlay {\n                    // Overlay: resolve to intermediate texture\n                    // (NOT the caller's view, which would overwrite\n                    // the 3D scene).\n                    let resolve = self.overlay_texture.as_ref().unwrap();\n                    (msaa_view as &wgpu::TextureView, Some(resolve))\n                } else {\n                    // Standalone: resolve directly to the caller's\n                    // view.\n                    (msaa_view as &wgpu::TextureView, Some(view))\n                }\n            } else {\n                (view, None)\n            }\n        } else {\n            (view, None)\n        };\n\n        {\n            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {\n                label: Self::LABEL,\n                color_attachments: &[Some(wgpu::RenderPassColorAttachment {\n                    view: color_view,\n                    resolve_target,\n                    ops: wgpu::Operations {\n                        load: load_op,\n                        store: wgpu::StoreOp::Store,\n                    },\n                    depth_slice: None,\n                })],\n                depth_stencil_attachment: None,\n                timestamp_writes: None,\n                occlusion_query_set: None,\n            });\n\n            render_pass.set_pipeline(&self.pipeline);\n            render_pass.set_bind_group(0, self.bindgroup.as_ref().unwrap(), &[]);\n\n            // Issue one draw call per element, sorted by z.\n            // The instance_index encodes the slab offset of the\n            // UiDrawCallDescriptor.\n            for &idx in &sorted_indices {\n                let dc = &self.draw_calls[idx];\n                let inst = dc.descriptor.id().inner();\n                render_pass.draw(0..dc.vertex_count, inst..inst + 1);\n            }\n        }\n\n        queue.submit(Some(encoder.finish()));\n\n        // Overlay + MSAA: alpha-blend the resolved UI texture onto\n        // the caller's view, preserving the 3D scene underneath.\n        if is_overlay && use_msaa {\n            if let Some(overlay) = &self.overlay_texture {\n                self.compositor.composite(device, queue, overlay, view);\n            }\n        }\n    }\n\n    // --- Private helpers ---\n\n    fn ortho2d(width: f32, height: f32) -> Mat4 {\n        Mat4::orthographic_rh(\n            0.0,    // left\n            width,  // right\n            height, // bottom\n            0.0,    // top\n            -1.0,   // near\n            1.0,    // far\n        )\n    }\n\n    /// Build a default [`UiDrawCallDescriptor`] for the given element\n    /// type, using the current viewport as the clip rect.\n    fn default_descriptor(&self, element_type: UiElementType) -> UiDrawCallDescriptor {\n        UiDrawCallDescriptor {\n            element_type,\n            position: Vec2::ZERO,\n            size: Vec2::new(100.0, 100.0),\n            corner_radii: Vec4::ZERO,\n            border_width: 0.0,\n            border_color: Vec4::ZERO,\n            fill_color: Vec4::ONE,\n            gradient: GradientDescriptor::default(),\n            atlas_texture_id: Id::NONE,\n            atlas_descriptor_id: Id::NONE,\n            clip_rect: Vec4::new(\n                0.0,\n                0.0,\n                self.viewport_size.x as f32,\n                self.viewport_size.y as f32,\n            ),\n            opacity: 1.0,\n            z: 0.0,\n        }\n    }\n\n    fn create_bindgroup_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {\n        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {\n            label: Self::LABEL,\n            entries: &[\n                // Binding 0: Slab storage buffer.\n                wgpu::BindGroupLayoutEntry {\n                    binding: 0,\n                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Buffer {\n                        ty: wgpu::BufferBindingType::Storage { read_only: true },\n                        has_dynamic_offset: false,\n                        min_binding_size: None,\n                    },\n                    count: None,\n                },\n                // Binding 1: Atlas texture (2D array).\n                wgpu::BindGroupLayoutEntry {\n                    binding: 1,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Texture {\n                        sample_type: wgpu::TextureSampleType::Float { filterable: true },\n                        view_dimension: wgpu::TextureViewDimension::D2Array,\n                        multisampled: false,\n                    },\n                    count: None,\n                },\n                // Binding 2: Atlas sampler.\n                wgpu::BindGroupLayoutEntry {\n                    binding: 2,\n                    visibility: wgpu::ShaderStages::FRAGMENT,\n                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),\n                    count: None,\n                },\n            ],\n        })\n    }\n\n    fn create_pipeline(\n        device: &wgpu::Device,\n        bindgroup_layout: &wgpu::BindGroupLayout,\n        format: wgpu::TextureFormat,\n        msaa_sample_count: u32,\n    ) -> wgpu::RenderPipeline {\n        let vertex_linkage = renderling::linkage::ui_vertex::linkage(device);\n        let fragment_linkage = renderling::linkage::ui_fragment::linkage(device);\n\n        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {\n            label: Self::LABEL,\n            bind_group_layouts: &[bindgroup_layout],\n            push_constant_ranges: &[],\n        });\n\n        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {\n            label: Self::LABEL,\n            layout: Some(&pipeline_layout),\n            vertex: wgpu::VertexState {\n                module: &vertex_linkage.module,\n                entry_point: Some(vertex_linkage.entry_point),\n                compilation_options: wgpu::PipelineCompilationOptions::default(),\n                buffers: &[],\n            },\n            primitive: wgpu::PrimitiveState {\n                topology: wgpu::PrimitiveTopology::TriangleList,\n                strip_index_format: None,\n                front_face: wgpu::FrontFace::Ccw,\n                cull_mode: None,\n                unclipped_depth: false,\n                polygon_mode: wgpu::PolygonMode::Fill,\n                conservative: false,\n            },\n            depth_stencil: None,\n            multisample: wgpu::MultisampleState {\n                count: msaa_sample_count,\n                mask: !0,\n                alpha_to_coverage_enabled: false,\n            },\n            fragment: Some(wgpu::FragmentState {\n                module: &fragment_linkage.module,\n                entry_point: Some(fragment_linkage.entry_point),\n                compilation_options: wgpu::PipelineCompilationOptions::default(),\n                targets: &[Some(wgpu::ColorTargetState {\n                    format,\n                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),\n                    write_mask: wgpu::ColorWrites::ALL,\n                })],\n            }),\n            multiview: None,\n            cache: None,\n        })\n    }\n\n    fn create_msaa_texture(\n        device: &wgpu::Device,\n        format: wgpu::TextureFormat,\n        size: UVec2,\n        sample_count: u32,\n    ) -> wgpu::TextureView {\n        let texture = device.create_texture(&wgpu::TextureDescriptor {\n            label: Some(\"renderling-ui-msaa\"),\n            size: wgpu::Extent3d {\n                width: size.x,\n                height: size.y,\n                depth_or_array_layers: 1,\n            },\n            mip_level_count: 1,\n            sample_count,\n            dimension: wgpu::TextureDimension::D2,\n            format,\n            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,\n            view_formats: &[],\n        });\n        texture.create_view(&wgpu::TextureViewDescriptor::default())\n    }\n\n    /// Create a non-MSAA intermediate texture for overlay compositing.\n    ///\n    /// When the UI is rendered as an overlay (no background clear) with\n    /// MSAA enabled, the MSAA texture resolves into this intermediate\n    /// texture, which is then alpha-blended onto the final target by\n    /// the compositor.\n    fn create_overlay_texture(\n        device: &wgpu::Device,\n        format: wgpu::TextureFormat,\n        size: UVec2,\n    ) -> wgpu::TextureView {\n        let texture = device.create_texture(&wgpu::TextureDescriptor {\n            label: Some(\"renderling-ui-overlay\"),\n            size: wgpu::Extent3d {\n                width: size.x,\n                height: size.y,\n                depth_or_array_layers: 1,\n            },\n            mip_level_count: 1,\n            sample_count: 1,\n            dimension: wgpu::TextureDimension::D2,\n            format,\n            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,\n            view_formats: &[],\n        });\n        texture.create_view(&wgpu::TextureViewDescriptor::default())\n    }\n\n    /// Create a bind group using the given slab buffer and atlas\n    /// texture.\n    fn create_bindgroup(\n        &self,\n        buffer: &SlabBuffer<wgpu::Buffer>,\n        atlas_tex: &renderling::texture::Texture,\n    ) -> wgpu::BindGroup {\n        self.slab\n            .device()\n            .create_bind_group(&wgpu::BindGroupDescriptor {\n                label: Self::LABEL,\n                layout: &self.bindgroup_layout,\n                entries: &[\n                    wgpu::BindGroupEntry {\n                        binding: 0,\n                        resource: buffer.as_entire_binding(),\n                    },\n                    wgpu::BindGroupEntry {\n                        binding: 1,\n                        resource: wgpu::BindingResource::TextureView(&atlas_tex.view),\n                    },\n                    wgpu::BindGroupEntry {\n                        binding: 2,\n                        resource: wgpu::BindingResource::Sampler(&atlas_tex.sampler),\n                    },\n                ],\n            })\n    }\n\n    /// Remove a draw call by its slab ID.\n    fn remove_by_id(&mut self, id: Id<UiDrawCallDescriptor>) {\n        self.draw_calls.retain(|dc| dc.descriptor.id() != id);\n        // The Hybrid is dropped here (removed from draw_calls Vec),\n        // which will cause the slab to reclaim its memory on the\n        // next commit.\n    }\n}\n"
  },
  {
    "path": "crates/renderling-ui/src/test.rs",
    "content": "//! Tests for the 2D/UI renderer.\n\n#[cfg(test)]\nmod tests {\n    use glam::{Vec2, Vec4};\n\n    use crate::{GradientDescriptor, UiRenderer};\n    use renderling::context::Context;\n\n    fn init_logging() {\n        let _ = env_logger::builder().is_test(true).try_init();\n    }\n\n    /// Save the rendered image for visual inspection and as a baseline\n    /// reference. Uses `img_diff::assert_img_eq` which will create the\n    /// expected image on first run.\n    fn save_and_assert(name: &str, img: image::RgbaImage) {\n        // Save a copy to test_output for inspection.\n        img_diff::save(name, img.clone());\n        // If the expected image doesn't exist yet, save it as the baseline.\n        let test_img_path = renderling_build::test_img_dir().join(name);\n        if !test_img_path.exists() {\n            std::fs::create_dir_all(test_img_path.parent().unwrap()).unwrap();\n            image::DynamicImage::from(img.clone())\n                .save(&test_img_path)\n                .unwrap();\n            log::info!(\"saved baseline image: {}\", test_img_path.display());\n        }\n        img_diff::assert_img_eq(name, img);\n    }\n\n    #[test]\n    fn can_render_rect() {\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(200, 200));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        let _rect = ui\n            .add_rect()\n            .with_position(Vec2::new(10.0, 10.0))\n            .with_size(Vec2::new(80.0, 60.0))\n            .with_fill_color(Vec4::new(0.2, 0.4, 0.8, 1.0));\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/rect.png\", img);\n    }\n\n    #[test]\n    fn can_render_rounded_rect() {\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(200, 200));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        let _rect = ui\n            .add_rect()\n            .with_position(Vec2::new(10.0, 10.0))\n            .with_size(Vec2::new(120.0, 80.0))\n            .with_corner_radii(Vec4::splat(16.0))\n            .with_fill_color(Vec4::new(0.8, 0.2, 0.3, 1.0));\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/rounded_rect.png\", img);\n    }\n\n    #[test]\n    fn can_render_circle() {\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(200, 200));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        let _circle = ui\n            .add_circle()\n            .with_center(Vec2::new(100.0, 100.0))\n            .with_radius(40.0)\n            .with_fill_color(Vec4::new(0.1, 0.7, 0.3, 1.0));\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/circle.png\", img);\n    }\n\n    #[test]\n    fn can_render_bordered_rect() {\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(200, 200));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        let _rect = ui\n            .add_rect()\n            .with_position(Vec2::new(20.0, 20.0))\n            .with_size(Vec2::new(100.0, 80.0))\n            .with_corner_radii(Vec4::splat(12.0))\n            .with_fill_color(Vec4::new(0.95, 0.95, 0.8, 1.0))\n            .with_border(3.0, Vec4::new(0.2, 0.2, 0.2, 1.0));\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/bordered_rect.png\", img);\n    }\n\n    #[test]\n    fn can_render_multiple_shapes() {\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(300, 200));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        // Background rect\n        let _rect = ui\n            .add_rect()\n            .with_position(Vec2::new(10.0, 10.0))\n            .with_size(Vec2::new(120.0, 80.0))\n            .with_corner_radii(Vec4::splat(8.0))\n            .with_fill_color(Vec4::new(0.2, 0.4, 0.8, 1.0))\n            .with_z(0.0);\n\n        // Circle on top\n        let _circle = ui\n            .add_circle()\n            .with_center(Vec2::new(200.0, 100.0))\n            .with_radius(35.0)\n            .with_fill_color(Vec4::new(0.9, 0.3, 0.1, 1.0))\n            .with_border(2.0, Vec4::new(0.0, 0.0, 0.0, 1.0))\n            .with_z(0.1);\n\n        // Ellipse\n        let _ellipse = ui\n            .add_ellipse()\n            .with_center(Vec2::new(150.0, 150.0))\n            .with_radii(Vec2::new(60.0, 30.0))\n            .with_fill_color(Vec4::new(0.1, 0.8, 0.4, 0.8))\n            .with_z(0.2);\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/multiple_shapes.png\", img);\n    }\n\n    #[test]\n    fn can_render_image() {\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(200, 200));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        // Create a programmatic 64x64 checkerboard image.\n        let size = 64u32;\n        let mut img = image::RgbaImage::new(size, size);\n        for y in 0..size {\n            for x in 0..size {\n                let checker = ((x / 8) + (y / 8)) % 2 == 0;\n                let c = if checker { 255 } else { 80 };\n                img.put_pixel(x, y, image::Rgba([c, c, c, 255]));\n            }\n        }\n\n        let atlas_img: renderling::atlas::AtlasImage = image::DynamicImage::ImageRgba8(img).into();\n        let _image_el = ui.add_image(atlas_img).with_position(Vec2::new(20.0, 20.0));\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let output = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/image.png\", output);\n    }\n\n    #[test]\n    fn can_render_image_with_tint() {\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(200, 200));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        // Create a 64x64 solid white image.\n        let size = 64u32;\n        let img = image::RgbaImage::from_pixel(size, size, image::Rgba([255, 255, 255, 255]));\n        let atlas_img: renderling::atlas::AtlasImage = image::DynamicImage::ImageRgba8(img).into();\n\n        // Apply a red tint.\n        let _image_el = ui\n            .add_image(atlas_img)\n            .with_position(Vec2::new(50.0, 50.0))\n            .with_tint(Vec4::new(1.0, 0.0, 0.0, 1.0));\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let output = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/image_tint.png\", output);\n    }\n\n    #[test]\n    fn can_render_gradient_rect() {\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(200, 200));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        let _rect = ui\n            .add_rect()\n            .with_position(Vec2::new(20.0, 20.0))\n            .with_size(Vec2::new(160.0, 100.0))\n            .with_corner_radii(Vec4::splat(12.0))\n            .with_gradient(Some(GradientDescriptor {\n                gradient_type: 1, // Linear\n                start: Vec2::new(0.0, 0.0),\n                end: Vec2::new(1.0, 0.0),\n                radius: 0.0,\n                color_start: Vec4::new(1.0, 0.0, 0.0, 1.0),\n                color_end: Vec4::new(0.0, 0.0, 1.0, 1.0),\n            }));\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/gradient_rect.png\", img);\n    }\n\n    #[cfg(feature = \"text\")]\n    #[test]\n    fn can_render_text() {\n        use crate::{FontArc, Section, Text};\n\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(400, 100));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        let font_bytes =\n            std::fs::read(\"../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf\").unwrap();\n        let font = FontArc::try_from_vec(font_bytes).unwrap();\n        let _font_id = ui.add_font(font);\n\n        let _text = ui.add_text(\n            Section::default()\n                .add_text(\n                    Text::new(\"Hello, renderling-ui!\")\n                        .with_scale(32.0)\n                        .with_color([0.0, 0.0, 0.0, 1.0]),\n                )\n                .with_screen_position((10.0, 10.0)),\n        );\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/text.png\", img);\n    }\n\n    #[cfg(feature = \"path\")]\n    #[test]\n    fn can_render_filled_path() {\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(200, 200));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        // Filled red triangle.\n        let _path = ui\n            .path_builder()\n            .with_fill_color(Vec4::new(1.0, 0.0, 0.0, 1.0))\n            .with_begin(Vec2::new(100.0, 20.0))\n            .with_line_to(Vec2::new(180.0, 160.0))\n            .with_line_to(Vec2::new(20.0, 160.0))\n            .with_end(true)\n            .fill(&mut ui);\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/filled_path.png\", img);\n    }\n\n    #[cfg(feature = \"path\")]\n    #[test]\n    fn can_render_stroked_path() {\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(200, 200));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        // Blue stroked circle.\n        let _path = ui\n            .path_builder()\n            .with_stroke_color(Vec4::new(0.0, 0.0, 1.0, 1.0))\n            .with_circle(Vec2::new(100.0, 100.0), 60.0)\n            .stroke(&mut ui);\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/stroked_path.png\", img);\n    }\n\n    #[cfg(feature = \"path\")]\n    #[test]\n    fn can_render_path_shapes() {\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(300, 200));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        // Filled rounded rectangle.\n        let _rect = ui\n            .path_builder()\n            .with_fill_color(Vec4::new(0.2, 0.6, 0.3, 1.0))\n            .with_rounded_rectangle(\n                Vec2::new(10.0, 10.0),\n                Vec2::new(140.0, 90.0),\n                12.0,\n                12.0,\n                12.0,\n                12.0,\n            )\n            .fill(&mut ui);\n\n        // Stroked ellipse.\n        let _ellipse = ui\n            .path_builder()\n            .with_stroke_color(Vec4::new(0.8, 0.2, 0.1, 1.0))\n            .with_stroke_config(crate::StrokeConfig {\n                line_width: 3.0,\n                ..Default::default()\n            })\n            .with_ellipse(Vec2::new(220.0, 100.0), Vec2::new(60.0, 40.0), 0.0)\n            .stroke(&mut ui);\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/path_shapes.png\", img);\n    }\n\n    #[cfg(feature = \"text\")]\n    #[test]\n    fn can_render_text_with_shapes() {\n        use crate::{FontArc, Section, Text};\n\n        init_logging();\n        let ctx = futures_lite::future::block_on(Context::headless(400, 100));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        let font_bytes =\n            std::fs::read(\"../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf\").unwrap();\n        let font = FontArc::try_from_vec(font_bytes).unwrap();\n        let _font_id = ui.add_font(font);\n\n        // Background rounded rect behind the text.\n        let _bg = ui\n            .add_rect()\n            .with_position(Vec2::new(5.0, 5.0))\n            .with_size(Vec2::new(350.0, 50.0))\n            .with_corner_radii(Vec4::splat(8.0))\n            .with_fill_color(Vec4::new(0.2, 0.4, 0.8, 1.0))\n            .with_z(0.0);\n\n        // Text on top.\n        let _text = ui\n            .add_text(\n                Section::default()\n                    .add_text(\n                        Text::new(\"Text on a rect!\")\n                            .with_scale(28.0)\n                            .with_color([1.0, 1.0, 1.0, 1.0]),\n                    )\n                    .with_screen_position((15.0, 15.0)),\n            )\n            .with_z(0.1);\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/text_with_shapes.png\", img);\n    }\n\n    /// Generates points for a star shape.\n    ///\n    /// `num_points` is the number of tips. Points alternate between\n    /// `outer_radius` and `inner_radius`.\n    fn star_points(num_points: usize, outer_radius: f32, inner_radius: f32) -> Vec<Vec2> {\n        let mut points = Vec::with_capacity(num_points * 2);\n        let angle_step = std::f32::consts::PI / num_points as f32;\n        for i in 0..num_points * 2 {\n            let angle = angle_step * i as f32;\n            let radius = if i % 2 == 0 {\n                outer_radius\n            } else {\n                inner_radius\n            };\n            points.push(Vec2::new(radius * angle.cos(), radius * angle.sin()));\n        }\n        points\n    }\n\n    #[cfg(feature = \"path\")]\n    #[test]\n    fn can_render_path_with_image_fill() {\n        use renderling::atlas::AtlasImage;\n\n        init_logging();\n        let w = 150.0;\n        let ctx = futures_lite::future::block_on(Context::headless(w as u32, w as u32));\n        let mut ui = UiRenderer::new(&ctx).with_background_color(Vec4::ONE);\n\n        // Load dirt texture into the atlas.\n        let atlas_img = AtlasImage::from_path(\"../../img/dirt.jpg\").unwrap();\n        let tex = ui.upload_image(atlas_img);\n\n        // Build a 7-pointed star polygon centered in the viewport, filled\n        // with the dirt image and stroked in red.\n        let center = Vec2::splat(w / 2.0);\n        let star = star_points(7, w / 2.0, w / 3.0);\n\n        let _fill = ui\n            .path_builder()\n            .with_fill_color(Vec4::ONE) // white tint = unmodified image\n            .with_fill_image(&tex)\n            .with_polygon(&star.iter().map(|p| *p + center).collect::<Vec<_>>())\n            .fill(&mut ui);\n\n        let _stroke = ui\n            .path_builder()\n            .with_stroke_color(Vec4::new(1.0, 0.0, 0.0, 1.0))\n            .with_stroke_config(crate::StrokeConfig {\n                line_width: 3.0,\n                ..Default::default()\n            })\n            .with_polygon(&star.iter().map(|p| *p + center).collect::<Vec<_>>())\n            .stroke(&mut ui);\n\n        let frame = ctx.get_next_frame().unwrap();\n        ui.render(&frame.view());\n        let img = futures_lite::future::block_on(frame.read_image()).unwrap();\n        save_and_assert(\"ui2d/path_image_fill.png\", img);\n    }\n}\n"
  },
  {
    "path": "crates/sandbox/Cargo.toml",
    "content": "[package]\nname = \"sandbox\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nrenderling = { path = \"../renderling\" }\nwgpu = { workspace = true }\nwinit = { workspace = true }\n"
  },
  {
    "path": "crates/sandbox/src/main.rs",
    "content": "//! This is a sandbox.\n//!\n//! This program will change on a whim and does not contain anything all that\n//! useful.\nuse glam::UVec2;\nuse renderling::{stage::Stage, Context};\nuse std::sync::Arc;\nuse winit::{\n    dpi::LogicalSize,\n    window::{Window, WindowAttributes},\n};\n\n#[allow(dead_code)]\nstruct Example {\n    ctx: Context,\n    window: Arc<Window>,\n    stage: Stage,\n}\n\nimpl Example {\n    fn event(&mut self, event: winit::event::WindowEvent) -> bool {\n        match &event {\n            winit::event::WindowEvent::CloseRequested\n            | winit::event::WindowEvent::KeyboardInput {\n                event:\n                    winit::event::KeyEvent {\n                        logical_key: winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape),\n                        ..\n                    },\n                ..\n            } => return true,\n\n            winit::event::WindowEvent::Resized(size) => {\n                let size = UVec2::new(size.width, size.height);\n                self.ctx.set_size(size);\n                self.stage.set_size(size)\n            }\n\n            winit::event::WindowEvent::RedrawRequested => {\n                self.ctx.get_device().poll(wgpu::PollType::Wait).unwrap();\n            }\n            _ => {}\n        }\n\n        false\n    }\n}\n\n#[derive(Default)]\nstruct App {\n    example: Option<Example>,\n}\n\nimpl winit::application::ApplicationHandler for App {\n    fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {\n        let attributes = WindowAttributes::default()\n            .with_inner_size(LogicalSize {\n                width: 100,\n                height: 100,\n            })\n            .with_title(\"renderling gltf viewer\");\n        let window = Arc::new(event_loop.create_window(attributes).unwrap());\n        let ctx = futures_lite::future::block_on(Context::from_winit_window(None, window.clone()));\n        let stage = ctx.new_stage();\n        self.example = Some(Example { ctx, window, stage });\n    }\n\n    fn window_event(\n        &mut self,\n        event_loop: &winit::event_loop::ActiveEventLoop,\n        _window_id: winit::window::WindowId,\n        event: winit::event::WindowEvent,\n    ) {\n        if let Some(example) = self.example.as_mut() {\n            if example.event(event) {\n                event_loop.exit();\n            }\n        }\n    }\n\n    fn about_to_wait(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {\n        if let Some(example) = self.example.as_mut() {\n            let frame = example.ctx.get_next_frame().unwrap();\n            example.stage.render(&frame.view());\n            frame.present();\n        }\n    }\n}\n\nfn main() {\n    let event_loop = winit::event_loop::EventLoop::new().unwrap();\n    event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);\n    let mut app = App::default();\n    event_loop.run_app(&mut app).unwrap();\n}\n"
  },
  {
    "path": "crates/wire-types/Cargo.toml",
    "content": "[package]\nname = \"wire-types\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nserde.workspace = true\n"
  },
  {
    "path": "crates/wire-types/src/lib.rs",
    "content": "/// Supported pixel type.\n#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]\npub enum PixelType {\n    Rgb8,\n    Rgba8,\n}\n\n/// Wire type for an RGB8 image.\n#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]\npub struct Image {\n    pub width: u32,\n    pub height: u32,\n    pub bytes: Vec<u8>,\n    pub pixel: PixelType,\n}\n\n#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]\npub struct Error {\n    pub description: String,\n}\n\nimpl From<String> for Error {\n    fn from(description: String) -> Self {\n        Error { description }\n    }\n}\n"
  },
  {
    "path": "crates/xtask/Cargo.toml",
    "content": "[package]\nname = \"xtask\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\naxum.workspace = true\nclap.workspace = true\nenv_logger.workspace = true\nfutures-util.workspace = true\nimage.workspace = true\nimg-diff = { path = \"../img-diff\" }\nlog.workspace = true\nnew_mime_guess.workspace = true\nrenderling_build = { path = \"../renderling-build\" }\nreqwest = { workspace = true, features = [\"stream\"] }\ntokio = { workspace = true, features = [\"full\"] }\nwire-types = { path = \"../wire-types\" }\n"
  },
  {
    "path": "crates/xtask/src/deps.rs",
    "content": "//! Xtask dependency helpers.\n//!\n//! This module helps installing xtask's required dependencies.\n\npub async fn has_binary(name: impl AsRef<str>) -> bool {\n    let output = tokio::process::Command::new(\"hash\")\n        .arg(name.as_ref())\n        .output()\n        .await\n        .expect(\"Failed to execute process\");\n\n    output.status.success()\n}\n\npub async fn cargo_install(name: impl AsRef<str>) {\n    log::warn!(\"installing mdbook\");\n\n    let mut process = tokio::process::Command::new(\"cargo\")\n        .args([\"install\", name.as_ref()])\n        .spawn()\n        .unwrap();\n    let status = process.wait().await.unwrap();\n    if !status.success() {\n        panic!(\"Failed installing {}\", name.as_ref());\n    }\n}\n"
  },
  {
    "path": "crates/xtask/src/main.rs",
    "content": "//! A build helper for the `renderling` project.\nuse clap::{Parser, Subcommand};\n\nmod deps;\nmod server;\n\n#[derive(Subcommand)]\nenum Command {\n    /// Compile the `renderling` library into multiple SPIR-V shader entry\n    /// points.\n    CompileShaders,\n    /// Generate Rust files linking `wgpu` shaders from SPIR-V shader entry\n    /// points.\n    GenerateLinkage {\n        /// Whether to generate WGSL shaders.\n        #[clap(long)]\n        wgsl: bool,\n\n        /// Only generate linkage for the given shader function.\n        #[clap(long)]\n        only_fn_with_name: Option<String>,\n\n        /// Print cargo output as if called from cargo (this is for testing).\n        #[clap(long)]\n        from_cargo: bool,\n    },\n    /// Run the WASM test server\n    WasmServer,\n    /// Compile for WASM and run headless browser tests\n    TestWasm {\n        /// Cargo args.\n        #[clap(last = true)]\n        args: Vec<String>,\n        /// Set to use chrome, otherwise firefox will be used.\n        #[clap(long)]\n        chrome: bool,\n    },\n    /// Perform actions regarding the manual\n    Manual(Manual),\n}\n\n#[derive(Parser)]\npub struct Manual {\n    /// Whether to skip building the docs\n    #[clap(long)]\n    no_build_docs: bool,\n\n    /// Whether to skip testing the manual\n    #[clap(long)]\n    no_test: bool,\n\n    /// The URL to the renderling docs\n    #[clap(long, default_value = \"http://localhost:4000\")]\n    docs_url: String,\n\n    /// Serve the manual instead of simply building it\n    #[clap(long)]\n    serve: bool,\n}\n\nimpl Manual {\n    async fn install_deps() {\n        const DEPS: &[&str] = &[\"mdbook\", \"mdbook-environment\"];\n        for dep in DEPS {\n            if !deps::has_binary(dep).await {\n                deps::cargo_install(dep).await;\n            }\n        }\n    }\n\n    async fn build_docs() {\n        log::info!(\"building docs\");\n        let mut process = tokio::process::Command::new(\"cargo\")\n            .args([\"doc\", \"-p\", \"renderling\", \"--all-features\", \"--no-deps\"])\n            .spawn()\n            .unwrap();\n        let status = process.wait().await.unwrap();\n        if !status.success() {\n            panic!(\"Failed building docs\");\n        }\n    }\n\n    async fn test() {\n        log::info!(\"testing the manual snippets\");\n        let mut process = tokio::process::Command::new(\"cargo\")\n            .args([\"test\", \"-p\", \"examples\"])\n            .spawn()\n            .unwrap();\n        let status = process.wait().await.unwrap();\n        if !status.success() {\n            panic!(\"Failed testing manual\");\n        }\n    }\n}\n\n#[derive(Parser)]\n#[clap(author, version, about)]\nstruct Cli {\n    #[clap(subcommand)]\n    /// The subcommand to run\n    subcommand: Command,\n}\n\n#[tokio::main]\nasync fn main() {\n    env_logger::builder().init();\n\n    log::info!(\"running xtask\");\n\n    let cli = Cli::parse();\n    match cli.subcommand {\n        Command::CompileShaders => {\n            let paths = renderling_build::RenderlingPaths::new().unwrap();\n\n            log::info!(\"Calling `cargo gpu {}\", paths.renderling_crate.display());\n\n            let output = std::process::Command::new(\"cargo\")\n                .args([\"gpu\", \"build\", \"--shader-crate\"])\n                .arg(&paths.renderling_crate)\n                .stdout(std::process::Stdio::inherit())\n                .stderr(std::process::Stdio::inherit())\n                .output()\n                .unwrap();\n            if !output.status.success() {\n                panic!(\"Building shaders failed :(\");\n            }\n        }\n        Command::GenerateLinkage {\n            wgsl,\n            from_cargo,\n            only_fn_with_name,\n        } => {\n            log::info!(\"Generating linkage for shaders\");\n            let paths = renderling_build::RenderlingPaths::new().unwrap();\n            paths.generate_linkage(from_cargo, wgsl, only_fn_with_name);\n        }\n        Command::TestWasm { args, chrome } => {\n            log::info!(\"testing WASM\");\n            let _proxy_handle = tokio::spawn(server::serve());\n            let mut test_handle = tokio::process::Command::new(\"wasm-pack\");\n            test_handle.args([\n                \"test\",\n                \"--headless\",\n                if chrome { \"--chrome\" } else { \"--firefox\" },\n                \"crates/renderling\",\n                \"--features\",\n                \"wasm\",\n            ]);\n            if !args.is_empty() {\n                test_handle.arg(\"--\").args(args);\n            }\n            let mut test_handle = test_handle.spawn().unwrap();\n            let status = test_handle.wait().await.unwrap();\n            if !status.success() {\n                panic!(\"Testing WASM failed :(\");\n            }\n        }\n        Command::WasmServer => {\n            server::serve().await;\n        }\n        Command::Manual(Manual {\n            no_build_docs,\n            no_test,\n            docs_url,\n            serve,\n        }) => {\n            log::info!(\"checking dependencies for the manual\");\n            Manual::install_deps().await;\n            if !no_test {\n                Manual::test().await;\n            }\n            if !no_build_docs {\n                Manual::build_docs().await;\n            }\n\n            if serve {\n                log::info!(\"serving manual\");\n\n                // serve the docs in the meantime\n                let _docs_handle = tokio::spawn(server::serve_docs());\n\n                let mut build = tokio::process::Command::new(\"mdbook\")\n                    .arg(\"serve\")\n                    .current_dir(\n                        std::path::PathBuf::from(env!(\"CARGO_WORKSPACE_DIR\")).join(\"manual\"),\n                    )\n                    .env(\"DOCS_URL\", docs_url)\n                    .spawn()\n                    .unwrap();\n                let build_status = build.wait().await.unwrap();\n                if !build_status.success() {\n                    log::error!(\"could not build the manual\");\n                }\n            } else {\n                log::info!(\"building manual\");\n\n                let mut build = tokio::process::Command::new(\"mdbook\")\n                    .arg(\"build\")\n                    .env(\"DOCS_URL\", docs_url)\n                    .current_dir(\n                        std::path::PathBuf::from(env!(\"CARGO_WORKSPACE_DIR\")).join(\"manual\"),\n                    )\n                    .spawn()\n                    .unwrap();\n                let build_status = build.wait().await.unwrap();\n                if !build_status.success() {\n                    log::error!(\"could not build the manual\");\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/xtask/src/server.rs",
    "content": "//! Axum web server for running the webdriver proxy.\n//!\n//! This proxy server allows the WASM tests to request static assets,\n//! as well as report test failures in a (hopefully) nice way.\n\nuse axum::{\n    body::{Body, Bytes},\n    extract::{Path, Request},\n    http::{HeaderMap, StatusCode},\n    response::{IntoResponse, Response},\n    routing::{any, get, options, post},\n    Json, Router,\n};\nuse image::DynamicImage;\nuse img_diff::DiffCfg;\nuse tokio::io::AsyncWriteExt;\nuse wire_types::Error;\n\npub async fn serve() {\n    log::info!(\"starting the xtask server\");\n    let app = Router::new()\n        .route(\"/test_img/{*path}\", get(static_file))\n        .route(\"/assert_img_eq/{*filename}\", options(accept))\n        .route(\"/assert_img_eq/{*filename}\", post(assert_img_eq))\n        .route(\"/save/{*filename}\", options(accept))\n        .route(\"/save/{*filename}\", post(save))\n        .route(\"/artifact/{*filename}\", options(accept))\n        .route(\"/artifact/{*filename}\", post(artifact))\n        .route(\"/{*rest}\", any(accept));\n    let listener = tokio::net::TcpListener::bind(\"127.0.0.1:4000\")\n        .await\n        .unwrap();\n    axum::serve(listener, app).await.unwrap();\n}\n\n/// Responds with access control headers to allow anything from anywhere.\nasync fn accept(request: Request) -> Response {\n    log::debug!(\"accept: {request:#?}\");\n    Response::builder()\n        .status(StatusCode::OK)\n        .header(\"accept\", \"*/*\")\n        .header(\"access-control-allow-origin\", \"*\")\n        .header(\"access-control-allow-methods\", \"*\")\n        .header(\"access-control-allow-headers\", \"*\")\n        .body(Body::default())\n        .unwrap()\n}\n\nasync fn static_file_inner(\n    path: impl AsRef<std::path::Path>,\n    prefix: impl AsRef<std::path::Path>,\n) -> Result<Response, StatusCode> {\n    log::info!(\n        \"requested '{}' '{}'\",\n        prefix.as_ref().display(),\n        path.as_ref().display()\n    );\n    let mut full_path = prefix.as_ref().join(path);\n    if full_path.is_dir() {\n        full_path = full_path.join(\"index.html\");\n    }\n    if full_path.exists() {\n        let bytes = tokio::fs::read(&full_path).await.map_err(|e| {\n            log::error!(\"could not read path '{full_path:?}': {e}\");\n            StatusCode::BAD_REQUEST\n        })?;\n        let mime = new_mime_guess::from_path(full_path);\n        let mimetype = if let Some(mt) = mime.first() {\n            mt.to_string()\n        } else {\n            \"application/octet-stream\".to_owned()\n        };\n        let resp = Response::builder()\n            .status(StatusCode::OK)\n            .header(\"content-type\", mimetype)\n            .header(\"access-control-allow-origin\", \"*\")\n            .body(Body::from(Bytes::copy_from_slice(&bytes)))\n            .map_err(|e| {\n                log::error!(\"colud not create response: {e}\");\n                StatusCode::INTERNAL_SERVER_ERROR\n            })?;\n        Ok(resp)\n    } else {\n        log::error!(\"{full_path:?} not found\");\n        Err(StatusCode::NOT_FOUND)\n    }\n}\n\nasync fn static_file(Path(path): Path<String>) -> Result<Response, StatusCode> {\n    let test_img_dir = std::path::PathBuf::from(std::env!(\"CARGO_WORKSPACE_DIR\")).join(\"test_img\");\n    static_file_inner(path, test_img_dir).await\n}\n\nfn image_from_wire(img: wire_types::Image) -> Result<image::DynamicImage, Error> {\n    match img.pixel {\n        wire_types::PixelType::Rgb8 => {\n            image::RgbImage::from_raw(img.width, img.height, img.bytes).map(DynamicImage::from)\n        }\n        wire_types::PixelType::Rgba8 => {\n            image::RgbaImage::from_raw(img.width, img.height, img.bytes).map(DynamicImage::from)\n        }\n    }\n    .ok_or_else(|| {\n        let description = \"could not construct image\".to_owned();\n        log::error!(\"{description}\");\n        Error { description }\n    })\n}\n\nasync fn assert_img_eq_inner(\n    filename: &str,\n    img: wire_types::Image,\n) -> Result<(), wire_types::Error> {\n    let seen = image_from_wire(img)?;\n\n    img_diff::assert_img_eq_cfg_result(\n        filename,\n        seen,\n        DiffCfg {\n            output_dir: renderling_build::wasm_test_output_dir(),\n            ..Default::default()\n        },\n    )\n    .map_err(|description| {\n        log::error!(\"{description}\");\n        Error { description }\n    })\n}\n\nasync fn assert_img_eq(\n    headers: HeaderMap,\n    Path(parts): Path<Vec<String>>,\n    Json(img): Json<wire_types::Image>,\n) -> Response {\n    let filename = parts.join(\"/\");\n    log::info!(\"asserting '{filename}'\");\n    log::info!(\"headers: {headers:#?}\");\n\n    let result = assert_img_eq_inner(&filename, img).await;\n    Response::builder()\n        .status(StatusCode::OK)\n        .header(\"accept\", \"*/*\")\n        .header(\"access-control-allow-origin\", \"*\")\n        .header(\"access-control-allow-methods\", \"*\")\n        .header(\"access-control-allow-headers\", \"*\")\n        .body(Json(result).into_response().into_body())\n        .unwrap()\n}\n\nasync fn save_inner(filename: &str, img: wire_types::Image) -> Result<(), Error> {\n    let img = image_from_wire(img)?;\n    img_diff::save_to(renderling_build::wasm_test_output_dir(), filename, img)\n        .map_err(|description| Error { description })\n}\n\nasync fn save(\n    headers: HeaderMap,\n    Path(parts): Path<Vec<String>>,\n    Json(img): Json<wire_types::Image>,\n) -> Response {\n    let filename = parts.join(\"/\");\n    log::info!(\"asserting '{filename}'\");\n    log::info!(\"headers: {headers:#?}\");\n    let result = save_inner(&filename, img).await;\n    Response::builder()\n        .status(StatusCode::OK)\n        .header(\"accept\", \"*/*\")\n        .header(\"access-control-allow-origin\", \"*\")\n        .header(\"access-control-allow-methods\", \"*\")\n        .header(\"access-control-allow-headers\", \"*\")\n        .body(Json(result).into_response().into_body())\n        .unwrap()\n}\n\nasync fn artifact_inner(filename: impl AsRef<std::path::Path>, body: Body) -> Result<(), Error> {\n    use futures_util::StreamExt;\n\n    let mut byte_stream = body.into_data_stream();\n    tokio::fs::create_dir_all(\n        filename\n            .as_ref()\n            .parent()\n            .ok_or_else(|| Error::from(format!(\"'{:?}' has no parent dir\", filename.as_ref())))?,\n    )\n    .await\n    .map_err(|e| Error::from(e.to_string()))?;\n    let mut file = tokio::fs::File::create(filename)\n        .await\n        .map_err(|e| Error::from(e.to_string()))?;\n    while let Some(result_bytes) = byte_stream.next().await {\n        let bytes = result_bytes.map_err(|e| Error::from(e.to_string()))?;\n        file.write_all(&bytes)\n            .await\n            .map_err(|e| Error::from(e.to_string()))?;\n    }\n    Ok(())\n}\n\nasync fn artifact(Path(parts): Path<Vec<String>>, body: Body) -> Response {\n    let filename = renderling_build::wasm_test_output_dir().join(parts.join(\"/\"));\n    log::info!(\"saving artifact to {filename:?}\");\n    let result = artifact_inner(filename, body).await;\n    Response::builder()\n        .status(StatusCode::OK)\n        .header(\"accept\", \"*/*\")\n        .header(\"access-control-allow-origin\", \"*\")\n        .header(\"access-control-allow-methods\", \"*\")\n        .header(\"access-control-allow-headers\", \"*\")\n        .body(Json(result).into_response().into_body())\n        .unwrap()\n}\n\npub async fn serve_docs() {\n    log::info!(\"starting the xtask docs server\");\n    let app = Router::new().route(\"/{*rest}\", get(docs));\n    let listener = tokio::net::TcpListener::bind(\"127.0.0.1:4000\")\n        .await\n        .unwrap();\n    log::info!(\"serving docs\");\n    axum::serve(listener, app).await.unwrap();\n}\n\nasync fn docs(Path(path): Path<String>) -> impl IntoResponse {\n    log::info!(\"path: {path:#?}\");\n    static_file_inner(path, \"target/doc\").await\n}\n"
  },
  {
    "path": "gltf/SimpleSkin.gltf",
    "content": "{\n  \"scene\" : 0,\n  \"scenes\" : [ {\n    \"nodes\" : [ 0, 1 ]\n  } ],\n  \n  \"nodes\" : [ {\n    \"skin\" : 0,\n    \"mesh\" : 0\n  }, {\n    \"children\" : [ 2 ]\n  }, {\n    \"translation\" : [ 0.0, 1.0, 0.0 ],\n    \"rotation\" : [ 0.0, 0.0, 0.0, 1.0 ]\n  } ],\n  \n  \"meshes\" : [ {\n    \"primitives\" : [ {\n      \"attributes\" : {\n        \"POSITION\" : 1,\n        \"JOINTS_0\" : 2,\n        \"WEIGHTS_0\" : 3\n      },\n      \"indices\" : 0\n    } ]\n  } ],\n\n  \"skins\" : [ {\n    \"inverseBindMatrices\" : 4,\n    \"joints\" : [ 1, 2 ]\n  } ],\n  \n  \"animations\" : [ {\n    \"channels\" : [ {\n      \"sampler\" : 0,\n      \"target\" : {\n        \"node\" : 2,\n        \"path\" : \"rotation\"\n      }\n    } ],\n    \"samplers\" : [ {\n      \"input\" : 5,\n      \"interpolation\" : \"LINEAR\",\n      \"output\" : 6\n    } ]\n  } ],\n  \n  \"buffers\" : [ {\n    \"uri\" : \"data:application/gltf-buffer;base64,AAABAAMAAAADAAIAAgADAAUAAgAFAAQABAAFAAcABAAHAAYABgAHAAkABgAJAAgAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAvwAAAD8AAAAAAAAAPwAAAD8AAAAAAAAAvwAAgD8AAAAAAAAAPwAAgD8AAAAAAAAAvwAAwD8AAAAAAAAAPwAAwD8AAAAAAAAAvwAAAEAAAAAAAAAAPwAAAEAAAAAA\",\n    \"byteLength\" : 168\n  }, {\n    \"uri\" : \"data:application/gltf-buffer;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAAAAQD8AAIA+AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAAAAgD4AAEA/AAAAAAAAAAAAAIA+AABAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAA=\",\n    \"byteLength\" : 320\n  }, {\n    \"uri\" : \"data:application/gltf-buffer;base64,AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8=\",\n    \"byteLength\" : 128\n  }, {\n    \"uri\" : \"data:application/gltf-buffer;base64,AAAAAAAAAD8AAIA/AADAPwAAAEAAACBAAABAQAAAYEAAAIBAAACQQAAAoEAAALBAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAPT9ND/0/TQ/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAPT9NL/0/TQ/AAAAAAAAAAD0/TS/9P00PwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAAAAAAAAAIA/\",\n    \"byteLength\" : 240\n  } ],\n  \n  \"bufferViews\" : [ {\n    \"buffer\" : 0,\n    \"byteLength\" : 48,\n    \"target\" : 34963\n  }, {\n    \"buffer\" : 0,\n    \"byteOffset\" : 48,\n    \"byteLength\" : 120,\n    \"target\" : 34962\n  }, {\n    \"buffer\" : 1,\n    \"byteLength\" : 320,\n    \"byteStride\" : 16\n  }, {\n    \"buffer\" : 2,\n    \"byteLength\" : 128\n  }, {\n    \"buffer\" : 3,\n    \"byteLength\" : 240\n  } ],\n\n  \"accessors\" : [ {\n    \"bufferView\" : 0,\n    \"componentType\" : 5123,\n    \"count\" : 24,\n    \"type\" : \"SCALAR\"\n  }, {\n    \"bufferView\" : 1,\n    \"componentType\" : 5126,\n    \"count\" : 10,\n    \"type\" : \"VEC3\",\n    \"max\" : [ 0.5, 2.0, 0.0 ],\n    \"min\" : [ -0.5, 0.0, 0.0 ]\n  }, {\n    \"bufferView\" : 2,\n    \"componentType\" : 5123,\n    \"count\" : 10,\n    \"type\" : \"VEC4\"\n  }, {\n    \"bufferView\" : 2,\n    \"byteOffset\" : 160,\n    \"componentType\" : 5126,\n    \"count\" : 10,\n    \"type\" : \"VEC4\"\n  }, {\n    \"bufferView\" : 3,\n    \"componentType\" : 5126,\n    \"count\" : 2,\n    \"type\" : \"MAT4\"\n  }, {\n    \"bufferView\" : 4,\n    \"componentType\" : 5126,\n    \"count\" : 12,\n    \"type\" : \"SCALAR\",\n    \"max\" : [ 5.5 ],\n    \"min\" : [ 0.0 ]\n  }, {\n    \"bufferView\" : 4,\n    \"byteOffset\" : 48,\n    \"componentType\" : 5126,\n    \"count\" : 12,\n    \"type\" : \"VEC4\",\n    \"max\" : [ 0.0, 0.0, 0.707, 1.0 ],\n    \"min\" : [ 0.0, 0.0, -0.707, 0.707 ]\n  } ],\n \n  \"asset\" : {\n    \"version\" : \"2.0\"\n  }\n}"
  },
  {
    "path": "gltf/animated_triangle.gltf",
    "content": "{\n  \"scene\": 0,\n  \"scenes\" : [\n    {\n      \"nodes\" : [ 0 ]\n    }\n  ],\n\n  \"nodes\" : [\n    {\n      \"mesh\" : 0,\n      \"rotation\" : [ 0.0, 0.0, 0.0, 1.0 ]\n    }\n  ],\n\n  \"meshes\" : [\n    {\n      \"primitives\" : [ {\n        \"attributes\" : {\n          \"POSITION\" : 1\n        },\n        \"indices\" : 0\n      } ]\n    }\n  ],\n\n  \"animations\": [\n    {\n      \"samplers\" : [\n        {\n          \"input\" : 2,\n          \"interpolation\" : \"LINEAR\",\n          \"output\" : 3\n        }\n      ],\n      \"channels\" : [ {\n        \"sampler\" : 0,\n        \"target\" : {\n          \"node\" : 0,\n          \"path\" : \"rotation\"\n        }\n      } ]\n    }\n  ],\n\n  \"buffers\" : [\n    {\n      \"uri\" : \"data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=\",\n      \"byteLength\" : 44\n    },\n    {\n      \"uri\" : \"data:application/octet-stream;base64,AAAAAAAAgD4AAAA/AABAPwAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAPT9ND/0/TS/AAAAAAAAAAAAAAAAAACAPw==\",\n      \"byteLength\" : 100\n    }\n  ],\n  \"bufferViews\" : [\n    {\n      \"buffer\" : 0,\n      \"byteOffset\" : 0,\n      \"byteLength\" : 6,\n      \"target\" : 34963\n    },\n    {\n      \"buffer\" : 0,\n      \"byteOffset\" : 8,\n      \"byteLength\" : 36,\n      \"target\" : 34962\n    },\n    {\n      \"buffer\" : 1,\n      \"byteOffset\" : 0,\n      \"byteLength\" : 100\n    }\n  ],\n  \"accessors\" : [\n    {\n      \"bufferView\" : 0,\n      \"byteOffset\" : 0,\n      \"componentType\" : 5123,\n      \"count\" : 3,\n      \"type\" : \"SCALAR\",\n      \"max\" : [ 2 ],\n      \"min\" : [ 0 ]\n    },\n    {\n      \"bufferView\" : 1,\n      \"byteOffset\" : 0,\n      \"componentType\" : 5126,\n      \"count\" : 3,\n      \"type\" : \"VEC3\",\n      \"max\" : [ 1.0, 1.0, 0.0 ],\n      \"min\" : [ 0.0, 0.0, 0.0 ]\n    },\n    {\n      \"bufferView\" : 2,\n      \"byteOffset\" : 0,\n      \"componentType\" : 5126,\n      \"count\" : 5,\n      \"type\" : \"SCALAR\",\n      \"max\" : [ 1.0 ],\n      \"min\" : [ 0.0 ]\n    },\n    {\n      \"bufferView\" : 2,\n      \"byteOffset\" : 20,\n      \"componentType\" : 5126,\n      \"count\" : 5,\n      \"type\" : \"VEC4\",\n      \"max\" : [ 0.0, 0.0, 1.0, 1.0 ],\n      \"min\" : [ 0.0, 0.0, 0.0, -0.707 ]\n    }\n  ],\n\n  \"asset\" : {\n    \"version\" : \"2.0\"\n  }\n\n}"
  },
  {
    "path": "gltf/gltfTutorial_003_MinimalGltfFile.gltf",
    "content": "{\n  \"scene\": 0,\n  \"scenes\" : [\n    {\n      \"nodes\" : [ 0 ]\n    }\n  ],\n\n  \"nodes\" : [\n    {\n      \"mesh\" : 0\n    }\n  ],\n\n  \"meshes\" : [\n    {\n      \"primitives\" : [ {\n        \"attributes\" : {\n          \"POSITION\" : 1\n        },\n        \"indices\" : 0\n      } ]\n    }\n  ],\n\n  \"buffers\" : [\n    {\n      \"uri\" : \"data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=\",\n      \"byteLength\" : 44\n    }\n  ],\n  \"bufferViews\" : [\n    {\n      \"buffer\" : 0,\n      \"byteOffset\" : 0,\n      \"byteLength\" : 6,\n      \"target\" : 34963\n    },\n    {\n      \"buffer\" : 0,\n      \"byteOffset\" : 8,\n      \"byteLength\" : 36,\n      \"target\" : 34962\n    }\n  ],\n  \"accessors\" : [\n    {\n      \"bufferView\" : 0,\n      \"byteOffset\" : 0,\n      \"componentType\" : 5123,\n      \"count\" : 3,\n      \"type\" : \"SCALAR\",\n      \"max\" : [ 2 ],\n      \"min\" : [ 0 ]\n    },\n    {\n      \"bufferView\" : 1,\n      \"byteOffset\" : 0,\n      \"componentType\" : 5126,\n      \"count\" : 3,\n      \"type\" : \"VEC3\",\n      \"max\" : [ 1.0, 1.0, 0.0 ],\n      \"min\" : [ 0.0, 0.0, 0.0 ]\n    }\n  ],\n\n  \"asset\" : {\n    \"version\" : \"2.0\"\n  }\n}"
  },
  {
    "path": "gltf/gltfTutorial_008_SimpleMeshes.gltf",
    "content": "{\n  \"scene\": 0,\n  \"scenes\" : [\n    {\n      \"nodes\" : [ 0, 1]\n    }\n  ],\n  \"nodes\" : [\n    {\n      \"mesh\" : 0\n    },\n    {\n      \"mesh\" : 0,\n      \"translation\" : [ 1.0, 0.0, 0.0 ]\n    }\n  ],\n\n  \"meshes\" : [\n    {\n      \"primitives\" : [ {\n        \"attributes\" : {\n          \"POSITION\" : 1,\n          \"NORMAL\" : 2\n        },\n        \"indices\" : 0\n      } ]\n    }\n  ],\n\n  \"buffers\" : [\n    {\n      \"uri\" : \"data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8=\",\n      \"byteLength\" : 80\n    }\n  ],\n  \"bufferViews\" : [\n    {\n      \"buffer\" : 0,\n      \"byteOffset\" : 0,\n      \"byteLength\" : 6,\n      \"target\" : 34963\n    },\n    {\n      \"buffer\" : 0,\n      \"byteOffset\" : 8,\n      \"byteLength\" : 72,\n      \"target\" : 34962\n    }\n  ],\n  \"accessors\" : [\n    {\n      \"bufferView\" : 0,\n      \"byteOffset\" : 0,\n      \"componentType\" : 5123,\n      \"count\" : 3,\n      \"type\" : \"SCALAR\",\n      \"max\" : [ 2 ],\n      \"min\" : [ 0 ]\n    },\n    {\n      \"bufferView\" : 1,\n      \"byteOffset\" : 0,\n      \"componentType\" : 5126,\n      \"count\" : 3,\n      \"type\" : \"VEC3\",\n      \"max\" : [ 1.0, 1.0, 0.0 ],\n      \"min\" : [ 0.0, 0.0, 0.0 ]\n    },\n    {\n      \"bufferView\" : 1,\n      \"byteOffset\" : 36,\n      \"componentType\" : 5126,\n      \"count\" : 3,\n      \"type\" : \"VEC3\",\n      \"max\" : [ 0.0, 0.0, 1.0 ],\n      \"min\" : [ 0.0, 0.0, 1.0 ]\n    }\n  ],\n\n  \"asset\" : {\n    \"version\" : \"2.0\"\n  }\n}"
  },
  {
    "path": "gltf/gltfTutorial_013_SimpleTexture.gltf",
    "content": "{\n  \"scene\": 0,\n  \"scenes\" : [ {\n    \"nodes\" : [ 0 ]\n  } ],\n  \"nodes\" : [ {\n    \"mesh\" : 0\n  } ],\n  \"meshes\" : [ {\n    \"primitives\" : [ {\n      \"attributes\" : {\n        \"POSITION\" : 1,\n        \"TEXCOORD_0\" : 2\n      },\n      \"indices\" : 0,\n      \"material\" : 0\n    } ]\n  } ],\n\n  \"materials\" : [ {\n    \"pbrMetallicRoughness\" : {\n      \"baseColorTexture\" : {\n        \"index\" : 0\n      },\n      \"metallicFactor\" : 0.0,\n      \"roughnessFactor\" : 1.0\n    }\n  } ],\n\n  \"textures\" : [ {\n    \"sampler\" : 0,\n    \"source\" : 0\n  } ],\n  \"images\" : [ {\n    \"uri\" : \"testTexture.png\"\n  } ],\n  \"samplers\" : [ {\n    \"magFilter\" : 9729,\n    \"minFilter\" : 9987,\n    \"wrapS\" : 33648,\n    \"wrapT\" : 33648\n  } ],\n\n  \"buffers\" : [ {\n    \"uri\" : \"data:application/gltf-buffer;base64,AAABAAIAAQADAAIAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAA\",\n    \"byteLength\" : 108\n  } ],\n  \"bufferViews\" : [ {\n    \"buffer\" : 0,\n    \"byteOffset\" : 0,\n    \"byteLength\" : 12,\n    \"target\" : 34963\n  }, {\n    \"buffer\" : 0,\n    \"byteOffset\" : 12,\n    \"byteLength\" : 96,\n    \"byteStride\" : 12,\n    \"target\" : 34962\n  } ],\n  \"accessors\" : [ {\n    \"bufferView\" : 0,\n    \"byteOffset\" : 0,\n    \"componentType\" : 5123,\n    \"count\" : 6,\n    \"type\" : \"SCALAR\",\n    \"max\" : [ 3 ],\n    \"min\" : [ 0 ]\n  }, {\n    \"bufferView\" : 1,\n    \"byteOffset\" : 0,\n    \"componentType\" : 5126,\n    \"count\" : 4,\n    \"type\" : \"VEC3\",\n    \"max\" : [ 1.0, 1.0, 0.0 ],\n    \"min\" : [ 0.0, 0.0, 0.0 ]\n  }, {\n    \"bufferView\" : 1,\n    \"byteOffset\" : 48,\n    \"componentType\" : 5126,\n    \"count\" : 4,\n    \"type\" : \"VEC2\",\n    \"max\" : [ 1.0, 1.0 ],\n    \"min\" : [ 0.0, 0.0 ]\n  } ],\n\n  \"asset\" : {\n    \"version\" : \"2.0\"\n  }\n}"
  },
  {
    "path": "gltf/gltfTutorial_017_SimpleMorphTarget.gltf",
    "content": "{\n  \"scene\": 0,\n  \"scenes\":[\n    {\n      \"nodes\":[\n        0\n      ]\n    }\n  ],\n  \"nodes\":[\n    {\n      \"mesh\":0\n    }\n  ],\n  \"meshes\":[\n    {\n      \"primitives\":[\n        {\n          \"attributes\":{\n            \"POSITION\":1\n          },\n          \"targets\":[\n            {\n              \"POSITION\":2\n            },\n            {\n              \"POSITION\":3\n            }\n          ],\n          \"indices\":0\n        }\n      ],\n      \"weights\":[\n        1.0,\n        0.5\n      ]\n    }\n  ],\n\n  \"animations\":[\n    {\n      \"samplers\":[\n        {\n          \"input\":4,\n          \"interpolation\":\"LINEAR\",\n          \"output\":5\n        }\n      ],\n      \"channels\":[\n        {\n          \"sampler\":0,\n          \"target\":{\n            \"node\":0,\n            \"path\":\"weights\"\n          }\n        }\n      ]\n    }\n  ],\n\n  \"buffers\":[\n    {\n      \"uri\":\"data:application/gltf-buffer;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAAAA=\",\n      \"byteLength\":116\n    },\n    {\n      \"uri\":\"data:application/gltf-buffer;base64,AAAAAAAAgD8AAABAAABAQAAAgEAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAA\",\n      \"byteLength\":60\n    }\n  ],\n  \"bufferViews\":[\n    {\n      \"buffer\":0,\n      \"byteOffset\":0,\n      \"byteLength\":6,\n      \"target\":34963\n    },\n    {\n      \"buffer\":0,\n      \"byteOffset\":8,\n      \"byteLength\":108,\n      \"byteStride\":12,\n      \"target\":34962\n    },\n    {\n      \"buffer\":1,\n      \"byteOffset\":0,\n      \"byteLength\":20\n    },\n    {\n      \"buffer\":1,\n      \"byteOffset\":20,\n      \"byteLength\":40\n    }\n  ],\n  \"accessors\":[\n    {\n      \"bufferView\":0,\n      \"byteOffset\":0,\n      \"componentType\":5123,\n      \"count\":3,\n      \"type\":\"SCALAR\",\n      \"max\":[\n        2\n      ],\n      \"min\":[\n        0\n      ]\n    },\n    {\n      \"bufferView\":1,\n      \"byteOffset\":0,\n      \"componentType\":5126,\n      \"count\":3,\n      \"type\":\"VEC3\",\n      \"max\":[\n        1.0,\n        0.5,\n        0.0\n      ],\n      \"min\":[\n        0.0,\n        0.0,\n        0.0\n      ]\n    },\n    {\n      \"bufferView\":1,\n      \"byteOffset\":36,\n      \"componentType\":5126,\n      \"count\":3,\n      \"type\":\"VEC3\",\n      \"max\":[\n        0.0,\n        1.0,\n        0.0\n      ],\n      \"min\":[\n        -1.0,\n        0.0,\n        0.0\n      ]\n    },\n    {\n      \"bufferView\":1,\n      \"byteOffset\":72,\n      \"componentType\":5126,\n      \"count\":3,\n      \"type\":\"VEC3\",\n      \"max\":[\n        1.0,\n        1.0,\n        0.0\n      ],\n      \"min\":[\n        0.0,\n        0.0,\n        0.0\n      ]\n    },\n    {\n      \"bufferView\":2,\n      \"byteOffset\":0,\n      \"componentType\":5126,\n      \"count\":5,\n      \"type\":\"SCALAR\",\n      \"max\":[\n        4.0\n      ],\n      \"min\":[\n        0.0\n      ]\n    },\n    {\n      \"bufferView\":3,\n      \"byteOffset\":0,\n      \"componentType\":5126,\n      \"count\":10,\n      \"type\":\"SCALAR\",\n      \"max\":[\n        1.0\n      ],\n      \"min\":[\n        0.0\n      ]\n    }\n  ],\n\n  \"asset\":{\n    \"version\":\"2.0\"\n  }\n}\n"
  },
  {
    "path": "gltf/gltfTutorial_019_SimpleSkin.gltf",
    "content": "{\n  \"scene\" : 0,\n  \"scenes\" : [ {\n    \"nodes\" : [ 0, 1 ]\n  } ],\n\n  \"nodes\" : [ {\n    \"skin\" : 0,\n    \"mesh\" : 0\n  }, {\n    \"children\" : [ 2 ]\n  }, {\n    \"translation\" : [ 0.0, 1.0, 0.0 ],\n    \"rotation\" : [ 0.0, 0.0, 0.0, 1.0 ]\n  } ],\n\n  \"meshes\" : [ {\n    \"primitives\" : [ {\n      \"attributes\" : {\n        \"POSITION\" : 1,\n        \"JOINTS_0\" : 2,\n        \"WEIGHTS_0\" : 3\n      },\n      \"indices\" : 0\n    } ]\n  } ],\n\n  \"skins\" : [ {\n    \"inverseBindMatrices\" : 4,\n    \"joints\" : [ 1, 2 ]\n  } ],\n\n  \"animations\" : [ {\n    \"channels\" : [ {\n      \"sampler\" : 0,\n      \"target\" : {\n        \"node\" : 2,\n        \"path\" : \"rotation\"\n      }\n    } ],\n    \"samplers\" : [ {\n      \"input\" : 5,\n      \"interpolation\" : \"LINEAR\",\n      \"output\" : 6\n    } ]\n  } ],\n\n  \"buffers\" : [ {\n    \"uri\" : \"data:application/gltf-buffer;base64,AAABAAMAAAADAAIAAgADAAUAAgAFAAQABAAFAAcABAAHAAYABgAHAAkABgAJAAgAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAvwAAAD8AAAAAAAAAPwAAAD8AAAAAAAAAvwAAgD8AAAAAAAAAPwAAgD8AAAAAAAAAvwAAwD8AAAAAAAAAPwAAwD8AAAAAAAAAvwAAAEAAAAAAAAAAPwAAAEAAAAAA\",\n    \"byteLength\" : 168\n  }, {\n    \"uri\" : \"data:application/gltf-buffer;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAAAAQD8AAIA+AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAAAAgD4AAEA/AAAAAAAAAAAAAIA+AABAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAA=\",\n    \"byteLength\" : 320\n  }, {\n    \"uri\" : \"data:application/gltf-buffer;base64,AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8=\",\n    \"byteLength\" : 128\n  }, {\n    \"uri\" : \"data:application/gltf-buffer;base64,AAAAAAAAAD8AAIA/AADAPwAAAEAAACBAAABAQAAAYEAAAIBAAACQQAAAoEAAALBAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAPT9ND/0/TQ/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAPT9NL/0/TQ/AAAAAAAAAAD0/TS/9P00PwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAAAAAAAAAIA/\",\n    \"byteLength\" : 240\n  } ],\n\n  \"bufferViews\" : [ {\n    \"buffer\" : 0,\n    \"byteLength\" : 48,\n    \"target\" : 34963\n  }, {\n    \"buffer\" : 0,\n    \"byteOffset\" : 48,\n    \"byteLength\" : 120,\n    \"target\" : 34962\n  }, {\n    \"buffer\" : 1,\n    \"byteLength\" : 320,\n    \"byteStride\" : 16\n  }, {\n    \"buffer\" : 2,\n    \"byteLength\" : 128\n  }, {\n    \"buffer\" : 3,\n    \"byteLength\" : 240\n  } ],\n\n  \"accessors\" : [ {\n    \"bufferView\" : 0,\n    \"componentType\" : 5123,\n    \"count\" : 24,\n    \"type\" : \"SCALAR\"\n  }, {\n    \"bufferView\" : 1,\n    \"componentType\" : 5126,\n    \"count\" : 10,\n    \"type\" : \"VEC3\",\n    \"max\" : [ 0.5, 2.0, 0.0 ],\n    \"min\" : [ -0.5, 0.0, 0.0 ]\n  }, {\n    \"bufferView\" : 2,\n    \"componentType\" : 5123,\n    \"count\" : 10,\n    \"type\" : \"VEC4\"\n  }, {\n    \"bufferView\" : 2,\n    \"byteOffset\" : 160,\n    \"componentType\" : 5126,\n    \"count\" : 10,\n    \"type\" : \"VEC4\"\n  }, {\n    \"bufferView\" : 3,\n    \"componentType\" : 5126,\n    \"count\" : 2,\n    \"type\" : \"MAT4\"\n  }, {\n    \"bufferView\" : 4,\n    \"componentType\" : 5126,\n    \"count\" : 12,\n    \"type\" : \"SCALAR\",\n    \"max\" : [ 5.5 ],\n    \"min\" : [ 0.0 ]\n  }, {\n    \"bufferView\" : 4,\n    \"byteOffset\" : 48,\n    \"componentType\" : 5126,\n    \"count\" : 12,\n    \"type\" : \"VEC4\",\n    \"max\" : [ 0.0, 0.0, 0.707, 1.0 ],\n    \"min\" : [ 0.0, 0.0, -0.707, 0.707 ]\n  } ],\n\n  \"asset\" : {\n    \"version\" : \"2.0\"\n  }\n}"
  },
  {
    "path": "gltf/shadow_mapping_only_cuboid.gltf",
    "content": "{\n\t\"asset\":{\n\t\t\"generator\":\"Khronos glTF Blender I/O v4.3.47\",\n\t\t\"version\":\"2.0\"\n\t},\n\t\"extensionsUsed\":[\n\t\t\"KHR_lights_punctual\"\n\t],\n\t\"extensionsRequired\":[\n\t\t\"KHR_lights_punctual\"\n\t],\n\t\"extensions\":{\n\t\t\"KHR_lights_punctual\":{\n\t\t\t\"lights\":[\n\t\t\t\t{\n\t\t\t\t\t\"color\":[\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1\n\t\t\t\t\t],\n\t\t\t\t\t\"intensity\":6830,\n\t\t\t\t\t\"type\":\"directional\",\n\t\t\t\t\t\"name\":\"Light\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t},\n\t\"scene\":0,\n\t\"scenes\":[\n\t\t{\n\t\t\t\"name\":\"Scene\",\n\t\t\t\"nodes\":[\n\t\t\t\t0,\n\t\t\t\t1,\n\t\t\t\t2,\n\t\t\t\t3\n\t\t\t]\n\t\t}\n\t],\n\t\"nodes\":[\n\t\t{\n\t\t\t\"mesh\":0,\n\t\t\t\"name\":\"Cube\",\n\t\t\t\"translation\":[\n\t\t\t\t0,\n\t\t\t\t0.8984647393226624,\n\t\t\t\t0\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"extensions\":{\n\t\t\t\t\"KHR_lights_punctual\":{\n\t\t\t\t\t\"light\":0\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"name\":\"Light\",\n\t\t\t\"rotation\":[\n\t\t\t\t0.09542951732873917,\n\t\t\t\t0.8279411792755127,\n\t\t\t\t0.3013063669204712,\n\t\t\t\t0.4632721543312073\n\t\t\t],\n\t\t\t\"translation\":[\n\t\t\t\t4.076245307922363,\n\t\t\t\t5.903861999511719,\n\t\t\t\t-1.0054539442062378\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"camera\":0,\n\t\t\t\"name\":\"Camera\",\n\t\t\t\"rotation\":[\n\t\t\t\t-0.03457474336028099,\n\t\t\t\t0.4687580168247223,\n\t\t\t\t0.004934974014759064,\n\t\t\t\t0.8826359510421753\n\t\t\t],\n\t\t\t\"translation\":[\n\t\t\t\t14.699949264526367,\n\t\t\t\t4.958309173583984,\n\t\t\t\t12.676651000976562\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"mesh\":1,\n\t\t\t\"name\":\"Plane\"\n\t\t}\n\t],\n\t\"cameras\":[\n\t\t{\n\t\t\t\"name\":\"Camera\",\n\t\t\t\"perspective\":{\n\t\t\t\t\"aspectRatio\":1.7777777777777777,\n\t\t\t\t\"yfov\":0.39959648408210363,\n\t\t\t\t\"zfar\":100,\n\t\t\t\t\"znear\":0.10000000149011612\n\t\t\t},\n\t\t\t\"type\":\"perspective\"\n\t\t}\n\t],\n\t\"materials\":[\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"name\":\"Material\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"baseColorFactor\":[\n\t\t\t\t\t0.8004003763198853,\n\t\t\t\t\t0.013363108038902283,\n\t\t\t\t\t0.020213128998875618,\n\t\t\t\t\t1\n\t\t\t\t],\n\t\t\t\t\"metallicFactor\":0,\n\t\t\t\t\"roughnessFactor\":0.5\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"name\":\"Material.001\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"metallicFactor\":0,\n\t\t\t\t\"roughnessFactor\":0.8815789222717285\n\t\t\t}\n\t\t}\n\t],\n\t\"meshes\":[\n\t\t{\n\t\t\t\"name\":\"Cube\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":0,\n\t\t\t\t\t\t\"NORMAL\":1,\n\t\t\t\t\t\t\"TEXCOORD_0\":2\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":3,\n\t\t\t\t\t\"material\":0\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\":\"Plane\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":4,\n\t\t\t\t\t\t\"NORMAL\":5,\n\t\t\t\t\t\t\"TEXCOORD_0\":6\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":7,\n\t\t\t\t\t\"material\":1\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"accessors\":[\n\t\t{\n\t\t\t\"bufferView\":0,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"max\":[\n\t\t\t\t1,\n\t\t\t\t3.7387936115264893,\n\t\t\t\t1\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-1,\n\t\t\t\t-1,\n\t\t\t\t-1\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":1,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":2,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":3,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":36,\n\t\t\t\"type\":\"SCALAR\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":4,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"max\":[\n\t\t\t\t23.336652755737305,\n\t\t\t\t0,\n\t\t\t\t23.336652755737305\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-23.336652755737305,\n\t\t\t\t-0.2270890176296234,\n\t\t\t\t-23.336652755737305\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":5,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":6,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":7,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":36,\n\t\t\t\"type\":\"SCALAR\"\n\t\t}\n\t],\n\t\"bufferViews\":[\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":0,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":288,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":192,\n\t\t\t\"byteOffset\":576,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":72,\n\t\t\t\"byteOffset\":768,\n\t\t\t\"target\":34963\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":840,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":1128,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":192,\n\t\t\t\"byteOffset\":1416,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":72,\n\t\t\t\"byteOffset\":1608,\n\t\t\t\"target\":34963\n\t\t}\n\t],\n\t\"buffers\":[\n\t\t{\n\t\t\t\"byteLength\":1680,\n\t\t\t\"uri\":\"shadow_mapping_only_cuboid.bin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "gltf/shadow_mapping_only_cuboid_red_and_blue.gltf",
    "content": "{\n\t\"asset\":{\n\t\t\"generator\":\"Khronos glTF Blender I/O v4.3.47\",\n\t\t\"version\":\"2.0\"\n\t},\n\t\"extensionsUsed\":[\n\t\t\"KHR_lights_punctual\"\n\t],\n\t\"extensionsRequired\":[\n\t\t\"KHR_lights_punctual\"\n\t],\n\t\"extensions\":{\n\t\t\"KHR_lights_punctual\":{\n\t\t\t\"lights\":[\n\t\t\t\t{\n\t\t\t\t\t\"color\":[\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\t0.006205391138792038\n\t\t\t\t\t],\n\t\t\t\t\t\"intensity\":3415,\n\t\t\t\t\t\"type\":\"directional\",\n\t\t\t\t\t\"name\":\"Light\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"color\":[\n\t\t\t\t\t\t0.007022588513791561,\n\t\t\t\t\t\t0.0040604667738080025,\n\t\t\t\t\t\t1\n\t\t\t\t\t],\n\t\t\t\t\t\"intensity\":3415,\n\t\t\t\t\t\"type\":\"directional\",\n\t\t\t\t\t\"name\":\"Sun\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t},\n\t\"scene\":0,\n\t\"scenes\":[\n\t\t{\n\t\t\t\"name\":\"Scene\",\n\t\t\t\"nodes\":[\n\t\t\t\t0,\n\t\t\t\t1,\n\t\t\t\t2,\n\t\t\t\t3,\n\t\t\t\t4\n\t\t\t]\n\t\t}\n\t],\n\t\"nodes\":[\n\t\t{\n\t\t\t\"mesh\":0,\n\t\t\t\"name\":\"Cube\",\n\t\t\t\"translation\":[\n\t\t\t\t0,\n\t\t\t\t0.8984647393226624,\n\t\t\t\t0\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"extensions\":{\n\t\t\t\t\"KHR_lights_punctual\":{\n\t\t\t\t\t\"light\":0\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"name\":\"Light\",\n\t\t\t\"rotation\":[\n\t\t\t\t0.09542951732873917,\n\t\t\t\t0.8279411792755127,\n\t\t\t\t0.3013063669204712,\n\t\t\t\t0.4632721543312073\n\t\t\t],\n\t\t\t\"translation\":[\n\t\t\t\t6.54069709777832,\n\t\t\t\t5.903861999511719,\n\t\t\t\t-0.4569058418273926\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"camera\":0,\n\t\t\t\"name\":\"Camera\",\n\t\t\t\"rotation\":[\n\t\t\t\t-0.03457474336028099,\n\t\t\t\t0.4687580168247223,\n\t\t\t\t0.004934974014759064,\n\t\t\t\t0.8826359510421753\n\t\t\t],\n\t\t\t\"translation\":[\n\t\t\t\t14.699949264526367,\n\t\t\t\t4.958309173583984,\n\t\t\t\t12.676651000976562\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"mesh\":1,\n\t\t\t\"name\":\"Plane\"\n\t\t},\n\t\t{\n\t\t\t\"extensions\":{\n\t\t\t\t\"KHR_lights_punctual\":{\n\t\t\t\t\t\"light\":1\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"name\":\"Sun\",\n\t\t\t\"rotation\":[\n\t\t\t\t-0.24051783978939056,\n\t\t\t\t0.49573880434036255,\n\t\t\t\t0.08695897459983826,\n\t\t\t\t0.8299592733383179\n\t\t\t],\n\t\t\t\"translation\":[\n\t\t\t\t10.709425926208496,\n\t\t\t\t5.903861999511719,\n\t\t\t\t7.419057846069336\n\t\t\t]\n\t\t}\n\t],\n\t\"cameras\":[\n\t\t{\n\t\t\t\"name\":\"Camera\",\n\t\t\t\"perspective\":{\n\t\t\t\t\"aspectRatio\":1.7777777777777777,\n\t\t\t\t\"yfov\":0.39959648408210363,\n\t\t\t\t\"zfar\":100,\n\t\t\t\t\"znear\":0.10000000149011612\n\t\t\t},\n\t\t\t\"type\":\"perspective\"\n\t\t}\n\t],\n\t\"materials\":[\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"name\":\"Material\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"baseColorFactor\":[\n\t\t\t\t\t0.8004003763198853,\n\t\t\t\t\t0.013363108038902283,\n\t\t\t\t\t0.020213128998875618,\n\t\t\t\t\t1\n\t\t\t\t],\n\t\t\t\t\"metallicFactor\":0,\n\t\t\t\t\"roughnessFactor\":0.5\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"name\":\"Material.001\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"metallicFactor\":0,\n\t\t\t\t\"roughnessFactor\":0.8815789222717285\n\t\t\t}\n\t\t}\n\t],\n\t\"meshes\":[\n\t\t{\n\t\t\t\"name\":\"Cube\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":0,\n\t\t\t\t\t\t\"NORMAL\":1,\n\t\t\t\t\t\t\"TEXCOORD_0\":2\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":3,\n\t\t\t\t\t\"material\":0\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\":\"Plane\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":4,\n\t\t\t\t\t\t\"NORMAL\":5,\n\t\t\t\t\t\t\"TEXCOORD_0\":6\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":7,\n\t\t\t\t\t\"material\":1\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"accessors\":[\n\t\t{\n\t\t\t\"bufferView\":0,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"max\":[\n\t\t\t\t1,\n\t\t\t\t3.7387936115264893,\n\t\t\t\t1\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-1,\n\t\t\t\t-1,\n\t\t\t\t-1\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":1,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":2,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":3,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":36,\n\t\t\t\"type\":\"SCALAR\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":4,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"max\":[\n\t\t\t\t23.336652755737305,\n\t\t\t\t0,\n\t\t\t\t23.336652755737305\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-23.336652755737305,\n\t\t\t\t-0.2270890176296234,\n\t\t\t\t-23.336652755737305\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":5,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":6,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":7,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":36,\n\t\t\t\"type\":\"SCALAR\"\n\t\t}\n\t],\n\t\"bufferViews\":[\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":0,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":288,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":192,\n\t\t\t\"byteOffset\":576,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":72,\n\t\t\t\"byteOffset\":768,\n\t\t\t\"target\":34963\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":840,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":1128,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":192,\n\t\t\t\"byteOffset\":1416,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":72,\n\t\t\t\"byteOffset\":1608,\n\t\t\t\"target\":34963\n\t\t}\n\t],\n\t\"buffers\":[\n\t\t{\n\t\t\t\"byteLength\":1680,\n\t\t\t\"uri\":\"shadow_mapping_only_cuboid_red_and_blue.bin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "gltf/shadow_mapping_sanity.gltf",
    "content": "{\n\t\"asset\":{\n\t\t\"generator\":\"Khronos glTF Blender I/O v4.3.47\",\n\t\t\"version\":\"2.0\"\n\t},\n\t\"extensionsUsed\":[\n\t\t\"KHR_materials_emissive_strength\",\n\t\t\"KHR_lights_punctual\"\n\t],\n\t\"extensionsRequired\":[\n\t\t\"KHR_lights_punctual\"\n\t],\n\t\"extensions\":{\n\t\t\"KHR_lights_punctual\":{\n\t\t\t\"lights\":[\n\t\t\t\t{\n\t\t\t\t\t\"color\":[\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1\n\t\t\t\t\t],\n\t\t\t\t\t\"intensity\":6830,\n\t\t\t\t\t\"type\":\"directional\",\n\t\t\t\t\t\"name\":\"Light\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t},\n\t\"scene\":0,\n\t\"scenes\":[\n\t\t{\n\t\t\t\"name\":\"Scene\",\n\t\t\t\"nodes\":[\n\t\t\t\t0,\n\t\t\t\t1,\n\t\t\t\t2,\n\t\t\t\t3,\n\t\t\t\t4,\n\t\t\t\t5,\n\t\t\t\t6,\n\t\t\t\t7\n\t\t\t]\n\t\t}\n\t],\n\t\"nodes\":[\n\t\t{\n\t\t\t\"mesh\":0,\n\t\t\t\"name\":\"Cube\",\n\t\t\t\"translation\":[\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\t0.8984647393226624\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"extensions\":{\n\t\t\t\t\"KHR_lights_punctual\":{\n\t\t\t\t\t\"light\":0\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"name\":\"Light\",\n\t\t\t\"rotation\":[\n\t\t\t\t0.39506182074546814,\n\t\t\t\t0.3723870813846588,\n\t\t\t\t0.7984986901283264,\n\t\t\t\t0.26010406017303467\n\t\t\t],\n\t\t\t\"translation\":[\n\t\t\t\t4.076245307922363,\n\t\t\t\t1.0054539442062378,\n\t\t\t\t5.903861999511719\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"camera\":0,\n\t\t\t\"name\":\"Camera\",\n\t\t\t\"rotation\":[\n\t\t\t\t0.599669873714447,\n\t\t\t\t0.327972412109375,\n\t\t\t\t0.3349515199661255,\n\t\t\t\t0.6485658884048462\n\t\t\t],\n\t\t\t\"translation\":[\n\t\t\t\t14.699949264526367,\n\t\t\t\t-12.676651000976562,\n\t\t\t\t4.958309173583984\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"mesh\":1,\n\t\t\t\"name\":\"Plane\"\n\t\t},\n\t\t{\n\t\t\t\"mesh\":2,\n\t\t\t\"name\":\"Sphere\",\n\t\t\t\"translation\":[\n\t\t\t\t2.62099552154541,\n\t\t\t\t0.7535718679428101,\n\t\t\t\t1.7896754741668701\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"mesh\":3,\n\t\t\t\"name\":\"Sphere.001\",\n\t\t\t\"translation\":[\n\t\t\t\t-3.5589241981506348,\n\t\t\t\t-1.1574621200561523,\n\t\t\t\t1.4128384590148926\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"mesh\":4,\n\t\t\t\"name\":\"Cylinder\",\n\t\t\t\"rotation\":[\n\t\t\t\t-0.14098228514194489,\n\t\t\t\t0.5328636765480042,\n\t\t\t\t0.2134113311767578,\n\t\t\t\t0.8066200613975525\n\t\t\t],\n\t\t\t\"scale\":[\n\t\t\t\t0.2753068208694458,\n\t\t\t\t0.2753068506717682,\n\t\t\t\t0.2753068208694458\n\t\t\t],\n\t\t\t\"translation\":[\n\t\t\t\t4.076245307922363,\n\t\t\t\t1.0054539442062378,\n\t\t\t\t5.903861999511719\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"mesh\":5,\n\t\t\t\"name\":\"Cylinder.001\",\n\t\t\t\"rotation\":[\n\t\t\t\t-0.14098228514194489,\n\t\t\t\t0.5328636765480042,\n\t\t\t\t0.2134113311767578,\n\t\t\t\t0.8066200613975525\n\t\t\t],\n\t\t\t\"scale\":[\n\t\t\t\t0.2753068208694458,\n\t\t\t\t0.2753068506717682,\n\t\t\t\t0.2753068208694458\n\t\t\t],\n\t\t\t\"translation\":[\n\t\t\t\t-6.663036346435547,\n\t\t\t\t-4.068678855895996,\n\t\t\t\t0.5199804306030273\n\t\t\t]\n\t\t}\n\t],\n\t\"cameras\":[\n\t\t{\n\t\t\t\"name\":\"Camera\",\n\t\t\t\"perspective\":{\n\t\t\t\t\"aspectRatio\":1.7777777777777777,\n\t\t\t\t\"yfov\":0.39959648408210363,\n\t\t\t\t\"zfar\":100,\n\t\t\t\t\"znear\":0.10000000149011612\n\t\t\t},\n\t\t\t\"type\":\"perspective\"\n\t\t}\n\t],\n\t\"materials\":[\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"name\":\"Material\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"baseColorFactor\":[\n\t\t\t\t\t0.8004003763198853,\n\t\t\t\t\t0.013363108038902283,\n\t\t\t\t\t0.020213128998875618,\n\t\t\t\t\t1\n\t\t\t\t],\n\t\t\t\t\"metallicFactor\":0,\n\t\t\t\t\"roughnessFactor\":0.5\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"name\":\"Material.001\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"metallicFactor\":0,\n\t\t\t\t\"roughnessFactor\":0.8815789222717285\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"name\":\"SphereMaterial\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"baseColorFactor\":[\n\t\t\t\t\t0.14135831594467163,\n\t\t\t\t\t0.19362950325012207,\n\t\t\t\t\t0.8004969358444214,\n\t\t\t\t\t1\n\t\t\t\t],\n\t\t\t\t\"metallicFactor\":0.6710526347160339,\n\t\t\t\t\"roughnessFactor\":0.21052631735801697\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"name\":\"Material.002\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"baseColorFactor\":[\n\t\t\t\t\t0.13696487247943878,\n\t\t\t\t\t0.8001965880393982,\n\t\t\t\t\t0.11292669177055359,\n\t\t\t\t\t1\n\t\t\t\t],\n\t\t\t\t\"metallicFactor\":0,\n\t\t\t\t\"roughnessFactor\":0.5\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"emissiveFactor\":[\n\t\t\t\t0,\n\t\t\t\t0.31788796186447144,\n\t\t\t\t1\n\t\t\t],\n\t\t\t\"extensions\":{\n\t\t\t\t\"KHR_materials_emissive_strength\":{\n\t\t\t\t\t\"emissiveStrength\":10\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"name\":\"LightCone\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"baseColorFactor\":[\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t\t1\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t],\n\t\"meshes\":[\n\t\t{\n\t\t\t\"name\":\"Cube\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":0,\n\t\t\t\t\t\t\"NORMAL\":1,\n\t\t\t\t\t\t\"TEXCOORD_0\":2\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":3,\n\t\t\t\t\t\"material\":0\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\":\"Plane\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":4,\n\t\t\t\t\t\t\"NORMAL\":5,\n\t\t\t\t\t\t\"TEXCOORD_0\":6\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":7,\n\t\t\t\t\t\"material\":1\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\":\"Sphere\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":8,\n\t\t\t\t\t\t\"NORMAL\":9,\n\t\t\t\t\t\t\"TEXCOORD_0\":10\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":11,\n\t\t\t\t\t\"material\":2\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\":\"Sphere.001\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":12,\n\t\t\t\t\t\t\"NORMAL\":13,\n\t\t\t\t\t\t\"TEXCOORD_0\":14\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":15,\n\t\t\t\t\t\"material\":3\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\":\"Cylinder\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":16,\n\t\t\t\t\t\t\"NORMAL\":17,\n\t\t\t\t\t\t\"TEXCOORD_0\":18\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":19,\n\t\t\t\t\t\"material\":4\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\":\"Cylinder.001\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":20,\n\t\t\t\t\t\t\"NORMAL\":21,\n\t\t\t\t\t\t\"TEXCOORD_0\":22\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":19,\n\t\t\t\t\t\"material\":4\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"accessors\":[\n\t\t{\n\t\t\t\"bufferView\":0,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"max\":[\n\t\t\t\t1,\n\t\t\t\t1,\n\t\t\t\t3.7387936115264893\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-1,\n\t\t\t\t-1,\n\t\t\t\t-1\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":1,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":2,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":3,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":36,\n\t\t\t\"type\":\"SCALAR\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":4,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"max\":[\n\t\t\t\t23.336652755737305,\n\t\t\t\t23.336652755737305,\n\t\t\t\t0\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-23.336652755737305,\n\t\t\t\t-23.336652755737305,\n\t\t\t\t-0.2270890176296234\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":5,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":6,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":7,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":36,\n\t\t\t\"type\":\"SCALAR\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":8,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":1984,\n\t\t\t\"max\":[\n\t\t\t\t0.9999997019767761,\n\t\t\t\t1,\n\t\t\t\t1\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-0.9999990463256836,\n\t\t\t\t-0.9999993443489075,\n\t\t\t\t-1\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":9,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":1984,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":10,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":1984,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":11,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":2880,\n\t\t\t\"type\":\"SCALAR\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":12,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":1984,\n\t\t\t\"max\":[\n\t\t\t\t0.9999997019767761,\n\t\t\t\t1,\n\t\t\t\t-0.09411358833312988\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-0.9999990463256836,\n\t\t\t\t-0.9999993443489075,\n\t\t\t\t-2.09411358833313\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":13,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":1984,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":14,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":1984,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":15,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":2880,\n\t\t\t\"type\":\"SCALAR\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":16,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":192,\n\t\t\t\"max\":[\n\t\t\t\t1,\n\t\t\t\t1,\n\t\t\t\t1\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-1,\n\t\t\t\t-1,\n\t\t\t\t-1\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":17,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":192,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":18,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":192,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":19,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":372,\n\t\t\t\"type\":\"SCALAR\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":20,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":192,\n\t\t\t\"max\":[\n\t\t\t\t1,\n\t\t\t\t1,\n\t\t\t\t1\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-1,\n\t\t\t\t-1,\n\t\t\t\t-1\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":21,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":192,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":22,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":192,\n\t\t\t\"type\":\"VEC2\"\n\t\t}\n\t],\n\t\"bufferViews\":[\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":0,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":288,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":192,\n\t\t\t\"byteOffset\":576,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":72,\n\t\t\t\"byteOffset\":768,\n\t\t\t\"target\":34963\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":840,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":1128,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":192,\n\t\t\t\"byteOffset\":1416,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":72,\n\t\t\t\"byteOffset\":1608,\n\t\t\t\"target\":34963\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":23808,\n\t\t\t\"byteOffset\":1680,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":23808,\n\t\t\t\"byteOffset\":25488,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":15872,\n\t\t\t\"byteOffset\":49296,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":5760,\n\t\t\t\"byteOffset\":65168,\n\t\t\t\"target\":34963\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":23808,\n\t\t\t\"byteOffset\":70928,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":23808,\n\t\t\t\"byteOffset\":94736,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":15872,\n\t\t\t\"byteOffset\":118544,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":5760,\n\t\t\t\"byteOffset\":134416,\n\t\t\t\"target\":34963\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":2304,\n\t\t\t\"byteOffset\":140176,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":2304,\n\t\t\t\"byteOffset\":142480,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":1536,\n\t\t\t\"byteOffset\":144784,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":744,\n\t\t\t\"byteOffset\":146320,\n\t\t\t\"target\":34963\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":2304,\n\t\t\t\"byteOffset\":147064,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":2304,\n\t\t\t\"byteOffset\":149368,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":1536,\n\t\t\t\"byteOffset\":151672,\n\t\t\t\"target\":34962\n\t\t}\n\t],\n\t\"buffers\":[\n\t\t{\n\t\t\t\"byteLength\":153208,\n\t\t\t\"uri\":\"shadow_mapping_sanity.bin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "gltf/shadow_mapping_sanity_camera.gltf",
    "content": "{\n\t\"asset\":{\n\t\t\"generator\":\"Khronos glTF Blender I/O v4.3.47\",\n\t\t\"version\":\"2.0\"\n\t},\n\t\"extensionsUsed\":[\n\t\t\"KHR_lights_punctual\"\n\t],\n\t\"extensionsRequired\":[\n\t\t\"KHR_lights_punctual\"\n\t],\n\t\"extensions\":{\n\t\t\"KHR_lights_punctual\":{\n\t\t\t\"lights\":[\n\t\t\t\t{\n\t\t\t\t\t\"color\":[\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t1\n\t\t\t\t\t],\n\t\t\t\t\t\"intensity\":6830,\n\t\t\t\t\t\"type\":\"directional\",\n\t\t\t\t\t\"name\":\"Light\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t},\n\t\"scene\":0,\n\t\"scenes\":[\n\t\t{\n\t\t\t\"name\":\"Scene\",\n\t\t\t\"nodes\":[\n\t\t\t\t0,\n\t\t\t\t1,\n\t\t\t\t2,\n\t\t\t\t3,\n\t\t\t\t4\n\t\t\t]\n\t\t}\n\t],\n\t\"nodes\":[\n\t\t{\n\t\t\t\"mesh\":0,\n\t\t\t\"name\":\"Cube\",\n\t\t\t\"translation\":[\n\t\t\t\t0,\n\t\t\t\t1.5244084596633911,\n\t\t\t\t0\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"extensions\":{\n\t\t\t\t\"KHR_lights_punctual\":{\n\t\t\t\t\t\"light\":0\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"name\":\"Light\",\n\t\t\t\"rotation\":[\n\t\t\t\t0.09542951732873917,\n\t\t\t\t0.8279411792755127,\n\t\t\t\t0.3013063669204712,\n\t\t\t\t0.4632721543312073\n\t\t\t],\n\t\t\t\"translation\":[\n\t\t\t\t4.076245307922363,\n\t\t\t\t5.903861999511719,\n\t\t\t\t-1.0054539442062378\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"camera\":0,\n\t\t\t\"name\":\"Camera\",\n\t\t\t\"rotation\":[\n\t\t\t\t-0.03457474336028099,\n\t\t\t\t0.4687580168247223,\n\t\t\t\t0.004934974014759064,\n\t\t\t\t0.8826359510421753\n\t\t\t],\n\t\t\t\"translation\":[\n\t\t\t\t14.699949264526367,\n\t\t\t\t4.958309173583984,\n\t\t\t\t12.676651000976562\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"mesh\":1,\n\t\t\t\"name\":\"Plane\"\n\t\t},\n\t\t{\n\t\t\t\"mesh\":2,\n\t\t\t\"name\":\"Sphere\",\n\t\t\t\"translation\":[\n\t\t\t\t2.62099552154541,\n\t\t\t\t1.7896754741668701,\n\t\t\t\t-0.7535718679428101\n\t\t\t]\n\t\t}\n\t],\n\t\"cameras\":[\n\t\t{\n\t\t\t\"name\":\"Camera\",\n\t\t\t\"perspective\":{\n\t\t\t\t\"aspectRatio\":1.7777777777777777,\n\t\t\t\t\"yfov\":0.39959648408210363,\n\t\t\t\t\"zfar\":100,\n\t\t\t\t\"znear\":0.10000000149011612\n\t\t\t},\n\t\t\t\"type\":\"perspective\"\n\t\t}\n\t],\n\t\"materials\":[\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"name\":\"Material\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"baseColorFactor\":[\n\t\t\t\t\t0.8004003763198853,\n\t\t\t\t\t0.013363108038902283,\n\t\t\t\t\t0.020213128998875618,\n\t\t\t\t\t1\n\t\t\t\t],\n\t\t\t\t\"metallicFactor\":0,\n\t\t\t\t\"roughnessFactor\":0.5\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"name\":\"Material.001\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"baseColorFactor\":[\n\t\t\t\t\t0.4381273686885834,\n\t\t\t\t\t0.4381273686885834,\n\t\t\t\t\t0.4381273686885834,\n\t\t\t\t\t1\n\t\t\t\t],\n\t\t\t\t\"metallicFactor\":0,\n\t\t\t\t\"roughnessFactor\":0.8815789222717285\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"doubleSided\":true,\n\t\t\t\"name\":\"SphereMaterial\",\n\t\t\t\"pbrMetallicRoughness\":{\n\t\t\t\t\"baseColorFactor\":[\n\t\t\t\t\t0.14135831594467163,\n\t\t\t\t\t0.19362950325012207,\n\t\t\t\t\t0.8004969358444214,\n\t\t\t\t\t1\n\t\t\t\t],\n\t\t\t\t\"metallicFactor\":0.6710526347160339,\n\t\t\t\t\"roughnessFactor\":0.21052631735801697\n\t\t\t}\n\t\t}\n\t],\n\t\"meshes\":[\n\t\t{\n\t\t\t\"name\":\"Cube\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":0,\n\t\t\t\t\t\t\"NORMAL\":1,\n\t\t\t\t\t\t\"TEXCOORD_0\":2\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":3,\n\t\t\t\t\t\"material\":0\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\":\"Plane\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":4,\n\t\t\t\t\t\t\"NORMAL\":5,\n\t\t\t\t\t\t\"TEXCOORD_0\":6\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":7,\n\t\t\t\t\t\"material\":1\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\":\"Sphere\",\n\t\t\t\"primitives\":[\n\t\t\t\t{\n\t\t\t\t\t\"attributes\":{\n\t\t\t\t\t\t\"POSITION\":8,\n\t\t\t\t\t\t\"NORMAL\":9,\n\t\t\t\t\t\t\"TEXCOORD_0\":10\n\t\t\t\t\t},\n\t\t\t\t\t\"indices\":11,\n\t\t\t\t\t\"material\":2\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"accessors\":[\n\t\t{\n\t\t\t\"bufferView\":0,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"max\":[\n\t\t\t\t1,\n\t\t\t\t3.7387936115264893,\n\t\t\t\t1\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-1,\n\t\t\t\t-1,\n\t\t\t\t-1\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":1,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":2,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":24,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":3,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":36,\n\t\t\t\"type\":\"SCALAR\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":4,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":4,\n\t\t\t\"max\":[\n\t\t\t\t23.336652755737305,\n\t\t\t\t0,\n\t\t\t\t23.336652755737305\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-23.336652755737305,\n\t\t\t\t0,\n\t\t\t\t-23.336652755737305\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":5,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":4,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":6,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":4,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":7,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":6,\n\t\t\t\"type\":\"SCALAR\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":8,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":1984,\n\t\t\t\"max\":[\n\t\t\t\t0.9999997019767761,\n\t\t\t\t1,\n\t\t\t\t0.9999993443489075\n\t\t\t],\n\t\t\t\"min\":[\n\t\t\t\t-0.9999990463256836,\n\t\t\t\t-1,\n\t\t\t\t-1\n\t\t\t],\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":9,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":1984,\n\t\t\t\"type\":\"VEC3\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":10,\n\t\t\t\"componentType\":5126,\n\t\t\t\"count\":1984,\n\t\t\t\"type\":\"VEC2\"\n\t\t},\n\t\t{\n\t\t\t\"bufferView\":11,\n\t\t\t\"componentType\":5123,\n\t\t\t\"count\":2880,\n\t\t\t\"type\":\"SCALAR\"\n\t\t}\n\t],\n\t\"bufferViews\":[\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":0,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":288,\n\t\t\t\"byteOffset\":288,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":192,\n\t\t\t\"byteOffset\":576,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":72,\n\t\t\t\"byteOffset\":768,\n\t\t\t\"target\":34963\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":48,\n\t\t\t\"byteOffset\":840,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":48,\n\t\t\t\"byteOffset\":888,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":32,\n\t\t\t\"byteOffset\":936,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":12,\n\t\t\t\"byteOffset\":968,\n\t\t\t\"target\":34963\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":23808,\n\t\t\t\"byteOffset\":980,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":23808,\n\t\t\t\"byteOffset\":24788,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":15872,\n\t\t\t\"byteOffset\":48596,\n\t\t\t\"target\":34962\n\t\t},\n\t\t{\n\t\t\t\"buffer\":0,\n\t\t\t\"byteLength\":5760,\n\t\t\t\"byteOffset\":64468,\n\t\t\t\"target\":34963\n\t\t}\n\t],\n\t\"buffers\":[\n\t\t{\n\t\t\t\"byteLength\":70228,\n\t\t\t\"uri\":\"shadow_mapping_sanity_camera.bin\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "gltf/simple_morph_triangle.gltf",
    "content": "{\n  \"scene\": 0,\n  \"scenes\":[\n    {\n      \"nodes\":[\n        0\n      ]\n    }\n  ],\n  \"nodes\":[\n    {\n      \"mesh\":0\n    }\n  ],\n  \"meshes\":[\n    {\n      \"primitives\":[\n        {\n          \"attributes\":{\n            \"POSITION\":1\n          },\n          \"targets\":[\n            {\n              \"POSITION\":2\n            },\n            {\n              \"POSITION\":3\n            }\n          ],\n          \"indices\":0\n        }\n      ],\n      \"weights\":[\n        1.0,\n        0.5\n      ]\n    }\n  ],\n\n  \"animations\":[\n    {\n      \"samplers\":[\n        {\n          \"input\":4,\n          \"interpolation\":\"LINEAR\",\n          \"output\":5\n        }\n      ],\n      \"channels\":[\n        {\n          \"sampler\":0,\n          \"target\":{\n            \"node\":0,\n            \"path\":\"weights\"\n          }\n        }\n      ]\n    }\n  ],\n\n  \"buffers\":[\n    {\n      \"uri\":\"data:application/gltf-buffer;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIA/AACAPwAAAAA=\",\n      \"byteLength\":116\n    },\n    {\n      \"uri\":\"data:application/gltf-buffer;base64,AAAAAAAAgD8AAABAAABAQAAAgEAAAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAA\",\n      \"byteLength\":60\n    }\n  ],\n  \"bufferViews\":[\n    {\n      \"buffer\":0,\n      \"byteOffset\":0,\n      \"byteLength\":6,\n      \"target\":34963\n    },\n    {\n      \"buffer\":0,\n      \"byteOffset\":8,\n      \"byteLength\":108,\n      \"byteStride\":12,\n      \"target\":34962\n    },\n    {\n      \"buffer\":1,\n      \"byteOffset\":0,\n      \"byteLength\":20\n    },\n    {\n      \"buffer\":1,\n      \"byteOffset\":20,\n      \"byteLength\":40\n    }\n  ],\n  \"accessors\":[\n    {\n      \"bufferView\":0,\n      \"byteOffset\":0,\n      \"componentType\":5123,\n      \"count\":3,\n      \"type\":\"SCALAR\",\n      \"max\":[\n        2\n      ],\n      \"min\":[\n        0\n      ]\n    },\n    {\n      \"bufferView\":1,\n      \"byteOffset\":0,\n      \"componentType\":5126,\n      \"count\":3,\n      \"type\":\"VEC3\",\n      \"max\":[\n        1.0,\n        0.5,\n        0.0\n      ],\n      \"min\":[\n        0.0,\n        0.0,\n        0.0\n      ]\n    },\n    {\n      \"bufferView\":1,\n      \"byteOffset\":36,\n      \"componentType\":5126,\n      \"count\":3,\n      \"type\":\"VEC3\",\n      \"max\":[\n        0.0,\n        1.0,\n        0.0\n      ],\n      \"min\":[\n        -1.0,\n        0.0,\n        0.0\n      ]\n    },\n    {\n      \"bufferView\":1,\n      \"byteOffset\":72,\n      \"componentType\":5126,\n      \"count\":3,\n      \"type\":\"VEC3\",\n      \"max\":[\n        1.0,\n        1.0,\n        0.0\n      ],\n      \"min\":[\n        0.0,\n        0.0,\n        0.0\n      ]\n    },\n    {\n      \"bufferView\":2,\n      \"byteOffset\":0,\n      \"componentType\":5126,\n      \"count\":5,\n      \"type\":\"SCALAR\",\n      \"max\":[\n        4.0\n      ],\n      \"min\":[\n        0.0\n      ]\n    },\n    {\n      \"bufferView\":3,\n      \"byteOffset\":0,\n      \"componentType\":5126,\n      \"count\":10,\n      \"type\":\"SCALAR\",\n      \"max\":[\n        1.0\n      ],\n      \"min\":[\n        0.0\n      ]\n    }\n  ],\n\n  \"asset\":{\n    \"version\":\"2.0\"\n  }\n}\n"
  },
  {
    "path": "manual/.gitignore",
    "content": "book\n"
  },
  {
    "path": "manual/book.toml",
    "content": "[book]\nauthors = [\"Schell Carl Scivally\"]\nlanguage = \"en\"\nsrc = \"src\"\ntitle = \"The Renderling Manual\"\ndescription = \"Operations manual for the Renderling real-time renderer\"\n\n# The \"environment\" preprocessor, provided by `mdbook-environment`,\n# references variables from here and the environment that can then be\n# interpolated in the book with `{{VARIABLE}}`.\n#\n# Setting variables here overrides any set in the environment, so for\n# variables that must change based on environment we add them here\n# commented out.\n[preprocessor.environment]\n# when deploying the manual \n# DOCS_URL = \"https://docs.rs/renderling/latest\"\n\n# when running locally\n# DOCS_URL = \"http://localhost:4000\"\n\n"
  },
  {
    "path": "manual/src/SUMMARY.md",
    "content": "# Summary\n\n- [Welcome](./welcome.md)  \n- [Project setup](./setup.md)\n- [Context creation](./context.md)\n- [Staging resources](./stage.md)\n- [Loading GLTF files](./gltf.md)\n- [Rendering with a skybox](./skybox.md)\n- [Lighting](./lighting.md)\n  - [Analytical lights](./lighting/analytical.md)\n  - [Image based lighting](./lighting/ibl.md)\n"
  },
  {
    "path": "manual/src/context.md",
    "content": "# Context\n\nThe first step of any `renderling` program starts with [`renderling::context::Context`][Context].\n\nThe `Context` is responsible for managing the underlying [`wgpu`][wgpu] runtime, including the\ninstance, adapter and queue.\nIt also sets up the [`RenderTarget`][RenderTarget], according to how the `Context` was created.\n\nOn that note, it's important to know that there are two main ways to create a `Context`:\n\n1. A headless context, which renders to a texture, can be created with\n  [`Context::headless`][Context#headless] or\n  [`Context::try_new_headless`][Context#try_headless], depending on your error\n  handling scenario.\n2. A surface context, with a window (possibly from [`winit`][winit]) or a canvas\n  from [`web-sys`][web-sys].\n\n```rust, ignore\n{{#include ../../crates/examples/src/context.rs:create}}\n```\n\n## Getting a frame\n\nAnother important concept is the [`Frame`][Frame]. Each time you'd like to present a new image\nyou must acquire a frame from the `Context` with [`Context::get_next_frame`][Context#get_next_frame]\nand present it with [`Frame::present`][Frame#present].\n\n### Presenting on WASM\n\nWhen on WASM (aka running in a browser), [`Frame::present`][Frame#present] is a noop.\nIt's still a good idea to use it though, so you don't forget when programming in native.\n\n### Saving the frame\n\nYou can also read out the frame to an image provided by the [`image`][image]\ncrate. See the [`Frame`][Frame] docs for help with the `read_*` functions.\n\n### Frame example\n\n```rust, ignore\n{{#include ../../crates/examples/src/context.rs:frame}}\n```\n\n[Context]: {{DOCS_URL}}/renderling/context/struct.Context.html\n[Context#headless]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.headless\n[Context#try_headless]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.try_headless\n[Context#get_next_frame]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.get_next_frame\n\n[Frame]: {{DOCS_URL}}/renderling/context/struct.Frame.html\n[Frame#present]: {{DOCS_URL}}/renderling/context/struct.Frame.html#method.present\n\n[RenderTarget]: {{DOCS_URL}}/renderling/context/struct.RenderTarget.html\n\n[image]: https://crates.io/crates/image\n[wgpu]: https://crates.io/crates/wgpu\n[winit]: https://crates.io/crates/winit\n[web-sys]: https://crates.io/crates/web-sys\n"
  },
  {
    "path": "manual/src/gltf.md",
    "content": "# Loading GLTF files 📂\n\n`renderling`'s built-in model format is [GLTF](https://www.khronos.org/gltf/), a\nversatile and efficient format for transmitting 3D models. GLTF, which stands\nfor GL Transmission Format, is designed to be a compact, interoperable format\nthat can be used across various platforms and applications. It supports a wide\nrange of features including geometry, materials, animations, and more, making it\na popular choice for 3D graphics.\n\n## Using GLTF files\n\nThe previous section on [staging resources](./stage) covered the creation of\nvarious GPU resources such as [`Camera`], [`Vertices`], [`Material`],\n[`Primitive`], and [`Transform`]. When you load a GLTF file into `renderling`,\nit automatically stages a collection of these resources. This means that the\nGLTF file is parsed, and the corresponding GPU resources are created and\nreturned to you, allowing you to integrate them into your application\nseamlessly.\n\n## Example\n\nWe'll start by creating our [`Context`], [`Stage`] and [`Camera`]:\n\n```rust,ignore\n{{#include ../../crates/examples/src/gltf.rs:setup}}\n```\n\nThen we load our GLTF file through the [`Stage`] with\n[`Stage::load_gltf_document_from_path`], and as long as there are no errors it returns a\n[`GltfDocument`]:\n\n```rust,ignore\n{{#include ../../crates/examples/src/gltf.rs:load}}\n```\n\nOn WASM we would use [`Stage::load_gltf_document_from_bytes`] as the filesystem\nis unavailable.\n\nNotice how in the above example we call [`GltfDocument::into_gpu_only`] to\nunload the mesh geometry from the CPU.\n\n## Render\n\n```rust,ignore\n{{#include ../../crates/examples/src/gltf.rs:render_1}}\n```\n\n## Result\n\n![a loaded GLTF file, a marble bust, in shadow](assets/gltf-example-shadow.png)\n\nBut wait! It's all in shadow.\n\nThis is because we haven't added any lighting.\n\nWe have two options here:\n1. Turn of lighting and show the scene \"unlit\", using [`Stage::set_has_lighting`]\n2. Add some lights\n\nFor now we'll go with option `1`, as lighting happens in a later section:\n\n```rust,ignore\n{{#include ../../crates/examples/src/gltf.rs:no_lights}}\n```\n\n![a loaded GLTF file, a marble bust, unlit](assets/gltf-example-unlit.png)\n\n[`Context`]: {{DOCS_URL}}/renderling/context/struct.Context.html\n[`Stage`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html\n[`Stage::load_gltf_document_from_bytes`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.load_gltf_document_from_bytes\n[`Stage::load_gltf_document_from_path`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.load_gltf_document_from_path\n[`Stage::set_has_lighting`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.set_has_lighting\n[`GltfDocument`]: {{DOCS_URL}}/renderling/gltf/struct.GltfDocument.html\n[`GltfDocument::into_gpu_only`]: {{DOCS_URL}}/renderling/gltf/struct.GltfDocument.html#method.into_gpu_only\n[`Camera`]: {{DOCS_URL}}/renderling/camera/struct.Camera.html\n[`Material`]: {{DOCS_URL}}/renderling/material/struct.Material.html\n[`Primitive`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html\n[`Vertices`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html\n[`Transform`]: {{DOCS_URL}}/renderling/transform/struct.Transform.html\n"
  },
  {
    "path": "manual/src/lighting/analytical.md",
    "content": "# Analytical lights\n\nAnalytical lighting in real-time rendering refers to the use of mathematical\nmodels to simulate the effects of light on surfaces. \n\nWhat that means in `renderling` is that analytical lights are the lights that\nyou create, configure and place programmatically, one by one, into the scene.\n\nTo do any lighting, though, we have to turn lighting back on in the stage:\n\n```rust,ignore\n{{#include ../../../crates/examples/src/lighting.rs:lighting_on}}\n```\n\n![image of a marble bust in shadow](../assets/lighting/no-lights.png)\n\nAs we talked about in [the GLTF example](/gltf.html#render), with no lights\non the stage, the bust renders in shadow.\n\nNow we're ready to add some lights.\n\n## Directional lights\n\nDirectional lights simulate light coming from a specific direction, like\nsunlight. They affect all objects in the scene equally, regardless of their\nposition, and do not diminish with distance. This makes them ideal for\nsimulating large-scale lighting effects.\n\nLet's create a directional light:\n\n```rust,ignore\n{{#include ../../../crates/examples/src/lighting.rs:directional}}\n```\n\n![image of a marble bust lit by a single directional light](../assets/lighting/directional.png)\n\nNot bad!\n\nBefore moving on we'll remove the directional light:\n\n```rust,ignore\n{{#include ../../../crates/examples/src/lighting.rs:remove_directional}}\n```\n\nDropping the light isn't strictly necessary, except to reclaim the resources.\n\n## Point lights\n\nPoint lights emit light equally in all directions from a single point in space,\nsimilar to a light bulb. They are ideal for simulating localized light sources\nand their intensity diminishes with distance, following the inverse square law.\nThis makes them suitable for creating realistic lighting effects in small areas.\n\nLet's create a point light:\n\n```rust,ignore\n{{#include ../../../crates/examples/src/lighting.rs:point}}\n```\n\n![image of a marble bust lit by a single point light](../assets/lighting/point.png)\n\nSimilarly we'll remove the point light before moving on:\n\n```rust,ignore\n{{#include ../../../crates/examples/src/lighting.rs:remove_point}}\n```\n\n## Spot lights\n\nSpot lights emit a cone of light from a single point, with a specified direction\nand angle. They are useful for highlighting specific areas or objects in a\nscene, such as a spotlight on a stage. The intensity of a spotlight diminishes\nwith distance and is also affected by the angle of the cone, allowing for\nprecise control over the lighting effect.\n\nLet's create a spotlight. One thing about spotlights though, they can be a bit fiddly\ndue to having a position, direction _and_ inner and outer cutoff values. For this reason\nwe'll place the spotlight at the camera's position and point it in the same direction, so\nyou can see the effect:\n\n```rust,ignore\n{{#include ../../../crates/examples/src/lighting.rs:spot}}\n```\n\n![image of a marble bust lit by a single spot light](../assets/lighting/spot.png)\n\nGood enough! Now on to image-based lighting, which uses environment maps to simulate complex lighting scenarios. This technique captures real-world lighting conditions and applies them to the scene, providing more realistic reflections and ambient lighting.\n"
  },
  {
    "path": "manual/src/lighting/ibl.md",
    "content": "# Image based lighting 🌐\n\nImage-based lighting (IBL) is a technique that uses environment maps to\nilluminate scenes. It captures real-world lighting conditions and applies them\nto 3D models, providing realistic reflections and ambient lighting. IBL is\nparticularly effective for creating natural-looking scenes by simulating complex\nlighting scenarios that are difficult to achieve with traditional analytical lights alone.\n\n## Example setup\n\nWe'll start from where we left off with the skybox example (before adding\nanalytical lights):\n\n```rust,ignore\n{{#include ../../../crates/examples/src/lighting.rs:ibl_setup}}\n```\n\nNow we'll add image based lighting.\n\n## The `Ibl` type\n\n[`Ibl`] is the type responsible for image based lighting.\nYou can think of it as a type of \"global\" light.\nMore than one [`Ibl`] may exist, but only one can be used\nby the stage at render time.\n\nCreating an [`Ibl`] follows the same expected builder pattern,\nand just like [`Skybox`] we call a familiar `use_*` function\non the [`Stage`] to use it:\n\n```rust,ignore\n{{#include ../../../crates/examples/src/lighting.rs:ibl}}\n```\n\n![image of a marble bust lit by the helipad environment map](../assets/lighting/ibl.png)\n\n## Mix it up! 🎨\n\nYou can mix global image based lighting with analytical lights, just as you\nmight expect. Here we'll build on the previous example to add a point light:\n\n```rust,ignore\n{{#include ../../../crates/examples/src/lighting.rs:mix}}\n```\n\n![image of a marble bust lit by the helipad environment map](../assets/lighting/ibl-analytical-mixed.png)\n\nBy combining IBL with analytical lights, you can achieve a rich and dynamic\nlighting environment that captures both the subtle nuances of ambient light and\nthe dramatic effects of direct illumination.\nExperiment with different environment maps and light setups to find the perfect\nbalance for your scene.\n\n[`Ibl`]: {{DOCS_URL}}/renderling/pbr/ibl/struct.Ibl.html\n[`Skybox`]: {{DOCS_URL}}/renderling/skybox/struct.Skybox.html\n[`Stage`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html\n"
  },
  {
    "path": "manual/src/lighting.md",
    "content": "# Lighting 💡\n\nLighting in `renderling` comes in a few flavors:\n\n1. **Unlit** - no lighting at all\n2. **Analytical lights** - specific lights created by the programmer\n    * directional \n    * point\n    * spot\n3. **Image based lighting** - lighting by 3d environment maps\n\n\n## Scene recap\n\nWe've already used the \"unlit\" method of turning off all lighting on the stage.\n\nLet's do a quick recap of our scene, starting where we left off with the skybox\nexample.\n\nWe created our context, and then our stage, and it's important to note that we\nused `.with_lighting(false)` on the stage, which tells the stage not to use any\nlighting.\nThis is the \"unlit\" lighting method mentioned above.\n\nThen we created a camera and loaded a GLTF file of a marble bust, then loaded an\nHDR image into a skybox, and then rendered:\n\n```rust,ignore\n{{#include ../../crates/examples/src/lighting.rs:setup}}\n```\n\n![renderling skybox](assets/skybox.png)\n\nNow let's learn about analytical lights, and then image based lighting.\n"
  },
  {
    "path": "manual/src/reflinks.md",
    "content": "# docs\n\n[`Camera`]: {{DOCS_URL}}/renderling/camera/struct.Camera.html\n[`Camera::with_default_perspective`]: {{DOCS_URL}}/renderling/camera/struct.Camera.html#method.with_default_perspective\n\n[`Context`]: {{DOCS_URL}}/renderling/context/struct.Context.html\n[`Context::new_stage`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.new_stage\n[`Context::headless`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.headless\n[`Context::try_headless`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.try_headless\n[`Context::get_next_frame`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.get_next_frame\n\n[`Frame`]: {{DOCS_URL}}/renderling/context/struct.Frame.html\n[`Frame::present`]: {{DOCS_URL}}/renderling/context/struct.Frame.html#method.present\n\n[`Primitive`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html\n\n[`Material`]: {{DOCS_URL}}/renderling/material/struct.Material.html\n\n[`Mat4`]: https://docs.rs/glam/latest/glam/f32/struct.Mat4.html\n\n[`RenderTarget`]: {{DOCS_URL}}/renderling/context/struct.RenderTarget.html\n\n[`Skybox`]: {{DOCS_URL}}/renderling/skybox/struct.Skybox.html\n\n[`Stage`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html\n[`Stage::new_camera`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_camera\n\n[`Vertices`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html\n[`Vertices::get_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.get_vertex\n[`Vertices::modify_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.modify_vertex\n[`Vertices::set_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.set_vertex\n\n[`Vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertex.html\n\n# friends\n\n[glam]: https://crates.io/crates/glam\n[image]: https://crates.io/crates/image\n[wgpu]: https://crates.io/crates/wgpu\n[winit]: https://crates.io/crates/winit\n[web-sys]: https://crates.io/crates/web-sys\n\n# other\n\n[builder-pattern]: https://rust-unofficial.github.io/patterns/patterns/creational/builder.html\n"
  },
  {
    "path": "manual/src/setup.md",
    "content": "# Setup\n\n`renderling` is a Rust library, so first you'll need to get familiar with the\nlanguage. Visit <https://www.rust-lang.org/learn/get-started> if you're not\nalready familiar.\n\nOnce you're ready, start a new project with `cargo new`.\nThen `cd` into your project directory and add `renderling` as a dependency:\n\n```\ncargo add --git https://github.com/schell/renderling.git --branch main\n```\n\n## patch crates.io\n\n`renderling` is special in that all the shaders are written in Rust using\n[Rust-GPU](https://rust-gpu.github.io/), which is currently between\nreleases. For this reason we need to add an entry to the `[patch.crates-io]`\nsection of our `Cargo.toml`:\n\n```toml\n[patch.crates-io]\nspirv-std = { git = \"https://github.com/rust-gpu/rust-gpu.git\", rev = \"05b34493ce661dccd6694cf58afc13e3c8f7a7e0\" }  \n```\n\nThis is a temporary workaround that will be resolved after the next Rust-GPU\nrelease.\n\nThe rest is Rust business as usual.\n\n## WASM\n\nTODO: write about setting up a WASM project.\n\n## Re-exports\n\n`renderling` **re-exports** [`glam`][glam] from its top level module,\nbecause it provides the underlying mathematical types used throughout the API.\n\n[glam]: https://crates.io/crates/glam\n"
  },
  {
    "path": "manual/src/skybox.md",
    "content": "# Rendering a skybox 🌌\n\nOne of the most striking effects we can provide is a\n[skybox](https://en.wikipedia.org/wiki/Skybox_(video_games)).\n\nUsing a skybox is an easy way to improve immersion, and with\n`renderling` your skyboxes can also illuminate the scene, but\nwe'll save that for a later example. For now let's set up\nsimple skybox for our marble bust scene.\n\n## Building on the stage example \n\nWe'll start out this example by extending the example from the\n[loading GLTF files](./gltf) section. In that example we loaded\na model of an old marble bust:\n\n```rust,ignore\n{{#include ../../crates/examples/src/skybox.rs:setup}}\n```\n\n![image of a marble bust](assets/gltf-example-unlit.png)\n\n## Adding the skybox\n\nIn `renderling`, skyboxes get their background from an \"HDR\" image.\nThese are typically large three dimensional images. You can find\nfree HDR images [at PolyHaven](https://polyhaven.com/hdris) and other\nplaces around the web. \n\nFor this example we'll be using this HDR:\n\n![Rooftop helipad](assets/helipad.jpg)\n\n```rust,ignore\n{{#include ../../crates/examples/src/skybox.rs:skybox}}\n```\n\nThen we render:\n\n```rust,ignore\n{{#include ../../crates/examples/src/skybox.rs:render_skybox}}\n```\n\n\n## Results\n\nAnd there we go!\n\n![renderling skybox](assets/skybox.png)\n"
  },
  {
    "path": "manual/src/stage.md",
    "content": "# Staging resources 🎭\n\nThe [`Stage`] is the most important type in `renderling`.\nIt's responsible for staging all your scene's data on the GPU, as well as\nlinking all the various effects together and rendering it all.\n\n<!--toc:start-->\n- [Stage creation](#stage-creation)\n- [Resource creation](#resource-creation)\n  - [Camera](#camera)\n    - [glam and re-exports](#glam-and-re-exports)\n    - [Creation](#creation)\n  - [Geometry](#geometry)\n  - [Material](#material)\n  - [Primitive](#primitive)\n- [Rendering](#rendering)\n- [Results](#results)\n- [Removing resources](#removing-resources)\n- [Visibility](#visibility)\n<!--toc:end-->\n\n\n## Stage creation \n\nThe `Stage` is created with [`Context::new_stage`].\n\n```rust,ignore\n{{#include ../../crates/examples/src/stage.rs:creation}}\n```\n\nNotice that context creation is _asynchronous_. Most of the `renderling` API is\nsynchronous, but context creation is one of two exceptions - the other being\nreading data back from the GPU.\n\nAlso note that we can set the background color of the stage using a `Vec4`.\nAbove we've set the background to a light gray.\n\n## Resource creation\n\nNow we'll begin using the `Stage` to create our scene's resources. At the end of\nall our staging we should end up with a [`Camera`] and one simple\n[`Primitive`] representing a colored unit cube, sitting right in\nfront of the camera.\n\n### Camera\n\nIn order to see our scene we need a [`Camera`].\n\nThe camera controls the way our scene looks when rendered. It uses separate projection and view\nmatrices to that end. Discussing these matrices is out of scope for this manual, but there are\nplenty of resources online about what they are and how to use them.\n\n#### glam and re-exports\n\nOne important detail about these matrices, though, is that they come from the [`glam`][glam]\nlibrary. Specifically they are [`Mat4`][Mat4], which are a 4x4 transformation matrix.\n\n#### Creation\n\nOn with our camera. Creation is dead simple using [`Stage::new_camera`].\n\n```rust,ignore\n{{#include ../../crates/examples/src/stage.rs:camera}}\n```\n\nEach resource returned by the many `Stage::new_*` functions return resources that adhere\nto the [builder pattern][builder]. That means the value a `Stage::new_*` function returns\ncan be chained with other calls that configure it. This pattern is nice because it\nallows your editor to display the customizations available, which makes API discovery\neasier for everyone.\n\nAbove we use [`Camera::with_default_perspective`] to\nset the camera to use a default perspective projection.\n\nNote that usually when we create a `Camera`, we have to tell\nthe `Stage` that we want to **use** the camera, but the first `Camera` created will\nautomatically be used. We could potentially have _many_ cameras and switch them around at will\nby calling `Stage::use_camera` before rendering.\n\n### Geometry\n\nThe first step to creating a [`Primitive`] is staging some vertices in a triangle\nmesh. For this example we'll use the triangle mesh of the unit cube. The\n[`renderling::math`][math] module provides a convenience function for generating this mesh.\n\n```rust,ignore\n{{#include ../../crates/examples/src/stage.rs:unit_cube_vertices}}\n```\n\nHere we create [`Vertices`], which stages the unit cube points on the GPU.\n\nNext we'll unload those points from the CPU, to free up the memory:\n\n```rust,ignore\n{{#include ../../crates/examples/src/stage.rs:unload_vertices}}\n```\n\nUnloading the CPU memory like this isn't strictly necessary, but it's beneficial to\nknow about. If we were planning on inspecting or modifying the underlying\n[`Vertex`] values with [`Vertices::get_vertex`] and\n[`Vertices::modify_vertex`], we could skip this step.\nAfter unloading, however, we can still set a [`Vertex`] at a specific\nindex using [`Vertices::set_vertex`].\n\n### Material\n\nNext we stage a [`Material`].\n\nMaterials denote how a mesh looks by specifying various colors and shading values,\nas well as whether or not the material is lit by our lighting, which we'll talk about\nin later chapters. For now we'll provide a material that doesn't really do anything\nexcept let the vertex colors show through.\n\n```rust,ignore\n{{#include ../../crates/examples/src/stage.rs:material}}\n```\n\n### Primitive\n\nNow that we have some [`Vertices`] and a [`Material`] we can create our primitive\nusing the familiar builder pattern.\n\n```rust,ignore\n{{#include ../../crates/examples/src/stage.rs:prim}}\n```\n\nWe don't actually do anything with the primitive at this point though.\n\n## Rendering\n\nNow the scene is set and we're ready to render.\n\nRendering is a three-step process:\n\n1. Get the next frame\n2. Render the staged scene into the view of the frame\n3. Present the frame\n\n```rust,ignore\n{{#include ../../crates/examples/src/stage.rs:render}}\n```\n\nAbove we added an extra step where we read an image of the frame from the GPU,\nso we can see it here.\n\n## Results\n\n![image of a unit cube with colored vertices](assets/stage-example.png)\n\nAnd there you have it! We've rendered a nice cube.\n\n## Removing resources\n\nTo remove resources from the stage we can usually just `Drop` them from all\nscopes. There are a few types that require extra work to remove, though.\n\n[`Primitive`]s must be manually removed with [`Stage::remove_primitive`],\nwhich removes the primitive from all internal lists (like the list of draw calls).\n\nLights must also be removed from the stage for similar reasons.\n\nNow we'll run through removing the cube primitive, but first let's see how many\nbytes we've committed to the GPU through the stage:\n\n```rust,ignore\n{{#include ../../crates/examples/src/stage.rs:committed_size_bytes}}\n```\n\nAs of this writing, these lines print out `8296`, or roughly 8k bytes.\nThat may seem like a lot for one cube, but keep in mind that is a count of\nall bytes in all buffers, including any internal machinery.\n\nNow let's remove the cube primitive, drop the other resources, and render again:\n\n```rust,ignore\n{{#include ../../crates/examples/src/stage.rs:removal}}\n```\n\n![the cube is gone](assets/stage-example-gone.png)\n\n## Visibility\n\nIf instead we wanted to keep the resources around but make the [`Primitive`] invisible,\nwe could have used [`Primitive::set_visible`].\n\nSee the [`Stage`] and [`Primitive`] docs for more info.\n\n[`Stage`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html\n[`Stage::new_camera`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.new_camera\n[`Stage::remove_primitive`]: {{DOCS_URL}}/renderling/stage/struct.Stage.html#method.remove_primitive\n[`Context::new_stage`]: {{DOCS_URL}}/renderling/context/struct.Context.html#method.new_stage\n[`Primitive`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html\n[`Primitive::set_visible`]: {{DOCS_URL}}/renderling/primitive/struct.Primitive.html#method.set_visible\n[`Camera`]: {{DOCS_URL}}/renderling/camera/struct.Camera.html\n[`Camera::with_default_perspective`]: {{DOCS_URL}}/renderling/camera/struct.Camera.html#method.with_default_perspective\n[`Vertices`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html\n[`Vertices::get_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.get_vertex\n[`Vertices::modify_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.modify_vertex\n[`Vertices::set_vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertices.html#method.set_vertex\n[`Vertex`]: {{DOCS_URL}}/renderling/geometry/struct.Vertex.html\n[`Material`]: {{DOCS_URL}}/renderling/material/struct.Material.html\n\n[math]: {{DOCS_URL}}/renderling/math/index.html\n\n[Mat4]: https://docs.rs/glam/latest/glam/f32/struct.Mat4.html\n\n[glam]: https://crates.io/crates/glam\n\n[builder]: https://rust-unofficial.github.io/patterns/patterns/creational/builder.html\n"
  },
  {
    "path": "manual/src/welcome.md",
    "content": "<div style=\"float: right; padding: 1em;\">\n   <img\n      style=\"image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges;\"\n      alt=\"renderling mascot\" width=\"180\"\n      src=\"https://github.com/user-attachments/assets/83eafc47-287c-4b5b-8fd7-2063e56b2338\"\n   />\n</div>\n\n# Welcome\n\nWelcome to the `renderling` operator's manual!\n\n`renderling` is a cutting-edge, GPU-driven renderer designed to efficiently\nhandle complex scenes by leveraging GPU capabilities for most rendering\noperations. It is particularly suited for indie game developers and researchers\ninterested in high-performance graphics rendering while working with GLTF files\nand large-scale scenes.\n\nThe library is written in Rust and supports modern rendering techniques such as\nforward+ rendering and physically based shading, making it ideal for\napplications requiring advanced lighting and material effects.\n\n\nThis project is funded through [NGI Zero Core](https://nlnet.nl/core), a fund\nestablished by [NLnet](https://nlnet.nl) with financial support from the\nEuropean Commission's [Next Generation Internet](https://ngi.eu) program. \nLearn more at the [NLnet project page](https://nlnet.nl/project/Renderling).\n\n[<img src=\"https://nlnet.nl/logo/banner.png\" alt=\"NLnet foundation logo\" width=\"20%\" />](https://nlnet.nl) [<img src=\"https://nlnet.nl/image/logos/NGI0_tag.svg\" alt=\"NGI Zero Logo\" width=\"20%\" />](https://nlnet.nl/core)\n\n## Helpful Links\n\n* The official website is <https://renderling.xyz>.\n  Here you can read the latest news and implementation details.\n  You're also likely reading this on this site!\n\n* The documentation is <{{DOCS_URL}}/renderling/index.html>.\n\n* The GitHub repo and issue track is <https:://github.com/schell/renderling>.\n\n* The project site on NLnet is <https://nlnet.nl/project/Renderling/>\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "blank_lines_upper_bound = 2\ncombine_control_expr = false\nformat_code_in_doc_comments = true\nformat_strings = true\nmax_width = 100\nimports_granularity= \"crate\"\nwrap_comments = true"
  }
]