diff --git a/.github/workflows/build-dev-and-ci.yml b/.github/workflows/build-dev-and-ci.yml
index 4e443817ce..adad7d842b 100644
--- a/.github/workflows/build-dev-and-ci.yml
+++ b/.github/workflows/build-dev-and-ci.yml
@@ -5,8 +5,7 @@ on:
branches:
- master
pull_request:
- branches:
- - master
+
env:
CARGO_TERM_COLOR: always
INDEX_HTML_HEAD_REPLACEMENT:
diff --git a/.gitignore b/.gitignore
index 815f5c3986..5f1f0aaff2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,4 @@ target/
perf.data*
profile.json
flamegraph.svg
-
+.idea/
diff --git a/Cargo.lock b/Cargo.lock
index 0c347cf8f7..34d54b3074 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -82,6 +82,12 @@ dependencies = [
"alloc-no-stdlib",
]
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
[[package]]
name = "android-activity"
version = "0.5.2"
@@ -637,6 +643,27 @@ dependencies = [
"serde",
]
+[[package]]
+name = "cargo-gpu"
+version = "0.1.0"
+source = "git+https://github.com/rust-gpu/cargo-gpu?rev=d8ef9f58257622c4f3eae215dc950f0c628fa3c1#d8ef9f58257622c4f3eae215dc950f0c628fa3c1"
+dependencies = [
+ "anyhow",
+ "cargo_metadata",
+ "clap",
+ "crossterm",
+ "directories",
+ "env_logger 0.10.2",
+ "log",
+ "naga 25.0.1",
+ "relative-path",
+ "rustc_codegen_spirv-target-specs",
+ "semver",
+ "serde",
+ "serde_json",
+ "spirv-builder",
+]
+
[[package]]
name = "cargo-platform"
version = "0.1.9"
@@ -776,9 +803,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.31"
+version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
+checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
dependencies = [
"clap_builder",
"clap_derive",
@@ -786,9 +813,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.31"
+version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
+checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
dependencies = [
"anstream",
"anstyle",
@@ -798,9 +825,9 @@ dependencies = [
[[package]]
name = "clap_derive"
-version = "4.5.28"
+version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
+checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -824,6 +851,17 @@ dependencies = [
"unicode-width",
]
+[[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 = "color"
version = "0.1.0"
@@ -1119,6 +1157,31 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+[[package]]
+name = "crossterm"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
+dependencies = [
+ "bitflags 2.9.0",
+ "crossterm_winapi",
+ "mio",
+ "parking_lot",
+ "rustix 0.38.44",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "crunchy"
version = "0.2.3"
@@ -1269,13 +1332,34 @@ dependencies = [
"crypto-common",
]
+[[package]]
+name = "directories"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
+dependencies = [
+ "dirs-sys 0.4.1",
+]
+
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
- "dirs-sys",
+ "dirs-sys 0.5.0",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users 0.4.6",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -1286,7 +1370,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
- "redox_users",
+ "redox_users 0.5.0",
"windows-sys 0.59.0",
]
@@ -1457,14 +1541,27 @@ dependencies = [
[[package]]
name = "env_logger"
-version = "0.11.6"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
+checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
- "humantime",
+ "jiff",
"log",
]
@@ -1568,6 +1665,18 @@ dependencies = [
"rustc_version",
]
+[[package]]
+name = "filetime"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "libredox",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "fixedbitset"
version = "0.4.2"
@@ -1691,6 +1800,15 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fsevent-sys"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "futf"
version = "0.1.5"
@@ -2298,6 +2416,15 @@ dependencies = [
"wgpu",
]
+[[package]]
+name = "graphene-core-shader"
+version = "0.1.0"
+dependencies = [
+ "bytemuck",
+ "glam",
+ "spirv-std",
+]
+
[[package]]
name = "graphene-std"
version = "0.1.0"
@@ -2371,7 +2498,7 @@ dependencies = [
"convert_case 0.7.1",
"derivative",
"dyn-any",
- "env_logger",
+ "env_logger 0.11.8",
"futures",
"glam",
"gpu-executor",
@@ -2516,13 +2643,14 @@ dependencies = [
[[package]]
name = "half"
-version = "2.4.1"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
+checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
dependencies = [
"bytemuck",
"cfg-if",
"crunchy",
+ "num-traits",
"serde",
]
@@ -2538,6 +2666,8 @@ version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
+ "allocator-api2",
+ "equivalent",
"foldhash",
]
@@ -3044,6 +3174,35 @@ dependencies = [
"cfb",
]
+[[package]]
+name = "inotify"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
+dependencies = [
+ "bitflags 1.3.2",
+ "inotify-sys",
+ "libc",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "interpolate_name"
version = "0.2.4"
@@ -3172,6 +3331,30 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "jiff"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93"
+dependencies = [
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.99",
+]
+
[[package]]
name = "jni"
version = "0.21.1"
@@ -3269,6 +3452,26 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
+[[package]]
+name = "kqueue"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
+dependencies = [
+ "kqueue-sys",
+ "libc",
+]
+
+[[package]]
+name = "kqueue-sys"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
+dependencies = [
+ "bitflags 1.3.2",
+ "libc",
+]
+
[[package]]
name = "kuchikiki"
version = "0.8.2"
@@ -3362,7 +3565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -3573,6 +3776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
+ "log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@@ -3608,7 +3812,7 @@ dependencies = [
"bit-set",
"bitflags 2.9.0",
"cfg_aliases 0.1.1",
- "codespan-reporting",
+ "codespan-reporting 0.11.1",
"hexf-parse",
"indexmap 2.7.1",
"log",
@@ -3620,6 +3824,29 @@ dependencies = [
"unicode-xid",
]
+[[package]]
+name = "naga"
+version = "25.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632"
+dependencies = [
+ "arrayvec",
+ "bit-set",
+ "bitflags 2.9.0",
+ "cfg_aliases 0.2.1",
+ "codespan-reporting 0.12.0",
+ "half",
+ "hashbrown 0.15.2",
+ "indexmap 2.7.1",
+ "log",
+ "num-traits",
+ "once_cell",
+ "petgraph 0.8.1",
+ "rustc-hash 1.1.0",
+ "spirv",
+ "thiserror 2.0.12",
+]
+
[[package]]
name = "native-tls"
version = "0.2.14"
@@ -3748,6 +3975,34 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+[[package]]
+name = "notify"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
+dependencies = [
+ "bitflags 2.9.0",
+ "filetime",
+ "fsevent-sys",
+ "inotify",
+ "kqueue",
+ "libc",
+ "log",
+ "mio",
+ "notify-types",
+ "walkdir",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "notify-types"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174"
+dependencies = [
+ "instant",
+]
+
[[package]]
name = "num-bigint"
version = "0.4.6"
@@ -4092,9 +4347,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.20.3"
+version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "oorandom"
@@ -4347,6 +4602,18 @@ dependencies = [
"indexmap 2.7.1",
]
+[[package]]
+name = "petgraph"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a98c6720655620a521dcc722d0ad66cd8afd5d86e34a89ef691c50b7b24de06"
+dependencies = [
+ "fixedbitset 0.5.7",
+ "hashbrown 0.15.2",
+ "indexmap 2.7.1",
+ "serde",
+]
+
[[package]]
name = "phf"
version = "0.8.0"
@@ -5011,6 +5278,12 @@ dependencies = [
"rgb",
]
+[[package]]
+name = "raw-string"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0501e134c6905fee1f10fed25b0a7e1261bf676cffac9543a7d0730dec01af2"
+
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -5071,6 +5344,17 @@ dependencies = [
"bitflags 2.9.0",
]
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.15",
+ "libredox",
+ "thiserror 1.0.69",
+]
+
[[package]]
name = "redox_users"
version = "0.5.0"
@@ -5111,6 +5395,12 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+[[package]]
+name = "relative-path"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
+
[[package]]
name = "renderdoc-sys"
version = "1.1.0"
@@ -5229,6 +5519,16 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
+[[package]]
+name = "rspirv"
+version = "0.12.0+sdk-1.3.268.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cf3a93856b6e5946537278df0d3075596371b1950ccff012f02b0f7eafec8d"
+dependencies = [
+ "rustc-hash 1.1.0",
+ "spirv",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@@ -5247,6 +5547,23 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+[[package]]
+name = "rustc_codegen_spirv-target-specs"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c89eaf493b3dfc730cda42a77014aad65e03213992c7afe0dff60a9f7d3dd94"
+
+[[package]]
+name = "rustc_codegen_spirv-types"
+version = "0.9.0"
+source = "git+https://github.com/Rust-GPU/rust-gpu?rev=8cb17db18d8a44e1de7c9b3ea2b65d5aaf24b919#8cb17db18d8a44e1de7c9b3ea2b65d5aaf24b919"
+dependencies = [
+ "rspirv",
+ "serde",
+ "serde_json",
+ "spirv",
+]
+
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -5691,6 +6008,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+[[package]]
+name = "signal-hook"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
@@ -5904,15 +6242,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
dependencies = [
"bitflags 2.9.0",
+ "serde",
+]
+
+[[package]]
+name = "spirv-builder"
+version = "0.9.0"
+source = "git+https://github.com/Rust-GPU/rust-gpu?rev=8cb17db18d8a44e1de7c9b3ea2b65d5aaf24b919#8cb17db18d8a44e1de7c9b3ea2b65d5aaf24b919"
+dependencies = [
+ "clap",
+ "memchr",
+ "notify",
+ "raw-string",
+ "rustc_codegen_spirv-types",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.12",
]
[[package]]
name = "spirv-std"
version = "0.9.0"
-source = "git+https://github.com/Rust-GPU/rust-gpu.git#6e2c84d4fe64e32df4c060c5a7f3e35a32e45421"
+source = "git+https://github.com/rust-gpu/rust-gpu?rev=9a357691334b9cdd13c82a740ced97c5d857bf4d#9a357691334b9cdd13c82a740ced97c5d857bf4d"
dependencies = [
"bitflags 1.3.2",
"glam",
+ "libm",
"num-traits",
"spirv-std-macros",
"spirv-std-types",
@@ -5921,7 +6277,7 @@ dependencies = [
[[package]]
name = "spirv-std-macros"
version = "0.9.0"
-source = "git+https://github.com/Rust-GPU/rust-gpu.git#6e2c84d4fe64e32df4c060c5a7f3e35a32e45421"
+source = "git+https://github.com/rust-gpu/rust-gpu?rev=9a357691334b9cdd13c82a740ced97c5d857bf4d#9a357691334b9cdd13c82a740ced97c5d857bf4d"
dependencies = [
"proc-macro2",
"quote",
@@ -5932,7 +6288,7 @@ dependencies = [
[[package]]
name = "spirv-std-types"
version = "0.9.0"
-source = "git+https://github.com/Rust-GPU/rust-gpu.git#6e2c84d4fe64e32df4c060c5a7f3e35a32e45421"
+source = "git+https://github.com/rust-gpu/rust-gpu?rev=9a357691334b9cdd13c82a740ced97c5d857bf4d#9a357691334b9cdd13c82a740ced97c5d857bf4d"
[[package]]
name = "stable_deref_trait"
@@ -7134,7 +7490,7 @@ version = "0.3.0"
source = "git+https://github.com/linebender/vello.git?rev=3275ec8#3275ec85d831180be81820de06cca29a97a757f5"
dependencies = [
"bytemuck",
- "naga",
+ "naga 23.1.0",
"thiserror 2.0.12",
"vello_encoding",
]
@@ -7540,7 +7896,7 @@ dependencies = [
"document-features",
"js-sys",
"log",
- "naga",
+ "naga 23.1.0",
"parking_lot",
"profiling",
"raw-window-handle",
@@ -7568,7 +7924,7 @@ dependencies = [
"document-features",
"indexmap 2.7.1",
"log",
- "naga",
+ "naga 23.1.0",
"once_cell",
"parking_lot",
"profiling",
@@ -7587,12 +7943,15 @@ dependencies = [
"anyhow",
"base64 0.22.1",
"bytemuck",
+ "cargo-gpu",
"dyn-any",
+ "env_logger 0.11.8",
"futures",
"futures-intrusive",
"glam",
"gpu-executor",
"graphene-core",
+ "graphene-core-shader",
"half",
"log",
"node-macro",
@@ -7632,7 +7991,7 @@ dependencies = [
"libloading 0.8.6",
"log",
"metal",
- "naga",
+ "naga 23.1.0",
"ndk-sys 0.5.0+25.2.9519653",
"objc",
"once_cell",
diff --git a/Cargo.toml b/Cargo.toml
index 1c50e83ee1..759c50b3db 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,7 @@ members = [
"frontend/wasm",
"frontend/src-tauri",
"node-graph/gcore",
+ "node-graph/gcore-shader",
"node-graph/gstd",
"node-graph/graph-craft",
"node-graph/graphene-cli",
@@ -69,7 +70,7 @@ axum = "0.8"
chrono = "0.4"
ron = "0.8"
fastnoise-lite = "1.1"
-spirv-std = { git = "https://github.com/Rust-GPU/rust-gpu.git" }
+spirv-std = { git = "https://github.com/rust-gpu/rust-gpu", rev = "9a357691334b9cdd13c82a740ced97c5d857bf4d" }
wgpu-types = "23"
wgpu = "23"
once_cell = "1.13" # Remove when `core::cell::LazyCell` () is stabilized in Rust 1.80 and we bump our MSRV
@@ -85,7 +86,7 @@ resvg = "0.44"
usvg = "0.44"
rand = { version = "0.9", default-features = false }
rand_chacha = "0.9"
-glam = { version = "0.29", default-features = false, features = ["serde"] }
+glam = { version = "0.29", default-features = false }
base64 = "0.22"
image = { version = "0.25", default-features = false, features = ["png"] }
rustybuzz = "0.20"
@@ -108,6 +109,7 @@ kurbo = { version = "0.11.0", features = ["serde"] }
petgraph = { version = "0.7.1", default-features = false, features = [
"graphmap",
] }
+cargo-gpu = { git = "https://github.com/rust-gpu/cargo-gpu", rev = "d8ef9f58257622c4f3eae215dc950f0c628fa3c1", features = ["wgsl-out"] }
[profile.dev]
opt-level = 1
diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
index bd5fab4e51..f79ed56eec 100644
--- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
+++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
@@ -14,6 +14,7 @@ use graph_craft::ProtoNodeIdentifier;
use graph_craft::concrete;
use graph_craft::document::value::*;
use graph_craft::document::*;
+use graphene_core::application_io::TextureFrameTable;
use graphene_core::raster::brush_cache::BrushCache;
use graphene_core::raster::image::ImageFrameTable;
use graphene_core::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha};
@@ -1850,6 +1851,91 @@ fn static_nodes() -> Vec {
description: Cow::Borrowed("TODO"),
properties: None,
},
+ #[cfg(feature = "gpu")]
+ DocumentNodeDefinition {
+ identifier: "GPU Invert",
+ category: "Debug: GPU",
+ node_template: NodeTemplate {
+ document_node: DocumentNode {
+ implementation: DocumentNodeImplementation::Network(NodeNetwork {
+ exports: vec![NodeInput::node(NodeId(2), 0)],
+ nodes: [
+ DocumentNode {
+ inputs: vec![NodeInput::scope("editor-api")],
+ implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::IntoNode<&WgpuExecutor>")),
+ ..Default::default()
+ },
+ DocumentNode {
+ inputs: vec![NodeInput::network(concrete!(TextureFrameTable), 0), NodeInput::node(NodeId(0), 0)],
+ manual_composition: Some(generic!(T)),
+ implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("wgpu_executor::gcore_shader_nodes::GpuInvertNode")),
+ ..Default::default()
+ },
+ DocumentNode {
+ manual_composition: Some(generic!(T)),
+ inputs: vec![NodeInput::node(NodeId(1), 0)],
+ implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::ImpureMemoNode")),
+ ..Default::default()
+ },
+ ]
+ .into_iter()
+ .enumerate()
+ .map(|(id, node)| (NodeId(id as u64), node))
+ .collect(),
+ ..Default::default()
+ }),
+ // experimentally determined to work
+ inputs: vec![NodeInput::value(TaggedValue::None, true)],
+ // inputs: vec![NodeInput::value(TaggedValue::TextureFrame(TextureFrameTable::default()), true)],
+ // inputs: vec![],
+ // inputs: vec![NodeInput::network(concrete!(TextureFrameTable), 0)],
+ ..Default::default()
+ },
+ persistent_node_metadata: DocumentNodePersistentMetadata {
+ input_properties: vec![("In", "TODO").into()],
+ output_names: vec!["Texture".to_string()],
+ network_metadata: Some(NodeNetworkMetadata {
+ persistent_metadata: NodeNetworkPersistentMetadata {
+ node_metadata: [
+ DocumentNodeMetadata {
+ persistent_metadata: DocumentNodePersistentMetadata {
+ display_name: "Extract Executor".to_string(),
+ node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ DocumentNodeMetadata {
+ persistent_metadata: DocumentNodePersistentMetadata {
+ display_name: "Upload Texture".to_string(),
+ node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ DocumentNodeMetadata {
+ persistent_metadata: DocumentNodePersistentMetadata {
+ display_name: "Cache".to_string(),
+ node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 0)),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ ]
+ .into_iter()
+ .enumerate()
+ .map(|(id, node)| (NodeId(id as u64), node))
+ .collect(),
+ ..Default::default()
+ },
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ },
+ description: Cow::Borrowed("TODO"),
+ properties: None,
+ },
DocumentNodeDefinition {
identifier: "Extract",
category: "Debug",
diff --git a/frontend/wasm/Cargo.toml b/frontend/wasm/Cargo.toml
index 09e91dd4f4..d5356e663e 100644
--- a/frontend/wasm/Cargo.toml
+++ b/frontend/wasm/Cargo.toml
@@ -36,7 +36,7 @@ serde-wasm-bindgen = { workspace = true }
js-sys = { workspace = true }
wasm-bindgen-futures = { workspace = true }
bezier-rs = { workspace = true }
-glam = { workspace = true }
+glam = { workspace = true, features = ["serde"] }
futures = { workspace = true }
math-parser = { workspace = true }
wgpu = { workspace = true, features = [
diff --git a/node-graph/gcore-shader/Cargo.toml b/node-graph/gcore-shader/Cargo.toml
new file mode 100644
index 0000000000..edfcd1a1c2
--- /dev/null
+++ b/node-graph/gcore-shader/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "graphene-core-shader"
+version = "0.1.0"
+edition = "2024"
+description = "Graphene nodes compiled to shaders"
+authors = ["Graphite Authors "]
+license = "MIT OR Apache-2.0"
+
+[lib]
+crate-type = ["rlib", "cdylib"]
+
+[features]
+gpu = ["glam/libm"]
+
+[dependencies]
+# Workspace dependencies
+spirv-std = { workspace = true }
+bytemuck = { workspace = true }
+glam = { workspace = true, features = [
+ "scalar-math", "bytemuck"
+] }
+
+[lints.rust]
+# the spirv target is not in the list of common cfgs so must be added manually
+unexpected_cfgs = { level = "warn", check-cfg = [
+ 'cfg(target_arch, values("spirv"))',
+] }
diff --git a/node-graph/gcore-shader/src/color.rs b/node-graph/gcore-shader/src/color.rs
new file mode 100644
index 0000000000..7f3f25bc24
--- /dev/null
+++ b/node-graph/gcore-shader/src/color.rs
@@ -0,0 +1,782 @@
+use bytemuck::{Pod, Zeroable};
+use core::hash::Hash;
+use glam::Vec4;
+#[cfg(target_arch = "spirv")]
+use spirv_std::num_traits::Euclid;
+#[cfg(target_arch = "spirv")]
+use spirv_std::num_traits::float::Float;
+
+// -----------------------------------------------------
+// custom color start
+// -----------------------------------------------------
+
+impl From for Color {
+ fn from(value: Vec4) -> Self {
+ Color {
+ red: value.x,
+ green: value.y,
+ blue: value.z,
+ alpha: value.w,
+ }
+ }
+}
+
+impl From for Vec4 {
+ fn from(value: Color) -> Self {
+ Vec4::new(value.red, value.green, value.blue, value.alpha)
+ }
+}
+
+// -----------------------------------------------------
+// custom color end
+// -----------------------------------------------------
+
+#[repr(C)]
+#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)]
+pub struct SRGBA8 {
+ red: u8,
+ green: u8,
+ blue: u8,
+ alpha: u8,
+}
+
+#[repr(C)]
+#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)]
+pub struct Luma(pub f32);
+
+/// Structure that represents a color.
+/// Internally alpha is stored as `f32` that ranges from `0.0` (transparent) to `1.0` (opaque).
+/// The other components (RGB) are stored as `f32` that range from `0.0` up to `f32::MAX`,
+/// the values encode the brightness of each channel proportional to the light intensity in cd/m² (nits) in HDR, and `0.0` (black) to `1.0` (white) in SDR color.
+#[repr(C)]
+#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)]
+pub struct Color {
+ red: f32,
+ green: f32,
+ blue: f32,
+ alpha: f32,
+}
+
+#[allow(clippy::derived_hash_with_manual_eq)]
+impl Hash for Color {
+ fn hash(&self, state: &mut H) {
+ self.red.to_bits().hash(state);
+ self.green.to_bits().hash(state);
+ self.blue.to_bits().hash(state);
+ self.alpha.to_bits().hash(state);
+ }
+}
+
+impl Color {
+ pub const BLACK: Color = Color::from_rgbf32_unchecked(0., 0., 0.);
+ pub const WHITE: Color = Color::from_rgbf32_unchecked(1., 1., 1.);
+ pub const RED: Color = Color::from_rgbf32_unchecked(1., 0., 0.);
+ pub const GREEN: Color = Color::from_rgbf32_unchecked(0., 1., 0.);
+ pub const BLUE: Color = Color::from_rgbf32_unchecked(0., 0., 1.);
+ pub const YELLOW: Color = Color::from_rgbf32_unchecked(1., 1., 0.);
+ pub const CYAN: Color = Color::from_rgbf32_unchecked(0., 1., 1.);
+ pub const MAGENTA: Color = Color::from_rgbf32_unchecked(1., 0., 1.);
+ pub const TRANSPARENT: Color = Self {
+ red: 0.,
+ green: 0.,
+ blue: 0.,
+ alpha: 0.,
+ };
+
+ /// Returns `Some(Color)` if `red`, `green`, `blue` and `alpha` have a valid value. Negative numbers (including `-0.0`), NaN, and infinity are not valid values and return `None`.
+ /// Alpha values greater than `1.0` are not valid.
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_rgbaf32(0.3, 0.14, 0.15, 0.92).unwrap();
+ /// assert!(color.components() == (0.3, 0.14, 0.15, 0.92));
+ ///
+ /// let color = Color::from_rgbaf32(1., 1., 1., f32::NAN);
+ /// assert!(color == None);
+ /// ```
+ #[inline(always)]
+ pub fn from_rgbaf32(red: f32, green: f32, blue: f32, alpha: f32) -> Option {
+ if alpha > 1. || [red, green, blue, alpha].iter().any(|c| c.is_sign_negative() || !c.is_finite()) {
+ return None;
+ }
+ let color = Color { red, green, blue, alpha };
+ Some(color)
+ }
+
+ /// Return an opaque `Color` from given `f32` RGB channels.
+ #[inline(always)]
+ pub const fn from_rgbf32_unchecked(red: f32, green: f32, blue: f32) -> Color {
+ Color { red, green, blue, alpha: 1. }
+ }
+
+ /// Return an opaque `Color` from given `f32` RGB channels.
+ #[inline(always)]
+ pub const fn from_rgbaf32_unchecked(red: f32, green: f32, blue: f32, alpha: f32) -> Color {
+ Color { red, green, blue, alpha }
+ }
+
+ /// Return an opaque `Color` from given `f32` RGB channels.
+ #[inline(always)]
+ pub fn from_unassociated_alpha(red: f32, green: f32, blue: f32, alpha: f32) -> Color {
+ Color::from_rgbaf32_unchecked(red * alpha, green * alpha, blue * alpha, alpha)
+ }
+
+ /// Return an opaque SDR `Color` given RGB channels from `0` to `255`, premultiplied by alpha.
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_rgb8_srgb(0x72, 0x67, 0x62);
+ /// let color2 = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0xFF);
+ /// assert_eq!(color, color2)
+ /// ```
+ #[inline(always)]
+ pub fn from_rgb8_srgb(red: u8, green: u8, blue: u8) -> Color {
+ Color::from_rgba8_srgb(red, green, blue, 255)
+ }
+
+ // TODO: Should this be premult?
+ /// Return an SDR `Color` given RGBA channels from `0` to `255`, premultiplied by alpha.
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0x61);
+ /// ```
+ #[inline(always)]
+ pub fn from_rgba8_srgb(red: u8, green: u8, blue: u8, alpha: u8) -> Color {
+ let map_range = |int_color| int_color as f32 / 255.;
+
+ let red = map_range(red);
+ let green = map_range(green);
+ let blue = map_range(blue);
+ let alpha = map_range(alpha);
+ Color { red, green, blue, alpha }.to_linear_srgb().map_rgb(|channel| channel * alpha)
+ }
+
+ /// Create a [Color] from a hue, saturation, lightness and alpha (all between 0 and 1)
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.);
+ /// ```
+ pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Color {
+ let temp1 = if lightness < 0.5 {
+ lightness * (saturation + 1.)
+ } else {
+ lightness + saturation - lightness * saturation
+ };
+ let temp2 = 2. * lightness - temp1;
+ #[cfg(not(target_arch = "spirv"))]
+ let rem = |x: f32| x.rem_euclid(1.);
+ #[cfg(target_arch = "spirv")]
+ let rem = |x: f32| x.rem_euclid(&1.);
+
+ let mut red = rem(hue + 1. / 3.);
+ let mut green = rem(hue);
+ let mut blue = rem(hue - 1. / 3.);
+
+ fn map_channel(channel: &mut f32, temp2: f32, temp1: f32) {
+ *channel = if *channel * 6. < 1. {
+ temp2 + (temp1 - temp2) * 6. * *channel
+ } else if *channel * 2. < 1. {
+ temp1
+ } else if *channel * 3. < 2. {
+ temp2 + (temp1 - temp2) * (2. / 3. - *channel) * 6.
+ } else {
+ temp2
+ }
+ .clamp(0., 1.);
+ }
+ map_channel(&mut red, temp2, temp1);
+ map_channel(&mut green, temp2, temp1);
+ map_channel(&mut blue, temp2, temp1);
+
+ Color { red, green, blue, alpha }
+ }
+
+ /// Return the `red` component.
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
+ /// assert!(color.r() == 0.114);
+ /// ```
+ #[inline(always)]
+ pub fn r(&self) -> f32 {
+ self.red
+ }
+
+ /// Return the `green` component.
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
+ /// assert!(color.g() == 0.103);
+ /// ```
+ #[inline(always)]
+ pub fn g(&self) -> f32 {
+ self.green
+ }
+
+ /// Return the `blue` component.
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
+ /// assert!(color.b() == 0.98);
+ /// ```
+ #[inline(always)]
+ pub fn b(&self) -> f32 {
+ self.blue
+ }
+
+ /// Return the `alpha` component without checking its expected `0.0` to `1.0` range.
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
+ /// assert!(color.a() == 0.97);
+ /// ```
+ #[inline(always)]
+ pub fn a(&self) -> f32 {
+ self.alpha
+ }
+
+ #[inline(always)]
+ pub fn average_rgb_channels(&self) -> f32 {
+ (self.red + self.green + self.blue) / 3.
+ }
+
+ #[inline(always)]
+ pub fn minimum_rgb_channels(&self) -> f32 {
+ self.red.min(self.green).min(self.blue)
+ }
+
+ #[inline(always)]
+ pub fn maximum_rgb_channels(&self) -> f32 {
+ self.red.max(self.green).max(self.blue)
+ }
+
+ // From https://stackoverflow.com/a/56678483/775283
+ #[inline(always)]
+ pub fn luminance_srgb(&self) -> f32 {
+ 0.2126 * self.red + 0.7152 * self.green + 0.0722 * self.blue
+ }
+
+ // From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
+ #[inline(always)]
+ pub fn luminance_rec_601(&self) -> f32 {
+ 0.299 * self.red + 0.587 * self.green + 0.114 * self.blue
+ }
+
+ // From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
+ #[inline(always)]
+ pub fn luminance_rec_601_rounded(&self) -> f32 {
+ 0.3 * self.red + 0.59 * self.green + 0.11 * self.blue
+ }
+
+ // From https://stackoverflow.com/a/56678483/775283
+ #[inline(always)]
+ pub fn luminance_perceptual(&self) -> f32 {
+ let luminance = self.luminance_srgb();
+
+ if luminance <= 0.008856 {
+ (luminance * 903.3) / 100.
+ } else {
+ (luminance.cbrt() * 116. - 16.) / 100.
+ }
+ }
+
+ #[inline(always)]
+ pub fn from_luminance(luminance: f32) -> Color {
+ Color {
+ red: luminance,
+ green: luminance,
+ blue: luminance,
+ alpha: 1.,
+ }
+ }
+
+ #[inline(always)]
+ pub fn with_luminance(&self, luminance: f32) -> Color {
+ let delta = luminance - self.luminance_rec_601_rounded();
+ self.map_rgb(|c| (c + delta).clamp(0., 1.))
+ }
+
+ #[inline(always)]
+ pub fn saturation(&self) -> f32 {
+ let max = (self.red).max(self.green).max(self.blue);
+ let min = (self.red).min(self.green).min(self.blue);
+
+ max - min
+ }
+
+ #[inline(always)]
+ pub fn with_saturation(&self, saturation: f32) -> Color {
+ let [hue, _, lightness, alpha] = self.to_hsla();
+ Color::from_hsla(hue, saturation, lightness, alpha)
+ }
+
+ pub fn with_alpha(&self, alpha: f32) -> Color {
+ Color {
+ red: self.red,
+ green: self.green,
+ blue: self.blue,
+ alpha,
+ }
+ }
+
+ pub fn with_red(&self, red: f32) -> Color {
+ Color {
+ red,
+ green: self.green,
+ blue: self.blue,
+ alpha: self.alpha,
+ }
+ }
+
+ pub fn with_green(&self, green: f32) -> Color {
+ Color {
+ red: self.red,
+ green,
+ blue: self.blue,
+ alpha: self.alpha,
+ }
+ }
+
+ pub fn with_blue(&self, blue: f32) -> Color {
+ Color {
+ red: self.red,
+ green: self.green,
+ blue,
+ alpha: self.alpha,
+ }
+ }
+
+ #[inline(always)]
+ pub fn blend_normal(_c_b: f32, c_s: f32) -> f32 {
+ c_s
+ }
+
+ #[inline(always)]
+ pub fn blend_multiply(c_b: f32, c_s: f32) -> f32 {
+ c_s * c_b
+ }
+
+ #[inline(always)]
+ pub fn blend_darken(c_b: f32, c_s: f32) -> f32 {
+ c_s.min(c_b)
+ }
+
+ #[inline(always)]
+ pub fn blend_color_burn(c_b: f32, c_s: f32) -> f32 {
+ if c_b == 1. {
+ 1.
+ } else if c_s == 0. {
+ 0.
+ } else {
+ 1. - ((1. - c_b) / c_s).min(1.)
+ }
+ }
+
+ #[inline(always)]
+ pub fn blend_linear_burn(c_b: f32, c_s: f32) -> f32 {
+ c_b + c_s - 1.
+ }
+
+ #[inline(always)]
+ pub fn blend_darker_color(&self, other: Color) -> Color {
+ if self.average_rgb_channels() <= other.average_rgb_channels() { *self } else { other }
+ }
+
+ #[inline(always)]
+ pub fn blend_screen(c_b: f32, c_s: f32) -> f32 {
+ 1. - (1. - c_s) * (1. - c_b)
+ }
+
+ #[inline(always)]
+ pub fn blend_lighten(c_b: f32, c_s: f32) -> f32 {
+ c_s.max(c_b)
+ }
+
+ #[inline(always)]
+ pub fn blend_color_dodge(c_b: f32, c_s: f32) -> f32 {
+ if c_s == 1. { 1. } else { (c_b / (1. - c_s)).min(1.) }
+ }
+
+ #[inline(always)]
+ pub fn blend_linear_dodge(c_b: f32, c_s: f32) -> f32 {
+ c_b + c_s
+ }
+
+ #[inline(always)]
+ pub fn blend_lighter_color(&self, other: Color) -> Color {
+ if self.average_rgb_channels() >= other.average_rgb_channels() { *self } else { other }
+ }
+
+ pub fn blend_softlight(c_b: f32, c_s: f32) -> f32 {
+ if c_s <= 0.5 {
+ c_b - (1. - 2. * c_s) * c_b * (1. - c_b)
+ } else {
+ let d: fn(f32) -> f32 = |x| if x <= 0.25 { ((16. * x - 12.) * x + 4.) * x } else { x.sqrt() };
+ c_b + (2. * c_s - 1.) * (d(c_b) - c_b)
+ }
+ }
+
+ pub fn blend_hardlight(c_b: f32, c_s: f32) -> f32 {
+ if c_s <= 0.5 {
+ Color::blend_multiply(2. * c_s, c_b)
+ } else {
+ Color::blend_screen(2. * c_s - 1., c_b)
+ }
+ }
+
+ pub fn blend_vivid_light(c_b: f32, c_s: f32) -> f32 {
+ if c_s <= 0.5 {
+ Color::blend_color_burn(2. * c_s, c_b)
+ } else {
+ Color::blend_color_dodge(2. * c_s - 1., c_b)
+ }
+ }
+
+ pub fn blend_linear_light(c_b: f32, c_s: f32) -> f32 {
+ if c_s <= 0.5 {
+ Color::blend_linear_burn(2. * c_s, c_b)
+ } else {
+ Color::blend_linear_dodge(2. * c_s - 1., c_b)
+ }
+ }
+
+ pub fn blend_pin_light(c_b: f32, c_s: f32) -> f32 {
+ if c_s <= 0.5 {
+ Color::blend_darken(2. * c_s, c_b)
+ } else {
+ Color::blend_lighten(2. * c_s - 1., c_b)
+ }
+ }
+
+ pub fn blend_hard_mix(c_b: f32, c_s: f32) -> f32 {
+ if Color::blend_linear_light(c_b, c_s) < 0.5 { 0. } else { 1. }
+ }
+
+ pub fn blend_difference(c_b: f32, c_s: f32) -> f32 {
+ (c_b - c_s).abs()
+ }
+
+ pub fn blend_exclusion(c_b: f32, c_s: f32) -> f32 {
+ c_b + c_s - 2. * c_b * c_s
+ }
+
+ pub fn blend_subtract(c_b: f32, c_s: f32) -> f32 {
+ c_b - c_s
+ }
+
+ pub fn blend_divide(c_b: f32, c_s: f32) -> f32 {
+ if c_b == 0. { 1. } else { c_b / c_s }
+ }
+
+ pub fn blend_hue(&self, c_s: Color) -> Color {
+ let sat_b = self.saturation();
+ let lum_b = self.luminance_rec_601();
+ c_s.with_saturation(sat_b).with_luminance(lum_b)
+ }
+
+ pub fn blend_saturation(&self, c_s: Color) -> Color {
+ let sat_s = c_s.saturation();
+ let lum_b = self.luminance_rec_601();
+
+ self.with_saturation(sat_s).with_luminance(lum_b)
+ }
+
+ pub fn blend_color(&self, c_s: Color) -> Color {
+ let lum_b = self.luminance_rec_601();
+
+ c_s.with_luminance(lum_b)
+ }
+
+ pub fn blend_luminosity(&self, c_s: Color) -> Color {
+ let lum_s = c_s.luminance_rec_601();
+
+ self.with_luminance(lum_s)
+ }
+
+ /// Return the all components as a tuple, first component is red, followed by green, followed by blue, followed by alpha.
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
+ /// assert_eq!(color.components(), (0.114, 0.103, 0.98, 0.97));
+ /// ```
+ #[inline(always)]
+ pub fn components(&self) -> (f32, f32, f32, f32) {
+ (self.red, self.green, self.blue, self.alpha)
+ }
+
+ /// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha. Use this if the [`Color`] is in linear space.
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
+ /// // TODO: Add test
+ /// ```
+ #[inline(always)]
+ pub fn to_rgba8_srgb(&self) -> [u8; 4] {
+ let gamma = self.to_gamma_srgb();
+ [(gamma.red * 255.) as u8, (gamma.green * 255.) as u8, (gamma.blue * 255.) as u8, (gamma.alpha * 255.) as u8]
+ }
+
+ // https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
+ /// Convert a [Color] to a hue, saturation, lightness and alpha (all between 0 and 1)
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.).to_hsla();
+ /// ```
+ pub fn to_hsla(&self) -> [f32; 4] {
+ let min_channel = self.red.min(self.green).min(self.blue);
+ let max_channel = self.red.max(self.green).max(self.blue);
+
+ let lightness = (min_channel + max_channel) / 2.;
+ let saturation = if min_channel == max_channel {
+ 0.
+ } else if lightness <= 0.5 {
+ (max_channel - min_channel) / (max_channel + min_channel)
+ } else {
+ (max_channel - min_channel) / (2. - max_channel - min_channel)
+ };
+ let hue = if self.red >= self.green && self.red >= self.blue {
+ (self.green - self.blue) / (max_channel - min_channel)
+ } else if self.green >= self.red && self.green >= self.blue {
+ 2. + (self.blue - self.red) / (max_channel - min_channel)
+ } else {
+ 4. + (self.red - self.green) / (max_channel - min_channel)
+ } / 6.;
+ #[cfg(not(target_arch = "spirv"))]
+ let hue = hue.rem_euclid(1.);
+ #[cfg(target_arch = "spirv")]
+ let hue = hue.rem_euclid(&1.);
+
+ [hue, saturation, lightness, self.alpha]
+ }
+
+ // TODO: Readd formatting
+
+ /// Creates a color from a 8-character RGBA hex string (without a # prefix).
+ ///
+ /// # Examples
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_rgba_str("7C67FA61").unwrap();
+ /// ```
+ pub fn from_rgba_str(color_str: &str) -> Option {
+ if color_str.len() != 8 {
+ return None;
+ }
+ let r = u8::from_str_radix(&color_str[0..2], 16).ok()?;
+ let g = u8::from_str_radix(&color_str[2..4], 16).ok()?;
+ let b = u8::from_str_radix(&color_str[4..6], 16).ok()?;
+ let a = u8::from_str_radix(&color_str[6..8], 16).ok()?;
+
+ Some(Color::from_rgba8_srgb(r, g, b, a))
+ }
+
+ /// Creates a color from a 6-character RGB hex string (without a # prefix).
+ ///
+ /// ```
+ /// use graphene_core::raster::color::Color;
+ /// let color = Color::from_rgb_str("7C67FA").unwrap();
+ /// ```
+ pub fn from_rgb_str(color_str: &str) -> Option {
+ if color_str.len() != 6 {
+ return None;
+ }
+ let r = u8::from_str_radix(&color_str[0..2], 16).ok()?;
+ let g = u8::from_str_radix(&color_str[2..4], 16).ok()?;
+ let b = u8::from_str_radix(&color_str[4..6], 16).ok()?;
+
+ Some(Color::from_rgb8_srgb(r, g, b))
+ }
+
+ /// Linearly interpolates between two colors based on t.
+ ///
+ /// T must be between 0 and 1.
+ #[inline(always)]
+ pub fn lerp(&self, other: &Color, t: f32) -> Self {
+ assert!((0. ..=1.).contains(&t));
+ Color::from_rgbaf32_unchecked(
+ self.red + ((other.red - self.red) * t),
+ self.green + ((other.green - self.green) * t),
+ self.blue + ((other.blue - self.blue) * t),
+ self.alpha + ((other.alpha - self.alpha) * t),
+ )
+ }
+
+ #[inline(always)]
+ pub fn gamma(&self, gamma: f32) -> Color {
+ let gamma = gamma.max(0.0001);
+
+ // From https://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-6-gamma-correction/
+ let inverse_gamma = 1. / gamma;
+ self.map_rgb(|c: f32| c.powf(inverse_gamma))
+ }
+
+ #[inline(always)]
+ pub fn to_linear_srgb(&self) -> Self {
+ Self {
+ red: Self::srgb_to_linear(self.red),
+ green: Self::srgb_to_linear(self.green),
+ blue: Self::srgb_to_linear(self.blue),
+ alpha: self.alpha,
+ }
+ }
+
+ #[inline(always)]
+ pub fn to_gamma_srgb(&self) -> Self {
+ Self {
+ red: Self::linear_to_srgb(self.red),
+ green: Self::linear_to_srgb(self.green),
+ blue: Self::linear_to_srgb(self.blue),
+ alpha: self.alpha,
+ }
+ }
+
+ #[inline(always)]
+ pub fn srgb_to_linear(channel: f32) -> f32 {
+ if channel <= 0.04045 { channel / 12.92 } else { ((channel + 0.055) / 1.055).powf(2.4) }
+ }
+
+ #[inline(always)]
+ pub fn linear_to_srgb(channel: f32) -> f32 {
+ if channel <= 0.0031308 { channel * 12.92 } else { 1.055 * channel.powf(1. / 2.4) - 0.055 }
+ }
+
+ #[inline(always)]
+ pub fn map_rgba f32>(&self, f: F) -> Self {
+ Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), f(self.a()))
+ }
+
+ #[inline(always)]
+ pub fn map_rgb f32>(&self, f: F) -> Self {
+ Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), self.a())
+ }
+
+ #[inline(always)]
+ pub fn apply_opacity(&self, opacity: f32) -> Self {
+ Self::from_rgbaf32_unchecked(self.r() * opacity, self.g() * opacity, self.b() * opacity, self.a() * opacity)
+ }
+
+ #[inline(always)]
+ pub fn to_associated_alpha(&self, alpha: f32) -> Self {
+ Self {
+ red: self.red * alpha,
+ green: self.green * alpha,
+ blue: self.blue * alpha,
+ alpha: self.alpha * alpha,
+ }
+ }
+
+ #[inline(always)]
+ pub fn to_unassociated_alpha(&self) -> Self {
+ if self.alpha == 0. {
+ return *self;
+ }
+ let unmultiply = 1. / self.alpha;
+ Self {
+ red: self.red * unmultiply,
+ green: self.green * unmultiply,
+ blue: self.blue * unmultiply,
+ alpha: self.alpha,
+ }
+ }
+
+ #[inline(always)]
+ pub fn blend_rgb f32>(&self, other: Color, f: F) -> Self {
+ let background = self.to_unassociated_alpha();
+ Color {
+ red: f(background.red, other.red).clamp(0., 1.),
+ green: f(background.green, other.green).clamp(0., 1.),
+ blue: f(background.blue, other.blue).clamp(0., 1.),
+ alpha: other.alpha,
+ }
+ }
+
+ #[inline(always)]
+ pub fn alpha_blend(&self, other: Color) -> Self {
+ let inv_alpha = 1. - other.alpha;
+ Self {
+ red: self.red * inv_alpha + other.red,
+ green: self.green * inv_alpha + other.green,
+ blue: self.blue * inv_alpha + other.blue,
+ alpha: self.alpha * inv_alpha + other.alpha,
+ }
+ }
+
+ #[inline(always)]
+ pub fn alpha_add(&self, other: Color) -> Self {
+ Self {
+ alpha: (self.alpha + other.alpha).clamp(0., 1.),
+ ..*self
+ }
+ }
+
+ #[inline(always)]
+ pub fn alpha_subtract(&self, other: Color) -> Self {
+ Self {
+ alpha: (self.alpha - other.alpha).clamp(0., 1.),
+ ..*self
+ }
+ }
+
+ #[inline(always)]
+ pub fn alpha_multiply(&self, other: Color) -> Self {
+ Self {
+ alpha: (self.alpha * other.alpha).clamp(0., 1.),
+ ..*self
+ }
+ }
+}
+
+#[test]
+fn hsl_roundtrip() {
+ for (red, green, blue) in [
+ (24, 98, 118),
+ (69, 11, 89),
+ (54, 82, 38),
+ (47, 76, 50),
+ (25, 15, 73),
+ (62, 57, 33),
+ (55, 2, 18),
+ (12, 3, 82),
+ (91, 16, 98),
+ (91, 39, 82),
+ (97, 53, 32),
+ (76, 8, 91),
+ (54, 87, 19),
+ (56, 24, 88),
+ (14, 82, 34),
+ (61, 86, 31),
+ (73, 60, 75),
+ (95, 79, 88),
+ (13, 34, 4),
+ (82, 84, 84),
+ (255, 255, 178),
+ ] {
+ let col = Color::from_rgb8_srgb(red, green, blue);
+ let [hue, saturation, lightness, alpha] = col.to_hsla();
+ let result = Color::from_hsla(hue, saturation, lightness, alpha);
+ assert!((col.r() - result.r()) < f32::EPSILON * 100.);
+ assert!((col.g() - result.g()) < f32::EPSILON * 100.);
+ assert!((col.b() - result.b()) < f32::EPSILON * 100.);
+ assert!((col.a() - result.a()) < f32::EPSILON * 100.);
+ }
+}
diff --git a/node-graph/gcore-shader/src/fullscreen_vertex.rs b/node-graph/gcore-shader/src/fullscreen_vertex.rs
new file mode 100644
index 0000000000..b8ef775b9f
--- /dev/null
+++ b/node-graph/gcore-shader/src/fullscreen_vertex.rs
@@ -0,0 +1,14 @@
+use glam::{Vec2, Vec4};
+use spirv_std::spirv;
+
+/// webgpu NDC is like OpenGL: (-1.0 .. 1.0, -1.0 .. 1.0, 0.0 .. 1.0)
+/// https://www.w3.org/TR/webgpu/#coordinate-systems
+const FULLSCREEN_VERTICES: [Vec2; 3] = [Vec2::new(-1., -1.), Vec2::new(-1., 3.), Vec2::new(3., -1.)];
+
+#[spirv(vertex)]
+pub fn fullscreen_vertex(#[spirv(vertex_index)] vertex_index: u32, #[spirv(position)] gl_position: &mut Vec4) {
+ // broken on edition 2024 branch
+ // let vertex = unsafe { *FULLSCREEN_VERTICES.index_unchecked(vertex_index as usize) };
+ let vertex = FULLSCREEN_VERTICES[vertex_index as usize];
+ *gl_position = Vec4::from((vertex, 0., 1.));
+}
diff --git a/node-graph/gcore-shader/src/gpu_invert.rs b/node-graph/gcore-shader/src/gpu_invert.rs
new file mode 100644
index 0000000000..09fe596710
--- /dev/null
+++ b/node-graph/gcore-shader/src/gpu_invert.rs
@@ -0,0 +1,39 @@
+use crate::color::Color;
+
+// exact copy of the invert node
+// #[node_macro::node(category("Raster: Adjustment"))]
+fn invert_copy(
+ // _: impl Ctx,
+ // #[implementations(
+ // Color,
+ // ImageFrameTable,
+ // GradientStops,
+ // )]
+ // mut input: T,
+ color: Color,
+) -> Color {
+ // input.adjust(|color| {
+ let color = color.to_gamma_srgb();
+
+ let color = color.map_rgb(|c| color.a() - c);
+
+ color.to_linear_srgb()
+ // });
+ // input
+}
+
+pub mod gpu_invert_shader {
+ use crate::color::Color;
+ use crate::gpu_invert::invert_copy;
+ use glam::{Vec4, Vec4Swizzles};
+ use spirv_std::image::sample_with::lod;
+ use spirv_std::image::{Image2d, ImageWithMethods};
+ use spirv_std::spirv;
+
+ #[spirv(fragment)]
+ pub fn gpu_invert_fragment(#[spirv(frag_coord)] frag_coord: Vec4, #[spirv(descriptor_set = 0, binding = 0)] texture: &Image2d, color_out: &mut Vec4) {
+ let color = Color::from(texture.fetch_with(frag_coord.xy().as_uvec2(), lod(0)));
+ let color = invert_copy(color);
+ *color_out = Vec4::from(color);
+ }
+}
diff --git a/node-graph/gcore-shader/src/lib.rs b/node-graph/gcore-shader/src/lib.rs
new file mode 100644
index 0000000000..91a4cde3c2
--- /dev/null
+++ b/node-graph/gcore-shader/src/lib.rs
@@ -0,0 +1,5 @@
+#![no_std]
+
+pub mod color;
+pub mod fullscreen_vertex;
+pub mod gpu_invert;
diff --git a/node-graph/gcore/Cargo.toml b/node-graph/gcore/Cargo.toml
index fa9abbec60..d85494d739 100644
--- a/node-graph/gcore/Cargo.toml
+++ b/node-graph/gcore/Cargo.toml
@@ -47,7 +47,7 @@ num-traits = { workspace = true, default-features = false, features = ["i128"] }
usvg = { workspace = true }
rand = { workspace = true, default-features = false, features = ["std_rng"] }
glam = { workspace = true, default-features = false, features = [
- "scalar-math",
+ "scalar-math", "serde"
] }
serde_json = { workspace = true }
petgraph = { workspace = true, default-features = false, features = [
diff --git a/node-graph/gcore/src/instances.rs b/node-graph/gcore/src/instances.rs
index 8c90777aa8..a65a0c102c 100644
--- a/node-graph/gcore/src/instances.rs
+++ b/node-graph/gcore/src/instances.rs
@@ -18,11 +18,24 @@ pub struct Instances {
impl Instances {
pub fn new(instance: T) -> Self {
+ Self::from(Instance::new(instance))
+ }
+
+ pub fn empty() -> Self {
+ Self {
+ instance: Vec::new(),
+ transform: Vec::new(),
+ alpha_blending: Vec::new(),
+ source_node_id: Vec::new(),
+ }
+ }
+
+ pub fn with_capacity(capacity: usize) -> Self {
Self {
- instance: vec![instance],
- transform: vec![DAffine2::IDENTITY],
- alpha_blending: vec![AlphaBlending::default()],
- source_node_id: vec![None],
+ instance: Vec::with_capacity(capacity),
+ transform: Vec::with_capacity(capacity),
+ alpha_blending: Vec::with_capacity(capacity),
+ source_node_id: Vec::with_capacity(capacity),
}
}
@@ -121,7 +134,7 @@ impl Default for Instances {
}
}
-impl core::hash::Hash for Instances {
+impl Hash for Instances {
fn hash(&self, state: &mut H) {
for instance in &self.instance {
instance.hash(state);
@@ -140,6 +153,29 @@ unsafe impl StaticType for Instances {
type Static = Instances;
}
+impl From> for Instances {
+ fn from(instance: Instance) -> Self {
+ Self {
+ instance: vec![instance.instance],
+ transform: vec![instance.transform],
+ alpha_blending: vec![instance.alpha_blending],
+ source_node_id: vec![instance.source_node_id],
+ }
+ }
+}
+
+impl FromIterator> for Instances {
+ fn from_iter>>(iter: I) -> Self {
+ let iter = iter.into_iter();
+ let (lower, _) = iter.size_hint();
+ let mut instances = Self::with_capacity(lower);
+ for instance in iter {
+ instances.push(instance);
+ }
+ instances
+ }
+}
+
fn one_daffine2_default() -> Vec {
vec![DAffine2::IDENTITY]
}
@@ -189,6 +225,15 @@ pub struct Instance {
}
impl Instance {
+ pub fn new(instance: T) -> Self {
+ Self {
+ instance,
+ transform: DAffine2::IDENTITY,
+ alpha_blending: AlphaBlending::default(),
+ source_node_id: None,
+ }
+ }
+
pub fn to_graphic_element(self) -> Instance
where
T: Into,
diff --git a/node-graph/gpu-executor/Cargo.toml b/node-graph/gpu-executor/Cargo.toml
index 38c6b77b38..6870add2c0 100644
--- a/node-graph/gpu-executor/Cargo.toml
+++ b/node-graph/gpu-executor/Cargo.toml
@@ -17,7 +17,7 @@ dyn-any = { workspace = true, features = ["log-bad-types", "rc", "glam"] }
num-traits = { workspace = true }
log = { workspace = true }
serde = { workspace = true }
-glam = { workspace = true }
+glam = { workspace = true, features = ["serde"] }
base64 = { workspace = true }
bytemuck = { workspace = true }
anyhow = { workspace = true }
diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml
index 35faf28618..653cacf90b 100644
--- a/node-graph/graph-craft/Cargo.toml
+++ b/node-graph/graph-craft/Cargo.toml
@@ -26,7 +26,7 @@ graphene-core = { workspace = true, features = ["std"] }
num-traits = { workspace = true }
log = { workspace = true }
futures = { workspace = true }
-glam = { workspace = true }
+glam = { workspace = true, features = ["serde"] }
base64 = { workspace = true }
bezier-rs = { workspace = true }
specta = { workspace = true }
diff --git a/node-graph/graphene-cli/Cargo.toml b/node-graph/graphene-cli/Cargo.toml
index 40f52e7071..a872878579 100644
--- a/node-graph/graphene-cli/Cargo.toml
+++ b/node-graph/graphene-cli/Cargo.toml
@@ -31,7 +31,7 @@ bitflags = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
bezier-rs = { workspace = true }
-glam = { workspace = true }
+glam = { workspace = true, features = ["serde"] }
graph-craft = { workspace = true, features = ["loading"] }
dyn-any = { workspace = true }
graphene-core = { workspace = true }
diff --git a/node-graph/interpreted-executor/Cargo.toml b/node-graph/interpreted-executor/Cargo.toml
index bafdf68ff4..41150b7cab 100644
--- a/node-graph/interpreted-executor/Cargo.toml
+++ b/node-graph/interpreted-executor/Cargo.toml
@@ -22,7 +22,7 @@ dyn-any = { workspace = true, features = ["log-bad-types", "glam"] }
num-traits = { workspace = true }
log = { workspace = true }
wgpu = { workspace = true }
-glam = { workspace = true }
+glam = { workspace = true, features = ["serde"] }
futures = { workspace = true }
once_cell = { workspace = true }
diff --git a/node-graph/wgpu-executor/Cargo.toml b/node-graph/wgpu-executor/Cargo.toml
index fd53617130..60ac0e61bd 100644
--- a/node-graph/wgpu-executor/Cargo.toml
+++ b/node-graph/wgpu-executor/Cargo.toml
@@ -15,11 +15,13 @@ gpu-executor = { path = "../gpu-executor" }
# Workspace dependencies
graphene-core = { workspace = true, features = ["std", "alloc", "gpu", "wgpu"] }
+# required for cargo watch to pick up changes
+graphene-core-shader = { path = "../gcore-shader" }
dyn-any = { workspace = true, features = ["log-bad-types", "rc", "glam"] }
node-macro = { workspace = true }
num-traits = { workspace = true }
log = { workspace = true }
-glam = { workspace = true }
+glam = { workspace = true, features = ["serde"] }
base64 = { workspace = true }
bytemuck = { workspace = true }
anyhow = { workspace = true }
@@ -40,3 +42,7 @@ half = "2.4.1"
# Optional dependencies
nvtx = { version = "1.3", optional = true }
+
+[build-dependencies]
+cargo-gpu = { workspace = true }
+env_logger = { workspace = true }
diff --git a/node-graph/wgpu-executor/build.rs b/node-graph/wgpu-executor/build.rs
new file mode 100644
index 0000000000..3991a32d48
--- /dev/null
+++ b/node-graph/wgpu-executor/build.rs
@@ -0,0 +1,34 @@
+use cargo_gpu::CompileResultNagaExt;
+use cargo_gpu::naga::back::wgsl::WriterFlags;
+use cargo_gpu::naga::valid::Capabilities;
+use cargo_gpu::spirv_builder::{MetadataPrintout, SpirvMetadata};
+use std::path::PathBuf;
+
+pub fn main() -> Result<(), Box> {
+ env_logger::builder().init();
+
+ let shader_crate = PathBuf::from("../gcore-shader");
+
+ // install the toolchain and build the `rustc_codegen_spirv` codegen backend with it
+ let backend = cargo_gpu::Install::from_shader_crate(shader_crate.clone()).run()?;
+
+ // build the shader crate
+ let mut builder = backend.to_spirv_builder(shader_crate, "spirv-unknown-vulkan1.2");
+ builder.print_metadata = MetadataPrintout::DependencyOnly;
+ builder.spirv_metadata = SpirvMetadata::Full;
+ builder.shader_crate_features.default_features = false;
+ builder.shader_crate_features.features = vec![String::from("gpu")];
+ let spv_result = builder.build()?;
+
+ // transpile the spv binaries to wgsl
+ let wgsl_result = spv_result.naga_transpile(Capabilities::empty())?.to_wgsl(WriterFlags::empty())?;
+ let path_to_wgsl = wgsl_result.module.unwrap_single();
+
+ // emit path to wgsl into env var, used in `quad.rs` like this:
+ // > include_str!(env!("WGSL_SHADER_PATH"))
+ println!("cargo::rustc-env=WGSL_SHADER_PATH={}", path_to_wgsl.display());
+
+ // you could also generate some rust source code into the `std::env::var("OUT_DIR")` dir
+ // and use `include!(concat!(env!("OUT_DIR"), "/shader_symbols.rs"));` to include it
+ Ok(())
+}
diff --git a/node-graph/wgpu-executor/src/gcore_shader_nodes.rs b/node-graph/wgpu-executor/src/gcore_shader_nodes.rs
new file mode 100644
index 0000000000..b8716ef314
--- /dev/null
+++ b/node-graph/wgpu-executor/src/gcore_shader_nodes.rs
@@ -0,0 +1,150 @@
+use crate::{Context, WgpuExecutor};
+use graphene_core::Ctx;
+use graphene_core::application_io::{ImageTexture, TextureFrameTable};
+use graphene_core::instances::Instance;
+use std::borrow::Cow;
+use std::sync::Arc;
+use wgpu::{
+ BindGroupDescriptor, BindGroupEntry, BindingResource, ColorTargetState, Device, Face, FragmentState, FrontFace, LoadOp, Operations, PolygonMode, PrimitiveState, PrimitiveTopology, Queue,
+ RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, ShaderModuleDescriptor, ShaderSource, StoreOp, TextureDescriptor, TextureDimension, TextureFormat,
+ TextureViewDescriptor, VertexState,
+};
+
+const WGSL_SHADER: &str = include_str!(env!("WGSL_SHADER_PATH"));
+
+#[node_macro::node(category(""))]
+async fn gpu_invert<'a: 'n>(_: impl Ctx, input: TextureFrameTable, executor: &'a WgpuExecutor) -> TextureFrameTable {
+ let Context { device, queue, .. } = &executor.context;
+ // this should be cached
+ let graphics_pipeline = GraphitePerPixelGraphicsPipeline::new(device, "gpu_invertgpu_invert_shadergpu_invert_fragment");
+ graphics_pipeline.run(input, queue)
+}
+
+pub struct GraphitePerPixelGraphicsPipeline {
+ device: Arc,
+ render_pipeline_f32: wgpu::RenderPipeline,
+ render_pipeline_f16: wgpu::RenderPipeline,
+ render_pipeline_srgb8: wgpu::RenderPipeline,
+}
+
+impl GraphitePerPixelGraphicsPipeline {
+ pub fn new(device: &Arc, fragment_shader_name: &str) -> Self {
+ let shader_module = device.create_shader_module(ShaderModuleDescriptor {
+ label: Some("graphite wgsl shader"),
+ source: ShaderSource::Wgsl(Cow::Borrowed(WGSL_SHADER)),
+ });
+ let create_render_pipeline = |format| {
+ device.create_render_pipeline(&RenderPipelineDescriptor {
+ label: Some("gpu_invert"),
+ layout: None,
+ vertex: VertexState {
+ module: &shader_module,
+ entry_point: Some("fullscreen_vertexfullscreen_vertex"),
+ compilation_options: Default::default(),
+ buffers: &[],
+ },
+ primitive: PrimitiveState {
+ topology: PrimitiveTopology::TriangleList,
+ strip_index_format: None,
+ front_face: FrontFace::Ccw,
+ cull_mode: Some(Face::Back),
+ unclipped_depth: false,
+ polygon_mode: PolygonMode::Fill,
+ conservative: false,
+ },
+ depth_stencil: None,
+ multisample: Default::default(),
+ fragment: Some(FragmentState {
+ module: &shader_module,
+ entry_point: Some(fragment_shader_name),
+ compilation_options: Default::default(),
+ targets: &[Some(ColorTargetState {
+ format,
+ blend: None,
+ write_mask: Default::default(),
+ })],
+ }),
+ multiview: None,
+ cache: None,
+ })
+ };
+ Self {
+ device: device.clone(),
+ render_pipeline_f32: create_render_pipeline(TextureFormat::Rgba32Float),
+ render_pipeline_f16: create_render_pipeline(TextureFormat::Rgba16Float),
+ render_pipeline_srgb8: create_render_pipeline(TextureFormat::Rgba8UnormSrgb),
+ }
+ }
+
+ pub fn get(&self, format: TextureFormat) -> &wgpu::RenderPipeline {
+ match format {
+ TextureFormat::Rgba32Float => &self.render_pipeline_f32,
+ TextureFormat::Rgba16Float => &self.render_pipeline_f16,
+ TextureFormat::Rgba8UnormSrgb => &self.render_pipeline_srgb8,
+ _ => panic!("unsupported"),
+ }
+ }
+
+ pub fn run(&self, input: TextureFrameTable, queue: &Arc) -> TextureFrameTable {
+ let device = &self.device;
+ let mut cmd = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("gpu_invert") });
+ let out = input
+ .instance_ref_iter()
+ .map(|instance| {
+ let view_in = instance.instance.texture.create_view(&TextureViewDescriptor::default());
+ let format = instance.instance.texture.format();
+ let pipeline = self.get(format);
+
+ let bind_group = device.create_bind_group(&BindGroupDescriptor {
+ label: Some("gpu_invert bind group"),
+ // `get_bind_group_layout` allocates unnecessary memory, we could create it manually to not do that
+ layout: &pipeline.get_bind_group_layout(0),
+ entries: &[BindGroupEntry {
+ binding: 0,
+ resource: BindingResource::TextureView(&view_in),
+ }],
+ });
+
+ let tex_out = device.create_texture(&TextureDescriptor {
+ label: Some("gpu_invert_out"),
+ size: instance.instance.texture.size(),
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: TextureDimension::D2,
+ format,
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::RENDER_ATTACHMENT,
+ view_formats: &[format],
+ });
+
+ let view_out = tex_out.create_view(&TextureViewDescriptor::default());
+ let mut rp = cmd.begin_render_pass(&RenderPassDescriptor {
+ label: Some("gpu_invert rp"),
+ color_attachments: &[Some(RenderPassColorAttachment {
+ view: &view_out,
+ resolve_target: None,
+ ops: Operations {
+ // should be dont_care but wgpu doesn't expose that
+ load: LoadOp::Clear(wgpu::Color::BLACK),
+ store: StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+ rp.set_pipeline(&pipeline);
+ rp.set_bind_group(0, Some(&bind_group), &[]);
+ rp.draw(0..3, 0..1);
+
+ Instance {
+ instance: ImageTexture { texture: Arc::new(tex_out) },
+ transform: *instance.transform,
+ alpha_blending: *instance.alpha_blending,
+ source_node_id: *instance.source_node_id,
+ }
+ })
+ .collect::();
+ queue.submit([cmd.finish()]);
+ out
+ }
+}
diff --git a/node-graph/wgpu-executor/src/lib.rs b/node-graph/wgpu-executor/src/lib.rs
index 9960f9e0c8..6bcf6fb69a 100644
--- a/node-graph/wgpu-executor/src/lib.rs
+++ b/node-graph/wgpu-executor/src/lib.rs
@@ -1,5 +1,6 @@
mod context;
mod executor;
+mod gcore_shader_nodes;
use anyhow::{Result, bail};
pub use context::Context;