diff --git a/.github/workflows/build-dev-and-ci.yml b/.github/workflows/build-dev-and-ci.yml index c55019fa2b..52beaa02fb 100644 --- a/.github/workflows/build-dev-and-ci.yml +++ b/.github/workflows/build-dev-and-ci.yml @@ -108,7 +108,7 @@ jobs: - name: 🧪 Run Rust tests run: | - mold -run cargo test --all-features --workspace + mold -run cargo test --all-features - name: 📃 Generate code documentation info for website if: github.ref == 'refs/heads/master' diff --git a/.nix/flake.nix b/.nix/flake.nix index ba9bd62714..eac77dd2c8 100644 --- a/.nix/flake.nix +++ b/.nix/flake.nix @@ -34,11 +34,52 @@ inherit system overlays; }; - rustc-wasm = pkgs.rust-bin.stable.latest.default.override { + rustExtensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ]; + rust = pkgs.rust-bin.stable.latest.default.override { targets = [ "wasm32-unknown-unknown" ]; - extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ]; + extensions = rustExtensions; }; + rustGPUToolchainPkg = pkgs.rust-bin.nightly."2025-06-23".default.override { + extensions = rustExtensions ++ [ "rustc-dev" "llvm-tools" ]; + }; + rustGPUToolchainRustPlatform = pkgs.makeRustPlatform { + cargo = rustGPUToolchainPkg; + rustc = rustGPUToolchainPkg; + }; + rustc_codegen_spirv = rustGPUToolchainRustPlatform.buildRustPackage (finalAttrs: { + pname = "rustc_codegen_spirv"; + version = "0-unstable-2025-08-04"; + src = pkgs.fetchFromGitHub { + owner = "Rust-GPU"; + repo = "rust-gpu"; + rev = "df1628a032d22c864397417c2871b74d602af986"; + hash = "sha256-AFt3Nc+NqK8DxNUhDBcOUmk3XDVcoToVeFIMYNszdbY="; + }; + cargoHash = "sha256-en3BYJWQabH064xeAwYQrvcr6EuWg/QjvsG+Jd6HHCk"; + cargoBuildFlags = [ "-p" "rustc_codegen_spirv" "--features=use-installed-tools" "--no-default-features" ]; + doCheck = false; + }); + + # Wrapper script for running rust commands with the rust toolchain used by rust-gpu. + # For example `rust-gpu cargo --version` or `rust-gpu rustc --version`. + execWithRustGPUEnvironment = pkgs.writeShellScriptBin "rust-gpu" '' + #!${pkgs.lib.getExe pkgs.bash} + + filtered_args=() + for arg in "$@"; do + case "$arg" in + +nightly|+nightly-*) ;; + *) filtered_args+=("$arg") ;; + esac + done + + export PATH="${pkgs.lib.makeBinPath [ rustGPUToolchainPkg pkgs.spirv-tools ]}:$PATH" + export RUSTC_CODEGEN_SPIRV_PATH="${rustc_codegen_spirv}/lib/librustc_codegen_spirv.so" + + exec ${"\${filtered_args[@]}"} + ''; + libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: { version = "139.0.17"; gitRevision = "6c347eb"; @@ -51,7 +92,6 @@ strip $out/lib/* ''; }); - libcefPath = pkgs.runCommand "libcef-path" {} '' mkdir -p $out @@ -85,7 +125,7 @@ # Development tools that don't need to be in LD_LIBRARY_PATH buildTools = [ - rustc-wasm + rust pkgs.nodejs pkgs.nodePackages.npm pkgs.binaryen @@ -97,6 +137,8 @@ # Linker pkgs.mold + + execWithRustGPUEnvironment ]; # Development tools that don't need to be in LD_LIBRARY_PATH devTools = with pkgs; [ diff --git a/Cargo.lock b/Cargo.lock index f23a90db8b..692082f54a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -643,6 +643,75 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "camino" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-gpu" +version = "0.1.0" +source = "git+https://github.com/rust-gpu/cargo-gpu?rev=e8ba9a0421c27a715277da116a6f2d59cdd3266d#e8ba9a0421c27a715277da116a6f2d59cdd3266d" +dependencies = [ + "anyhow", + "cargo_metadata", + "clap", + "crossterm", + "directories", + "env_logger", + "log", + "relative-path", + "rustc_codegen_spirv-target-specs 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "semver", + "serde", + "serde_json", + "spirv-builder", +] + +[[package]] +name = "cargo-platform" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84982c6c0ae343635a3a4ee6dedef965513735c8b183caa7289fa6e27399ebd4" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-util-schemas" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dc1a6f7b5651af85774ae5a34b4e8be397d9cf4bc063b7e6dbd99a841837830" +dependencies = [ + "semver", + "serde", + "serde-untagged", + "serde-value", + "thiserror 2.0.12", + "toml", + "unicode-xid", + "url", +] + +[[package]] +name = "cargo_metadata" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cfca2aaa699835ba88faf58a06342a314a950d2b9686165e038286c30316868" +dependencies = [ + "camino", + "cargo-platform", + "cargo-util-schemas", + "semver", + "serde", + "serde_json", + "thiserror 2.0.12", +] + [[package]] name = "cast" version = "0.3.0" @@ -1038,6 +1107,33 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.0.7", + "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.4" @@ -1099,6 +1195,27 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "diff" version = "0.1.13" @@ -1115,6 +1232,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1333,6 +1459,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.13" @@ -1584,6 +1720,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 = "futures" version = "0.3.31" @@ -1977,10 +2122,13 @@ dependencies = [ "graphene-core", "half", "log", + "node-macro", "num-derive", "num-traits", + "num_enum", "serde", "specta", + "spirv-std", ] [[package]] @@ -2020,16 +2168,28 @@ dependencies = [ "glam", "graphene-core", "graphene-core-shaders", + "graphene-raster-nodes-shaders", "image", "kurbo", "ndarray", "node-macro", "num-traits", + "num_enum", "rand 0.9.1", "rand_chacha 0.9.0", "serde", "specta", + "spirv-std", "tokio", + "wgpu-executor", +] + +[[package]] +name = "graphene-raster-nodes-shaders" +version = "0.1.0" +dependencies = [ + "cargo-gpu", + "env_logger", ] [[package]] @@ -2087,8 +2247,8 @@ dependencies = [ "dirs", "futures", "glam", + "graphite-desktop-embedded-resources", "graphite-desktop-wrapper", - "include_dir", "libc", "objc2-io-surface", "objc2-metal 0.3.1", @@ -2104,6 +2264,13 @@ dependencies = [ "winit", ] +[[package]] +name = "graphite-desktop-embedded-resources" +version = "0.1.0" +dependencies = [ + "include_dir", +] + [[package]] name = "graphite-desktop-wrapper" version = "0.1.0" @@ -2662,6 +2829,26 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.1", + "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 = "interpolate_name" version = "0.2.4" @@ -2868,6 +3055,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 = "kurbo" version = "0.11.2" @@ -2925,9 +3132,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -3130,6 +3337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -3155,7 +3363,7 @@ dependencies = [ "petgraph 0.8.2", "rustc-hash 1.1.0", "spirv", - "strum", + "strum 0.26.3", "thiserror 2.0.12", "unicode-ident", ] @@ -3261,7 +3469,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "strum", + "strum 0.26.3", "syn 2.0.104", ] @@ -3281,6 +3489,30 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.9.1", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3768,6 +4000,15 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-float" version = "4.6.0" @@ -4440,6 +4681,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" @@ -4565,6 +4812,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0" +dependencies = [ + "serde", +] + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -4705,6 +4961,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.25" @@ -4723,6 +4989,33 @@ 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-target-specs" +version = "0.9.0" +source = "git+https://github.com/rust-gpu/rust-gpu?rev=3f05f5482824e3b1fbb44c9ef90a8795a0204c7c#3f05f5482824e3b1fbb44c9ef90a8795a0204c7c" +dependencies = [ + "serde", + "strum 0.27.2", + "thiserror 2.0.12", +] + +[[package]] +name = "rustc_codegen_spirv-types" +version = "0.9.0" +source = "git+https://github.com/rust-gpu/rust-gpu?rev=3f05f5482824e3b1fbb44c9ef90a8795a0204c7c#3f05f5482824e3b1fbb44c9ef90a8795a0204c7c" +dependencies = [ + "rspirv", + "serde", + "serde_json", + "spirv", +] + [[package]] name = "rustix" version = "0.38.44" @@ -4895,6 +5188,9 @@ name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] [[package]] name = "serde" @@ -4905,6 +5201,27 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float 2.10.1", + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -4929,9 +5246,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -5003,6 +5320,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.5" @@ -5180,8 +5518,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ "bitflags 2.9.1", + "serde", +] + +[[package]] +name = "spirv-builder" +version = "0.9.0" +source = "git+https://github.com/rust-gpu/rust-gpu?rev=3f05f5482824e3b1fbb44c9ef90a8795a0204c7c#3f05f5482824e3b1fbb44c9ef90a8795a0204c7c" +dependencies = [ + "cargo_metadata", + "clap", + "log", + "memchr", + "notify", + "raw-string", + "rustc_codegen_spirv-target-specs 0.9.0 (git+https://github.com/rust-gpu/rust-gpu?rev=3f05f5482824e3b1fbb44c9ef90a8795a0204c7c)", + "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?rev=3f05f5482824e3b1fbb44c9ef90a8795a0204c7c#3f05f5482824e3b1fbb44c9ef90a8795a0204c7c" +dependencies = [ + "bitflags 1.3.2", + "bytemuck", + "glam", + "libm", + "num-traits", + "spirv-std-macros", + "spirv-std-types", +] + +[[package]] +name = "spirv-std-macros" +version = "0.9.0" +source = "git+https://github.com/rust-gpu/rust-gpu?rev=3f05f5482824e3b1fbb44c9ef90a8795a0204c7c#3f05f5482824e3b1fbb44c9ef90a8795a0204c7c" +dependencies = [ + "proc-macro2", + "quote", + "spirv-std-types", + "syn 2.0.104", +] + +[[package]] +name = "spirv-std-types" +version = "0.9.0" +source = "git+https://github.com/rust-gpu/rust-gpu?rev=3f05f5482824e3b1fbb44c9ef90a8795a0204c7c#3f05f5482824e3b1fbb44c9ef90a8795a0204c7c" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -5215,7 +5603,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -5231,6 +5628,18 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5639,9 +6048,16 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -5769,6 +6185,12 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.18.0" @@ -5846,6 +6268,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unit-prefix" version = "0.5.1" @@ -6427,7 +6855,7 @@ dependencies = [ "naga", "ndk-sys 0.5.0+25.2.9519653", "objc", - "ordered-float", + "ordered-float 4.6.0", "parking_lot", "portable-atomic", "profiling", diff --git a/Cargo.toml b/Cargo.toml index 5a19b8907a..3d265fe26f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,43 +1,57 @@ [workspace] members = [ - "editor", "desktop", "desktop/wrapper", - "proc-macros", + "desktop/embedded-resources", + "editor", "frontend/wasm", + "libraries/dyn-any", + "libraries/path-bool", + "libraries/math-parser", "node-graph/gapplication-io", "node-graph/gbrush", "node-graph/gcore", "node-graph/gcore-shaders", - "node-graph/gstd", "node-graph/gmath-nodes", "node-graph/gpath-bool", "node-graph/graph-craft", "node-graph/graphene-cli", "node-graph/graster-nodes", + "node-graph/graster-nodes/shaders", + "node-graph/gstd", "node-graph/gsvg-renderer", "node-graph/interpreted-executor", "node-graph/node-macro", "node-graph/preprocessor", - "libraries/dyn-any", - "libraries/path-bool", - "libraries/math-parser", + "node-graph/wgpu-executor", + "proc-macros", ] default-members = [ + "desktop", + "desktop/wrapper", "editor", "frontend/wasm", + "libraries/dyn-any", + "libraries/path-bool", + "libraries/math-parser", + "node-graph/gapplication-io", "node-graph/gbrush", "node-graph/gcore", "node-graph/gcore-shaders", - "node-graph/gstd", + "node-graph/graster-nodes/shaders", "node-graph/gmath-nodes", "node-graph/gpath-bool", "node-graph/graph-craft", "node-graph/graphene-cli", "node-graph/graster-nodes", + "node-graph/gstd", "node-graph/gsvg-renderer", "node-graph/interpreted-executor", "node-graph/node-macro", + "node-graph/preprocessor", + "node-graph/wgpu-executor", + # blocked by https://github.com/rust-lang/cargo/issues/15890 +# "proc-macros", ] resolver = "2" @@ -70,7 +84,7 @@ graphite-proc-macros = { path = "proc-macros" } # Workspace dependencies rustc-hash = "2.0" -bytemuck = { version = "1.13", features = ["derive"] } +bytemuck = { version = "1.13", features = ["derive", "min_const_generics"] } serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" serde-wasm-bindgen = "0.6" @@ -140,7 +154,7 @@ parley = "0.5.0" skrifa = "0.32.0" pretty_assertions = "1.4.1" fern = { version = "0.7", features = ["colored"] } -num_enum = "0.7" +num_enum = { version = "0.7", default-features = false } num-derive = "0.4" num-traits = { version = "0.2", default-features = false, features = ["libm"] } specta = { version = "2.0.0-rc.22", features = [ @@ -178,6 +192,11 @@ tracing = "0.1.41" rfd = "0.15.4" open = "5.3.2" poly-cool = "0.2.0" +spirv-std = { git = "https://github.com/rust-gpu/rust-gpu", rev = "3f05f5482824e3b1fbb44c9ef90a8795a0204c7c", features = ["bytemuck"] } +cargo-gpu = { git = "https://github.com/rust-gpu/cargo-gpu", rev = "e8ba9a0421c27a715277da116a6f2d59cdd3266d" } + +[workspace.lints.rust] +unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] } [profile.dev] opt-level = 1 diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 1ddf52622e..fe97a9d1fa 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -9,7 +9,9 @@ edition = "2024" rust-version = "1.87" [features] -default = ["gpu", "accelerated_paint"] +default = ["recommended", "embedded_resources"] +recommended = ["gpu", "accelerated_paint"] +embedded_resources = ["dep:graphite-desktop-embedded-resources"] gpu = ["graphite-desktop-wrapper/gpu"] # Hardware acceleration features @@ -19,15 +21,15 @@ accelerated_paint_d3d11 = ["windows", "ash"] accelerated_paint_iosurface = ["objc2-io-surface", "objc2-metal", "core-foundation"] [dependencies] -# # Local dependencies +# Local dependencies graphite-desktop-wrapper = { path = "wrapper" } +graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true } wgpu = { workspace = true } winit = { workspace = true, features = ["serde"] } thiserror = { workspace = true } futures = { workspace = true } cef = { workspace = true } -include_dir = { workspace = true } tracing-subscriber = { workspace = true } tracing = { workspace = true } dirs = { workspace = true } diff --git a/desktop/embedded-resources/Cargo.toml b/desktop/embedded-resources/Cargo.toml new file mode 100644 index 0000000000..9b061b9cf3 --- /dev/null +++ b/desktop/embedded-resources/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "graphite-desktop-embedded-resources" +version = "0.1.0" +description = "Graphite Desktop Embedded Resources" +authors = ["Graphite Authors "] +license = "Apache-2.0" +repository = "" +edition = "2024" +rust-version = "1.87" + +[dependencies] +include_dir = { workspace = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(embedded_resources)'] } diff --git a/desktop/embedded-resources/build.rs b/desktop/embedded-resources/build.rs new file mode 100644 index 0000000000..899780fcaa --- /dev/null +++ b/desktop/embedded-resources/build.rs @@ -0,0 +1,17 @@ +const RESOURCES: &str = "../../frontend/dist"; + +// Check if the directory `RESOURCES` exists and sets the embedded_resources cfg accordingly +// Absolute path of `RESOURCES` available via the `EMBEDDED_RESOURCES` environment variable +fn main() { + let crate_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + + println!("cargo:rerun-if-changed={RESOURCES}"); + if let Ok(resources) = crate_dir.join(RESOURCES).canonicalize() + && resources.exists() + { + println!("cargo:rustc-cfg=embedded_resources"); + println!("cargo:rustc-env=EMBEDDED_RESOURCES={}", resources.to_string_lossy()); + } else { + println!("cargo:warning=Resource directory does not exist. Resources will not be embedded. Did you forget to build the frontend?"); + } +} diff --git a/desktop/embedded-resources/src/lib.rs b/desktop/embedded-resources/src/lib.rs new file mode 100644 index 0000000000..fb007d8138 --- /dev/null +++ b/desktop/embedded-resources/src/lib.rs @@ -0,0 +1,10 @@ +//! This crate provides `EMBEDDED_RESOURCES` that can be included in the desktop application binary. +//! It is intended to be used by the `embedded_resources` feature of the `graphite-desktop` crate. +//! The build script checks if the specified resources directory exists and sets the `embedded_resources` cfg flag accordingly. +//! If the resources directory does not exist, resources will not be embedded and a warning will be reported during compilation. + +#[cfg(embedded_resources)] +pub static EMBEDDED_RESOURCES: Option = Some(include_dir::include_dir!("$EMBEDDED_RESOURCES")); + +#[cfg(not(embedded_resources))] +pub static EMBEDDED_RESOURCES: Option = None; diff --git a/desktop/src/cef.rs b/desktop/src/cef.rs index 11c83e06b0..cbe60c2191 100644 --- a/desktop/src/cef.rs +++ b/desktop/src/cef.rs @@ -16,6 +16,9 @@ use crate::CustomEvent; use crate::render::FrameBufferRef; use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message}; +use std::fs::File; +use std::io::{Cursor, Read}; +use std::path::PathBuf; use std::sync::mpsc::Receiver; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -27,7 +30,6 @@ mod input; mod internal; mod ipc; mod platform; -mod scheme_handler; mod utility; #[cfg(feature = "accelerated_paint")] @@ -38,11 +40,12 @@ use texture_import::SharedTextureHandle; pub(crate) use context::{CefContext, CefContextBuilder, InitError}; use winit::event_loop::EventLoopProxy; -pub(crate) trait CefEventHandler: Clone { +pub(crate) trait CefEventHandler: Clone + Send + Sync + 'static { fn window_size(&self) -> WindowSize; fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>); #[cfg(feature = "accelerated_paint")] fn draw_gpu(&self, shared_texture: SharedTextureHandle); + fn load_resource(&self, path: PathBuf) -> Option; /// Scheudule the main event loop to run the cef event loop after the timeout /// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation. fn schedule_cef_message_loop_work(&self, scheduled_time: Instant); @@ -62,12 +65,34 @@ impl WindowSize { } } +#[derive(Clone)] +pub(crate) struct Resource { + pub(crate) reader: ResourceReader, + pub(crate) mimetype: Option, +} + +#[expect(dead_code)] +#[derive(Clone)] +pub(crate) enum ResourceReader { + Embedded(Cursor<&'static [u8]>), + File(Arc), +} +impl Read for ResourceReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self { + ResourceReader::Embedded(cursor) => cursor.read(buf), + ResourceReader::File(file) => file.as_ref().read(buf), + } + } +} + #[derive(Clone)] pub(crate) struct CefHandler { window_size_receiver: Arc>, event_loop_proxy: EventLoopProxy, wgpu_context: WgpuContext, } + struct WindowSizeReceiver { receiver: Receiver, window_size: WindowSize, @@ -142,6 +167,73 @@ impl CefEventHandler for CefHandler { let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture)); } + #[cfg(feature = "accelerated_paint")] + fn draw_gpu(&self, shared_texture: SharedTextureHandle) { + match shared_texture.import_texture(&self.wgpu_context.device) { + Ok(texture) => { + let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture)); + } + Err(e) => { + tracing::error!("Failed to import shared texture: {}", e); + } + } + } + + fn load_resource(&self, path: PathBuf) -> Option { + let path = if path.as_os_str().is_empty() { PathBuf::from("index.html") } else { path }; + + let mimetype = match path.extension().and_then(|s| s.to_str()).unwrap_or("") { + "html" => Some("text/html".to_string()), + "css" => Some("text/css".to_string()), + "txt" => Some("text/plain".to_string()), + "wasm" => Some("application/wasm".to_string()), + "js" => Some("application/javascript".to_string()), + "png" => Some("image/png".to_string()), + "jpg" | "jpeg" => Some("image/jpeg".to_string()), + "svg" => Some("image/svg+xml".to_string()), + "xml" => Some("application/xml".to_string()), + "json" => Some("application/json".to_string()), + "ico" => Some("image/x-icon".to_string()), + "woff" => Some("font/woff".to_string()), + "woff2" => Some("font/woff2".to_string()), + "ttf" => Some("font/ttf".to_string()), + "otf" => Some("font/otf".to_string()), + "webmanifest" => Some("application/manifest+json".to_string()), + "graphite" => Some("application/graphite+json".to_string()), + _ => None, + }; + + #[cfg(feature = "embedded_resources")] + { + if let Some(resources) = &graphite_desktop_embedded_resources::EMBEDDED_RESOURCES + && let Some(file) = resources.get_file(&path) + { + return Some(Resource { + reader: ResourceReader::Embedded(Cursor::new(file.contents())), + mimetype, + }); + } + } + + #[cfg(not(feature = "embedded_resources"))] + { + use std::path::Path; + let asset_path_env = std::env::var("GRAPHITE_RESOURCES").ok()?; + let asset_path = Path::new(&asset_path_env); + let file_path = asset_path.join(path.strip_prefix("/").unwrap_or(&path)); + if file_path.exists() && file_path.is_file() { + if let Ok(file) = std::fs::File::open(file_path) { + return Some(Resource { + reader: ResourceReader::File(file.into()), + mimetype, + }); + } + } + } + + None + } + fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) { let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time)); } @@ -157,16 +249,4 @@ impl CefEventHandler for CefHandler { }; let _ = self.event_loop_proxy.send_event(CustomEvent::DesktopWrapperMessage(desktop_wrapper_message)); } - - #[cfg(feature = "accelerated_paint")] - fn draw_gpu(&self, shared_texture: SharedTextureHandle) { - match shared_texture.import_texture(&self.wgpu_context.device) { - Ok(texture) => { - let _ = self.event_loop_proxy.send_event(CustomEvent::UiUpdate(texture)); - } - Err(e) => { - tracing::error!("Failed to import shared texture: {}", e); - } - } - } } diff --git a/desktop/src/cef/consts.rs b/desktop/src/cef/consts.rs index a0031c8852..99b269c744 100644 --- a/desktop/src/cef/consts.rs +++ b/desktop/src/cef/consts.rs @@ -1,2 +1,2 @@ -pub(crate) const GRAPHITE_SCHEME: &str = "graphite-static"; -pub(crate) const FRONTEND_DOMAIN: &str = "frontend"; +pub(crate) const RESOURCE_SCHEME: &str = "resources"; +pub(crate) const RESOURCE_DOMAIN: &str = "resources"; diff --git a/desktop/src/cef/context/builder.rs b/desktop/src/cef/context/builder.rs index c1a92b92ed..c771c4aecf 100644 --- a/desktop/src/cef/context/builder.rs +++ b/desktop/src/cef/context/builder.rs @@ -6,20 +6,21 @@ use cef::{ use super::CefContext; use super::singlethreaded::SingleThreadedCefContext; -use crate::cef::CefHandler; -use crate::cef::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME}; +use crate::cef::CefEventHandler; +use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME}; use crate::cef::dirs::{cef_cache_dir, cef_data_dir}; use crate::cef::input::InputState; use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderHandlerImpl, RenderProcessAppImpl}; -pub(crate) struct CefContextBuilder { +pub(crate) struct CefContextBuilder { pub(crate) args: Args, pub(crate) is_sub_process: bool, + _marker: std::marker::PhantomData, } -unsafe impl Send for CefContextBuilder {} +unsafe impl Send for CefContextBuilder {} -impl CefContextBuilder { +impl CefContextBuilder { pub(crate) fn new() -> Self { #[cfg(target_os = "macos")] let _loader = { @@ -34,7 +35,11 @@ impl CefContextBuilder { let switch = CefString::from("type"); let is_sub_process = cmd.has_switch(Some(&switch)) == 1; - Self { args, is_sub_process } + Self { + args, + is_sub_process, + _marker: std::marker::PhantomData, + } } pub(crate) fn is_sub_process(&self) -> bool { @@ -45,7 +50,7 @@ impl CefContextBuilder { let cmd = self.args.as_cmd_line().unwrap(); let switch = CefString::from("type"); let process_type = CefString::from(&cmd.switch_value(Some(&switch))); - let mut app = RenderProcessAppImpl::app(); + let mut app = RenderProcessAppImpl::::app(); let ret = execute_process(Some(self.args.as_main_args()), Some(&mut app), std::ptr::null_mut()); if ret >= 0 { SetupError::SubprocessFailed(process_type.to_string()) @@ -55,7 +60,7 @@ impl CefContextBuilder { } #[cfg(target_os = "macos")] - pub(crate) fn initialize(self, event_handler: CefHandler) -> Result { + pub(crate) fn initialize(self, event_handler: H) -> Result { let settings = Settings { windowless_rendering_enabled: 1, multi_threaded_message_loop: 0, @@ -71,7 +76,7 @@ impl CefContextBuilder { } #[cfg(not(target_os = "macos"))] - pub(crate) fn initialize(self, event_handler: CefHandler) -> Result { + pub(crate) fn initialize(self, event_handler: H) -> Result { let settings = Settings { windowless_rendering_enabled: 1, multi_threaded_message_loop: 1, @@ -97,7 +102,7 @@ impl CefContextBuilder { Ok(super::multithreaded::MultiThreadedCefContextProxy) } - fn initialize_inner(self, event_handler: &CefHandler, settings: Settings) -> Result<(), InitError> { + fn initialize_inner(self, event_handler: &H, settings: Settings) -> Result<(), InitError> { let mut cef_app = App::new(BrowserProcessAppImpl::new(event_handler.clone())); let result = cef::initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut()); // Attention! Wrapping this in an extra App is necessary, otherwise the program still compiles but segfaults @@ -113,11 +118,11 @@ impl CefContextBuilder { } } -fn create_browser(event_handler: CefHandler) -> Result { +fn create_browser(event_handler: H) -> Result { let render_handler = RenderHandler::new(RenderHandlerImpl::new(event_handler.clone())); let mut client = Client::new(BrowserProcessClientImpl::new(render_handler, event_handler.clone())); - let url = CefString::from(format!("{GRAPHITE_SCHEME}://{FRONTEND_DOMAIN}/").as_str()); + let url = CefString::from(format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/").as_str()); let window_info = WindowInfo { windowless_rendering_enabled: 1, diff --git a/desktop/src/cef/internal.rs b/desktop/src/cef/internal.rs index 5157852458..2e48c6df2c 100644 --- a/desktop/src/cef/internal.rs +++ b/desktop/src/cef/internal.rs @@ -2,10 +2,14 @@ mod browser_process_app; mod browser_process_client; mod browser_process_handler; mod browser_process_life_span_handler; + mod render_process_app; mod render_process_handler; mod render_process_v8_handler; +mod resource_handler; +mod scheme_handler_factory; + pub(super) mod render_handler; pub(super) mod task; diff --git a/desktop/src/cef/internal/browser_process_app.rs b/desktop/src/cef/internal/browser_process_app.rs index 165d27ce4c..7a0ec0feb1 100644 --- a/desktop/src/cef/internal/browser_process_app.rs +++ b/desktop/src/cef/internal/browser_process_app.rs @@ -5,11 +5,9 @@ use cef::rc::{Rc, RcImpl}; use cef::sys::{_cef_app_t, cef_base_ref_counted_t}; use cef::{BrowserProcessHandler, CefString, ImplApp, ImplCommandLine, SchemeRegistrar, WrapApp}; -use crate::cef::CefEventHandler; - -use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory; - use super::browser_process_handler::BrowserProcessHandlerImpl; +use super::scheme_handler_factory::SchemeHandlerFactoryImpl; +use crate::cef::CefEventHandler; pub(crate) struct BrowserProcessAppImpl { object: *mut RcImpl<_cef_app_t, Self>, @@ -30,7 +28,7 @@ impl ImplApp for BrowserProcessAppImpl { } fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { - GraphiteSchemeHandlerFactory::register_schemes(registrar); + SchemeHandlerFactoryImpl::::register_schemes(registrar); } fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) { diff --git a/desktop/src/cef/internal/browser_process_handler.rs b/desktop/src/cef/internal/browser_process_handler.rs index cd80ce6084..780218a604 100644 --- a/desktop/src/cef/internal/browser_process_handler.rs +++ b/desktop/src/cef/internal/browser_process_handler.rs @@ -4,9 +4,9 @@ use cef::rc::{Rc, RcImpl}; use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_browser_process_handler_t}; use cef::{CefString, ImplBrowserProcessHandler, SchemeHandlerFactory, WrapBrowserProcessHandler}; +use super::scheme_handler_factory::SchemeHandlerFactoryImpl; use crate::cef::CefEventHandler; -use crate::cef::consts::GRAPHITE_SCHEME; -use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory; +use crate::cef::consts::RESOURCE_SCHEME; pub(crate) struct BrowserProcessHandlerImpl { object: *mut RcImpl, @@ -23,7 +23,11 @@ impl BrowserProcessHandlerImpl { impl ImplBrowserProcessHandler for BrowserProcessHandlerImpl { fn on_context_initialized(&self) { - cef::register_scheme_handler_factory(Some(&CefString::from(GRAPHITE_SCHEME)), None, Some(&mut SchemeHandlerFactory::new(GraphiteSchemeHandlerFactory::new()))); + cef::register_scheme_handler_factory( + Some(&CefString::from(RESOURCE_SCHEME)), + None, + Some(&mut SchemeHandlerFactory::new(SchemeHandlerFactoryImpl::new(self.event_handler.clone()))), + ); } fn on_schedule_message_pump_work(&self, delay_ms: i64) { diff --git a/desktop/src/cef/internal/render_handler.rs b/desktop/src/cef/internal/render_handler.rs index c430630dac..dd725a01b9 100644 --- a/desktop/src/cef/internal/render_handler.rs +++ b/desktop/src/cef/internal/render_handler.rs @@ -9,7 +9,6 @@ pub(crate) struct RenderHandlerImpl { object: *mut RcImpl<_cef_render_handler_t, Self>, event_handler: H, } - impl RenderHandlerImpl { pub(crate) fn new(event_handler: H) -> Self { Self { @@ -18,6 +17,7 @@ impl RenderHandlerImpl { } } } + impl ImplRenderHandler for RenderHandlerImpl { fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) { if let Some(rect) = rect { diff --git a/desktop/src/cef/internal/render_process_app.rs b/desktop/src/cef/internal/render_process_app.rs index d9e66bafbd..300690d771 100644 --- a/desktop/src/cef/internal/render_process_app.rs +++ b/desktop/src/cef/internal/render_process_app.rs @@ -3,13 +3,14 @@ use cef::sys::{_cef_app_t, cef_base_ref_counted_t}; use cef::{App, ImplApp, RenderProcessHandler, SchemeRegistrar, WrapApp}; use super::render_process_handler::RenderProcessHandlerImpl; -use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory; +use super::scheme_handler_factory::SchemeHandlerFactoryImpl; +use crate::cef::CefEventHandler; -pub(crate) struct RenderProcessAppImpl { +pub(crate) struct RenderProcessAppImpl { object: *mut RcImpl<_cef_app_t, Self>, render_process_handler: RenderProcessHandler, } -impl RenderProcessAppImpl { +impl RenderProcessAppImpl { pub(crate) fn app() -> App { App::new(Self { object: std::ptr::null_mut(), @@ -18,9 +19,9 @@ impl RenderProcessAppImpl { } } -impl ImplApp for RenderProcessAppImpl { +impl ImplApp for RenderProcessAppImpl { fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { - GraphiteSchemeHandlerFactory::register_schemes(registrar); + SchemeHandlerFactoryImpl::::register_schemes(registrar); } fn render_process_handler(&self) -> Option { @@ -32,7 +33,7 @@ impl ImplApp for RenderProcessAppImpl { } } -impl Clone for RenderProcessAppImpl { +impl Clone for RenderProcessAppImpl { fn clone(&self) -> Self { unsafe { let rc_impl = &mut *self.object; @@ -44,7 +45,7 @@ impl Clone for RenderProcessAppImpl { } } } -impl Rc for RenderProcessAppImpl { +impl Rc for RenderProcessAppImpl { fn as_base(&self) -> &cef_base_ref_counted_t { unsafe { let base = &*self.object; @@ -52,7 +53,7 @@ impl Rc for RenderProcessAppImpl { } } } -impl WrapApp for RenderProcessAppImpl { +impl WrapApp for RenderProcessAppImpl { fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) { self.object = object; } diff --git a/desktop/src/cef/internal/render_process_v8_handler.rs b/desktop/src/cef/internal/render_process_v8_handler.rs index 76cb136b4f..0ea935a1b2 100644 --- a/desktop/src/cef/internal/render_process_v8_handler.rs +++ b/desktop/src/cef/internal/render_process_v8_handler.rs @@ -5,7 +5,6 @@ use crate::cef::ipc::{MessageType, SendMessage}; pub struct BrowserProcessV8HandlerImpl { object: *mut cef::rc::RcImpl, } - impl BrowserProcessV8HandlerImpl { pub(crate) fn new() -> Self { Self { object: std::ptr::null_mut() } @@ -72,7 +71,6 @@ impl Clone for BrowserProcessV8HandlerImpl { Self { object: self.object } } } - impl Rc for BrowserProcessV8HandlerImpl { fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t { unsafe { @@ -81,7 +79,6 @@ impl Rc for BrowserProcessV8HandlerImpl { } } } - impl WrapV8Handler for BrowserProcessV8HandlerImpl { fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl) { self.object = object; diff --git a/desktop/src/cef/internal/resource_handler.rs b/desktop/src/cef/internal/resource_handler.rs new file mode 100644 index 0000000000..8c21b2cfb5 --- /dev/null +++ b/desktop/src/cef/internal/resource_handler.rs @@ -0,0 +1,108 @@ +use cef::rc::{Rc, RcImpl}; +use cef::sys::{_cef_resource_handler_t, cef_base_ref_counted_t}; +use cef::{Callback, CefString, ImplResourceHandler, ImplResponse, Request, ResourceReadCallback, Response, WrapResourceHandler}; +use std::cell::RefCell; +use std::ffi::c_int; +use std::io::Read; + +use crate::cef::{Resource, ResourceReader}; + +pub(crate) struct ResourceHandlerImpl { + object: *mut RcImpl<_cef_resource_handler_t, Self>, + reader: Option>, + mimetype: Option, +} + +impl ResourceHandlerImpl { + pub fn new(resource: Option) -> Self { + if let Some(resource) = resource { + Self { + object: std::ptr::null_mut(), + reader: Some(resource.reader.into()), + mimetype: resource.mimetype, + } + } else { + Self { + object: std::ptr::null_mut(), + reader: None, + mimetype: None, + } + } + } +} + +impl ImplResourceHandler for ResourceHandlerImpl { + fn open(&self, _request: Option<&mut Request>, handle_request: Option<&mut c_int>, _callback: Option<&mut Callback>) -> c_int { + if let Some(handle_request) = handle_request { + *handle_request = 1; + } + 1 + } + + fn response_headers(&self, response: Option<&mut Response>, response_length: Option<&mut i64>, _redirect_url: Option<&mut CefString>) { + if let Some(response_length) = response_length { + *response_length = -1; // Indicating that the length is unknown + } + if let Some(response) = response { + if self.reader.is_some() { + if let Some(mimetype) = &self.mimetype { + let cef_mime = CefString::from(mimetype.as_str()); + response.set_mime_type(Some(&cef_mime)); + } else { + response.set_mime_type(None); + } + response.set_status(200); + } else { + response.set_status(404); + response.set_mime_type(Some(&CefString::from("text/plain"))); + } + } + } + + fn read(&self, data_out: *mut u8, bytes_to_read: c_int, bytes_read: Option<&mut c_int>, _callback: Option<&mut ResourceReadCallback>) -> c_int { + let Some(bytes_read) = bytes_read else { unreachable!() }; + let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read as usize) }; + if let Some(reader) = &self.reader { + if let Ok(read) = reader.borrow_mut().read(out) { + *bytes_read = read as i32; + if read > 0 { + return 1; // Indicating that data was read + } + } else { + *bytes_read = -2; // Indicating ERR_FAILED + } + } + 0 // Indicating no data was read + } + + fn get_raw(&self) -> *mut _cef_resource_handler_t { + self.object.cast() + } +} + +impl Clone for ResourceHandlerImpl { + fn clone(&self) -> Self { + unsafe { + let rc_impl = &mut *self.object; + rc_impl.interface.add_ref(); + } + Self { + object: self.object, + reader: self.reader.clone(), + mimetype: self.mimetype.clone(), + } + } +} +impl Rc for ResourceHandlerImpl { + fn as_base(&self) -> &cef_base_ref_counted_t { + unsafe { + let base = &*self.object; + std::mem::transmute(&base.cef_object) + } + } +} +impl WrapResourceHandler for ResourceHandlerImpl { + fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_handler_t, Self>) { + self.object = object; + } +} diff --git a/desktop/src/cef/internal/scheme_handler_factory.rs b/desktop/src/cef/internal/scheme_handler_factory.rs new file mode 100644 index 0000000000..83c006c7a6 --- /dev/null +++ b/desktop/src/cef/internal/scheme_handler_factory.rs @@ -0,0 +1,86 @@ +use cef::rc::{Rc, RcImpl}; +use cef::sys::{_cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t}; +use cef::{Browser, CefString, Frame, ImplRequest, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, SchemeRegistrar, WrapSchemeHandlerFactory}; + +use super::resource_handler::ResourceHandlerImpl; +use crate::cef::CefEventHandler; +use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME}; + +pub(crate) struct SchemeHandlerFactoryImpl { + object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>, + event_handler: H, +} +impl SchemeHandlerFactoryImpl { + pub(crate) fn new(event_handler: H) -> Self { + Self { + object: std::ptr::null_mut(), + event_handler, + } + } + + pub(crate) fn register_schemes(registrar: Option<&mut SchemeRegistrar>) { + if let Some(registrar) = registrar { + let mut scheme_options = 0; + scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32; + scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32; + scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32; + scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32; + registrar.add_custom_scheme(Some(&CefString::from(RESOURCE_SCHEME)), scheme_options); + } + } +} + +impl ImplSchemeHandlerFactory for SchemeHandlerFactoryImpl { + fn create(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, scheme_name: Option<&CefString>, request: Option<&mut Request>) -> Option { + if let Some(scheme_name) = scheme_name { + if scheme_name.to_string() != RESOURCE_SCHEME { + return None; + } + if let Some(request) = request { + let url = CefString::from(&request.url()).to_string(); + let path = url.strip_prefix(&format!("{RESOURCE_SCHEME}://")).unwrap(); + let domain = path.split('/').next().unwrap_or(""); + let path = path.strip_prefix(domain).unwrap_or(""); + let path = path.trim_start_matches('/'); + return match domain { + RESOURCE_DOMAIN => { + let resource = self.event_handler.load_resource(path.to_string().into()); + Some(ResourceHandler::new(ResourceHandlerImpl::new(resource))) + } + _ => None, + }; + } + return None; + } + None + } + fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t { + self.object.cast() + } +} + +impl Clone for SchemeHandlerFactoryImpl { + fn clone(&self) -> Self { + unsafe { + let rc_impl = &mut *self.object; + rc_impl.interface.add_ref(); + } + Self { + object: self.object, + event_handler: self.event_handler.clone(), + } + } +} +impl Rc for SchemeHandlerFactoryImpl { + fn as_base(&self) -> &cef_base_ref_counted_t { + unsafe { + let base = &*self.object; + std::mem::transmute(&base.cef_object) + } + } +} +impl WrapSchemeHandlerFactory for SchemeHandlerFactoryImpl { + fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) { + self.object = object; + } +} diff --git a/desktop/src/cef/scheme_handler.rs b/desktop/src/cef/scheme_handler.rs deleted file mode 100644 index 9c211f6277..0000000000 --- a/desktop/src/cef/scheme_handler.rs +++ /dev/null @@ -1,223 +0,0 @@ -use std::cell::RefCell; -use std::ffi::c_int; -use std::ops::DerefMut; -use std::slice::Iter; - -use cef::rc::{Rc, RcImpl}; -use cef::sys::{_cef_resource_handler_t, _cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t}; -use cef::{ - Browser, Callback, CefString, Frame, ImplRequest, ImplResourceHandler, ImplResponse, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, ResourceReadCallback, Response, - SchemeRegistrar, WrapResourceHandler, WrapSchemeHandlerFactory, -}; -use include_dir::{Dir, include_dir}; - -use super::consts::{FRONTEND_DOMAIN, GRAPHITE_SCHEME}; - -pub(crate) struct GraphiteSchemeHandlerFactory { - object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>, -} -impl GraphiteSchemeHandlerFactory { - pub(crate) fn new() -> Self { - Self { object: std::ptr::null_mut() } - } - - pub(crate) fn register_schemes(registrar: Option<&mut SchemeRegistrar>) { - if let Some(registrar) = registrar { - let mut scheme_options = 0; - scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32; - scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32; - scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32; - scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32; - registrar.add_custom_scheme(Some(&CefString::from(GRAPHITE_SCHEME)), scheme_options); - } - } -} -impl ImplSchemeHandlerFactory for GraphiteSchemeHandlerFactory { - fn create(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, scheme_name: Option<&CefString>, request: Option<&mut Request>) -> Option { - if let Some(scheme_name) = scheme_name { - if scheme_name.to_string() != GRAPHITE_SCHEME { - return None; - } - if let Some(request) = request { - let url = CefString::from(&request.url()).to_string(); - let path = url.strip_prefix(&format!("{GRAPHITE_SCHEME}://")).unwrap(); - let domain = path.split('/').next().unwrap_or(""); - let path = path.strip_prefix(domain).unwrap_or(""); - let path = path.trim_start_matches('/'); - return match domain { - FRONTEND_DOMAIN => { - if path.is_empty() { - Some(ResourceHandler::new(GraphiteFrontendResourceHandler::new("index.html"))) - } else { - Some(ResourceHandler::new(GraphiteFrontendResourceHandler::new(path))) - } - } - _ => None, - }; - } - return None; - } - None - } - fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t { - self.object.cast() - } -} - -static FRONTEND: Dir = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist"); - -struct GraphiteFrontendResourceHandler<'a> { - object: *mut RcImpl<_cef_resource_handler_t, Self>, - data: Option>>, - mimetype: Option, -} -impl<'a> GraphiteFrontendResourceHandler<'a> { - pub fn new(path: &str) -> Self { - let file = FRONTEND.get_file(path); - let data = if let Some(file) = file { - Some(RefCell::new(file.contents().iter())) - } else { - tracing::error!("Failed to find asset at path: {}", path); - None - }; - let mimetype = if let Some(file) = file { - let ext = file.path().extension().and_then(|s| s.to_str()).unwrap_or(""); - - // We know what file types will be in the assets this should be fine - match ext { - "html" => Some("text/html".to_string()), - "css" => Some("text/css".to_string()), - "txt" => Some("text/plain".to_string()), - "wasm" => Some("application/wasm".to_string()), - "js" => Some("application/javascript".to_string()), - "png" => Some("image/png".to_string()), - "jpg" | "jpeg" => Some("image/jpeg".to_string()), - "svg" => Some("image/svg+xml".to_string()), - "xml" => Some("application/xml".to_string()), - "json" => Some("application/json".to_string()), - "ico" => Some("image/x-icon".to_string()), - "woff" => Some("font/woff".to_string()), - "woff2" => Some("font/woff2".to_string()), - "ttf" => Some("font/ttf".to_string()), - "otf" => Some("font/otf".to_string()), - "webmanifest" => Some("application/manifest+json".to_string()), - "graphite" => Some("application/graphite+json".to_string()), - _ => None, - } - } else { - None - }; - Self { - object: std::ptr::null_mut(), - data, - mimetype, - } - } -} -impl<'a> ImplResourceHandler for GraphiteFrontendResourceHandler<'a> { - fn open(&self, _request: Option<&mut Request>, handle_request: Option<&mut c_int>, _callback: Option<&mut Callback>) -> c_int { - if let Some(handle_request) = handle_request { - *handle_request = 1; - } - 1 - } - - fn response_headers(&self, response: Option<&mut Response>, response_length: Option<&mut i64>, _redirect_url: Option<&mut CefString>) { - if let Some(response_length) = response_length { - *response_length = -1; // Indicating that the length is unknown - } - if let Some(response) = response { - if self.data.is_some() { - if let Some(mimetype) = &self.mimetype { - let cef_mime = CefString::from(mimetype.as_str()); - response.set_mime_type(Some(&cef_mime)); - } else { - response.set_mime_type(None); - } - response.set_status(200); - } else { - response.set_status(404); - response.set_mime_type(Some(&CefString::from("text/plain"))); - } - } - } - - fn read(&self, data_out: *mut u8, bytes_to_read: c_int, bytes_read: Option<&mut c_int>, _callback: Option<&mut ResourceReadCallback>) -> c_int { - let mut read = 0; - - let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read as usize) }; - if let Some(data) = &self.data { - let mut data = data.borrow_mut(); - - for (out, &data) in out.iter_mut().zip(data.deref_mut()) { - *out = data; - read += 1; - } - } - - if let Some(bytes_read) = bytes_read { - *bytes_read = read; - } - - if read > 0 { - 1 // Indicating that data was read - } else { - 0 // Indicating no data was read - } - } - - fn get_raw(&self) -> *mut _cef_resource_handler_t { - self.object.cast() - } -} - -impl WrapSchemeHandlerFactory for GraphiteSchemeHandlerFactory { - fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) { - self.object = object; - } -} -impl<'a> WrapResourceHandler for GraphiteFrontendResourceHandler<'a> { - fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_handler_t, Self>) { - self.object = object; - } -} - -impl Clone for GraphiteSchemeHandlerFactory { - fn clone(&self) -> Self { - unsafe { - let rc_impl = &mut *self.object; - rc_impl.interface.add_ref(); - } - Self { object: self.object } - } -} -impl<'a> Clone for GraphiteFrontendResourceHandler<'a> { - fn clone(&self) -> Self { - unsafe { - let rc_impl = &mut *self.object; - rc_impl.interface.add_ref(); - } - Self { - object: self.object, - data: self.data.clone(), - mimetype: self.mimetype.clone(), - } - } -} - -impl Rc for GraphiteSchemeHandlerFactory { - fn as_base(&self) -> &cef_base_ref_counted_t { - unsafe { - let base = &*self.object; - std::mem::transmute(&base.cef_object) - } - } -} -impl<'a> Rc for GraphiteFrontendResourceHandler<'a> { - fn as_base(&self) -> &cef_base_ref_counted_t { - unsafe { - let base = &*self.object; - std::mem::transmute(&base.cef_object) - } - } -} diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 944899cfbd..080d05b692 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -1,6 +1,7 @@ use std::process::exit; use std::time::Instant; +use cef::CefHandler; use tracing_subscriber::EnvFilter; use winit::event_loop::EventLoop; @@ -30,7 +31,7 @@ pub(crate) enum CustomEvent { fn main() { tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init(); - let cef_context_builder = cef::CefContextBuilder::new(); + let cef_context_builder = cef::CefContextBuilder::::new(); if cef_context_builder.is_sub_process() { // We are in a CEF subprocess diff --git a/desktop/wrapper/Cargo.toml b/desktop/wrapper/Cargo.toml index 0a8704978a..5e61337515 100644 --- a/desktop/wrapper/Cargo.toml +++ b/desktop/wrapper/Cargo.toml @@ -16,7 +16,6 @@ gpu = ["graphite-editor/gpu"] # Local dependencies graphite-editor = { path = "../../editor", features = [ "gpu", - "ron", "vello", ] } graphene-std = { workspace = true } diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 6b36914df7..faf444b1a1 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -16,7 +16,6 @@ wasm = ["wasm-bindgen", "graphene-std/wasm"] gpu = ["interpreted-executor/gpu", "wgpu-executor"] resvg = ["graphene-std/resvg"] vello = ["graphene-std/vello", "resvg"] -ron = [] [dependencies] # Local dependencies diff --git a/frontend/package.json b/frontend/package.json index 02eb2e41cc..ffeaebf503 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,10 +23,10 @@ "setup": "node package-installer.js", "native:build-dev": "wasm-pack build ./wasm --dev --target=web --features native", "native:build-production": "wasm-pack build ./wasm --release --target=web --features native", - "wasm:build-dev": "wasm-pack build ./wasm --dev --target=web", + "wasm:build-dev": "wasm-pack build ./wasm --dev --target=web --features shader-nodes", "wasm:build-profiling": "wasm-pack build ./wasm --profiling --target=web", "wasm:build-production": "wasm-pack build ./wasm --release --target=web", - "wasm:watch-dev": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --dev --target=web -- --color=always\"", + "wasm:watch-dev": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --dev --target=web -- --features shader-nodes --color=always\"", "wasm:watch-profiling": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --profiling --target=web -- --color=always\"", "wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --release --target=web -- --color=always\"" }, diff --git a/frontend/wasm/Cargo.toml b/frontend/wasm/Cargo.toml index 90b8ebe6ba..99c7668dfa 100644 --- a/frontend/wasm/Cargo.toml +++ b/frontend/wasm/Cargo.toml @@ -14,6 +14,7 @@ license = "Apache-2.0" default = ["gpu"] gpu = ["editor/gpu"] native = [] +shader-nodes = ["graphene-std/shader-nodes"] [lib] crate-type = ["cdylib", "rlib"] diff --git a/node-graph/gcore-shaders/Cargo.toml b/node-graph/gcore-shaders/Cargo.toml index ebb23e1234..b9a37d24c8 100644 --- a/node-graph/gcore-shaders/Cargo.toml +++ b/node-graph/gcore-shaders/Cargo.toml @@ -7,9 +7,29 @@ authors = ["Graphite Authors "] license = "MIT OR Apache-2.0" [features] -std = ["dep:dyn-any", "dep:serde", "dep:specta", "dep:log", "glam/debug-glam-assert", "glam/std", "glam/serde", "half/std", "half/serde", "num-traits/std"] +# any feature that +# * must be usable in shaders +# * but requires std +# * and should be on by default +# should be in this list instead of `[workspace.dependency]` +std = [ + "dep:dyn-any", + "dep:serde", + "dep:specta", + "dep:log", + "glam/debug-glam-assert", + "glam/std", + "glam/serde", + "half/std", + "half/serde", + "num-traits/std", + "num_enum/std", +] [dependencies] +# Local dependencies +node-macro = { workspace = true } + # Local std dependencies dyn-any = { workspace = true, optional = true } @@ -19,6 +39,8 @@ glam = { workspace = true } half = { workspace = true, default-features = false } num-derive = { workspace = true } num-traits = { workspace = true } +num_enum = { workspace = true } +spirv-std = { workspace = true } # Workspace std dependencies serde = { workspace = true, optional = true } diff --git a/node-graph/gcore-shaders/src/blending.rs b/node-graph/gcore-shaders/src/blending.rs index c3701e2cc0..747c4ba0a8 100644 --- a/node-graph/gcore-shaders/src/blending.rs +++ b/node-graph/gcore-shaders/src/blending.rs @@ -1,9 +1,11 @@ use core::fmt::Display; use core::hash::{Hash, Hasher}; +use node_macro::BufferStruct; +use num_enum::{FromPrimitive, IntoPrimitive}; #[cfg(not(feature = "std"))] use num_traits::float::Float; -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, BufferStruct)] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", serde(default))] pub struct AlphaBlending { @@ -66,7 +68,7 @@ impl AlphaBlending { } #[repr(i32)] -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, BufferStruct, FromPrimitive, IntoPrimitive)] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] pub enum BlendMode { // Basic group diff --git a/node-graph/gcore-shaders/src/color/color_types.rs b/node-graph/gcore-shaders/src/color/color_types.rs index 2cb4d231bd..5127a0c07a 100644 --- a/node-graph/gcore-shaders/src/color/color_types.rs +++ b/node-graph/gcore-shaders/src/color/color_types.rs @@ -3,7 +3,9 @@ use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float}; use bytemuck::{Pod, Zeroable}; use core::fmt::Debug; use core::hash::Hash; +use glam::Vec4; use half::f16; +use node_macro::BufferStruct; #[cfg(not(feature = "std"))] use num_traits::Euclid; #[cfg(not(feature = "std"))] @@ -214,7 +216,7 @@ impl Pixel for Luma {} /// 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)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable, BufferStruct)] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] pub struct Color { red: f32, @@ -1075,6 +1077,21 @@ impl Color { ..*self } } + + #[inline(always)] + pub const fn from_vec4(vec: Vec4) -> Self { + Self { + red: vec.x, + green: vec.y, + blue: vec.z, + alpha: vec.w, + } + } + + #[inline(always)] + pub fn to_vec4(&self) -> Vec4 { + Vec4::new(self.red, self.green, self.blue, self.alpha) + } } #[cfg(test)] diff --git a/node-graph/gcore-shaders/src/lib.rs b/node-graph/gcore-shaders/src/lib.rs index 9b310ea9d7..b6b681e51f 100644 --- a/node-graph/gcore-shaders/src/lib.rs +++ b/node-graph/gcore-shaders/src/lib.rs @@ -5,6 +5,7 @@ pub mod choice_type; pub mod color; pub mod context; pub mod registry; +pub mod shaders; pub use context::Ctx; pub use glam; diff --git a/node-graph/gcore-shaders/src/shaders/buffer_struct/glam.rs b/node-graph/gcore-shaders/src/shaders/buffer_struct/glam.rs new file mode 100644 index 0000000000..568f017045 --- /dev/null +++ b/node-graph/gcore-shaders/src/shaders/buffer_struct/glam.rs @@ -0,0 +1,114 @@ +use crate::shaders::buffer_struct::BufferStruct; + +macro_rules! glam_array { + ($t:ty, $a:ty) => { + unsafe impl BufferStruct for $t { + type Buffer = $a; + + #[inline] + fn write(from: Self) -> Self::Buffer { + <$t>::to_array(&from) + } + + #[inline] + fn read(from: Self::Buffer) -> Self { + <$t>::from_array(from) + } + } + }; +} + +macro_rules! glam_cols_array { + ($t:ty, $a:ty) => { + unsafe impl BufferStruct for $t { + type Buffer = $a; + + #[inline] + fn write(from: Self) -> Self::Buffer { + <$t>::to_cols_array(&from) + } + + #[inline] + fn read(from: Self::Buffer) -> Self { + <$t>::from_cols_array(&from) + } + } + }; +} + +glam_array!(glam::Vec2, [f32; 2]); +glam_array!(glam::Vec3, [f32; 3]); +// glam_array!(Vec3A, [f32; 4]); +glam_array!(glam::Vec4, [f32; 4]); +glam_array!(glam::Quat, [f32; 4]); +glam_cols_array!(glam::Mat2, [f32; 4]); +glam_cols_array!(glam::Mat3, [f32; 9]); +// glam_cols_array!(Mat3A, [f32; 4]); +glam_cols_array!(glam::Mat4, [f32; 16]); +glam_cols_array!(glam::Affine2, [f32; 6]); +glam_cols_array!(glam::Affine3A, [f32; 12]); + +glam_array!(glam::DVec2, [f64; 2]); +glam_array!(glam::DVec3, [f64; 3]); +glam_array!(glam::DVec4, [f64; 4]); +glam_array!(glam::DQuat, [f64; 4]); +glam_cols_array!(glam::DMat2, [f64; 4]); +glam_cols_array!(glam::DMat3, [f64; 9]); +glam_cols_array!(glam::DMat4, [f64; 16]); +glam_cols_array!(glam::DAffine2, [f64; 6]); +glam_cols_array!(glam::DAffine3, [f64; 12]); + +glam_array!(glam::I16Vec2, [i16; 2]); +glam_array!(glam::I16Vec3, [i16; 3]); +glam_array!(glam::I16Vec4, [i16; 4]); + +glam_array!(glam::U16Vec2, [u16; 2]); +glam_array!(glam::U16Vec3, [u16; 3]); +glam_array!(glam::U16Vec4, [u16; 4]); + +glam_array!(glam::IVec2, [i32; 2]); +glam_array!(glam::IVec3, [i32; 3]); +glam_array!(glam::IVec4, [i32; 4]); + +glam_array!(glam::UVec2, [u32; 2]); +glam_array!(glam::UVec3, [u32; 3]); +glam_array!(glam::UVec4, [u32; 4]); + +glam_array!(glam::I64Vec2, [i64; 2]); +glam_array!(glam::I64Vec3, [i64; 3]); +glam_array!(glam::I64Vec4, [i64; 4]); + +glam_array!(glam::U64Vec2, [u64; 2]); +glam_array!(glam::U64Vec3, [u64; 3]); +glam_array!(glam::U64Vec4, [u64; 4]); + +unsafe impl BufferStruct for glam::Vec3A { + type Buffer = [f32; 4]; + + #[inline] + fn write(from: Self) -> Self::Buffer { + glam::Vec4::to_array(&from.extend(0.)) + } + + #[inline] + fn read(from: Self::Buffer) -> Self { + glam::Vec3A::from_vec4(glam::Vec4::from_array(from)) + } +} + +/// do NOT use slices, otherwise spirv will fail to compile +unsafe impl BufferStruct for glam::Mat3A { + type Buffer = [f32; 12]; + + #[inline] + fn write(from: Self) -> Self::Buffer { + let a = from.to_cols_array(); + [a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], 0., 0., 0.] + } + + #[inline] + fn read(from: Self::Buffer) -> Self { + let a = from; + glam::Mat3A::from_cols_array(&[a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]]) + } +} diff --git a/node-graph/gcore-shaders/src/shaders/buffer_struct/mod.rs b/node-graph/gcore-shaders/src/shaders/buffer_struct/mod.rs new file mode 100644 index 0000000000..07fca85e46 --- /dev/null +++ b/node-graph/gcore-shaders/src/shaders/buffer_struct/mod.rs @@ -0,0 +1,63 @@ +//! I (@firestar99) copied this entire mod from one of my projects, as I haven't uploaded that lib to crates. Hopefully +//! rust-gpu improves and this entire thing becomes unnecessary in the future. +//! +//! https://github.com/Firestar99/nanite-at-home/tree/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/buffer-content + +use bytemuck::Pod; + +mod glam; +mod primitive; + +/// A BufferStruct is a "parallel representation" of the original struct with some fundamental types remapped. This +/// struct hierarchy represents how data is stored in GPU Buffers, where all types must be [`Pod`] to allow +/// transmuting them to `&[u8]` with [`bytemuck`]. +/// +/// Notable type remappings (original: buffer): +/// * bool: u32 of 0 or 1 +/// * any repr(u32) enum: u32 with remapping via [`num_enum`] +/// +/// By adding `#[derive(ShaderStruct)]` to your struct (or enum), a parallel `{name}Buffer` struct is created with all +/// the members of the original struct, but with their types using the associated remapped types as specified by this +/// trait. +/// +/// # Origin +/// I (@firestar99) copied this entire mod from my [Nanite-at-home] project, specifically the [buffer-content] crate +/// and the [buffer_struct] proc macro. The variant here has quite some modifications, to both cleaned up some of the +/// mistakes my implementation has and to customize it a bit for graphite. +/// +/// Hopefully rust-gpu improves to the point where this remapping becomes unnecessary. +/// +/// [Nanite-at-home]: https://github.com/Firestar99/nanite-at-home +/// [buffer-content]: https://github.com/Firestar99/nanite-at-home/tree/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/buffer-content +/// [buffer_struct]: https://github.com/Firestar99/nanite-at-home/blob/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/macros/src/buffer_struct.rs +/// +/// # Safety +/// The associated type Transfer must be the same on all targets. Writing followed by reading back a value must result +/// in the same value. +pub unsafe trait BufferStruct: Copy + Send + Sync + 'static { + type Buffer: Pod + Send + Sync; + + fn write(from: Self) -> Self::Buffer; + + fn read(from: Self::Buffer) -> Self; +} + +/// Trait marking all [`BufferStruct`] whose read and write methods are identity. While [`BufferStruct`] only +/// requires `t == read(write(t))`, this trait additionally requires `t == read(t) == write(t)`. As this removes the +/// conversion requirement for writing to or reading from a buffer, one can acquire slices from buffers created of these +/// types. +/// +/// Implementing this type is completely safe due to the [`Pod`] requirement. +pub trait BufferStructIdentity: Pod + Send + Sync {} + +unsafe impl BufferStruct for T { + type Buffer = Self; + + fn write(from: Self) -> Self::Buffer { + from + } + + fn read(from: Self::Buffer) -> Self { + from + } +} diff --git a/node-graph/gcore-shaders/src/shaders/buffer_struct/primitive.rs b/node-graph/gcore-shaders/src/shaders/buffer_struct/primitive.rs new file mode 100644 index 0000000000..872bb7becb --- /dev/null +++ b/node-graph/gcore-shaders/src/shaders/buffer_struct/primitive.rs @@ -0,0 +1,135 @@ +use crate::shaders::buffer_struct::{BufferStruct, BufferStructIdentity}; +use bytemuck::Pod; +use core::marker::PhantomData; +use core::num::Wrapping; +use spirv_std::arch::IndexUnchecked; + +macro_rules! identity { + ($t:ty) => { + impl BufferStructIdentity for $t {} + }; +} + +identity!(()); +identity!(u8); +identity!(u16); +identity!(u32); +identity!(u64); +identity!(u128); +identity!(usize); +identity!(i8); +identity!(i16); +identity!(i32); +identity!(i64); +identity!(i128); +identity!(isize); +identity!(f32); +identity!(f64); + +identity!(spirv_std::arch::SubgroupMask); +identity!(spirv_std::memory::Semantics); +identity!(spirv_std::ray_tracing::RayFlags); +identity!(spirv_std::indirect_command::DrawIndirectCommand); +identity!(spirv_std::indirect_command::DrawIndexedIndirectCommand); +identity!(spirv_std::indirect_command::DispatchIndirectCommand); +identity!(spirv_std::indirect_command::DrawMeshTasksIndirectCommandEXT); +identity!(spirv_std::indirect_command::TraceRaysIndirectCommandKHR); +// not pod +// identity!(spirv_std::indirect_command::TraceRaysIndirectCommand2KHR); + +unsafe impl BufferStruct for bool { + type Buffer = u32; + + #[inline] + fn write(from: Self) -> Self::Buffer { + from as u32 + } + + #[inline] + fn read(from: Self::Buffer) -> Self { + from != 0 + } +} + +unsafe impl BufferStruct for Wrapping +where + // unfortunately has to be Pod, even though AnyBitPattern would be sufficient, + // due to bytemuck doing `impl AnyBitPattern for T {}` + // see https://github.com/Lokathor/bytemuck/issues/164 + T::Buffer: Pod, +{ + type Buffer = Wrapping; + + #[inline] + fn write(from: Self) -> Self::Buffer { + Wrapping(T::write(from.0)) + } + + #[inline] + fn read(from: Self::Buffer) -> Self { + Wrapping(T::read(from.0)) + } +} + +unsafe impl BufferStruct for PhantomData { + type Buffer = PhantomData; + + #[inline] + fn write(_: Self) -> Self::Buffer { + PhantomData {} + } + + #[inline] + fn read(_: Self::Buffer) -> Self { + PhantomData {} + } +} + +/// Potential problem: you can't impl this for an array of BufferStruct, as it'll conflict with this impl due to the +/// blanket impl on all BufferStructPlain types. +unsafe impl BufferStruct for [T; N] +where + // rust-gpu does not like `[T; N].map()` nor `core::array::from_fn()` nor transmuting arrays with a const generic + // length, so for now we need to require T: Default and T::Transfer: Default for all arrays. + T: Default, + // unfortunately has to be Pod, even though AnyBitPattern would be sufficient, + // due to bytemuck doing `impl AnyBitPattern for T {}` + // see https://github.com/Lokathor/bytemuck/issues/164 + T::Buffer: Pod + Default, +{ + type Buffer = [T::Buffer; N]; + + #[inline] + fn write(from: Self) -> Self::Buffer { + unsafe { + let mut ret = [T::Buffer::default(); N]; + for i in 0..N { + *ret.index_unchecked_mut(i) = T::write(*from.index_unchecked(i)); + } + ret + } + } + + #[inline] + fn read(from: Self::Buffer) -> Self { + unsafe { + let mut ret = [T::default(); N]; + for i in 0..N { + *ret.index_unchecked_mut(i) = T::read(*from.index_unchecked(i)); + } + ret + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roundtrip_bool() { + for x in [false, true] { + assert_eq!(x, BufferStruct::read(&BufferStruct::write(&x))); + } + } +} diff --git a/node-graph/gcore-shaders/src/shaders/mod.rs b/node-graph/gcore-shaders/src/shaders/mod.rs new file mode 100644 index 0000000000..1725a29516 --- /dev/null +++ b/node-graph/gcore-shaders/src/shaders/mod.rs @@ -0,0 +1,10 @@ +//! supporting infrastructure for shaders + +pub mod buffer_struct; + +pub mod __private { + pub use bytemuck; + pub use glam; + pub use num_enum; + pub use spirv_std; +} diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs index 3b098147ff..be1cbf967d 100644 --- a/node-graph/gcore/src/lib.rs +++ b/node-graph/gcore/src/lib.rs @@ -41,6 +41,7 @@ pub use graphene_core_shaders::AsU32; pub use graphene_core_shaders::blending; pub use graphene_core_shaders::choice_type; pub use graphene_core_shaders::color; +pub use graphene_core_shaders::shaders; pub use graphic::Graphic; pub use memo::MemoHash; pub use num_traits; diff --git a/node-graph/gcore/src/raster_types.rs b/node-graph/gcore/src/raster_types.rs index 97dd138145..7efae73fb8 100644 --- a/node-graph/gcore/src/raster_types.rs +++ b/node-graph/gcore/src/raster_types.rs @@ -137,7 +137,7 @@ mod gpu { #[derive(Clone, Debug, PartialEq, Hash)] pub struct GPU { - texture: wgpu::Texture, + pub texture: wgpu::Texture, } impl Sealed for Raster {} diff --git a/node-graph/graster-nodes/Cargo.toml b/node-graph/graster-nodes/Cargo.toml index c0844b98b1..c51c4485f9 100644 --- a/node-graph/graster-nodes/Cargo.toml +++ b/node-graph/graster-nodes/Cargo.toml @@ -6,8 +6,19 @@ description = "graphene raster data format" authors = ["Graphite Authors "] license = "MIT OR Apache-2.0" +[lib] +crate-type = ["rlib", "dylib"] + +[lints] +workspace = true + [features] default = ["std"] +shader-nodes = [ + "std", + "dep:graphene-raster-nodes-shaders", + "dep:wgpu-executor", +] std = [ "dep:graphene-core", "dep:dyn-any", @@ -19,8 +30,6 @@ std = [ "dep:serde", "dep:specta", "dep:kurbo", - "glam/debug-glam-assert", - "glam/serde", ] [dependencies] @@ -31,11 +40,15 @@ node-macro = { workspace = true } # Local std dependencies dyn-any = { workspace = true, optional = true } graphene-core = { workspace = true, optional = true } +wgpu-executor = { workspace = true, optional = true } +graphene-raster-nodes-shaders = { path = "./shaders", optional = true } # Workspace dependencies bytemuck = { workspace = true } glam = { workspace = true } +spirv-std = { workspace = true } num-traits = { workspace = true } +num_enum = { workspace = true } # Workspace std dependencies specta = { workspace = true, optional = true } diff --git a/node-graph/graster-nodes/shaders/Cargo.toml b/node-graph/graster-nodes/shaders/Cargo.toml new file mode 100644 index 0000000000..65a4fed409 --- /dev/null +++ b/node-graph/graster-nodes/shaders/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "graphene-raster-nodes-shaders" +version = "0.1.0" +edition = "2024" +description = "graphene raster data format" +authors = ["Graphite Authors "] +license = "MIT OR Apache-2.0" + +[dependencies] + +[build-dependencies] +cargo-gpu = { workspace = true } +env_logger = { workspace = true } diff --git a/node-graph/graster-nodes/shaders/build.rs b/node-graph/graster-nodes/shaders/build.rs new file mode 100644 index 0000000000..50061aefcd --- /dev/null +++ b/node-graph/graster-nodes/shaders/build.rs @@ -0,0 +1,36 @@ +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(concat!(env!("CARGO_MANIFEST_DIR"), "/..")); + + let rustc_codegen_spirv_path = std::env::var("RUSTC_CODEGEN_SPIRV_PATH").unwrap_or_default(); + let mut builder = if rustc_codegen_spirv_path.is_empty() { + // install the toolchain and build the `rustc_codegen_spirv` codegen backend with it + cargo_gpu::Install::from_shader_crate(shader_crate.clone()) + .run()? + .to_spirv_builder(shader_crate, "spirv-unknown-naga-wgsl") + } else { + // use the `RUSTC_CODEGEN_SPIRV` environment variable to find the codegen backend + let mut builder = cargo_gpu::spirv_builder::SpirvBuilder::new(shader_crate.clone(), "spirv-unknown-naga-wgsl"); + builder.rustc_codegen_spirv_location = Some(PathBuf::from(rustc_codegen_spirv_path)); + builder.toolchain_overwrite = Some("nightly".to_string()); + builder.path_to_target_spec = Some(PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/spirv-unknown-naga-wgsl.json"))); + builder + }; + + // build the shader crate + builder.print_metadata = MetadataPrintout::DependencyOnly; + builder.spirv_metadata = SpirvMetadata::Full; + builder.shader_crate_features.default_features = false; + let wgsl_result = builder.build()?; + let path_to_spv = wgsl_result.module.unwrap_single(); + + // needs to be fixed upstream + let path_to_wgsl = path_to_spv.with_extension("wgsl"); + + println!("cargo::rustc-env=WGSL_SHADER_PATH={}", path_to_wgsl.display()); + Ok(()) +} diff --git a/node-graph/graster-nodes/shaders/spirv-unknown-naga-wgsl.json b/node-graph/graster-nodes/shaders/spirv-unknown-naga-wgsl.json new file mode 100644 index 0000000000..00f17162c0 --- /dev/null +++ b/node-graph/graster-nodes/shaders/spirv-unknown-naga-wgsl.json @@ -0,0 +1,26 @@ +{ + "allows-weak-linkage": false, + "arch": "spirv", + "crt-objects-fallback": "false", + "crt-static-allows-dylibs": true, + "crt-static-respected": true, + "data-layout": "e-m:e-p:32:32:32-i64:64-n8:16:32:64", + "dll-prefix": "", + "dll-suffix": ".spv.json", + "dynamic-linking": true, + "emit-debug-gdb-scripts": false, + "env": "naga-wgsl", + "linker-flavor": "unix", + "linker-is-gnu": false, + "llvm-target": "spirv-unknown-naga-wgsl", + "main-needs-argc-argv": false, + "metadata": { + "description": null, + "host_tools": null, + "std": null, + "tier": null + }, + "panic-strategy": "abort", + "simd-types-indirect": false, + "target-pointer-width": "32" +} diff --git a/node-graph/graster-nodes/shaders/src/lib.rs b/node-graph/graster-nodes/shaders/src/lib.rs new file mode 100644 index 0000000000..51053ca7ee --- /dev/null +++ b/node-graph/graster-nodes/shaders/src/lib.rs @@ -0,0 +1 @@ +pub const WGSL_SHADER: &str = include_str!(env!("WGSL_SHADER_PATH")); diff --git a/node-graph/graster-nodes/src/adjustments.rs b/node-graph/graster-nodes/src/adjustments.rs index 1274ef5fe4..e8b6e57bed 100644 --- a/node-graph/graster-nodes/src/adjustments.rs +++ b/node-graph/graster-nodes/src/adjustments.rs @@ -3,6 +3,7 @@ use crate::adjust::Adjust; use crate::cubic_spline::CubicSplines; use core::fmt::Debug; +use glam::{Vec3, Vec4}; #[cfg(feature = "std")] use graphene_core::gradient::GradientStops; #[cfg(feature = "std")] @@ -12,6 +13,8 @@ use graphene_core::table::Table; use graphene_core_shaders::color::Color; use graphene_core_shaders::context::Ctx; use graphene_core_shaders::registry::types::{AngleF32, PercentageF32, SignedPercentageF32}; +use node_macro::BufferStruct; +use num_enum::{FromPrimitive, IntoPrimitive}; #[cfg(not(feature = "std"))] use num_traits::float::Float; @@ -30,9 +33,10 @@ use num_traits::float::Float; // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27clrL%27%20%3D%20Color%20Lookup // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Color%20Lookup%20(Photoshop%20CS6 -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, node_macro::ChoiceType)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[widget(Dropdown)] +#[repr(u32)] pub enum LuminanceCalculation { #[default] #[label("sRGB")] @@ -52,6 +56,7 @@ fn luminance>( Table, GradientStops, )] + #[gpu_image] mut input: T, luminance_calc: LuminanceCalculation, ) -> T { @@ -77,6 +82,7 @@ fn gamma_correction>( Table, GradientStops, )] + #[gpu_image] mut input: T, #[default(2.2)] #[range((0.01, 10.))] @@ -98,6 +104,7 @@ fn extract_channel>( Table, GradientStops, )] + #[gpu_image] mut input: T, channel: RedGreenBlueAlpha, ) -> T { @@ -122,6 +129,7 @@ fn make_opaque>( Table, GradientStops, )] + #[gpu_image] mut input: T, ) -> T { input.adjust(|color| { @@ -133,37 +141,61 @@ fn make_opaque>( input } +/// See [`brightness_contrast`] +#[node_macro::node( + name("Brightness/Contrast classic"), + category("Raster: Adjustment"), + properties("brightness_contrast_properties"), + shader_node(PerPixelAdjust) +)] +fn brightness_contrast_classic>( + _: impl Ctx, + #[implementations( + Table>, + Table, + Table, + GradientStops, + )] + #[gpu_image] + mut input: T, + brightness: SignedPercentageF32, + contrast: SignedPercentageF32, +) -> T { + let brightness = brightness / 255.; + + let contrast = contrast / 100.; + let contrast = if contrast > 0. { (contrast * core::f32::consts::FRAC_PI_2 - 0.01).tan() } else { contrast }; + + let offset = brightness * contrast + brightness - contrast / 2.; + + input.adjust(|color| color.to_gamma_srgb().map_rgb(|c| (c + c * contrast + offset).clamp(0., 1.)).to_linear_srgb()); + + input +} + // Aims for interoperable compatibility with: // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27brit%27%20%3D%20Brightness/Contrast // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Padding-,Brightness%20and%20Contrast,-Key%20is%20%27brit // // Some further analysis available at: // https://geraldbakker.nl/psnumbers/brightness-contrast.html -#[node_macro::node(name("Brightness/Contrast"), category("Raster: Adjustment"), properties("brightness_contrast_properties"), shader_node(PerPixelAdjust))] +#[node_macro::node(name("Brightness/Contrast"), category("Raster: Adjustment"), properties("brightness_contrast_properties"), cfg(feature = "std"))] fn brightness_contrast>( - _: impl Ctx, + _ctx: impl Ctx, #[implementations( Table>, Table, Table, GradientStops, )] + #[gpu_image] mut input: T, brightness: SignedPercentageF32, contrast: SignedPercentageF32, use_classic: bool, ) -> T { if use_classic { - let brightness = brightness / 255.; - - let contrast = contrast / 100.; - let contrast = if contrast > 0. { (contrast * core::f32::consts::FRAC_PI_2 - 0.01).tan() } else { contrast }; - - let offset = brightness * contrast + brightness - contrast / 2.; - - input.adjust(|color| color.to_gamma_srgb().map_rgb(|c| (c + c * contrast + offset).clamp(0., 1.)).to_linear_srgb()); - - return input; + return brightness_contrast_classic(_ctx, input, brightness, contrast); } const WINDOW_SIZE: usize = 1024; @@ -238,6 +270,7 @@ fn levels>( Table, GradientStops, )] + #[gpu_image] mut image: T, #[default(0.)] shadows: PercentageF32, #[default(50.)] midtones: PercentageF32, @@ -306,6 +339,7 @@ fn black_and_white>( Table, GradientStops, )] + #[gpu_image] mut image: T, #[default(Color::BLACK)] tint: Color, #[default(40.)] @@ -379,6 +413,7 @@ fn hue_saturation>( Table, GradientStops, )] + #[gpu_image] mut input: T, hue_shift: AngleF32, saturation_shift: SignedPercentageF32, @@ -414,6 +449,7 @@ fn invert>( Table, GradientStops, )] + #[gpu_image] mut input: T, ) -> T { input.adjust(|color| { @@ -437,6 +473,7 @@ fn threshold>( Table, GradientStops, )] + #[gpu_image] mut image: T, #[default(50.)] min_luminance: PercentageF32, #[default(100.)] max_luminance: PercentageF32, @@ -483,6 +520,7 @@ fn vibrance>( Table, GradientStops, )] + #[gpu_image] mut image: T, vibrance: SignedPercentageF32, ) -> T { @@ -537,7 +575,8 @@ fn vibrance>( } /// Color Channel -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] +#[repr(u32)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum RedGreenBlue { @@ -548,9 +587,10 @@ pub enum RedGreenBlue { } /// Color Channel -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[widget(Radio)] +#[repr(u32)] pub enum RedGreenBlueAlpha { #[default] Red, @@ -649,6 +689,7 @@ fn channel_mixer>( Table, GradientStops, )] + #[gpu_image] mut image: T, monochrome: bool, @@ -736,7 +777,8 @@ fn channel_mixer>( image } -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] +#[repr(u32)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum RelativeAbsolute { @@ -745,8 +787,8 @@ pub enum RelativeAbsolute { Absolute, } -#[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] +#[repr(u32)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] pub enum SelectiveColorChoice { #[default] @@ -778,6 +820,7 @@ fn selective_color>( Table, GradientStops, )] + #[gpu_image] mut image: T, mode: RelativeAbsolute, @@ -862,7 +905,7 @@ fn selective_color>( RelativeAbsolute::Absolute => (-1., -1., -1.), }; - let (sum_r, sum_g, sum_b) = [ + let array = [ (SelectiveColorChoice::Reds, (r_c, r_m, r_y, r_k)), (SelectiveColorChoice::Yellows, (y_c, y_m, y_y, y_k)), (SelectiveColorChoice::Greens, (g_c, g_m, g_y, g_k)), @@ -872,14 +915,16 @@ fn selective_color>( (SelectiveColorChoice::Whites, (w_c, w_m, w_y, w_k)), (SelectiveColorChoice::Neutrals, (n_c, n_m, n_y, n_k)), (SelectiveColorChoice::Blacks, (k_c, k_m, k_y, k_k)), - ] - .into_iter() - .fold((0., 0., 0.), |acc, (color_parameter_group, (c, m, y, k))| { + ]; + let mut sum = Vec3::ZERO; + for i in 0..array.len() { + let (color_parameter_group, (c, m, y, k)) = array[i]; + // Skip this color parameter group... // ...if it's unchanged from the default of zero offset on all CMYK parameters, or... // ...if this pixel's color isn't in the range affected by this color parameter group if (c < f32::EPSILON && m < f32::EPSILON && y < f32::EPSILON && k < f32::EPSILON) || (!pixel_color_range(color_parameter_group)) { - return acc; + continue; } let (c, m, y, k) = (c / 100., m / 100., y / 100., k / 100.); @@ -892,14 +937,15 @@ fn selective_color>( SelectiveColorChoice::Blacks => 1. - max(r, g, b) * 2., }; - let offset_r = ((c + k * (c + 1.)) * slope_r).clamp(-r, -r + 1.) * color_parameter_group_scale_factor; - let offset_g = ((m + k * (m + 1.)) * slope_g).clamp(-g, -g + 1.) * color_parameter_group_scale_factor; - let offset_b = ((y + k * (y + 1.)) * slope_b).clamp(-b, -b + 1.) * color_parameter_group_scale_factor; + let offset_r = f32::clamp((c + k * (c + 1.)) * slope_r, -r, -r + 1.) * color_parameter_group_scale_factor; + let offset_g = f32::clamp((m + k * (m + 1.)) * slope_g, -g, -g + 1.) * color_parameter_group_scale_factor; + let offset_b = f32::clamp((y + k * (y + 1.)) * slope_b, -b, -b + 1.) * color_parameter_group_scale_factor; - (acc.0 + offset_r, acc.1 + offset_g, acc.2 + offset_b) - }); + sum += Vec3::new(offset_r, offset_g, offset_b); + } - let color = Color::from_rgbaf32_unchecked((r + sum_r).clamp(0., 1.), (g + sum_g).clamp(0., 1.), (b + sum_b).clamp(0., 1.), a); + let rgb = Vec3::new(r, g, b); + let color = Color::from_vec4(Vec4::from(((sum + rgb).clamp(Vec3::ZERO, Vec3::ONE), a))); color.to_linear_srgb() }); @@ -921,6 +967,7 @@ fn posterize>( Table, GradientStops, )] + #[gpu_image] mut input: T, #[default(4)] #[hard_min(2.)] @@ -955,6 +1002,7 @@ fn exposure>( Table, GradientStops, )] + #[gpu_image] mut input: T, exposure: f32, offset: f32, diff --git a/node-graph/graster-nodes/src/blending_nodes.rs b/node-graph/graster-nodes/src/blending_nodes.rs index 6efc335fbb..13f93773d4 100644 --- a/node-graph/graster-nodes/src/blending_nodes.rs +++ b/node-graph/graster-nodes/src/blending_nodes.rs @@ -132,7 +132,7 @@ pub fn apply_blend_mode(foreground: Color, background: Color, blend_mode: BlendM } } -#[node_macro::node(category("Raster"), shader_node(PerPixelAdjust))] +#[node_macro::node(category("Raster"), cfg(feature = "std"))] fn blend + Send>( _: impl Ctx, #[implementations( @@ -141,6 +141,7 @@ fn blend + Send>( Table, GradientStops, )] + #[gpu_image] over: T, #[expose] #[implementations( @@ -149,6 +150,7 @@ fn blend + Send>( Table, GradientStops, )] + #[gpu_image] under: T, blend_mode: BlendMode, #[default(100.)] opacity: PercentageF32, @@ -165,6 +167,7 @@ fn color_overlay>( Table, GradientStops, )] + #[gpu_image] mut image: T, #[default(Color::BLACK)] color: Color, blend_mode: BlendMode, diff --git a/node-graph/graster-nodes/src/fullscreen_vertex.rs b/node-graph/graster-nodes/src/fullscreen_vertex.rs new file mode 100644 index 0000000000..b8ef775b9f --- /dev/null +++ b/node-graph/graster-nodes/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/graster-nodes/src/lib.rs b/node-graph/graster-nodes/src/lib.rs index 8dc169cea6..080504f9ea 100644 --- a/node-graph/graster-nodes/src/lib.rs +++ b/node-graph/graster-nodes/src/lib.rs @@ -4,6 +4,11 @@ pub mod adjust; pub mod adjustments; pub mod blending_nodes; pub mod cubic_spline; +pub mod fullscreen_vertex; + +/// required by shader macro +#[cfg(feature = "shader-nodes")] +pub use graphene_raster_nodes_shaders::WGSL_SHADER; #[cfg(feature = "std")] pub mod curve; diff --git a/node-graph/gstd/Cargo.toml b/node-graph/gstd/Cargo.toml index 6eb7e8a075..7f634c96ac 100644 --- a/node-graph/gstd/Cargo.toml +++ b/node-graph/gstd/Cargo.toml @@ -21,6 +21,7 @@ image-compare = [] vello = ["dep:vello", "gpu"] resvg = [] wayland = ["graph-craft/wayland"] +shader-nodes = ["graphene-raster-nodes/shader-nodes"] [dependencies] # Local dependencies diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index d8351fb87e..3917a14c3c 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -19,7 +19,7 @@ use graphene_core::transform::{Footprint, Transform}; use graphene_core::uuid::{NodeId, generate_uuid}; use graphene_core::vector::Vector; use graphene_core::vector::click_target::{ClickTarget, FreePoint}; -use graphene_core::vector::style::{Fill, Stroke, StrokeAlign, ViewMode}; +use graphene_core::vector::style::{Fill, PaintOrder, Stroke, StrokeAlign, ViewMode}; use graphene_core::{Artboard, Graphic}; use kurbo::Affine; use num_traits::Zero; @@ -202,6 +202,12 @@ pub fn format_transform_matrix(transform: DAffine2) -> String { }) + ")" } +fn max_scale(transform: DAffine2) -> f64 { + let sx = transform.x_axis.length_squared(); + let sy = transform.y_axis.length_squared(); + (sx + sy).sqrt() +} + pub fn to_transform(transform: DAffine2) -> usvg::Transform { let cols = transform.to_cols_array(); usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32) @@ -690,11 +696,36 @@ impl Render for Table { } else { MaskType::Mask }; + let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed()); let can_draw_aligned_stroke = path_is_closed && vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()); - let can_use_paint_order = !(row.element.style.fill().is_none() || mask_type == MaskType::Clip); + let can_use_paint_order = !(row.element.style.fill().is_none() || !row.element.style.fill().is_opaque() || mask_type == MaskType::Clip); + + let needs_separate_fill = can_draw_aligned_stroke && !can_use_paint_order; + let wants_stroke_below = vector.style.stroke().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow); + + if needs_separate_fill && !wants_stroke_below { + render.leaf_tag("path", |attributes| { + attributes.push("d", path.clone()); + let matrix = format_transform_matrix(element_transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + let mut style = row.element.style.clone(); + style.clear_stroke(); + let fill_and_stroke = style.render( + &mut attributes.0.svg_defs, + element_transform, + applied_stroke_transform, + bounds_matrix, + transformed_bounds_matrix, + &render_params, + ); + attributes.push_val(fill_and_stroke); + }); + } - let push_id = if can_draw_aligned_stroke && !can_use_paint_order { + let push_id = needs_separate_fill.then_some({ let id = format!("alignment-{}", generate_uuid()); let mut element = row.element.clone(); @@ -708,13 +739,11 @@ impl Render for Table { source_node_id: None, }); - Some((id, mask_type, vector_row)) - } else { - None - }; + (id, mask_type, vector_row) + }); render.leaf_tag("path", |attributes| { - attributes.push("d", path); + attributes.push("d", path.clone()); let matrix = format_transform_matrix(element_transform); if !matrix.is_empty() { attributes.push("transform", matrix); @@ -724,16 +753,24 @@ impl Render for Table { if let Some((ref id, mask_type, ref vector_row)) = push_id { let mut svg = SvgRender::new(); vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform)); - - let weight = row.element.style.stroke().unwrap().weight * row.transform.matrix2.determinant(); + let stroke = row.element.style.stroke().unwrap(); + let weight = stroke.effective_width() * max_scale(applied_stroke_transform); let quad = Quad::from_box(transformed_bounds).inflate(weight); let (x, y) = quad.top_left().into(); let (width, height) = (quad.bottom_right() - quad.top_left()).into(); + write!(defs, r##"{}"##, svg.svg_defs).unwrap(); let rect = format!(r##""##); + match mask_type { MaskType::Clip => write!(defs, r##"{}"##, svg.svg.to_svg_string()).unwrap(), - MaskType::Mask => write!(defs, r##"{}{}"##, rect, svg.svg.to_svg_string()).unwrap(), + MaskType::Mask => write!( + defs, + r##"{}{}"##, + rect, + svg.svg.to_svg_string() + ) + .unwrap(), } } @@ -741,10 +778,12 @@ impl Render for Table { render_params.aligned_strokes = can_draw_aligned_stroke; render_params.override_paint_order = can_draw_aligned_stroke && can_use_paint_order; - let fill_and_stroke = row - .element - .style - .render(defs, element_transform, applied_stroke_transform, bounds_matrix, transformed_bounds_matrix, &render_params); + let mut style = row.element.style.clone(); + if needs_separate_fill { + style.clear_fill(); + } + + let fill_and_stroke = style.render(defs, element_transform, applied_stroke_transform, bounds_matrix, transformed_bounds_matrix, &render_params); if let Some((id, mask_type, _)) = push_id { let selector = format!("url(#{id})"); @@ -761,6 +800,28 @@ impl Render for Table { attributes.push("style", row.alpha_blending.blend_mode.render()); } }); + + // When splitting passes and stroke is below, draw the fill after the stroke. + if needs_separate_fill && wants_stroke_below { + render.leaf_tag("path", |attributes| { + attributes.push("d", path); + let matrix = format_transform_matrix(element_transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + let mut style = row.element.style.clone(); + style.clear_stroke(); + let fill_and_stroke = style.render( + &mut attributes.0.svg_defs, + element_transform, + applied_stroke_transform, + bounds_matrix, + transformed_bounds_matrix, + &render_params, + ); + attributes.push_val(fill_and_stroke); + }); + } } } @@ -800,8 +861,8 @@ impl Render for Table { let opacity = row.alpha_blending.opacity(render_params.for_mask); if opacity < 1. || row.alpha_blending.blend_mode != BlendMode::default() { layer = true; - let weight = row.element.style.stroke().unwrap().weight; - let quad = Quad::from_box(layer_bounds).inflate(weight * element_transform.matrix2.determinant()); + let weight = row.element.style.stroke().unwrap().effective_width(); + let quad = Quad::from_box(layer_bounds).inflate(weight * max_scale(applied_stroke_transform)); let layer_bounds = quad.bounding_box(); scene.push_layer( peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver), @@ -814,30 +875,98 @@ impl Render for Table { let can_draw_aligned_stroke = row.element.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && row.element.stroke_bezier_paths().all(|path| path.closed()); - let reorder_for_outside = row.element.style.stroke().is_some_and(|stroke| stroke.align == StrokeAlign::Outside) && !row.element.style.fill().is_none(); - let use_layer = can_draw_aligned_stroke && !reorder_for_outside; - if use_layer { - let mut element = row.element.clone(); - element.style.clear_stroke(); - element.style.set_fill(Fill::solid(Color::BLACK)); + let use_layer = can_draw_aligned_stroke; + let wants_stroke_below = row.element.style.stroke().is_some_and(|s| s.paint_order == graphene_core::vector::style::PaintOrder::StrokeBelow); - let vector_table = Table::new_from_row(TableRow { - element, - alpha_blending: *row.alpha_blending, - transform: *row.transform, - source_node_id: None, - }); + // Closures to avoid duplicated fill/stroke drawing logic + let do_fill = |scene: &mut Scene| match row.element.style.fill() { + Fill::Solid(color) => { + let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])); + scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path); + } + Fill::Gradient(gradient) => { + let mut stops = peniko::ColorStops::new(); + for &(offset, color) in &gradient.stops { + stops.push(peniko::ColorStop { + offset: offset as f32, + color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])), + }); + } - let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); - let weight = row.element.style.stroke().unwrap().weight; - let quad = Quad::from_box(bounds).inflate(weight * element_transform.matrix2.determinant()); - let bounds = quad.bounding_box(); - let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); + let bounds = row.element.nonzero_bounding_box(); + let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); - scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect); - vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform)); - scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcIn), 1., kurbo::Affine::IDENTITY, &rect); - } + let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. { + parent_transform.inverse() + } else { + Default::default() + }; + let mod_points = inverse_parent_transform * multiplied_transform * bound_transform; + + let start = mod_points.transform_point2(gradient.start); + let end = mod_points.transform_point2(gradient.end); + + let fill = peniko::Brush::Gradient(peniko::Gradient { + kind: match gradient.gradient_type { + GradientType::Linear => peniko::GradientKind::Linear { + start: to_point(start), + end: to_point(end), + }, + GradientType::Radial => { + let radius = start.distance(end); + peniko::GradientKind::Radial { + start_center: to_point(start), + start_radius: 0., + end_center: to_point(start), + end_radius: radius as f32, + } + } + }, + stops, + ..Default::default() + }); + let inverse_element_transform = if element_transform.matrix2.determinant() != 0. { + element_transform.inverse() + } else { + Default::default() + }; + let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array()); + scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path); + } + Fill::None => {} + }; + + let do_stroke = |scene: &mut Scene, width_scale: f64| { + if let Some(stroke) = row.element.style.stroke() { + let color = match stroke.color { + Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]), + None => peniko::Color::TRANSPARENT, + }; + let cap = match stroke.cap { + StrokeCap::Butt => Cap::Butt, + StrokeCap::Round => Cap::Round, + StrokeCap::Square => Cap::Square, + }; + let join = match stroke.join { + StrokeJoin::Miter => Join::Miter, + StrokeJoin::Bevel => Join::Bevel, + StrokeJoin::Round => Join::Round, + }; + let stroke = kurbo::Stroke { + width: stroke.weight * width_scale, + miter_limit: stroke.join_miter_limit, + join, + start_cap: cap, + end_cap: cap, + dash_pattern: stroke.dash_lengths.into(), + dash_offset: stroke.dash_offset, + }; + + if stroke.width > 0. { + scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path); + } + } + }; // Render the path match render_params.view_mode { @@ -861,120 +990,76 @@ impl Render for Table { scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color, None, &path); } _ => { - enum Op { - Fill, - Stroke, - } + if use_layer { + let mut element = row.element.clone(); + element.style.clear_stroke(); + element.style.set_fill(Fill::solid(Color::BLACK)); + + let vector_table = Table::new_from_row(TableRow { + element, + alpha_blending: *row.alpha_blending, + transform: *row.transform, + source_node_id: None, + }); + + let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); + let weight = row.element.style.stroke().unwrap().effective_width(); + let quad = Quad::from_box(bounds).inflate(weight * max_scale(applied_stroke_transform)); + let bounds = quad.bounding_box(); + let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); + + let compose = if row.element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) { + peniko::Compose::SrcOut + } else { + peniko::Compose::SrcIn + }; + + if wants_stroke_below { + scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect); + vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform)); + scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect); + + do_stroke(scene, 2.); + + scene.pop_layer(); + scene.pop_layer(); + + do_fill(scene); + } else { + // Fill first (unclipped), then stroke (clipped) above + do_fill(scene); + + scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect); + vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform)); + scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect); + + do_stroke(scene, 2.); + + scene.pop_layer(); + scene.pop_layer(); + } + } else { + // Non-aligned strokes or open paths: default order behavior + enum Op { + Fill, + Stroke, + } - let order = match row.element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) || reorder_for_outside { - true => [Op::Stroke, Op::Fill], - false => [Op::Fill, Op::Stroke], // Default - }; + let order = match row.element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) { + true => [Op::Stroke, Op::Fill], + false => [Op::Fill, Op::Stroke], // Default + }; - for operation in order { - match operation { - Op::Fill => { - match row.element.style.fill() { - Fill::Solid(color) => { - let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])); - scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path); - } - Fill::Gradient(gradient) => { - let mut stops = peniko::ColorStops::new(); - for &(offset, color) in &gradient.stops { - stops.push(peniko::ColorStop { - offset: offset as f32, - color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])), - }); - } - // Compute bounding box of the shape to determine the gradient start and end points - let bounds = row.element.nonzero_bounding_box(); - let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); - - let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. { - parent_transform.inverse() - } else { - Default::default() - }; - let mod_points = inverse_parent_transform * multiplied_transform * bound_transform; - - let start = mod_points.transform_point2(gradient.start); - let end = mod_points.transform_point2(gradient.end); - - let fill = peniko::Brush::Gradient(peniko::Gradient { - kind: match gradient.gradient_type { - GradientType::Linear => peniko::GradientKind::Linear { - start: to_point(start), - end: to_point(end), - }, - GradientType::Radial => { - let radius = start.distance(end); - peniko::GradientKind::Radial { - start_center: to_point(start), - start_radius: 0., - end_center: to_point(start), - end_radius: radius as f32, - } - } - }, - stops, - ..Default::default() - }); - // Vello does `element_transform * brush_transform` internally. We don't want element_transform to have any impact so we need to left multiply by the inverse. - // This makes the final internal brush transform equal to `parent_transform`, allowing you to stretch a gradient by transforming the parent folder. - let inverse_element_transform = if element_transform.matrix2.determinant() != 0. { - element_transform.inverse() - } else { - Default::default() - }; - let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array()); - scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path); - } - Fill::None => {} - }; - } - Op::Stroke => { - if let Some(stroke) = row.element.style.stroke() { - let color = match stroke.color { - Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]), - None => peniko::Color::TRANSPARENT, - }; - let cap = match stroke.cap { - StrokeCap::Butt => Cap::Butt, - StrokeCap::Round => Cap::Round, - StrokeCap::Square => Cap::Square, - }; - let join = match stroke.join { - StrokeJoin::Miter => Join::Miter, - StrokeJoin::Bevel => Join::Bevel, - StrokeJoin::Round => Join::Round, - }; - let stroke = kurbo::Stroke { - width: stroke.weight * if can_draw_aligned_stroke { 2. } else { 1. }, - miter_limit: stroke.join_miter_limit, - join, - start_cap: cap, - end_cap: cap, - dash_pattern: stroke.dash_lengths.into(), - dash_offset: stroke.dash_offset, - }; - - // Draw the stroke if it's visible - if stroke.width > 0. { - scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path); - } - } + for operation in order { + match operation { + Op::Fill => do_fill(scene), + Op::Stroke => do_stroke(scene, 1.), } } } } } - if use_layer { - scene.pop_layer(); - scene.pop_layer(); - } - // If we pushed a layer for opacity or a blend mode, we need to pop it if layer { scene.pop_layer(); @@ -1028,7 +1113,7 @@ impl Render for Table { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for row in self.iter() { - let stroke_width = row.element.style.stroke().as_ref().map_or(0., Stroke::weight); + let stroke_width = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width); let filled = row.element.style.fill() != &Fill::None; let fill = |mut subpath: Subpath<_>| { if filled { diff --git a/node-graph/node-macro/src/buffer_struct.rs b/node-graph/node-macro/src/buffer_struct.rs new file mode 100644 index 0000000000..1caa94b997 --- /dev/null +++ b/node-graph/node-macro/src/buffer_struct.rs @@ -0,0 +1,261 @@ +use crate::crate_ident::CrateIdent; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{ToTokens, format_ident, quote}; +use std::collections::HashSet; +use syn::punctuated::Punctuated; +use syn::visit_mut::VisitMut; +use syn::{Fields, GenericParam, Generics, Item, ItemEnum, ItemStruct, Meta, MetaList, Path, PathSegment, Result, Token, TypeParam, TypeParamBound, visit_mut}; + +pub fn derive_buffer_struct(crate_ident: &CrateIdent, content: proc_macro::TokenStream) -> Result { + let item = syn::parse::(content)?; + match &item { + Item::Enum(item) => derive_buffer_struct_enum(crate_ident, item), + Item::Struct(item) => derive_buffer_struct_struct(crate_ident, item), + _ => Err(syn::Error::new_spanned(&item, "Expected a struct or an enum")), + } +} + +pub fn derive_buffer_struct_enum(crate_ident: &CrateIdent, item: &ItemEnum) -> Result { + let gcore_shaders = crate_ident.gcore_shaders()?; + let mod_buffer_struct = quote!(#gcore_shaders::shaders::buffer_struct); + let reexport = quote!(#gcore_shaders::shaders::__private); + + if !item.generics.params.is_empty() { + return Err(syn::Error::new_spanned(&item.generics, "enum must not have any generics")); + } + + let enum_requirements_error = || { + syn::Error::new( + Span::call_site(), + "deriving `BufferStruct` on an enum requires `#[repr(u32)]` and `#[derive(num_enum::FromPrimitive, num_enum::IntoPrimitive)]`", + ) + }; + let repr_path = Path::from(format_ident!("repr")); + let repr = item + .attrs + .iter() + .filter_map(|a| match &a.meta { + Meta::List(MetaList { path, tokens, .. }) if *path == repr_path => Some(tokens), + _ => None, + }) + .next() + .ok_or_else(enum_requirements_error)?; + + let ident = &item.ident; + Ok(quote! { + unsafe impl #mod_buffer_struct::BufferStruct for #ident + { + type Buffer = #repr; + + fn write(from: Self) -> Self::Buffer { + <#repr as From>::from(from) + } + + fn read(from: Self::Buffer) -> Self { + ::from_primitive(from) + } + } + }) +} + +/// see `BufferStruct` docs +/// +/// This is also largely copied from my (@firestar99) project and adjusted +pub fn derive_buffer_struct_struct(crate_ident: &CrateIdent, item: &ItemStruct) -> Result { + let gcore_shaders = crate_ident.gcore_shaders()?; + let mod_buffer_struct = quote!(#gcore_shaders::shaders::buffer_struct); + let reexport = quote!(#gcore_shaders::shaders::__private); + + let generics = item + .generics + .params + .iter() + .filter_map(|g| match g { + GenericParam::Lifetime(_) => None, + GenericParam::Type(t) => Some(t.ident.clone()), + GenericParam::Const(c) => Some(c.ident.clone()), + }) + .collect(); + + let mut members_buffer = Punctuated::::new(); + let mut write = Punctuated::::new(); + let mut read = Punctuated::::new(); + let mut gen_name_gen = GenericNameGen::new(); + let mut gen_ref_tys = Vec::new(); + let (members_buffer, write, read) = match &item.fields { + Fields::Named(named) => { + for f in &named.named { + let name = f.ident.as_ref().unwrap(); + let mut ty = f.ty.clone(); + let mut visitor = GenericsVisitor::new(&item.ident, &generics); + visit_mut::visit_type_mut(&mut visitor, &mut ty); + if visitor.found_generics { + gen_ref_tys.push(f.ty.clone()); + let gen_ident = gen_name_gen.next(); + members_buffer.push(quote!(#name: #gen_ident)); + } else { + members_buffer.push(quote! { + #name: <#ty as #mod_buffer_struct::BufferStruct>::Buffer + }); + } + + write.push(quote! { + #name: <#ty as #mod_buffer_struct::BufferStruct>::write(from.#name) + }); + read.push(quote! { + #name: <#ty as #mod_buffer_struct::BufferStruct>::read(from.#name) + }); + } + (quote!({#members_buffer}), quote!(Self::Buffer {#write}), quote!(Self {#read})) + } + Fields::Unnamed(unnamed) => { + for (i, f) in unnamed.unnamed.iter().enumerate() { + let mut ty = f.ty.clone(); + let mut visitor = GenericsVisitor::new(&item.ident, &generics); + visit_mut::visit_type_mut(&mut visitor, &mut ty); + if visitor.found_generics { + gen_ref_tys.push(f.ty.clone()); + members_buffer.push(gen_name_gen.next().into_token_stream()); + } else { + members_buffer.push(quote! { + <#ty as #mod_buffer_struct::BufferStruct>::Buffer + }); + } + + let index = syn::Index::from(i); + write.push(quote! { + <#ty as #mod_buffer_struct::BufferStruct>::write(from.#index) + }); + read.push(quote! { + <#ty as #mod_buffer_struct::BufferStruct>::read(from.#index) + }); + } + (quote!((#members_buffer);), quote!(Self::Buffer(#write)), quote!(Self(#read))) + } + Fields::Unit => (quote!(;), quote!(let _ = from; Self::Buffer {}), quote!(let _ = from; Self::Shader {})), + }; + + let generics_decl = &item.generics; + let generics_ref = decl_to_ref(item.generics.params.iter()); + let generics_where = gen_ref_tys + .iter() + .map(|ty| quote!(#ty: #mod_buffer_struct::BufferStruct)) + .collect::>() + .into_token_stream(); + + let generics_decl_any = gen_name_gen.decl(quote! { + #reexport::bytemuck::Pod + Send + Sync + }); + let generics_ref_buffer = gen_ref_tys + .iter() + .map(|ty| quote!(<#ty as #mod_buffer_struct::BufferStruct>::Buffer)) + .collect::>() + .into_token_stream(); + + let vis = &item.vis; + let ident = &item.ident; + let buffer_ident = format_ident!("{}Buffer", ident); + Ok(quote! { + #[repr(C)] + #[derive(Copy, Clone, #reexport::bytemuck::Zeroable, #reexport::bytemuck::Pod)] + #vis struct #buffer_ident #generics_decl_any #members_buffer + + unsafe impl #generics_decl #mod_buffer_struct::BufferStruct for #ident #generics_ref + where + #ident #generics_ref: Copy, + #generics_where + { + type Buffer = #buffer_ident <#generics_ref_buffer>; + + fn write(from: Self) -> Self::Buffer { + #write + } + + fn read(from: Self::Buffer) -> Self { + #read + } + } + }) +} + +struct GenericsVisitor<'a> { + self_ident: &'a Ident, + generics: &'a HashSet, + found_generics: bool, +} + +impl<'a> GenericsVisitor<'a> { + pub fn new(self_ident: &'a Ident, generics: &'a HashSet) -> Self { + Self { + self_ident, + generics, + found_generics: false, + } + } +} + +impl VisitMut for GenericsVisitor<'_> { + fn visit_ident_mut(&mut self, i: &mut Ident) { + if self.generics.contains(i) { + self.found_generics = true; + } + visit_mut::visit_ident_mut(self, i); + } + + fn visit_path_segment_mut(&mut self, i: &mut PathSegment) { + if i.ident.to_string() == "Self" { + i.ident = self.self_ident.clone(); + } + visit_mut::visit_path_segment_mut(self, i); + } +} + +struct GenericNameGen(u32); + +impl GenericNameGen { + pub fn new() -> Self { + Self(0) + } + + pub fn next(&mut self) -> Ident { + let i = self.0; + self.0 += 1; + format_ident!("T{}", i) + } + + pub fn decl(self, ty: TokenStream) -> Generics { + let params: Punctuated = (0..self.0) + .map(|i| { + GenericParam::Type(TypeParam { + attrs: Vec::new(), + ident: format_ident!("T{}", i), + colon_token: Some(Default::default()), + bounds: Punctuated::from_iter([TypeParamBound::Verbatim(ty.clone())]), + eq_token: None, + default: None, + }) + }) + .collect(); + if !params.is_empty() { + Generics { + lt_token: Some(Default::default()), + params, + gt_token: Some(Default::default()), + where_clause: None, + } + } else { + Generics::default() + } + } +} + +fn decl_to_ref<'a>(generics: impl Iterator) -> TokenStream { + let out = generics + .map(|generic| match generic { + GenericParam::Lifetime(l) => l.lifetime.to_token_stream(), + GenericParam::Type(t) => t.ident.to_token_stream(), + GenericParam::Const(c) => c.ident.to_token_stream(), + }) + .collect::>(); + if out.is_empty() { TokenStream::new() } else { quote!(<#out>) } +} diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 57a8b75cfd..6b96e10aea 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -1,6 +1,5 @@ use crate::parsing::*; use convert_case::{Case, Casing}; -use proc_macro_crate::FoundCrate; use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, format_ident, quote, quote_spanned}; use std::sync::atomic::AtomicU64; @@ -10,7 +9,7 @@ use syn::token::Comma; use syn::{Error, Ident, PatIdent, Token, WhereClause, WherePredicate, parse_quote}; static NODE_ID: AtomicU64 = AtomicU64::new(0); -pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result { +pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result { let ParsedNodeFn { vis, attributes, @@ -24,10 +23,10 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result syn::Result quote!(crate), - FoundCrate::Name(name) => { - let ident = Ident::new(name, proc_macro2::Span::call_site()); - quote!( #ident ) - } - }; - let mut future_idents = Vec::new(); let field_types: Vec<_> = fields @@ -295,6 +286,8 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result syn::Result, + gcore_shaders: syn::Result, + wgpu_executor: syn::Result, +} + +impl CrateIdent { + pub fn gcore(&self) -> syn::Result<&TokenStream> { + self.gcore.as_ref().map_err(Clone::clone) + } + + pub fn gcore_shaders(&self) -> syn::Result<&TokenStream> { + self.gcore_shaders.as_ref().map_err(Clone::clone) + } + + pub fn wgpu_executor(&self) -> syn::Result<&TokenStream> { + self.wgpu_executor.as_ref().map_err(Clone::clone) + } +} + +impl Default for CrateIdent { + fn default() -> Self { + let find_crate = |orig_name| match crate_name(orig_name) { + Ok(FoundCrate::Itself) => Ok(quote!(crate)), + Ok(FoundCrate::Name(name)) => { + let name = format_ident!("{}", name); + Ok(quote!(::#name)) + } + Err(e) => Err(syn::Error::new(Span::call_site(), &format!("Could not find dependency on `{orig_name}`:\n{e}"))), + }; + + let gcore = find_crate("graphene-core"); + let gcore_shaders = find_crate("graphene-core-shaders").or_else(|eshaders| { + gcore + .as_ref() + .map(Clone::clone) + .map_err(|ecore| syn::Error::new(Span::call_site(), &format!("{ecore}\n\nFallback: {eshaders}"))) + }); + let wgpu_executor = find_crate("wgpu-executor"); + Self { gcore, gcore_shaders, wgpu_executor } + } +} diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index 001d3074ce..53901d153d 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -1,8 +1,11 @@ +use crate::crate_ident::CrateIdent; use proc_macro::TokenStream; use proc_macro_error2::proc_macro_error; use syn::GenericParam; +mod buffer_struct; mod codegen; +mod crate_ident; mod derive_choice_type; mod parsing; mod shader_nodes; @@ -13,7 +16,7 @@ mod validation; #[proc_macro_attribute] pub fn node(attr: TokenStream, item: TokenStream) -> TokenStream { // Performs the `node_impl` macro's functionality of attaching an `impl Node for TheGivenStruct` block to the node struct - parsing::new_node_fn(attr.into(), item.into()).into() + parsing::new_node_fn(attr.into(), item.into()).unwrap_or_else(|err| err.to_compile_error()).into() } /// Generate meta-information for an enum. @@ -27,5 +30,12 @@ pub fn node(attr: TokenStream, item: TokenStream) -> TokenStream { /// Doc comments on a variant become tooltip text. #[proc_macro_derive(ChoiceType, attributes(widget, menu_separator, label, icon))] pub fn derive_choice_type(input_item: TokenStream) -> TokenStream { - TokenStream::from(derive_choice_type::derive_choice_type_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error())) + derive_choice_type::derive_choice_type_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()).into() +} + +/// Derive a struct to implement `ShaderStruct`, see that for docs. +#[proc_macro_derive(BufferStruct)] +pub fn derive_buffer_struct(input_item: TokenStream) -> TokenStream { + let crate_ident = CrateIdent::default(); + TokenStream::from(buffer_struct::derive_buffer_struct(&crate_ident, input_item.into()).unwrap_or_else(|err| err.to_compile_error())) } diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index ef08388499..b86ca2cae9 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -12,6 +12,7 @@ use syn::{ }; use crate::codegen::generate_node_code; +use crate::crate_ident::CrateIdent; use crate::shader_nodes::ShaderNodeType; #[derive(Clone, Debug)] @@ -35,11 +36,10 @@ pub(crate) struct ParsedNodeFn { pub(crate) is_async: bool, pub(crate) fields: Vec, pub(crate) body: TokenStream2, - pub(crate) crate_name: proc_macro_crate::FoundCrate, pub(crate) description: String, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub(crate) struct NodeFnAttributes { pub(crate) category: Option, pub(crate) display_name: Option, @@ -120,6 +120,8 @@ pub enum ParsedFieldType { Node(NodeParsedField), } +/// a param of any kind, either a concrete type or a generic type with a set of possible types specified via +/// `#[implementation(type)]` #[derive(Clone, Debug)] pub struct RegularParsedField { pub ty: Type, @@ -131,8 +133,10 @@ pub struct RegularParsedField { pub number_hard_max: Option, pub number_mode_range: Option, pub implementations: Punctuated, + pub gpu_image: bool, } +/// a param of `impl Node` with `#[implementation(in -> out)]` #[derive(Clone, Debug)] pub struct NodeParsedField { pub input_type: Type, @@ -140,7 +144,7 @@ pub struct NodeParsedField { pub implementations: Punctuated, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub(crate) struct Input { pub(crate) pat_ident: PatIdent, pub(crate) ty: Type, @@ -309,12 +313,6 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result syn::Result syn::Resul .map_err(|e| Error::new_spanned(attr, format!("Invalid `step` for argument '{ident}': {e}\nUSAGE EXAMPLE: #[step(2.)]"))) }) .transpose()?; + let gpu_image = extract_attribute(attrs, "gpu_image").is_some(); let (is_node, node_input_type, node_output_type) = parse_node_type(&ty); let description = attrs @@ -590,6 +588,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul ty, value_source, implementations, + gpu_image, }), name, description, @@ -636,28 +635,16 @@ fn extract_attribute<'a>(attrs: &'a [Attribute], name: &str) -> Option<&'a Attri } // Modify the new_node_fn function to use the code generation -pub fn new_node_fn(attr: TokenStream2, item: TokenStream2) -> TokenStream2 { - let parse_result = parse_node_fn(attr, item.clone()); - let Ok(mut parsed_node) = parse_result else { - let e = parse_result.unwrap_err(); - return Error::new(e.span(), format!("Failed to parse node function: {e}")).to_compile_error(); - }; - +pub fn new_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result { + let crate_ident = CrateIdent::default(); + let mut parsed_node = parse_node_fn(attr, item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node function: {e}")))?; parsed_node.replace_impl_trait_in_input(); - if let Err(e) = crate::validation::validate_node_fn(&parsed_node) { - return Error::new(e.span(), format!("Validation Error:\n{e}")).to_compile_error(); - } - match generate_node_code(&parsed_node) { - Ok(parsed) => parsed, - Err(e) => { - // Return the error as a compile error - Error::new(e.span(), format!("Failed to parse node function: {e}")).to_compile_error() - } - } + crate::validation::validate_node_fn(&parsed_node).map_err(|e| Error::new(e.span(), format!("Validation Error: {e}")))?; + generate_node_code(&crate_ident, &parsed_node).map_err(|e| Error::new(e.span(), format!("Failed to generate node code: {e}"))) } impl ParsedNodeFn { - fn replace_impl_trait_in_input(&mut self) { + pub fn replace_impl_trait_in_input(&mut self) { if let Type::ImplTrait(impl_trait) = self.input.ty.clone() { let ident = Ident::new("_Input", impl_trait.span()); let mut bounds = impl_trait.bounds; @@ -829,13 +816,13 @@ mod tests { number_hard_max: None, number_mode_range: None, implementations: Punctuated::new(), + gpu_image: false, }), number_display_decimal_places: None, number_step: None, unit: None, }], body: TokenStream2::new(), - crate_name: FoundCrate::Itself, description: String::from("Multi\nLine\n"), }; @@ -909,6 +896,7 @@ mod tests { number_hard_max: None, number_mode_range: None, implementations: Punctuated::new(), + gpu_image: false, }), number_display_decimal_places: None, number_step: None, @@ -916,7 +904,6 @@ mod tests { }, ], body: TokenStream2::new(), - crate_name: FoundCrate::Itself, description: String::from("Hello\n\t\t\t\tWorld\n"), }; @@ -972,13 +959,13 @@ mod tests { number_hard_max: None, number_mode_range: None, implementations: Punctuated::new(), + gpu_image: false, }), number_display_decimal_places: None, number_step: None, unit: None, }], body: TokenStream2::new(), - crate_name: FoundCrate::Itself, description: "Test\n".into(), }; @@ -1038,13 +1025,13 @@ mod tests { p.push(parse_quote!(f64)); p }, + gpu_image: false, }), number_display_decimal_places: None, number_step: None, unit: None, }], body: TokenStream2::new(), - crate_name: FoundCrate::Itself, description: String::new(), }; @@ -1106,13 +1093,13 @@ mod tests { number_hard_max: None, number_mode_range: Some(parse_quote!((0., 100.))), implementations: Punctuated::new(), + gpu_image: false, }), number_display_decimal_places: None, number_step: None, unit: None, }], body: TokenStream2::new(), - crate_name: FoundCrate::Itself, description: String::new(), }; @@ -1167,13 +1154,13 @@ mod tests { number_hard_max: None, number_mode_range: None, implementations: Punctuated::new(), + gpu_image: false, }), number_display_decimal_places: None, number_step: None, unit: None, }], body: TokenStream2::new(), - crate_name: FoundCrate::Itself, description: String::new(), }; @@ -1215,7 +1202,6 @@ mod tests { is_async: false, fields: vec![], body: TokenStream2::new(), - crate_name: FoundCrate::Itself, description: String::new(), }; diff --git a/node-graph/node-macro/src/shader_nodes.rs b/node-graph/node-macro/src/shader_nodes.rs deleted file mode 100644 index 919d3ef878..0000000000 --- a/node-graph/node-macro/src/shader_nodes.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::parsing::NodeFnAttributes; -use proc_macro2::{Ident, TokenStream}; -use quote::quote; -use strum::{EnumString, VariantNames}; -use syn::Error; -use syn::parse::{Parse, ParseStream}; - -pub const STD_FEATURE_GATE: &str = "std"; - -pub fn modify_cfg(attributes: &NodeFnAttributes) -> TokenStream { - match (&attributes.cfg, &attributes.shader_node) { - (Some(cfg), Some(_)) => quote!(#[cfg(all(#cfg, feature = #STD_FEATURE_GATE))]), - (Some(cfg), None) => quote!(#[cfg(#cfg)]), - (None, Some(_)) => quote!(#[cfg(feature = #STD_FEATURE_GATE)]), - (None, None) => quote!(), - } -} - -#[derive(Debug, EnumString, VariantNames)] -pub(crate) enum ShaderNodeType { - PerPixelAdjust, -} - -impl Parse for ShaderNodeType { - fn parse(input: ParseStream) -> syn::Result { - let ident: Ident = input.parse()?; - Ok(match ident.to_string().as_str() { - "PerPixelAdjust" => ShaderNodeType::PerPixelAdjust, - _ => return Err(Error::new_spanned(&ident, format!("attr 'shader_node' must be one of {:?}", Self::VARIANTS))), - }) - } -} diff --git a/node-graph/node-macro/src/shader_nodes/mod.rs b/node-graph/node-macro/src/shader_nodes/mod.rs new file mode 100644 index 0000000000..7ff3677d61 --- /dev/null +++ b/node-graph/node-macro/src/shader_nodes/mod.rs @@ -0,0 +1,79 @@ +use crate::crate_ident::CrateIdent; +use crate::parsing::{NodeFnAttributes, ParsedNodeFn}; +use crate::shader_nodes::per_pixel_adjust::PerPixelAdjust; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use strum::VariantNames; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{Error, Token}; + +pub mod per_pixel_adjust; + +pub const STD_FEATURE_GATE: &str = "std"; +pub const SHADER_NODES_FEATURE_GATE: &str = "shader-nodes"; + +pub fn modify_cfg(attributes: &NodeFnAttributes) -> TokenStream { + let feature_gate = match &attributes.shader_node { + // shader node cfg is done on the mod + Some(ShaderNodeType::ShaderNode) => quote!(), + Some(_) => quote!(feature = #STD_FEATURE_GATE), + None => quote!(), + }; + let cfgs: Punctuated<_, Token![,]> = match &attributes.cfg { + None => [&feature_gate].into_iter().collect(), + Some(cfg) => [cfg, &feature_gate].into_iter().collect(), + }; + quote!(#[cfg(all(#cfgs))]) +} + +#[derive(Debug, Clone, VariantNames)] +pub(crate) enum ShaderNodeType { + /// Marker for this node being in a gpu node crate, but not having a gpu implementation. This is distinct from not + /// declaring `shader_node` at all, as it will wrap the CPU node with a `#[cfg(feature = "std")]` feature gate. + None, + /// Marker for this node being a generated gpu node implementation, that should not emit anything to prevent + /// recursively generating more gpu nodes. But it still counts as a gpu node and will get the + /// `#[cfg(feature = "std")]` feature gate around it's impl. + ShaderNode, + PerPixelAdjust(PerPixelAdjust), +} + +impl Parse for ShaderNodeType { + fn parse(input: ParseStream) -> syn::Result { + let ident: Ident = input.parse()?; + Ok(match ident.to_string().as_str() { + "None" => ShaderNodeType::None, + "PerPixelAdjust" => ShaderNodeType::PerPixelAdjust(PerPixelAdjust::parse(input)?), + _ => return Err(Error::new_spanned(&ident, format!("attr 'shader_node' must be one of {:?}", Self::VARIANTS))), + }) + } +} + +pub trait ShaderCodegen { + fn codegen(&self, crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result; +} + +impl ShaderCodegen for ShaderNodeType { + fn codegen(&self, crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result { + match self { + ShaderNodeType::None | ShaderNodeType::ShaderNode => (), + _ => { + if parsed.is_async { + return Err(Error::new_spanned(&parsed.fn_name, "Shader nodes must not be async")); + } + } + } + + match self { + ShaderNodeType::None | ShaderNodeType::ShaderNode => Ok(ShaderTokens::default()), + ShaderNodeType::PerPixelAdjust(x) => x.codegen(crate_ident, parsed), + } + } +} + +#[derive(Clone, Default)] +pub struct ShaderTokens { + pub shader_entry_point: TokenStream, + pub gpu_node: TokenStream, +} diff --git a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs new file mode 100644 index 0000000000..49c084d6d9 --- /dev/null +++ b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs @@ -0,0 +1,342 @@ +use crate::crate_ident::CrateIdent; +use crate::parsing::{Input, NodeFnAttributes, ParsedField, ParsedFieldType, ParsedNodeFn, RegularParsedField}; +use crate::shader_nodes::{SHADER_NODES_FEATURE_GATE, ShaderCodegen, ShaderNodeType, ShaderTokens}; +use convert_case::{Case, Casing}; +use proc_macro2::{Ident, TokenStream}; +use quote::{ToTokens, format_ident, quote}; +use std::borrow::Cow; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{LitStr, PatIdent, Type, parse_quote}; + +#[derive(Debug, Clone)] +pub struct PerPixelAdjust {} + +impl Parse for PerPixelAdjust { + fn parse(_input: ParseStream) -> syn::Result { + Ok(Self {}) + } +} + +impl ShaderCodegen for PerPixelAdjust { + fn codegen(&self, crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result { + let fn_name = &parsed.fn_name; + + let mut params; + let has_uniform; + { + // categorize params + params = parsed + .fields + .iter() + .map(|f| { + let ident = &f.pat_ident; + match &f.ty { + ParsedFieldType::Node { .. } => Err(syn::Error::new_spanned(ident, "PerPixelAdjust shader nodes cannot accept other nodes as generics")), + ParsedFieldType::Regular(RegularParsedField { gpu_image: false, ty, .. }) => Ok(Param { + ident: Cow::Borrowed(&ident.ident), + ty: ty.to_token_stream(), + param_type: ParamType::Uniform, + }), + ParsedFieldType::Regular(RegularParsedField { gpu_image: true, .. }) => { + let param = Param { + ident: Cow::Owned(format_ident!("image_{}", &ident.ident)), + ty: quote!(Image2d), + param_type: ParamType::Image { binding: 0 }, + }; + Ok(param) + } + } + }) + .collect::>>()?; + + has_uniform = params.iter().any(|p| matches!(p.param_type, ParamType::Uniform)); + + // assign image bindings + // if an arg_buffer exists, bindings for images start at 1 to leave 0 for arg buffer + let mut binding_cnt = if has_uniform { 1 } else { 0 }; + for p in params.iter_mut() { + match &mut p.param_type { + ParamType::Image { binding } => { + *binding = binding_cnt; + binding_cnt += 1; + } + ParamType::Uniform => {} + } + } + } + + let entry_point_mod = format_ident!("{}_gpu_entry_point", fn_name); + let entry_point_name_ident = format_ident!("ENTRY_POINT_NAME"); + let entry_point_name = quote!(#entry_point_mod::#entry_point_name_ident); + let uniform_struct_ident = format_ident!("Uniform"); + let uniform_struct = quote!(#entry_point_mod::#uniform_struct_ident); + let shader_node_mod = format_ident!("{}_shader_node", fn_name); + + let codegen = PerPixelAdjustCodegen { + crate_ident, + parsed, + params, + has_uniform, + entry_point_mod, + entry_point_name_ident, + entry_point_name, + uniform_struct_ident, + uniform_struct, + shader_node_mod, + }; + + Ok(ShaderTokens { + shader_entry_point: codegen.codegen_shader_entry_point()?, + gpu_node: codegen.codegen_gpu_node()?, + }) + } +} + +pub struct PerPixelAdjustCodegen<'a> { + crate_ident: &'a CrateIdent, + parsed: &'a ParsedNodeFn, + params: Vec>, + has_uniform: bool, + entry_point_mod: Ident, + entry_point_name_ident: Ident, + entry_point_name: TokenStream, + uniform_struct_ident: Ident, + uniform_struct: TokenStream, + shader_node_mod: Ident, +} + +impl PerPixelAdjustCodegen<'_> { + fn codegen_shader_entry_point(&self) -> syn::Result { + let fn_name = &self.parsed.fn_name; + let gcore_shaders = self.crate_ident.gcore_shaders()?; + let reexport = quote!(#gcore_shaders::shaders::__private); + + let uniform_members = self + .params + .iter() + .filter_map(|Param { ident, ty, param_type }| match param_type { + ParamType::Image { .. } => None, + ParamType::Uniform => Some(quote! {#ident: #ty}), + }) + .collect::>(); + let uniform_struct_ident = &self.uniform_struct_ident; + let uniform_struct = parse_quote! { + #[repr(C)] + #[derive(Copy, Clone)] + pub struct #uniform_struct_ident { + #(pub #uniform_members),* + } + }; + let uniform_struct_shader_struct_derive = crate::buffer_struct::derive_buffer_struct_struct(&self.crate_ident, &uniform_struct)?; + + let image_params = self + .params + .iter() + .filter_map(|Param { ident, ty, param_type }| match param_type { + ParamType::Image { binding } => Some(quote! {#[spirv(descriptor_set = 0, binding = #binding)] #ident: &#ty}), + ParamType::Uniform => None, + }) + .collect::>(); + let call_args = self + .params + .iter() + .map(|Param { ident, param_type, .. }| match param_type { + ParamType::Image { .. } => quote!(Color::from_vec4(#ident.fetch_with(texel_coord, lod(0)))), + ParamType::Uniform => quote!(uniform.#ident), + }) + .collect::>(); + let context = quote!(()); + + let entry_point_mod = &self.entry_point_mod; + let entry_point_name = &self.entry_point_name_ident; + Ok(quote! { + pub mod #entry_point_mod { + use super::*; + use #gcore_shaders::color::Color; + use #reexport::glam::{Vec4, Vec4Swizzles}; + use #reexport::spirv_std::spirv; + use #reexport::spirv_std::image::{Image2d, ImageWithMethods}; + use #reexport::spirv_std::image::sample_with::lod; + + pub const #entry_point_name: &str = core::concat!(core::module_path!(), "::entry_point"); + + #uniform_struct + #uniform_struct_shader_struct_derive + + #[spirv(fragment)] + pub fn entry_point( + #[spirv(frag_coord)] frag_coord: Vec4, + color_out: &mut Vec4, + #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] uniform: &UniformBuffer, + #(#image_params),* + ) { + let uniform = ::read(*uniform); + let texel_coord = frag_coord.xy().as_uvec2(); + let color: Color = #fn_name(#context, #(#call_args),*); + *color_out = color.to_vec4(); + } + } + }) + } + + fn codegen_gpu_node(&self) -> syn::Result { + let gcore = self.crate_ident.gcore()?; + let wgpu_executor = self.crate_ident.wgpu_executor()?; + + // adapt fields for gpu node + let raster_gpu: Type = parse_quote!(#gcore::table::Table<#gcore::raster_types::Raster<#gcore::raster_types::GPU>>); + let mut fields = self + .parsed + .fields + .iter() + .map(|f| match &f.ty { + ParsedFieldType::Regular(reg @ RegularParsedField { gpu_image: true, .. }) => Ok(ParsedField { + pat_ident: PatIdent { + mutability: None, + by_ref: None, + ..f.pat_ident.clone() + }, + ty: ParsedFieldType::Regular(RegularParsedField { + ty: raster_gpu.clone(), + implementations: Punctuated::default(), + ..reg.clone() + }), + ..f.clone() + }), + ParsedFieldType::Regular(RegularParsedField { gpu_image: false, .. }) => Ok(ParsedField { + pat_ident: PatIdent { + mutability: None, + by_ref: None, + ..f.pat_ident.clone() + }, + ..f.clone() + }), + ParsedFieldType::Node { .. } => Err(syn::Error::new_spanned(&f.pat_ident, "PerPixelAdjust shader nodes cannot accept other nodes as generics")), + }) + .collect::>>()?; + + // insert wgpu_executor field + let executor = format_ident!("__wgpu_executor"); + fields.push(ParsedField { + pat_ident: PatIdent { + attrs: vec![], + by_ref: None, + mutability: None, + ident: parse_quote!(#executor), + subpat: None, + }, + name: None, + description: "".to_string(), + widget_override: Default::default(), + ty: ParsedFieldType::Regular(RegularParsedField { + ty: parse_quote!(&'a WgpuExecutor), + exposed: false, + value_source: Default::default(), + number_soft_min: None, + number_soft_max: None, + number_hard_min: None, + number_hard_max: None, + number_mode_range: None, + implementations: Default::default(), + gpu_image: false, + }), + number_display_decimal_places: None, + number_step: None, + unit: None, + }); + + // find exactly one gpu_image field, runtime doesn't support more than 1 atm + let gpu_image_field = { + let mut iter = fields.iter().filter(|f| matches!(f.ty, ParsedFieldType::Regular(RegularParsedField { gpu_image: true, .. }))); + match (iter.next(), iter.next()) { + (Some(v), None) => Ok(v), + (Some(_), Some(more)) => Err(syn::Error::new_spanned(&more.pat_ident, "No more than one parameter must be annotated with `#[gpu_image]`")), + (None, _) => Err(syn::Error::new_spanned(&self.parsed.fn_name, "At least one parameter must be annotated with `#[gpu_image]`")), + }? + }; + let gpu_image = &gpu_image_field.pat_ident.ident; + + // uniform buffer struct construction + let has_uniform = self.has_uniform; + let uniform_buffer = if has_uniform { + let uniform_struct = &self.uniform_struct; + let uniform_members = self + .params + .iter() + .filter_map(|p| match p.param_type { + ParamType::Image { .. } => None, + ParamType::Uniform => Some(p.ident.as_ref()), + }) + .collect::>(); + quote!(Some(&super::#uniform_struct { + #(#uniform_members),* + })) + } else { + // explicit generics placed here cause it's easier than explicitly writing `run_per_pixel_adjust::<()>` + quote!(Option::<&()>::None) + }; + + // node function body + let entry_point_name = &self.entry_point_name; + let body = quote! { + { + #executor.shader_runtime.run_per_pixel_adjust(&::wgpu_executor::shader_runtime::per_pixel_adjust_runtime::Shaders { + wgsl_shader: crate::WGSL_SHADER, + fragment_shader_name: super::#entry_point_name, + has_uniform: #has_uniform, + }, #gpu_image, #uniform_buffer).await + } + }; + + // call node codegen + let mut parsed_node_fn = ParsedNodeFn { + vis: self.parsed.vis.clone(), + attributes: NodeFnAttributes { + display_name: self.parsed.attributes.display_name.as_ref().map(|name| LitStr::new(&format!("{} GPU", name.value()), name.span())), + shader_node: Some(ShaderNodeType::ShaderNode), + ..self.parsed.attributes.clone() + }, + fn_name: self.shader_node_mod.clone(), + struct_name: format_ident!("{}", self.shader_node_mod.to_string().to_case(Case::Pascal)), + mod_name: self.shader_node_mod.clone(), + fn_generics: vec![parse_quote!('a: 'n)], + where_clause: None, + input: Input { + pat_ident: self.parsed.input.pat_ident.clone(), + ty: parse_quote!(impl #gcore::context::Ctx), + implementations: Default::default(), + }, + output_type: raster_gpu, + is_async: true, + fields, + body, + description: self.parsed.description.clone(), + }; + parsed_node_fn.replace_impl_trait_in_input(); + let gpu_node_impl = crate::codegen::generate_node_code(self.crate_ident, &parsed_node_fn)?; + + // wrap node in `mod #gpu_node_mod` + let shader_node_mod = &self.shader_node_mod; + Ok(quote! { + #[cfg(feature = #SHADER_NODES_FEATURE_GATE)] + mod #shader_node_mod { + use super::*; + use #wgpu_executor::WgpuExecutor; + + #gpu_node_impl + } + }) + } +} + +struct Param<'a> { + ident: Cow<'a, Ident>, + ty: TokenStream, + param_type: ParamType, +} + +enum ParamType { + Image { binding: u32 }, + Uniform, +} diff --git a/node-graph/wgpu-executor/src/lib.rs b/node-graph/wgpu-executor/src/lib.rs index 920a002c4e..0b42dd631e 100644 --- a/node-graph/wgpu-executor/src/lib.rs +++ b/node-graph/wgpu-executor/src/lib.rs @@ -1,6 +1,8 @@ mod context; +pub mod shader_runtime; pub mod texture_upload; +use crate::shader_runtime::ShaderRuntime; use anyhow::Result; pub use context::Context; use dyn_any::StaticType; @@ -18,6 +20,7 @@ use wgpu::{Origin3d, SurfaceConfiguration, TextureAspect}; pub struct WgpuExecutor { pub context: Context, vello_renderer: Mutex, + pub shader_runtime: ShaderRuntime, } impl std::fmt::Debug for WgpuExecutor { @@ -195,6 +198,7 @@ impl WgpuExecutor { .ok()?; Some(Self { + shader_runtime: ShaderRuntime::new(&context), context, vello_renderer: vello_renderer.into(), }) diff --git a/node-graph/wgpu-executor/src/shader_runtime/mod.rs b/node-graph/wgpu-executor/src/shader_runtime/mod.rs new file mode 100644 index 0000000000..e7e0df8d94 --- /dev/null +++ b/node-graph/wgpu-executor/src/shader_runtime/mod.rs @@ -0,0 +1,20 @@ +use crate::Context; +use crate::shader_runtime::per_pixel_adjust_runtime::PerPixelAdjustShaderRuntime; + +pub mod per_pixel_adjust_runtime; + +pub const FULLSCREEN_VERTEX_SHADER_NAME: &str = "fullscreen_vertexfullscreen_vertex"; + +pub struct ShaderRuntime { + context: Context, + per_pixel_adjust: PerPixelAdjustShaderRuntime, +} + +impl ShaderRuntime { + pub fn new(context: &Context) -> Self { + Self { + context: context.clone(), + per_pixel_adjust: PerPixelAdjustShaderRuntime::new(), + } + } +} diff --git a/node-graph/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs b/node-graph/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs new file mode 100644 index 0000000000..a6610576d0 --- /dev/null +++ b/node-graph/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs @@ -0,0 +1,241 @@ +use crate::Context; +use crate::shader_runtime::{FULLSCREEN_VERTEX_SHADER_NAME, ShaderRuntime}; +use futures::lock::Mutex; +use graphene_core::raster_types::{GPU, Raster}; +use graphene_core::shaders::buffer_struct::BufferStruct; +use graphene_core::table::{Table, TableRow}; +use std::borrow::Cow; +use std::collections::HashMap; +use wgpu::util::{BufferInitDescriptor, DeviceExt}; +use wgpu::{ + BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, Buffer, BufferBinding, BufferBindingType, BufferUsages, ColorTargetState, Face, + FragmentState, FrontFace, LoadOp, Operations, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, + ShaderModuleDescriptor, ShaderSource, ShaderStages, StoreOp, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureViewDescriptor, TextureViewDimension, VertexState, +}; + +pub struct PerPixelAdjustShaderRuntime { + // TODO: PerPixelAdjustGraphicsPipeline already contains the key as `name` + pipeline_cache: Mutex>, +} + +impl PerPixelAdjustShaderRuntime { + pub fn new() -> Self { + Self { + pipeline_cache: Mutex::new(HashMap::new()), + } + } +} + +impl ShaderRuntime { + pub async fn run_per_pixel_adjust(&self, shaders: &Shaders<'_>, textures: Table>, args: Option<&T>) -> Table> { + let mut cache = self.per_pixel_adjust.pipeline_cache.lock().await; + let pipeline = cache + .entry(shaders.fragment_shader_name.to_owned()) + .or_insert_with(|| PerPixelAdjustGraphicsPipeline::new(&self.context, &shaders)); + + let arg_buffer = args.map(|args| { + let device = &self.context.device; + device.create_buffer_init(&BufferInitDescriptor { + label: Some(&format!("{} arg buffer", pipeline.name.as_str())), + usage: BufferUsages::STORAGE, + contents: bytemuck::bytes_of(&T::write(*args)), + }) + }); + pipeline.dispatch(&self.context, textures, arg_buffer) + } +} + +pub struct Shaders<'a> { + pub wgsl_shader: &'a str, + pub fragment_shader_name: &'a str, + pub has_uniform: bool, +} + +pub struct PerPixelAdjustGraphicsPipeline { + name: String, + has_uniform: bool, + pipeline: wgpu::RenderPipeline, +} + +impl PerPixelAdjustGraphicsPipeline { + pub fn new(context: &Context, info: &Shaders) -> Self { + let device = &context.device; + let name = info.fragment_shader_name.to_owned(); + + let fragment_name = &name; + let fragment_name = &fragment_name[(fragment_name.find("::").unwrap() + 2)..]; + // TODO workaround to naga removing `:` + let fragment_name = fragment_name.replace(":", ""); + let shader_module = device.create_shader_module(ShaderModuleDescriptor { + label: Some(&format!("PerPixelAdjust {} wgsl shader", name)), + source: ShaderSource::Wgsl(Cow::Borrowed(info.wgsl_shader)), + }); + + let entries: &[_] = if info.has_uniform { + &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: false }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + ] + } else { + &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: false }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }] + }; + let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { + label: Some(&format!("PerPixelAdjust {} PipelineLayout", name)), + bind_group_layouts: &[&device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some(&format!("PerPixelAdjust {} BindGroupLayout 0", name)), + entries, + })], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { + label: Some(&format!("PerPixelAdjust {} Pipeline", name)), + layout: Some(&pipeline_layout), + vertex: VertexState { + module: &shader_module, + entry_point: Some(FULLSCREEN_VERTEX_SHADER_NAME), + 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_name), + compilation_options: Default::default(), + targets: &[Some(ColorTargetState { + format: TextureFormat::Rgba8UnormSrgb, + blend: None, + write_mask: Default::default(), + })], + }), + multiview: None, + cache: None, + }); + Self { + pipeline, + name, + has_uniform: info.has_uniform, + } + } + + pub fn dispatch(&self, context: &Context, textures: Table>, arg_buffer: Option) -> Table> { + assert_eq!(self.has_uniform, arg_buffer.is_some()); + let device = &context.device; + let name = self.name.as_str(); + + let mut cmd = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("gpu_invert") }); + let out = textures + .iter() + .map(|instance| { + let tex_in = &instance.element.texture; + let view_in = tex_in.create_view(&TextureViewDescriptor::default()); + let format = tex_in.format(); + + let entries: &[_] = if let Some(arg_buffer) = arg_buffer.as_ref() { + &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::Buffer(BufferBinding { + buffer: arg_buffer, + offset: 0, + size: None, + }), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::TextureView(&view_in), + }, + ] + } else { + &[BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&view_in), + }] + }; + let bind_group = device.create_bind_group(&BindGroupDescriptor { + label: Some(&format!("{name} bind group")), + // `get_bind_group_layout` allocates unnecessary memory, we could create it manually to not do that + layout: &self.pipeline.get_bind_group_layout(0), + entries, + }); + + let tex_out = device.create_texture(&TextureDescriptor { + label: Some(&format!("{name} texture out")), + size: tex_in.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(&format!("{name} render pipeline")), + 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(&self.pipeline); + rp.set_bind_group(0, Some(&bind_group), &[]); + rp.draw(0..3, 0..1); + + TableRow { + element: Raster::new(GPU { texture: tex_out }), + transform: *instance.transform, + alpha_blending: *instance.alpha_blending, + source_node_id: *instance.source_node_id, + } + }) + .collect::>(); + context.queue.submit([cmd.finish()]); + out + } +}