diff --git a/.agents/resume b/.agents/resume index cfd85f4..440c0ad 100755 --- a/.agents/resume +++ b/.agents/resume @@ -1,16 +1,4 @@ #!/usr/bin/env bash set -euo pipefail - -export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" - -for command in node npm cargo wasm-pack rsw; do - if ! command -v "$command" >/dev/null 2>&1; then - echo "Missing required command after orb resume: $command" >&2 - exit 1 - fi -done - -echo "Ensuring orb development services are running..." +command -v npm >/dev/null amp orb services ensure - -echo "Orb environment ready." diff --git a/.agents/setup b/.agents/setup index e6ddab9..9607cad 100755 --- a/.agents/setup +++ b/.agents/setup @@ -1,128 +1,4 @@ #!/usr/bin/env bash set -euo pipefail - -NODE_VERSION="22.23.1" -WASM_PACK_VERSION="0.15.0" -RSW_VERSION="0.8.0" - -export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" -mkdir -p "$HOME/.local/bin" "$HOME/.cargo/bin" - -missing_packages=() -for package in libvulkan1 mesa-vulkan-drivers xauth xvfb; do - if ! dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -Fq "install ok installed"; then - missing_packages+=("$package") - fi -done -if ((${#missing_packages[@]})); then - echo "Installing WebGPU runtime dependencies..." - sudo apt-get update - sudo apt-get install -y "${missing_packages[@]}" -fi - -install_node() { - local machine node_arch archive install_dir tmp_dir - machine="$(uname -m)" - case "$machine" in - x86_64) node_arch="x64" ;; - aarch64) node_arch="arm64" ;; - *) - echo "Unsupported architecture for Node.js: $machine" >&2 - return 1 - ;; - esac - - archive="node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" - install_dir="$HOME/.local/node-v${NODE_VERSION}-linux-${node_arch}" - if [[ ! -x "$install_dir/bin/node" ]]; then - echo "Installing Node.js v${NODE_VERSION}..." - tmp_dir="$(mktemp -d)" - trap 'rm -rf "$tmp_dir"' RETURN - curl --fail --location --silent --show-error \ - "https://nodejs.org/dist/v${NODE_VERSION}/${archive}" \ - --output "$tmp_dir/$archive" - curl --fail --location --silent --show-error \ - "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" \ - --output "$tmp_dir/SHASUMS256.txt" - ( - cd "$tmp_dir" - grep " ${archive}$" SHASUMS256.txt | sha256sum --check --strict - ) - tar -xJf "$tmp_dir/$archive" -C "$HOME/.local" - rm -rf "$tmp_dir" - trap - RETURN - fi - - for command in node npm npx corepack; do - ln -sfn "$install_dir/bin/$command" "$HOME/.local/bin/$command" - done -} - -install_wasm_pack() { - local machine target archive tmp_dir - machine="$(uname -m)" - case "$machine" in - x86_64) target="x86_64-unknown-linux-musl" ;; - aarch64) target="aarch64-unknown-linux-musl" ;; - *) - echo "Unsupported architecture for wasm-pack: $machine" >&2 - return 1 - ;; - esac - - if ! wasm-pack --version 2>/dev/null | grep -Fq "${WASM_PACK_VERSION}"; then - echo "Installing wasm-pack ${WASM_PACK_VERSION}..." - archive="wasm-pack-v${WASM_PACK_VERSION}-${target}.tar.gz" - tmp_dir="$(mktemp -d)" - trap 'rm -rf "$tmp_dir"' RETURN - curl --fail --location --silent --show-error \ - "https://github.com/wasm-bindgen/wasm-pack/releases/download/v${WASM_PACK_VERSION}/${archive}" \ - --output "$tmp_dir/$archive" - tar -xzf "$tmp_dir/$archive" -C "$tmp_dir" - install -m 0755 \ - "$tmp_dir/wasm-pack-v${WASM_PACK_VERSION}-${target}/wasm-pack" \ - "$HOME/.cargo/bin/wasm-pack" - rm -rf "$tmp_dir" - trap - RETURN - fi -} - -install_node - -if ! command -v rustup >/dev/null 2>&1; then - echo "Installing rustup..." - curl --proto '=https' --tlsv1.2 --fail --silent --show-error \ - https://sh.rustup.rs | sh -s -- -y --profile minimal --no-modify-path \ - --default-toolchain none -fi -source "$HOME/.cargo/env" - -toolchain="$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" -echo "Installing Rust toolchain ${toolchain}..." -rustup toolchain install "$toolchain" --profile minimal \ - --component rust-src \ - --component rustfmt \ - --target wasm32-unknown-unknown - -install_wasm_pack - -if ! rsw --version 2>/dev/null | grep -Fq "${RSW_VERSION}"; then - echo "Installing rsw ${RSW_VERSION}..." - cargo install rsw --version "$RSW_VERSION" --locked -fi - -# Amp shells include ~/.local/bin, while setup's PATH changes do not persist. -for command in cargo rustc rustdoc rustfmt rustup wasm-pack rsw; do - ln -sfn "$HOME/.cargo/bin/$command" "$HOME/.local/bin/$command" -done - -echo "Installing JavaScript dependencies..." npm ci - -echo "Building the application..." -npm run build - -echo "Starting orb development services..." amp orb services ensure - -echo "Orb setup complete." diff --git a/.amp/services.yaml b/.amp/services.yaml index 2b4b0b9..584c2b8 100644 --- a/.amp/services.yaml +++ b/.amp/services.yaml @@ -1,6 +1,6 @@ services: - yawn-examples: - command: npm run examples + yawn-docs: + command: npm start portal: - title: Yawn docs and playgrounds - description: VitePress package tutorials and isolated WebGPU playgrounds with hot reload. + title: Yawn + description: Documentation and minimal WebGPU playground. diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index a2e8e39..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,15 +0,0 @@ -[build] -target = "wasm32-unknown-unknown" -rustflags = [ - '-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals', - '-Clink-args=--shared-memory', - '-Clink-args=--max-memory=1073741824', - '-Clink-args=--import-memory', - '-Clink-args=--export=__wasm_init_tls', - '-Clink-args=--export=__tls_size', - '-Clink-args=--export=__tls_align', - '-Clink-args=--export=__tls_base', -] - -[unstable] -build-std = ['std', 'panic_abort'] diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 6871389..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -static/sponza.glb filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 887b670..ea8c5ab 100644 --- a/.gitignore +++ b/.gitignore @@ -23,30 +23,11 @@ dist-ssr *.sln *.sw? -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -# Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb - -/pkg -/wasm-pack.log .vscode/ .idea/ -# WASM build artifacts -.rsw/ -static/level-editor/ docs/.vitepress/cache/ +docs/.vitepress/dist/ # Amp runtime artifacts .amp/in/ diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 92e1083..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,1392 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "ash" -version = "0.38.0+1.3.281" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" -dependencies = [ - "libloading", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" -dependencies = [ - "serde", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytemuck" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "cfg-if" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "codespan-reporting" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.9.1", - "core-foundation", - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "document-features" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" -dependencies = [ - "litrs", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "flate2" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - -[[package]] -name = "glow" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" -dependencies = [ - "gl_generator", -] - -[[package]] -name = "gpu-alloc" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" -dependencies = [ - "bitflags 2.9.1", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" -dependencies = [ - "bitflags 2.9.1", -] - -[[package]] -name = "gpu-allocator" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" -dependencies = [ - "log", - "presser", - "thiserror 1.0.69", - "windows", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.9.1", - "gpu-descriptor-types", - "hashbrown", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" -dependencies = [ - "bitflags 2.9.1", -] - -[[package]] -name = "half" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" -dependencies = [ - "cfg-if", - "crunchy", - "num-traits", -] - -[[package]] -name = "hashbrown" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "image" -version = "0.25.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" -dependencies = [ - "bytemuck", - "byteorder-lite", - "num-traits", - "png", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "indexmap" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "js-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "khronos-egl" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - -[[package]] -name = "libc" -version = "0.2.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" - -[[package]] -name = "libloading" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" -dependencies = [ - "cfg-if", - "windows-targets 0.53.3", -] - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "litrs" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" - -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "metal" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" -dependencies = [ - "bitflags 2.9.1", - "block", - "core-graphics-types", - "foreign-types", - "log", - "objc", - "paste", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "naga" -version = "26.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916cbc7cb27db60be930a4e2da243cf4bc39569195f22fd8ee419cd31d5b662c" -dependencies = [ - "arrayvec", - "bit-set", - "bitflags 2.9.1", - "cfg-if", - "cfg_aliases", - "codespan-reporting", - "half", - "hashbrown", - "hexf-parse", - "indexmap", - "libm", - "log", - "num-traits", - "once_cell", - "rustc-hash", - "spirv", - "thiserror 2.0.15", - "unicode-ident", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "ordered-float" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2c1f9f56e534ac6a9b8a4600bdf0f530fb393b5f393e7b4d03489c3cf0c3f01" -dependencies = [ - "num-traits", -] - -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "presser" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "range-alloc" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" -dependencies = [ - "bitflags 2.9.1", -] - -[[package]] -name = "renderdoc-sys" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" - -[[package]] -name = "renderer" -version = "0.1.0" -dependencies = [ - "bytemuck", - "console_error_panic_hook", - "image", - "js-sys", - "log", - "serde", - "serde_json", - "thiserror 2.0.15", - "ultraviolet", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-logger", - "web-sys", - "wgpu", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustversion" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "safe_arch" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.142" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spirv" -version = "0.3.0+sdk-1.3.268.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" -dependencies = [ - "bitflags 2.9.1", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "syn" -version = "2.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" -dependencies = [ - "thiserror-impl 2.0.15", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ultraviolet" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea519dad475ee0446b8172793c3c327e4fc81dafdaf05aaac510e630000b6296" -dependencies = [ - "wide", -] - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-width" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasm-bindgen" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-logger" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" -dependencies = [ - "log", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wgpu" -version = "26.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70b6ff82bbf6e9206828e1a3178e851f8c20f1c9028e74dd3a8090741ccd5798" -dependencies = [ - "arrayvec", - "bitflags 2.9.1", - "cfg-if", - "cfg_aliases", - "document-features", - "hashbrown", - "js-sys", - "log", - "naga", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "26.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f62f1053bd28c2268f42916f31588f81f64796e2ff91b81293515017ca8bd9" -dependencies = [ - "arrayvec", - "bit-set", - "bit-vec", - "bitflags 2.9.1", - "cfg_aliases", - "document-features", - "hashbrown", - "indexmap", - "log", - "naga", - "once_cell", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "rustc-hash", - "smallvec", - "thiserror 2.0.15", - "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core-deps-apple" -version = "26.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18ae5fbde6a4cbebae38358aa73fcd6e0f15c6144b67ef5dc91ded0db125dbdf" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-emscripten" -version = "26.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7670e390f416006f746b4600fdd9136455e3627f5bd763abf9a65daa216dd2d" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-windows-linux-android" -version = "26.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720a5cb9d12b3d337c15ff0e24d3e97ed11490ff3f7506e7f3d98c68fa5d6f14" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-hal" -version = "26.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5981b1b884e1d0166cb02af818d0d5e0448b5754eb77bb5a28d8f8fb94c95f9f" -dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bit-set", - "bitflags 2.9.1", - "block", - "bytemuck", - "cfg-if", - "cfg_aliases", - "core-graphics-types", - "glow", - "glutin_wgl_sys", - "gpu-alloc", - "gpu-allocator", - "gpu-descriptor", - "hashbrown", - "js-sys", - "khronos-egl", - "libc", - "libloading", - "log", - "metal", - "naga", - "ndk-sys", - "objc", - "ordered-float", - "parking_lot", - "portable-atomic", - "portable-atomic-util", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "smallvec", - "thiserror 2.0.15", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "windows", - "windows-core", -] - -[[package]] -name = "wgpu-types" -version = "26.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca7a8d8af57c18f57d393601a1fb159ace8b2328f1b6b5f80893f7d672c9ae2" -dependencies = [ - "bitflags 2.9.1", - "bytemuck", - "js-sys", - "log", - "thiserror 2.0.15", - "web-sys", -] - -[[package]] -name = "wide" -version = "0.7.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" -dependencies = [ - "bytemuck", - "safe_arch", -] - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-strings", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "xml-rs" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-jpeg" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" -dependencies = [ - "zune-core", -] diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index c0027bb..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[workspace] -members = ["renderer"] -resolver = "2" - -[workspace.dependencies] -wasm-bindgen = { version = "0.2.100", features = ["enable-interning"] } -wasm-bindgen-futures = "0.4.50" -console_error_panic_hook = "0.1.7" -log = "0.4.27" -wasm-logger = "0.2.0" -web-sys = { version = "0.3.77", features = [ - "OffscreenCanvas", - "DedicatedWorkerGlobalScope", - "MessageEvent" -]} -js-sys = "0.3.77" -bytemuck = { version = "1.23.1", features = ["derive"] } -wgpu = "26.0.1" -thiserror = "2.0.15" -ultraviolet = "0.10.0" -image = { version = "0.25", default-features = false, features = ["png", "jpeg"] } - -[profile.release] -opt-level = "z" -lto = true diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 06e2325..0000000 --- a/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -FROM node:22-alpine - -# Install Rust nightly and required tools -RUN apk add --no-cache \ - build-base \ - curl \ - && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ - && source ~/.cargo/env \ - && rustup default nightly-2025-10-20 \ - && rustup toolchain install nightly-2025-10-20 \ - && rustup target add wasm32-unknown-unknown --toolchain nightly-2025-10-20 \ - && rustup component add rust-src --toolchain nightly-2025-10-20 \ - && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - -# Set environment variables for Rust -ENV PATH="/root/.cargo/bin:$PATH" -ENV RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals" - -# Set working directory -WORKDIR /app - -# Copy package files -COPY package.json yarn.lock ./ - -# Install Node.js dependencies -RUN yarn install - -# Copy source code -COPY . . - -# Build the project -RUN yarn run build - -# Expose port for serving -EXPOSE 8080 - -# Command to serve the built application -CMD ["yarn", "start", "--", "--host"] diff --git a/README.md b/README.md index 115cc3b..90f6432 100644 --- a/README.md +++ b/README.md @@ -1,152 +1,19 @@ # Yawn -Yawn is a Rust/WGPU renderer whose application boundary is worker messages plus -shared WebAssembly memory. Backward compatibility is intentionally deferred until -1.0. - -## Architecture +Yawn Core is two things: a generic structure-of-arrays arena in a `SharedArrayBuffer`, and a render-graph worker that turns externally supplied WGSL into an up-front WebGPU loadout. ```text -FXNode ───────────────┐ - ├─> canonical DAG AST ─> S-expression ─> Yawn render worker -JavaScript objects ──┘ │ - ├─> graph compiler - ├─> transient allocator - └─> prepared GPU loadout - -Any browser thread ── infrequent commands ──────────────────> worker -Any browser thread ── atomic SOA writes ────────────────────> shared WASM memory -glTF import worker ── parse URL ──> generic render-data packet ─> fixed shared SOA ─┘ +JSO or FXNode → AST → S-expression → graph worker → WebGPU + ↑ +any thread → direct shared row writes ┘ ``` -The canonical AST is the only public render-graph wire format. Nodes are named -definitions and `(ref "node" "socket")` forms are edges, so an output can fan out -without expanding into a tree. Core parses the S-expression, validates the DAG, -culls dead work, calculates resource lifetimes, aliases compatible non-overlapping -transients, coalesces render passes, and allocates the resulting textures and GPU -pipelines before activating a graph. +Messages allocate `{ name, rows, stride, format }` arrays and load graphs. Existing render data is changed by writing `f32`, `u32`, or `i32` rows directly. Allocations are 64-byte aligned, row strides are multiples of 16 bytes, and compatible non-overlapping transient textures share physical allocations. -Authored render shaders use Yawn's fixed scene ABI. Render and compute declarations -carry source, entry points, and dispatch/state metadata and are prepared with the -graph loadout. Core contains no built-in shader source or pipeline declarations. -Its public responsibility stops at shared render data and render-graph compilation, -loadouts, lifecycle, and transient resource management; conveniences live outside it. - -## Packages - -- `packages/yawn-core` (`@yawn/core`) — render-data shared arrays and render-graph - lifecycle transport; it returns `[slot, generation]` render-data handles. -- `addons/render-graph-ast` — canonical immutable DAG AST and S-expression serializer. -- `addons/render-graph-js` — plain-object/fluent graph APIs that serialize and load ASTs. -- `addons/render-graph-fxnode` — FXNode snapshot exporter and diagnostic mapping. -- `addons/default-pipelines` — optional scene/frame shader and compute declarations. -- `addons/gltf-import` — glTF worker that writes format-neutral render-data packets directly to a fixed SOA. -- `addons/mesh-handles` — conventional mesh, instance, camera, and material objects plus optional BVH picking. - -The editable playground and Render Graph Studio under `examples/` consume the -packages through their public APIs; no example source or shader lives in core. -Tutorial-style package guides and all focused recipes live under `docs/`, including -AST/JSO/FXNode authoring, render and compute programs, graph activation, shared glTF -import, mesh instances, custom SOA columns, SAB animation, picking, and worker use. - -Example graph authoring: - -```js -import { RenderGraph, ref } from "@yawn/render-graph-js"; -import { defaultPipelines } from "@yawn/default-pipelines"; - -const graph = new RenderGraph("main", 1) - .renderPipeline(defaultPipelines.render[1]) - .renderPipeline(defaultPipelines.render[2]) - .renderPipeline(defaultPipelines.render[3]) - .computePipeline({ - name: "prepare", - shader: "@compute @workgroup_size(1) fn main() {}", - entry: "main", - dispatch: [1, 1, 1], - }) - .node("mesh", "mesh", { version: 2 }) - .node("draw", "gltf_standard", { - version: 2, - inputs: { mesh: [ref("mesh", "mesh")] }, - }); - -// Add the required attachments and frame output, then let the addon own the wire encoding: -await graph.load(core); -``` - -## Shared render data - -`@yawn/core` exposes 64-byte-aligned shared SOA columns. Every stride is a multiple -of 16 bytes and scalar lanes are atomic `u32`, `i32`, or IEEE-754 `f32` bits. The -built-in instance transform/type columns are generation-guarded so a stale handle -cannot mutate a reused slot. The built-in `camera.state` column is one 64-byte, -16-lane `f32` row containing eye, target, up, and projection parameters. - -Allocate application columns infrequently through the worker: - -```js -const velocity = await core.allocateArray({ - name: "instance.velocity", - domain: "instance", // also "mesh" or "fixed" - scalar: "f32", - lanes: 4, -}); - -velocity.write(instanceSlot, [1, 0, 0, 0]); -``` - -Camera state has no dedicated core API. Read and write it through the same render-data -SOA interface as every other hot value; these mutations do not enqueue worker messages: - -```js -const camera = core.array("camera.state"); -const state = camera.read(0); -state[0] = nextEye[0]; -state[1] = nextEye[1]; -state[2] = nextEye[2]; -camera.write(0, state); -``` - -Import a GLB without transferring its bytes through renderer messages: - -```js -import { GltfImporter } from "@yawn/gltf-import"; -import { CameraHandle, MaterialHandles, MeshHandles } from "@yawn/mesh-handles"; - -const importer = new GltfImporter(core); -const imported = await importer.load(gltfUrl); -const meshes = new MeshHandles(core).fromImportedScene(imported); -const materials = new MaterialHandles(core).fromImportedScene(imported); -const camera = new CameraHandle(core); -meshes[0].defaultInstance.setTransform(nextTransform); // direct shared-SOA write -materials[0].roughness = 0.35; // direct shared-SOA write -camera.position = [4, 3, 6]; // direct shared-SOA write -``` - -The renderer grows mesh/instance-domain columns with render-data capacity and -publishes replacement descriptors through the core's `yawn-soa-layout` event. -Typed-array views refresh when shared WASM memory grows. Messages are reserved for -allocation and lifecycle operations. Existing instance values and bulk asset uploads -use shared memory; a GLB commit message contains only an array ID and byte count. - -`YawnCore` accepts a transport bridge whose worker endpoint can be a `Worker` or a -started `MessagePort`, so the same API can run on the browser main thread or another -worker. Optional snapshot/BVH picking is owned entirely by the mesh-handles addon. - -Cross-origin isolation is required (`COOP: same-origin`, `COEP: require-corp`). The -Vite development and preview servers already set both headers. - -## Development +WGSL, pipelines, glTF import, and conventional mesh/camera/material handles live in `addons/`; core contains no shader or scene model. ```sh -npm run examples -npm run test:js -cargo check --workspace +npm start ``` -Production build: - -```sh -npm run build-release -``` +This opens the docs. The complete runnable example is at `/playground`. diff --git a/addons/default-pipelines/src/index.js b/addons/default-pipelines/src/index.js index 5f22ce2..5dc0039 100644 --- a/addons/default-pipelines/src/index.js +++ b/addons/default-pipelines/src/index.js @@ -1,86 +1,50 @@ -export const gltfShader = /* wgsl */ ` -struct UniformData { resolution: vec2, time: f32, _padding0: f32, camera_position: vec4 } -struct MaterialData { base_color_factor: vec4, emissive_factor: vec4, surface_factors: vec4, alpha_optics: vec4, flags: vec4, uv_sets: vec4, debug_extras: vec4 } -@group(0) @binding(0) var uni: UniformData; -@group(1) @binding(0) var view_proj: mat4x4; -@group(2) @binding(0) var material: MaterialData; -@group(2) @binding(1) var base_tex: texture_2d; -@group(2) @binding(2) var mr_tex: texture_2d; -@group(2) @binding(3) var normal_tex: texture_2d; -@group(2) @binding(4) var occlusion_tex: texture_2d; -@group(2) @binding(5) var emissive_tex: texture_2d; -@group(2) @binding(6) var base_sampler: sampler; -@group(2) @binding(7) var mr_sampler: sampler; -@group(2) @binding(8) var normal_sampler: sampler; -@group(2) @binding(9) var occlusion_sampler: sampler; -@group(2) @binding(10) var emissive_sampler: sampler; -struct VertexInput { @location(0) pos: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(3) model_col0: vec4, @location(4) model_col1: vec4, @location(5) model_col2: vec4, @location(6) model_col3: vec4, @location(7) normal_col0: vec4, @location(8) normal_col1: vec4, @location(9) normal_col2: vec4, @location(10) tangent: vec4 } -struct VertexOutput { @builtin(position) clip_position: vec4, @location(0) world_pos: vec3, @location(1) normal: vec3, @location(2) tangent: vec3, @location(3) bitangent: vec3, @location(4) uv: vec2, @location(5) @interpolate(flat) determinant_sign: f32 } -fn safe_normalize(v: vec3, fallback: vec3) -> vec3 { let l2 = dot(v, v); return select(fallback, v * inverseSqrt(l2), l2 > 1e-12 && l2 < 1e30); } -@vertex fn vs_main(in: VertexInput) -> VertexOutput { - var out: VertexOutput; let model = mat4x4(in.model_col0, in.model_col1, in.model_col2, in.model_col3); - let linear = mat3x3(in.model_col0.xyz, in.model_col1.xyz, in.model_col2.xyz); let nm = mat3x3(in.normal_col0.xyz, in.normal_col1.xyz, in.normal_col2.xyz); - let world = model * vec4(in.pos, 1.0); let n = safe_normalize(nm * in.normal, vec3(0,1,0)); let raw_t = linear * in.tangent.xyz; - var t = raw_t - n * dot(n, raw_t); if dot(t,t) < 1e-8 { t = cross(select(vec3(0,1,0), vec3(1,0,0), abs(n.x) < 0.9), n); } t = safe_normalize(t, vec3(1,0,0)); - out.clip_position = view_proj * world; out.world_pos = world.xyz; out.normal = n; out.tangent = t; out.bitangent = safe_normalize(cross(n,t), vec3(0,0,1)) * in.tangent.w * in.normal_col0.w; out.uv = in.uv; out.determinant_sign = in.normal_col0.w; return out; -} -struct Closure { base: vec4, mr: vec2, normal_map: vec3, ao: f32, emissive: vec3 } -fn sample_closure(uv: vec2) -> Closure { - let bits = material.flags.x; var c: Closure; - c.base = material.base_color_factor * select(vec4(1), textureSample(base_tex, base_sampler, uv), (bits & 1u) != 0u); - let mr = select(vec4(1), textureSample(mr_tex, mr_sampler, uv), (bits & 2u) != 0u); c.mr = vec2(clamp(material.surface_factors.x * mr.b,0,1), clamp(material.surface_factors.y * mr.g,0.045,1)); - c.normal_map = select(vec3(0.5,0.5,1), textureSample(normal_tex, normal_sampler, uv).xyz, (bits & 4u) != 0u); - let occ = select(1.0, textureSample(occlusion_tex, occlusion_sampler, uv).r, (bits & 8u) != 0u); c.ao = mix(1.0, occ, material.surface_factors.w); - c.emissive = material.emissive_factor.rgb * select(vec3(1), textureSample(emissive_tex, emissive_sampler, uv).rgb, (bits & 16u) != 0u); return c; -} -fn schlick(f0: vec3, v_h: f32) -> vec3 { return f0 + (vec3(1)-f0) * pow(1.0-clamp(v_h,0,1),5.0); } -fn ggx_d(n_h_input: f32, a: f32) -> f32 { let n_h=clamp(n_h_input,0.0,1.0); let a2=a*a; let nh2=n_h*n_h; let q=(1.0-nh2)+a2*nh2; return a2/(3.14159265*q*q); } -fn smith_v(n_v: f32, n_l: f32, a: f32) -> f32 { let a2=a*a; let gv=n_l*sqrt(max(n_v*n_v*(1.0-a2)+a2,0)); let gl=n_v*sqrt(max(n_l*n_l*(1.0-a2)+a2,0)); return 0.5/max(gv+gl,1e-6); } -@fragment fn fs_main(in: VertexOutput, @builtin(front_facing) front: bool) -> @location(0) vec4 { - let c=sample_closure(in.uv); if material.alpha_optics.x == 1.0 && c.base.a < material.alpha_optics.y { discard; } - let physical_front=front == (in.determinant_sign > 0); let orientation=select(-1.0,1.0,physical_front || material.flags.y == 0u); let map=c.normal_map*2.0-1.0; - let n=safe_normalize(mat3x3(in.tangent,in.bitangent,in.normal)*safe_normalize(vec3(map.xy*material.surface_factors.z,map.z),vec3(0,0,1)),in.normal)*orientation; - let v=safe_normalize(uni.camera_position.xyz-in.world_pos,n); let l=safe_normalize(vec3(0.35,1,0.45),vec3(0,1,0)); let h=safe_normalize(v+l,n); - let nv=max(dot(n,v),0); let nl=max(dot(n,l),0); let nh=dot(n,h); let vh=max(dot(v,h),0); let a=c.mr.y*c.mr.y; - let f0=mix(vec3(material.alpha_optics.w),c.base.rgb,c.mr.x); let direct_f=schlick(f0,vh); let env_f=schlick(f0,nv); let spec=direct_f*ggx_d(nh,a)*smith_v(nv,nl,a); let diffuse=(vec3(1)-direct_f)*(1.0-c.mr.x)*c.base.rgb/3.14159265; - let sun=(diffuse+spec)*nl*vec3(3.0,2.85,2.65); let up=clamp(n.y*0.5+0.5,0,1); let sky=mix(vec3(0.055,0.045,0.035),vec3(0.24,0.36,0.58),up); - let reflection=reflect(-v,n); let horizon=clamp(reflection.y*0.5+0.5,0,1); let env_spec=env_f*mix(vec3(0.04,0.035,0.03),vec3(0.28,0.42,0.7),horizon)*(1.0-0.65*c.mr.y); - let color=sun+(vec3(1)-env_f)*(1.0-c.mr.x)*c.base.rgb*sky*c.ao+env_spec*c.ao+c.emissive; return vec4(color,1.0); -}`; +const triangleShader = /* wgsl */ ` +struct Tint { color: vec4 } +@group(0) @binding(0) var tint: Tint; -export const groundShader = /* wgsl */ ` -@group(0) @binding(0) var application: array, 3>; -@group(1) @binding(0) var view_proj: mat4x4; -struct Input { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(3) c0: vec4, @location(4) c1: vec4, @location(5) c2: vec4, @location(6) c3: vec4 } -struct Output { @builtin(position) position: vec4, @location(0) normal: vec3 } -@vertex fn vs_main(input: Input) -> Output { var output: Output; output.position=view_proj*mat4x4(input.c0,input.c1,input.c2,input.c3)*vec4(input.position,1); output.normal=input.normal; return output; } -@fragment fn fs_main(input: Output) -> @location(0) vec4 { return vec4(vec3(0.12)+max(input.normal.y,0.0)*vec3(0.16),1); } +struct Vertex { @builtin(position) position: vec4 } + +@vertex fn vertex(@builtin(vertex_index) index: u32) -> Vertex { + let positions = array(vec2(-0.75, -0.65), vec2(0.75, -0.65), vec2(0.0, 0.75)); + var output: Vertex; + output.position = vec4(positions[index], 0.0, 1.0); + return output; +} + +@fragment fn fragment() -> @location(0) vec4 { return tint.color; } `; -export const frameShader = /* wgsl */ ` -@group(0) @binding(0) var source_texture: texture_2d; -@group(0) @binding(1) var second_texture: texture_2d; -@group(0) @binding(2) var linear_clamp: sampler; -struct Parameters { values: array, 8> } -@group(0) @binding(3) var parameters: Parameters; -struct VertexOut { @builtin(position) position: vec4, @location(0) uv: vec2 } -@vertex fn vs_main(@builtin(vertex_index) index: u32) -> VertexOut { let positions=array(vec2(-1.0,-1.0),vec2(3.0,-1.0),vec2(-1.0,3.0)); let p=positions[index]; var out:VertexOut; out.position=vec4(p,0,1); out.uv=p*vec2(0.5,-0.5)+vec2(0.5); return out; } -fn aces(x:vec3)->vec3{return clamp((x*(2.51*x+vec3(0.03)))/(x*(2.43*x+vec3(0.59))+vec3(0.14)),vec3(0),vec3(1));} -fn linear_to_srgb(x:vec3)->vec3{let safe=clamp(x,vec3(0),vec3(1));return select(1.055*pow(safe,vec3(1.0/2.4))-vec3(0.055),safe*12.92,safe<=vec3(0.0031308));} -@fragment fn fs_frame_out(in:VertexOut)->@location(0) vec4{let sampled=textureSampleLevel(source_texture,linear_clamp,in.uv,0);var rgb=max(sampled.rgb*exp2(parameters.values[0].z),vec3(0));if parameters.values[0].x>0.5{if parameters.values[0].y>1.5{rgb=aces(rgb);}else if parameters.values[0].y>0.5{rgb=rgb/(vec3(1)+rgb);}}if parameters.values[0].w>0.5{rgb=linear_to_srgb(rgb);}return vec4(clamp(rgb,vec3(0),vec3(1)),clamp(sampled.a,0,1));} +const noopComputeShader = /* wgsl */ ` +@compute @workgroup_size(1) fn main() {} `; -export const noopComputeShader = /* wgsl */ `@compute @workgroup_size(1) fn main() {}`; - -/** Optional declarations copied into each graph that wants these implementations. */ -export const defaultPipelines = Object.freeze({ - render: Object.freeze([ - Object.freeze({ name: "ground_plane", shader: groundShader, vertexEntry: "vs_main", fragmentEntry: "fs_main" }), - Object.freeze({ name: "gltf_standard", shader: gltfShader, vertexEntry: "vs_main", fragmentEntry: "fs_main", material: true }), - Object.freeze({ name: "gltf_standard_double_sided", shader: gltfShader, vertexEntry: "vs_main", fragmentEntry: "fs_main", doubleSided: true, material: true }), - Object.freeze({ name: "frame_out", shader: frameShader, vertexEntry: "vs_main", fragmentEntry: "fs_frame_out" }), - ]), - compute: Object.freeze([ - Object.freeze({ name: "initialize_scene", shader: noopComputeShader, entry: "main", dispatch: [1, 1, 1] }), - ]), -}); +/** A complete external graph used by the minimal playground; core contains neither program. */ +export function triangleGraph(colorArray = "triangle.color") { + return { + id: "triangle", + resources: { + buffers: [{ id: "color", array: colorArray, usage: ["uniform"] }], + }, + pipelines: { + compute: [{ id: "prepare", code: noopComputeShader }], + render: [{ + id: "triangle", + code: triangleShader, + vertex: { entry: "vertex" }, + fragment: { entry: "fragment", targets: [{ format: "canvas" }] }, + }], + }, + passes: [ + { id: "prepare", type: "compute", pipeline: "prepare", dispatch: [1, 1, 1] }, + { + id: "draw", + type: "render", + pipeline: "triangle", + after: ["prepare"], + bindings: [{ group: 0, binding: 0, resource: "color" }], + color: [{ resource: "canvas", clear: [0.025, 0.035, 0.055, 1] }], + draw: { vertices: 3 }, + }, + ], + }; +} diff --git a/addons/gltf-import/src/index.js b/addons/gltf-import/src/index.js index f53a657..cfffa7c 100644 --- a/addons/gltf-import/src/index.js +++ b/addons/gltf-import/src/index.js @@ -1,75 +1,29 @@ -export class GltfImportError extends Error { - constructor(code) { - super(code); - this.name = "GltfImportError"; - this.code = code; - } -} - -function frameCamera(core, bounds, framing) { - if (!bounds || framing === false) return; - if (framing !== undefined && framing !== "exterior" && framing !== "interior") - throw new TypeError("framing must be exterior, interior, or false"); - const min = bounds.min, max = bounds.max; - if (!Array.isArray(min) || !Array.isArray(max) || min.length !== 3 || max.length !== 3) return; - const center = min.map((value, axis) => (value + max[axis]) * 0.5); - const extent = max.map((value, axis) => value - min[axis]); - const radius = Math.max(1, Math.hypot(...extent) * 0.5); - const camera = core.array("camera.state"); - const state = camera.read(0); - const interior = framing === "interior"; - const eye = interior - ? [center[0], center[1] + radius * 0.05, center[2]] - : [center[0] + radius * 1.8, center[1] + radius * 1.4, center[2] + radius * 1.8]; - const target = interior ? [center[0] + radius, center[1], center[2]] : center; - state.splice(0, 3, ...eye); - state.splice(4, 3, ...target); - state.splice(8, 3, 0, 1, 0); - state[14] = Math.max(radius * 0.001, 0.1); - state[15] = Math.max(radius * 6, 1.1); - camera.write(0, state); -} - -/** Parses glTF in a dedicated worker and publishes a generic render-data packet through shared memory. */ +/** Fetches and parses glTF in a worker, then lets that worker write the packet into Yawn's SAB arena. */ export class GltfImporter { #core; #worker; #next = 1; #pending = new Map(); - #tail = Promise.resolve(); - #disposed = false; constructor(core, { workerFactory } = {}) { - if (!core?.allocateArray || !core?.commitRenderDataUpload) - throw new TypeError("core must implement the Yawn shared render-data protocol"); + if (!core?.allocateRows) throw new TypeError("core must be a YawnCore instance"); this.#core = core; - this.#worker = workerFactory - ? workerFactory() - : new Worker(new URL("./worker.js", import.meta.url), { - type: "module", - name: "yawn-gltf-import", - }); - this.#worker.addEventListener("message", event => this.#message(event.data)); + this.#worker = workerFactory?.() ?? new Worker(new URL("./worker.js", import.meta.url), { + type: "module", + name: "yawn-gltf-import", + }); + this.#worker.addEventListener("message", ({ data }) => this.#message(data)); this.#worker.addEventListener("error", () => this.#fail("GLTF_WORKER_ERROR")); - this.#worker.addEventListener("messageerror", () => this.#fail("GLTF_WORKER_ERROR")); this.#worker.start?.(); } - load(url, options = {}) { - if (this.#disposed) return Promise.reject(new GltfImportError("DISPOSED")); + load(url) { const source = url instanceof URL ? url.href : url; - if (typeof source !== "string" || !source) return Promise.reject(new TypeError("url must be a URL or nonempty string")); - const operation = this.#tail.then(() => this.#start(source, options)); - this.#tail = operation.catch(() => {}); - return operation; - } - - #start(url, options) { - let request = this.#next++ >>> 0; - if (!request) request = this.#next++ >>> 0; + if (typeof source !== "string" || !source) throw new TypeError("url is required"); + const request = this.#next++; return new Promise((resolve, reject) => { - this.#pending.set(request, { resolve, reject, options, array: null }); - this.#worker.postMessage({ type: "load", request, url }); + this.#pending.set(request, { resolve, reject }); + this.#worker.postMessage({ type: "load", request, url: source }); }); } @@ -78,31 +32,17 @@ export class GltfImporter { if (!pending) return; try { if (message.type === "allocate") { - const length = Math.ceil(message.byteLength / 16); - pending.array = await this.#core.allocateArray({ - name: "upload.renderData", - domain: "fixed", - scalar: "u32", - lanes: 4, + pending.array = await this.#core.allocateRows({ + name: `gltf.${message.request}`, + rows: Math.ceil(message.byteLength / 16), stride: 16, - length, + format: "u32", }); - this.#worker.postMessage({ - type: "storage", - request: message.request, - ...pending.array.share(), - }); - } else if (message.type === "ready") { - const result = await this.#core.commitRenderDataUpload( - pending.array, - message.byteLength, - ); - frameCamera(this.#core, result.bounds, pending.options.framing); + this.#worker.postMessage({ type: "storage", request: message.request, ...pending.array.share() }); + } else { this.#pending.delete(message.request); - pending.resolve(result); - } else if (message.type === "error") { - this.#pending.delete(message.request); - pending.reject(new GltfImportError(message.code || "GLTF_IMPORT_FAILED")); + if (message.type === "ready") pending.resolve({ array: pending.array, byteLength: message.byteLength }); + else pending.reject(new Error(message.error ?? "GLTF_IMPORT_FAILED")); } } catch (error) { this.#pending.delete(message.request); @@ -111,16 +51,12 @@ export class GltfImporter { } #fail(code) { - if (this.#disposed) return; - this.#disposed = true; - const error = new GltfImportError(code); - for (const pending of this.#pending.values()) pending.reject(error); + for (const { reject } of this.#pending.values()) reject(new Error(code)); this.#pending.clear(); - this.#worker.terminate?.(); } dispose() { - if (this.#disposed) return; this.#fail("DISPOSED"); + this.#worker.terminate(); } } diff --git a/addons/gltf-import/src/shared-upload.js b/addons/gltf-import/src/shared-upload.js deleted file mode 100644 index 646270b..0000000 --- a/addons/gltf-import/src/shared-upload.js +++ /dev/null @@ -1,43 +0,0 @@ -const MAGIC = 0x414f5359; - -/** Publishes one byte payload into a packed fixed SOA allocation. */ -export function writeSharedUpload(buffer, descriptor, bytes) { - if (!(buffer instanceof SharedArrayBuffer) || !(bytes instanceof Uint8Array)) - throw new TypeError("shared upload requires SharedArrayBuffer storage and Uint8Array bytes"); - if ( - !descriptor || - descriptor.domain !== "fixed" || - descriptor.scalar !== "u32" || - descriptor.stride !== descriptor.lanes * 4 || - descriptor.controlPtr % 64 !== 0 || - descriptor.dataOffset !== 64 || - bytes.byteLength < 1 || - bytes.byteLength > descriptor.length * descriptor.lanes * 4 - ) throw new TypeError("invalid packed fixed SOA upload"); - - const control = new Int32Array(buffer, descriptor.controlPtr, 16); - if ( - (Atomics.load(control, 0) >>> 0) !== MAGIC || - (Atomics.load(control, 2) >>> 0) !== descriptor.id - ) throw new Error("SOA_PROTOCOL_MISMATCH"); - - let sequence; - for (let attempt = 0; attempt < 1024; attempt++) { - const candidate = Atomics.load(control, 9) >>> 0; - if (!(candidate & 1) && (Atomics.compareExchange(control, 9, candidate | 0, (candidate + 1) | 0) >>> 0) === candidate) { - sequence = candidate; - break; - } - } - if (sequence === undefined) throw new Error("SOA_BUSY"); - try { - new Uint8Array( - buffer, - descriptor.controlPtr + descriptor.dataOffset, - bytes.byteLength, - ).set(bytes); - } finally { - Atomics.store(control, 9, (sequence + 2) | 0); - Atomics.notify(control, 9); - } -} diff --git a/addons/gltf-import/src/worker.js b/addons/gltf-import/src/worker.js index 5929054..6c8f781 100644 --- a/addons/gltf-import/src/worker.js +++ b/addons/gltf-import/src/worker.js @@ -1,4 +1,3 @@ -import { writeSharedUpload } from "./shared-upload.js"; import { gltfToRenderDataPacket } from "./gltf.js"; const downloads = new Map(); @@ -19,12 +18,16 @@ addEventListener("message", async ({ data: message }) => { if (message?.type === "storage") { const packet = downloads.get(request); if (!packet) throw new Error("GLTF_REQUEST_UNKNOWN"); - writeSharedUpload(message.buffer, message.descriptor, packet); + const { buffer, descriptor } = message; + if (!(buffer instanceof SharedArrayBuffer) || descriptor?.format !== "u32" || + descriptor.stride !== 16 || descriptor.offset % 64 || packet.byteLength > descriptor.rows * descriptor.stride) + throw new Error("GLTF_STORAGE_INVALID"); + new Uint8Array(buffer, descriptor.offset, packet.byteLength).set(packet); downloads.delete(request); postMessage({ type: "ready", request, byteLength: packet.byteLength }); } } catch (error) { downloads.delete(request); - postMessage({ type: "error", request, code: error?.message || "GLTF_IMPORT_FAILED" }); + postMessage({ type: "error", request, error: error?.message || "GLTF_IMPORT_FAILED" }); } }); diff --git a/addons/mesh-handles/package.json b/addons/mesh-handles/package.json index 3035a98..e7c25d3 100644 --- a/addons/mesh-handles/package.json +++ b/addons/mesh-handles/package.json @@ -3,8 +3,5 @@ "version": "0.1.0", "description": "Conventional mesh, instance, camera, and material handles over Yawn render data", "type": "module", - "exports": "./src/index.js", - "dependencies": { - "@yawn/core": "0.1.0" - } + "exports": "./src/index.js" } diff --git a/addons/mesh-handles/src/bvh-core.js b/addons/mesh-handles/src/bvh-core.js deleted file mode 100644 index 66c4056..0000000 --- a/addons/mesh-handles/src/bvh-core.js +++ /dev/null @@ -1,26 +0,0 @@ -/** Worker-local acceleration structure used by the optional mesh-handle addon. */ -export class DerivedBvh { - constructor() { this.count = 0; this.identity = new Uint32Array(); this.meshIdentity = new Uint32Array(); this.bounds = new Float32Array(); this.nodeBounds = new Float32Array(); this.left = this.right = new Int32Array(); this.leafStart = this.leafCount = new Uint32Array(); this.leaves = new Uint32Array(); this.root = -1; this.rebuilds = 0; this.refits = 0; } - update(snapshot) { - const s = snapshot.streams, n = snapshot.instanceCount; - let changed = n !== this.count; - if (!changed) for (let i = 0; i < n; i++) if (this.identity[i * 2] !== s.instanceSlot[i] || this.identity[i * 2 + 1] !== s.instanceGeneration[i] || this.meshIdentity[i * 2] !== s.instanceMeshSlot[i] || this.meshIdentity[i * 2 + 1] !== s.instanceMeshGeneration[i]) { changed = true; break; } - this.count = n; this.identity = new Uint32Array(n * 2); this.meshIdentity = new Uint32Array(n * 2); this.bounds = new Float32Array(n * 6); - for (let i = 0; i < n; i++) { this.identity.set([s.instanceSlot[i], s.instanceGeneration[i]], i * 2); this.meshIdentity.set([s.instanceMeshSlot[i], s.instanceMeshGeneration[i]], i * 2); this.bounds.set(s.instanceWorldMin.subarray(i * 3, i * 3 + 3), i * 6); this.bounds.set(s.instanceWorldMax.subarray(i * 3, i * 3 + 3), i * 6 + 3); } - changed ? this.rebuild() : this.refit(); - } - rebuild() { - this.rebuilds++; const nodes = [], leaves = []; - const build = indices => { const at = nodes.length, node = {left: -1, right: -1, start: 0, count: 0, bounds: [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]}; nodes.push(node); for (const i of indices) for (let a = 0; a < 3; a++) { node.bounds[a] = Math.min(node.bounds[a], this.bounds[i * 6 + a]); node.bounds[a + 3] = Math.max(node.bounds[a + 3], this.bounds[i * 6 + a + 3]); } if (indices.length <= 2) { node.start = leaves.length; node.count = indices.length; leaves.push(...indices); return at; } let axis = 0, extent = node.bounds[3] - node.bounds[0]; for (let a = 1; a < 3; a++) if (node.bounds[a + 3] - node.bounds[a] > extent) { axis = a; extent = node.bounds[a + 3] - node.bounds[a]; } indices.sort((a, b) => (this.bounds[a * 6 + axis] + this.bounds[a * 6 + axis + 3]) - (this.bounds[b * 6 + axis] + this.bounds[b * 6 + axis + 3]) || a - b); const mid = indices.length >> 1; node.left = build(indices.slice(0, mid)); node.right = build(indices.slice(mid)); return at; }; - this.root = this.count ? build(Array.from({length: this.count}, (_, i) => i)) : -1; const n = nodes.length; - this.nodeBounds = new Float32Array(n * 6); this.left = new Int32Array(n); this.right = new Int32Array(n); this.leafStart = new Uint32Array(n); this.leafCount = new Uint32Array(n); this.leaves = Uint32Array.from(leaves); - nodes.forEach((x, i) => { this.nodeBounds.set(x.bounds, i * 6); this.left[i] = x.left; this.right[i] = x.right; this.leafStart[i] = x.start; this.leafCount[i] = x.count; }); - } - refit() { this.refits++; for (let n = this.left.length - 1; n >= 0; n--) { const at = n * 6; for (let a = 0; a < 3; a++) { let lo = Infinity, hi = -Infinity; if (this.leafCount[n]) for (let j = 0; j < this.leafCount[n]; j++) { const i = this.leaves[this.leafStart[n] + j]; lo = Math.min(lo, this.bounds[i * 6 + a]); hi = Math.max(hi, this.bounds[i * 6 + a + 3]); } else { lo = Math.min(this.nodeBounds[this.left[n] * 6 + a], this.nodeBounds[this.right[n] * 6 + a]); hi = Math.max(this.nodeBounds[this.left[n] * 6 + a + 3], this.nodeBounds[this.right[n] * 6 + a + 3]); } this.nodeBounds[at + a] = lo; this.nodeBounds[at + a + 3] = hi; } } } - pick(origin, direction, maxDistance = Infinity, maxHits = 1) { - if (this.root < 0) return []; let magnitude = Math.hypot(...direction); const dir = direction.map(v => v / magnitude); - const intersect = (array, at) => { let lo = 0, hi = maxDistance; for (let a = 0; a < 3; a++) { const min = array[at + a], max = array[at + a + 3]; if (dir[a] === 0) { if (origin[a] < min || origin[a] > max) return Infinity; } else { let x = (min - origin[a]) / dir[a], y = (max - origin[a]) / dir[a]; if (x > y) [x, y] = [y, x]; lo = Math.max(lo, x); hi = Math.min(hi, y); if (lo > hi) return Infinity; } } return lo; }; - const hits = [], stack = [this.root]; while (stack.length) { const n = stack.pop(); if (intersect(this.nodeBounds, n * 6) === Infinity) continue; if (this.leafCount[n]) for (let j = 0; j < this.leafCount[n]; j++) { const i = this.leaves[this.leafStart[n] + j]; const distance = intersect(this.bounds, i * 6); if (distance !== Infinity) hits.push({slot: this.identity[i * 2], generation: this.identity[i * 2 + 1], distance}); } else stack.push(this.right[n], this.left[n]); } - hits.sort((a, b) => a.distance - b.distance || a.slot - b.slot || a.generation - b.generation); return hits.slice(0, maxHits); - } -} diff --git a/addons/mesh-handles/src/bvh-worker.js b/addons/mesh-handles/src/bvh-worker.js deleted file mode 100644 index af0bd61..0000000 --- a/addons/mesh-handles/src/bvh-worker.js +++ /dev/null @@ -1,27 +0,0 @@ -import { SnapshotReader } from "./snapshot.js"; -import { DerivedBvh } from "./bvh-core.js"; - -let reader, bvh = new DerivedBvh(), epoch = 0, updating = false, requestedEpoch = 0; -function ensureEpoch(expected) { - if (!reader) return false; - const latest = reader.latest(); - if (latest.epoch !== expected) return false; - if (epoch === expected) return true; - const result = reader.transaction(snapshot => { bvh.update(snapshot); epoch = snapshot.epoch; }, expected); - return result !== null && epoch === expected; -} -function coalescedUpdate(hint = 0) { - requestedEpoch = Math.max(requestedEpoch, hint >>> 0); - if (updating) return; - updating = true; - queueMicrotask(() => { try { const latest = reader?.latest(); if (latest?.epoch && latest.epoch !== epoch) ensureEpoch(latest.epoch); if (epoch) postMessage({type: "updated", epoch}); } catch (error) { postMessage({type: "fatal", code: "PICK_PROTOCOL_MISMATCH", message: String(error)}); } finally { updating = false; if (requestedEpoch > epoch) coalescedUpdate(); } }); -} -addEventListener("message", event => { - const m = event.data; - try { - if (m.type === "init") { reader = new SnapshotReader(m.memory, m.controlPtr); if (m.controlVersion !== 1 || m.schemaVersion !== 2) throw Object.assign(new Error("version"), {code: "PICK_PROTOCOL_MISMATCH"}); postMessage({type: "ready"}); } - else if (m.type === "update") coalescedUpdate(m.epoch); - else if (m.type === "pick") { if (!ensureEpoch(m.epoch)) { postMessage({type: "pick", request: m.request, stale: true, epoch}); return; } const hits = bvh.pick(m.origin, m.direction, m.maxDistance, m.maxHits); const latest = reader.latest().epoch; postMessage({type: "pick", request: m.request, stale: latest !== m.epoch || epoch !== m.epoch, epoch, hits}); } - else if (m.type === "dispose") close(); - } catch (error) { postMessage({type: "fatal", code: error.name === "SnapshotProtocolError" || error.code === "PICK_PROTOCOL_MISMATCH" ? "PICK_PROTOCOL_MISMATCH" : "PICK_WORKER_ERROR", message: String(error)}); } -}); diff --git a/addons/mesh-handles/src/index.js b/addons/mesh-handles/src/index.js index 1e4e9f0..c287bec 100644 --- a/addons/mesh-handles/src/index.js +++ b/addons/mesh-handles/src/index.js @@ -1,362 +1,55 @@ -import { RendererError } from "@yawn/core"; -import { SnapshotReader } from "./snapshot.js"; +const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; -const TOKEN = Symbol("yawn mesh handle addon"); -const SNAPSHOT_EVENT = "yawn-render-data-snapshot"; -const PUBLISHED_EVENT = "yawn-render-data-snapshot-published"; -const createPickingWorker = () => new Worker( - new URL("./bvh-worker.js", import.meta.url), - { type: "module", name: "yawn-spatial-query" }, -); - -/** Conventional mesh/instance objects layered entirely over the Yawn core protocol. */ -export class MeshHandles { - #core; - #factory; #worker; #reader; #snapshot; #epoch = 0; #next = 1; #picks = new Map(); #disposed = false; - #onSnapshot; #onPublished; - - constructor(core, { pickingWorkerFactory = createPickingWorker } = {}) { - this.#core = core; - this.#factory = pickingWorkerFactory; - this.#onSnapshot = event => this.#installSnapshot(event.detail); - this.#onPublished = event => this.#publish(event.detail?.epoch); - core.addEventListener?.(SNAPSHOT_EVENT, this.#onSnapshot); - core.addEventListener?.(PUBLISHED_EVENT, this.#onPublished); - if (core.renderDataSnapshot) this.#installSnapshot(core.renderDataSnapshot); +class Handle { + constructor(array, row = 0) { + this.array = array; + this.row = row; } - - fromImportedScene(result) { - if (!result || !Array.isArray(result.meshes)) throw new TypeError("invalid imported scene"); - return result.meshes.map((mesh) => new Mesh(TOKEN, this.#core, mesh)); - } - - async pickRay(origin, direction, options) { - const result = await this.#pickRay(origin, direction, options); - return { - ...result, - hits: result.hits.map((hit) => ({ - ...hit, - instance: new Instance(TOKEN, this.#core, hit.instance), - })), - }; - } - - #installSnapshot(snapshot) { - try { - if (snapshot?.controlVersion !== 1 || snapshot?.schemaVersion !== 2) throw new Error("version"); - this.#snapshot = snapshot; - this.#reader = new SnapshotReader(snapshot.memory, snapshot.controlPtr); - this.#epoch = this.#reader.latest().epoch; - this.#worker?.postMessage({ type: "init", ...snapshot }); - } catch { - this.#disable("PICK_PROTOCOL_MISMATCH"); - } - } - - #publish(epoch) { - if (this.#disposed || !this.#reader) return; - try { - this.#epoch = this.#reader.latest().epoch; - this.#worker?.postMessage({ type: "update", epoch: this.#epoch || (epoch >>> 0) }); - } catch { - this.#disable("PICK_PROTOCOL_MISMATCH"); - } - } - - #ensureWorker() { - if (this.#worker) return true; - if (!this.#reader || !this.#factory) return false; - try { - this.#worker = this.#factory(); - this.#worker.addEventListener("message", event => this.#workerMessage(event.data)); - this.#worker.addEventListener("error", () => this.#disable("PICK_WORKER_ERROR")); - this.#worker.addEventListener("messageerror", () => this.#disable("PICK_WORKER_ERROR")); - this.#worker.start?.(); - this.#worker.postMessage({ type: "init", ...this.#snapshot }); - if (this.#epoch) this.#worker.postMessage({ type: "update", epoch: this.#epoch }); - return true; - } catch { - this.#disable("PICK_WORKER_ERROR"); - return false; - } - } - - #disable(code) { - const error = new RendererError(code); - for (const pending of this.#picks.values()) pending.reject(error); - this.#picks.clear(); - try { this.#worker?.terminate?.(); } catch { /* best effort */ } - this.#worker = null; - } - - #workerMessage(message) { - if (message?.type === "fatal") { this.#disable(message.code || "PICK_WORKER_ERROR"); return; } - if (message?.type !== "pick") return; - const pending = this.#picks.get(message.request); - if (!pending) return; - this.#picks.delete(message.request); - let latest; - try { latest = this.#reader.latest().epoch; this.#epoch = latest; } - catch { pending.reject(new RendererError("PICK_PROTOCOL_MISMATCH")); this.#disable("PICK_PROTOCOL_MISMATCH"); return; } - if (message.stale || pending.epoch !== message.epoch || message.epoch !== latest) { - if (!pending.retried && latest) this.#sendPick({ ...pending, retried: true }, latest); - else pending.reject(new RendererError("PICK_STALE")); - return; - } - pending.resolve({ - epoch: latest, - hits: (message.hits || []).map(hit => ({ - instance: [hit.slot >>> 0, hit.generation >>> 0], - distance: hit.distance, - })), - }); - } - - #sendPick(pending, epoch) { - const request = this.#next++ >>> 0 || this.#next++; - pending.epoch = epoch; - this.#picks.set(request, pending); - try { - this.#worker.postMessage({ - type: "pick", request, epoch, - origin: pending.origin, direction: pending.direction, - maxDistance: pending.maxDistance, maxHits: pending.maxHits, - }); - } catch { - this.#picks.delete(request); - pending.reject(new RendererError("PICK_WORKER_ERROR")); - } - } - - #pickRay(origin, direction, { maxDistance = Infinity, maxHits = 1 } = {}) { - const vector = (value, name) => { - if (!value || value.length !== 3 || [...value].some(x => typeof x !== "number" || !Number.isFinite(x))) - throw new TypeError(`${name} must contain 3 finite numbers`); - return [...value]; - }; - origin = vector(origin, "origin"); - direction = vector(direction, "direction"); - if (direction.every(x => x === 0)) throw new TypeError("direction must be nonzero"); - if (typeof maxDistance !== "number" || (!(Number.isFinite(maxDistance) && maxDistance >= 0) && maxDistance !== Infinity) || !Number.isInteger(maxHits) || maxHits < 1 || maxHits > 64) - throw new TypeError("invalid pick options"); - if (this.#disposed) return Promise.reject(new RendererError("DISPOSED")); - if (!this.#ensureWorker()) return Promise.reject(new RendererError("PICK_UNAVAILABLE")); - let epoch; - try { epoch = this.#reader.latest().epoch; this.#epoch = epoch; } - catch { return Promise.reject(new RendererError("PICK_PROTOCOL_MISMATCH")); } - if (!epoch) return Promise.reject(new RendererError("PICK_STALE")); - return new Promise((resolve, reject) => this.#sendPick({ - resolve, reject, origin, direction, maxDistance, maxHits, retried: false, - }, epoch)); - } - - dispose() { - if (this.#disposed) return; - this.#disposed = true; - this.#core.removeEventListener?.(SNAPSHOT_EVENT, this.#onSnapshot); - this.#core.removeEventListener?.(PUBLISHED_EVENT, this.#onPublished); - try { this.#worker?.postMessage?.({ type: "dispose" }); } catch { /* best effort */ } - this.#disable("DISPOSED"); - } -} - -export class Mesh { - #core; #handle; #defaultInstance; #defaultType; - - constructor(token, core, descriptor) { - if (token !== TOKEN) throw new TypeError("Mesh cannot be constructed directly"); - this.#core = core; - this.#handle = Object.freeze([...descriptor.handle]); - this.#defaultInstance = new Instance(TOKEN, core, descriptor.defaultInstance); - this.#defaultType = Object.freeze([...descriptor.defaultType]); - } - - get handle() { return this.#handle; } - get defaultInstance() { return this.#defaultInstance; } - - async createInstance(transform, { type = this.#defaultType } = {}) { - return new Instance(TOKEN, this.#core, await this.#core.createInstance(this.#handle, transform, { type })); - } -} - -export class Instance { - #core; #handle; #dead = false; - - constructor(token, core, handle) { - if (token !== TOKEN) throw new TypeError("Instance cannot be constructed directly"); - this.#core = core; - this.#handle = Object.freeze([...handle]); - } - - get handle() { return this.#handle; } - #live() { if (this.#dead) throw new Error("STALE_HANDLE"); } - setType(words) { this.#live(); this.#core.setInstanceType(this.#handle, words); } - setTransform(transform) { this.#live(); this.#core.setInstanceTransform(this.#handle, transform); } - async destroy() { this.#live(); await this.#core.destroyInstance(this.#handle); this.#dead = true; } -} - -function requireArray(core, name, { domain, scalar, lanes }) { - if (!core?.array) throw new TypeError("core must implement the Yawn shared render-data protocol"); - const array = core.array(name); - if (array.domain !== domain || array.scalar !== scalar || array.lanes !== lanes) - throw new RendererError("SOA_PROTOCOL_MISMATCH"); - return array; -} - -function vector(value, length, name) { - if (!value || value.length !== length || [...value].some(item => typeof item !== "number" || !Number.isFinite(item))) - throw new TypeError(`${name} must contain ${length} finite numbers`); - return Array.from(value); -} - -function finite(value, name) { - if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${name} must be finite`); - return value; -} - -/** Conventional camera properties backed by the canonical SIMD-width camera SOA row. */ -export class CameraHandle { - #array; - - constructor(core) { - this.#array = requireArray(core, "camera.state", { domain: "fixed", scalar: "f32", lanes: 16 }); - } - - get state() { return this.#array.read(0); } - set state(value) { this.#write(vector(value, 16, "state")); } - get position() { return this.state.slice(0, 3); } - set position(value) { this.update({ position: value }); } - get target() { return this.state.slice(4, 7); } - set target(value) { this.update({ target: value }); } - get up() { return this.state.slice(8, 11); } - set up(value) { this.update({ up: value }); } - get fovY() { return this.state[12]; } - set fovY(value) { this.update({ fovY: value }); } - get aspect() { return this.state[13]; } - set aspect(value) { this.update({ aspect: value }); } - get near() { return this.state[14]; } - set near(value) { this.update({ near: value }); } - get far() { return this.state[15]; } - set far(value) { this.update({ far: value }); } - - update(properties = {}) { - if (!properties || typeof properties !== "object") throw new TypeError("camera properties must be an object"); - const known = new Set(["position", "target", "up", "fovY", "aspect", "near", "far"]); - for (const key of Object.keys(properties)) if (!known.has(key)) throw new TypeError(`unknown camera property '${key}'`); + get state() { return this.array.read(this.row); } + set state(value) { this.array.write(this.row, value); } + patch(offset, values) { const state = this.state; - if (properties.position !== undefined) state.splice(0, 3, ...vector(properties.position, 3, "position")); - if (properties.target !== undefined) state.splice(4, 3, ...vector(properties.target, 3, "target")); - if (properties.up !== undefined) state.splice(8, 3, ...vector(properties.up, 3, "up")); - if (properties.fovY !== undefined) state[12] = finite(properties.fovY, "fovY"); - if (properties.aspect !== undefined) state[13] = finite(properties.aspect, "aspect"); - if (properties.near !== undefined) state[14] = finite(properties.near, "near"); - if (properties.far !== undefined) state[15] = finite(properties.far, "far"); - this.#write(state); - return this; - } - - lookAt(position, target, { up = this.up } = {}) { - return this.update({ position, target, up }); - } - - #write(state) { - const offset = state.slice(0, 3).map((value, axis) => state[4 + axis] - value); - const up = state.slice(8, 11); - const cross = [ - offset[1] * up[2] - offset[2] * up[1], - offset[2] * up[0] - offset[0] * up[2], - offset[0] * up[1] - offset[1] * up[0], - ]; - if (Math.hypot(...offset) < 0.1 || Math.hypot(...up) === 0 || Math.hypot(...cross) === 0) - throw new RangeError("camera position, target, and up do not define a view"); - if (!(state[12] > 0 && state[12] < Math.PI) || state[13] <= 0 || state[14] <= 0 || state[15] <= state[14]) - throw new RangeError("camera projection is invalid"); - state[3] = state[7] = 1; - state[11] = 0; - this.#array.write(0, state); + state.splice(offset, values.length, ...values); + this.state = state; } } -const FLOAT_WORD = new ArrayBuffer(4); -const FLOAT_VIEW = new Float32Array(FLOAT_WORD); -const WORD_VIEW = new Uint32Array(FLOAT_WORD); -function wordToFloat(word) { WORD_VIEW[0] = word; return FLOAT_VIEW[0]; } -function floatToWord(value) { FLOAT_VIEW[0] = value; return WORD_VIEW[0]; } - -/** Creates scene-scoped material objects over packed material SOA rows. */ -export class MaterialHandles { - #array; - - constructor(core) { - this.#array = requireArray(core, "material.state", { domain: "fixed", scalar: "u32", lanes: 28 }); - } - - fromImportedScene(result) { - if (!result || !Array.isArray(result.materials)) throw new TypeError("invalid imported scene"); - return result.materials.map(material => this.get(material?.key)); - } - - get(key) { - if (!Number.isInteger(key) || key < 0 || key > 0xffffffff || key >= this.#array.length) - throw new RangeError("material key is outside the shared material rows"); - return new MaterialHandle(TOKEN, this.#array, key); +/** Conventional camera values over one caller-owned shared row. */ +export class CameraHandle extends Handle { + static async create(core, name = "camera") { + const array = await core.allocateRows({ name, rows: 1, stride: 64, format: "f32" }); + const camera = new CameraHandle(array); + camera.state = [0, 0, 4, 0, 0, 0, 0, 0, 0, 1, 0, 0, Math.PI / 3, 1, 0.1, 1000]; + return camera; } + get position() { return this.state.slice(0, 3); } + set position(value) { this.patch(0, value); } + get target() { return this.state.slice(4, 7); } + set target(value) { this.patch(4, value); } } -export class MaterialHandle { - #array; #key; - - constructor(token, array, key) { - if (token !== TOKEN) throw new TypeError("MaterialHandle cannot be constructed directly"); - this.#array = array; - this.#key = key; +/** Conventional material properties over one eight-float shared row. */ +export class MaterialHandle extends Handle { + static async create(core, name = "material") { + const material = new MaterialHandle(await core.allocateRows({ name, rows: 1, stride: 32, format: "f32" })); + material.state = [1, 1, 1, 1, 0, 1, 0, 0]; + return material; } + get baseColor() { return this.state.slice(0, 4); } + set baseColor(value) { this.patch(0, value); } + get metallic() { return this.state[4]; } + set metallic(value) { this.patch(4, [value]); } + get roughness() { return this.state[5]; } + set roughness(value) { this.patch(5, [value]); } +} - get key() { return this.#key; } - get baseColor() { return this.#floats(0, 4); } - set baseColor(value) { this.update({ baseColor: value }); } - get emissive() { return this.#floats(4, 3); } - set emissive(value) { this.update({ emissive: value }); } - get metallic() { return this.#float(8); } - set metallic(value) { this.update({ metallic: value }); } - get roughness() { return this.#float(9); } - set roughness(value) { this.update({ roughness: value }); } - get normalScale() { return this.#float(10); } - set normalScale(value) { this.update({ normalScale: value }); } - get occlusionStrength() { return this.#float(11); } - set occlusionStrength(value) { this.update({ occlusionStrength: value }); } - get alphaCutoff() { return this.#float(13); } - set alphaCutoff(value) { this.update({ alphaCutoff: value }); } - get ior() { return this.#float(14); } - set ior(value) { this.update({ ior: value }); } - - update(properties = {}) { - if (!properties || typeof properties !== "object") throw new TypeError("material properties must be an object"); - const known = new Set(["baseColor", "emissive", "metallic", "roughness", "normalScale", "occlusionStrength", "alphaCutoff", "ior"]); - for (const key of Object.keys(properties)) if (!known.has(key)) throw new TypeError(`unknown material property '${key}'`); - const words = this.#array.read(this.#key); - const setFloat = (lane, value, name) => { words[lane] = floatToWord(finite(value, name)); }; - if (properties.baseColor !== undefined) - vector(properties.baseColor, 4, "baseColor").forEach((value, lane) => setFloat(lane, value, "baseColor")); - if (properties.emissive !== undefined) - vector(properties.emissive, 3, "emissive").forEach((value, lane) => setFloat(4 + lane, value, "emissive")); - for (const [name, lane] of [["metallic", 8], ["roughness", 9], ["normalScale", 10], ["occlusionStrength", 11], ["alphaCutoff", 13]]) { - if (properties[name] !== undefined) setFloat(lane, properties[name], name); - } - if (properties.metallic !== undefined && !(properties.metallic >= 0 && properties.metallic <= 1)) throw new RangeError("metallic must be in [0, 1]"); - if (properties.roughness !== undefined && !(properties.roughness >= 0 && properties.roughness <= 1)) throw new RangeError("roughness must be in [0, 1]"); - if (properties.occlusionStrength !== undefined && !(properties.occlusionStrength >= 0 && properties.occlusionStrength <= 1)) throw new RangeError("occlusionStrength must be in [0, 1]"); - if (properties.alphaCutoff !== undefined && !(properties.alphaCutoff >= 0 && properties.alphaCutoff <= 1)) throw new RangeError("alphaCutoff must be in [0, 1]"); - if (properties.ior !== undefined) { - const ior = finite(properties.ior, "ior"); - if (ior !== 0 && ior < 1) throw new RangeError("ior must be 0 or at least 1"); - setFloat(14, ior, "ior"); - setFloat(15, ior === 0 ? 1 : ((ior - 1) / (ior + 1)) ** 2, "ior"); - } - this.#array.write(this.#key, words); - return this; +/** Conventional mesh transform over one SIMD-aligned shared row. */ +export class MeshHandle extends Handle { + static async create(core, name = "mesh") { + const mesh = new MeshHandle(await core.allocateRows({ name, rows: 1, stride: 64, format: "f32" })); + mesh.transform = identity; + return mesh; } - - #float(lane) { return wordToFloat(this.#array.read(this.#key)[lane]); } - #floats(start, length) { return this.#array.read(this.#key).slice(start, start + length).map(wordToFloat); } + get transform() { return this.state; } + set transform(value) { this.state = value; } } diff --git a/addons/mesh-handles/src/snapshot.js b/addons/mesh-handles/src/snapshot.js deleted file mode 100644 index 6787eb2..0000000 --- a/addons/mesh-handles/src/snapshot.js +++ /dev/null @@ -1,96 +0,0 @@ -/** Shared render-data snapshot protocol consumed by the mesh-handle BVH worker. */ -export const SNAPSHOT = Object.freeze({ - MAGIC: 0x504e5359, BLOB_MAGIC: 0x32534452, VERSION: 1, BYTES: 256, - SLOTS: 3, SLOT_BYTES: 64, SCHEMA: 2, INIT: 0, OPEN: 1, FAILED: 2, - CLOSED: 3, FREE: 0, WRITING: 1, READY: 2, READING: 3, -}); -export const STREAM_NAMES = ["meshSlot", "meshGeneration", "meshLocalMin", "meshLocalMax", "instanceSlot", "instanceGeneration", "instanceMeshSlot", "instanceMeshGeneration", "instanceModel", "instanceWorldMin", "instanceWorldMax", "instanceType"]; -const COMPONENTS = [1, 1, 3, 3, 1, 1, 1, 1, 16, 3, 3, 16]; -const SCALARS = [1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1]; -const STRIDES = COMPONENTS.map(n => n * 4); - -export class SnapshotProtocolError extends Error { - constructor(code) { super(code); this.code = code; this.name = "SnapshotProtocolError"; } -} - -const bad = code => { throw new SnapshotProtocolError(code); }; -const add = (a, b) => { const n = a + b; if (!Number.isSafeInteger(n) || n > 0xffffffff) bad("BAD_RANGE"); return n; }; -const mul = (a, b) => { const n = a * b; if (!Number.isSafeInteger(n) || n > 0xffffffff) bad("BAD_RANGE"); return n; }; - -export class SnapshotReader { - constructor(memory, controlPtr) { - if (!memory || !(memory.buffer instanceof SharedArrayBuffer)) bad("BAD_MEMORY"); - if (!Number.isInteger(controlPtr) || controlPtr < 0 || controlPtr % 64 || add(controlPtr, 256) > memory.buffer.byteLength) bad("BAD_CONTROL_POINTER"); - this.memory = memory; this.controlPtr = controlPtr; this.buffer = null; - this.refresh(); this.validateControl(); - } - refresh() { - if (this.buffer === this.memory.buffer) return; - this.buffer = this.memory.buffer; - if (add(this.controlPtr, 256) > this.buffer.byteLength) bad("BAD_CONTROL_POINTER"); - this.control = new Int32Array(this.buffer, this.controlPtr, 64); - } - validateControl() { - const h = this.control; - if ((Atomics.load(h, 0) >>> 0) !== SNAPSHOT.MAGIC) bad("BAD_MAGIC"); - if ((Atomics.load(h, 1) >>> 0) !== SNAPSHOT.VERSION) bad("BAD_VERSION"); - if ((Atomics.load(h, 2) >>> 0) !== SNAPSHOT.BYTES || (Atomics.load(h, 3) >>> 0) !== SNAPSHOT.SLOTS || (Atomics.load(h, 4) >>> 0) !== SNAPSHOT.SLOT_BYTES || (Atomics.load(h, 5) >>> 0) !== SNAPSHOT.SCHEMA) bad("BAD_LAYOUT"); - const lifecycle = Atomics.load(h, 6) >>> 0; - if (lifecycle > SNAPSHOT.CLOSED) bad("BAD_LIFECYCLE"); - if ((Atomics.load(h, 15) >>> 0) !== 0) bad("BAD_RESERVED"); - } - latest() { - this.refresh(); this.validateControl(); - for (let tries = 0; tries < 16; tries++) { - const a = Atomics.load(this.control, 7) >>> 0; - if (a & 1) continue; - const lifecycle = Atomics.load(this.control, 6) >>> 0; - const value = { lifecycle, epoch: Atomics.load(this.control, 8) >>> 0, slot: Atomics.load(this.control, 9) >>> 0, revisionLo: Atomics.load(this.control, 10) >>> 0, revisionHi: Atomics.load(this.control, 11) >>> 0, wasmPages: Atomics.load(this.control, 12) >>> 0, layoutEpoch: Atomics.load(this.control, 13) >>> 0, error: Atomics.load(this.control, 14) >>> 0 }; - const b = Atomics.load(this.control, 7) >>> 0; - if (a === b && !(b & 1)) { - if (lifecycle === SNAPSHOT.FAILED) bad("SNAPSHOT_FAILED"); - if (lifecycle === SNAPSHOT.CLOSED) bad("SNAPSHOT_CLOSED"); - if (lifecycle !== SNAPSHOT.INIT && lifecycle !== SNAPSHOT.OPEN) bad("BAD_LIFECYCLE"); - if (value.wasmPages && value.wasmPages > this.buffer.byteLength / 65536) bad("BAD_WASM_PAGES"); - return value; - } - } - bad("UNSTABLE_CONTROL"); - } - transaction(fn, expectedEpoch = 0) { - this.refresh(); - const latest = this.latest(); - if (!latest.epoch || latest.slot >= SNAPSHOT.SLOTS || (expectedEpoch && latest.epoch !== expectedEpoch)) return null; - let control = this.control; - const base = 16 + latest.slot * 16; - if (Atomics.compareExchange(control, base, SNAPSHOT.READY, SNAPSHOT.READING) !== SNAPSHOT.READY) return null; - try { - // memory.grow replaces memory.buffer even after the slot has been pinned. - this.refresh(); control = this.control; - const slot = Array.from({length: 16}, (_, i) => Atomics.load(control, base + i) >>> 0); - if (slot[0] !== SNAPSHOT.READING || slot[1] !== latest.epoch || slot[2] !== latest.layoutEpoch || slot[5] !== latest.revisionLo || slot[6] !== latest.revisionHi || slot[9] !== SNAPSHOT.SCHEMA || slot[10] !== 64) return null; - if (slot.slice(11).some(Boolean)) bad("BAD_SLOT_RESERVED"); - const ptr = slot[3], bytes = slot[4]; - if (ptr % 16 || bytes < 448 || bytes % 16 || add(ptr, bytes) > this.buffer.byteLength) bad("BAD_SLOT"); - const u32 = new Uint32Array(this.buffer, ptr, bytes / 4); - if (u32[0] !== SNAPSHOT.BLOB_MAGIC || u32[1] !== SNAPSHOT.SCHEMA || u32[2] !== 64 || u32[3] !== bytes || u32[4] !== slot[1] || u32[5] !== slot[5] || u32[6] !== slot[6] || u32[7] !== 12 || u32[8] !== 64 || u32[9] !== 32 || u32[10] !== slot[7] || u32[11] !== slot[8] || u32[12] !== 0x01020304 || u32[13] !== 3) bad("BAD_BLOB"); - if (u32[14] || u32[15]) bad("BAD_BLOB_RESERVED"); - const ranges = [], streams = {}; - for (let i = 0; i < 12; i++) { - const d = 16 + i * 8, semantic = u32[d], scalar = u32[d + 1], offset = u32[d + 2], count = u32[d + 3], components = u32[d + 4], stride = u32[d + 5], width = u32[d + 6], reserved = u32[d + 7]; - const want = i < 4 ? slot[7] : slot[8]; - if (semantic !== i + 1 || scalar !== SCALARS[i] || count !== want || components !== COMPONENTS[i] || stride !== STRIDES[i] || width !== 4 || reserved || offset < 448 || offset % 16) bad("BAD_DESCRIPTOR"); - const end = add(offset, mul(stride, count)); - if (end > bytes) bad("BAD_DESCRIPTOR_RANGE"); - if (count) ranges.push([offset, end]); - const Type = scalar === 2 ? Float32Array : Uint32Array; - streams[STREAM_NAMES[i]] = new Type(this.buffer, add(ptr, offset), mul(count, components)); - } - ranges.sort((a, b) => a[0] - b[0]); - for (let i = 1; i < ranges.length; i++) if (ranges[i][0] < ranges[i - 1][1]) bad("OVERLAPPING_STREAMS"); - return fn(Object.freeze({epoch: slot[1], revisionLo: slot[5], revisionHi: slot[6], meshCount: slot[7], instanceCount: slot[8], streams: Object.freeze(streams)})); - } finally { - Atomics.store(control, base, SNAPSHOT.FREE); Atomics.notify(control, base); - } - } -} diff --git a/addons/render-graph-ast/src/index.js b/addons/render-graph-ast/src/index.js index 71baec7..5fc6f90 100644 --- a/addons/render-graph-ast/src/index.js +++ b/addons/render-graph-ast/src/index.js @@ -1,191 +1,34 @@ -/** Canonical DAG AST shared by every Yawn render-graph frontend. */ -const AST_KIND = "yawn-render-graph"; -const AST_VERSION = 1; -const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_.-]*$/; +export const GRAPH_AST_VERSION = 1; -export class GraphAstError extends TypeError { - constructor(code, message = code) { - super(message); - this.name = "GraphAstError"; - this.code = code; - } -} - -const fail = (code, message) => { - throw new GraphAstError(code, message); -}; -const object = (value) => - value !== null && typeof value === "object" && !Array.isArray(value); -const identifier = (value) => - typeof value === "string" && - IDENTIFIER.test(value) && - new TextEncoder().encode(value).length <= 64; -const finiteData = (value) => - value === null || - typeof value === "string" || - typeof value === "boolean" || +const data = value => value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value)) || - (Array.isArray(value) && value.every(finiteData)) || - (object(value) && Object.values(value).every(finiteData)); -const clone = (value) => structuredClone(value); -const freeze = (value) => { - if (value && typeof value === "object" && !Object.isFrozen(value)) { - Object.freeze(value); + (Array.isArray(value) && value.every(data)) || + (value?.constructor === Object && Object.values(value).every(data)); + +function freeze(value) { + if (value && typeof value === "object") { Object.values(value).forEach(freeze); + Object.freeze(value); } return value; -}; -const u32 = (value, name) => { - if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) - fail("AST_U32", `${name} must be a uint32`); - return value; -}; - -function normalizePipelines(raw = {}) { - if (!object(raw)) fail("AST_PIPELINES", "pipelines must be an object"); - const render = (raw.render ?? []).map((pipeline) => { - if (!object(pipeline)) fail("AST_PIPELINE", "render pipeline must be an object"); - const result = { - name: pipeline.name, - shader: pipeline.shader, - vertexEntry: pipeline.vertexEntry ?? "vs_main", - fragmentEntry: pipeline.fragmentEntry ?? "fs_main", - doubleSided: pipeline.doubleSided ?? false, - material: pipeline.material ?? false, - }; - if ( - !identifier(result.name) || - !identifier(result.vertexEntry) || - !identifier(result.fragmentEntry) || - typeof result.shader !== "string" || - typeof result.doubleSided !== "boolean" || - typeof result.material !== "boolean" - ) - fail("AST_PIPELINE", "invalid render pipeline declaration"); - return result; - }); - const compute = (raw.compute ?? []).map((pipeline) => { - if (!object(pipeline)) fail("AST_PIPELINE", "compute pipeline must be an object"); - const result = { - name: pipeline.name, - shader: pipeline.shader, - entry: pipeline.entry ?? "main", - dispatch: Array.from(pipeline.dispatch ?? []), - }; - if ( - !identifier(result.name) || - !identifier(result.entry) || - typeof result.shader !== "string" || - result.dispatch.length !== 3 || - result.dispatch.some((value) => u32(value, "dispatch") === 0) - ) - fail("AST_PIPELINE", "invalid compute pipeline declaration"); - return result; - }); - const names = new Set(); - for (const pipeline of [...render, ...compute]) { - if (names.has(pipeline.name)) - fail("AST_PIPELINE_DUPLICATE", `duplicate pipeline '${pipeline.name}'`); - names.add(pipeline.name); - } - return { render, compute }; } -function normalizeNode(raw) { - if (!object(raw) || !identifier(raw.id) || !object(raw.executor)) - fail("AST_NODE", "invalid node"); - if (raw.state !== "enabled" && raw.state !== "muted") - fail("AST_NODE", "node state must be enabled or muted"); - if (!identifier(raw.executor.key)) fail("AST_NODE", "invalid executor key"); - const parameters = clone(raw.parameters ?? {}); - if (!finiteData(parameters)) fail("AST_DATA", "parameters must be finite data"); - if (!object(raw.inputs ?? {})) fail("AST_NODE", "inputs must be an object"); - const inputs = {}; - for (const name of Object.keys(raw.inputs ?? {}).sort()) { - if (!identifier(name) || !Array.isArray(raw.inputs[name])) - fail("AST_INPUT", "invalid node input"); - inputs[name] = raw.inputs[name].map((reference) => { - if (!object(reference) || !identifier(reference.node) || !identifier(reference.socket)) - fail("AST_REFERENCE", "invalid DAG reference"); - return { node: reference.node, socket: reference.socket }; - }); - } - return { - id: raw.id, - state: raw.state, - executor: { key: raw.executor.key, version: u32(raw.executor.version, "executor version") }, - parameters, - inputs, - }; +/** Creates the data-only AST consumed by every graph frontend. DAG edges are pass `after` IDs. */ +export function createGraphAst(graph) { + if (!data(graph) || graph?.constructor !== Object || typeof graph.id !== "string" || + !Array.isArray(graph.passes)) throw new TypeError("GRAPH_AST"); + return freeze(structuredClone(graph)); } -/** Creates the canonical in-memory graph AST shared by all authoring frontends. */ -export function createGraphAst({ kind, version, id, revision, pipelines = {}, nodes }) { - if (kind !== undefined && kind !== AST_KIND) fail("AST_KIND", "invalid AST kind"); - if (version !== undefined && version !== AST_VERSION) fail("AST_VERSION", "unsupported AST version"); - if (!identifier(id)) fail("AST_ID", "invalid graph id"); - u32(revision, "revision"); - if (revision === 0) fail("AST_REVISION", "revision must be nonzero"); - if (!Array.isArray(nodes)) fail("AST_NODES", "nodes must be an array"); - const normalized = nodes.map(normalizeNode); - const ids = new Set(); - for (const node of normalized) { - if (ids.has(node.id)) fail("AST_NODE_DUPLICATE", `duplicate node '${node.id}'`); - ids.add(node.id); - } - return freeze({ - kind: AST_KIND, - version: AST_VERSION, - id, - revision, - pipelines: normalizePipelines(pipelines), - nodes: normalized, - }); -} - -export const reference = (node, socket) => { - if (!identifier(node) || !identifier(socket)) fail("AST_REFERENCE", "invalid DAG reference"); - return Object.freeze({ node, socket }); -}; - -function data(value) { - if (value === null) return "null"; - if (typeof value === "boolean") return String(value); - if (typeof value === "number") { - if (!Number.isFinite(value)) fail("AST_DATA", "numbers must be finite"); - return JSON.stringify(value); - } +function encode(value) { + if (value === null || typeof value === "boolean" || typeof value === "number") return String(value); if (typeof value === "string") return JSON.stringify(value); - if (Array.isArray(value)) return `(array${value.map((item) => ` ${data(item)}`).join("")})`; - if (object(value)) - return `(object${Object.keys(value) - .sort() - .map((key) => ` (field ${JSON.stringify(key)} ${data(value[key])})`) - .join("")})`; - fail("AST_DATA", "unsupported AST data value"); + if (Array.isArray(value)) return `(array${value.map(item => ` ${encode(item)}`).join("")})`; + return `(object${Object.keys(value).sort().map(key => + ` (field ${JSON.stringify(key)} ${encode(value[key])})`).join("")})`; } -/** Serializes a graph AST to the only graph wire format accepted by Yawn core. */ -export function serializeGraphAst(raw) { - const graph = createGraphAst(raw); - const nodes = graph.nodes - .map((node) => { - const inputs = Object.entries(node.inputs) - .map( - ([name, references]) => - `\n (input ${JSON.stringify(name)}${references - .map( - (reference) => - ` (ref ${JSON.stringify(reference.node)} ${JSON.stringify(reference.socket)})`, - ) - .join("")})`, - ) - .join(""); - return `\n (node ${JSON.stringify(node.id)} ${node.state}\n (executor ${JSON.stringify(node.executor.key)} ${node.executor.version})\n (params ${data(node.parameters)})\n (inputs${inputs})\n )`; - }) - .join(""); - return `(yawn-graph ${AST_VERSION}\n (id ${JSON.stringify(graph.id)})\n (revision ${graph.revision})\n (pipelines ${data(graph.pipelines)})\n (nodes${nodes}))\n`; +/** Serializes the AST as an S-expression; named pass references preserve DAG fan-out. */ +export function serializeGraphAst(graph) { + return `(yawn-graph ${GRAPH_AST_VERSION} ${encode(createGraphAst(graph))})`; } - -export const GRAPH_AST_KIND = AST_KIND; -export const GRAPH_AST_VERSION = AST_VERSION; diff --git a/addons/render-graph-fxnode/package.json b/addons/render-graph-fxnode/package.json index 4f904bb..d6175f4 100644 --- a/addons/render-graph-fxnode/package.json +++ b/addons/render-graph-fxnode/package.json @@ -3,10 +3,7 @@ "version": "0.1.0", "description": "FXNode frontend for Yawn render graphs", "type": "module", - "exports": { - ".": "./src/index.js", - "./catalog": "./src/catalog.js" - }, + "exports": "./src/index.js", "dependencies": { "@yawn/render-graph-ast": "0.1.0" } diff --git a/addons/render-graph-fxnode/src/adapter.js b/addons/render-graph-fxnode/src/adapter.js deleted file mode 100644 index 0453c0d..0000000 --- a/addons/render-graph-fxnode/src/adapter.js +++ /dev/null @@ -1,601 +0,0 @@ -// Converts Yawn's FXNode authoring document into the canonical render-graph AST. -import { - CATALOG_VERSION, - descriptors, - GRAPH_ID, - nodeDefinitions, - socketTypes, -} from "./catalog.js"; -import { createGraphAst } from "@yawn/render-graph-ast"; - -class AuthoringGraphError extends Error { - constructor(code, details = {}) { - super(code); - this.name = "AuthoringGraphError"; - this.code = code; - this.details = Object.freeze({ ...details }); - } -} - -const fail = (code, details) => { - throw new AuthoringGraphError(code, details); -}; -const object = (value) => - value !== null && typeof value === "object" && !Array.isArray(value); -const identifier = (value) => - typeof value === "string" && - /^[A-Za-z][A-Za-z0-9_.-]*$/.test(value) && - new TextEncoder().encode(value).length <= 64; -const exactKeys = (value, keys) => - object(value) && - Object.keys(value).length === keys.length && - keys.every((key) => Object.hasOwn(value, key)); -const finiteJson = (value) => - value === null || - typeof value === "string" || - typeof value === "boolean" || - (typeof value === "number" && Number.isFinite(value)) || - (Array.isArray(value) && value.every(finiteJson)) || - (object(value) && Object.values(value).every(finiteJson)); -const canonical = (value) => - Array.isArray(value) - ? value.map(canonical) - : object(value) - ? Object.fromEntries( - Object.keys(value) - .sort() - .map((key) => [key, canonical(value[key])]), - ) - : value; -const deepFreeze = (value) => { - if (value && typeof value === "object" && !Object.isFrozen(value)) { - Object.freeze(value); - for (const child of Object.values(value)) deepFreeze(child); - } - return value; -}; -const sourceMaps = new WeakMap(); -export const mapAuthoringDiagnostic = (ir, diagnostic) => { - const details = diagnostic?.details; - const path = [ - details?.path, - diagnostic?.path, - details?.field, - diagnostic?.field, - ].find((value) => typeof value === "string"); - const map = sourceMaps.get(ir); - let match; - if (path && map) - for (const key of Object.keys(map)) - if ( - (path === key || - path.startsWith(`${key}.`) || - path.startsWith(`${key}[`)) && - (!match || key.length > match.length) - ) - match = key; - return deepFreeze({ - name: diagnostic?.name, - code: diagnostic?.code, - message: details?.message ?? diagnostic?.message ?? diagnostic?.code, - details: details === undefined ? undefined : structuredClone(details), - path, - source: match ? structuredClone(map[match]) : undefined, - }); -}; - -const mapValuePaths = (paths, path, source, value) => { - paths[path] = source; - if (Array.isArray(value)) - value.forEach((child, index) => - mapValuePaths(paths, `${path}[${index}]`, source, child), - ); - else if (object(value)) - for (const key of Object.keys(value)) - mapValuePaths(paths, `${path}.${key}`, source, value[key]); -}; - -function parameterValue(raw, schema, nodeId, key, semanticType) { - if (!exactKeys(raw, ["kind", "value"]) || raw.kind !== schema.type) - fail("AUTHORING_PARAMETER", { nodeId, parameter: key }); - const value = raw.value; - const bounded = (number) => - Number.isFinite(number) && - (schema.minimum === undefined || number >= schema.minimum) && - (schema.maximum === undefined || number <= schema.maximum); - const valid = - schema.type === "number" - ? bounded(value) && (!schema.integer || Number.isSafeInteger(value)) - : schema.type === "string" - ? typeof value === "string" && - (!schema.enum || schema.enum.includes(value)) - : schema.type === "boolean" - ? typeof value === "boolean" - : schema.type === "vector" || schema.type === "color" - ? Array.isArray(value) && - value.length === (semanticType?.startsWith("vec") ? Number(semanticType.at(-1)) : (schema.type === "vector" ? 3 : 4)) && - value.every(bounded) - : schema.type === "json" && finiteJson(value) && validSemanticValue(value, semanticType); - if (!valid) fail("AUTHORING_PARAMETER", { nodeId, parameter: key }); - return canonical(structuredClone(raw.value)); -} - -function validSemanticValue(value, type) { - if (!type) return true; - const finiteVector = (candidate, size) => - Array.isArray(candidate) && candidate.length === size && candidate.every(Number.isFinite); - const vector = /^vec([24])$/.exec(type); - if (vector) return finiteVector(value, Number(vector[1])); - if (type === "u32x16") - return Array.isArray(value) && value.length === 16 && - value.every((word) => Number.isInteger(word) && word >= 0 && word <= 0xffffffff); - if (type === "local_aabb") - return exactKeys(value, ["min", "max"]) && finiteVector(value.min, 3) && finiteVector(value.max, 3); - const match = /^mat([234])$/.exec(type); - if (match) { - const size = Number(match[1]); - return Array.isArray(value) && value.length === size && - value.every((column) => finiteVector(column, size)); - } - return false; -} - -export function adaptFxNodeSnapshot(raw, revision = 1, { pipelines = {} } = {}) { - try { - const rootKeys = [ - "graphId", - "catalogVersion", - "nodes", - "links", - "metadata", - "version", - ]; - if ( - !exactKeys(raw, rootKeys) || - !Array.isArray(raw.nodes) || - !Array.isArray(raw.links) || - !object(raw.metadata) || - !finiteJson(raw.metadata) - ) - fail("AUTHORING_SHAPE"); - if (raw.graphId !== GRAPH_ID || raw.catalogVersion !== CATALOG_VERSION) - fail("AUTHORING_CATALOG"); - if (!Number.isSafeInteger(raw.version) || raw.version < 0) - fail("AUTHORING_SHAPE"); - if (!Number.isInteger(revision) || revision < 1 || revision > 0xffffffff) - fail("AUTHORING_REVISION"); - const nodes = new Map(), - sockets = new Map(), - paths = {}; - for (let ordinal = 0; ordinal < raw.nodes.length; ordinal++) { - const n = raw.nodes[ordinal]; - if (!object(n) || !identifier(n.id)) fail("AUTHORING_ID", { id: n?.id }); - if (nodes.has(n.id)) fail("AUTHORING_ID_DUPLICATE", { id: n.id }); - const descriptor = descriptors[n.typeId], - definition = nodeDefinitions[n.typeId]; - if (!descriptor) - fail("AUTHORING_NODE_TYPE", { nodeId: n.id, typeId: n.typeId }); - const nodeKeys = [ - "id", - "typeId", - "typeVersion", - "position", - "size", - "label", - "parameters", - "sockets", - "muted", - "collapsed", - "extensions", - "known", - ]; - if (Object.hasOwn(n, "parentId")) nodeKeys.push("parentId"); - if ( - !exactKeys(n, nodeKeys) || - n.known !== true || - n.typeVersion !== descriptor.version || - typeof n.muted !== "boolean" || - typeof n.collapsed !== "boolean" || - typeof n.label !== "string" || - !exactKeys(n.position, ["x", "y"]) || - !Number.isFinite(n.position.x) || - !Number.isFinite(n.position.y) || - !exactKeys(n.size, ["x", "y"]) || - !Number.isFinite(n.size.x) || - !Number.isFinite(n.size.y) || - n.size.x <= 0 || - n.size.y <= 0 || - (Object.hasOwn(n, "parentId") && !identifier(n.parentId)) || - !object(n.extensions) || - !finiteJson(n.extensions) || - !Array.isArray(n.sockets) || - !object(n.parameters) - ) - fail("AUTHORING_NODE_INVALID", { nodeId: n.id }); - const parameterKeys = Object.keys(definition.parameters); - if ( - Object.keys(n.parameters).length !== parameterKeys.length || - !parameterKeys.every((key) => Object.hasOwn(n.parameters, key)) - ) - fail("AUTHORING_PARAMETER_SET", { nodeId: n.id }); - const parameters = Object.fromEntries( - parameterKeys.map((key) => [ - key, - parameterValue( - n.parameters[key], - definition.parameters[key], - n.id, - key, - ), - ]), - ); - if (n.typeId === "bloom_blur") - parameters.direction = - parameters.direction === "horizontal" ? [1, 0] : [0, 1]; - if (n.typeId === "frustum_cull") { - parameters.camera = parameters.cameraSelection; - delete parameters.cameraSelection; - } - if (n.typeId === "texture") { - const extent = - parameters.extentMode === "absolute" - ? { - kind: "absolute", - width: parameters.absoluteWidth, - height: parameters.absoluteHeight, - depthOrArrayLayers: parameters.depthOrArrayLayers, - } - : { - kind: "surface_relative", - width: { - numerator: parameters.relativeWidthNumerator, - denominator: parameters.relativeWidthDenominator, - }, - height: { - numerator: parameters.relativeHeightNumerator, - denominator: parameters.relativeHeightDenominator, - }, - depthOrArrayLayers: parameters.depthOrArrayLayers, - }; - const flat = structuredClone(parameters); - Object.keys(parameters).forEach((key) => delete parameters[key]); - Object.assign(parameters, { - residency: flat.residency, - texture: { - dimension: flat.dimension, - format: flat.format, - extent, - mipLevelCount: flat.mipLevelCount, - sampleCount: Number(flat.sampleCount), - viewFormats: flat.viewFormat === "none" ? [] : [flat.viewFormat], - }, - }); - } - const expected = [ - ...Object.keys(descriptor.inputs), - ...Object.keys(descriptor.outputs), - ]; - if (n.sockets.length !== expected.length) - fail("AUTHORING_SOCKET_SET", { nodeId: n.id }); - for (const s of n.sockets) { - if (!object(s) || !expected.includes(s.key) || sockets.has(s.id)) - fail("AUTHORING_SOCKET", { nodeId: n.id, socket: s?.key }); - const input = descriptor.inputs[s.key], - socketDefinition = definition.sockets[s.key], - direction = input ? "input" : "output", - dataType = socketDefinition.type, - socketKeys = [ - "id", - "key", - "label", - "direction", - "dataType", - "accepts", - "maxIncomingLinks", - ...(socketDefinition.value ? ["defaultValue"] : []), - "visible", - ]; - if ( - !exactKeys(s, socketKeys) || - s.id !== `${n.id}:${s.key}` || - s.label !== socketDefinition.title || - s.direction !== direction || - s.dataType !== dataType || - !Array.isArray(s.accepts) || - s.accepts.length !== - (direction === "input" - ? socketTypes[dataType].acceptsFrom.length - : 0) || - !s.accepts.every( - (v, i) => - v === - (direction === "input" - ? socketTypes[dataType].acceptsFrom[i] - : undefined), - ) || - (socketDefinition.value - ? (() => { - try { - parameterValue( - s.defaultValue, - socketDefinition.value, - n.id, - s.key, - input.accepted.types[0], - ); - return false; - } catch { - return true; - } - })() - : s.defaultValue !== undefined) || - s.visible !== socketDefinition.visible || - s.maxIncomingLinks !== socketDefinition.maxIncomingLinks - ) - fail("AUTHORING_SOCKET", { nodeId: n.id, socket: s.key }); - sockets.set(s.id, { - node: n.id, - key: s.key, - semanticName: (input ?? descriptor.outputs[s.key]).semanticName ?? s.key, - direction, - semanticType: input - ? input.accepted.types[0] - : descriptor.outputs[s.key].type, - authoringType: s.dataType, - maxIncomingLinks: s.maxIncomingLinks, - defaultValue: socketDefinition.value - ? structuredClone(s.defaultValue) - : undefined, - }); - } - if (new Set(n.sockets.map((s) => s.key)).size !== expected.length) - fail("AUTHORING_SOCKET_SET", { nodeId: n.id }); - for (const key of Object.keys(descriptor.inputs)) { - const authoredDefault = sockets.get(`${n.id}:${key}`).defaultValue; - if (authoredDefault) - parameters[`${descriptor.inputs[key].semanticName ?? key}Default`] = parameterValue( - authoredDefault, - definition.sockets[key].value, - n.id, - key, - descriptor.inputs[key].accepted.types[0], - ); - } - nodes.set(n.id, { - ordinal, - value: { - id: n.id, - state: n.muted ? "muted" : "enabled", - executor: { key: n.typeId, version: descriptor.version }, - parameters, - inputs: {}, - }, - }); - } - const incoming = new Map(), - linkIds = new Set(), - linkSources = new Map(); - for (let ordinal = 0; ordinal < raw.links.length; ordinal++) { - const link = raw.links[ordinal]; - if ( - !object(link) || - !identifier(link.id) || - linkIds.has(link.id) || - !exactKeys(link, [ - "id", - "fromNodeId", - "fromSocketId", - "toNodeId", - "toSocketId", - "muted", - "extensions", - ]) || - typeof link.muted !== "boolean" || - !object(link.extensions) || - !finiteJson(link.extensions) - ) - fail("AUTHORING_LINK", { linkId: link?.id }); - linkIds.add(link.id); - const from = sockets.get(link.fromSocketId), - to = sockets.get(link.toSocketId); - if ( - !from || - !to || - link.fromNodeId !== from.node || - link.toNodeId !== to.node || - from.direction !== "output" || - to.direction !== "input" || - (!link.muted && - (incoming.get(link.toSocketId) ?? 0) >= to.maxIncomingLinks) - ) - fail( - !link.muted && - (incoming.get(link.toSocketId) ?? 0) >= - (to?.maxIncomingLinks ?? Infinity) - ? "AUTHORING_LINK_INCOMING" - : "AUTHORING_LINK", - !link.muted && - (incoming.get(link.toSocketId) ?? 0) >= - (to?.maxIncomingLinks ?? Infinity) - ? { socketId: link.toSocketId } - : { linkId: link.id }, - ); - const accepted = - descriptors[nodes.get(to.node).value.executor.key].inputs[to.key] - .accepted.types; - const authoringAccepted = - socketTypes[ - nodeDefinitions[nodes.get(to.node).value.executor.key].sockets[to.key] - .type - ].acceptsFrom; - if ( - !accepted.includes(from.semanticType) || - !authoringAccepted.includes(from.authoringType) - ) - fail("AUTHORING_LINK_TYPE", { linkId: link.id }); - const linkSource = { - kind: "link", - linkId: link.id, - fromNodeId: link.fromNodeId, - fromSocketId: link.fromSocketId, - toNodeId: link.toNodeId, - toSocketId: link.toSocketId, - muted: link.muted, - nodeId: to.node, - input: to.key, - fromSocket: from.key, - toSocket: to.key, - }; - linkSources.set(link.id, linkSource); - if (!link.muted) { - incoming.set(link.toSocketId, (incoming.get(link.toSocketId) ?? 0) + 1); - (nodes.get(to.node).value.inputs[to.semanticName] ??= []).push({ - node: from.node, - socket: from.semanticName, - }); - } - } - const ordered = [...nodes.values()].sort((a, b) => - a.value.id < b.value.id - ? -1 - : a.value.id > b.value.id - ? 1 - : a.ordinal - b.ordinal, - ); - for (let wireOrdinal = 0; wireOrdinal < ordered.length; wireOrdinal++) { - const item = ordered[wireOrdinal]; - item.value.inputs = Object.fromEntries( - Object.keys(descriptors[item.value.executor.key].inputs) - .map((key) => descriptors[item.value.executor.key].inputs[key].semanticName ?? key) - .filter((semanticName) => Object.hasOwn(item.value.inputs, semanticName)) - .map((semanticName) => [semanticName, item.value.inputs[semanticName]]), - ); - const base = `nodes[${wireOrdinal}]`; - const nodeSource = { kind: "node", nodeId: item.value.id }; - paths[base] = nodeSource; - for (const field of [ - "id", - "state", - "executor", - "executor.key", - "executor.version", - ]) - paths[`${base}.${field}`] = nodeSource; - paths[`${base}.parameters`] = nodeSource; - const parameterSource = (parameter) => ({ - kind: "parameter", - nodeId: item.value.id, - parameter, - }); - if (item.value.executor.key === "texture") { - const root = `${base}.parameters`; - const texture = item.value.parameters.texture; - paths[`${root}.residency`] = parameterSource("residency"); - paths[`${root}.texture`] = nodeSource; - paths[`${root}.texture.dimension`] = parameterSource("dimension"); - paths[`${root}.texture.format`] = parameterSource("format"); - paths[`${root}.texture.extent`] = parameterSource("extentMode"); - paths[`${root}.texture.extent.kind`] = parameterSource("extentMode"); - paths[`${root}.texture.extent.depthOrArrayLayers`] = - parameterSource("depthOrArrayLayers"); - if (texture.extent.kind === "absolute") { - paths[`${root}.texture.extent.width`] = - parameterSource("absoluteWidth"); - paths[`${root}.texture.extent.height`] = - parameterSource("absoluteHeight"); - } else { - paths[`${root}.texture.extent.width`] = parameterSource("extentMode"); - paths[`${root}.texture.extent.width.numerator`] = parameterSource( - "relativeWidthNumerator", - ); - paths[`${root}.texture.extent.width.denominator`] = parameterSource( - "relativeWidthDenominator", - ); - paths[`${root}.texture.extent.height`] = - parameterSource("extentMode"); - paths[`${root}.texture.extent.height.numerator`] = parameterSource( - "relativeHeightNumerator", - ); - paths[`${root}.texture.extent.height.denominator`] = parameterSource( - "relativeHeightDenominator", - ); - } - paths[`${root}.texture.mipLevelCount`] = - parameterSource("mipLevelCount"); - paths[`${root}.texture.sampleCount`] = parameterSource("sampleCount"); - mapValuePaths( - paths, - `${root}.texture.viewFormats`, - parameterSource("viewFormat"), - texture.viewFormats, - ); - } else - for (const key of Object.keys(item.value.parameters)) - mapValuePaths( - paths, - `${base}.parameters.${key}`, - key.endsWith("Default") && Object.hasOwn(descriptors[item.value.executor.key].inputs, key.slice(0, -7)) - ? { - kind: "input", - nodeId: item.value.id, - input: key.slice(0, -7), - socketId: `${item.value.id}:${key.slice(0, -7)}`, - unconnected: true, - } - : parameterSource( - item.value.executor.key === "frustum_cull" && key === "camera" - ? "cameraSelection" - : key, - ), - item.value.parameters[key], - ); - for (const key of Object.keys( - descriptors[item.value.executor.key].inputs, - )) { - const links = raw.links.filter( - (x) => - !x.muted && - x.toNodeId === item.value.id && - sockets.get(x.toSocketId)?.key === key, - ); - const source = linkSources.get(links[0]?.id) ?? { - kind: "input", - nodeId: item.value.id, - input: key, - socketId: `${item.value.id}:${key}`, - unconnected: true, - }; - const semanticName = descriptors[item.value.executor.key].inputs[key].semanticName ?? key; - paths[`${base}.inputs.${semanticName}`] = source; - for (const [index, link] of links.entries()) { - const linkSource = linkSources.get(link.id); - paths[`${base}.inputs.${semanticName}[${index}]`] = linkSource; - paths[`${base}.inputs.${semanticName}[${index}].node`] = linkSource; - paths[`${base}.inputs.${semanticName}[${index}].socket`] = { - kind: "socket", - nodeId: link.fromNodeId, - socketId: link.fromSocketId, - socket: sockets.get(link.fromSocketId).key, - linkId: link.id, - }; - } - } - paths[`${base}.inputs`] = nodeSource; - } - const ir = createGraphAst({ - id: GRAPH_ID, - revision, - pipelines, - nodes: ordered.map((item) => item.value), - }); - const graphSource = { kind: "graph", graphId: GRAPH_ID }; - for (const field of ["version", "id", "revision", "pipelines", "nodes"]) - paths[field] = graphSource; - deepFreeze(paths); - sourceMaps.set(ir, paths); - return ir; - } catch (error) { - if (error instanceof AuthoringGraphError) throw error; - fail("AUTHORING_SHAPE"); - } -} diff --git a/addons/render-graph-fxnode/src/catalog.js b/addons/render-graph-fxnode/src/catalog.js deleted file mode 100644 index 92b09cf..0000000 --- a/addons/render-graph-fxnode/src/catalog.js +++ /dev/null @@ -1,535 +0,0 @@ -// Yawn's FXNode contract catalog lives with this addon, not core or the example app. -export const GRAPH_ID = "authored_gpu_culling"; -export const CATALOG_VERSION = 13; -const exact = (type) => ({ kind: "exact", types: [type] }); -const i = (type, minimum = 1, authoringType, defaultPolicy = minimum ? "none" : "parameter_literal", maximum = 1) => ({ - accepted: typeof type === "string" ? exact(type) : type, - cardinality: { minimum, maximum }, - ...(authoringType ? { authoringType } : {}), - defaultPolicy, -}); -const o = (type, semanticName) => ({ type, ...(semanticName ? { semanticName } : {}) }); -const expression = (inputs, outputs) => ({ - version: 1, - execution: "expression", - inputs: Object.fromEntries(Object.entries(inputs).map(([name, type]) => [name, i(type, 0)])), - outputs: Object.fromEntries(Object.entries(outputs).map(([name, type]) => [name, o(type)])), - parameters: {}, -}); -const numbered = (prefix, count, type) => - Object.fromEntries(Array.from({ length: count }, (_, index) => [`${prefix}${index}`, type])); -const expressionCatalog = { - and: { ...expression({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } }, - or: { ...expression({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } }, - not: expression({ operand: "bool" }, { value: "bool" }), - xor: { ...expression({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } }, - xnor: { ...expression({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } }, - greater_than_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }), - less_than_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }), - equals_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }), - greater_than_u32: expression({ left: "u32", right: "u32" }, { value: "bool" }), - less_than_u32: expression({ left: "u32", right: "u32" }, { value: "bool" }), - equals_u32: expression({ left: "u32", right: "u32" }, { value: "bool" }), - separate_vec2: expression({ vector: "vec2" }, { x: "f32", y: "f32" }), - combine_vec2: expression({ x: "f32", y: "f32" }, { vector: "vec2" }), - separate_vec3: expression({ vector: "vec3" }, { x: "f32", y: "f32", z: "f32" }), - combine_vec3: expression({ x: "f32", y: "f32", z: "f32" }, { vector: "vec3" }), - separate_vec4: expression({ vector: "vec4" }, { x: "f32", y: "f32", z: "f32", w: "f32" }), - combine_vec4: expression({ x: "f32", y: "f32", z: "f32", w: "f32" }, { vector: "vec4" }), - separate_mat2: expression({ matrix: "mat2" }, numbered("column", 2, "vec2")), - combine_mat2: expression(numbered("column", 2, "vec2"), { matrix: "mat2" }), - separate_mat3: expression({ matrix: "mat3" }, numbered("column", 3, "vec3")), - combine_mat3: expression(numbered("column", 3, "vec3"), { matrix: "mat3" }), - separate_mat4: expression({ matrix: "mat4" }, numbered("column", 4, "vec4")), - combine_mat4: expression(numbered("column", 4, "vec4"), { matrix: "mat4" }), - separate_u32x16: expression({ value: "u32x16" }, numbered("word", 16, "u32")), - combine_u32x16: expression(numbered("word", 16, "u32"), { value: "u32x16" }), - separate_u32_bits: expression({ value: "u32" }, numbered("bit", 32, "bool")), - combine_u32_bits: expression(numbered("bit", 32, "bool"), { value: "u32" }), - separate_local_aabb: expression({ value: "local_aabb" }, { min: "vec3", max: "vec3" }), -}; -const texture = { - residency: "transient", - texture: { - dimension: "d2", - format: "rgba16_float", - extent: { - kind: "surface_relative", - width: { numerator: 1, denominator: 1 }, - height: { numerator: 1, denominator: 1 }, - depthOrArrayLayers: 1, - }, - mipLevelCount: 1, - sampleCount: 1, - viewFormats: [], - }, -}; -const rasterInputs = () => ({ - mesh: i("mesh_data"), - predicate: i("bool", 0), - "input.color": { ...i("texture", 0, undefined, "compiler_texture"), semanticName: "color" }, - "input.depth": { ...i("texture", 0, undefined, "compiler_texture"), semanticName: "depth" }, -}); -const rasterOutputs = () => ({ "output.color": o("texture", "color"), "output.depth": o("texture", "depth") }); -const rasterParameters = () => ({ depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] }); -const raster = () => ({ version: 2, execution: "render", inputs: rasterInputs(), outputs: rasterOutputs(), parameters: rasterParameters() }); -export const NODE_TITLE_OVERRIDES = Object.freeze({ - ground_plane: "Ground Plane", - gltf_standard: "glTF Standard", - gltf_standard_double_sided: "glTF Standard — Double-Sided", -}); -export const semanticCatalog = Object.freeze({ - mesh: { - version: 2, - execution: "source", - inputs: {}, - outputs: { - mesh: o("mesh_data"), - type: o("u32x16"), - localAabb: o("local_aabb"), - }, - parameters: {}, - }, - texture: { - version: 2, - execution: "source", - inputs: {}, - outputs: { texture: o("texture") }, - parameters: { - residency: "transient", - format: "rgba16_float", - dimension: "d2", - extentMode: "surface_relative", - absoluteWidth: 1, - absoluteHeight: 1, - relativeWidthNumerator: 1, - relativeWidthDenominator: 1, - relativeHeightNumerator: 1, - relativeHeightDenominator: 1, - depthOrArrayLayers: 1, - mipLevelCount: 1, - sampleCount: "1", - viewFormat: "none", - }, - }, - frustum_cull: { - version: 2, - execution: "expression", - inputs: { - mesh: i("mesh_data"), - localAabb: i("local_aabb"), - }, - outputs: { isFrustumCulled: o("bool") }, - parameters: { cameraSelection: "active" }, - }, - ground_plane: raster(), - gltf_standard: raster(), - gltf_standard_double_sided: raster(), - ...expressionCatalog, - fullscreen_copy: { - version: 1, - execution: "render", - inputs: { - source: i("texture"), - colorTarget: i("texture"), - }, - outputs: { color: o("texture") }, - parameters: {}, - }, - color_balance: { - version: 1, - execution: "render", - inputs: { source: i("texture"), colorTarget: i("texture") }, - outputs: { color: o("texture") }, - parameters: { - mode: "lift_gamma_gain", factor: 1, - lift: 0, liftColor: [1, 1, 1, 1], gamma: 1, gammaColor: [1, 1, 1, 1], gain: 1, gainColor: [1, 1, 1, 1], - offset: 0, offsetColor: [1, 1, 1, 1], power: 1, powerColor: [1, 1, 1, 1], slope: 1, slopeColor: [1, 1, 1, 1], - }, - }, - exposure_contrast: { - version: 1, execution: "render", - inputs: { source: i("texture"), colorTarget: i("texture") }, outputs: { color: o("texture") }, - parameters: { exposureStops: 0, contrast: 1, pivot: 0.18, factor: 1 }, - }, - saturation: { - version: 1, execution: "render", - inputs: { source: i("texture"), colorTarget: i("texture") }, outputs: { color: o("texture") }, - parameters: { saturation: 1, factor: 1 }, - }, - channel_mixer: { - version: 1, execution: "render", - inputs: { source: i("texture"), colorTarget: i("texture") }, outputs: { color: o("texture") }, - parameters: { redOutput: [1, 0, 0], greenOutput: [0, 1, 0], blueOutput: [0, 0, 1], factor: 1 }, - }, - bloom_extract: { - version: 1, - execution: "render", - inputs: { - source: i("texture"), - colorTarget: i("texture"), - }, - outputs: { color: o("texture") }, - parameters: { threshold: 1, knee: 0.5 }, - }, - bloom_blur: { - version: 1, - execution: "render", - inputs: { - source: i("texture"), - colorTarget: i("texture"), - }, - outputs: { color: o("texture") }, - parameters: { direction: [1, 0], radius: 1 }, - }, - bloom_composite: { - version: 1, - execution: "render", - inputs: { - source: i("texture"), - bloom: i("texture"), - colorTarget: i("texture"), - }, - outputs: { color: o("texture") }, - parameters: { intensity: 1 }, - }, - luminance_edge: { - version: 1, - execution: "render", - inputs: { - source: i("texture"), - colorTarget: i("texture"), - }, - outputs: { color: o("texture") }, - parameters: { strength: 2 }, - }, - frame_out: { - version: 3, - execution: "frame", - inputs: { color: i("texture") }, - outputs: {}, - parameters: { surfaceFormat: "preferred", hdrEnabled: true, toneMapper: "aces", exposureStops: 0, outputTransfer: "srgb", scaleMode: "stretch", filter: "linear", backgroundColor: [0, 0, 0, 1] }, - }, -}); -const socketColors = [ - "#d17c7c", - "#d19e7c", - "#d1c77c", - "#9ed17c", - "#7cd1a5", - "#7ccbd1", - "#7c98d1", - "#a27cd1", - "#d17cb8", -]; -export const socketTypes = Object.fromEntries( - [ - "texture", - "mesh_data", - "bool", "f32", "u32", "vec2", "vec3", "vec4", - "mat2", "mat3", "mat4", "u32x16", "local_aabb", - ].map((type, index) => [ - type, - { - title: type.replaceAll("_", " "), - color: socketColors[index % socketColors.length], - acceptsFrom: [type], - }, - ]), -); -export const theme = { - background: "#151820", - grid: "#292e3a", - frame: "#30343a80", - frameHeader: "#59616c", - body: "#292e39", - control: "#24272b", - controlFill: "#4775b8", - controlEditing: "#181a1d", - textSelection: "#4775b8", - outline: "#0b0d12", - text: "#edf1f7", - muted: "#969eaa", - shadow: "#00000088", - nodeSelected: "#ff9f43", - nodeActive: "#ffffff", - unknownHeader: "#555b64", - unknownSocket: "#999999", - linkMuted: "#d94b4b", - knifeMuted: "#e85b5b", - emphasis: "#ffffff", - focus: "#f5a623", - editOutline: "#666a70", - resize: "#8b8e95", - muteOverlay: "#14141459", - boxSelectionFill: "#f5a6231f", - checkerLight: "#aaaaaa", - checkerDark: "#777777", - widgetBorder: "#111216", - rampBorder: "#111111", - resourceBackground: "#202228", -}; -export const styles = { - source: { header: "#3977a8" }, - compute: { header: "#725a9b" }, - expression: { header: "#725a9b" }, - cpu_preparation: { header: "#8a6d3b" }, - render: { header: "#426b43" }, - frame: { header: "#a75d37" }, -}; -const socket = (title, direction, type, value = null, capacity = 1) => ({ - title, - direction, - type, - maxIncomingLinks: direction === "input" ? capacity : 0, - visible: true, - value, - showValue: value !== null, -}); -const tagged = (kind, value) => ({ kind, value: structuredClone(value) }); -const number = (value, minimum, maximum) => ({ - type: "number", - default: tagged("number", value), - ...(minimum !== undefined ? { minimum } : {}), - ...(maximum !== undefined ? { maximum } : {}), -}); -const enumeration = (value, values) => ({ - type: "string", - default: tagged("string", value), - enum: values, -}); -const boolean = (value) => ({ - type: "boolean", - default: tagged("boolean", value), -}); -const color = (value, minimum = 0, maximum = 1) => ({ - type: "color", - default: tagged("color", value), - minimum, - maximum, -}); -const vector = (value, minimum, maximum) => ({ - type: "vector", default: tagged("vector", value), - ...(minimum !== undefined ? { minimum } : {}), - ...(maximum !== undefined ? { maximum } : {}), -}); -const json = (value) => ({ type: "json", default: tagged("json", value) }); -const socketDefault = (type, value) => { - if (type === "bool") return boolean(value); - if (type === "f32") return number(value); - if (type === "u32") return { ...number(value, 0, 0xffffffff), integer: true }; - if (type === "vec3") return vector(value); - return json(value); -}; -const zero = (type) => { - if (type === "bool") return false; - if (type === "f32" || type === "u32") return 0; - if (/^vec[234]$/.test(type)) return Array(Number(type.at(-1))).fill(0); - if (type === "u32x16") return Array(16).fill(0); - if (type === "local_aabb") return { min: [0, 0, 0], max: [0, 0, 0] }; - const size = Number(type.at(-1)); - return Array.from({ length: size }, (_, column) => - Array.from({ length: size }, (_, row) => Number(column === row))); -}; -const defaultForInput = (key, name, type) => { - if (["ground_plane", "gltf_standard", "gltf_standard_double_sided"].includes(key) && name === "predicate") return true; - if (key === "and") return true; - if (/^combine_mat[234]$/.test(key)) { - const index = Number(name.replace("column", "")); - return zero(type).map((_, row) => Number(index === row)); - } - return zero(type); -}; -const parameterSchemas = { - texture: { - residency: enumeration("transient", ["transient", "persistent"]), - format: enumeration("rgba16_float", [ - "rgba8_unorm", - "rgba8_unorm_srgb", - "bgra8_unorm", - "bgra8_unorm_srgb", - "rgba16_float", - "r32_float", - "depth32_float", - ]), - dimension: enumeration("d2", ["d1", "d2", "d3"]), - extentMode: enumeration("surface_relative", [ - "surface_relative", - "absolute", - ]), - absoluteWidth: { ...number(1, 1, 0xffffffff), integer: true }, - absoluteHeight: { ...number(1, 1, 0xffffffff), integer: true }, - relativeWidthNumerator: { ...number(1, 1, 0xffffffff), integer: true }, - relativeWidthDenominator: { ...number(1, 1, 0xffffffff), integer: true }, - relativeHeightNumerator: { ...number(1, 1, 0xffffffff), integer: true }, - relativeHeightDenominator: { ...number(1, 1, 0xffffffff), integer: true }, - depthOrArrayLayers: { ...number(1, 1, 0xffffffff), integer: true }, - mipLevelCount: { ...number(1, 1, 0xffffffff), integer: true }, - sampleCount: enumeration("1", ["1", "4"]), - viewFormat: enumeration("none", [ - "none", - "rgba8_unorm", - "rgba8_unorm_srgb", - "bgra8_unorm", - "bgra8_unorm_srgb", - "rgba16_float", - "r32_float", - "depth32_float", - ]), - }, - mesh: {}, - frustum_cull: { cameraSelection: enumeration("active", ["active"]) }, - ground_plane: { - depthCompare: enumeration("less_equal", [ - "never", - "less", - "equal", - "less_equal", - "greater", - "not_equal", - "greater_equal", - "always", - ]), - depthWriteEnabled: boolean(true), - clearDepth: number(1, 0, 1), - clearColor: color([0.015, 0.02, 0.03, 1]), - }, - fullscreen_copy: {}, - color_balance: { - mode: enumeration("lift_gamma_gain", ["lift_gamma_gain", "offset_power_slope"]), factor: number(1, 0, 1), - lift: number(0, -1, 1), liftColor: color([1, 1, 1, 1], 0, 4), gamma: number(1, 0.01, 4), gammaColor: color([1, 1, 1, 1], 0, 4), gain: number(1, 0, 4), gainColor: color([1, 1, 1, 1], 0, 4), - offset: number(0, -1, 1), offsetColor: color([1, 1, 1, 1], 0, 2), power: number(1, 0.01, 4), powerColor: color([1, 1, 1, 1], 0, 4), slope: number(1, 0, 4), slopeColor: color([1, 1, 1, 1], 0, 4), - }, - exposure_contrast: { exposureStops: number(0, -10, 10), contrast: number(1, 0.01, 4), pivot: number(0.18, 0.001, 4), factor: number(1, 0, 1) }, - saturation: { saturation: number(1, 0, 4), factor: number(1, 0, 1) }, - channel_mixer: { redOutput: vector([1, 0, 0], -2, 2), greenOutput: vector([0, 1, 0], -2, 2), blueOutput: vector([0, 0, 1], -2, 2), factor: number(1, 0, 1) }, - bloom_extract: { threshold: number(1, 0, 64), knee: number(0.5, 0, 1) }, - bloom_blur: { - direction: enumeration("horizontal", ["horizontal", "vertical"]), - radius: number(1, 1, 16), - }, - bloom_composite: { intensity: number(1, 0, 16) }, - luminance_edge: { strength: number(2, 0, 16) }, - frame_out: { - surfaceFormat: enumeration("preferred", ["preferred", "rgba8_unorm", "bgra8_unorm", "rgba16_float"]), - hdrEnabled: boolean(true), - toneMapper: enumeration("aces", ["aces", "reinhard", "none"]), - exposureStops: number(0, -10, 10), - outputTransfer: enumeration("srgb", ["srgb", "linear"]), - scaleMode: enumeration("stretch", ["stretch", "contain", "cover"]), - filter: enumeration("linear", ["linear", "nearest"]), - backgroundColor: color([0, 0, 0, 1]), - }, -}; -for (const key of Object.keys(expressionCatalog)) parameterSchemas[key] = {}; -parameterSchemas.gltf_standard = structuredClone(parameterSchemas.ground_plane); -parameterSchemas.gltf_standard_double_sided = structuredClone(parameterSchemas.ground_plane); -export const nodeDefinitions = Object.fromEntries( - Object.entries(semanticCatalog).map(([key, c]) => { - const sockets = { - ...Object.fromEntries( - Object.entries(c.inputs).map(([n, v]) => [ - n, - socket( - v.semanticName ?? n, - "input", - v.authoringType ?? v.accepted.types[0], - v.defaultPolicy === "parameter_literal" ? socketDefault(v.accepted.types[0], defaultForInput(key, n, v.accepted.types[0])) : null, - v.cardinality.maximum, - ), - ]), - ), - ...Object.fromEntries( - Object.entries(c.outputs).map(([n, v]) => [ - n, - socket(v.semanticName ?? n, "output", v.authoringType ?? v.type), - ]), - ), - }, - parameters = parameterSchemas[key]; - if ( - !parameters || - Object.keys(parameters).length !== Object.keys(c.parameters).length || - !Object.keys(c.parameters).every((name) => - Object.hasOwn(parameters, name), - ) - ) - throw new Error(`parameter schema mismatch for ${key}`); - return [ - key, - { - version: c.version, - title: NODE_TITLE_OVERRIDES[key] ?? key.replaceAll("_", " "), - behavior: "standard", - style: c.execution, - parameters, - sockets, - ui: [ - ...Object.keys(parameters).map((parameter) => ({ - kind: "parameter", - parameter, - ...(key === "frustum_cull" && parameter === "cameraSelection" - ? { title: "Camera" } - : {}), - })), - ...Object.keys(sockets).map((socket) => ({ kind: "socket", socket })), - ], - muteBypass: [], - migrations: [], - }, - ]; - }), -); -nodeDefinitions.mesh.sockets.localAabb.title = "Local AABB"; -for (const key of Object.keys(NODE_TITLE_OVERRIDES)) { - for (const item of nodeDefinitions[key].ui) { - if (item.parameter === "clearColor") item.title = "Initial Color"; - if (item.parameter === "clearDepth") item.title = "Initial Depth"; - } -} -nodeDefinitions.color_balance.ui = [ - { kind: "parameter", parameter: "mode" }, - { kind: "widget", widget: "grading-wheels", bindings: [ - { title: "Lift", scalar: "lift", color: "liftColor" }, { title: "Gamma", scalar: "gamma", color: "gammaColor" }, { title: "Gain", scalar: "gain", color: "gainColor" }, - ], visibleWhen: { parameter: "mode", equals: "lift_gamma_gain" } }, - { kind: "widget", widget: "grading-wheels", bindings: [ - { title: "Offset", scalar: "offset", color: "offsetColor" }, { title: "Power", scalar: "power", color: "powerColor" }, { title: "Slope", scalar: "slope", color: "slopeColor" }, - ], visibleWhen: { parameter: "mode", equals: "offset_power_slope" } }, - { kind: "parameter", parameter: "factor" }, - { kind: "socket", socket: "source" }, { kind: "socket", socket: "colorTarget" }, { kind: "socket", socket: "color" }, -]; -nodeDefinitions.frame_out.ui = [ - { kind: "text", variant: "section", title: "Canvas Presentation" }, - { kind: "parameter", parameter: "surfaceFormat", title: "Surface Format" }, - { kind: "text", variant: "section", title: "Display Transform" }, - { kind: "parameter", parameter: "hdrEnabled", title: "HDR" }, - { kind: "parameter", parameter: "toneMapper", title: "Tone Mapper", visibleWhen: { parameter: "hdrEnabled", equals: true } }, - { kind: "parameter", parameter: "exposureStops", title: "Exposure", visibleWhen: { parameter: "hdrEnabled", equals: true } }, - { kind: "parameter", parameter: "outputTransfer", title: "Transfer" }, - { kind: "parameter", parameter: "scaleMode", title: "Scale" }, - { kind: "parameter", parameter: "filter" }, - { kind: "parameter", parameter: "backgroundColor", title: "Background", visibleWhen: { parameter: "scaleMode", equals: "contain" } }, - { kind: "socket", socket: "color" }, -]; -export const fxNodeComposition = Object.freeze({ - schemaVersion: 2, - id: "yawn.render-graph", - version: CATALOG_VERSION, - compatibility: { wildcardInputTypes: [] }, - socketTypes, - nodeStyles: styles, - resources: {}, - theme, - nodes: nodeDefinitions, -}); -export const descriptors = Object.fromEntries( - Object.entries(semanticCatalog).map(([key, c]) => [ - key, - { - version: c.version, - inputs: c.inputs, - outputs: c.outputs, - parameters: c.parameters, - }, - ]), -); diff --git a/addons/render-graph-fxnode/src/index.js b/addons/render-graph-fxnode/src/index.js index 96e6985..672d791 100644 --- a/addons/render-graph-fxnode/src/index.js +++ b/addons/render-graph-fxnode/src/index.js @@ -1,6 +1,18 @@ -// FXNode is an optional authoring frontend. Its exporter is intentionally the only -// FXNode-aware code that feeds the canonical AST package. -export { - adaptFxNodeSnapshot, - mapAuthoringDiagnostic, -} from "./adapter.js"; +import { createGraphAst } from "@yawn/render-graph-ast"; + +/** Exports FXNode nodes with a `pass` payload and links as the canonical pass DAG. */ +export function adaptFxNodeSnapshot(snapshot, { pipelines = {}, resources = {} } = {}) { + if (!Array.isArray(snapshot?.nodes) || !Array.isArray(snapshot?.links)) throw new TypeError("FXNODE_GRAPH"); + const passes = snapshot.nodes.map(node => { + if (!node?.id || !node.pass) throw new TypeError("FXNODE_PASS"); + return { ...structuredClone(node.pass), id: node.id, after: [...(node.pass.after ?? [])] }; + }); + const byId = new Map(passes.map(pass => [pass.id, pass])); + for (const link of snapshot.links) { + const source = link.fromNodeId ?? link.from?.node; + const target = link.toNodeId ?? link.to?.node; + if (!byId.has(source) || !byId.has(target)) throw new TypeError("FXNODE_LINK"); + if (!byId.get(target).after.includes(source)) byId.get(target).after.push(source); + } + return createGraphAst({ id: snapshot.graphId ?? "fxnode", pipelines, resources, passes }); +} diff --git a/addons/render-graph-js/src/index.js b/addons/render-graph-js/src/index.js index 446078e..9e092fe 100644 --- a/addons/render-graph-js/src/index.js +++ b/addons/render-graph-js/src/index.js @@ -1,67 +1,12 @@ -import { - createGraphAst, - reference, - serializeGraphAst, -} from "@yawn/render-graph-ast"; +import { createGraphAst, serializeGraphAst } from "@yawn/render-graph-ast"; -/** Compiles a plain JavaScript object description into the canonical graph AST. */ -export const graphFromObject = (description) => createGraphAst(description); +/** Converts a plain JavaScript object into the canonical render-graph AST. */ +export const graphFromObject = graph => createGraphAst(graph); -/** Small mutable authoring facade; `ast()` returns an immutable canonical AST. */ -export class RenderGraph { - #id; - #revision; - #nodes = []; - #render = []; - #compute = []; - - constructor(id, revision = 1) { - this.#id = id; - this.#revision = revision; - } - - renderPipeline(declaration) { - this.#render.push(structuredClone(declaration)); - return this; - } - - computePipeline(declaration) { - this.#compute.push(structuredClone(declaration)); - return this; - } - - node(id, executor, { version = 1, parameters = {}, inputs = {}, state = "enabled" } = {}) { - this.#nodes.push({ - id, - state, - executor: { key: executor, version }, - parameters: structuredClone(parameters), - inputs: structuredClone(inputs), - }); - return this; - } - - ast() { - return createGraphAst({ - id: this.#id, - revision: this.#revision, - pipelines: { render: this.#render, compute: this.#compute }, - nodes: this.#nodes, - }); - } - - serialize() { - return serializeGraphAst(this.ast()); - } - - load(core) { - return core.compileGraph(this.serialize()); - } +/** Serializes a JSO graph and asks Yawn Core to prepare and activate its loadout. */ +export function loadGraph(core, graph) { + if (!core?.loadGraph) throw new TypeError("core must be a YawnCore instance"); + return core.loadGraph(serializeGraphAst(graphFromObject(graph))); } -/** Canonicalizes a JSO/AST and sends its S-expression wire form to Yawn core. */ -export function loadGraph(core, description) { - return core.compileGraph(serializeGraphAst(graphFromObject(description))); -} - -export { reference as ref, serializeGraphAst }; +export { serializeGraphAst }; diff --git a/docs/.vitepress/Playground.vue b/docs/.vitepress/Playground.vue new file mode 100644 index 0000000..bfc3d65 --- /dev/null +++ b/docs/.vitepress/Playground.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 9af5242..a336c8b 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -1,67 +1,36 @@ import { defineConfig } from "vitepress"; +const headers = { + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", +}; + +const isolation = { + name: "cross-origin-isolation", + configureServer(server) { + server.middlewares.use((_, response, next) => { + for (const [name, value] of Object.entries(headers)) { + response.setHeader(name, value); + } + next(); + }); + }, +}; + export default defineConfig({ title: "Yawn", - description: "Worker-native WebGPU rendering with shared render data.", - base: "/docs/", - outDir: "../dist/docs", + description: "Shared render data and a render graph.", cleanUrls: true, - head: [ - ["meta", { name: "theme-color", content: "#0d1117" }], - ["link", { rel: "icon", href: "data:image/svg+xml," }], - ], themeConfig: { - logo: { - light: "data:image/svg+xml,", - dark: "data:image/svg+xml,", - }, nav: [ - { text: "Learn", link: "/guide/first-scene" }, - { text: "Packages", link: "/packages/" }, - { text: "Recipes", link: "/recipes/" }, - { text: "Playground", link: "/../playground/" }, + { text: "Architecture", link: "/" }, + { text: "Playground", link: "/playground" }, ], - sidebar: [ - { - text: "Get started", - items: [ - { text: "Your first scene", link: "/guide/first-scene" }, - { text: "How Yawn fits together", link: "/guide/architecture" }, - ], - }, - { - text: "Package tutorials", - items: [ - { text: "Package map", link: "/packages/" }, - { text: "Core and render data", link: "/packages/core" }, - { text: "Render graph frontends", link: "/packages/render-graph" }, - { text: "glTF import worker", link: "/packages/gltf-import" }, - { text: "Conventional handles", link: "/packages/mesh-handles" }, - ], - }, - { - text: "Recipes", - items: [ - { text: "All recipes", link: "/recipes/" }, - { text: "Graph authoring", link: "/recipes/graph-authoring" }, - { text: "Pipelines and loadouts", link: "/recipes/pipelines" }, - { text: "Assets and render data", link: "/recipes/render-data" }, - { text: "Runtime interaction", link: "/recipes/runtime" }, - ], - }, - ], - socialLinks: [ - { icon: "github", link: "https://github.com/heaust-ops/yawn" }, - ], - search: { provider: "local" }, - outline: { level: [2, 3] }, - editLink: { - pattern: "https://github.com/heaust-ops/yawn/edit/feat/core/docs/:path", - text: "Edit this page on GitHub", - }, - footer: { - message: "Core owns render data and render graphs. Addons own conveniences.", - copyright: "Yawn is pre-1.0 software.", - }, + }, + vite: { + plugins: [isolation], + worker: { format: "es" }, + server: { allowedHosts: true, headers }, + preview: { allowedHosts: true, headers }, }, }); diff --git a/docs/.vitepress/theme/Playground.vue b/docs/.vitepress/theme/Playground.vue deleted file mode 100644 index dc21d5d..0000000 --- a/docs/.vitepress/theme/Playground.vue +++ /dev/null @@ -1,31 +0,0 @@ - - -