From debafb6f7e34571ec140d1c64e9236365cea471c Mon Sep 17 00:00:00 2001
From: Firestar99 <firestar99@sydow.cloud>
Date: Thu, 5 Jun 2025 13:02:00 +0200
Subject: [PATCH 01/10] gitignore .idea/

---
 .gitignore | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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/

From 6f4556033501678c7e61b20e2b2ac841a735676f Mon Sep 17 00:00:00 2001
From: Firestar99 <firestar99@sydow.cloud>
Date: Tue, 3 Jun 2025 14:38:59 +0200
Subject: [PATCH 02/10] move "glam/serde" dependency to individual crates

---
 Cargo.toml                                 | 2 +-
 frontend/wasm/Cargo.toml                   | 2 +-
 node-graph/gcore/Cargo.toml                | 2 +-
 node-graph/gpu-executor/Cargo.toml         | 2 +-
 node-graph/graph-craft/Cargo.toml          | 2 +-
 node-graph/graphene-cli/Cargo.toml         | 2 +-
 node-graph/interpreted-executor/Cargo.toml | 2 +-
 node-graph/wgpu-executor/Cargo.toml        | 2 +-
 8 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 1c50e83ee1..67e450b5a1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -85,7 +85,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"
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/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/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..582413752f 100644
--- a/node-graph/wgpu-executor/Cargo.toml
+++ b/node-graph/wgpu-executor/Cargo.toml
@@ -19,7 +19,7 @@ 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 }

From eb8605a0b3cd2a03895f250daa2ed21c1a8fbcab Mon Sep 17 00:00:00 2001
From: Firestar99 <firestar99@sydow.cloud>
Date: Thu, 5 Jun 2025 14:21:26 +0200
Subject: [PATCH 03/10] Instances extension

