Build Rust render graph core
Keep hot render state in a generic shared SOA arena and use worker messages only for allocation, graph compilation, loadout changes, and render pacing. Compile external S-expression graphs into upfront WebGPU resources, transient aliases, and reusable render bundles. Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Generated
+589
@@ -2,36 +2,361 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.7.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[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 = "2.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.3"
|
version = "3.20.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.25.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck_derive"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||||
|
|
||||||
|
[[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 = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "document-features"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||||
|
dependencies = [
|
||||||
|
"litrs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gloo-timers"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "half"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crunchy",
|
||||||
|
"num-traits",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hexf-parse"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown 0.17.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.189"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libloading"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libm"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "naga"
|
||||||
|
version = "26.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "916cbc7cb27db60be930a4e2da243cf4bc39569195f22fd8ee419cd31d5b662c"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"bit-set",
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"codespan-reporting",
|
||||||
|
"half",
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"hexf-parse",
|
||||||
|
"indexmap",
|
||||||
|
"libm",
|
||||||
|
"log",
|
||||||
|
"num-traits",
|
||||||
|
"once_cell",
|
||||||
|
"rustc-hash",
|
||||||
|
"thiserror",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"libm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
@@ -41,6 +366,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "profiling"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.47"
|
version = "1.0.47"
|
||||||
@@ -50,12 +381,45 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "renderdoc-sys"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -63,6 +427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -98,6 +463,24 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.119"
|
version = "2.0.119"
|
||||||
@@ -120,12 +503,47 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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 = "2.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.127"
|
version = "0.2.127"
|
||||||
@@ -139,6 +557,16 @@ dependencies = [
|
|||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-futures"
|
||||||
|
version = "0.4.77"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.127"
|
version = "0.2.127"
|
||||||
@@ -171,12 +599,173 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
|
||||||
|
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",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"document-features",
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"naga",
|
||||||
|
"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",
|
||||||
|
"cfg_aliases",
|
||||||
|
"document-features",
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"naga",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
"portable-atomic",
|
||||||
|
"profiling",
|
||||||
|
"raw-window-handle",
|
||||||
|
"rustc-hash",
|
||||||
|
"serde",
|
||||||
|
"smallvec",
|
||||||
|
"thiserror",
|
||||||
|
"wgpu-core-deps-windows-linux-android",
|
||||||
|
"wgpu-hal",
|
||||||
|
"wgpu-types",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8d0e67224cc7305b3b4eb2cc57ca4c4c3afc665c1d1bee162ea806e19c47bdd"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libloading",
|
||||||
|
"log",
|
||||||
|
"naga",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"raw-window-handle",
|
||||||
|
"renderdoc-sys",
|
||||||
|
"thiserror",
|
||||||
|
"wgpu-types",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wgpu-types"
|
||||||
|
version = "26.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eca7a8d8af57c18f57d393601a1fb159ace8b2328f1b6b5f80893f7d672c9ae2"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"bytemuck",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yawn-core"
|
name = "yawn-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"gloo-timers",
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"wgpu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.56"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.56"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# Yawn
|
# Yawn
|
||||||
|
|
||||||
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.
|
Yawn Core is two things: a generic structure-of-arrays arena in a `SharedArrayBuffer`, and a Rust render graph that turns externally supplied WGSL into an up-front WebGPU loadout.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
JSO or FXNode → AST → S-expression → graph worker → WebGPU
|
JSO or FXNode → AST → S-expression → worker messages → Rust/WebGPU
|
||||||
↑
|
↑
|
||||||
any thread → direct shared row writes ┘
|
any thread → direct shared row writes ─────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
The arena starts with only one eight-float `info` row for frame timing and direct SAB render skipping. Messages create or delete other `{ name, rows, stride, format }` arrays, allocate named slots, compile graphs, switch loadouts, and control render pacing. 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.
|
||||||
|
|
||||||
WGSL, pipelines, glTF import, and conventional mesh/camera/material handles live in `addons/`; core contains no shader or scene model.
|
WGSL, pipelines, glTF import, and conventional mesh/camera/material handles live in `addons/`; core contains no shader or scene model.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ edition = "2021"
|
|||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
path = "lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
gloo-timers = { version = "0.3", features = ["futures"] }
|
||||||
|
js-sys = "0.3"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
web-sys = { version = "0.3", features = ["OffscreenCanvas"] }
|
||||||
|
wgpu = { version = "26", default-features = false, features = ["serde", "webgpu", "wgsl"] }
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
const types = { f32: Float32Array, u32: Uint32Array, i32: Int32Array };
|
const views = Object.freeze({ f32: Float32Array, u32: Uint32Array, i32: Int32Array });
|
||||||
|
|
||||||
export class SharedRows {
|
export class SharedRows {
|
||||||
constructor(buffer, descriptor) {
|
constructor(buffer, descriptor) {
|
||||||
|
if (!(buffer instanceof SharedArrayBuffer)) throw new TypeError("ROWS_DESCRIPTOR");
|
||||||
this.buffer = buffer;
|
this.buffer = buffer;
|
||||||
|
this.update(descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(descriptor) {
|
||||||
|
if (!views[descriptor.format] || descriptor.name !== (this.descriptor?.name ?? descriptor.name))
|
||||||
|
throw new TypeError("ROWS_DESCRIPTOR");
|
||||||
this.descriptor = Object.freeze(descriptor);
|
this.descriptor = Object.freeze(descriptor);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return this.descriptor.name; }
|
get name() { return this.descriptor.name; }
|
||||||
@@ -11,16 +19,18 @@ export class SharedRows {
|
|||||||
get stride() { return this.descriptor.stride; }
|
get stride() { return this.descriptor.stride; }
|
||||||
get format() { return this.descriptor.format; }
|
get format() { return this.descriptor.format; }
|
||||||
get view() {
|
get view() {
|
||||||
return new types[this.format](this.buffer, this.descriptor.offset, this.rows * this.stride / 4);
|
const View = views[this.format];
|
||||||
|
return new View(this.buffer, this.descriptor.offset, this.descriptor.bytes / View.BYTES_PER_ELEMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
row(index) {
|
row(index) {
|
||||||
if (!Number.isInteger(index) || index < 0 || index >= this.rows) throw new RangeError("ROW_RANGE");
|
if (!Number.isInteger(index) || index < 0 || index >= this.rows) throw new RangeError("ROW_RANGE");
|
||||||
const width = this.stride / 4;
|
const width = this.stride / views[this.format].BYTES_PER_ELEMENT;
|
||||||
return this.view.subarray(index * width, (index + 1) * width);
|
return this.view.subarray(index * width, (index + 1) * width);
|
||||||
}
|
}
|
||||||
|
|
||||||
read(index) { return Array.from(this.row(index)); }
|
read(index) { return Array.from(this.row(index)); }
|
||||||
|
|
||||||
write(index, values) {
|
write(index, values) {
|
||||||
const row = this.row(index);
|
const row = this.row(index);
|
||||||
if (!values || values.length !== row.length) throw new RangeError("ROW_WIDTH");
|
if (!values || values.length !== row.length) throw new RangeError("ROW_WIDTH");
|
||||||
@@ -49,30 +59,74 @@ export class YawnCore {
|
|||||||
this.#worker.addEventListener("messageerror", () => this.#fail("WORKER_ERROR"));
|
this.#worker.addEventListener("messageerror", () => this.#fail("WORKER_ERROR"));
|
||||||
this.#worker.start?.();
|
this.#worker.start?.();
|
||||||
const offscreen = canvas.transferControlToOffscreen?.() ?? canvas;
|
const offscreen = canvas.transferControlToOffscreen?.() ?? canvas;
|
||||||
this.ready = this.#request("init", { canvas: offscreen, arenaBytes }, [offscreen])
|
this.ready = this.#request("init", { canvas: offscreen, arenaBytes }, [offscreen]).then(result => {
|
||||||
.then(({ buffer }) => { this.#buffer = buffer; });
|
this.#buffer = result.buffer;
|
||||||
|
for (const descriptor of result.rows) this.#arrays.set(
|
||||||
|
descriptor.name,
|
||||||
|
new SharedRows(this.#buffer, descriptor),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async allocateRows({ name, rows, stride, format }) {
|
async createRows({ name, rows, stride, format }) {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
const descriptor = await this.#request("allocate", { name, rows, stride, format });
|
const descriptor = await this.#request("create-rows", { name, rows, stride, format });
|
||||||
const array = new SharedRows(this.#buffer, descriptor);
|
const array = this.#arrays.get(name)?.update(descriptor)
|
||||||
|
?? new SharedRows(this.#buffer, descriptor);
|
||||||
this.#arrays.set(name, array);
|
this.#arrays.set(name, array);
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteRows(name) {
|
||||||
|
await this.ready;
|
||||||
|
await this.#request("delete-rows", { name });
|
||||||
|
this.#arrays.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async allocateObject(name) {
|
||||||
|
await this.ready;
|
||||||
|
const { id, rows } = await this.#request("allocate-object", { name });
|
||||||
|
this.#arrays.get(name).update(rows);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteObject(name, id) {
|
||||||
|
await this.ready;
|
||||||
|
return this.#request("delete-object", { name, id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async compileGraph(serialized) {
|
||||||
|
await this.ready;
|
||||||
|
return this.#request("compile-graph", { serialized });
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchLoadout(id) {
|
||||||
|
await this.ready;
|
||||||
|
return this.#request("switch-loadout", { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async play() {
|
||||||
|
await this.ready;
|
||||||
|
return this.#request("play");
|
||||||
|
}
|
||||||
|
|
||||||
|
async pause() {
|
||||||
|
await this.ready;
|
||||||
|
return this.#request("pause");
|
||||||
|
}
|
||||||
|
|
||||||
|
async setFps(fps) {
|
||||||
|
await this.ready;
|
||||||
|
return this.#request("set-fps", { fps });
|
||||||
|
}
|
||||||
|
|
||||||
array(name) {
|
array(name) {
|
||||||
const array = this.#arrays.get(name);
|
const array = this.#arrays.get(name);
|
||||||
if (!array) throw new Error(`UNKNOWN_ARRAY: ${name}`);
|
if (!array) throw new Error(`UNKNOWN_ARRAY: ${name}`);
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadGraph(serialized) {
|
#request(type, payload = {}, transfer = []) {
|
||||||
await this.ready;
|
|
||||||
return this.#request("load-graph", { serialized });
|
|
||||||
}
|
|
||||||
|
|
||||||
#request(type, payload, transfer = []) {
|
|
||||||
const request = this.#next++;
|
const request = this.#next++;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.#pending.set(request, { resolve, reject });
|
this.#pending.set(request, { resolve, reject });
|
||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[path = "render_graph/compiler.rs"]
|
||||||
|
mod compiler;
|
||||||
|
#[path = "renderer/wgpu.rs"]
|
||||||
|
mod gpu;
|
||||||
|
#[path = "render_graph/gpu_resource.rs"]
|
||||||
|
mod gpu_resource;
|
||||||
|
#[path = "render_graph/render_graph.rs"]
|
||||||
|
mod graph;
|
||||||
|
#[path = "renderer/render.rs"]
|
||||||
|
mod render;
|
||||||
|
mod render_data;
|
||||||
|
#[path = "render_graph/store.rs"]
|
||||||
|
mod store;
|
||||||
|
|
||||||
|
use gpu::Wgpu;
|
||||||
|
use render::RenderLoop;
|
||||||
|
use render_data::RenderData;
|
||||||
|
use store::Store;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct Core {
|
||||||
|
data: Rc<RefCell<RenderData>>,
|
||||||
|
gpu: Rc<RefCell<Option<Wgpu>>>,
|
||||||
|
store: Rc<RefCell<Store>>,
|
||||||
|
render: Rc<RenderLoop>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl Core {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(arena_bytes: u32) -> Result<Self, JsError> {
|
||||||
|
Ok(Self {
|
||||||
|
data: Rc::new(RefCell::new(
|
||||||
|
RenderData::new(arena_bytes).map_err(JsError::new)?,
|
||||||
|
)),
|
||||||
|
gpu: Rc::new(RefCell::new(None)),
|
||||||
|
store: Rc::new(RefCell::new(Store::default())),
|
||||||
|
render: Rc::new(RenderLoop::new()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn initialize(&self, canvas: web_sys::OffscreenCanvas) -> Result<(), JsError> {
|
||||||
|
let gpu = Wgpu::new(canvas)
|
||||||
|
.await
|
||||||
|
.map_err(|error| JsError::new(&error))?;
|
||||||
|
*self.gpu.borrow_mut() = Some(gpu);
|
||||||
|
self.render
|
||||||
|
.start(self.gpu.clone(), self.store.clone(), self.data.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rows(&self) -> String {
|
||||||
|
serde_json::to_string(&self.data.borrow().descriptors()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_rows(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
rows: u32,
|
||||||
|
stride: u32,
|
||||||
|
format: String,
|
||||||
|
) -> Result<String, JsError> {
|
||||||
|
let rows = self
|
||||||
|
.data
|
||||||
|
.borrow_mut()
|
||||||
|
.create_rows(name, rows, stride, format)
|
||||||
|
.map_err(JsError::new)?;
|
||||||
|
if let Some(gpu) = self.gpu.borrow().as_ref() {
|
||||||
|
self.store
|
||||||
|
.borrow_mut()
|
||||||
|
.refresh_rows(&rows.name, gpu, &self.data.borrow())
|
||||||
|
.map_err(|error| JsError::new(&error))?;
|
||||||
|
}
|
||||||
|
Ok(serde_json::to_string(&rows).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_rows(&self, name: String) -> Result<(), JsError> {
|
||||||
|
if self.store.borrow().uses_rows(&name) {
|
||||||
|
return Err(JsError::new("ROWS_ACTIVE"));
|
||||||
|
}
|
||||||
|
self.data
|
||||||
|
.borrow_mut()
|
||||||
|
.delete_rows(&name)
|
||||||
|
.map_err(JsError::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocate_object(&self, name: &str) -> Result<String, JsError> {
|
||||||
|
let (id, grew) = self
|
||||||
|
.data
|
||||||
|
.borrow_mut()
|
||||||
|
.allocate_object(name)
|
||||||
|
.map_err(JsError::new)?;
|
||||||
|
let rows = self
|
||||||
|
.data
|
||||||
|
.borrow()
|
||||||
|
.rows(name)
|
||||||
|
.ok_or_else(|| JsError::new("ROWS_UNKNOWN"))?
|
||||||
|
.clone();
|
||||||
|
if grew {
|
||||||
|
if let Some(gpu) = self.gpu.borrow().as_ref() {
|
||||||
|
self.store
|
||||||
|
.borrow_mut()
|
||||||
|
.refresh_rows(name, gpu, &self.data.borrow())
|
||||||
|
.map_err(|error| JsError::new(&error))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(serde_json::json!({ "id": id, "rows": rows }).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_object(&self, name: &str, id: u32) -> Result<(), JsError> {
|
||||||
|
self.data
|
||||||
|
.borrow_mut()
|
||||||
|
.delete_object(name, id)
|
||||||
|
.map_err(JsError::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compile_graph(&self, source: &str) -> Result<String, JsError> {
|
||||||
|
let graph = compiler::compile(source).map_err(JsError::new)?;
|
||||||
|
Ok(self.store.borrow_mut().save(graph))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_loadout(&self, id: &str) -> Result<(), JsError> {
|
||||||
|
let gpu = self.gpu.borrow();
|
||||||
|
let gpu = gpu
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| JsError::new("WEBGPU_UNINITIALIZED"))?;
|
||||||
|
self.store
|
||||||
|
.borrow_mut()
|
||||||
|
.switch(id, gpu, &self.data.borrow())
|
||||||
|
.map_err(|error| JsError::new(&error))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(&self) {
|
||||||
|
self.render.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pause(&self) {
|
||||||
|
self.render.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_fps(&self, fps: u32) -> Result<(), JsError> {
|
||||||
|
self.render.set_fps(fps).map_err(JsError::new)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -3,6 +3,6 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Rust/WASM render data and render graph core",
|
"description": "Rust/WASM render data and render graph core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./src/index.js",
|
"exports": "./index.js",
|
||||||
"files": ["src", "pkg"]
|
"files": ["index.js", "worker.js", "pkg"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
const ALIGNMENT: u32 = 64;
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
pub struct Rows {
|
||||||
|
pub name: String,
|
||||||
|
pub rows: u32,
|
||||||
|
pub stride: u32,
|
||||||
|
pub format: String,
|
||||||
|
pub offset: u32,
|
||||||
|
pub bytes: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Slots {
|
||||||
|
active: HashSet<u32>,
|
||||||
|
free: Vec<u32>,
|
||||||
|
next: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RenderData {
|
||||||
|
arena: Box<[u8]>,
|
||||||
|
arena_start: usize,
|
||||||
|
base: u32,
|
||||||
|
free: BTreeMap<u32, u32>,
|
||||||
|
rows: HashMap<String, Rows>,
|
||||||
|
slots: HashMap<String, Slots>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderData {
|
||||||
|
pub fn new(capacity: u32) -> Result<Self, &'static str> {
|
||||||
|
if capacity < ALIGNMENT || capacity > u32::MAX - (ALIGNMENT - 1) {
|
||||||
|
return Err("INIT");
|
||||||
|
}
|
||||||
|
let mut arena = vec![0; (capacity + ALIGNMENT - 1) as usize].into_boxed_slice();
|
||||||
|
let pointer = arena.as_mut_ptr() as usize;
|
||||||
|
let aligned = (pointer + ALIGNMENT as usize - 1) & !(ALIGNMENT as usize - 1);
|
||||||
|
let mut data = Self {
|
||||||
|
arena,
|
||||||
|
arena_start: aligned - pointer,
|
||||||
|
base: aligned as u32,
|
||||||
|
free: BTreeMap::from([(0, capacity)]),
|
||||||
|
rows: HashMap::new(),
|
||||||
|
slots: HashMap::new(),
|
||||||
|
};
|
||||||
|
data.create_rows("info".into(), 1, 32, "f32".into())?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_rows(
|
||||||
|
&mut self,
|
||||||
|
name: String,
|
||||||
|
rows: u32,
|
||||||
|
stride: u32,
|
||||||
|
format: String,
|
||||||
|
) -> Result<Rows, &'static str> {
|
||||||
|
if name.is_empty()
|
||||||
|
|| rows == 0
|
||||||
|
|| stride < 16
|
||||||
|
|| stride % 16 != 0
|
||||||
|
|| !matches!(format.as_str(), "f32" | "u32" | "i32")
|
||||||
|
{
|
||||||
|
return Err("ROWS");
|
||||||
|
}
|
||||||
|
if let Some(current) = self.rows.get(&name).cloned() {
|
||||||
|
if current.stride != stride || current.format != format {
|
||||||
|
return Err("ROWS");
|
||||||
|
}
|
||||||
|
if rows <= current.rows {
|
||||||
|
return Ok(current);
|
||||||
|
}
|
||||||
|
if name == "info" {
|
||||||
|
return Err("ROWS_BUILTIN");
|
||||||
|
}
|
||||||
|
return self.grow_rows(current, rows);
|
||||||
|
}
|
||||||
|
let bytes = rows.checked_mul(stride).ok_or("ARENA_OOM")?;
|
||||||
|
let offset = self.reserve(bytes)?;
|
||||||
|
let end = offset + bytes;
|
||||||
|
self.arena[self.arena_start + offset as usize..self.arena_start + end as usize].fill(0);
|
||||||
|
let descriptor = Rows {
|
||||||
|
name: name.clone(),
|
||||||
|
rows,
|
||||||
|
stride,
|
||||||
|
format,
|
||||||
|
offset: self.base + offset,
|
||||||
|
bytes,
|
||||||
|
};
|
||||||
|
self.rows.insert(name, descriptor.clone());
|
||||||
|
self.slots.insert(descriptor.name.clone(), Slots::default());
|
||||||
|
Ok(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_rows(&mut self, name: &str) -> Result<(), &'static str> {
|
||||||
|
if name == "info" {
|
||||||
|
return Err("ROWS_BUILTIN");
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.slots
|
||||||
|
.get(name)
|
||||||
|
.is_some_and(|slots| !slots.active.is_empty())
|
||||||
|
{
|
||||||
|
return Err("ROWS_ACTIVE");
|
||||||
|
}
|
||||||
|
let rows = self.rows.remove(name).ok_or("ROWS_UNKNOWN")?;
|
||||||
|
self.slots.remove(name);
|
||||||
|
self.release(rows.offset - self.base, rows.bytes);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rows(&self, name: &str) -> Option<&Rows> {
|
||||||
|
self.rows.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn descriptors(&self) -> Vec<Rows> {
|
||||||
|
let mut rows = self.rows.values().cloned().collect::<Vec<_>>();
|
||||||
|
rows.sort_by_key(|rows| rows.offset);
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bytes(&self, name: &str) -> Option<&[u8]> {
|
||||||
|
let rows = self.rows(name)?;
|
||||||
|
let start = self.arena_start + (rows.offset - self.base) as usize;
|
||||||
|
Some(&self.arena[start..start + rows.bytes as usize])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocate_object(&mut self, name: &str) -> Result<(u32, bool), &'static str> {
|
||||||
|
if name == "info" {
|
||||||
|
return Err("ROWS_BUILTIN");
|
||||||
|
}
|
||||||
|
let slots = self.slots.get(name).ok_or("ROWS_UNKNOWN")?;
|
||||||
|
let reused = slots.free.last().copied();
|
||||||
|
let id = reused.unwrap_or(slots.next);
|
||||||
|
let next = id.checked_add(1).ok_or("OBJECT_LIMIT")?;
|
||||||
|
let capacity = self.rows[name].rows;
|
||||||
|
let grew = id >= capacity;
|
||||||
|
if grew {
|
||||||
|
let descriptor = self.rows[name].clone();
|
||||||
|
self.grow_rows(descriptor, next)?;
|
||||||
|
}
|
||||||
|
let slots = self.slots.get_mut(name).unwrap();
|
||||||
|
if reused.is_some() {
|
||||||
|
slots.free.pop();
|
||||||
|
} else {
|
||||||
|
slots.next = next;
|
||||||
|
}
|
||||||
|
slots.active.insert(id);
|
||||||
|
Ok((id, grew))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_object(&mut self, name: &str, id: u32) -> Result<(), &'static str> {
|
||||||
|
let slots = self.slots.get_mut(name).ok_or("ROWS_UNKNOWN")?;
|
||||||
|
if !slots.active.remove(&id) {
|
||||||
|
return Err("OBJECT_UNKNOWN");
|
||||||
|
}
|
||||||
|
let rows = self.rows[name].clone();
|
||||||
|
let start = self.arena_start + (rows.offset - self.base + id * rows.stride) as usize;
|
||||||
|
self.arena[start..start + rows.stride as usize].fill(0);
|
||||||
|
slots.free.push(id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grow_rows(&mut self, mut descriptor: Rows, rows: u32) -> Result<Rows, &'static str> {
|
||||||
|
let bytes = rows.checked_mul(descriptor.stride).ok_or("ARENA_OOM")?;
|
||||||
|
let old_offset = descriptor.offset - self.base;
|
||||||
|
let old_end = old_offset + descriptor.bytes;
|
||||||
|
let extra = bytes - descriptor.bytes;
|
||||||
|
if self
|
||||||
|
.free
|
||||||
|
.get(&old_end)
|
||||||
|
.is_some_and(|available| *available >= extra)
|
||||||
|
{
|
||||||
|
let available = self.free.remove(&old_end).unwrap();
|
||||||
|
if available > extra {
|
||||||
|
self.free.insert(old_end + extra, available - extra);
|
||||||
|
}
|
||||||
|
self.arena[self.arena_start + old_end as usize
|
||||||
|
..self.arena_start + (old_end + extra) as usize]
|
||||||
|
.fill(0);
|
||||||
|
} else {
|
||||||
|
let offset = self.reserve(bytes)?;
|
||||||
|
let start = self.arena_start + offset as usize;
|
||||||
|
self.arena[start..start + bytes as usize].fill(0);
|
||||||
|
self.arena.copy_within(
|
||||||
|
self.arena_start + old_offset as usize..self.arena_start + old_end as usize,
|
||||||
|
start,
|
||||||
|
);
|
||||||
|
self.release(old_offset, descriptor.bytes);
|
||||||
|
descriptor.offset = self.base + offset;
|
||||||
|
}
|
||||||
|
descriptor.rows = rows;
|
||||||
|
descriptor.bytes = bytes;
|
||||||
|
self.rows
|
||||||
|
.insert(descriptor.name.clone(), descriptor.clone());
|
||||||
|
Ok(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_info(&mut self, delta: f32, frame: u32, elapsed: f32, fps: u32) {
|
||||||
|
let start = self.arena_start + (self.rows["info"].offset - self.base) as usize;
|
||||||
|
for (index, value) in [delta, frame as f32, elapsed, fps as f32]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
self.arena[start + index * 4..start + index * 4 + 4]
|
||||||
|
.copy_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn skip_render(&self) -> bool {
|
||||||
|
let rows = &self.rows["info"];
|
||||||
|
let start = self.arena_start + (rows.offset - self.base) as usize + 16;
|
||||||
|
f32::from_le_bytes(self.arena[start..start + 4].try_into().unwrap()) == 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reserve(&mut self, bytes: u32) -> Result<u32, &'static str> {
|
||||||
|
let (block, block_bytes, offset) = self
|
||||||
|
.free
|
||||||
|
.iter()
|
||||||
|
.find_map(|(&block, &block_bytes)| {
|
||||||
|
let offset = align(block)?;
|
||||||
|
(offset.checked_add(bytes)? <= block.checked_add(block_bytes)?).then_some((
|
||||||
|
block,
|
||||||
|
block_bytes,
|
||||||
|
offset,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.ok_or("ARENA_OOM")?;
|
||||||
|
self.free.remove(&block);
|
||||||
|
if offset > block {
|
||||||
|
self.free.insert(block, offset - block);
|
||||||
|
}
|
||||||
|
let end = offset + bytes;
|
||||||
|
let block_end = block + block_bytes;
|
||||||
|
if end < block_end {
|
||||||
|
self.free.insert(end, block_end - end);
|
||||||
|
}
|
||||||
|
Ok(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release(&mut self, mut offset: u32, mut bytes: u32) {
|
||||||
|
if let Some((&previous, &previous_bytes)) = self.free.range(..offset).next_back() {
|
||||||
|
if previous + previous_bytes == offset {
|
||||||
|
self.free.remove(&previous);
|
||||||
|
offset = previous;
|
||||||
|
bytes += previous_bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((&next, &next_bytes)) = self.free.range(offset..).next() {
|
||||||
|
if offset + bytes == next {
|
||||||
|
self.free.remove(&next);
|
||||||
|
bytes += next_bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.free.insert(offset, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align(value: u32) -> Option<u32> {
|
||||||
|
value
|
||||||
|
.checked_add(ALIGNMENT - 1)
|
||||||
|
.map(|value| value & !(ALIGNMENT - 1))
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
use serde_json::{Map, Value};
|
||||||
|
|
||||||
|
use crate::graph::RenderGraph;
|
||||||
|
|
||||||
|
pub fn compile(source: &str) -> Result<RenderGraph, &'static str> {
|
||||||
|
let Expression::List(mut root) = Parser::parse(source)? else {
|
||||||
|
return Err("GRAPH_WIRE");
|
||||||
|
};
|
||||||
|
if root.len() != 3
|
||||||
|
|| !matches!(&root[0], Expression::Atom(Value::String(tag)) if tag == "yawn-graph")
|
||||||
|
|| !matches!(&root[1], Expression::Atom(Value::Number(version)) if version.as_u64() == Some(1))
|
||||||
|
{
|
||||||
|
return Err("GRAPH_WIRE");
|
||||||
|
}
|
||||||
|
let value = decode(root.pop().unwrap())?;
|
||||||
|
let mut graph: RenderGraph = serde_json::from_value(value).map_err(|_| "GRAPH_SHAPE")?;
|
||||||
|
graph.prepare()?;
|
||||||
|
Ok(graph)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Expression {
|
||||||
|
Atom(Value),
|
||||||
|
List(Vec<Expression>),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Parser<'a> {
|
||||||
|
source: &'a [u8],
|
||||||
|
at: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Parser<'a> {
|
||||||
|
fn parse(source: &'a str) -> Result<Expression, &'static str> {
|
||||||
|
let mut parser = Self {
|
||||||
|
source: source.as_bytes(),
|
||||||
|
at: 0,
|
||||||
|
};
|
||||||
|
let expression = parser.expression()?;
|
||||||
|
parser.whitespace();
|
||||||
|
(parser.at == parser.source.len())
|
||||||
|
.then_some(expression)
|
||||||
|
.ok_or("GRAPH_WIRE")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expression(&mut self) -> Result<Expression, &'static str> {
|
||||||
|
self.whitespace();
|
||||||
|
match self.source.get(self.at) {
|
||||||
|
Some(b'(') => self.list(),
|
||||||
|
Some(b'"') => self.string(),
|
||||||
|
Some(b')') | None => Err("GRAPH_WIRE"),
|
||||||
|
Some(_) => self.atom(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&mut self) -> Result<Expression, &'static str> {
|
||||||
|
self.at += 1;
|
||||||
|
let mut values = Vec::new();
|
||||||
|
loop {
|
||||||
|
self.whitespace();
|
||||||
|
match self.source.get(self.at) {
|
||||||
|
Some(b')') => {
|
||||||
|
self.at += 1;
|
||||||
|
return Ok(Expression::List(values));
|
||||||
|
}
|
||||||
|
None => return Err("GRAPH_WIRE"),
|
||||||
|
_ => values.push(self.expression()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string(&mut self) -> Result<Expression, &'static str> {
|
||||||
|
let start = self.at;
|
||||||
|
self.at += 1;
|
||||||
|
let mut escaped = false;
|
||||||
|
while let Some(&byte) = self.source.get(self.at) {
|
||||||
|
self.at += 1;
|
||||||
|
if escaped {
|
||||||
|
escaped = false;
|
||||||
|
} else if byte == b'\\' {
|
||||||
|
escaped = true;
|
||||||
|
} else if byte == b'"' {
|
||||||
|
let value = serde_json::from_slice(&self.source[start..self.at])
|
||||||
|
.map_err(|_| "GRAPH_WIRE")?;
|
||||||
|
return Ok(Expression::Atom(Value::String(value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err("GRAPH_WIRE")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn atom(&mut self) -> Result<Expression, &'static str> {
|
||||||
|
let start = self.at;
|
||||||
|
while self
|
||||||
|
.source
|
||||||
|
.get(self.at)
|
||||||
|
.is_some_and(|byte| !byte.is_ascii_whitespace() && !matches!(byte, b'(' | b')'))
|
||||||
|
{
|
||||||
|
self.at += 1;
|
||||||
|
}
|
||||||
|
let token = std::str::from_utf8(&self.source[start..self.at]).map_err(|_| "GRAPH_WIRE")?;
|
||||||
|
let value = match token {
|
||||||
|
"true" => Value::Bool(true),
|
||||||
|
"false" => Value::Bool(false),
|
||||||
|
"null" => Value::Null,
|
||||||
|
_ => serde_json::from_str::<Value>(token)
|
||||||
|
.ok()
|
||||||
|
.filter(Value::is_number)
|
||||||
|
.unwrap_or_else(|| Value::String(token.into())),
|
||||||
|
};
|
||||||
|
Ok(Expression::Atom(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn whitespace(&mut self) {
|
||||||
|
while self
|
||||||
|
.source
|
||||||
|
.get(self.at)
|
||||||
|
.is_some_and(u8::is_ascii_whitespace)
|
||||||
|
{
|
||||||
|
self.at += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode(expression: Expression) -> Result<Value, &'static str> {
|
||||||
|
let Expression::List(mut values) = expression else {
|
||||||
|
return match expression {
|
||||||
|
Expression::Atom(value) => Ok(value),
|
||||||
|
Expression::List(_) => unreachable!(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if values.is_empty() {
|
||||||
|
return Err("GRAPH_WIRE");
|
||||||
|
}
|
||||||
|
let tag = match values.remove(0) {
|
||||||
|
Expression::Atom(Value::String(tag)) => tag,
|
||||||
|
_ => return Err("GRAPH_WIRE"),
|
||||||
|
};
|
||||||
|
if tag == "array" {
|
||||||
|
return values.into_iter().map(decode).collect();
|
||||||
|
}
|
||||||
|
if tag != "object" {
|
||||||
|
return Err("GRAPH_WIRE");
|
||||||
|
}
|
||||||
|
let mut object = Map::new();
|
||||||
|
for field in values {
|
||||||
|
let Expression::List(mut field) = field else {
|
||||||
|
return Err("GRAPH_WIRE");
|
||||||
|
};
|
||||||
|
if field.len() != 3
|
||||||
|
|| !matches!(&field[0], Expression::Atom(Value::String(tag)) if tag == "field")
|
||||||
|
{
|
||||||
|
return Err("GRAPH_WIRE");
|
||||||
|
}
|
||||||
|
let value = decode(field.pop().unwrap())?;
|
||||||
|
let key = match field.pop().unwrap() {
|
||||||
|
Expression::Atom(Value::String(key)) => key,
|
||||||
|
_ => return Err("GRAPH_WIRE"),
|
||||||
|
};
|
||||||
|
if object.insert(key, value).is_some() {
|
||||||
|
return Err("GRAPH_WIRE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Value::Object(object))
|
||||||
|
}
|
||||||
@@ -0,0 +1,545 @@
|
|||||||
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::num::NonZeroU64;
|
||||||
|
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::gpu::Wgpu;
|
||||||
|
use crate::graph::{Binding, Extent, Pass, RenderGraph, RenderPipeline, Texture};
|
||||||
|
use crate::render_data::RenderData;
|
||||||
|
|
||||||
|
pub struct GpuResources {
|
||||||
|
pub buffers: HashMap<String, GpuBuffer>,
|
||||||
|
pub textures: Vec<GpuTexture>,
|
||||||
|
pub texture_slots: HashMap<String, usize>,
|
||||||
|
pub samplers: HashMap<String, wgpu::Sampler>,
|
||||||
|
pub render_pipelines: HashMap<String, wgpu::RenderPipeline>,
|
||||||
|
pub compute_pipelines: HashMap<String, wgpu::ComputePipeline>,
|
||||||
|
pub passes: Vec<GpuPass>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GpuBuffer {
|
||||||
|
pub buffer: wgpu::Buffer,
|
||||||
|
pub source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GpuTexture {
|
||||||
|
pub _texture: wgpu::Texture,
|
||||||
|
pub view: wgpu::TextureView,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GpuPass {
|
||||||
|
Render(wgpu::RenderBundle),
|
||||||
|
Compute {
|
||||||
|
pipeline: wgpu::ComputePipeline,
|
||||||
|
bind_groups: Vec<(u32, wgpu::BindGroup)>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GpuResources {
|
||||||
|
pub fn activate(graph: &RenderGraph, gpu: &Wgpu, data: &RenderData) -> Result<Self, String> {
|
||||||
|
let mut buffers = HashMap::new();
|
||||||
|
for source in &graph.resources.buffers {
|
||||||
|
let rows = data.rows(&source.array).ok_or("GRAPH_ARRAY_UNKNOWN")?;
|
||||||
|
let buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some(&source.id),
|
||||||
|
size: u64::from(rows.bytes.max(4)),
|
||||||
|
usage: buffer_usage(&source.usage)?,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
gpu.queue
|
||||||
|
.write_buffer(&buffer, 0, data.bytes(&source.array).unwrap());
|
||||||
|
buffers.insert(
|
||||||
|
source.id.clone(),
|
||||||
|
GpuBuffer {
|
||||||
|
buffer,
|
||||||
|
source: source.array.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut textures = Vec::new();
|
||||||
|
let mut physical_slots = HashMap::new();
|
||||||
|
let mut texture_slots = HashMap::new();
|
||||||
|
for source in &graph.resources.textures {
|
||||||
|
let physical = match physical_slots.get(&source.slot) {
|
||||||
|
Some(&physical) => physical,
|
||||||
|
None => {
|
||||||
|
let descriptor = texture_descriptor(source, gpu.width, gpu.height)?;
|
||||||
|
let texture = gpu.device.create_texture(&descriptor);
|
||||||
|
let physical = textures.len();
|
||||||
|
textures.push(GpuTexture {
|
||||||
|
view: texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||||
|
_texture: texture,
|
||||||
|
});
|
||||||
|
physical_slots.insert(source.slot, physical);
|
||||||
|
physical
|
||||||
|
}
|
||||||
|
};
|
||||||
|
texture_slots.insert(source.id.clone(), physical);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut samplers = HashMap::new();
|
||||||
|
for source in &graph.resources.samplers {
|
||||||
|
samplers.insert(
|
||||||
|
source.id.clone(),
|
||||||
|
gpu.device
|
||||||
|
.create_sampler(&sampler_descriptor(&source.id, &source.descriptor)?),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let render_pipelines = graph
|
||||||
|
.pipelines
|
||||||
|
.render
|
||||||
|
.iter()
|
||||||
|
.map(|source| {
|
||||||
|
create_render_pipeline(source, gpu).map(|pipeline| (source.id.clone(), pipeline))
|
||||||
|
})
|
||||||
|
.collect::<Result<HashMap<_, _>, _>>()?;
|
||||||
|
let compute_pipelines = graph
|
||||||
|
.pipelines
|
||||||
|
.compute
|
||||||
|
.iter()
|
||||||
|
.map(|source| {
|
||||||
|
let module = gpu
|
||||||
|
.device
|
||||||
|
.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some(&source.id),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(source.code.clone().into()),
|
||||||
|
});
|
||||||
|
let pipeline =
|
||||||
|
gpu.device
|
||||||
|
.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||||
|
label: Some(&source.id),
|
||||||
|
layout: None,
|
||||||
|
module: &module,
|
||||||
|
entry_point: Some(&source.entry),
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
Ok::<_, String>((source.id.clone(), pipeline))
|
||||||
|
})
|
||||||
|
.collect::<Result<HashMap<_, _>, _>>()?;
|
||||||
|
|
||||||
|
let mut resources = Self {
|
||||||
|
buffers,
|
||||||
|
textures,
|
||||||
|
texture_slots,
|
||||||
|
samplers,
|
||||||
|
render_pipelines,
|
||||||
|
compute_pipelines,
|
||||||
|
passes: Vec::new(),
|
||||||
|
};
|
||||||
|
for pass in &graph.passes {
|
||||||
|
let compiled = match pass.kind.as_str() {
|
||||||
|
"render" => GpuPass::Render(resources.render_bundle(graph, pass, gpu)?),
|
||||||
|
"compute" => {
|
||||||
|
let pipeline = resources
|
||||||
|
.compute_pipelines
|
||||||
|
.get(&pass.pipeline)
|
||||||
|
.ok_or("GRAPH_PIPELINE")?
|
||||||
|
.clone();
|
||||||
|
let bind_groups = resources.bind_groups(
|
||||||
|
pass,
|
||||||
|
|group| pipeline.get_bind_group_layout(group),
|
||||||
|
gpu,
|
||||||
|
)?;
|
||||||
|
GpuPass::Compute {
|
||||||
|
pipeline,
|
||||||
|
bind_groups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Err("GRAPH_PASS".into()),
|
||||||
|
};
|
||||||
|
resources.passes.push(compiled);
|
||||||
|
}
|
||||||
|
Ok(resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_view(&self, id: &str) -> Option<&wgpu::TextureView> {
|
||||||
|
self.texture_slots
|
||||||
|
.get(id)
|
||||||
|
.and_then(|slot| self.textures.get(*slot))
|
||||||
|
.map(|texture| &texture.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_bundle(
|
||||||
|
&self,
|
||||||
|
graph: &RenderGraph,
|
||||||
|
pass: &Pass,
|
||||||
|
gpu: &Wgpu,
|
||||||
|
) -> Result<wgpu::RenderBundle, String> {
|
||||||
|
let pipeline = self
|
||||||
|
.render_pipelines
|
||||||
|
.get(&pass.pipeline)
|
||||||
|
.ok_or("GRAPH_PIPELINE")?;
|
||||||
|
let declaration = graph
|
||||||
|
.pipelines
|
||||||
|
.render
|
||||||
|
.iter()
|
||||||
|
.find(|pipeline| pipeline.id == pass.pipeline)
|
||||||
|
.ok_or("GRAPH_PIPELINE")?;
|
||||||
|
let color_formats = pass
|
||||||
|
.color
|
||||||
|
.iter()
|
||||||
|
.map(|attachment| attachment_format(graph, &attachment.resource, gpu.format).map(Some))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let depth_stencil = pass
|
||||||
|
.depth
|
||||||
|
.as_ref()
|
||||||
|
.map(|attachment| {
|
||||||
|
Ok::<_, String>(wgpu::RenderBundleDepthStencil {
|
||||||
|
format: attachment_format(graph, &attachment.resource, gpu.format)?,
|
||||||
|
depth_read_only: false,
|
||||||
|
stencil_read_only: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
let mut encoder =
|
||||||
|
gpu.device
|
||||||
|
.create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
|
||||||
|
label: Some(&pass.id),
|
||||||
|
color_formats: &color_formats,
|
||||||
|
depth_stencil,
|
||||||
|
sample_count: multisample(&declaration.multisample)?.count,
|
||||||
|
multiview: None,
|
||||||
|
});
|
||||||
|
encoder.set_pipeline(pipeline);
|
||||||
|
for (group, bind_group) in
|
||||||
|
self.bind_groups(pass, |group| pipeline.get_bind_group_layout(group), gpu)?
|
||||||
|
{
|
||||||
|
encoder.set_bind_group(group, &bind_group, &[]);
|
||||||
|
}
|
||||||
|
for binding in &pass.vertex_buffers {
|
||||||
|
let buffer = &self
|
||||||
|
.buffers
|
||||||
|
.get(&binding.resource)
|
||||||
|
.ok_or("GRAPH_RESOURCE_UNKNOWN")?
|
||||||
|
.buffer;
|
||||||
|
encoder.set_vertex_buffer(binding.slot, buffer.slice(binding.offset..));
|
||||||
|
}
|
||||||
|
if let Some(binding) = &pass.index_buffer {
|
||||||
|
let buffer = &self
|
||||||
|
.buffers
|
||||||
|
.get(&binding.resource)
|
||||||
|
.ok_or("GRAPH_RESOURCE_UNKNOWN")?
|
||||||
|
.buffer;
|
||||||
|
encoder.set_index_buffer(
|
||||||
|
buffer.slice(binding.offset..),
|
||||||
|
parse(&binding.format, "GRAPH_INDEX_FORMAT")?,
|
||||||
|
);
|
||||||
|
encoder.draw_indexed(
|
||||||
|
pass.draw.first_index..pass.draw.first_index + pass.draw.indices,
|
||||||
|
pass.draw.base_vertex,
|
||||||
|
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
encoder.draw(
|
||||||
|
pass.draw.first_vertex..pass.draw.first_vertex + pass.draw.vertices,
|
||||||
|
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(encoder.finish(&wgpu::RenderBundleDescriptor {
|
||||||
|
label: Some(&pass.id),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind_groups(
|
||||||
|
&self,
|
||||||
|
pass: &Pass,
|
||||||
|
layout: impl Fn(u32) -> wgpu::BindGroupLayout,
|
||||||
|
gpu: &Wgpu,
|
||||||
|
) -> Result<Vec<(u32, wgpu::BindGroup)>, String> {
|
||||||
|
let mut groups: BTreeMap<u32, Vec<&Binding>> = BTreeMap::new();
|
||||||
|
for binding in &pass.bindings {
|
||||||
|
groups.entry(binding.group).or_default().push(binding);
|
||||||
|
}
|
||||||
|
groups
|
||||||
|
.into_iter()
|
||||||
|
.map(|(group, bindings)| {
|
||||||
|
let entries = bindings
|
||||||
|
.into_iter()
|
||||||
|
.map(|binding| {
|
||||||
|
let resource = if let Some(buffer) = self.buffers.get(&binding.resource) {
|
||||||
|
wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||||
|
buffer: &buffer.buffer,
|
||||||
|
offset: binding.offset,
|
||||||
|
size: binding.size.and_then(NonZeroU64::new),
|
||||||
|
})
|
||||||
|
} else if let Some(view) = self.texture_view(&binding.resource) {
|
||||||
|
wgpu::BindingResource::TextureView(view)
|
||||||
|
} else if let Some(sampler) = self.samplers.get(&binding.resource) {
|
||||||
|
wgpu::BindingResource::Sampler(sampler)
|
||||||
|
} else {
|
||||||
|
return Err("GRAPH_RESOURCE_UNKNOWN".into());
|
||||||
|
};
|
||||||
|
Ok(wgpu::BindGroupEntry {
|
||||||
|
binding: binding.binding,
|
||||||
|
resource,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, String>>()?;
|
||||||
|
let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some(&pass.id),
|
||||||
|
layout: &layout(group),
|
||||||
|
entries: &entries,
|
||||||
|
});
|
||||||
|
Ok((group, bind_group))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_render_pipeline(
|
||||||
|
source: &RenderPipeline,
|
||||||
|
gpu: &Wgpu,
|
||||||
|
) -> Result<wgpu::RenderPipeline, String> {
|
||||||
|
let module = gpu
|
||||||
|
.device
|
||||||
|
.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some(&source.id),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(source.code.clone().into()),
|
||||||
|
});
|
||||||
|
let attributes = source
|
||||||
|
.vertex
|
||||||
|
.buffers
|
||||||
|
.iter()
|
||||||
|
.map(|buffer| {
|
||||||
|
buffer
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.map(|attribute| {
|
||||||
|
Ok(wgpu::VertexAttribute {
|
||||||
|
format: parse(&attribute.format, "GRAPH_VERTEX_FORMAT")?,
|
||||||
|
offset: attribute.offset,
|
||||||
|
shader_location: attribute.shader_location,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, String>>()
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let layouts = source
|
||||||
|
.vertex
|
||||||
|
.buffers
|
||||||
|
.iter()
|
||||||
|
.zip(&attributes)
|
||||||
|
.map(|(buffer, attributes)| {
|
||||||
|
Ok(wgpu::VertexBufferLayout {
|
||||||
|
array_stride: buffer.array_stride,
|
||||||
|
step_mode: parse(&buffer.step_mode, "GRAPH_VERTEX_STEP")?,
|
||||||
|
attributes,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, String>>()?;
|
||||||
|
let targets = source
|
||||||
|
.fragment
|
||||||
|
.targets
|
||||||
|
.iter()
|
||||||
|
.map(|target| {
|
||||||
|
let format = if target.format == "canvas" {
|
||||||
|
gpu.format
|
||||||
|
} else {
|
||||||
|
parse(&target.format, "GRAPH_TEXTURE_FORMAT")?
|
||||||
|
};
|
||||||
|
let blend = (!target.blend.is_null())
|
||||||
|
.then(|| {
|
||||||
|
serde_json::from_value::<wgpu::BlendState>(target.blend.clone())
|
||||||
|
.map_err(|_| String::from("GRAPH_BLEND"))
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
let write_mask = match target.write_mask {
|
||||||
|
Some(bits) => wgpu::ColorWrites::from_bits(bits).ok_or("GRAPH_WRITE_MASK")?,
|
||||||
|
None => wgpu::ColorWrites::ALL,
|
||||||
|
};
|
||||||
|
Ok(Some(wgpu::ColorTargetState {
|
||||||
|
format,
|
||||||
|
blend,
|
||||||
|
write_mask,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, String>>()?;
|
||||||
|
Ok(gpu
|
||||||
|
.device
|
||||||
|
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some(&source.id),
|
||||||
|
layout: None,
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &module,
|
||||||
|
entry_point: Some(&source.vertex.entry),
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
buffers: &layouts,
|
||||||
|
},
|
||||||
|
primitive: primitive(&source.primitive)?,
|
||||||
|
depth_stencil: depth_stencil(&source.depth_stencil)?,
|
||||||
|
multisample: multisample(&source.multisample)?,
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &module,
|
||||||
|
entry_point: Some(&source.fragment.entry),
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
targets: &targets,
|
||||||
|
}),
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_usage(names: &[String]) -> Result<wgpu::BufferUsages, String> {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.try_fold(wgpu::BufferUsages::COPY_DST, |usage, name| {
|
||||||
|
Ok(usage
|
||||||
|
| match name.as_str() {
|
||||||
|
"uniform" => wgpu::BufferUsages::UNIFORM,
|
||||||
|
"storage" => wgpu::BufferUsages::STORAGE,
|
||||||
|
"vertex" => wgpu::BufferUsages::VERTEX,
|
||||||
|
"index" => wgpu::BufferUsages::INDEX,
|
||||||
|
"indirect" => wgpu::BufferUsages::INDIRECT,
|
||||||
|
"copySrc" => wgpu::BufferUsages::COPY_SRC,
|
||||||
|
_ => return Err("GRAPH_BUFFER_USAGE".into()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn texture_usage(names: &[String]) -> Result<wgpu::TextureUsages, String> {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.try_fold(wgpu::TextureUsages::empty(), |usage, name| {
|
||||||
|
Ok(usage
|
||||||
|
| match name.as_str() {
|
||||||
|
"render" => wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
"sampled" => wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
"storage" => wgpu::TextureUsages::STORAGE_BINDING,
|
||||||
|
"copySrc" => wgpu::TextureUsages::COPY_SRC,
|
||||||
|
"copyDst" => wgpu::TextureUsages::COPY_DST,
|
||||||
|
_ => return Err("GRAPH_TEXTURE_USAGE".into()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn texture_descriptor(
|
||||||
|
source: &Texture,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> Result<wgpu::TextureDescriptor<'_>, String> {
|
||||||
|
let value = |index, canvas| -> Result<u32, String> {
|
||||||
|
match source.size.get(index) {
|
||||||
|
Some(Extent::Pixels(value)) => Ok(*value),
|
||||||
|
Some(Extent::Canvas(value)) if value == "canvas" => Ok(canvas),
|
||||||
|
Some(Extent::Canvas(_)) => Err("GRAPH_TEXTURE_SIZE".into()),
|
||||||
|
None => Ok(canvas),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(wgpu::TextureDescriptor {
|
||||||
|
label: Some(&source.id),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: value(0, width)?,
|
||||||
|
height: value(1, height)?,
|
||||||
|
depth_or_array_layers: value(2, 1)?,
|
||||||
|
},
|
||||||
|
mip_level_count: source.mip_level_count,
|
||||||
|
sample_count: source.sample_count,
|
||||||
|
dimension: parse(&source.dimension, "GRAPH_TEXTURE_DIMENSION")?,
|
||||||
|
format: parse(&source.format, "GRAPH_TEXTURE_FORMAT")?,
|
||||||
|
usage: texture_usage(&source.usage)?,
|
||||||
|
view_formats: &[],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attachment_format(
|
||||||
|
graph: &RenderGraph,
|
||||||
|
id: &str,
|
||||||
|
surface: wgpu::TextureFormat,
|
||||||
|
) -> Result<wgpu::TextureFormat, String> {
|
||||||
|
if id == "canvas" {
|
||||||
|
return Ok(surface);
|
||||||
|
}
|
||||||
|
graph
|
||||||
|
.resources
|
||||||
|
.textures
|
||||||
|
.iter()
|
||||||
|
.find(|texture| texture.id == id)
|
||||||
|
.ok_or_else(|| "GRAPH_ATTACHMENT".into())
|
||||||
|
.and_then(|texture| parse(&texture.format, "GRAPH_TEXTURE_FORMAT"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primitive(value: &Value) -> Result<wgpu::PrimitiveState, String> {
|
||||||
|
if value.is_null() {
|
||||||
|
return Ok(Default::default());
|
||||||
|
}
|
||||||
|
serde_json::from_value(value.clone()).map_err(|_| "GRAPH_PRIMITIVE".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depth_stencil(value: &Value) -> Result<Option<wgpu::DepthStencilState>, String> {
|
||||||
|
if value.is_null() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
serde_json::from_value(value.clone())
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|_| "GRAPH_DEPTH_STENCIL".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn multisample(value: &Value) -> Result<wgpu::MultisampleState, String> {
|
||||||
|
if value.is_null() {
|
||||||
|
return Ok(Default::default());
|
||||||
|
}
|
||||||
|
let object = value.as_object().ok_or("GRAPH_MULTISAMPLE")?;
|
||||||
|
Ok(wgpu::MultisampleState {
|
||||||
|
count: object.get("count").and_then(Value::as_u64).unwrap_or(1) as u32,
|
||||||
|
mask: object
|
||||||
|
.get("mask")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.unwrap_or(u64::MAX),
|
||||||
|
alpha_to_coverage_enabled: object
|
||||||
|
.get("alphaToCoverageEnabled")
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(false),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sampler_descriptor<'a>(
|
||||||
|
label: &'a str,
|
||||||
|
value: &Value,
|
||||||
|
) -> Result<wgpu::SamplerDescriptor<'a>, String> {
|
||||||
|
let mut descriptor = wgpu::SamplerDescriptor {
|
||||||
|
label: Some(label),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let Some(object) = value.as_object() else {
|
||||||
|
return Ok(descriptor);
|
||||||
|
};
|
||||||
|
macro_rules! enum_field {
|
||||||
|
($json:literal, $field:ident, $code:literal) => {
|
||||||
|
if let Some(value) = object.get($json).and_then(Value::as_str) {
|
||||||
|
descriptor.$field = parse(value, $code)?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
enum_field!("addressModeU", address_mode_u, "GRAPH_SAMPLER");
|
||||||
|
enum_field!("addressModeV", address_mode_v, "GRAPH_SAMPLER");
|
||||||
|
enum_field!("addressModeW", address_mode_w, "GRAPH_SAMPLER");
|
||||||
|
enum_field!("magFilter", mag_filter, "GRAPH_SAMPLER");
|
||||||
|
enum_field!("minFilter", min_filter, "GRAPH_SAMPLER");
|
||||||
|
enum_field!("mipmapFilter", mipmap_filter, "GRAPH_SAMPLER");
|
||||||
|
descriptor.lod_min_clamp = object
|
||||||
|
.get("lodMinClamp")
|
||||||
|
.and_then(Value::as_f64)
|
||||||
|
.unwrap_or(0.0) as f32;
|
||||||
|
descriptor.lod_max_clamp = object
|
||||||
|
.get("lodMaxClamp")
|
||||||
|
.and_then(Value::as_f64)
|
||||||
|
.unwrap_or(32.0) as f32;
|
||||||
|
descriptor.compare = object
|
||||||
|
.get("compare")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(|value| parse(value, "GRAPH_SAMPLER"))
|
||||||
|
.transpose()?;
|
||||||
|
descriptor.anisotropy_clamp = object
|
||||||
|
.get("anisotropyClamp")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.unwrap_or(1) as u16;
|
||||||
|
Ok(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse<T: DeserializeOwned>(value: &str, code: &str) -> Result<T, String> {
|
||||||
|
serde_json::from_value(Value::String(value.into())).map_err(|_| code.into())
|
||||||
|
}
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RenderGraph {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub resources: ResourceDeclarations,
|
||||||
|
#[serde(default)]
|
||||||
|
pub pipelines: PipelineDeclarations,
|
||||||
|
pub passes: Vec<Pass>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Deserialize)]
|
||||||
|
pub struct ResourceDeclarations {
|
||||||
|
#[serde(default)]
|
||||||
|
pub buffers: Vec<Buffer>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub textures: Vec<Texture>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub samplers: Vec<Sampler>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct Buffer {
|
||||||
|
pub id: String,
|
||||||
|
pub array: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub usage: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Texture {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub size: Vec<Extent>,
|
||||||
|
pub format: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub usage: Vec<String>,
|
||||||
|
#[serde(default = "one")]
|
||||||
|
pub mip_level_count: u32,
|
||||||
|
#[serde(default = "one")]
|
||||||
|
pub sample_count: u32,
|
||||||
|
#[serde(default = "dimension")]
|
||||||
|
pub dimension: String,
|
||||||
|
#[serde(default = "yes")]
|
||||||
|
pub transient: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub slot: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Extent {
|
||||||
|
Pixels(u32),
|
||||||
|
Canvas(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct Sampler {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub descriptor: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Deserialize)]
|
||||||
|
pub struct PipelineDeclarations {
|
||||||
|
#[serde(default)]
|
||||||
|
pub render: Vec<RenderPipeline>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub compute: Vec<ComputePipeline>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RenderPipeline {
|
||||||
|
pub id: String,
|
||||||
|
pub code: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub vertex: VertexStage,
|
||||||
|
#[serde(default)]
|
||||||
|
pub fragment: FragmentStage,
|
||||||
|
#[serde(default)]
|
||||||
|
pub primitive: Value,
|
||||||
|
#[serde(default)]
|
||||||
|
pub depth_stencil: Value,
|
||||||
|
#[serde(default)]
|
||||||
|
pub multisample: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct ComputePipeline {
|
||||||
|
pub id: String,
|
||||||
|
pub code: String,
|
||||||
|
#[serde(default = "compute_entry")]
|
||||||
|
pub entry: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct VertexStage {
|
||||||
|
#[serde(default = "vertex_entry")]
|
||||||
|
pub entry: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub buffers: Vec<VertexBuffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VertexStage {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
entry: vertex_entry(),
|
||||||
|
buffers: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VertexBuffer {
|
||||||
|
pub array_stride: u64,
|
||||||
|
#[serde(default = "vertex_step")]
|
||||||
|
pub step_mode: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub attributes: Vec<VertexAttribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VertexAttribute {
|
||||||
|
pub format: String,
|
||||||
|
pub offset: u64,
|
||||||
|
pub shader_location: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct FragmentStage {
|
||||||
|
#[serde(default = "fragment_entry")]
|
||||||
|
pub entry: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub targets: Vec<FragmentTarget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FragmentStage {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
entry: fragment_entry(),
|
||||||
|
targets: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FragmentTarget {
|
||||||
|
pub format: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub blend: Value,
|
||||||
|
pub write_mask: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Pass {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub kind: String,
|
||||||
|
pub pipeline: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub after: Vec<String>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub dependencies: Vec<usize>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub bindings: Vec<Binding>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub color: Vec<ColorAttachment>,
|
||||||
|
pub depth: Option<DepthAttachment>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub vertex_buffers: Vec<VertexBinding>,
|
||||||
|
pub index_buffer: Option<IndexBinding>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub draw: Draw,
|
||||||
|
#[serde(default = "dispatch")]
|
||||||
|
pub dispatch: [u32; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct Binding {
|
||||||
|
#[serde(default)]
|
||||||
|
pub group: u32,
|
||||||
|
pub binding: u32,
|
||||||
|
pub resource: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub offset: u64,
|
||||||
|
pub size: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct ColorAttachment {
|
||||||
|
pub resource: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub clear: Vec<f32>,
|
||||||
|
#[serde(default = "clear_op")]
|
||||||
|
pub load: String,
|
||||||
|
#[serde(default = "store_op")]
|
||||||
|
pub store: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct DepthAttachment {
|
||||||
|
pub resource: String,
|
||||||
|
#[serde(default = "one_f32")]
|
||||||
|
pub clear: f32,
|
||||||
|
#[serde(default = "clear_op")]
|
||||||
|
pub load: String,
|
||||||
|
#[serde(default = "store_op")]
|
||||||
|
pub store: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct VertexBinding {
|
||||||
|
#[serde(default)]
|
||||||
|
pub slot: u32,
|
||||||
|
pub resource: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub offset: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct IndexBinding {
|
||||||
|
pub resource: String,
|
||||||
|
#[serde(default = "index_format")]
|
||||||
|
pub format: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub offset: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Draw {
|
||||||
|
#[serde(default = "three")]
|
||||||
|
pub vertices: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub indices: u32,
|
||||||
|
#[serde(default = "one")]
|
||||||
|
pub instances: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub first_vertex: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub first_index: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub base_vertex: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub first_instance: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Draw {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
vertices: 3,
|
||||||
|
indices: 0,
|
||||||
|
instances: 1,
|
||||||
|
first_vertex: 0,
|
||||||
|
first_index: 0,
|
||||||
|
base_vertex: 0,
|
||||||
|
first_instance: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderGraph {
|
||||||
|
pub fn prepare(&mut self) -> Result<(), &'static str> {
|
||||||
|
if self.id.is_empty() || self.passes.is_empty() {
|
||||||
|
return Err("GRAPH_SHAPE");
|
||||||
|
}
|
||||||
|
let mut ids = HashMap::new();
|
||||||
|
for (index, pass) in self.passes.iter().enumerate() {
|
||||||
|
if pass.id.is_empty() || ids.insert(pass.id.clone(), index).is_some() {
|
||||||
|
return Err("GRAPH_PASS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let dependencies = self
|
||||||
|
.passes
|
||||||
|
.iter()
|
||||||
|
.map(|pass| {
|
||||||
|
pass.after
|
||||||
|
.iter()
|
||||||
|
.map(|id| ids.get(id).copied().ok_or("GRAPH_DEPENDENCY"))
|
||||||
|
.collect::<Result<HashSet<_>, _>>()
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let mut emitted = vec![false; self.passes.len()];
|
||||||
|
let mut order = Vec::with_capacity(self.passes.len());
|
||||||
|
while order.len() < self.passes.len() {
|
||||||
|
let ready = (0..self.passes.len()).find(|&index| {
|
||||||
|
!emitted[index]
|
||||||
|
&& dependencies[index]
|
||||||
|
.iter()
|
||||||
|
.all(|dependency| emitted[*dependency])
|
||||||
|
});
|
||||||
|
let index = ready.ok_or("GRAPH_CYCLE")?;
|
||||||
|
emitted[index] = true;
|
||||||
|
order.push(index);
|
||||||
|
}
|
||||||
|
let original = self.passes.clone();
|
||||||
|
self.passes = order
|
||||||
|
.into_iter()
|
||||||
|
.map(|index| original[index].clone())
|
||||||
|
.collect();
|
||||||
|
let sorted_ids: HashMap<_, _> = self
|
||||||
|
.passes
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, pass)| (pass.id.clone(), index))
|
||||||
|
.collect();
|
||||||
|
for pass in &mut self.passes {
|
||||||
|
pass.dependencies = pass.after.iter().map(|id| sorted_ids[id]).collect();
|
||||||
|
}
|
||||||
|
self.plan_resources()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plan_resources(&mut self) -> Result<(), &'static str> {
|
||||||
|
let mut used = HashSet::new();
|
||||||
|
let mut lifetimes = HashMap::new();
|
||||||
|
let mut render_pipelines = HashSet::new();
|
||||||
|
let mut compute_pipelines = HashSet::new();
|
||||||
|
for (frame, pass) in self.passes.iter().enumerate() {
|
||||||
|
match pass.kind.as_str() {
|
||||||
|
"render" => _ = render_pipelines.insert(pass.pipeline.clone()),
|
||||||
|
"compute" => _ = compute_pipelines.insert(pass.pipeline.clone()),
|
||||||
|
_ => return Err("GRAPH_PASS"),
|
||||||
|
}
|
||||||
|
for id in pass.resources() {
|
||||||
|
used.insert(id.to_owned());
|
||||||
|
lifetimes
|
||||||
|
.entry(id.to_owned())
|
||||||
|
.and_modify(|range: &mut (usize, usize)| range.1 = frame)
|
||||||
|
.or_insert((frame, frame));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.resources
|
||||||
|
.buffers
|
||||||
|
.retain(|value| used.contains(&value.id));
|
||||||
|
self.resources
|
||||||
|
.samplers
|
||||||
|
.retain(|value| used.contains(&value.id));
|
||||||
|
self.pipelines
|
||||||
|
.render
|
||||||
|
.retain(|value| render_pipelines.contains(&value.id));
|
||||||
|
self.pipelines
|
||||||
|
.compute
|
||||||
|
.retain(|value| compute_pipelines.contains(&value.id));
|
||||||
|
|
||||||
|
let mut textures = self
|
||||||
|
.resources
|
||||||
|
.textures
|
||||||
|
.drain(..)
|
||||||
|
.filter_map(|texture| {
|
||||||
|
lifetimes
|
||||||
|
.get(&texture.id)
|
||||||
|
.copied()
|
||||||
|
.map(|range| (texture, range))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
textures.sort_by_key(|value| value.1 .0);
|
||||||
|
let mut slots: Vec<(String, usize, bool)> = Vec::new();
|
||||||
|
for (texture, (first, last)) in &mut textures {
|
||||||
|
let key = texture.key()?;
|
||||||
|
let reusable = texture
|
||||||
|
.transient
|
||||||
|
.then(|| {
|
||||||
|
slots
|
||||||
|
.iter()
|
||||||
|
.position(|slot| slot.2 && slot.0 == key && slot.1 < *first)
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
texture.slot = match reusable {
|
||||||
|
Some(slot) => {
|
||||||
|
slots[slot].1 = *last;
|
||||||
|
slot
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
slots.push((key, *last, texture.transient));
|
||||||
|
slots.len() - 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.resources.textures = textures.into_iter().map(|value| value.0).collect();
|
||||||
|
self.validate_ids()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_ids(&self) -> Result<(), &'static str> {
|
||||||
|
let mut resources = HashSet::new();
|
||||||
|
for id in self
|
||||||
|
.resources
|
||||||
|
.buffers
|
||||||
|
.iter()
|
||||||
|
.map(|value| &value.id)
|
||||||
|
.chain(self.resources.textures.iter().map(|value| &value.id))
|
||||||
|
.chain(self.resources.samplers.iter().map(|value| &value.id))
|
||||||
|
{
|
||||||
|
if id.is_empty() || !resources.insert(id) {
|
||||||
|
return Err("GRAPH_RESOURCE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut pipelines = HashSet::new();
|
||||||
|
for id in self
|
||||||
|
.pipelines
|
||||||
|
.render
|
||||||
|
.iter()
|
||||||
|
.map(|value| &value.id)
|
||||||
|
.chain(self.pipelines.compute.iter().map(|value| &value.id))
|
||||||
|
{
|
||||||
|
if id.is_empty() || !pipelines.insert(id) {
|
||||||
|
return Err("GRAPH_PIPELINE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pass {
|
||||||
|
fn resources(&self) -> Vec<&str> {
|
||||||
|
self.bindings
|
||||||
|
.iter()
|
||||||
|
.map(|value| value.resource.as_str())
|
||||||
|
.chain(self.color.iter().map(|value| value.resource.as_str()))
|
||||||
|
.chain(self.depth.iter().map(|value| value.resource.as_str()))
|
||||||
|
.chain(
|
||||||
|
self.vertex_buffers
|
||||||
|
.iter()
|
||||||
|
.map(|value| value.resource.as_str()),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
self.index_buffer
|
||||||
|
.iter()
|
||||||
|
.map(|value| value.resource.as_str()),
|
||||||
|
)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Texture {
|
||||||
|
fn key(&self) -> Result<String, &'static str> {
|
||||||
|
let mut usage = self.usage.clone();
|
||||||
|
usage.sort_unstable();
|
||||||
|
usage.dedup();
|
||||||
|
serde_json::to_string(&(
|
||||||
|
&self.size,
|
||||||
|
&self.format,
|
||||||
|
usage,
|
||||||
|
self.mip_level_count,
|
||||||
|
self.sample_count,
|
||||||
|
&self.dimension,
|
||||||
|
))
|
||||||
|
.map_err(|_| "GRAPH_RESOURCE")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn one() -> u32 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
fn three() -> u32 {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
fn one_f32() -> f32 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
fn yes() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn dimension() -> String {
|
||||||
|
"2d".into()
|
||||||
|
}
|
||||||
|
fn vertex_entry() -> String {
|
||||||
|
"vertex".into()
|
||||||
|
}
|
||||||
|
fn fragment_entry() -> String {
|
||||||
|
"fragment".into()
|
||||||
|
}
|
||||||
|
fn compute_entry() -> String {
|
||||||
|
"main".into()
|
||||||
|
}
|
||||||
|
fn vertex_step() -> String {
|
||||||
|
"vertex".into()
|
||||||
|
}
|
||||||
|
fn clear_op() -> String {
|
||||||
|
"clear".into()
|
||||||
|
}
|
||||||
|
fn store_op() -> String {
|
||||||
|
"store".into()
|
||||||
|
}
|
||||||
|
fn index_format() -> String {
|
||||||
|
"uint32".into()
|
||||||
|
}
|
||||||
|
fn dispatch() -> [u32; 3] {
|
||||||
|
[1, 1, 1]
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::gpu::Wgpu;
|
||||||
|
use crate::gpu_resource::GpuResources;
|
||||||
|
use crate::graph::RenderGraph;
|
||||||
|
use crate::render_data::RenderData;
|
||||||
|
|
||||||
|
pub struct Loadout {
|
||||||
|
pub graph: RenderGraph,
|
||||||
|
pub resources: GpuResources,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Store {
|
||||||
|
graphs: HashMap<String, RenderGraph>,
|
||||||
|
active: Option<Loadout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
pub fn save(&mut self, graph: RenderGraph) -> String {
|
||||||
|
let id = graph.id.clone();
|
||||||
|
self.graphs.insert(id.clone(), graph);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch(&mut self, id: &str, gpu: &Wgpu, data: &RenderData) -> Result<(), String> {
|
||||||
|
let graph = self.graphs.get(id).ok_or("GRAPH_UNKNOWN")?.clone();
|
||||||
|
let resources = GpuResources::activate(&graph, gpu, data)?;
|
||||||
|
self.active = Some(Loadout { graph, resources });
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active_mut(&mut self) -> Option<&mut Loadout> {
|
||||||
|
self.active.as_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uses_rows(&self, name: &str) -> bool {
|
||||||
|
self.active.as_ref().is_some_and(|active| {
|
||||||
|
active
|
||||||
|
.graph
|
||||||
|
.resources
|
||||||
|
.buffers
|
||||||
|
.iter()
|
||||||
|
.any(|buffer| buffer.array == name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_rows(
|
||||||
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
gpu: &Wgpu,
|
||||||
|
data: &RenderData,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if !self.uses_rows(name) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let graph = self.active.as_ref().unwrap().graph.clone();
|
||||||
|
let resources = GpuResources::activate(&graph, gpu, data)?;
|
||||||
|
self.active = Some(Loadout { graph, resources });
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gloo_timers::future::TimeoutFuture;
|
||||||
|
|
||||||
|
use crate::gpu::Wgpu;
|
||||||
|
use crate::gpu_resource::GpuPass;
|
||||||
|
use crate::graph::{ColorAttachment, DepthAttachment};
|
||||||
|
use crate::render_data::RenderData;
|
||||||
|
use crate::store::{Loadout, Store};
|
||||||
|
|
||||||
|
pub struct RenderLoop {
|
||||||
|
playing: Cell<bool>,
|
||||||
|
fps: Cell<u32>,
|
||||||
|
started: Cell<bool>,
|
||||||
|
frame: Cell<u32>,
|
||||||
|
elapsed: Cell<f64>,
|
||||||
|
last: Cell<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderLoop {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
playing: Cell::new(true),
|
||||||
|
fps: Cell::new(60),
|
||||||
|
started: Cell::new(false),
|
||||||
|
frame: Cell::new(0),
|
||||||
|
elapsed: Cell::new(0.0),
|
||||||
|
last: Cell::new(js_sys::Date::now()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(&self) {
|
||||||
|
self.last.set(js_sys::Date::now());
|
||||||
|
self.playing.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pause(&self) {
|
||||||
|
self.playing.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_fps(&self, fps: u32) -> Result<(), &'static str> {
|
||||||
|
if !(1..=1000).contains(&fps) {
|
||||||
|
return Err("FPS");
|
||||||
|
}
|
||||||
|
self.fps.set(fps);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
gpu: Rc<RefCell<Option<Wgpu>>>,
|
||||||
|
store: Rc<RefCell<Store>>,
|
||||||
|
data: Rc<RefCell<RenderData>>,
|
||||||
|
) {
|
||||||
|
if self.started.replace(true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let control = self.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
loop {
|
||||||
|
let started = js_sys::Date::now();
|
||||||
|
if control.playing.get() {
|
||||||
|
let delta = (started - control.last.replace(started)) / 1000.0;
|
||||||
|
let elapsed = control.elapsed.get() + delta;
|
||||||
|
control.elapsed.set(elapsed);
|
||||||
|
let frame = control.frame.get().wrapping_add(1);
|
||||||
|
control.frame.set(frame);
|
||||||
|
let skip = {
|
||||||
|
let mut data = data.borrow_mut();
|
||||||
|
data.update_info(delta as f32, frame, elapsed as f32, control.fps.get());
|
||||||
|
data.skip_render()
|
||||||
|
};
|
||||||
|
if !skip {
|
||||||
|
if let (Some(gpu), Some(loadout)) =
|
||||||
|
(gpu.borrow_mut().as_mut(), store.borrow_mut().active_mut())
|
||||||
|
{
|
||||||
|
let _ = gpu.render(loadout, &data.borrow());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let target = 1000.0 / f64::from(control.fps.get());
|
||||||
|
let wait = (target - (js_sys::Date::now() - started)).max(0.0) as u32;
|
||||||
|
TimeoutFuture::new(if control.playing.get() { wait } else { 50 }).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wgpu {
|
||||||
|
fn render(&mut self, loadout: &mut Loadout, data: &RenderData) -> Result<(), String> {
|
||||||
|
for buffer in loadout.resources.buffers.values() {
|
||||||
|
self.queue.write_buffer(
|
||||||
|
&buffer.buffer,
|
||||||
|
0,
|
||||||
|
data.bytes(&buffer.source).ok_or("ROWS_UNKNOWN")?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let output = match self.surface.get_current_texture() {
|
||||||
|
Ok(output) => output,
|
||||||
|
Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
self.surface.get_current_texture().map_err(|_| "SURFACE")?
|
||||||
|
}
|
||||||
|
Err(wgpu::SurfaceError::Timeout) => return Ok(()),
|
||||||
|
Err(_) => return Err("SURFACE".into()),
|
||||||
|
};
|
||||||
|
let surface_view = output
|
||||||
|
.texture
|
||||||
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let mut encoder = self
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("frame"),
|
||||||
|
});
|
||||||
|
for (pass, compiled) in loadout.graph.passes.iter().zip(&loadout.resources.passes) {
|
||||||
|
match compiled {
|
||||||
|
GpuPass::Compute {
|
||||||
|
pipeline,
|
||||||
|
bind_groups,
|
||||||
|
} => {
|
||||||
|
let mut command = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||||
|
label: Some(&pass.id),
|
||||||
|
timestamp_writes: None,
|
||||||
|
});
|
||||||
|
command.set_pipeline(pipeline);
|
||||||
|
for (group, bind_group) in bind_groups {
|
||||||
|
command.set_bind_group(*group, bind_group, &[]);
|
||||||
|
}
|
||||||
|
command.dispatch_workgroups(
|
||||||
|
pass.dispatch[0],
|
||||||
|
pass.dispatch[1],
|
||||||
|
pass.dispatch[2],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
GpuPass::Render(bundle) => {
|
||||||
|
let colors = pass
|
||||||
|
.color
|
||||||
|
.iter()
|
||||||
|
.map(|attachment| {
|
||||||
|
Ok(Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: view(
|
||||||
|
&loadout.resources,
|
||||||
|
&surface_view,
|
||||||
|
&attachment.resource,
|
||||||
|
)?,
|
||||||
|
depth_slice: None,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: color_ops(attachment)?,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, String>>()?;
|
||||||
|
let depth = pass
|
||||||
|
.depth
|
||||||
|
.as_ref()
|
||||||
|
.map(|attachment| {
|
||||||
|
Ok::<_, String>(wgpu::RenderPassDepthStencilAttachment {
|
||||||
|
view: view(
|
||||||
|
&loadout.resources,
|
||||||
|
&surface_view,
|
||||||
|
&attachment.resource,
|
||||||
|
)?,
|
||||||
|
depth_ops: Some(depth_ops(attachment)?),
|
||||||
|
stencil_ops: None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
let mut command = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some(&pass.id),
|
||||||
|
color_attachments: &colors,
|
||||||
|
depth_stencil_attachment: depth,
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
});
|
||||||
|
command.execute_bundles([bundle]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.queue.submit([encoder.finish()]);
|
||||||
|
output.present();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view<'a>(
|
||||||
|
resources: &'a crate::gpu_resource::GpuResources,
|
||||||
|
surface: &'a wgpu::TextureView,
|
||||||
|
id: &str,
|
||||||
|
) -> Result<&'a wgpu::TextureView, String> {
|
||||||
|
if id == "canvas" {
|
||||||
|
Ok(surface)
|
||||||
|
} else {
|
||||||
|
resources
|
||||||
|
.texture_view(id)
|
||||||
|
.ok_or_else(|| "GRAPH_ATTACHMENT".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color_ops(attachment: &ColorAttachment) -> Result<wgpu::Operations<wgpu::Color>, String> {
|
||||||
|
let clear = attachment.clear.as_slice();
|
||||||
|
Ok(wgpu::Operations {
|
||||||
|
load: match attachment.load.as_str() {
|
||||||
|
"load" => wgpu::LoadOp::Load,
|
||||||
|
"clear" => wgpu::LoadOp::Clear(wgpu::Color {
|
||||||
|
r: f64::from(clear.first().copied().unwrap_or(0.0)),
|
||||||
|
g: f64::from(clear.get(1).copied().unwrap_or(0.0)),
|
||||||
|
b: f64::from(clear.get(2).copied().unwrap_or(0.0)),
|
||||||
|
a: f64::from(clear.get(3).copied().unwrap_or(1.0)),
|
||||||
|
}),
|
||||||
|
_ => return Err("GRAPH_LOAD_OP".into()),
|
||||||
|
},
|
||||||
|
store: store_op(&attachment.store)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depth_ops(attachment: &DepthAttachment) -> Result<wgpu::Operations<f32>, String> {
|
||||||
|
Ok(wgpu::Operations {
|
||||||
|
load: match attachment.load.as_str() {
|
||||||
|
"load" => wgpu::LoadOp::Load,
|
||||||
|
"clear" => wgpu::LoadOp::Clear(attachment.clear),
|
||||||
|
_ => return Err("GRAPH_LOAD_OP".into()),
|
||||||
|
},
|
||||||
|
store: store_op(&attachment.store)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store_op(value: &str) -> Result<wgpu::StoreOp, String> {
|
||||||
|
match value {
|
||||||
|
"store" => Ok(wgpu::StoreOp::Store),
|
||||||
|
"discard" => Ok(wgpu::StoreOp::Discard),
|
||||||
|
_ => Err("GRAPH_STORE_OP".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
pub struct Wgpu {
|
||||||
|
_instance: wgpu::Instance,
|
||||||
|
pub surface: wgpu::Surface<'static>,
|
||||||
|
pub device: wgpu::Device,
|
||||||
|
pub queue: wgpu::Queue,
|
||||||
|
pub config: wgpu::SurfaceConfiguration,
|
||||||
|
pub format: wgpu::TextureFormat,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wgpu {
|
||||||
|
pub async fn new(canvas: web_sys::OffscreenCanvas) -> Result<Self, String> {
|
||||||
|
let width = canvas.width();
|
||||||
|
let height = canvas.height();
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
return Err("CANVAS_SIZE".into());
|
||||||
|
}
|
||||||
|
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||||
|
backends: wgpu::Backends::BROWSER_WEBGPU,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let surface = instance
|
||||||
|
.create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas))
|
||||||
|
.map_err(|_| "SURFACE")?;
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| "WEBGPU_UNAVAILABLE")?;
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(&wgpu::DeviceDescriptor::default())
|
||||||
|
.await
|
||||||
|
.map_err(|_| "DEVICE")?;
|
||||||
|
let config = surface
|
||||||
|
.get_default_config(&adapter, width, height)
|
||||||
|
.ok_or("SURFACE")?;
|
||||||
|
let format = config.format;
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
Ok(Self {
|
||||||
|
_instance: instance,
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
config,
|
||||||
|
format,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
-424
@@ -1,424 +0,0 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
use serde_json::{Map, Number, Value};
|
|
||||||
use wasm_bindgen::prelude::*;
|
|
||||||
|
|
||||||
const ALIGNMENT: u32 = 64;
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub struct Core {
|
|
||||||
_arena: Box<[u8]>,
|
|
||||||
base: u32,
|
|
||||||
capacity: u32,
|
|
||||||
used: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
impl Core {
|
|
||||||
#[wasm_bindgen(constructor)]
|
|
||||||
pub fn new(arena_bytes: u32) -> Result<Self, JsError> {
|
|
||||||
if arena_bytes < ALIGNMENT || arena_bytes > u32::MAX - (ALIGNMENT - 1) {
|
|
||||||
return Err(JsError::new("INIT"));
|
|
||||||
}
|
|
||||||
let mut arena = vec![0; (arena_bytes + ALIGNMENT - 1) as usize].into_boxed_slice();
|
|
||||||
let pointer = arena.as_mut_ptr() as usize;
|
|
||||||
let base = ((pointer + ALIGNMENT as usize - 1) & !(ALIGNMENT as usize - 1)) as u32;
|
|
||||||
Ok(Self {
|
|
||||||
_arena: arena,
|
|
||||||
base,
|
|
||||||
capacity: arena_bytes,
|
|
||||||
used: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn allocate(&mut self, rows: u32, stride: u32, format: &str) -> Result<u32, JsError> {
|
|
||||||
if rows == 0 || stride < 16 || stride % 16 != 0 || !matches!(format, "f32" | "u32" | "i32")
|
|
||||||
{
|
|
||||||
return Err(JsError::new("ALLOCATION"));
|
|
||||||
}
|
|
||||||
let offset = align(self.used, ALIGNMENT).ok_or_else(|| JsError::new("ARENA_OOM"))?;
|
|
||||||
let bytes = rows
|
|
||||||
.checked_mul(stride)
|
|
||||||
.ok_or_else(|| JsError::new("ARENA_OOM"))?;
|
|
||||||
self.used = offset
|
|
||||||
.checked_add(bytes)
|
|
||||||
.filter(|end| *end <= self.capacity)
|
|
||||||
.ok_or_else(|| JsError::new("ARENA_OOM"))?;
|
|
||||||
Ok(self.base + offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compile_graph(&self, source: &str) -> Result<String, JsError> {
|
|
||||||
compile_graph(source).map_err(JsError::new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn align(value: u32, alignment: u32) -> Option<u32> {
|
|
||||||
value
|
|
||||||
.checked_add(alignment - 1)
|
|
||||||
.map(|value| value & !(alignment - 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Expression {
|
|
||||||
Atom(Value),
|
|
||||||
List(Vec<Expression>),
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Parser<'a> {
|
|
||||||
source: &'a [u8],
|
|
||||||
at: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Parser<'a> {
|
|
||||||
fn parse(source: &'a str) -> Result<Expression, &'static str> {
|
|
||||||
let mut parser = Parser {
|
|
||||||
source: source.as_bytes(),
|
|
||||||
at: 0,
|
|
||||||
};
|
|
||||||
let expression = parser.expression()?;
|
|
||||||
parser.whitespace();
|
|
||||||
if parser.at != parser.source.len() {
|
|
||||||
return Err("GRAPH_WIRE");
|
|
||||||
}
|
|
||||||
Ok(expression)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expression(&mut self) -> Result<Expression, &'static str> {
|
|
||||||
self.whitespace();
|
|
||||||
match self.source.get(self.at) {
|
|
||||||
Some(b'(') => self.list(),
|
|
||||||
Some(b'"') => self.string(),
|
|
||||||
Some(b')') | None => Err("GRAPH_WIRE"),
|
|
||||||
Some(_) => self.atom(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list(&mut self) -> Result<Expression, &'static str> {
|
|
||||||
self.at += 1;
|
|
||||||
let mut values = Vec::new();
|
|
||||||
loop {
|
|
||||||
self.whitespace();
|
|
||||||
match self.source.get(self.at) {
|
|
||||||
Some(b')') => {
|
|
||||||
self.at += 1;
|
|
||||||
return Ok(Expression::List(values));
|
|
||||||
}
|
|
||||||
None => return Err("GRAPH_WIRE"),
|
|
||||||
_ => values.push(self.expression()?),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn string(&mut self) -> Result<Expression, &'static str> {
|
|
||||||
let start = self.at;
|
|
||||||
self.at += 1;
|
|
||||||
let mut escaped = false;
|
|
||||||
while let Some(&byte) = self.source.get(self.at) {
|
|
||||||
self.at += 1;
|
|
||||||
if escaped {
|
|
||||||
escaped = false;
|
|
||||||
} else if byte == b'\\' {
|
|
||||||
escaped = true;
|
|
||||||
} else if byte == b'"' {
|
|
||||||
let value = serde_json::from_slice(&self.source[start..self.at])
|
|
||||||
.map_err(|_| "GRAPH_WIRE")?;
|
|
||||||
return Ok(Expression::Atom(Value::String(value)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err("GRAPH_WIRE")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn atom(&mut self) -> Result<Expression, &'static str> {
|
|
||||||
let start = self.at;
|
|
||||||
while self
|
|
||||||
.source
|
|
||||||
.get(self.at)
|
|
||||||
.is_some_and(|byte| !byte.is_ascii_whitespace() && !matches!(byte, b'(' | b')'))
|
|
||||||
{
|
|
||||||
self.at += 1;
|
|
||||||
}
|
|
||||||
let token = std::str::from_utf8(&self.source[start..self.at]).map_err(|_| "GRAPH_WIRE")?;
|
|
||||||
let value = match token {
|
|
||||||
"true" => Value::Bool(true),
|
|
||||||
"false" => Value::Bool(false),
|
|
||||||
"null" => Value::Null,
|
|
||||||
_ => serde_json::from_str::<Value>(token)
|
|
||||||
.ok()
|
|
||||||
.filter(Value::is_number)
|
|
||||||
.unwrap_or_else(|| Value::String(token.into())),
|
|
||||||
};
|
|
||||||
Ok(Expression::Atom(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn whitespace(&mut self) {
|
|
||||||
while self
|
|
||||||
.source
|
|
||||||
.get(self.at)
|
|
||||||
.is_some_and(u8::is_ascii_whitespace)
|
|
||||||
{
|
|
||||||
self.at += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compile_graph(source: &str) -> Result<String, &'static str> {
|
|
||||||
let root = Parser::parse(source)?;
|
|
||||||
let Expression::List(mut root) = root else {
|
|
||||||
return Err("GRAPH_WIRE");
|
|
||||||
};
|
|
||||||
if root.len() != 3
|
|
||||||
|| !matches!(&root[0], Expression::Atom(Value::String(tag)) if tag == "yawn-graph")
|
|
||||||
|| !matches!(&root[1], Expression::Atom(Value::Number(version)) if version.as_u64() == Some(1))
|
|
||||||
{
|
|
||||||
return Err("GRAPH_WIRE");
|
|
||||||
}
|
|
||||||
let mut graph = decode(root.pop().unwrap())?;
|
|
||||||
let object = graph.as_object_mut().ok_or("GRAPH_SHAPE")?;
|
|
||||||
if !object.get("id").is_some_and(Value::is_string) {
|
|
||||||
return Err("GRAPH_SHAPE");
|
|
||||||
}
|
|
||||||
let passes = sort_passes(object.get("passes").ok_or("GRAPH_PASS")?)?;
|
|
||||||
plan_resources(object, &passes)?;
|
|
||||||
object.insert("passes".into(), Value::Array(passes));
|
|
||||||
serde_json::to_string(&graph).map_err(|_| "GRAPH_WIRE")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode(expression: Expression) -> Result<Value, &'static str> {
|
|
||||||
let Expression::List(mut values) = expression else {
|
|
||||||
return match expression {
|
|
||||||
Expression::Atom(value) => Ok(value),
|
|
||||||
Expression::List(_) => unreachable!(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if values.is_empty() {
|
|
||||||
return Err("GRAPH_WIRE");
|
|
||||||
}
|
|
||||||
let tag = match values.remove(0) {
|
|
||||||
Expression::Atom(Value::String(tag)) => tag,
|
|
||||||
_ => return Err("GRAPH_WIRE"),
|
|
||||||
};
|
|
||||||
if tag == "array" {
|
|
||||||
return values.into_iter().map(decode).collect();
|
|
||||||
}
|
|
||||||
if tag != "object" {
|
|
||||||
return Err("GRAPH_WIRE");
|
|
||||||
}
|
|
||||||
let mut object = Map::new();
|
|
||||||
for field in values {
|
|
||||||
let Expression::List(mut field) = field else {
|
|
||||||
return Err("GRAPH_WIRE");
|
|
||||||
};
|
|
||||||
if field.len() != 3
|
|
||||||
|| !matches!(&field[0], Expression::Atom(Value::String(tag)) if tag == "field")
|
|
||||||
{
|
|
||||||
return Err("GRAPH_WIRE");
|
|
||||||
}
|
|
||||||
let value = decode(field.pop().unwrap())?;
|
|
||||||
let key = match field.pop().unwrap() {
|
|
||||||
Expression::Atom(Value::String(key)) => key,
|
|
||||||
_ => return Err("GRAPH_WIRE"),
|
|
||||||
};
|
|
||||||
if object.insert(key, value).is_some() {
|
|
||||||
return Err("GRAPH_WIRE");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Value::Object(object))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sort_passes(value: &Value) -> Result<Vec<Value>, &'static str> {
|
|
||||||
let passes = value.as_array().ok_or("GRAPH_PASS")?;
|
|
||||||
let mut ids = HashMap::new();
|
|
||||||
for (index, pass) in passes.iter().enumerate() {
|
|
||||||
let id = pass
|
|
||||||
.as_object()
|
|
||||||
.and_then(|pass| pass.get("id"))
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.ok_or("GRAPH_PASS")?;
|
|
||||||
if ids.insert(id, index).is_some() {
|
|
||||||
return Err("GRAPH_PASS");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut dependencies = Vec::with_capacity(passes.len());
|
|
||||||
for pass in passes {
|
|
||||||
let after = pass.get("after").map_or(Ok(&[][..]), |after| {
|
|
||||||
after.as_array().map(Vec::as_slice).ok_or("GRAPH_ARRAY")
|
|
||||||
})?;
|
|
||||||
dependencies.push(
|
|
||||||
after
|
|
||||||
.iter()
|
|
||||||
.map(|dependency| {
|
|
||||||
dependency
|
|
||||||
.as_str()
|
|
||||||
.and_then(|id| ids.get(id).copied())
|
|
||||||
.ok_or("GRAPH_DEPENDENCY")
|
|
||||||
})
|
|
||||||
.collect::<Result<HashSet<_>, _>>()?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let mut emitted = vec![false; passes.len()];
|
|
||||||
let mut result = Vec::with_capacity(passes.len());
|
|
||||||
while result.len() < passes.len() {
|
|
||||||
let ready = (0..passes.len()).find(|&index| {
|
|
||||||
!emitted[index]
|
|
||||||
&& dependencies[index]
|
|
||||||
.iter()
|
|
||||||
.all(|dependency| emitted[*dependency])
|
|
||||||
});
|
|
||||||
let index = ready.ok_or("GRAPH_CYCLE")?;
|
|
||||||
emitted[index] = true;
|
|
||||||
result.push(passes[index].clone());
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn plan_resources(graph: &mut Map<String, Value>, passes: &[Value]) -> Result<(), &'static str> {
|
|
||||||
let mut used = HashSet::new();
|
|
||||||
let mut lifetimes = HashMap::new();
|
|
||||||
for (frame, pass) in passes.iter().enumerate() {
|
|
||||||
for id in pass_resources(pass)? {
|
|
||||||
used.insert(id.to_owned());
|
|
||||||
lifetimes
|
|
||||||
.entry(id.to_owned())
|
|
||||||
.and_modify(|lifetime: &mut (usize, usize)| lifetime.1 = frame)
|
|
||||||
.or_insert((frame, frame));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let Some(resources) = graph.get_mut("resources") else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let resources = resources.as_object_mut().ok_or("GRAPH_RESOURCE")?;
|
|
||||||
for kind in ["buffers", "samplers"] {
|
|
||||||
if let Some(declarations) = resources.get_mut(kind) {
|
|
||||||
let declarations = declarations.as_array_mut().ok_or("GRAPH_RESOURCE")?;
|
|
||||||
let mut ids = HashSet::new();
|
|
||||||
declarations.retain(|declaration| {
|
|
||||||
declaration
|
|
||||||
.get("id")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.is_some_and(|id| ids.insert(id.to_owned()) && used.contains(id))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let Some(textures) = resources.get_mut("textures") else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let textures = textures.as_array_mut().ok_or("GRAPH_RESOURCE")?;
|
|
||||||
let mut ids = HashSet::new();
|
|
||||||
let mut planned = Vec::new();
|
|
||||||
for mut declaration in textures.drain(..) {
|
|
||||||
let object = declaration.as_object_mut().ok_or("GRAPH_RESOURCE")?;
|
|
||||||
let id = object
|
|
||||||
.get("id")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.ok_or("GRAPH_RESOURCE")?;
|
|
||||||
if !ids.insert(id.to_owned()) {
|
|
||||||
return Err("GRAPH_RESOURCE");
|
|
||||||
}
|
|
||||||
let Some(&(first, last)) = lifetimes.get(id) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
planned.push((declaration, first, last));
|
|
||||||
}
|
|
||||||
planned.sort_by_key(|value| value.1);
|
|
||||||
let mut output = Vec::new();
|
|
||||||
let mut slots: Vec<(String, usize)> = Vec::new();
|
|
||||||
for (mut declaration, first, last) in planned {
|
|
||||||
let object = declaration.as_object_mut().unwrap();
|
|
||||||
let key = texture_key(object)?;
|
|
||||||
let slot = if object.get("transient") == Some(&Value::Bool(false)) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
slots
|
|
||||||
.iter()
|
|
||||||
.position(|value| value.0 == key && value.1 < first)
|
|
||||||
};
|
|
||||||
let slot = match slot {
|
|
||||||
Some(slot) => {
|
|
||||||
slots[slot].1 = last;
|
|
||||||
slot
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
slots.push((key, last));
|
|
||||||
slots.len() - 1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
object.insert("slot".into(), Value::Number(Number::from(slot as u64)));
|
|
||||||
output.push(declaration);
|
|
||||||
}
|
|
||||||
*textures = output;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pass_resources(pass: &Value) -> Result<Vec<&str>, &'static str> {
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for field in ["bindings", "color", "vertexBuffers"] {
|
|
||||||
if let Some(values) = pass.get(field) {
|
|
||||||
for value in values.as_array().ok_or("GRAPH_ARRAY")? {
|
|
||||||
result.push(
|
|
||||||
value
|
|
||||||
.get("resource")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.ok_or("GRAPH_RESOURCE")?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for field in ["depth", "indexBuffer"] {
|
|
||||||
if let Some(value) = pass.get(field) {
|
|
||||||
result.push(
|
|
||||||
value
|
|
||||||
.get("resource")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.ok_or("GRAPH_RESOURCE")?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn texture_key(texture: &Map<String, Value>) -> Result<String, &'static str> {
|
|
||||||
let mut size = texture.get("size").cloned().unwrap_or_else(|| {
|
|
||||||
Value::Array(vec![
|
|
||||||
Value::String("canvas".into()),
|
|
||||||
Value::String("canvas".into()),
|
|
||||||
])
|
|
||||||
});
|
|
||||||
let size = size.as_array_mut().ok_or("GRAPH_TEXTURE_SIZE")?;
|
|
||||||
if !(2..=3).contains(&size.len()) {
|
|
||||||
return Err("GRAPH_TEXTURE_SIZE");
|
|
||||||
}
|
|
||||||
if size.len() == 2 {
|
|
||||||
size.push(Value::Number(Number::from(1)));
|
|
||||||
}
|
|
||||||
let format = texture
|
|
||||||
.get("format")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.ok_or("GRAPH_RESOURCE")?;
|
|
||||||
let mut usage = texture.get("usage").map_or(Ok(Vec::new()), |usage| {
|
|
||||||
usage
|
|
||||||
.as_array()
|
|
||||||
.ok_or("GRAPH_TEXTURE_USAGE")?
|
|
||||||
.iter()
|
|
||||||
.map(|value| value.as_str().ok_or("GRAPH_TEXTURE_USAGE"))
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
})?;
|
|
||||||
usage.sort_unstable();
|
|
||||||
usage.dedup();
|
|
||||||
serde_json::to_string(&(
|
|
||||||
size,
|
|
||||||
format,
|
|
||||||
usage,
|
|
||||||
texture
|
|
||||||
.get("mipLevelCount")
|
|
||||||
.and_then(Value::as_u64)
|
|
||||||
.unwrap_or(1),
|
|
||||||
texture
|
|
||||||
.get("sampleCount")
|
|
||||||
.and_then(Value::as_u64)
|
|
||||||
.unwrap_or(1),
|
|
||||||
texture
|
|
||||||
.get("dimension")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.unwrap_or("2d"),
|
|
||||||
))
|
|
||||||
.map_err(|_| "GRAPH_RESOURCE")
|
|
||||||
}
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
import initWasm, { Core as WasmCore } from "../pkg/yawn_core.js";
|
|
||||||
|
|
||||||
let canvas, context, device, surfaceFormat, memory, core, loadout;
|
|
||||||
const arrays = new Map();
|
|
||||||
const align = (value, multiple) => Math.ceil(value / multiple) * multiple;
|
|
||||||
const fail = code => { throw new Error(code); };
|
|
||||||
const list = value => value === undefined ? [] : Array.isArray(value) ? value : fail("GRAPH_ARRAY");
|
|
||||||
|
|
||||||
function index(items, code) {
|
|
||||||
const result = new Map();
|
|
||||||
for (const item of list(items)) {
|
|
||||||
if (!item || typeof item.id !== "string" || result.has(item.id)) fail(code);
|
|
||||||
result.set(item.id, item);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bufferUsage = names => list(names).reduce((usage, name) => usage | ({
|
|
||||||
uniform: GPUBufferUsage.UNIFORM, storage: GPUBufferUsage.STORAGE,
|
|
||||||
vertex: GPUBufferUsage.VERTEX, index: GPUBufferUsage.INDEX,
|
|
||||||
indirect: GPUBufferUsage.INDIRECT, copySrc: GPUBufferUsage.COPY_SRC,
|
|
||||||
}[name] ?? fail("GRAPH_BUFFER_USAGE")), GPUBufferUsage.COPY_DST);
|
|
||||||
const textureUsage = names => list(names).reduce((usage, name) => usage | ({
|
|
||||||
render: GPUTextureUsage.RENDER_ATTACHMENT, sampled: GPUTextureUsage.TEXTURE_BINDING,
|
|
||||||
storage: GPUTextureUsage.STORAGE_BINDING, copySrc: GPUTextureUsage.COPY_SRC,
|
|
||||||
copyDst: GPUTextureUsage.COPY_DST,
|
|
||||||
}[name] ?? fail("GRAPH_TEXTURE_USAGE")), 0);
|
|
||||||
|
|
||||||
async function compile(graph) {
|
|
||||||
if (!graph || typeof graph.id !== "string") fail("GRAPH_SHAPE");
|
|
||||||
const passes = list(graph.passes);
|
|
||||||
const renderDeclarations = index(graph.pipelines?.render, "GRAPH_PIPELINE");
|
|
||||||
const computeDeclarations = index(graph.pipelines?.compute, "GRAPH_PIPELINE");
|
|
||||||
const resources = new Map(), owned = [];
|
|
||||||
try {
|
|
||||||
for (const declaration of list(graph.resources?.buffers)) {
|
|
||||||
const source = arrays.get(declaration.array);
|
|
||||||
if (!source) fail("GRAPH_ARRAY_UNKNOWN");
|
|
||||||
const gpu = device.createBuffer({ size: align(source.bytes, 4), usage: bufferUsage(declaration.usage) });
|
|
||||||
resources.set(declaration.id, { kind: "buffer", gpu, source });
|
|
||||||
owned.push(gpu);
|
|
||||||
}
|
|
||||||
|
|
||||||
const textureSlots = new Map();
|
|
||||||
for (const declaration of list(graph.resources?.textures)) {
|
|
||||||
if (!Number.isInteger(declaration.slot)) fail("GRAPH_RESOURCE");
|
|
||||||
const size = declaration.size ?? ["canvas", "canvas"];
|
|
||||||
if (!Array.isArray(size) || size.length < 2 || size.length > 3) fail("GRAPH_TEXTURE_SIZE");
|
|
||||||
const descriptor = {
|
|
||||||
size: [size[0] === "canvas" ? canvas.width : size[0], size[1] === "canvas" ? canvas.height : size[1], size[2] ?? 1],
|
|
||||||
format: declaration.format,
|
|
||||||
usage: textureUsage(declaration.usage),
|
|
||||||
mipLevelCount: declaration.mipLevelCount ?? 1,
|
|
||||||
sampleCount: declaration.sampleCount ?? 1,
|
|
||||||
dimension: declaration.dimension ?? "2d",
|
|
||||||
};
|
|
||||||
let slot = textureSlots.get(declaration.slot);
|
|
||||||
if (!slot) {
|
|
||||||
const gpu = device.createTexture(descriptor);
|
|
||||||
slot = { gpu, view: gpu.createView() };
|
|
||||||
textureSlots.set(declaration.slot, slot);
|
|
||||||
owned.push(gpu);
|
|
||||||
}
|
|
||||||
resources.set(declaration.id, { kind: "texture", gpu: slot.gpu, view: slot.view });
|
|
||||||
}
|
|
||||||
for (const declaration of list(graph.resources?.samplers))
|
|
||||||
resources.set(declaration.id, {
|
|
||||||
kind: "sampler", gpu: device.createSampler(declaration.descriptor),
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderPipelines = new Map(), computePipelines = new Map();
|
|
||||||
await Promise.all([...new Set(passes.filter(x => x.type === "render").map(x => x.pipeline))].map(async id => {
|
|
||||||
const declaration = renderDeclarations.get(id);
|
|
||||||
if (typeof declaration?.code !== "string") fail("GRAPH_PIPELINE");
|
|
||||||
const module = device.createShaderModule({ code: declaration.code });
|
|
||||||
renderPipelines.set(id, await device.createRenderPipelineAsync({
|
|
||||||
layout: "auto",
|
|
||||||
vertex: { module, entryPoint: declaration.vertex?.entry ?? "vertex", buffers: declaration.vertex?.buffers ?? [] },
|
|
||||||
fragment: {
|
|
||||||
module,
|
|
||||||
entryPoint: declaration.fragment?.entry ?? "fragment",
|
|
||||||
targets: list(declaration.fragment?.targets).map(target => ({
|
|
||||||
...target, format: target.format === "canvas" ? surfaceFormat : target.format,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
primitive: declaration.primitive,
|
|
||||||
depthStencil: declaration.depthStencil,
|
|
||||||
multisample: declaration.multisample,
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
await Promise.all([...new Set(passes.filter(x => x.type === "compute").map(x => x.pipeline))].map(async id => {
|
|
||||||
const declaration = computeDeclarations.get(id);
|
|
||||||
if (typeof declaration?.code !== "string") fail("GRAPH_PIPELINE");
|
|
||||||
const module = device.createShaderModule({ code: declaration.code });
|
|
||||||
computePipelines.set(id, await device.createComputePipelineAsync({
|
|
||||||
layout: "auto", compute: { module, entryPoint: declaration.entry ?? "main" },
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
|
|
||||||
const bindGroups = (pass, pipeline) => {
|
|
||||||
const groups = new Map();
|
|
||||||
for (const binding of list(pass.bindings)) {
|
|
||||||
const resource = resources.get(binding.resource);
|
|
||||||
if (!resource) fail("GRAPH_RESOURCE_UNKNOWN");
|
|
||||||
const value = resource.kind === "buffer"
|
|
||||||
? { buffer: resource.gpu, offset: binding.offset ?? 0, ...(binding.size ? { size: binding.size } : {}) }
|
|
||||||
: resource.kind === "texture" ? resource.view : resource.gpu;
|
|
||||||
if (!groups.has(binding.group ?? 0)) groups.set(binding.group ?? 0, []);
|
|
||||||
groups.get(binding.group ?? 0).push({ binding: binding.binding, resource: value });
|
|
||||||
}
|
|
||||||
return [...groups].map(([group, entries]) => [group, device.createBindGroup({
|
|
||||||
layout: pipeline.getBindGroupLayout(group), entries,
|
|
||||||
})]);
|
|
||||||
};
|
|
||||||
const compiled = passes.map(pass => {
|
|
||||||
const pipeline = (pass.type === "render" ? renderPipelines : computePipelines).get(pass.pipeline);
|
|
||||||
if (!pipeline) fail("GRAPH_PASS");
|
|
||||||
return { pass, pipeline, bindGroups: bindGroups(pass, pipeline) };
|
|
||||||
});
|
|
||||||
return { id: graph.id, passes: compiled, resources, owned };
|
|
||||||
} catch (error) {
|
|
||||||
owned.forEach(resource => resource.destroy?.());
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearColor = (value = [0, 0, 0, 1]) => Array.isArray(value)
|
|
||||||
? { r: value[0], g: value[1], b: value[2], a: value[3] }
|
|
||||||
: value;
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
if (!loadout) return;
|
|
||||||
for (const resource of loadout.resources.values())
|
|
||||||
if (resource.kind === "buffer") device.queue.writeBuffer(
|
|
||||||
resource.gpu, 0, new Uint8Array(memory, resource.source.offset, resource.source.bytes),
|
|
||||||
);
|
|
||||||
const encoder = device.createCommandEncoder();
|
|
||||||
for (const { pass, pipeline, bindGroups } of loadout.passes) {
|
|
||||||
if (pass.type === "compute") {
|
|
||||||
const command = encoder.beginComputePass();
|
|
||||||
command.setPipeline(pipeline);
|
|
||||||
bindGroups.forEach(([group, value]) => command.setBindGroup(group, value));
|
|
||||||
command.dispatchWorkgroups(...(pass.dispatch ?? [1, 1, 1]));
|
|
||||||
command.end();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const view = id => id === "canvas"
|
|
||||||
? context.getCurrentTexture().createView()
|
|
||||||
: loadout.resources.get(id)?.view ?? fail("GRAPH_ATTACHMENT");
|
|
||||||
const command = encoder.beginRenderPass({
|
|
||||||
colorAttachments: list(pass.color).map(attachment => ({
|
|
||||||
view: view(attachment.resource), loadOp: attachment.load ?? "clear",
|
|
||||||
storeOp: attachment.store ?? "store", clearValue: clearColor(attachment.clear),
|
|
||||||
})),
|
|
||||||
...(pass.depth ? { depthStencilAttachment: {
|
|
||||||
view: view(pass.depth.resource), depthLoadOp: pass.depth.load ?? "clear",
|
|
||||||
depthStoreOp: pass.depth.store ?? "store", depthClearValue: pass.depth.clear ?? 1,
|
|
||||||
} } : {}),
|
|
||||||
});
|
|
||||||
command.setPipeline(pipeline);
|
|
||||||
bindGroups.forEach(([group, value]) => command.setBindGroup(group, value));
|
|
||||||
list(pass.vertexBuffers).forEach(binding => command.setVertexBuffer(
|
|
||||||
binding.slot ?? 0, loadout.resources.get(binding.resource)?.gpu ?? fail("GRAPH_RESOURCE_UNKNOWN"), binding.offset ?? 0,
|
|
||||||
));
|
|
||||||
if (pass.indexBuffer) command.setIndexBuffer(
|
|
||||||
loadout.resources.get(pass.indexBuffer.resource)?.gpu ?? fail("GRAPH_RESOURCE_UNKNOWN"),
|
|
||||||
pass.indexBuffer.format ?? "uint32", pass.indexBuffer.offset ?? 0,
|
|
||||||
);
|
|
||||||
const draw = pass.draw ?? {};
|
|
||||||
if (pass.indexBuffer) command.drawIndexed(draw.indices ?? 0, draw.instances ?? 1, draw.firstIndex ?? 0, draw.baseVertex ?? 0, draw.firstInstance ?? 0);
|
|
||||||
else command.draw(draw.vertices ?? 3, draw.instances ?? 1, draw.firstVertex ?? 0, draw.firstInstance ?? 0);
|
|
||||||
command.end();
|
|
||||||
}
|
|
||||||
device.queue.submit([encoder.finish()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function tick() {
|
|
||||||
try { render(); } catch (error) {
|
|
||||||
postMessage({ type: "runtime-error", error: error?.message ?? "RENDER_ERROR" });
|
|
||||||
loadout = undefined;
|
|
||||||
}
|
|
||||||
(globalThis.requestAnimationFrame ?? (callback => setTimeout(callback, 16)))(tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener("message", async ({ data: message }) => {
|
|
||||||
try {
|
|
||||||
let result;
|
|
||||||
if (message.type === "init") {
|
|
||||||
if (!(message.canvas instanceof OffscreenCanvas) || !Number.isInteger(message.arenaBytes) || message.arenaBytes < 64) fail("INIT");
|
|
||||||
canvas = message.canvas;
|
|
||||||
const wasm = await initWasm();
|
|
||||||
core = new WasmCore(message.arenaBytes);
|
|
||||||
memory = wasm.memory.buffer;
|
|
||||||
if (!(memory instanceof SharedArrayBuffer)) fail("WASM_MEMORY_NOT_SHARED");
|
|
||||||
const adapter = await navigator.gpu?.requestAdapter();
|
|
||||||
if (!adapter) fail("WEBGPU_UNAVAILABLE");
|
|
||||||
device = await adapter.requestDevice();
|
|
||||||
context = canvas.getContext("webgpu");
|
|
||||||
surfaceFormat = navigator.gpu.getPreferredCanvasFormat();
|
|
||||||
context.configure({ device, format: surfaceFormat, alphaMode: "opaque" });
|
|
||||||
device.lost.then(() => { postMessage({ type: "runtime-error", error: "DEVICE_LOST" }); loadout = undefined; });
|
|
||||||
result = { buffer: memory };
|
|
||||||
tick();
|
|
||||||
} else if (message.type === "allocate") {
|
|
||||||
const { name, rows, stride, format } = message;
|
|
||||||
if (typeof name !== "string" || !name || arrays.has(name)) fail("ALLOCATION");
|
|
||||||
const offset = core.allocate(rows, stride, format), bytes = rows * stride;
|
|
||||||
result = { name, rows, stride, format, offset };
|
|
||||||
arrays.set(name, { ...result, bytes });
|
|
||||||
} else if (message.type === "load-graph") {
|
|
||||||
const next = await compile(JSON.parse(core.compile_graph(message.serialized)));
|
|
||||||
const previous = loadout;
|
|
||||||
loadout = next;
|
|
||||||
previous?.owned.forEach(resource => resource.destroy?.());
|
|
||||||
result = { id: next.id };
|
|
||||||
} else fail("MESSAGE");
|
|
||||||
postMessage({ request: message.request, result });
|
|
||||||
} catch (error) {
|
|
||||||
postMessage({ request: message.request, error: error?.message ?? "CORE_ERROR" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import initWasm, { Core } from "./pkg/yawn_core.js";
|
||||||
|
|
||||||
|
let core;
|
||||||
|
|
||||||
|
const fail = code => { throw new Error(code); };
|
||||||
|
|
||||||
|
addEventListener("message", async ({ data: message }) => {
|
||||||
|
try {
|
||||||
|
let result;
|
||||||
|
if (message?.type === "init") {
|
||||||
|
if (core || !(message.canvas instanceof OffscreenCanvas)) fail("INIT");
|
||||||
|
const wasm = await initWasm();
|
||||||
|
core = new Core(message.arenaBytes);
|
||||||
|
await core.initialize(message.canvas);
|
||||||
|
const buffer = wasm.memory.buffer;
|
||||||
|
if (!(buffer instanceof SharedArrayBuffer)) fail("WASM_MEMORY_NOT_SHARED");
|
||||||
|
result = { buffer, rows: JSON.parse(core.rows()) };
|
||||||
|
} else {
|
||||||
|
if (!core) fail("UNINITIALIZED");
|
||||||
|
switch (message?.type) {
|
||||||
|
case "create-rows":
|
||||||
|
result = JSON.parse(core.create_rows(message.name, message.rows, message.stride, message.format));
|
||||||
|
break;
|
||||||
|
case "delete-rows":
|
||||||
|
core.delete_rows(message.name);
|
||||||
|
break;
|
||||||
|
case "allocate-object":
|
||||||
|
result = JSON.parse(core.allocate_object(message.name));
|
||||||
|
break;
|
||||||
|
case "delete-object":
|
||||||
|
core.delete_object(message.name, message.id);
|
||||||
|
break;
|
||||||
|
case "compile-graph":
|
||||||
|
result = core.compile_graph(message.serialized);
|
||||||
|
break;
|
||||||
|
case "switch-loadout":
|
||||||
|
core.switch_loadout(message.id);
|
||||||
|
break;
|
||||||
|
case "play":
|
||||||
|
core.play();
|
||||||
|
break;
|
||||||
|
case "pause":
|
||||||
|
core.pause();
|
||||||
|
break;
|
||||||
|
case "set-fps":
|
||||||
|
core.set_fps(message.fps);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fail("MESSAGE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postMessage({ request: message.request, result });
|
||||||
|
} catch (error) {
|
||||||
|
postMessage({ request: message?.request, error: error?.message ?? "CORE_ERROR" });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -25,7 +25,7 @@ onMounted(async () => {
|
|||||||
canvas.value.height = 540;
|
canvas.value.height = 540;
|
||||||
core = new YawnCore(canvas.value);
|
core = new YawnCore(canvas.value);
|
||||||
await core.ready;
|
await core.ready;
|
||||||
color = await core.allocateRows({
|
color = await core.createRows({
|
||||||
name: "triangle.color",
|
name: "triangle.color",
|
||||||
rows: 1,
|
rows: 1,
|
||||||
stride: 16,
|
stride: 16,
|
||||||
|
|||||||
Reference in New Issue
Block a user