---
 node-graph/gcore/src/instances.rs | 55 ++++++++++++++++++++++++++++---
 1 file changed, 50 insertions(+), 5 deletions(-)

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<T> {
 
 impl<T> Instances<T> {
 	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<T> Default for Instances<T> {
 	}
 }
 
-impl<T: Hash> core::hash::Hash for Instances<T> {
+impl<T: Hash> Hash for Instances<T> {
 	fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
 		for instance in &self.instance {
 			instance.hash(state);
@@ -140,6 +153,29 @@ unsafe impl<T: StaticType + 'static> StaticType for Instances<T> {
 	type Static = Instances<T>;
 }
 
+impl<T> From<Instance<T>> for Instances<T> {
+	fn from(instance: Instance<T>) -> 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<T> FromIterator<Instance<T>> for Instances<T> {
+	fn from_iter<I: IntoIterator<Item = Instance<T>>>(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<DAffine2> {
 	vec![DAffine2::IDENTITY]
 }
@@ -189,6 +225,15 @@ pub struct Instance<T> {
 }
 
 impl<T> Instance<T> {
+	pub fn new(instance: T) -> Self {
+		Self {
+			instance,
+			transform: DAffine2::IDENTITY,
+			alpha_blending: AlphaBlending::default(),
+			source_node_id: None,
+		}
+	}
+
 	pub fn to_graphic_element<U>(self) -> Instance<U>
 	where
 		T: Into<U>,

From bf2673428ae3260e4b55204bc930ab01ed8591d3 Mon Sep 17 00:00:00 2001
From: Firestar99 <firestar99@sydow.cloud>
Date: Thu, 5 Jun 2025 16:00:36 +0200
Subject: [PATCH 04/10] gpu invert node demo

---
 Cargo.lock                                    | 409 ++++++++-
 Cargo.toml                                    |   4 +-
 .../node_graph/document_node_definitions.rs   |  86 ++
 node-graph/gcore-shader/Cargo.toml            |  27 +
 node-graph/gcore-shader/src/color.rs          | 782 ++++++++++++++++++
 .../gcore-shader/src/fullscreen_vertex.rs     |  14 +
 node-graph/gcore-shader/src/gpu_invert.rs     |  39 +
 node-graph/gcore-shader/src/lib.rs            |   5 +
 node-graph/wgpu-executor/Cargo.toml           |   6 +
 node-graph/wgpu-executor/build.rs             |  34 +
 .../wgpu-executor/src/gcore_shader_nodes.rs   | 150 ++++
 node-graph/wgpu-executor/src/lib.rs           |   1 +
 12 files changed, 1531 insertions(+), 26 deletions(-)
 create mode 100644 node-graph/gcore-shader/Cargo.toml
 create mode 100644 node-graph/gcore-shader/src/color.rs
 create mode 100644 node-graph/gcore-shader/src/fullscreen_vertex.rs
 create mode 100644 node-graph/gcore-shader/src/gpu_invert.rs
 create mode 100644 node-graph/gcore-shader/src/lib.rs
 create mode 100644 node-graph/wgpu-executor/build.rs
 create mode 100644 node-graph/wgpu-executor/src/gcore_shader_nodes.rs

diff --git a/Cargo.lock b/Cargo.lock
index 0c347cf8f7..f04ee9c183 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=7f5358bf3bac7363c8017409942464365ca61fd8#7f5358bf3bac7363c8017409942464365ca61fd8"
+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 67e450b5a1..c8a9f2dac9 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` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>) is stabilized in Rust 1.80 and we bump our MSRV
@@ -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 = "7f5358bf3bac7363c8017409942464365ca61fd8", 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<DocumentNodeDefinition> {
 			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/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 <contact@graphite.rs>"]
+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<Vec4> for Color {
+	fn from(value: Vec4) -> Self {
+		Color {
+			red: value.x,
+			green: value.y,
+			blue: value.z,
+			alpha: value.w,
+		}
+	}
+}
+
+impl From<Color> 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<H: core::hash::Hasher>(&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<Color> {
+		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<Color> {
+		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<Color> {
+		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<F: Fn(f32) -> 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<F: Fn(f32) -> 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<F: Fn(f32, f32) -> 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<Color>,
+	// 	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/wgpu-executor/Cargo.toml b/node-graph/wgpu-executor/Cargo.toml
index 582413752f..60ac0e61bd 100644
--- a/node-graph/wgpu-executor/Cargo.toml
+++ b/node-graph/wgpu-executor/Cargo.toml
@@ -15,6 +15,8 @@ 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 }
@@ -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<dyn std::error::Error>> {
+	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<Device>,
+	render_pipeline_f32: wgpu::RenderPipeline,
+	render_pipeline_f16: wgpu::RenderPipeline,
+	render_pipeline_srgb8: wgpu::RenderPipeline,
+}
+
+impl GraphitePerPixelGraphicsPipeline {
+	pub fn new(device: &Arc<Device>, 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<Queue>) -> 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::<TextureFrameTable>();
+		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;

From 709a40799f7afc4168ef6bb9740e14b1ca895bfe Mon Sep 17 00:00:00 2001
From: Firestar99 <firestar99@sydow.cloud>
Date: Thu, 5 Jun 2025 16:31:35 +0200
Subject: [PATCH 05/10] update rust-gpu to fix ci fail

---
 Cargo.lock | 2 +-
 Cargo.toml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index f04ee9c183..cbe08b97c6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -646,7 +646,7 @@ dependencies = [
 [[package]]
 name = "cargo-gpu"
 version = "0.1.0"
-source = "git+https://github.com/rust-gpu/cargo-gpu?rev=7f5358bf3bac7363c8017409942464365ca61fd8#7f5358bf3bac7363c8017409942464365ca61fd8"
+source = "git+https://github.com/rust-gpu/cargo-gpu?rev=d8ef9f58257622c4f3eae215dc950f0c628fa3c1#d8ef9f58257622c4f3eae215dc950f0c628fa3c1"
 dependencies = [
  "anyhow",
  "cargo_metadata",
diff --git a/Cargo.toml b/Cargo.toml
index c8a9f2dac9..2f595c980e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -109,7 +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 = "7f5358bf3bac7363c8017409942464365ca61fd8", features = ["wgsl-out"] }
+cargo-gpu = { git = "https://github.com/rust-gpu/cargo-gpu", rev = "d8ef9f58257622c4f3eae215dc950f0c628fa3c1", features = ["wgsl-out"] }
 
 [profile.dev]
 opt-level = 1

From 17f1fa1f48dd3c5964ae6de248861db1baa579d8 Mon Sep 17 00:00:00 2001
From: Firestar99 <firestar99@sydow.cloud>
Date: Sat, 7 Jun 2025 12:27:25 +0200
Subject: [PATCH 06/10] make spirv-std git repo lowercase

---
 Cargo.lock | 6 +++---
 Cargo.toml | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index cbe08b97c6..34d54b3074 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6264,7 +6264,7 @@ dependencies = [
 [[package]]
 name = "spirv-std"
 version = "0.9.0"
-source = "git+https://github.com/Rust-GPU/rust-gpu?rev=9a357691334b9cdd13c82a740ced97c5d857bf4d#9a357691334b9cdd13c82a740ced97c5d857bf4d"
+source = "git+https://github.com/rust-gpu/rust-gpu?rev=9a357691334b9cdd13c82a740ced97c5d857bf4d#9a357691334b9cdd13c82a740ced97c5d857bf4d"
 dependencies = [
  "bitflags 1.3.2",
  "glam",
@@ -6277,7 +6277,7 @@ dependencies = [
 [[package]]
 name = "spirv-std-macros"
 version = "0.9.0"
-source = "git+https://github.com/Rust-GPU/rust-gpu?rev=9a357691334b9cdd13c82a740ced97c5d857bf4d#9a357691334b9cdd13c82a740ced97c5d857bf4d"
+source = "git+https://github.com/rust-gpu/rust-gpu?rev=9a357691334b9cdd13c82a740ced97c5d857bf4d#9a357691334b9cdd13c82a740ced97c5d857bf4d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -6288,7 +6288,7 @@ dependencies = [
 [[package]]
 name = "spirv-std-types"
 version = "0.9.0"
-source = "git+https://github.com/Rust-GPU/rust-gpu?rev=9a357691334b9cdd13c82a740ced97c5d857bf4d#9a357691334b9cdd13c82a740ced97c5d857bf4d"
+source = "git+https://github.com/rust-gpu/rust-gpu?rev=9a357691334b9cdd13c82a740ced97c5d857bf4d#9a357691334b9cdd13c82a740ced97c5d857bf4d"
 
 [[package]]
 name = "stable_deref_trait"
diff --git a/Cargo.toml b/Cargo.toml
index 2f595c980e..759c50b3db 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -70,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", rev = "9a357691334b9cdd13c82a740ced97c5d857bf4d" }
+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` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>) is stabilized in Rust 1.80 and we bump our MSRV

From 9cf82cc7d1a9ac43168cc51784a04ceb90e41775 Mon Sep 17 00:00:00 2001
From: Firestar99 <firestar99@sydow.cloud>
Date: Sat, 7 Jun 2025 12:36:21 +0200
Subject: [PATCH 07/10] retrigger ci


From 239082baa0d58a9169f43120133d43cc490e8db2 Mon Sep 17 00:00:00 2001
From: Keavon Chambers <keavon@keavon.com>
Date: Sat, 7 Jun 2025 03:40:17 -0700
Subject: [PATCH 08/10] another attempt to trigger CI


From e15080cacd9269632a50b2c863fb37d6c3275068 Mon Sep 17 00:00:00 2001
From: Firestar99 <firestar99@sydow.cloud>
Date: Sat, 7 Jun 2025 12:42:03 +0200
Subject: [PATCH 09/10] retrigger ci2


From 6e5f28632a1f754fd127858a939b998906aecad1 Mon Sep 17 00:00:00 2001
From: Firestar99 <firestar99@sydow.cloud>
Date: Sat, 7 Jun 2025 13:16:59 +0200
Subject: [PATCH 10/10] ci: run on PRs not targeting master

---
 .github/workflows/build-dev-and-ci.yml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

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: <script defer data-domain="dev.graphite.rs" data-api="https://graphite.rs/visit/event" src="https://graphite.rs/visit/script.hash.js"></script>