diff --git a/AGENTS.md b/AGENTS.md index 943f2d0e4..abaab40ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,7 +43,6 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se | `horizon-server` | `horizon-server`: `server.toml`, CLI, supervisor of the headless game ([docs/hosting.md](docs/hosting.md)) | | `horizon-server-files` | `server.properties`, eula, ops/whitelist/bans, usercache | | `horizon-friends`, `horizon-relay` | Play with Friends: invite codes, encrypted tunnel, the relay ([docs/play-with-friends.md](docs/play-with-friends.md)) | -| `horizon-shaderpack` | OptiFine/Iris packs: preprocessor, GLSL → WGSL ([docs/shader-packs.md](docs/shader-packs.md)) | | `horizon-mods`, `horizon-mod-sdk`, `cargo-horizon-mod` | Sandboxed WebAssembly mods: runtime, SDK + WIT, `cargo horizon-mod` ([docs/mods.md](docs/mods.md)) | | `horizon-port-pack` | Porting Java modpacks ([docs/modpacks.md](docs/modpacks.md)) | | `horizon-theme` | Design tokens and fonts for the launcher, the game's own menus and the website | @@ -126,7 +125,6 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end | Input | game.rs `fn window_event`, `fn key_pressed`, `fn play_mouse`; L: `keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `osk.rs`, `clipboard.rs`; G: `pad.rs`, `pad_menu.rs` | | Audio | L: `audio.rs` (mixer), `sound_engine.rs`, `sound_events.rs`, `music.rs`, `ambience.rs`; G: `music.rs`, `subtitle_hud.rs` | | Resource packs | L: `packs.rs` + `packs/`, `pack_screen.rs`, `atlas_sources.rs`, `sprites.rs`, `mipmap.rs`, `model_library.rs`, `font.rs`, `lang.rs`; crate `horizon-assets`; G: `packs.rs` | -| Shader packs | L: `shader_pack.rs` (Horizon WGSL packs), `java_pack.rs` + crate `horizon-shaderpack` (Iris/OptiFine), `gfx/pack_stages.rs`, `gfx/java_stages.rs`; G: `shader_packs.rs` | | Agent control | L: `control.rs` (socket), `bin/horizon-ctl/`; G: `control.rs`, `agent.rs`, `events.rs`, `ui_control.rs` ([docs/agent-control.md](docs/agent-control.md)) | | Mods | crate `horizon-mods`; G: `mods.rs`; L: `mod_content.rs`, `mod_list.rs` | | Horizon extras, settings | L: `extras.rs`, `graphics.rs`, `settings.rs`, `minimap.rs`; G: `extras.rs`, `tweaks.rs`, `options.rs` | diff --git a/Cargo.lock b/Cargo.lock index 1a1b288c9..46b99b819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,15 +178,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" -[[package]] -name = "ascii-canvas" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" -dependencies = [ - "term", -] - [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -214,30 +205,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec 0.8.0", -] - [[package]] name = "bit-set" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" dependencies = [ - "bit-vec 0.9.1", + "bit-vec", ] -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bit-vec" version = "0.9.1" @@ -283,16 +259,6 @@ dependencies = [ "objc2 0.6.5", ] -[[package]] -name = "borsh" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" -dependencies = [ - "bytes", - "cfg_aliases", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -484,15 +450,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "coords-hud" version = "0.1.0" @@ -797,41 +754,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.119", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.119", -] - [[package]] name = "dasp_sample" version = "0.11.0" @@ -849,29 +771,6 @@ dependencies = [ "syn 3.0.6", ] -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.119", - "unicode-xid", -] - [[package]] name = "digest" version = "0.10.7" @@ -958,15 +857,6 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" -[[package]] -name = "ena" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" -dependencies = [ - "log", -] - [[package]] name = "encoding_rs" version = "0.8.42" @@ -1230,58 +1120,6 @@ version = "0.30.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" -[[package]] -name = "glsl-lang" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "983a18d0c2ecad18ac9ff3c94b069d6bd03b47405a200836d9d8d63def8e34de" -dependencies = [ - "glsl-lang-lexer", - "glsl-lang-types", - "lalrpop", - "lalrpop-util", - "lang-util", - "once_cell", - "thiserror 2.0.21", -] - -[[package]] -name = "glsl-lang-lexer" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abdbc0bdfbc81d557f4865e695457eb23d68031bfb061fda3444f56511b26eee" -dependencies = [ - "glsl-lang-pp", - "glsl-lang-types", - "lalrpop-util", - "lang-util", - "thiserror 2.0.21", -] - -[[package]] -name = "glsl-lang-pp" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88677876fb27f116015d0ecebda6d77bdf65f3a6fa3f5dfeed10b8c8f04cb2c5" -dependencies = [ - "arrayvec", - "derive_more", - "lang-util", - "string_cache", - "string_cache_codegen", - "thiserror 2.0.21", -] - -[[package]] -name = "glsl-lang-types" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8b1532b424d40df634cf2dc2be56d59f994b9ac07fa7c1e3183fd33db9cb07" -dependencies = [ - "lang-util", - "thiserror 2.0.21", -] - [[package]] name = "gpu-allocator" version = "0.28.0" @@ -1428,7 +1266,6 @@ dependencies = [ "horizon-mods", "horizon-protocol", "horizon-server-files", - "horizon-shaderpack", "horizon-telemetry", "horizon-theme", "horizon-threads", @@ -1443,7 +1280,6 @@ dependencies = [ "serde_json", "sha2", "symphonia", - "toml", "ureq", "wgpu", "winit", @@ -1594,16 +1430,6 @@ dependencies = [ "ureq", ] -[[package]] -name = "horizon-shaderpack" -version = "0.1.0" -dependencies = [ - "glsl-lang", - "naga", - "pp-rs", - "zip", -] - [[package]] name = "horizon-telemetry" version = "0.1.0" @@ -1755,12 +1581,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.1.0" @@ -1943,73 +1763,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keccak" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "lalrpop" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" -dependencies = [ - "ascii-canvas", - "bit-set 0.8.0", - "ena", - "itertools", - "lalrpop-util", - "petgraph 0.7.1", - "pico-args", - "regex", - "regex-syntax", - "sha3", - "string_cache", - "term", - "unicode-xid", - "walkdir", -] - -[[package]] -name = "lalrpop-util" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" -dependencies = [ - "regex-automata", - "rustversion", -] - -[[package]] -name = "lang-util" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1715a1e56998f119cd6d49ac8a69802dcbe4fca96d8291243d6dae87abeb913" -dependencies = [ - "derive_more", - "lalrpop-util", - "lang-util-derive", - "line-span", - "smol_str 0.3.6", - "text-size", -] - -[[package]] -name = "lang-util-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2acc66aedf0e445534e0a6b3f0f6ee20ef6ebb9647dbe10735e52ea8d6a096" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "lazy_static" version = "1.5.1" @@ -2066,12 +1819,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "line-span" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29fc123b2f6600099ca18248f69e3ee02b09c4188c0d98e9a690d90dec3e408a" - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2183,7 +1930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" dependencies = [ "arrayvec", - "bit-set 0.10.0", + "bit-set", "bitflags 2.13.2", "cfg-if", "cfg_aliases", @@ -2196,7 +1943,6 @@ dependencies = [ "naga-types", "num-traits", "once_cell", - "pp-rs", "rustc-hash 1.1.0", "spirv", "thiserror 2.0.21", @@ -2245,12 +1991,6 @@ dependencies = [ "jni-sys 0.3.1", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - [[package]] name = "nix" version = "0.31.3" @@ -2750,16 +2490,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap", -] - [[package]] name = "petgraph" version = "0.8.3" @@ -2771,31 +2501,6 @@ dependencies = [ "indexmap", ] -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project" version = "1.1.13" @@ -2914,21 +2619,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "pp-rs" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb458bb7f6e250e6eb79d5026badc10a3ebb8f9a15d1fff0f13d17c71f4d6dee" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "presser" version = "0.3.1" @@ -3022,15 +2712,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" -dependencies = [ - "rand_core", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -3369,16 +3050,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha3" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" -dependencies = [ - "digest", - "keccak", -] - [[package]] name = "shlex" version = "2.0.1" @@ -3407,12 +3078,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "siphasher" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" - [[package]] name = "slab" version = "0.4.12" @@ -3462,16 +3127,6 @@ dependencies = [ "serde", ] -[[package]] -name = "smol_str" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" -dependencies = [ - "borsh", - "serde_core", -] - [[package]] name = "socket2" version = "0.5.10" @@ -3521,37 +3176,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "subtle" version = "2.6.1" @@ -3668,15 +3292,6 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" -[[package]] -name = "term" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -3686,12 +3301,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "text-size" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" - [[package]] name = "thiserror" version = "1.0.69" @@ -3852,7 +3461,7 @@ checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" dependencies = [ "memchr", "nom", - "petgraph 0.8.3", + "petgraph", ] [[package]] @@ -4573,8 +4182,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" dependencies = [ "arrayvec", - "bit-set 0.10.0", - "bit-vec 0.9.1", + "bit-set", + "bit-vec", "bitflags 2.13.2", "bytemuck", "cfg_aliases", @@ -4624,7 +4233,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set 0.10.0", + "bit-set", "bitflags 2.13.2", "block2 0.6.2", "bytemuck", @@ -5122,7 +4731,7 @@ dependencies = [ "rustix 0.38.44", "sctk-adwaita", "smithay-client-toolkit", - "smol_str 0.2.2", + "smol_str", "tracing", "unicode-segmentation", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index bc67ec748..14493dc01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] exclude = ["launcher", "crates/horizon-telemetry-server", "third_party/wgpu-hal"] -members = ["crates/horizon-core", "crates/horizon-dualsense", "crates/horizon-threads", "crates/horizon-world", "crates/voxel-kernels", "crates/horizon-client", "crates/horizon-server-files", "crates/horizon-server", "crates/horizon-bots", "crates/horizon-assets", "crates/horizon-protocol", "crates/horizon-theme", "crates/horizon-shaderpack", "crates/horizon-prepare", "crates/horizon-telemetry", "crates/horizon-bench", "crates/horizon-friends", "crates/horizon-relay", "crates/horizon-mod-sdk", "crates/horizon-mods", "crates/horizon-mods/tests/guest", "crates/cargo-horizon-mod", "crates/horizon-port-pack", "crates/horizon-release-sign", "crates/horizon-mod-sdk/examples/coords-hud", "xtask"] +members = ["crates/horizon-core", "crates/horizon-dualsense", "crates/horizon-threads", "crates/horizon-world", "crates/voxel-kernels", "crates/horizon-client", "crates/horizon-server-files", "crates/horizon-server", "crates/horizon-bots", "crates/horizon-assets", "crates/horizon-protocol", "crates/horizon-theme", "crates/horizon-prepare", "crates/horizon-telemetry", "crates/horizon-bench", "crates/horizon-friends", "crates/horizon-relay", "crates/horizon-mod-sdk", "crates/horizon-mods", "crates/horizon-mods/tests/guest", "crates/cargo-horizon-mod", "crates/horizon-port-pack", "crates/horizon-release-sign", "crates/horizon-mod-sdk/examples/coords-hud", "xtask"] resolver = "2" [workspace.package] diff --git a/README.md b/README.md index 4abb52b86..6dd192d5d 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ The [documentation](https://playhorizon.dev/docs/) covers each of these. - **The game:** singleplayer with vanilla's world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on saves in vanilla's format. Worlds from 1.8 to 26.2 are upgraded as vanilla does. [Status table](#project-status). - **Horizon's own extras:** new menus (vanilla's are one setting away), graphics presets, distant terrain up to 1024 chunks, controller support, optional zoom, minimap and more, all off by default where they would change the vanilla look. See [Settings](https://playhorizon.dev/docs/settings). -- **Resource packs, shader packs and data packs:** resource packs work, Java shader packs run their full-screen effects (experimental), and data packs load functions, predicates and item modifiers. +- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates and item modifiers. - **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining offline-mode servers. Horizon cannot sign in to a Microsoft account yet, so it cannot join servers that check accounts. See [Multiplayer](https://playhorizon.dev/docs/multiplayer). - **Mods:** Horizon has its own mod system (Rust compiled to WebAssembly). Java mod jars do not run. See [Mods and data packs](https://playhorizon.dev/docs/mods). @@ -57,7 +57,7 @@ Screenshots, captured in the Rust client itself with vanilla assets and no mods Survival at dusk: a zombie walks up, with the hotbar, health, hunger and experience - Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics, Advanced Graphics and Shader Packs + Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics and Advanced Graphics A plains village among savanna hills, with farms, villagers and an iron golem diff --git a/crates/horizon-client/Cargo.toml b/crates/horizon-client/Cargo.toml index 300872765..35e9bee5c 100644 --- a/crates/horizon-client/Cargo.toml +++ b/crates/horizon-client/Cargo.toml @@ -29,13 +29,9 @@ png = "0.18" sha2 = "0.10.9" serde = { version = "1", features = ["derive"] } serde_json = { version = "1.0.151", features = ["raw_value"] } -# Shader packs: pack.toml, and WGSL checked before it reaches wgpu. -toml = "1.1" # hlsl-out: the gfx.rs tests translate every shader as wgpu's DirectX 12 backend does. A normal feature, # not a dev-dependency's, so builds, tests and examples share one build of naga, wgpu and this crate. naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] } -# Java (OptiFine/Iris) shader packs, translated to WGSL. -horizon-shaderpack = { path = "../horizon-shaderpack" } # Sound (ALSA on Linux); the `audio` feature. cpal = { version = "0.16", optional = true } symphonia = { version = "0.5", default-features = false, features = ["ogg", "vorbis"] } diff --git a/crates/horizon-client/src/bin/horizon-ctl/commands.rs b/crates/horizon-client/src/bin/horizon-ctl/commands.rs index f2db98535..cf7be69a6 100644 --- a/crates/horizon-client/src/bin/horizon-ctl/commands.rs +++ b/crates/horizon-client/src/bin/horizon-ctl/commands.rs @@ -147,7 +147,6 @@ const PAGES: Ty = Ty::Enum(&[ "graphics-advanced", "extras", "horizon", - "shader-packs", "agent", ]); diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs index 7ac3d0829..533973805 100644 --- a/crates/horizon-client/src/game.rs +++ b/crates/horizon-client/src/game.rs @@ -138,7 +138,6 @@ mod screens; mod sculk; mod sculk_fixture; mod server_travel; -mod shader_packs; mod shield; mod sign_edit; mod sniffers; @@ -1199,8 +1198,6 @@ struct Game<'a> { modifiers: winit::keyboard::ModifiersState, /// The window's fullscreen request and reported state. fullscreen: horizon_client::window::FullscreenState, - /// The selected Horizon shader pack (`game/shader_packs.rs`). - shader_packs: shader_packs::ShaderPacks, /// MouseHandler's accumulated look and wheel state. mouse: MouseLook, /// An agent moved the mouse (`mouse-move`) since the pointer was last @@ -7434,30 +7431,17 @@ impl Game<'_> { self.tick_player_model(); Ok(()) } - /// What the loaded Java shader pack changes in this dimension's vanilla - /// sky (`sunPathRotation`, `clouds`, `sun`, `moon`); `None` without one. - fn java_sky(&self) -> Option<(f32, horizon_shaderpack::properties::SkyDirectives)> { - self.shader_packs.java_sky(match self.dim { - Dim::Overworld => 0, - Dim::Nether => -1, - Dim::End => 1, - }) - } /// SkyRenderer.renderSunMoonAndStars: bodies on a frame rotated Y -90 then X by angle. - /// A Java pack's `sunPathRotation` tilts it about Z in between, and its - /// `sun = false` / `moon = false` leave those out, as Iris does. Returns - /// the sun and moon quads, and the stars' frame and brightness (the - /// renderer keeps their geometry, built once). + /// Returns the sun and moon quads, and the stars' frame and brightness + /// (the renderer keeps their geometry, built once). fn celestial_geometry( &self, sky: &horizon_client::environment::Sky, ) -> (Vec, Option<(glam::Mat4, f32)>) { let mut out = Vec::new(); let atlas = &self.gui.atlas; - let (tilt, keep) = self.java_sky().unwrap_or_default(); let frame = |angle: f32| { glam::Mat4::from_rotation_y((-90.0f32).to_radians()) - * glam::Mat4::from_rotation_z(tilt.to_radians()) * glam::Mat4::from_rotation_x(angle.to_radians()) }; let mut body = |name: &str, angle: f32, size: f32, color: u32| { @@ -7479,14 +7463,12 @@ impl Game<'_> { }); } }; - if keep.sun { - body( - "environment/celestial/sun", - sky.sun_angle, - 30.0, - 0xFFFF_FFFF, - ); - } + body( + "environment/celestial/sun", + sky.sun_angle, + 30.0, + 0xFFFF_FFFF, + ); let phases = [ "full_moon", "waning_gibbous", @@ -7498,14 +7480,12 @@ impl Game<'_> { "waxing_gibbous", ]; let phase = phases[(self.world.day_time / 24000).rem_euclid(8) as usize]; - if keep.moon { - body( - &format!("environment/celestial/moon/{phase}"), - sky.moon_angle, - 20.0, - 0xFFFF_FFFF, - ); - } + body( + &format!("environment/celestial/moon/{phase}"), + sky.moon_angle, + 20.0, + 0xFFFF_FFFF, + ); // SkyRenderer.renderStars: the static star buffer on the same frame. let stars = (sky.star_brightness > 0.0).then(|| (frame(sky.star_angle), sky.star_brightness)); @@ -9831,7 +9811,6 @@ impl Game<'_> { } return Ok(false); } - self.poll_shader_pack(); let Some(size) = self .renderer .as_ref() @@ -10044,18 +10023,6 @@ impl Game<'_> { self.dragon_fireball_geometry(camera, alpha, &mut dragon); entities.extend(dragon); } - // A Java pack's `clouds` replaces the cloud option (`renderClouds`: - // "false", "fast", "true"), as Iris overrides `getCloudsType`. - let clouds_mode = { - use horizon_shaderpack::properties::CloudSetting; - match self.java_sky().and_then(|(_, sky)| sky.clouds) { - Some(CloudSetting::Off) => "false", - Some(CloudSetting::Fast) => "fast", - Some(CloudSetting::Fancy) => "true", - None => self.settings.clouds.as_str(), - } - .to_owned() - }; let (celestial, stars) = if title { (Vec::new(), None) } else if self.dim == Dim::Overworld @@ -10099,7 +10066,6 @@ impl Game<'_> { let frame = Frame { weather: &weather, weather_time: horizon_client::weather::Time::new(self.world.game_time, alpha as f32), - shader: self.shader_inputs(camera, alpha as f32), nether_lighting: self.dim == Dim::Nether, camera, yaw, @@ -10147,7 +10113,7 @@ impl Game<'_> { hud: &hud, clouds: (!title && self.dim == Dim::Overworld - && horizon_client::clouds::visible(&clouds_mode, sky.cloud_color[3])) + && horizon_client::clouds::visible(&self.settings.clouds, sky.cloud_color[3])) .then(|| self.gui.atlas.sprites.get("horizon/white")) .flatten() .map(|white| horizon_client::clouds::Input { @@ -10157,7 +10123,7 @@ impl Game<'_> { time: self.world.game_time, partial_tick: alpha, range: self.settings.cloud_range, - fast: clouds_mode == "fast", + fast: self.settings.clouds == "fast", color: sky.cloud_color, }), entities: &entities, @@ -10875,9 +10841,6 @@ impl ApplicationHandler for Game<'_> { WindowEvent::CloseRequested => { self.pending_save.as_mut().unwrap().quit = true; } - WindowEvent::DroppedFile(path) => { - self.drop_shader_pack(&path); - } WindowEvent::Focused(focused) => { self.focused = focused; if !focused { @@ -11760,7 +11723,6 @@ impl<'a> Game<'a> { focused: true, modifiers: winit::keyboard::ModifiersState::empty(), fullscreen: horizon_client::window::FullscreenState::new(false), - shader_packs: Default::default(), mouse: MouseLook::default(), agent_pointer: false, smooth_camera: false, @@ -12719,7 +12681,6 @@ fn loading_frame(hud: &HudBatch) -> Frame<'_> { night_vision: 0.0, block_flicker: 0.0, menu_blur: None, - shader: Default::default(), } } diff --git a/crates/horizon-client/src/game/control.rs b/crates/horizon-client/src/game/control.rs index bc02012d5..205a66074 100644 --- a/crates/horizon-client/src/game/control.rs +++ b/crates/horizon-client/src/game/control.rs @@ -589,7 +589,6 @@ impl Game<'_> { "credits" => Page::Credits, "horizon" => Page::Horizon, "agent" => Page::Agent, - "shader-packs" => Page::ShaderPacks, other => return Err(format!("unknown options page {other}").into()), }; if self.screen == Screen::Playing && self.title.is_none() { diff --git a/crates/horizon-client/src/game/debug_keys.rs b/crates/horizon-client/src/game/debug_keys.rs index be9f76b3f..a94f3f01b 100644 --- a/crates/horizon-client/src/game/debug_keys.rs +++ b/crates/horizon-client/src/game/debug_keys.rs @@ -593,7 +593,6 @@ impl Game<'_> { DebugAction::ReloadResourcePacks => { self.debug_translated("debug.reload_resourcepacks.message"); self.reload_packs(); - self.load_shader_pack(); true } DebugAction::CopyLocation if player => { diff --git a/crates/horizon-client/src/game/friends.rs b/crates/horizon-client/src/game/friends.rs index f55e96392..b6d1ad660 100644 --- a/crates/horizon-client/src/game/friends.rs +++ b/crates/horizon-client/src/game/friends.rs @@ -200,7 +200,7 @@ impl Game<'_> { } Action::Copy => self.friends_copy(), Action::Stop => self.friends_stop(), - // The system browser, as the Shader Packs page opens its folder. + // The system browser, as the folder buttons open their folders. Action::Help => { if let Some(url) = &view.help { super::packs::open_folder(std::path::Path::new(url)); diff --git a/crates/horizon-client/src/game/options.rs b/crates/horizon-client/src/game/options.rs index d422f0218..d947f91d0 100644 --- a/crates/horizon-client/src/game/options.rs +++ b/crates/horizon-client/src/game/options.rs @@ -237,15 +237,7 @@ impl Game<'_> { let _ = std::fs::create_dir_all(&dir); super::packs::open_folder(&dir); } - Event::OpenShaderPackFolder => { - let dir = horizon_client::shader_pack::folder(); - let _ = std::fs::create_dir_all(&dir); - super::packs::open_folder(&dir); - } Event::Credits => self.open_win_screen(false, super::win::Finish::Options), - Event::ReloadShaderPack => { - self.load_shader_pack(); - } } } Ok(()) @@ -294,14 +286,6 @@ impl Game<'_> { font.reset_cache(); self.ui.chat.rescale(&self.gui.atlas); } - // A new renderer (resource reload, first window) takes the loaded pack. - let stale = self - .renderer - .as_ref() - .is_some_and(|r| r.shader_pack_id() != self.shader_packs.id()); - if stale && let Err(e) = self.apply_shader_pack() { - log::warn!("Horizon: shader pack: {e}"); - } } /// `Window.updateFullscreenState`: a fullscreen change made by the window /// system is adopted by the option (`Minecraft.fullscreenStateChanged`). diff --git a/crates/horizon-client/src/game/render_tick.rs b/crates/horizon-client/src/game/render_tick.rs index e947d2af5..331db8833 100644 --- a/crates/horizon-client/src/game/render_tick.rs +++ b/crates/horizon-client/src/game/render_tick.rs @@ -104,45 +104,6 @@ impl Game<'_> { }, ) } - /// What a shader pack's `Frame` uniform reads of the game this frame. - pub(super) fn shader_inputs( - &self, - camera: glam::DVec3, - partial_tick: f32, - ) -> horizon_client::gfx::ShaderInputs { - let (rain, thunder) = if self.dim == Dim::Overworld { - self.world.weather.levels(self.clock.fraction() as f32) - } else { - (0.0, 0.0) - }; - let at = [camera.x, camera.y, camera.z]; - let day = self.world.day_time; - let has_sky_light = self.dim == Dim::Overworld; - // CommonUniforms.getEyeBrightness: the camera entity's eye block. - let eye = self.eye_position().unwrap_or(at).map(|v| v.floor() as i32); - let light = self.world.light(eye[0], eye[1], eye[2]); - let eye_light = [light & 15, if has_sky_light { light >> 4 } else { 0 }]; - horizon_client::gfx::ShaderInputs { - rain, - thunder, - submerged: match fog_type(&self.world, self.res.facts(), at, self.forward()) { - FogType::Atmospheric => 0, - FogType::Water => 1, - FogType::Lava => 2, - FogType::PowderSnow => 3, - }, - dimension: match self.dim { - Dim::Overworld => 0, - Dim::Nether => -1, - Dim::End => 1, - }, - has_sky_light, - moon_phase: day.div_euclid(24_000).rem_euclid(8) as u8, - day_fraction: (day.rem_euclid(24_000) as f32 + partial_tick) / 24_000.0, - partial_tick, - eye_light, - } - } /// `SkyRenderer.shouldRenderDarkDisc`: eye below the horizon (y 63) and not under water. pub(super) fn dark_disc(&self, camera: glam::DVec3) -> bool { camera.y < 63.0 diff --git a/crates/horizon-client/src/game/shader_packs.rs b/crates/horizon-client/src/game/shader_packs.rs deleted file mode 100644 index 41ce50d3f..000000000 --- a/crates/horizon-client/src/game/shader_packs.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! The selected Horizon shader pack (`shaderPack` in `horizon-graphics.txt`, -//! docs/shader-packs.md): loaded from `shaderpacks/`, given to the renderer, -//! and loaded again when the selection changes, on F3 + T, or when its files -//! change (checked twice a second). A pack that fails leaves the last good -//! one (or the vanilla picture) on and says why in chat. -use super::Game; -use horizon_client::shader_pack::{self, ShaderPack, Stamp}; -use horizon_client::text::Text; -use horizon_shaderpack::properties::SkyDirectives; -use std::{ - path::PathBuf, - time::{Duration, Instant}, -}; - -#[derive(Default)] -pub(super) struct ShaderPacks { - /// The pack the renderer runs. - pack: Option, - /// The selection and its files at the last load, success or not. - selected: String, - stamp: Stamp, - checked: Option, -} - -impl ShaderPacks { - /// The loaded pack's folder name. - pub(super) fn id(&self) -> Option<&str> { - self.pack.as_ref().map(|p| p.id.as_str()) - } - /// What a loaded Java pack changes in the vanilla sky of `dimension` - /// (0 overworld, -1 the Nether, 1 the End): its `sunPathRotation`, and - /// whether it keeps the clouds, sun and moon (Iris's sky mixins). - pub(super) fn java_sky(&self, dimension: i8) -> Option<(f32, SkyDirectives)> { - let java = self.pack.as_ref()?.java.as_ref()?; - let folder = &java.folders[java.folder_for(dimension)?]; - Some((folder.directives.sun_path_rotation, java.sky)) - } -} - -pub(super) fn dir() -> PathBuf { - shader_pack::folder() -} - -impl Game<'_> { - /// Load the selected pack and give it to the renderer. - pub(super) fn load_shader_pack(&mut self) { - let id = self.settings.horizon.shader_pack.clone(); - let dir = dir(); - let state = &mut self.shader_packs; - state.selected = id.clone(); - state.checked = Some(Instant::now()); - let pack = if id.is_empty() { - state.stamp = Stamp::default(); - None - } else { - state.stamp = Stamp::of(&dir.join(&id)); - // Java packs are told which GPU they run on. - let (vendor, renderer) = self.renderer.as_ref().map_or(("Unknown", ""), |r| { - let name = r.info.name.as_str(); - ( - horizon_client::java_pack::vendor_name(r.info.vendor, name), - name, - ) - }); - let values = shader_pack::load_values(&dir, &id); - match ShaderPack::load_any(&dir.join(&id), &values, vendor, renderer) { - Ok(pack) => Some(pack), - Err(e) => return self.shader_pack_error(&id, &e), - } - }; - if let Some(r) = self.renderer.as_mut() - && let Err(e) = r.set_shader_pack(pack.as_ref(), &self.pack.render.sprite_layers) - { - return self.shader_pack_error(&id, &e.to_string()); - } - self.shader_packs.pack = pack; - self.ui.options.shader_pack_error = None; - } - /// Give the loaded pack to the renderer (also after it is rebuilt). - pub(super) fn apply_shader_pack(&mut self) -> horizon_client::Result<()> { - match self.renderer.as_mut() { - Some(r) => r.set_shader_pack( - self.shader_packs.pack.as_ref(), - &self.pack.render.sprite_layers, - ), - None => Ok(()), - } - } - /// Every frame: a new selection or edited pack files load it again. - pub(super) fn poll_shader_pack(&mut self) { - let state = &mut self.shader_packs; - if self.settings.horizon.shader_pack != state.selected { - return self.load_shader_pack(); - } - if state.selected.is_empty() - || state - .checked - .is_some_and(|t| t.elapsed() < Duration::from_millis(500)) - { - return; - } - state.checked = Some(Instant::now()); - if Stamp::of(&dir().join(&state.selected)) != state.stamp { - self.load_shader_pack(); - } - } - /// A file dropped on the window: a shader pack while the Shader Packs - /// page shows (`OptionsUi::drop_shader_pack`). - pub(super) fn drop_shader_pack(&mut self, path: &std::path::Path) { - match self.ui.options.drop_shader_pack(&mut self.settings, path) { - Some(Ok(())) => { - self.settings.save(&Self::options_path()); - self.load_shader_pack(); - } - Some(Err(e)) => { - log::warn!("Horizon: shader pack {}: {e}", path.display()); - self.system_message(Text::literal(format!("Shader pack: {e}"))); - } - None => {} - } - } - fn shader_pack_error(&mut self, id: &str, error: &str) { - log::warn!("Horizon: shader pack {id}: {error}"); - // The Shader Packs page says why on the pack's row: the first line, - // since translator errors go on with a drawn source excerpt. - let first = error.lines().map(str::trim).find(|l| !l.is_empty()); - self.ui.options.shader_pack_error = - Some((id.to_owned(), first.unwrap_or(error).to_owned())); - self.system_message(Text::literal(format!("Shader pack {id}: {error}"))); - } -} diff --git a/crates/horizon-client/src/game/startup_menu.rs b/crates/horizon-client/src/game/startup_menu.rs index ebf062a83..e6cb6b04f 100644 --- a/crates/horizon-client/src/game/startup_menu.rs +++ b/crates/horizon-client/src/game/startup_menu.rs @@ -367,11 +367,6 @@ impl Loading { let _ = std::fs::create_dir_all(&dir); packs::open_folder(&dir); } - Event::OpenShaderPackFolder => { - let dir = horizon_client::shader_pack::folder(); - let _ = std::fs::create_dir_all(&dir); - packs::open_folder(&dir); - } Event::Credits => { if let Some(gui) = &self.gui { let name = self.options.as_ref().map_or_else( @@ -387,9 +382,6 @@ impl Loading { self.menu.win = Some((screen, Instant::now())); } } - Event::ReloadShaderPack => { - self.save_menu_settings(); - } // The startup menu runs before the game opens a control socket. Event::ButtonClick | Event::CopyAgentDetails => {} } @@ -455,17 +447,6 @@ impl Loading { } self.apply_menu_settings(); } - WindowEvent::DroppedFile(path) => { - match self - .menu - .options - .drop_shader_pack(&mut self.settings, &path) - { - Some(Ok(())) => self.save_menu_settings(), - Some(Err(e)) => log::warn!("Horizon: shader pack {}: {e}", path.display()), - None => {} - } - } WindowEvent::RedrawRequested => self.draw(), WindowEvent::ModifiersChanged(modifiers) => self.menu.modifiers = modifiers.state(), WindowEvent::Focused(false) => { diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs index 59a3b5d03..c369ee1c7 100644 --- a/crates/horizon-client/src/gfx.rs +++ b/crates/horizon-client/src/gfx.rs @@ -65,12 +65,9 @@ mod arena; mod cull; mod end_sky; mod flat; -pub(crate) mod java_stages; -pub(crate) mod java_terrain; pub mod lightning; mod lod; mod occlusion; -mod pack_stages; mod page_runs; pub mod panorama; mod resident; @@ -90,7 +87,6 @@ fn world_pass_after_culling<'e>( encoder: &'e mut wgpu::CommandEncoder, view: &wgpu::TextureView, depth: &wgpu::TextureView, - pack: bool, ) -> wgpu::RenderPass<'e> { encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Horizon world, after culling"), @@ -107,11 +103,7 @@ fn world_pass_after_culling<'e>( view: depth, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, - store: if pack { - wgpu::StoreOp::Store - } else { - wgpu::StoreOp::Discard - }, + store: wgpu::StoreOp::Discard, }), stencil_ops: None, }), @@ -159,18 +151,6 @@ struct Globals { dynamic_lights: [[f32; 4]; crate::dynamic_light::MAX], } pub type SectionKey = (i32, i32, i32); -/// The sections a Java pack's shadow map draws ([`Renderer`]'s `shadow`). -#[derive(Default)] -struct ShadowList { - visible: Vec, - draws: Vec<[u32; 5]>, - pages: Vec<(u32, Option)>, - /// How many of `draws` are solid. - solid: u32, - indirect: Option, - capacity: u64, -} - struct SectionGpu { /// Arena page and first vertex in it: solid quads, then translucent ones. page: u32, @@ -291,28 +271,6 @@ pub struct Frame<'a> { /// `Screen.extractBlurredBackground`: blur the frame by this radius /// (`menuBackgroundBlurriness`) before the screen is drawn over it. pub menu_blur: Option, - /// What a shader pack's `Frame` uniform reads beyond the rest of the frame. - pub shader: ShaderInputs, -} -/// A shader pack's view of the game this frame (docs/shader-packs.md, `Frame`). -#[derive(Clone, Copy, Debug, Default)] -pub struct ShaderInputs { - /// `Level.getRainLevel` and `getThunderLevel` at the partial tick. - pub rain: f32, - pub thunder: f32, - /// The camera's fog type: 0 air, 1 water, 2 lava, 3 powder snow. - pub submerged: u8, - /// 0 overworld, -1 the Nether, 1 the End. - pub dimension: i8, - pub has_sky_light: bool, - /// `DimensionType.moonPhase`, 0..=7. - pub moon_phase: u8, - /// The time of day as a fraction (0 sunrise, 0.25 noon) and the partial tick. - pub day_fraction: f32, - pub partial_tick: f32, - /// The block and sky light (0..15) of the block holding the camera - /// entity's eyes; no sky light without a sky. - pub eye_light: [u8; 2], } /// One frame of the world border wall (`WorldBorderRenderer.render`). #[derive(Clone, Debug)] @@ -380,11 +338,6 @@ pub struct Renderer { /// (`crosshair_globals`). crosshair_group: wgpu::BindGroup, crosshair_globals: wgpu::Buffer, - shadow_pipeline: wgpu::RenderPipeline, - shadow_entity_pipeline: wgpu::RenderPipeline, - shadow_globals: wgpu::Buffer, - shadow_group: wgpu::BindGroup, - shadow_entity_group: wgpu::BindGroup, entity_pipeline: wgpu::RenderPipeline, weather: weather::Weather, crumbling_pipeline: wgpu::RenderPipeline, @@ -492,10 +445,6 @@ pub struct Renderer { /// built, and their pages. phase2_draws: Vec<[u32; 5]>, phase2_pages: Vec<(u32, Option)>, - /// A Java pack's shadow map (`gfx/java_terrain.rs`): the sections in - /// the light's view, their draws (solid then translucent, instances - /// after the camera's), and the indirect buffer they're uploaded to. - shadow: ShadowList, /// Each section's solid draws, to group them by arena page. solid_runs: page_runs::PageRuns, phase2_runs: page_runs::PageRuns, @@ -507,17 +456,6 @@ pub struct Renderer { occlusion: occlusion::Occlusion, /// Resolution scale and FXAA (Horizon settings). world_post: world_post::Post, - /// The selected shader pack's stages (Horizon setting; none by default). - pack: Option, - /// A Java pack's terrain programs (`gfx/java_terrain.rs`): their page - /// groups' layout, the glue's table, each arena page's group. - java_page_layout: wgpu::BindGroupLayout, - /// The device renders to and filters 16-bit normalized formats. - java_norm16: bool, - java_table: Option, - java_pages: Vec>, - /// The sprite layers and mip levels `animate` wrote this frame. - animated: Vec<(u32, u32)>, pub hud: HudRenderer, /// Vanilla's menu background blur, made on first use. menu_blur: Option, @@ -866,10 +804,7 @@ async fn open_device( | cull::Cull::features(&adapter) | (adapter.features() & (wgpu::Features::INDIRECT_FIRST_INSTANCE - | wgpu::Features::MAPPABLE_PRIMARY_BUFFERS - // Java shader packs' 16-bit colour targets. - | wgpu::Features::TEXTURE_FORMAT_16BIT_NORM - | wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES)); + | wgpu::Features::MAPPABLE_PRIMARY_BUFFERS)); let driver = format!("{} {}", info.driver, info.driver_info); startup_step(format_args!( "requesting the device (driver {}) with features [{features}] and the adapter's \ @@ -1342,38 +1277,6 @@ impl Renderer { usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); - let shadow_globals = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Shader pack shadow globals"), - size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let shadow_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Terrain shadow"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_terrain"), - compilation_options: Default::default(), - buffers: &terrain_buffers, - }, - primitive: wgpu::PrimitiveState { - topology: tri, - cull_mode: Some(wgpu::Face::Back), - ..Default::default() - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: wgpu::TextureFormat::Depth32Float, - depth_write_enabled: Some(true), - depth_compare: Some(wgpu::CompareFunction::GreaterEqual), - stencil: Default::default(), - bias: Default::default(), - }), - multisample: Default::default(), - fragment: None, - multiview_mask: None, - cache: None, - }); let prepared = prepared.unwrap_or_else(|| { PreparedTextures::new(sprite_pixels, sprite_size, layers, sprite_mips, forcefield) }); @@ -1399,10 +1302,8 @@ impl Renderer { sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8Unorm, - // COPY_SRC: animated sprites into a Java pack's atlas. usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::STORAGE_BINDING, view_formats: &[], }, @@ -1489,16 +1390,6 @@ impl Renderer { &lightmap_view, &lightmap_sampler, ); - let shadow_group = terrain_group( - &device, - "Shader pack shadow terrain resources", - &bindings, - &shadow_globals, - &sprite_views, - &sprite_sampler, - &lightmap_view, - &lightmap_sampler, - ); let index_quads = 1 << 16; let indices = Self::quad_indices(&device, index_quads); let instance_capacity = 4096; @@ -1660,32 +1551,6 @@ impl Renderer { }, ], }); - let shadow_entity_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Shader pack shadow entity resources"), - layout: &entity_bindings, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: shadow_globals.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&atlas_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&atlas_sampler), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::TextureView(&lightmap_view), - }, - wgpu::BindGroupEntry { - binding: 4, - resource: wgpu::BindingResource::Sampler(&lightmap_sampler), - }, - ], - }); let entity_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Entity layout"), bind_group_layouts: &[Some(&entity_bindings)], @@ -1796,32 +1661,6 @@ impl Renderer { wgpu::ColorWrites::ALL, ) }; - let shadow_entity_pipeline = - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Entity shadow"), - layout: Some(&entity_layout), - vertex: wgpu::VertexState { - module: &entity_shader, - entry_point: Some("vs_entity"), - compilation_options: Default::default(), - buffers: &entity_vertex, - }, - primitive: wgpu::PrimitiveState { - cull_mode: None, - ..Default::default() - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: wgpu::TextureFormat::Depth32Float, - depth_write_enabled: Some(true), - depth_compare: Some(wgpu::CompareFunction::GreaterEqual), - stencil: Default::default(), - bias: Default::default(), - }), - multisample: Default::default(), - fragment: None, - multiview_mask: None, - cache: None, - }); // RenderPipelines.WATER_MASK: the boat's water patch writes depth only. let water_mask_pipeline = entity_pipeline_with( "Water mask", @@ -2266,8 +2105,6 @@ impl Renderer { std::mem::size_of::() as u64, 1 << 20, ); - let java_page_layout = java_terrain::page_layout(&device); - let java_norm16 = java_terrain::norm16_targets(&adapter, &device); // Sections as one index list per frame where multi-draw is emulated. let flat = flat::wanted(&adapter, &device).then(|| { let buffers = [Some(wgpu::VertexBufferLayout { @@ -2378,11 +2215,6 @@ impl Renderer { debug_line_capacity: 4096, crosshair_group, crosshair_globals, - shadow_pipeline, - shadow_entity_pipeline, - shadow_globals, - shadow_group, - shadow_entity_group, entity_pipeline, weather, crumbling_pipeline, @@ -2462,19 +2294,12 @@ impl Renderer { draw_pages: Vec::new(), phase2_draws: Vec::new(), phase2_pages: Vec::new(), - shadow: ShadowList::default(), solid_runs: page_runs::PageRuns::default(), phase2_runs: page_runs::PageRuns::default(), block_entities: HashMap::new(), block_entity_revision: next_block_entity_revision(), occlusion: occlusion::Occlusion::new(crate::chunk::MIN_Y, crate::chunk::HEIGHT / 16), world_post, - pack: None, - java_page_layout, - java_norm16, - java_table: None, - java_pages: Vec::new(), - animated: Vec::new(), hud, menu_blur: None, info, @@ -2784,33 +2609,7 @@ impl Renderer { first: u32, count: u32, ) { - self.draw_sections_with(pass, indirect, base, first, count, |_, _| {}); - } - /// [`Self::draw_sections`]; `page` binds what else a page's draws need. - fn draw_sections_with( - &self, - pass: &mut wgpu::RenderPass<'_>, - indirect: &wgpu::Buffer, - base: u32, - first: u32, - count: u32, - page_group: impl Fn(&mut wgpu::RenderPass<'_>, u32), - ) { - let list = (&self.draws[..], &self.draw_pages[..]); - self.draw_list(pass, indirect, list, base, first, count, page_group); - } - /// [`Self::draw_sections_with`] over another list of draws and pages. - #[allow(clippy::too_many_arguments)] - fn draw_list( - &self, - pass: &mut wgpu::RenderPass<'_>, - indirect: &wgpu::Buffer, - (draws, pages): (&[[u32; 5]], &[(u32, Option)]), - base: u32, - first: u32, - count: u32, - page_group: impl Fn(&mut wgpu::RenderPass<'_>, u32), - ) { + let (draws, pages) = (&self.draws[..], &self.draw_pages[..]); let (mut i, end) = (first as usize, (first + count) as usize); while i < end { let (page, index_page) = pages[i]; @@ -2819,7 +2618,6 @@ impl Renderer { .take_while(|&&p| p == (page, index_page)) .count(); pass.set_vertex_buffer(0, self.arena.buffer(page).slice(..)); - page_group(pass, page); let indices = index_page.map_or(&self.indices, |p| self.sorted_indices.buffer(p)); pass.set_index_buffer(indices.slice(..), wgpu::IndexFormat::Uint32); if self.multi_draw { @@ -2833,127 +2631,6 @@ impl Renderer { i += run; } } - /// One `layer` of Horizon's terrain with a Java pack's geometry - /// pipelines (`java_terrain::DrawTerrain`): the frame's flat draws or its - /// section draws, each page with its group 3. `draws` are the frame's - /// phase 1 solid draws, all its solid draws and its translucent ones; - /// `culling` tells whether phase 2's come from occlusion culling. - fn draw_java_terrain( - &self, - pass: &mut wgpu::RenderPass<'_>, - layer: java_terrain::Layer, - pipelines: &java_terrain::TerrainPipelines, - flat_frame: bool, - draws: [u32; 3], - culling: bool, - ) { - use java_terrain::Layer; - let page = |pass: &mut wgpu::RenderPass<'_>, page: u32| { - if let Some(Some((_, group))) = self.java_pages.get(page as usize) { - pass.set_bind_group(3, group, &[]); - } - }; - if matches!(layer, Layer::ShadowSolid | Layer::ShadowTranslucent) { - let shadow = &self.shadow; - let all = shadow.draws.len() as u32; - let (first, count) = if layer == Layer::ShadowSolid { - (0, shadow.solid) - } else { - (shadow.solid, all - shadow.solid) - }; - // Without multi-draw the draws are read from the list. - let indirect = shadow.indirect.as_ref().unwrap_or(&self.indirect); - if count == 0 { - return; - } - pass.set_bind_group(2, pipelines.group, &[]); - pass.set_pipeline(pipelines.instanced); - pass.set_vertex_buffer(1, self.instances.slice(..)); - let list = (&shadow.draws[..], &shadow.pages[..]); - self.draw_list(pass, indirect, list, 0, first, count, page); - return; - } - pass.set_bind_group(2, pipelines.group, &[]); - match (self.flat.as_ref().filter(|_| flat_frame), pipelines.flat) { - // Without occlusion culling, phase 1 is every solid call. - (Some(flat), Some(pipeline)) => { - let vertices = |p| self.arena.buffer(p); - pass.set_pipeline(pipeline); - match layer { - Layer::Solid => { - flat.draw(pass, &flat.calls[..flat.phase1_calls], vertices, page) - } - Layer::Revealed if culling => flat.draw_phase2(pass, vertices, page), - Layer::Revealed => {} - Layer::Translucent => { - flat.draw(pass, &flat.calls[flat.solid_calls..], vertices, page) - } - // Drawn from the shadow list, above. - Layer::ShadowSolid | Layer::ShadowTranslucent => {} - } - } - (Some(_), None) => {} - (None, _) => { - let [phase1, solid, translucent_draws] = draws; - let cull = self.cull.as_ref().filter(|_| culling); - let (indirect, base, first, count) = match (layer, cull) { - (Layer::Solid, Some(_)) => (&self.indirect, 0, 0, phase1), - (Layer::Solid, None) => (&self.indirect, 0, 0, solid), - (Layer::Revealed, Some(c)) => (&c.phase2, phase1, phase1, solid - phase1), - (Layer::Revealed, None) => return, - (Layer::Translucent, _) => (&self.indirect, 0, solid, translucent_draws), - (Layer::ShadowSolid | Layer::ShadowTranslucent, _) => return, - }; - if count == 0 { - return; - } - pass.set_pipeline(pipelines.instanced); - pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32); - pass.set_vertex_buffer(1, self.instances.slice(..)); - self.draw_sections_with(pass, indirect, base, first, count, page); - } - } - } - /// Each arena page's group for a Java pack's terrain programs: its - /// vertices and the glue's table. - fn prepare_java_pages(&mut self) { - let Some(table) = &self.java_table else { - return; - }; - let count = self.arena.page_count() as usize; - self.java_pages.resize_with(count, || None); - let limit = self.device.limits().max_storage_buffer_binding_size; - for page in 0..count { - if !self.arena.has_page(page as u32) { - self.java_pages[page] = None; - continue; - } - let buffer = self.arena.buffer(page as u32); - if matches!(&self.java_pages[page], Some((b, _)) if b == buffer) { - continue; - } - let size = buffer.size().min(limit / 4 * 4); - let group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Java shader pack terrain page"), - layout: &self.java_page_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer, - offset: 0, - size: std::num::NonZeroU64::new(size), - }), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: table.as_entire_binding(), - }, - ], - }); - self.java_pages[page] = Some((buffer.clone(), group)); - } - } fn release_section(&mut self, key: SectionKey) { if let Some(s) = self.sections.remove(key) { self.release_mesh(s); @@ -3036,61 +2713,6 @@ impl Renderer { self.release_section(key); } } - /// Run `pack`'s stages over the world from now on (`None`: no pack, the - /// vanilla frame). On error the previous pack stays. `sprite_layers` - /// names the block sprites, for a Java pack's `block.properties`. - pub fn set_shader_pack( - &mut self, - pack: Option<&crate::shader_pack::ShaderPack>, - sprite_layers: &std::collections::BTreeMap, - ) -> Result<()> { - let format = self.config.format.remove_srgb_suffix(); - let layouts = java_stages::GeometryLayouts { - flat: self.flat.as_ref().map(|f| f.draw_layout()), - page: &self.java_page_layout, - }; - let table = pack.and_then(|p| p.java.as_ref()).map(|java| { - let table = java_terrain::table(sprite_layers, &java.block_ids); - wgpu::util::DeviceExt::create_buffer_init( - &self.device, - &wgpu::util::BufferInitDescriptor { - label: Some("Java shader pack terrain table"), - contents: bytemuck::cast_slice(&table), - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - }, - ) - }); - self.pack = match pack { - Some(pack) => Some(match &pack.java { - Some(java) => Pack::Java(java_stages::JavaStages::new( - &self.device, - &self.queue, - format, - &pack.id, - java, - &layouts, - self.java_norm16, - )?), - None => Pack::Horizon(pack_stages::PackStages::new( - &self.device, - &self.queue, - format, - pack, - )?), - }), - None => None, - }; - self.java_table = table; - self.java_pages.clear(); - Ok(()) - } - /// The folder name of the pack `set_shader_pack` gave. - pub fn shader_pack_id(&self) -> Option<&str> { - self.pack.as_ref().map(|p| match p { - Pack::Horizon(p) => p.id.as_str(), - Pack::Java(p) => p.id.as_str(), - }) - } /// Horizon's resolution scale (world pixels per window pixel) and FXAA. pub fn set_world_post(&mut self, scale: f32, antialias: bool) { self.world_post.scale = scale.clamp(0.25, 2.0); @@ -3120,16 +2742,6 @@ impl Renderer { &self.lightmap_view, &self.lightmap_sampler, ); - self.shadow_group = terrain_group( - &self.device, - "Shader pack shadow terrain resources", - &self.terrain_layout, - &self.shadow_globals, - &views, - &self.sprite_sampler, - &self.lightmap_view, - &self.lightmap_sampler, - ); } pub fn view_projection(&self, frame: &Frame) -> Mat4 { self.projection( @@ -3222,15 +2834,6 @@ impl Renderer { } fn animate(&mut self, encoder: &mut wgpu::CommandEncoder, tick: u64) { self.animations.animate(encoder, &mut self.uploads, tick); - // A Java pack's atlas copies what changed, every level. - let levels = self.mip_levels; - self.animated.clear(); - self.animated.extend( - self.animations - .changed - .iter() - .flat_map(|&layer| (0..levels).map(move |level| (layer, level))), - ); } pub fn render(&mut self, frame: &Frame) -> Result { let rendered = self.draw_frame(frame); @@ -3402,19 +3005,6 @@ impl Renderer { }; self.uploads .buffer(&mut encoder, &self.globals, 0, bytemuck::bytes_of(&globals)); - // Vanilla multiplies the bob into the projection (GameRenderer.renderLevel); - // Iris moves it into the model-view for packs (MixinModelViewBobbing), whose - // depth reconstruction expects a plain perspective `gbufferProjection`. - // `projection * view` is the same either way. - let pack_uniform = shader_pack_uniform( - frame, - frame.bob * view, - world_projection(frame.fov_degrees, aspect, frame.depth_far), - sun, - fog_color, - globals.fog, - globals.fog_ends, - ); // GameRenderer.render3dHud: the hand's projection at hudFov, with the // bob in its pose rather than the projection (same product). let hand_projection = Mat4::from_cols_array(&crate::camera::hand_projection( @@ -3485,57 +3075,7 @@ impl Renderer { visible_lods.sort_unstable_by_key(|v| (v.distance, v.key)); } self.drawn_sections = visible.len() + visible_lods.len(); - // A Java pack's shadow map: the sections in the light's orthographic - // box, whatever the camera sees (Iris turns cave culling off too). - let mut shadow_visible = std::mem::take(&mut self.shadow.visible); - shadow_visible.clear(); - let shadow_view = match &self.pack { - Some(Pack::Java(j)) if !frame.title && self.java_table.is_some() => j.shadow_view( - frame.shader.dimension, - &java_stages::JavaFrame { - eye_light: frame.shader.eye_light, - sun_angle: frame.sky.sun_angle, - moon_angle: frame.sky.moon_angle, - }, - camera.as_vec3(), - ), - _ => None, - }; - if let Some((light_view, [half, near, far])) = shadow_view { - // Inside when `plane · (p, 1) >= 0`: |x|, |y| <= half and the - // eye depth between the near and far planes (GL looks down -z). - let (x, y, z) = (light_view.row(0), light_view.row(1), light_view.row(2)); - let planes = [ - Vec4::W * half - x, - Vec4::W * half + x, - Vec4::W * half - y, - Vec4::W * half + y, - -z - Vec4::W * near, - z + Vec4::W * far, - ]; - // Toward the light: the light's view looks down its -z. - let light = z.truncate().normalize_or_zero(); - let casts = |k: SectionKey| { - let min = DVec3::new( - f64::from(k.0 * 16), - f64::from(crate::chunk::MIN_Y + k.1 * 16), - f64::from(k.2 * 16), - ) - camera; - let min = min.as_vec3(); - view.casts_into(min, min + Vec3::splat(16.0), light) - }; - crate::spans::time("shadow visibility", || { - self.sections.visible( - camera, - (frame.detail_distance / 16.0) as i32, - &Frustum { planes }, - casts, - |_, _| false, - &mut shadow_visible, - ) - }); - } - let instances = self.drawn_sections + shadow_visible.len(); + let instances = self.drawn_sections; if instances as u64 > self.instance_capacity { self.instance_capacity = (instances as u64).next_power_of_two(); self.instances = self.device.create_buffer(&wgpu::BufferDescriptor { @@ -3552,63 +3092,8 @@ impl Renderer { visible .iter() .map(|v| section_instance(v.origin)) - .chain(visible_lods.iter().map(|v| lod::instance(v.origin))) - .chain(shadow_visible.iter().map(|v| section_instance(v.origin))), + .chain(visible_lods.iter().map(|v| lod::instance(v.origin))), ); - // The shadow map's draws: every solid quad (no face is culled), then - // the translucent ones in the order the camera's sort left them. - let shadow = &mut self.shadow; - shadow.draws.clear(); - shadow.pages.clear(); - for pass in [false, true] { - for (j, v) in shadow_visible.iter().enumerate() { - let s = self.sections.get(v.slot); - let instance = (self.drawn_sections + j) as u32; - match (pass, s.sorted_indices) { - (false, _) if s.solid > 0 => { - shadow - .draws - .push([s.solid * 6, 1, 0, s.start as u32, instance]); - shadow.pages.push((s.page, None)); - } - (true, Some((index_page, index_start))) if s.translucent > 0 => { - let first = s.start + u64::from(s.solid) * 4; - shadow.draws.push([ - s.translucent * 6, - 1, - index_start as u32, - first as u32, - instance, - ]); - shadow.pages.push((s.page, Some(index_page))); - } - _ => {} - } - } - if !pass { - shadow.solid = shadow.draws.len() as u32; - } - } - if self.multi_draw && !shadow.draws.is_empty() { - if shadow.draws.len() as u64 > shadow.capacity || shadow.indirect.is_none() { - shadow.capacity = (shadow.draws.len() as u64).next_power_of_two(); - shadow.indirect = Some(self.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Java shader pack shadow draws"), - size: shadow.capacity * 20, - usage: wgpu::BufferUsages::INDIRECT | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - })); - } - if let Some(indirect) = &shadow.indirect { - self.uploads.buffer( - &mut encoder, - indirect, - 0, - bytemuck::cast_slice(&shadow.draws), - ); - } - } - shadow.visible = shadow_visible; if !self.origins.is_empty() { self.uploads.buffer( &mut encoder, @@ -3625,12 +3110,6 @@ impl Renderer { let eye = (frame.bob * crate::camera::view_matrix(frame.yaw, frame.pitch)) .inverse() .transform_point3(Vec3::ZERO); - // A Java pack that draws the terrain (`gfx/java_terrain.rs`): the - // world pass leaves it out, and occlusion culling tests against the - // pack's depth (`Cull::test_gl`). - let java_terrain = !frame.title - && self.java_table.is_some() - && matches!(&self.pack, Some(Pack::Java(j)) if j.draws_terrain(frame.shader.dimension)); // Occlusion culling leaves the sections its last result did not see // to phase 2, after the others, with instance counts of 0. Whether // the frame has draws enough goes by the last one's. @@ -4092,14 +3571,6 @@ impl Renderer { bytemuck::cast_slice(&self.draws), ); } - if java_terrain { - self.prepare_java_pages(); - if let Some(table) = &self.java_table { - let nether = i32::from(frame.shader.dimension == -1); - self.queue - .write_buffer(table, 4, bytemuck::bytes_of(&nether)); - } - } let view_target = target_texture.create_view(&wgpu::TextureViewDescriptor { format: Some(self.config.format.remove_srgb_suffix()), ..Default::default() @@ -4108,44 +3579,9 @@ impl Renderer { timer.begin(&mut encoder); } // The world's targets: the screen, or the scaled target of `post`. - let pack = self.pack.is_some() && !frame.title; - // What the pack reads beyond the world's colour and depth: each costs - // a pass (the shadow map, the G-buffer) or a copy (the opaque depth). - let (shadow_cfg, gbuffer, opaque_depth) = match self.pack.as_ref().filter(|_| pack) { - Some(Pack::Horizon(s)) => ( - s.needs_shadow.then_some((s.shadow_res, s.shadow_dist)), - s.needs_gbuffer, - s.needs_opaque_depth, - ), - Some(Pack::Java(s)) => (None, false, s.reads_opaque_depth), - None => (None, false, false), - }; - if let Some((shadow_res, shadow_dist)) = shadow_cfg { - self.world_post - .prepare_shadow(&self.device, shadow_res, shadow_dist, sun, -sun); - let shadow_globals = Globals { - view_proj: (self.world_post.shadow_proj * self.world_post.shadow_view) - .to_cols_array_2d(), - ..globals - }; - self.uploads.buffer( - &mut encoder, - &self.shadow_globals, - 0, - bytemuck::bytes_of(&shadow_globals), - ); - } else { - self.world_post.shadow_active = false; - } - let scaled = self.world_post.prepare( - &self.device, - [self.config.width, self.config.height], - pack, - world_post::Extras { - opaque_depth, - gbuffer, - }, - ); + let scaled = self + .world_post + .prepare(&self.device, [self.config.width, self.config.height]); let (world_view, world_depth) = match &scaled { Some((color, depth)) => (color, depth), None => (&view_target, &self.depth), @@ -4154,36 +3590,6 @@ impl Renderer { cull.bind_depth(&self.device, world_depth); } let cull = self.cull.as_ref().filter(|_| culling && solid_draws > 0); - if shadow_cfg.is_some() { - let shadow_view = self.world_post.shadow_depth().clone(); - let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Horizon shadow"), - color_attachments: &[], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &shadow_view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(0.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - ..Default::default() - }); - if solid_draws > 0 && !flat_frame { - shadow_pass.set_bind_group(0, &self.shadow_group, &[]); - shadow_pass.set_pipeline(&self.shadow_pipeline); - shadow_pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32); - shadow_pass.set_vertex_buffer(1, self.instances.slice(..)); - self.draw_sections(&mut shadow_pass, &self.indirect, 0, 0, solid_draws); - } - if entity_quads > 0 { - shadow_pass.set_pipeline(&self.shadow_entity_pipeline); - shadow_pass.set_bind_group(0, &self.shadow_entity_group, &[]); - shadow_pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32); - shadow_pass.set_vertex_buffer(0, self.entity_buffer.slice(..)); - shadow_pass.draw_indexed(0..entity_quads * 6, 0, 0..1); - } - } { let attachments = [Some(wgpu::RenderPassColorAttachment { view: world_view, @@ -4212,9 +3618,8 @@ impl Renderer { view: world_depth, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(0.0), - // A shader pack reads the world's depth, and - // phase 2 of occlusion culling draws over it. - store: if pack || cull.is_some() { + // Phase 2 of occlusion culling draws over it. + store: if cull.is_some() { wgpu::StoreOp::Store } else { wgpu::StoreOp::Discard @@ -4261,7 +3666,6 @@ impl Renderer { } } match self.flat.as_ref().filter(|_| flat_frame) { - _ if java_terrain => {} Some(flat) => { pass.set_pipeline(&flat.solid); let vertices = |p| self.arena.buffer(p); @@ -4269,32 +3673,17 @@ impl Renderer { // As below: phase 1, the test, then phase 2's draws // that the test saw, copied after it. Some(cull) => { - flat.draw( - &mut pass, - &flat.calls[..flat.phase1_calls], - vertices, - |_, _| {}, - ); + flat.draw(&mut pass, &flat.calls[..flat.phase1_calls], vertices); cull.test(&mut pass); drop(pass); cull.test_pass(&mut encoder); flat.flatten_phase2(&mut encoder); - pass = world_pass_after_culling( - &mut encoder, - world_view, - world_depth, - pack, - ); + pass = world_pass_after_culling(&mut encoder, world_view, world_depth); pass.set_bind_group(0, &self.group, &[]); pass.set_pipeline(&flat.solid); - flat.draw_phase2(&mut pass, vertices, |_, _| {}); + flat.draw_phase2(&mut pass, vertices); } - None => flat.draw( - &mut pass, - &flat.calls[..flat.solid_calls], - vertices, - |_, _| {}, - ), + None => flat.draw(&mut pass, &flat.calls[..flat.solid_calls], vertices), } pass.set_bind_group(0, &self.group, &[]); pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32); @@ -4313,12 +3702,7 @@ impl Renderer { cull.test(&mut pass); drop(pass); cull.test_pass(&mut encoder); - pass = world_pass_after_culling( - &mut encoder, - world_view, - world_depth, - pack, - ); + pass = world_pass_after_culling(&mut encoder, world_view, world_depth); pass.set_bind_group(0, &self.group, &[]); pass.set_pipeline(&self.solid); pass.set_index_buffer( @@ -4406,43 +3790,12 @@ impl Renderer { pass.set_vertex_buffer(0, self.debug_line_buffer.slice(..)); pass.draw(0..4, debug_depth..debug_depth + debug_top); } - if opaque_depth { - drop(pass); - self.world_post.capture_opaque_depth(&mut encoder); - let attachments = [Some(wgpu::RenderPassColorAttachment { - view: world_view, - resolve_target: None, - depth_slice: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })]; - pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Horizon world, translucent"), - color_attachments: &attachments, - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: world_depth, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - ..Default::default() - }); - pass.set_bind_group(0, &self.group, &[]); - } match self.flat.as_ref().filter(|_| flat_frame) { - _ if java_terrain => {} Some(flat) => { pass.set_pipeline(&flat.translucent); - flat.draw( - &mut pass, - &flat.calls[flat.solid_calls..], - |p| self.arena.buffer(p), - |_, _| {}, - ); + flat.draw(&mut pass, &flat.calls[flat.solid_calls..], |p| { + self.arena.buffer(p) + }); pass.set_bind_group(0, &self.group, &[]); pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32); } @@ -4508,80 +3861,7 @@ impl Renderer { ); } } - if gbuffer && scaled.is_some() { - self.world_post.build_gbuffer( - &self.queue, - &mut encoder, - &pack_uniform, - frame.shader.eye_light, - frame.shader.rain, - ); - } - if pack - && let (Some((color, depth)), Some(size)) = (&scaled, self.world_post.size()) - && let Some(mut stages) = self.pack.take() - { - // The pack's terrain programs draw with the frame's draws. - let draws = [phase1_draws, solid_draws, translucent_draws]; - let draw = |render: &mut wgpu::RenderPass<'_>, - layer: java_terrain::Layer, - pipelines: &java_terrain::TerrainPipelines| { - self.draw_java_terrain(render, layer, pipelines, flat_frame, draws, culling); - }; - let cull = self.cull.as_ref().filter(|_| culling && solid_draws > 0); - let occlusion = |encoder: &mut wgpu::CommandEncoder, depth: &wgpu::TextureView| { - if let Some(cull) = cull { - cull.test_gl(&self.device, encoder, depth); - // The flat path's phase 2: the calls the test saw. - if let Some(flat) = self.flat.as_ref().filter(|_| flat_frame) { - flat.flatten_phase2(encoder); - } - } - }; - let world = java_terrain::JavaWorld { - sprites: &self.sprite_textures, - split: sprite_split(), - levels: self.mipmaps, - animated: &self.animated, - lightmap: &self.lightmap_view, - draw: &draw, - occlusion: cull - .is_some() - .then_some(&occlusion as &java_terrain::OcclusionTest), - }; - let output = match &mut stages { - Pack::Horizon(stages) => stages.run( - &self.device, - &self.queue, - &mut encoder, - color, - depth, - size, - &self.world_post, - pack_uniform, - frame.shader.eye_light, - ), - Pack::Java(stages) => stages.run( - &self.device, - &self.queue, - &mut encoder, - color, - depth, - size, - &self.world_post, - pack_uniform, - java_stages::JavaFrame { - eye_light: frame.shader.eye_light, - sun_angle: frame.sky.sun_angle, - moon_angle: frame.sky.moon_angle, - }, - java_terrain.then_some(&world), - ), - }; - self.world_post - .finish_with(&mut encoder, &view_target, output); - self.pack = Some(stages); - } else if scaled.is_some() { + if scaled.is_some() { self.world_post.finish(&mut encoder, &view_target); } if hand_quads > 0 { @@ -4891,22 +4171,6 @@ impl Frustum { p.truncate().dot(v) + p.w < 0.0 }) } - /// Whether the camera-relative box `min..max`, lit from the direction - /// `light` (toward the light), can cast a shadow inside: its shadow is - /// the box swept away from the light, which stays outside a plane the - /// box is outside of unless the sweep crosses it (Iris's advanced - /// shadow culling, conservatively). - pub fn casts_into(&self, min: Vec3, max: Vec3, light: Vec3) -> bool { - !self.planes.iter().any(|p| { - let v = Vec3::new( - if p.x >= 0.0 { max.x } else { min.x }, - if p.y >= 0.0 { max.y } else { min.y }, - if p.z >= 0.0 { max.z } else { min.z }, - ); - p.truncate().dot(v) + p.w < 0.0 && p.truncate().dot(light) >= 0.0 - }) - } - /// Whether the camera-relative box `min..max` is entirely inside. Then /// `intersects` accepts every box within it: each plane's distance only /// grows from this box's nearest corner to the inner box's farthest, and @@ -4925,66 +4189,6 @@ impl Frustum { /// Vanilla's `Projection`: reversed depth (near maps to 1, far to 0, tests /// are GREATER_THAN_OR_EQUAL), which keeps far faces a hair apart, like /// fire_side's 0.01 pixel in front of its block, from z-fighting. -/// The prelude's `Frame` (`shader_pack_prelude.wgsl`); `resolution` and the -/// time are filled per stage. -fn shader_pack_uniform( - frame: &Frame, - view: Mat4, - projection: Mat4, - sun: Vec3, - fog_color: [f32; 4], - fog: [f32; 4], - fog_ends: [f32; 4], -) -> pack_stages::FrameUniform { - let s = frame.shader; - let sky = frame.sky.sky_color; - pack_stages::FrameUniform { - view: view.to_cols_array_2d(), - projection: projection.to_cols_array_2d(), - inverse_view: view.inverse().to_cols_array_2d(), - inverse_projection: projection.inverse().to_cols_array_2d(), - camera_position: [ - frame.camera.x as f32, - frame.camera.y as f32, - frame.camera.z as f32, - 0.0, - ], - time: [0.0, 0.0, s.day_fraction, s.partial_tick], - sun: [sun.x, sun.y, sun.z, frame.sky.sun_angle.to_radians()], - moon: [-sun.x, -sun.y, -sun.z, f32::from(s.moon_phase)], - sky_color: [sky[0], sky[1], sky[2], 1.0], - fog_color, - fog: [fog[0], fog[1], fog_ends[0], fog_ends[1]], - weather: [s.rain, s.thunder, 0.0, 0.0], - camera_state: [ - f32::from(s.submerged), - frame.night_vision, - frame.darkness.1, - 0.0, - ], - view_params: [ - 0.05, - frame.depth_far, - frame.distance, - frame.fov_degrees.to_radians(), - ], - dimension: [ - f32::from(s.dimension), - f32::from(u8::from(s.has_sky_light)), - 0.0, - 0.0, - ], - ..Default::default() - } -} - -/// The selected shader pack on the GPU: a Horizon pack's WGSL stages, or a -/// Java pack's translated programs. -enum Pack { - Horizon(pack_stages::PackStages), - Java(java_stages::JavaStages), -} - /// The section the camera is in. fn camera_section(camera: DVec3) -> SectionKey { ( @@ -5013,31 +4217,6 @@ fn frustum(m: Mat4) -> [Vec4; 6] { #[cfg(test)] mod tests { - #[test] - fn shadow_casters_are_the_boxes_whose_shadow_can_reach_the_view() { - use glam::Vec3; - // Looking north (-z) from the origin, level. - let m = super::world_projection(70.0, 16.0 / 9.0, 256.0) - * crate::camera::view_matrix(180.0, 0.0); - let view = super::Frustum { - planes: super::frustum(m), - }; - let cube = - |x: f32, y: f32, z: f32| (Vec3::new(x, y, z), Vec3::new(x + 16.0, y + 16.0, z + 16.0)); - let noon = Vec3::Y; - // In view: casts. - let (min, max) = cube(-8.0, -8.0, -64.0); - assert!(view.intersects(min, max) && view.casts_into(min, max, noon)); - // Far above the view, the sun overhead: its shadow falls into it. - let (min, max) = cube(-8.0, 200.0, -64.0); - assert!(!view.intersects(min, max) && view.casts_into(min, max, noon)); - // Behind the camera: its shadow falls straight down, behind it too. - let (min, max) = cube(-8.0, -8.0, 32.0); - assert!(!view.casts_into(min, max, noon)); - // The same box with a low sun behind the camera (south): it does. - assert!(view.casts_into(min, max, Vec3::new(0.0, 0.2, 1.0).normalize())); - } - #[test] fn surface_is_8_bit_and_not_srgb() { use super::surface_format; @@ -5155,11 +4334,7 @@ mod tests { ); assert!(files.len() >= 10, "{files:?}"); for path in files { - let mut text = std::fs::read_to_string(&path).unwrap(); - // Api 2's prelude follows api 1's (`shader_pack::ShaderPack::load`). - if path.ends_with("shader_pack_prelude_api2.wgsl") { - text = format!("{}\n{text}", crate::shader_pack::PRELUDE); - } + let text = std::fs::read_to_string(&path).unwrap(); // entity.wgsl comes after the overlay's constants (entity_shader). let module = naga::front::wgsl::parse_str(&text) .or_else(|_| { diff --git a/crates/horizon-client/src/gfx/animate.rs b/crates/horizon-client/src/gfx/animate.rs index b16c4af17..f4528902b 100644 --- a/crates/horizon-client/src/gfx/animate.rs +++ b/crates/horizon-client/src/gfx/animate.rs @@ -51,9 +51,6 @@ pub(super) struct SpriteAnimator { pipeline: wgpu::ComputePipeline, /// Edge of the arrays' layers. size: u32, - /// The sprite layers (across the arrays) the last `animate` rewrote, at - /// every mip level: a Java pack's atlas copies them. - pub changed: Vec, } impl SpriteAnimator { @@ -208,7 +205,6 @@ impl SpriteAnimator { jobs, pipeline, size, - changed: Vec::new(), } } @@ -220,7 +216,6 @@ impl SpriteAnimator { uploads: &mut Uploads, tick: u64, ) { - self.changed.clear(); if self.collect(tick) == 0 { return; } @@ -256,7 +251,6 @@ impl SpriteAnimator { continue; } sprite.shown = Some(blend); - self.changed.push(sprite.animation.layer); let job = [ sprite.layer, sprite.frames[blend.frame], diff --git a/crates/horizon-client/src/gfx/arena.rs b/crates/horizon-client/src/gfx/arena.rs index 14a264cd2..8da835aa2 100644 --- a/crates/horizon-client/src/gfx/arena.rs +++ b/crates/horizon-client/src/gfx/arena.rs @@ -65,11 +65,8 @@ pub struct Arena { } impl Arena { - /// Vertices; also storage, which a Java shader pack's terrain programs - /// read a quad's corners from (`gfx/java_terrain.rs`). pub fn new(device: &wgpu::Device, stride: u64, capacity: u64) -> Self { - let usage = wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::STORAGE; - Self::with_usage(device, stride, capacity, usage) + Self::with_usage(device, stride, capacity, wgpu::BufferUsages::VERTEX) } /// Sorted translucent indices; `storage` when compute passes read them. pub fn indices(device: &wgpu::Device, capacity: u64, storage: bool) -> Self { @@ -148,10 +145,6 @@ impl Arena { pub fn page_vertices(&self) -> u64 { self.page_limit } - /// One past the last page index. - pub fn page_count(&self) -> u32 { - self.pages.len() as u32 - } pub fn has_page(&self, page: u32) -> bool { self.pages.get(page as usize).is_some_and(Option::is_some) } diff --git a/crates/horizon-client/src/gfx/cull.rs b/crates/horizon-client/src/gfx/cull.rs index 65700cb49..856bd86dd 100644 --- a/crates/horizon-client/src/gfx/cull.rs +++ b/crates/horizon-client/src/gfx/cull.rs @@ -108,14 +108,6 @@ fn fs_box_early(in: Box) -> @location(0) vec4 { see(in.test); return vec4(0.0); } - -// fs_box_early against a Java shader pack's depth (`vs_box_gl`), in a pass -// without colour targets. -@fragment -@early_depth_test(force) -fn fs_box_early_depth(in: Box) { - see(in.test); -} "; /// The test's depth texture binding (`InShader`), for the view it was made with. @@ -126,8 +118,6 @@ struct DepthGroup { pub struct Cull { pipeline: wgpu::RenderPipeline, - /// [`Self::test_gl`]'s: a depth-only pass in OpenGL's layout. - gl_pipeline: wgpu::RenderPipeline, layout: wgpu::BindGroupLayout, group: Option, depth_test: DepthTest, @@ -271,76 +261,55 @@ impl Cull { bind_group_layouts: &[Some(&layout), depth_layout.as_ref()], ..Default::default() }); - // `gl`: a Java shader pack's depth (`test_gl`), in OpenGL's layout - // (y flipped, and with it the faces' winding; 0 near), without a - // colour target. In the shader (`InShader`): no colour target, and - // the depth attached read only (a pass needs an attachment), its - // test left to the shader. - let pipeline = |label, gl: bool| { - let color_target = [Some(wgpu::ColorTargetState { - format: color, - blend: None, - write_mask: wgpu::ColorWrites::empty(), - })]; - let (fragment_entry, targets, depth_compare) = match (depth_test, gl) { - (DepthTest::Early, false) => ( - "fs_box_early", - &color_target[..], - wgpu::CompareFunction::GreaterEqual, - ), - (DepthTest::Early, true) => ( - "fs_box_early_depth", - &[][..], - wgpu::CompareFunction::LessEqual, - ), - (DepthTest::InShader, false) => ("fs_box", &[][..], wgpu::CompareFunction::Always), - (DepthTest::InShader, true) => { - ("fs_box_gl", &[][..], wgpu::CompareFunction::Always) - } - }; - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some(label), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &module, - entry_point: Some(if gl { "vs_box_gl" } else { "vs_box" }), - compilation_options: Default::default(), - buffers: &[], - }, - primitive: wgpu::PrimitiveState { - cull_mode: Some(wgpu::Face::Back), - front_face: if gl { - wgpu::FrontFace::Cw - } else { - wgpu::FrontFace::Ccw - }, - ..Default::default() - }, - // The pass's depth test (reversed in the world pass, OpenGL's - // in a Java pack's), without writes. - depth_stencil: Some(wgpu::DepthStencilState { - format: wgpu::TextureFormat::Depth32Float, - depth_write_enabled: Some(false), - depth_compare: Some(depth_compare), - stencil: Default::default(), - bias: Default::default(), - }), - multisample: Default::default(), - fragment: Some(wgpu::FragmentState { - module: &module, - entry_point: Some(fragment_entry), - compilation_options: Default::default(), - targets, - }), - multiview_mask: None, - cache: None, - }) + // In the shader (`InShader`): no colour target, and the depth + // attached read only (a pass needs an attachment), its test left to + // the shader. + let color_target = [Some(wgpu::ColorTargetState { + format: color, + blend: None, + write_mask: wgpu::ColorWrites::empty(), + })]; + let (fragment_entry, targets, depth_compare) = match depth_test { + DepthTest::Early => ( + "fs_box_early", + &color_target[..], + wgpu::CompareFunction::GreaterEqual, + ), + DepthTest::InShader => ("fs_box", &[][..], wgpu::CompareFunction::Always), }; - let gl_pipeline = pipeline("Section culling, OpenGL layout", true); - let pipeline = pipeline("Section culling", false); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Section culling"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &module, + entry_point: Some("vs_box"), + compilation_options: Default::default(), + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + cull_mode: Some(wgpu::Face::Back), + ..Default::default() + }, + // The world pass's reversed depth test, without writes. + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: Some(false), + depth_compare: Some(depth_compare), + stencil: Default::default(), + bias: Default::default(), + }), + multisample: Default::default(), + fragment: Some(wgpu::FragmentState { + module: &module, + entry_point: Some(fragment_entry), + compilation_options: Default::default(), + targets, + }), + multiview_mask: None, + cache: None, + }); Self { pipeline, - gl_pipeline, layout, group: None, depth_test, @@ -586,51 +555,6 @@ impl Cull { pass.draw(0..36, 0..self.test_data.len() as u32); } - /// [`Self::test`] for a Java shader pack that draws the terrain - /// (`gfx/java_stages.rs`): in a pass of its own against `depth`, the - /// pack's depth after phase 1 in OpenGL's layout (y up, 0 near), attached - /// read only, and also read by the shader where the test runs there - /// (`InShader`). - pub fn test_gl( - &self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - depth: &wgpu::TextureView, - ) { - let Some(Group { group, .. }) = &self.group else { - return; - }; - if self.test_data.is_empty() { - return; - } - let depth_group = self.depth_layout.as_ref().map(|layout| { - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Section culling depth, Java shader pack"), - layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(depth), - }], - }) - }); - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Section culling, Java shader pack"), - color_attachments: &[], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: depth, - depth_ops: None, - stencil_ops: None, - }), - ..Default::default() - }); - pass.set_pipeline(&self.gl_pipeline); - pass.set_bind_group(0, group, &[]); - if let Some(depth) = &depth_group { - pass.set_bind_group(1, depth, &[]); - } - pass.draw(0..36, 0..self.test_data.len() as u32); - } - /// After the world passes: copy the results for reading back, when a /// readback buffer is free. pub fn end(&mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder) { diff --git a/crates/horizon-client/src/gfx/cull.wgsl b/crates/horizon-client/src/gfx/cull.wgsl index 07a04ebc4..b06d4ccc2 100644 --- a/crates/horizon-client/src/gfx/cull.wgsl +++ b/crates/horizon-client/src/gfx/cull.wgsl @@ -52,10 +52,6 @@ fn corner(origin: vec3, c: u32) -> vec3 { // section fail the depth test against the final depth, which is only nearer. @vertex fn vs_box(@builtin(vertex_index) v: u32, @builtin(instance_index) t: u32) -> Box { - return box_vertex(v, t); -} - -fn box_vertex(v: u32, t: u32) -> Box { let origin = origins[tests[t].x].xyz; var out: Box; out.test = t; @@ -90,15 +86,6 @@ fn box_vertex(v: u32, t: u32) -> Box { return out; } -// The same box in OpenGL's layout, for a Java shader pack's terrain depth -// (gfx/java_stages.rs): y flipped, depth from 0 near to 1 far. -@vertex -fn vs_box_gl(@builtin(vertex_index) v: u32, @builtin(instance_index) t: u32) -> Box { - var out = box_vertex(v, t); - out.position = vec4(out.position.x, -out.position.y, out.position.w - out.position.z, out.position.w); - return out; -} - // The box is seen at this fragment: store the stamp, and give its phase 2 // draws their instance. fn see(t: u32) { @@ -124,12 +111,3 @@ fn fs_box(in: Box) { see(in.test); } } - -// fs_box against a Java shader pack's depth in OpenGL's layout (`vs_box_gl`): -// less or equal passing. -@fragment -fn fs_box_gl(in: Box) { - if in.position.z <= textureLoad(depth, vec2(in.position.xy), 0) { - see(in.test); - } -} diff --git a/crates/horizon-client/src/gfx/flat.rs b/crates/horizon-client/src/gfx/flat.rs index 4c491315c..f6823c5d8 100644 --- a/crates/horizon-client/src/gfx/flat.rs +++ b/crates/horizon-client/src/gfx/flat.rs @@ -640,36 +640,26 @@ impl Flat { pass.set_bind_group(0, group, &[]); pass.dispatch_workgroups((*count).min(GROUP_LIMIT), count.div_ceil(GROUP_LIMIT), 1); } - /// The bind group layout of the draws' group 1 (block slots, slot - /// origins). - pub fn draw_layout(&self) -> &wgpu::BindGroupLayout { - &self.draw_layout - } /// Draws `calls` with a flat pipeline set and `vertices` giving each - /// page's vertex buffer; `page` binds what else a page's draws need (a - /// Java pack's terrain reads the page's vertices). + /// page's vertex buffer. pub fn draw<'a>( &self, pass: &mut wgpu::RenderPass<'_>, calls: &[Call], vertices: impl Fn(u32) -> &'a wgpu::Buffer, - page: impl Fn(&mut wgpu::RenderPass<'_>, u32), ) { pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32); for call in calls { self.bind_page(pass, call.page, &vertices); - page(pass, call.page); pass.draw_indexed(call.first..call.first + call.count, 0, 0..1); } } - /// Draws phase 2's calls, as `flatten_phase2` left them; `page` as in - /// [`Self::draw`]. + /// Draws phase 2's calls, as `flatten_phase2` left them. pub fn draw_phase2<'a>( &self, pass: &mut wgpu::RenderPass<'_>, vertices: impl Fn(u32) -> &'a wgpu::Buffer, - page: impl Fn(&mut wgpu::RenderPass<'_>, u32), ) { pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32); for (k, call) in self.calls[self.phase1_calls..self.solid_calls] @@ -677,7 +667,6 @@ impl Flat { .enumerate() { self.bind_page(pass, call.page, &vertices); - page(pass, call.page); pass.draw_indexed_indirect(&self.phase2_calls, k as u64 * 20); } } diff --git a/crates/horizon-client/src/gfx/java_stages.rs b/crates/horizon-client/src/gfx/java_stages.rs deleted file mode 100644 index a5419ed7e..000000000 --- a/crates/horizon-client/src/gfx/java_stages.rs +++ /dev/null @@ -1,3047 +0,0 @@ -//! A Java (OptiFine/Iris) shader pack on the GPU (docs/shader-packs.md): -//! Iris's colour targets (`colortex0..`, each with a main and an alternate -//! side) and the pack's `begin`, `prepare`, `deferred`, `composite` and -//! `final` programs as full-screen passes over them, as Iris's -//! `CompositeRenderer` runs them. Horizon's world enters the target the -//! pack's terrain program draws to (`colortex0` for most packs) and -//! `depthtex0` after `prepare`, where Iris's gbuffers programs would have -//! drawn (`depthtex1`/`depthtex2`: its depth before translucent geometry), -//! and the last picture goes to `world_post`. The targets keep GL's layout -//! (row 0 at the bottom, which the translated vertex programs produce), so -//! the world is flipped going in and the picture coming out. -//! -//! When the pack has terrain programs (`gfx/java_terrain.rs`), Horizon's -//! world comes without its terrain: the pack's `gbuffers_terrain` draws it -//! into the pack's targets right after, against the world's depth, and its -//! `gbuffers_water` draws the translucent terrain after `deferred`, as in -//! Iris. `depthtex1` and `depthtex2` then hold the depth before it. -use super::java_terrain::{Atlas, JavaWorld, Layer, TerrainPipelines}; -use super::pack_stages::{FrameUniform, noise, rgba_view}; -use crate::java_pack::{JavaFolder, JavaPasses, NOISE_KEY, blend_state, pass_blends, texture_key}; -use crate::shader_pack::Rgba; -use glam::{Mat3, Mat4, Vec3, Vec4}; -use horizon_shaderpack::pipeline::GeometryProgram; -use horizon_shaderpack::pipeline::TargetSettings; -use horizon_shaderpack::pipeline::{Directives, Program, target_index}; -use horizon_shaderpack::uniforms::{CustomUniforms, SmoothFloat, UniformState}; -use std::collections::{BTreeMap, BTreeSet, HashMap}; - -/// What Iris's uniforms read from the game that the frame uniform doesn't -/// carry. -#[derive(Clone, Copy, Debug, Default)] -pub struct JavaFrame { - /// The block and sky light at the camera's eye block, 0..15 - /// (`CommonUniforms.getEyeBrightness`). - pub eye_light: [u8; 2], - /// `visual/sun_angle` and `visual/moon_angle`, degrees (0: overhead). - pub sun_angle: f32, - pub moon_angle: f32, -} - -/// The vertex buffers a pass may read (wgpu's default limit). -const MAX_ATTRIBUTES: usize = 8; - -pub(super) const BLIT: &str = r#" -@group(0) @binding(0) var color: texture_2d; -@group(0) @binding(1) var depth: texture_depth_2d; -@group(0) @binding(2) var opaque_depth: texture_depth_2d; - -@vertex -fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f { - let uv = vec2f(f32((i << 1u) & 2u), f32(i & 2u)); - return vec4f(uv * 2.0 - 1.0, 0.0, 1.0); -} - -struct World { - @location(0) color: vec4f, - @location(1) depth: vec4f, - @location(2) opaque_depth: vec4f, -} - -// The world's colour, depth and pre-translucent depth, upside down into GL's -// layout; Horizon's reversed depth (near 1) as GL's (near 0). -fn world_texel(p: vec4f) -> World { - let size = textureDimensions(depth); - let xy = vec2u(u32(p.x), size.y - 1u - u32(p.y)); - return World( - textureLoad(color, xy, 0), - vec4f(1.0 - textureLoad(depth, xy, 0)), - vec4f(1.0 - textureLoad(opaque_depth, xy, 0)), - ); -} - -@fragment -fn world_in(@builtin(position) p: vec4f) -> World { - return world_texel(p); -} - -struct WorldDepth { - @location(0) color: vec4f, - @location(1) depth: vec4f, - @location(2) opaque_depth: vec4f, - @builtin(frag_depth) test: f32, -} - -// The same, and the depth into the depth buffer the pack's geometry -// programs test against. -@fragment -fn world_in_depth(@builtin(position) p: vec4f) -> WorldDepth { - let world = world_texel(p); - return WorldDepth(world.color, world.depth, world.opaque_depth, world.depth.x); -} - -struct Depths { - @location(0) all: vec4f, - @location(1) solid: vec4f, -} - -// The depth buffer after the pack's geometry into `depthtex0` (and -// `depthtex1`, after solid terrain), both in GL's layout already. -@fragment -fn depth_out_both(@builtin(position) p: vec4f) -> Depths { - let d = vec4f(textureLoad(depth, vec2u(p.xy), 0)); - return Depths(d, d); -} - -@fragment -fn depth_out(@builtin(position) p: vec4f) -> @location(0) vec4f { - return vec4f(textureLoad(depth, vec2u(p.xy), 0)); -} - -// A target, upside down back into wgpu's layout. -@fragment -fn present(@builtin(position) p: vec4f) -> @location(0) vec4f { - let size = textureDimensions(color); - return textureLoad(color, vec2u(u32(p.x), size.y - 1u - u32(p.y)), 0); -} -"#; - -/// A wgpu format for each of Iris's (`InternalTextureFormat`), renderable -/// everywhere: 8-bit unsigned normalized formats stay, signed normalized, -/// 16-bit and packed float ones become half floats, integer ones 32-bit. -/// `norm16`: the device has 16-bit normalized formats, which keep packed -/// G-buffers (Chocapic13' packs two 8-bit values per `RGBA16` channel) -/// exact where a half float rounds them. -fn target_format(iris: &str, norm16: bool) -> wgpu::TextureFormat { - use wgpu::TextureFormat as F; - let channels = if iris.starts_with("RGB") || iris.contains("_B") { - 4 - } else if iris.starts_with("RG") { - 2 - } else { - 1 - }; - let pick = |one, two, four| match channels { - 1 => one, - 2 => two, - _ => four, - }; - match iris { - "R8" | "RG8" | "RGB8" | "RGBA8" | "RGBA" | "RGBA2" | "RGBA4" | "R3_G3_B2" | "RGB5_A1" - | "RGB565" => pick(F::R8Unorm, F::Rg8Unorm, F::Rgba8Unorm), - "R16" | "RG16" | "RGB16" | "RGBA16" if norm16 => { - pick(F::R16Unorm, F::Rg16Unorm, F::Rgba16Unorm) - } - "R16_SNORM" | "RG16_SNORM" | "RGB16_SNORM" | "RGBA16_SNORM" if norm16 => { - pick(F::R16Snorm, F::Rg16Snorm, F::Rgba16Snorm) - } - "RGB10_A2" => F::Rgb10a2Unorm, - "RGB10_A2UI" => F::Rgb10a2Uint, - f if f.ends_with("32F") => pick(F::R32Float, F::Rg32Float, F::Rgba32Float), - f if f.ends_with("UI") => pick(F::R32Uint, F::Rg32Uint, F::Rgba32Uint), - f if f.ends_with('I') => pick(F::R32Sint, F::Rg32Sint, F::Rgba32Sint), - _ => pick(F::R16Float, F::Rg16Float, F::Rgba16Float), - } -} - -/// How a pass reads a texture of `format`. -fn sample_type(format: wgpu::TextureFormat) -> wgpu::TextureSampleType { - match format.sample_type(None, None) { - Some(wgpu::TextureSampleType::Float { .. }) => wgpu::TextureSampleType::Float { - filterable: !matches!( - format, - wgpu::TextureFormat::R32Float - | wgpu::TextureFormat::Rg32Float - | wgpu::TextureFormat::Rgba32Float - ), - }, - Some(t) => t, - None => wgpu::TextureSampleType::Float { filterable: false }, - } -} - -/// The textures besides the targets a pass may read. -struct Views<'a> { - depth: &'a wgpu::TextureView, - depth_solid: &'a wgpu::TextureView, - atlas: Option<&'a wgpu::TextureView>, - lightmap: Option<&'a wgpu::TextureView>, - shadow: Option<&'a ShadowViews>, -} - -/// The shadow map's textures, as the passes read them. -struct ShadowViews { - /// `shadowtex0` and `shadowtex1` for comparisons (`Depth32Float`). - depth: wgpu::TextureView, - depth_solid: wgpu::TextureView, - /// The same as plain floats, for `sampler2D` reads (`R32Float`). - float: wgpu::TextureView, - float_solid: wgpu::TextureView, - /// `shadowcolor`, for the targets the shadow programs write. - colors: Vec>, -} - -/// A colour target's two sides. -struct Target { - format: wgpu::TextureFormat, - main: wgpu::Texture, - alt: wgpu::Texture, - main_view: wgpu::TextureView, - alt_view: wgpu::TextureView, - clear: Option, -} - -/// What a sampler of the pack reads. -#[derive(Clone, Copy, Debug, PartialEq)] -enum Source { - Target(u32), - /// `depthtex0` / `gdepthtex`: the world's depth (including translucent water). - Depth, - /// `depthtex1` / `depthtex2`: the depth before translucent terrain: after - /// the pack's own solid terrain when it draws it, else Horizon's world's - /// before translucent water. - DepthSolid, - Noise, - /// `shadowtex0` (with translucent terrain) or `shadowtex1` (without): - /// the shadow map when the pack draws one, else a far depth (nothing in - /// shadow). - ShadowDepth(u32), - /// `shadowcolor`: the shadow map's colour target, else white. - ShadowColor(u32), - /// Distant Horizons' and Voxy's depth: far, as without those mods. - Far, - /// Anything else (custom textures): white or black. - Blank(bool), - /// In geometry programs: the block atlas (`gtexture`), the lightmap, - /// and a flat normal map for `normals` (sprites have no LabPBR maps). - Atlas, - Lightmap, - FlatNormal, -} - -/// What sampler `name` reads, in a geometry program or a full-screen one. -fn source(name: &str, geometry: bool) -> Source { - match name { - "texture" | "gtexture" | "tex" if geometry => Source::Atlas, - "lightmap" if geometry => Source::Lightmap, - "normals" if geometry => Source::FlatNormal, - "specular" if geometry => Source::Blank(false), - "depthtex0" | "gdepthtex" => Source::Depth, - "depthtex1" | "depthtex2" => Source::DepthSolid, - "noisetex" => Source::Noise, - "shadowtex0" | "shadow" | "watershadow" => Source::ShadowDepth(0), - "shadowtex1" => Source::ShadowDepth(1), - n if n.starts_with("dhDepthTex") || n.starts_with("vxDepthTex") => Source::Far, - "shadowcolor" => Source::ShadowColor(0), - n if let Some(i) = n.strip_prefix("shadowcolor").and_then(|i| i.parse().ok()) => { - Source::ShadowColor(i) - } - n => target_index(n).map_or(Source::Blank(false), Source::Target), - } -} - -/// A sampler of a pass: its texture and sampler bindings. -struct SamplerBinding { - name: String, - source: Source, - dimension: wgpu::TextureViewDimension, - /// `isampler` (true) or `usampler` (false). - signed: Option, - texture: u32, - sampler: u32, - /// The shared sampler it uses (`iris_linear`), if any. - shared: Option, - compare: bool, -} - -/// A scalar slot of the uniform block: byte offset, and whether it holds -/// an integer (`int`, `uint` or `bool`). -type Slot = (u32, bool); - -struct PassGpu { - program: String, - pipeline: wgpu::RenderPipeline, - layout: wgpu::BindGroupLayout, - samplers: Vec, - /// Uniform members by name, their scalar slots in order. - uniforms: Vec<(String, Vec)>, - uniform: Option<(u32, wgpu::Buffer)>, - /// Each vertex buffer slot's data: 0 position, 1 white, 2 up, 3 zero. - attributes: Vec, - draw_buffers: Vec, - reads_alt: BTreeSet, - scale: f32, - /// Geometry programs: the flat draws' pipeline (where the renderer has - /// them), and the targets written on their alternate side. - flat_pipeline: Option, - geometry: Option>, - /// A shadow program: the light's matrices, `shadowcolor` targets. - shadow: bool, -} - -/// What a pass draws into. -enum Output<'a> { - /// A full-screen pass: its targets, or for `final` the screen's format. - Screen(Option), - /// A geometry program over Horizon's terrain. - Geometry(GeometrySetup<'a>), -} - -struct GeometrySetup<'a> { - /// The vertex program for flat draws. - flat_vertex: &'a horizon_shaderpack::translate::Translated, - /// Each draw buffer's blending. - blends: &'a [Option], - layouts: &'a GeometryLayouts<'a>, - /// A shadow program: no face culling (Iris's `ShadowRenderer`). - shadow: bool, -} - -/// The renderer's bind group layouts a geometry program's pipelines use: -/// the flat draws' group 1 (`gfx/flat.rs`, when it has them) and the page -/// group 3 ([`super::java_terrain::page_layout`]). -pub struct GeometryLayouts<'a> { - pub flat: Option<&'a wgpu::BindGroupLayout>, - pub page: &'a wgpu::BindGroupLayout, -} - -/// A folder's terrain and water programs. -struct FolderGeometry { - terrain: PassGpu, - water: Option, - translucent_at: usize, - /// Its shadow programs, when it has some and they compile. - shadow: Option, -} - -/// A folder's shadow map: its programs over the solid and translucent -/// terrain, its `shadowcolor` targets, and where it runs. -struct FolderShadow { - solid: PassGpu, - water: Option, - /// Per `shadowcolor` target: its format and clear colour. - targets: Vec<(wgpu::TextureFormat, Option)>, - /// How many passes run before it (`begin*`). - at: usize, -} - -/// What every pass of the pack shares. -struct Common { - linear: wgpu::Sampler, - /// The block atlas's, as vanilla's: nearest, mipmapped. - atlas: wgpu::Sampler, - nearest: wgpu::Sampler, - repeat: wgpu::Sampler, - compare: wgpu::Sampler, - noise: wgpu::TextureView, - custom_textures: BTreeMap, - /// Full-screen triangle data per attribute kind (see `PassGpu::attributes`). - vertices: [wgpu::Buffer; 4], - world_layout: wgpu::BindGroupLayout, - present_layout: wgpu::BindGroupLayout, - /// The geometry's depth buffer, read into `depthtex0` and `depthtex1`. - depth_layout: wgpu::BindGroupLayout, - blit: wgpu::ShaderModule, -} - -/// The textures of one folder at one size. -struct Bound { - folder: usize, - size: [u32; 2], - /// `world_post`'s targets it binds (`Post::generation`). - post_generation: u64, - targets: BTreeMap, - depth: wgpu::TextureView, - /// `depthtex1`: `depth` before translucent terrain, or `depth` itself. - depth_solid: wgpu::TextureView, - /// Per pass, then `final`. - groups: Vec, - geometry: Option, - shadow: Option, - world_in: (wgpu::RenderPipeline, wgpu::BindGroup), - /// The world's target, and whether the world goes to its alternate side. - world_target: u32, - world_alt: bool, - final_target: Option, - present: (wgpu::RenderPipeline, wgpu::BindGroup), - output_view: wgpu::TextureView, - output: wgpu::BindGroup, - /// Blank textures, kept alive by the bind groups too. - _blank: Vec, -} - -/// The shadow map's textures and its programs' groups. -struct BoundShadow { - views: ShadowViews, - /// The depth buffer the shadow programs draw with (`shadowtex0`), and - /// its copy before translucent terrain (`shadowtex1`). - depth: wgpu::Texture, - depth_solid: wgpu::Texture, - /// The `shadowcolor` targets cleared each frame, with their colour, - /// and those cleared only when made (kept between frames). - clears: Vec<(wgpu::TextureView, wgpu::Color)>, - initial: Vec<(wgpu::TextureView, wgpu::Color)>, - solid: wgpu::BindGroup, - water: Option, - /// `depth` for `depth_out` into the float copies. - out_group: wgpu::BindGroup, - /// A pass reads the depth as plain floats: the copies are made. - float_reads: bool, -} - -/// What a folder's geometry programs draw with at one size. -struct BoundGeometry { - /// The depth buffer the geometry tests against: the world's, then the - /// terrain's. - test: wgpu::TextureView, - terrain: wgpu::BindGroup, - water: Option, - /// `world_in` that also fills `test`. - world_in: wgpu::RenderPipeline, - /// `test` into `depthtex0` and `depthtex1`, and into `depthtex0` alone. - out_both: wgpu::RenderPipeline, - out: wgpu::RenderPipeline, - out_group: wgpu::BindGroup, -} - -struct Folder { - targets: Vec, - world_at: usize, - world_target: u32, - flipped_after_prepare: BTreeSet, - flipped_at_end: BTreeSet, - passes: Vec, - final_pass: Option, - directives: Directives, - geometry: Option, -} - -pub struct JavaStages { - pub id: String, - /// A pass samples `depthtex1`/`depthtex2`: the world's depth is kept - /// before translucent geometry draws. - pub reads_opaque_depth: bool, - format: wgpu::TextureFormat, - common: Common, - folders: Vec, - folder_for: [Option; 3], - bound: Option, - started: std::time::Instant, - last: Option, - frames: u32, - previous: Option<(Mat4, Mat4, Vec3)>, - uniforms: CustomUniforms, - uniform_state: UniformState, - /// `eyeBrightnessSmooth`, block then sky. - eye_smooth: [SmoothFloat; 2], - /// The block atlas of the geometry programs. - atlas: Option, - /// The device has 16-bit normalized formats ([`target_format`]). - norm16: bool, - /// `wetness`: `rainStrength` smoothed with `wetnessHalflife` going up - /// and `drynessHalflife` going down. - wetness_smooth: SmoothFloat, -} - -impl JavaStages { - /// The pipelines of a loaded Java pack, or wgpu's error. `norm16`: the - /// device has 16-bit normalized targets ([`target_format`]). - #[allow(clippy::too_many_arguments)] - pub fn new( - device: &wgpu::Device, - queue: &wgpu::Queue, - format: wgpu::TextureFormat, - id: &str, - pack: &JavaPasses, - layouts: &GeometryLayouts, - norm16: bool, - ) -> Result { - let common = Common::new(device, queue, &pack.custom_textures); - let folder_for = [-1, 0, 1].map(|d| pack.folder_for(d)); - let scope = device.push_error_scope(wgpu::ErrorFilter::Validation); - let folders = pack - .folders - .iter() - .map(|plan: &JavaFolder| { - let formats: Vec = plan - .targets - .iter() - .map(|t| target_format(&t.format, norm16)) - .collect(); - let passes = plan - .passes - .iter() - .map(|p| PassGpu::new(device, p, &formats, Output::Screen(None))) - .collect::, _>>()?; - let final_pass = plan - .final_pass - .as_ref() - .map(|p| PassGpu::new(device, p, &formats, Output::Screen(Some(format)))) - .transpose()?; - Ok::<_, String>(Folder { - targets: plan.targets.clone(), - world_at: plan.world_at, - world_target: plan.world_target, - flipped_after_prepare: plan.flipped_after_prepare.clone(), - flipped_at_end: plan.flipped_at_end.clone(), - passes, - final_pass, - directives: plan.directives, - geometry: folder_geometry(device, plan, &formats, layouts), - }) - }) - .collect::, _>>()?; - if let Some(error) = pollster::block_on(scope.pop()) { - return Err(error.to_string()); - } - let reads_opaque_depth = folders - .iter() - .flat_map(|f| f.passes.iter().chain(&f.final_pass)) - .flat_map(|p| &p.samplers) - .any(|s| s.source == Source::DepthSolid); - Ok(Self { - id: id.to_owned(), - reads_opaque_depth, - format, - common, - folders, - folder_for, - bound: None, - started: std::time::Instant::now(), - last: None, - frames: 0, - previous: None, - uniforms: pack.uniforms.clone(), - uniform_state: UniformState::default(), - eye_smooth: [SmoothFloat::default(); 2], - atlas: None, - norm16, - wetness_smooth: SmoothFloat::default(), - }) - } - - /// Where the pack's shadow map looks this frame in `dimension`, when - /// it draws one: its view of camera-relative positions (`camera` is the - /// camera's position), and its half width, near and far planes, for - /// the renderer to pick the sections it draws. - pub fn shadow_view( - &self, - dimension: i8, - world: &JavaFrame, - camera: Vec3, - ) -> Option<(Mat4, [f32; 3])> { - let index = self.folder_for[(dimension.clamp(-1, 1) + 1) as usize].unwrap_or(0); - let folder = self.folders.get(index)?; - folder.geometry.as_ref()?.shadow.as_ref()?; - let d = folder.directives; - let (view, _) = shadow_matrices(&d, shadow_angle(world), camera); - Some((view, [d.shadow.distance, d.shadow.near, d.shadow.far])) - } - - /// Whether the pack draws the terrain in `dimension` (0 overworld, -1 - /// the Nether, 1 the End): the renderer then leaves it out of the world - /// and gives `run` a [`JavaWorld`]. - pub fn draws_terrain(&self, dimension: i8) -> bool { - let d = i32::from(dimension).clamp(-1, 1); - let folder = self.folder_for[(d + 1) as usize].unwrap_or(0); - self.folders - .get(folder) - .is_some_and(|f| f.geometry.is_some()) - } - - /// Run the passes of the world's dimension over its `color` and `depth` - /// (`size`) and return `post`'s bindings of the picture. `world` is given - /// when the world was drawn without its terrain ([`Self::draws_terrain`]): - /// the pack's geometry programs draw it. - #[allow(clippy::too_many_arguments)] - pub fn run( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - color: &wgpu::TextureView, - depth: &wgpu::TextureView, - size: [u32; 2], - post: &super::world_post::Post, - frame: FrameUniform, - world: JavaFrame, - terrain: Option<&JavaWorld>, - ) -> &wgpu::BindGroup { - let dimension = (frame.dimension[0] as i32).clamp(-1, 1); - let folder = self.folder_for[(dimension + 1) as usize].unwrap_or(0); - let terrain = terrain.filter(|_| self.folders[folder].geometry.is_some()); - if let Some(w) = terrain - && self.atlas.as_ref().is_none_or(|a| !a.fits(w)) - { - self.atlas = Atlas::new(device, queue, w); - self.bound = None; - } - let fresh = self.bound.as_ref().is_none_or(|b| { - b.folder != folder - || b.size != size - || b.post_generation != post.generation() - || b.geometry.is_some() != terrain.is_some() - }); - if fresh { - self.bound = Some(self.bind(device, queue, folder, color, depth, size, post, terrain)); - } - let now = std::time::Instant::now(); - let frame_time = self.last.map_or(0.0, |t| (now - t).as_secs_f32()); - self.last = Some(now); - let directives = self.folders[folder].directives; - let mut inputs = Inputs::new( - &frame, - size, - self.started.elapsed().as_secs_f32(), - frame_time, - self.frames, - self.previous, - ); - inputs.set_world(&world, directives.sun_path_rotation); - (inputs.shadow_view, inputs.shadow_projection) = - shadow_matrices(&directives, inputs.shadow_angle, inputs.camera); - if let Some(atlas) = &self.atlas { - inputs.atlas_size = atlas.size().map(|v| v as f32); - } - let half_life = directives.eye_brightness_half_life; - for (i, smooth) in self.eye_smooth.iter_mut().enumerate() { - inputs.eye_smooth[i] = smooth.update(inputs.eye[i], half_life, half_life, frame_time); - } - inputs.wetness = self.wetness_smooth.update( - frame.weather[0], - directives.wetness_half_life, - directives.dryness_half_life, - frame_time, - ); - self.uniforms - .update(&mut self.uniform_state, frame_time, &|name| { - inputs.builtin(name) - }); - inputs.custom = self.uniform_state.values().clone(); - self.previous = Some((inputs.view, inputs.projection, inputs.camera)); - self.frames = self.frames.wrapping_add(1); - let bound = self.bound.as_ref().expect("bound above"); - let folder = &self.folders[bound.folder]; - - // Iris clears both sides of the targets that ask for it. - for target in bound.targets.values() { - if let Some(clear) = target.clear { - for view in [&target.main_view, &target.alt_view] { - clear_view(encoder, view, clear); - } - } - } - if let (Some(w), Some(atlas)) = (terrain, &mut self.atlas) { - atlas.update(device, encoder, w); - } - // The world after `prepare`, the terrain with it; translucent - // terrain after `deferred`. - let world_at = folder.world_at.min(folder.passes.len()); - let geometry = folder - .geometry - .as_ref() - .zip(bound.geometry.as_ref()) - .zip(terrain); - let translucent_at = geometry.map_or(usize::MAX, |((g, _), _)| { - g.translucent_at.min(folder.passes.len()) - }); - let shadow = geometry - .and_then(|((g, b), w)| Some((g.shadow.as_ref()?, bound.shadow.as_ref()?, b, w))); - for i in 0..=folder.passes.len() { - if let Some((sh, b, g, w)) = shadow - && i == sh.at.min(folder.passes.len()) - { - draw_shadow(queue, encoder, sh, b, g, &inputs, w, fresh); - } - if i == world_at { - world_in(encoder, bound); - if let Some(((g, b), w)) = geometry { - g.terrain.draw_geometry( - queue, encoder, bound, &b.terrain, &b.test, &inputs, w, false, - ); - depth_out(encoder, bound, true); - } - } - if i == translucent_at - && let Some(((g, b), w)) = geometry - && let (Some(water), Some(group)) = (&g.water, &b.water) - { - water.draw_geometry(queue, encoder, bound, group, &b.test, &inputs, w, true); - depth_out(encoder, bound, false); - } - if let (Some(pass), Some(group)) = (folder.passes.get(i), bound.groups.get(i)) { - pass.draw(queue, encoder, bound, group, &self.common, &inputs); - } - } - if let (Some(pass), Some(view)) = (&folder.final_pass, &bound.final_target) { - let group = bound.groups.last().expect("final's group"); - pass.draw_to( - queue, - encoder, - &[Some(view)], - size, - group, - &self.common, - &inputs, - ); - } - { - let attachments = [Some(color_attachment(&bound.output_view, None))]; - let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Java shader pack picture"), - color_attachments: &attachments, - ..Default::default() - }); - render.set_pipeline(&bound.present.0); - render.set_bind_group(0, &bound.present.1, &[]); - render.draw(0..3, 0..1); - } - // The next frame starts unflipped: the last writes go back to main. - for index in &folder.flipped_at_end { - if let Some(t) = bound.targets.get(index) { - encoder.copy_texture_to_texture( - t.alt.as_image_copy(), - t.main.as_image_copy(), - t.main.size(), - ); - } - } - &bound.output - } - - /// The shadow map's textures at the folder's `shadowMapResolution`, - /// and its programs' groups. - #[allow(clippy::too_many_arguments)] - fn bind_shadow( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - folder: &Folder, - shadow: &FolderShadow, - targets: &BTreeMap, - world: Option<&JavaWorld>, - blank: &mut Vec, - ) -> BoundShadow { - let side = folder - .directives - .shadow - .resolution - .clamp(1, device.limits().max_texture_dimension_2d); - let texture = |label, format, usage| { - device.create_texture(&wgpu::TextureDescriptor { - label: Some(label), - size: wgpu::Extent3d { - width: side, - height: side, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format, - usage, - view_formats: &[], - }) - }; - use wgpu::TextureUsages as U; - let depth = texture( - "Java shader pack shadow depth", - wgpu::TextureFormat::Depth32Float, - U::RENDER_ATTACHMENT | U::TEXTURE_BINDING | U::COPY_SRC, - ); - let depth_solid = texture( - "Java shader pack shadow depth before translucent terrain", - wgpu::TextureFormat::Depth32Float, - U::TEXTURE_BINDING | U::COPY_DST, - ); - let float = |label| { - texture( - label, - wgpu::TextureFormat::R32Float, - U::RENDER_ATTACHMENT | U::TEXTURE_BINDING, - ) - .create_view(&Default::default()) - }; - // The colour targets the shadow programs write or a pass reads. - let passes = folder.passes.iter().chain(&folder.final_pass).chain( - folder - .geometry - .iter() - .flat_map(|g| std::iter::once(&g.terrain).chain(&g.water)), - ); - let mut used: BTreeSet = shadow - .solid - .draw_buffers - .iter() - .chain(shadow.water.iter().flat_map(|w| &w.draw_buffers)) - .copied() - .collect(); - let mut float_reads = false; - for s in passes.clone().flat_map(|p| &p.samplers) { - match s.source { - Source::ShadowColor(i) => { - used.insert(i); - } - Source::ShadowDepth(_) if !s.compare => float_reads = true, - _ => {} - } - } - let mut colors = Vec::new(); - let mut clears = Vec::new(); - let mut initial = Vec::new(); - for (i, &(format, clear)) in shadow.targets.iter().enumerate() { - if !used.contains(&(i as u32)) { - colors.push(None); - continue; - } - let view = texture( - "Java shader pack shadowcolor", - format, - U::RENDER_ATTACHMENT | U::TEXTURE_BINDING, - ) - .create_view(&Default::default()); - match clear { - Some(color) => clears.push((view.clone(), color)), - // Kept between frames: white to start with, as Iris's. - None => initial.push((view.clone(), wgpu::Color::WHITE)), - } - colors.push(Some(view)); - } - let views = ShadowViews { - depth: depth.create_view(&Default::default()), - depth_solid: depth_solid.create_view(&Default::default()), - float: float("Java shader pack shadowtex0"), - float_solid: float("Java shader pack shadowtex1"), - colors, - }; - // The shadow programs read neither the shadow map nor the size of - // the world's depth. - let unused = texture( - "Java shader pack shadow pass depth", - wgpu::TextureFormat::R32Float, - U::TEXTURE_BINDING, - ) - .create_view(&Default::default()); - let pass_views = Views { - depth: &unused, - depth_solid: &unused, - atlas: self.atlas.as_ref().map(|a| &a.view), - lightmap: world.map(|w| w.lightmap), - shadow: None, - }; - let mut bind = - |pass: &PassGpu| pass.bind(device, queue, targets, &pass_views, &self.common, blank); - let solid = bind(&shadow.solid); - let water = shadow.water.as_ref().map(&mut bind); - let out_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Java shader pack shadow depth"), - layout: &self.common.depth_layout, - entries: &[wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&views.depth), - }], - }); - BoundShadow { - views, - depth, - depth_solid, - clears, - initial, - solid, - water, - out_group, - float_reads, - } - } - - #[allow(clippy::too_many_arguments)] - fn bind( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - folder_index: usize, - color: &wgpu::TextureView, - world_depth: &wgpu::TextureView, - size: [u32; 2], - post: &super::world_post::Post, - world: Option<&JavaWorld>, - ) -> Bound { - let folder = &self.folders[folder_index]; - let plan = folder; - let all = folder.passes.iter().chain(&folder.final_pass); - let geometry = folder.geometry.as_ref().filter(|_| world.is_some()); - let geometry_passes = geometry - .into_iter() - .flat_map(|g| std::iter::once(&g.terrain).chain(&g.water)); - // Every target a pass reads or writes, colortex0 (shown without - // `final`) and the world's. - let mut used: BTreeSet = [0, folder.world_target].into(); - for pass in all.clone().chain(geometry_passes) { - used.extend(pass.draw_buffers.iter().copied()); - used.extend(pass.samplers.iter().filter_map(|s| match s.source { - Source::Target(i) => Some(i), - _ => None, - })); - } - let texture = |label, format, usage, size: [u32; 2]| { - device.create_texture(&wgpu::TextureDescriptor { - label: Some(label), - size: wgpu::Extent3d { - width: size[0].max(1), - height: size[1].max(1), - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format, - usage, - view_formats: &[], - }) - }; - let target_usage = wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::COPY_DST; - let targets: BTreeMap = used - .iter() - .filter_map(|&i| { - let settings = plan.targets.get(i as usize)?; - let format = target_format(&settings.format, self.norm16); - let target_size = match settings.size { - Some(horizon_shaderpack::pipeline::TargetSize::Relative(w, h)) if i != 0 => [ - (size[0] as f32 * w).round() as u32, - (size[1] as f32 * h).round() as u32, - ], - Some(horizon_shaderpack::pipeline::TargetSize::Pixels(w, h)) if i != 0 => { - [w, h] - } - _ => size, - }; - let main = texture("Java shader pack target", format, target_usage, target_size); - let alt = texture("Java shader pack target", format, target_usage, target_size); - // Iris: colortex0 to the fog colour, colortex1 to white. - let clear = settings.clear.then(|| { - let [r, g, b, a] = settings.clear_color.unwrap_or(match i { - 1 => [1.0; 4], - _ => [0.0; 4], - }); - wgpu::Color { - r: r.into(), - g: g.into(), - b: b.into(), - a: a.into(), - } - }); - Some(( - i, - Target { - format, - main_view: main.create_view(&Default::default()), - alt_view: alt.create_view(&Default::default()), - main, - alt, - clear, - }, - )) - }) - .collect(); - let depth = texture( - "Java shader pack depth", - wgpu::TextureFormat::R32Float, - wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - size, - ) - .create_view(&Default::default()); - // Written by `world_in` (Horizon's depth before translucent water), - // then by the pack's solid terrain when it draws it. - let depth_solid = texture( - "Java shader pack depth before translucent terrain", - wgpu::TextureFormat::R32Float, - wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - size, - ) - .create_view(&Default::default()); - let mut blank = Vec::new(); - let shadow = geometry - .and_then(|g| g.shadow.as_ref()) - .map(|sh| self.bind_shadow(device, queue, folder, sh, &targets, world, &mut blank)); - let views = Views { - depth: &depth, - depth_solid: &depth_solid, - atlas: self.atlas.as_ref().map(|a| &a.view), - lightmap: world.map(|w| w.lightmap), - shadow: shadow.as_ref().map(|s| &s.views), - }; - let groups = all - .clone() - .map(|pass| pass.bind(device, queue, &targets, &views, &self.common, &mut blank)) - .collect(); - let geometry = geometry.map(|g| { - let test = texture( - "Java shader pack geometry depth", - wgpu::TextureFormat::Depth32Float, - wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - size, - ) - .create_view(&Default::default()); - let r32 = wgpu::TextureFormat::R32Float; - let out_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Java shader pack geometry depth"), - layout: &self.common.depth_layout, - entries: &[wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&test), - }], - }); - let mut bind = |pass: &PassGpu| { - pass.bind(device, queue, &targets, &views, &self.common, &mut blank) - }; - BoundGeometry { - terrain: bind(&g.terrain), - water: g.water.as_ref().map(&mut bind), - world_in: blit_pipeline( - device, - &self.common, - &self.common.world_layout, - "world_in_depth", - &[targets[&folder.world_target].format, r32, r32], - true, - ), - out_both: blit_pipeline( - device, - &self.common, - &self.common.depth_layout, - "depth_out_both", - &[r32, r32], - false, - ), - out: blit_pipeline( - device, - &self.common, - &self.common.depth_layout, - "depth_out", - &[r32], - false, - ), - out_group, - test, - } - }); - - // The world goes into its target's side the gbuffers would write. - let world_target = &targets[&folder.world_target]; - let world_pipeline = blit_pipeline( - device, - &self.common, - &self.common.world_layout, - "world_in", - &[ - world_target.format, - wgpu::TextureFormat::R32Float, - wgpu::TextureFormat::R32Float, - ], - false, - ); - let world_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Java shader pack world"), - layout: &self.common.world_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(color), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(world_depth), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::TextureView(post.opaque_depth()), - }, - ], - }); - - let final_target = folder.final_pass.as_ref().map(|_| { - texture( - "Java shader pack final", - self.format, - wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - size, - ) - .create_view(&Default::default()) - }); - // Without `final`, Iris shows colortex0 as the last pass left it. - let colortex0 = &targets[&0]; - let shown = final_target.clone().unwrap_or_else(|| { - if plan.flipped_at_end.contains(&0) { - colortex0.alt_view.clone() - } else { - colortex0.main_view.clone() - } - }); - let present_pipeline = blit_pipeline( - device, - &self.common, - &self.common.present_layout, - "present", - &[self.format], - false, - ); - let present_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Java shader pack picture"), - layout: &self.common.present_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&shown), - }], - }); - let output_view = texture( - "Java shader pack picture", - self.format, - wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - size, - ) - .create_view(&Default::default()); - let output = post.bind(device, &output_view); - Bound { - folder: folder_index, - size, - post_generation: post.generation(), - targets, - depth, - depth_solid, - groups, - geometry, - shadow, - world_in: (world_pipeline, world_group), - world_target: folder.world_target, - world_alt: plan.flipped_after_prepare.contains(&folder.world_target), - final_target, - present: (present_pipeline, present_group), - output_view, - output, - _blank: blank, - } - } -} - -/// A folder's terrain and water programs on the GPU, or `None` (with a -/// warning: the terrain is then Horizon's) when wgpu refuses them. -fn folder_geometry( - device: &wgpu::Device, - plan: &JavaFolder, - formats: &[wgpu::TextureFormat], - layouts: &GeometryLayouts, -) -> Option { - let terrain = plan.terrain.as_ref()?; - let scope = device.push_error_scope(wgpu::ErrorFilter::Validation); - let gpu = |g: &GeometryProgram, blends: &[Option]| { - PassGpu::new( - device, - &g.program, - formats, - Output::Geometry(GeometrySetup { - flat_vertex: g.variants.first().unwrap_or(&g.program.vertex), - blends, - layouts, - shadow: false, - }), - ) - }; - let made = gpu(terrain, &plan.terrain_blends).and_then(|terrain| { - let water = plan - .water - .as_ref() - .map(|w| gpu(w, &plan.water_blends)) - .transpose()?; - Ok(FolderGeometry { - terrain, - water, - translucent_at: plan.translucent_at, - shadow: None, - }) - }); - let error = pollster::block_on(scope.pop()); - match (made, error) { - (Ok(mut geometry), None) => { - geometry.shadow = folder_shadow(device, plan, layouts); - Some(geometry) - } - (Err(e), _) => { - log::warn!("Horizon: shader pack {}: {e}", terrain.program.pass.program); - None - } - (_, Some(e)) => { - log::warn!("Horizon: shader pack {}: {e}", terrain.program.pass.program); - None - } - } -} - -/// A folder's shadow programs on the GPU, or `None` (with a warning: the -/// pack then reads a shadow map with nothing in shadow) when wgpu refuses -/// them. -fn folder_shadow( - device: &wgpu::Device, - plan: &JavaFolder, - layouts: &GeometryLayouts, -) -> Option { - let solid = plan.shadow.as_ref()?; - let formats: Vec = plan - .shadow_targets - .iter() - .map(|t| target_format(&t.format, false)) - .collect(); - // Instanced draws only: the flat draws hold what the camera sees. - let layouts = GeometryLayouts { - flat: None, - page: layouts.page, - }; - let scope = device.push_error_scope(wgpu::ErrorFilter::Validation); - let gpu = |g: &GeometryProgram, blends: &[Option]| { - PassGpu::new( - device, - &g.program, - &formats, - Output::Geometry(GeometrySetup { - flat_vertex: &g.program.vertex, - blends, - layouts: &layouts, - shadow: true, - }), - ) - }; - let made = gpu(solid, &plan.shadow_blends).and_then(|solid_gpu| { - let water = plan - .shadow_water - .as_ref() - .map(|w| gpu(w, &plan.shadow_water_blends)) - .transpose()?; - Ok((solid_gpu, water)) - }); - let error = pollster::block_on(scope.pop()); - let warn = |e: &dyn std::fmt::Display| { - log::warn!("Horizon: shader pack {}: {e}", solid.program.pass.program); - }; - match (made, error) { - (Ok((solid_gpu, water)), None) => Some(FolderShadow { - solid: solid_gpu, - water, - targets: plan - .shadow_targets - .iter() - .zip(&formats) - .map(|(t, &format)| { - let clear = t.clear.then(|| { - let [r, g, b, a] = t.clear_color.unwrap_or([1.0; 4]); - wgpu::Color { - r: r.into(), - g: g.into(), - b: b.into(), - a: a.into(), - } - }); - (format, clear) - }) - .collect(), - at: plan.shadow_at, - }), - (Err(e), _) => { - warn(&e); - None - } - (_, Some(e)) => { - warn(&e); - None - } - } -} - -/// The shadow map: Horizon's terrain from the light with the pack's -/// shadow programs, solid then translucent (`ShadowRenderer`), with the -/// depth before translucent terrain kept for `shadowtex1`. -#[allow(clippy::too_many_arguments)] -fn draw_shadow( - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - shadow: &FolderShadow, - b: &BoundShadow, - geometry: &BoundGeometry, - inputs: &Inputs, - world: &JavaWorld, - fresh: bool, -) { - if fresh { - for (view, color) in &b.initial { - clear_view(encoder, view, *color); - } - } - for (view, color) in &b.clears { - clear_view(encoder, view, *color); - } - let draw = |encoder: &mut wgpu::CommandEncoder, - pass: &PassGpu, - group: &wgpu::BindGroup, - layer: Layer, - first: bool| { - pass.write_uniforms(queue, inputs); - let attachments: Vec> = pass - .draw_buffers - .iter() - .map(|&i| { - let view = b.views.colors.get(i as usize)?.as_ref()?; - Some(color_attachment(view, None)) - }) - .collect(); - let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(&pass.program), - color_attachments: &attachments, - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &b.views.depth, - depth_ops: Some(wgpu::Operations { - load: if first { - wgpu::LoadOp::Clear(1.0) - } else { - wgpu::LoadOp::Load - }, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - ..Default::default() - }); - (world.draw)( - &mut render, - layer, - &TerrainPipelines { - instanced: &pass.pipeline, - flat: None, - group, - }, - ); - }; - let float_out = |encoder: &mut wgpu::CommandEncoder, solid: bool| { - if !b.float_reads { - return; - } - let mut attachments = vec![Some(color_attachment(&b.views.float, None))]; - if solid { - attachments.push(Some(color_attachment(&b.views.float_solid, None))); - } - let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Java shader pack shadow depth"), - color_attachments: &attachments, - ..Default::default() - }); - render.set_pipeline(if solid { - &geometry.out_both - } else { - &geometry.out - }); - render.set_bind_group(0, &b.out_group, &[]); - render.draw(0..3, 0..1); - }; - draw(encoder, &shadow.solid, &b.solid, Layer::ShadowSolid, true); - encoder.copy_texture_to_texture( - b.depth.as_image_copy(), - b.depth_solid.as_image_copy(), - b.depth.size(), - ); - float_out(encoder, true); - if let (Some(pass), Some(group)) = (&shadow.water, &b.water) { - draw(encoder, pass, group, Layer::ShadowTranslucent, false); - float_out(encoder, false); - } -} - -/// Iris's `shadowModelView` and `shadowProjection` (`ShadowMatrices`): -/// the light's view of camera-relative positions, snapped to the pack's -/// `shadowIntervalSize` as the camera moves, and an orthographic -/// projection `shadowDistance` wide each way of the camera. -pub fn shadow_matrices(directives: &Directives, shadow_angle: f32, camera: Vec3) -> (Mat4, Mat4) { - let sky = if shadow_angle < 0.25 { - shadow_angle + 0.75 - } else { - shadow_angle - 0.25 - }; - let mut view = Mat4::from_rotation_x(90f32.to_radians()) - * Mat4::from_rotation_z((sky * -360.0).to_radians()) - * Mat4::from_rotation_x(directives.sun_path_rotation.to_radians()); - let d = &directives.shadow; - if d.interval != 0.0 { - // Java's `%` keeps the dividend's sign, as Rust's does. - let offset = Vec3::new( - camera.x % d.interval, - camera.y % d.interval, - camera.z % d.interval, - ) - Vec3::splat(d.interval / 2.0); - view *= Mat4::from_translation(offset); - } - let h = d.distance; - let projection = Mat4::orthographic_rh_gl(-h, h, -h, h, d.near, d.far); - (view, projection) -} - -fn world_in(encoder: &mut wgpu::CommandEncoder, bound: &Bound) { - let target = &bound.targets[&bound.world_target]; - let side = if bound.world_alt { - &target.alt_view - } else { - &target.main_view - }; - let attachments = [ - Some(color_attachment(side, None)), - Some(color_attachment(&bound.depth, None)), - Some(color_attachment(&bound.depth_solid, None)), - ]; - let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Java shader pack world"), - color_attachments: &attachments, - depth_stencil_attachment: bound.geometry.as_ref().map(|g| { - wgpu::RenderPassDepthStencilAttachment { - view: &g.test, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - } - }), - ..Default::default() - }); - match &bound.geometry { - Some(g) => render.set_pipeline(&g.world_in), - None => render.set_pipeline(&bound.world_in.0), - } - render.set_bind_group(0, &bound.world_in.1, &[]); - render.draw(0..3, 0..1); -} - -/// The geometry's depth buffer into `depthtex0`, and `depthtex1` too after -/// solid terrain. -fn depth_out(encoder: &mut wgpu::CommandEncoder, bound: &Bound, solid: bool) { - let Some(g) = &bound.geometry else { - return; - }; - let mut attachments = vec![Some(color_attachment(&bound.depth, None))]; - if solid { - attachments.push(Some(color_attachment(&bound.depth_solid, None))); - } - let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Java shader pack geometry depth"), - color_attachments: &attachments, - ..Default::default() - }); - render.set_pipeline(if solid { &g.out_both } else { &g.out }); - render.set_bind_group(0, &g.out_group, &[]); - render.draw(0..3, 0..1); -} - -fn color_attachment( - view: &wgpu::TextureView, - clear: Option, -) -> wgpu::RenderPassColorAttachment<'_> { - wgpu::RenderPassColorAttachment { - view, - resolve_target: None, - depth_slice: None, - ops: wgpu::Operations { - load: clear.map_or(wgpu::LoadOp::Load, wgpu::LoadOp::Clear), - store: wgpu::StoreOp::Store, - }, - } -} - -fn clear_view(encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView, color: wgpu::Color) { - let attachments = [Some(color_attachment(view, Some(color)))]; - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Java shader pack clear"), - color_attachments: &attachments, - ..Default::default() - }); -} - -/// A full-screen pipeline of `BLIT`'s `entry`; `depth`: it writes the -/// geometry's depth buffer. -fn blit_pipeline( - device: &wgpu::Device, - common: &Common, - layout: &wgpu::BindGroupLayout, - entry: &str, - formats: &[wgpu::TextureFormat], - depth: bool, -) -> wgpu::RenderPipeline { - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Java shader pack blit"), - bind_group_layouts: &[Some(layout)], - ..Default::default() - }); - let targets: Vec> = - formats.iter().map(|&f| Some(f.into())).collect(); - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some(entry), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &common.blit, - entry_point: Some("vs"), - compilation_options: Default::default(), - buffers: &[], - }, - primitive: Default::default(), - depth_stencil: depth.then_some(wgpu::DepthStencilState { - format: wgpu::TextureFormat::Depth32Float, - depth_write_enabled: Some(true), - depth_compare: Some(wgpu::CompareFunction::Always), - stencil: Default::default(), - bias: Default::default(), - }), - multisample: Default::default(), - fragment: Some(wgpu::FragmentState { - module: &common.blit, - entry_point: Some(entry), - compilation_options: Default::default(), - targets: &targets, - }), - multiview_mask: None, - cache: None, - }) -} - -impl Common { - /// `custom` holds the pack's `texture.*` images (`JavaPasses::custom_textures`). - fn new(device: &wgpu::Device, queue: &wgpu::Queue, custom: &BTreeMap) -> Self { - let sampler = |filter, address, compare| { - device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("Java shader pack"), - address_mode_u: address, - address_mode_v: address, - address_mode_w: address, - mag_filter: filter, - min_filter: filter, - compare, - ..Default::default() - }) - }; - use wgpu::{AddressMode::*, FilterMode::*}; - let noise = match custom.get(NOISE_KEY) { - Some(image) => rgba_view(device, queue, "noisetex", image), - None => rgba_view(device, queue, "noisetex", &noise()), - }; - let custom_textures = custom - .iter() - .filter(|(key, _)| *key != NOISE_KEY) - .map(|(key, image)| (key.clone(), rgba_view(device, queue, key, image))) - .collect(); - // The full-screen triangle (0, 0), (2, 0), (0, 2): the pack's - // projection maps 0..1 to the screen. Texture coordinates are the - // position; colours white; normals up. - let vertices = [ - [ - [0.0, 0.0, 0.0, 1.0], - [2.0, 0.0, 0.0, 1.0], - [0.0, 2.0, 0.0, 1.0], - ], - [[1.0f32; 4]; 3], - [[0.0, 0.0, 1.0, 0.0]; 3], - [[0.0; 4]; 3], - ] - .map(|data| { - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Java shader pack vertices"), - size: std::mem::size_of_val(&data) as u64, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - queue.write_buffer(&buffer, 0, bytemuck::cast_slice(&data)); - buffer - }); - let texture_entry = |binding, sample_type| wgpu::BindGroupLayoutEntry { - binding, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }; - let unfilterable = wgpu::TextureSampleType::Float { filterable: false }; - Self { - linear: sampler(Linear, ClampToEdge, None), - atlas: device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("Java shader pack block atlas"), - mag_filter: Nearest, - min_filter: Nearest, - mipmap_filter: wgpu::MipmapFilterMode::Nearest, - ..Default::default() - }), - nearest: sampler(Nearest, ClampToEdge, None), - repeat: sampler(Linear, Repeat, None), - compare: sampler(Linear, ClampToEdge, Some(wgpu::CompareFunction::LessEqual)), - noise, - custom_textures, - vertices, - world_layout: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Java shader pack world"), - entries: &[ - texture_entry(0, unfilterable), - texture_entry(1, wgpu::TextureSampleType::Depth), - texture_entry(2, wgpu::TextureSampleType::Depth), - ], - }), - present_layout: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Java shader pack picture"), - entries: &[texture_entry(0, unfilterable)], - }), - depth_layout: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Java shader pack geometry depth"), - entries: &[texture_entry(1, wgpu::TextureSampleType::Depth)], - }), - blit: device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Java shader pack blit"), - source: wgpu::ShaderSource::Wgsl(BLIT.into()), - }), - } - } -} - -/// The view dimension and integer kind of a GLSL sampler type. -fn sampler_kind(kind: &str) -> Option<(wgpu::TextureViewDimension, Option, bool)> { - use wgpu::TextureViewDimension as D; - let signed = match kind.as_bytes().first() { - Some(b'i') => Some(true), - Some(b'u') => Some(false), - _ => None, - }; - let rest = kind - .trim_start_matches(['i', 'u']) - .strip_prefix("sampler")?; - let dimension = if rest.starts_with("2DArray") { - D::D2Array - } else if rest.starts_with("2D") || rest.starts_with("2DRect") { - D::D2 - } else if rest.starts_with("3D") { - D::D3 - } else if rest.starts_with("CubeArray") { - D::CubeArray - } else if rest.starts_with("Cube") { - D::Cube - } else if rest.starts_with("1D") { - D::D1 - } else { - return None; - }; - Some((dimension, signed, rest.ends_with("Shadow"))) -} - -impl PassGpu { - fn new( - device: &wgpu::Device, - program: &Program, - formats: &[wgpu::TextureFormat], - output: Output, - ) -> Result { - let (screen, setup) = match output { - Output::Screen(screen) => (screen, None), - Output::Geometry(setup) => (None, Some(setup)), - }; - let name = program.pass.program.clone(); - let link = &program.link; - let mut samplers = Vec::new(); - let mut seen = BTreeSet::new(); - for (sampler, kind) in program - .vertex - .interface - .samplers - .iter() - .chain(&program.fragment.interface.samplers) - { - if !seen.insert(sampler.clone()) { - continue; - } - let (dimension, signed, compare) = sampler_kind(kind) - .ok_or_else(|| format!("{name}: {kind} {sampler} isn't supported"))?; - let binding = |var: &str| { - link.bindings - .get(var) - .copied() - .ok_or_else(|| format!("{name}: {sampler} has no binding")) - }; - let var = program - .fragment - .interface - .sampler_vars - .get(sampler) - .or_else(|| program.vertex.interface.sampler_vars.get(sampler)) - .cloned() - .unwrap_or_else(|| format!("{sampler}_s")); - samplers.push(SamplerBinding { - name: sampler.clone(), - source: source(sampler, setup.is_some()), - dimension, - signed, - texture: binding(&format!("{sampler}_t"))?, - sampler: binding(&var)?, - shared: var.starts_with("iris_").then_some(var), - compare, - }); - } - // The layout: the uniform block, then each sampler's texture and sampler. - let block = [&program.fragment, &program.vertex] - .into_iter() - .find_map(|t| uniform_block(&t.module)); - let mut entries = Vec::new(); - if block.is_some() { - entries.push(wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }); - } - let mut sampler_bindings = BTreeSet::new(); - for s in &samplers { - let (sample_type, filtering) = match (s.compare, s.source) { - (true, _) => (wgpu::TextureSampleType::Depth, None), - (false, Source::Target(i)) => { - let format = formats - .get(i as usize) - .copied() - .unwrap_or(wgpu::TextureFormat::Rgba8Unorm); - let t = sample_type(format); - ( - t, - Some(matches!( - t, - wgpu::TextureSampleType::Float { filterable: true } - )), - ) - } - ( - false, - Source::Depth | Source::DepthSolid | Source::ShadowDepth(_) | Source::Far, - ) => ( - wgpu::TextureSampleType::Float { filterable: false }, - Some(false), - ), - (false, _) => ( - wgpu::TextureSampleType::Float { filterable: true }, - Some(true), - ), - }; - let (sample_type, filtering) = match s.signed { - Some(true) => (wgpu::TextureSampleType::Sint, Some(false)), - Some(false) => (wgpu::TextureSampleType::Uint, Some(false)), - None => (sample_type, filtering), - }; - // A shared sampler filters as its name says. - let filtering = match s.shared.as_deref() { - Some("iris_compare") => None, - Some("iris_nearest") => Some(false), - Some(_) => Some(true), - None => filtering, - }; - entries.push(wgpu::BindGroupLayoutEntry { - binding: s.texture, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type, - view_dimension: s.dimension, - multisampled: false, - }, - count: None, - }); - if !sampler_bindings.insert(s.sampler) { - continue; - } - entries.push(wgpu::BindGroupLayoutEntry { - binding: s.sampler, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Sampler(match filtering { - None => wgpu::SamplerBindingType::Comparison, - Some(true) => wgpu::SamplerBindingType::Filtering, - Some(false) => wgpu::SamplerBindingType::NonFiltering, - }), - count: None, - }); - } - let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some(&name), - entries: &entries, - }); - if let Some(setup) = setup { - return Self::geometry(device, program, formats, setup, layout, samplers, block); - } - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some(&name), - bind_group_layouts: &[Some(&layout)], - ..Default::default() - }); - - // Vertex inputs, each from its own buffer of the full-screen triangle. - let inputs = vertex_inputs(&program.vertex.module); - if inputs.len() > MAX_ATTRIBUTES { - return Err(format!( - "{name}: more than {MAX_ATTRIBUTES} vertex attributes" - )); - } - let attribute_layouts: Vec<[wgpu::VertexAttribute; 1]> = inputs - .iter() - .map(|(_, location, format)| { - [wgpu::VertexAttribute { - format: *format, - offset: 0, - shader_location: *location, - }] - }) - .collect(); - let buffers: Vec> = attribute_layouts - .iter() - .map(|attributes| { - Some(wgpu::VertexBufferLayout { - array_stride: 16, - step_mode: wgpu::VertexStepMode::Vertex, - attributes, - }) - }) - .collect(); - let attributes = inputs - .iter() - .map(|(name, _, _)| match name.as_str() { - n if n == "iris_Vertex" || n.starts_with("iris_MultiTexCoord") => 0, - "iris_Color" => 1, - "iris_Normal" => 2, - _ => 3, - }) - .collect(); - - // Output location `l` writes target `draw_buffers[l]` (or the screen). - let outputs = &program.fragment.interface.outputs; - let blends = pass_blends(&program.pass, &program.draw_buffers, None); - let target_states: Vec> = match screen { - Some(format) => vec![Some(wgpu::ColorTargetState { - format, - blend: program.pass.blend.as_ref().and_then(blend_state), - write_mask: if outputs.contains_key(&0) { - wgpu::ColorWrites::ALL - } else { - wgpu::ColorWrites::empty() - }, - })], - None => program - .draw_buffers - .iter() - .enumerate() - .map(|(l, &i)| { - let format = formats - .get(i as usize) - .copied() - .unwrap_or(wgpu::TextureFormat::Rgba8Unorm); - // Only filterable float/unorm formats support blending. - let can_blend = matches!( - sample_type(format), - wgpu::TextureSampleType::Float { filterable: true } - ); - Some(wgpu::ColorTargetState { - format, - blend: blends[l].filter(|_| can_blend), - write_mask: if outputs.contains_key(&(l as u32)) { - wgpu::ColorWrites::ALL - } else { - wgpu::ColorWrites::empty() - }, - }) - }) - .collect(), - }; - let module = |t: &horizon_shaderpack::translate::Translated, label: &str| { - device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some(label), - source: wgpu::ShaderSource::Wgsl(t.wgsl.as_str().into()), - }) - }; - let vertex_module = module(&program.vertex, &format!("{name}.vsh")); - let fragment_module = module(&program.fragment, &format!("{name}.fsh")); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some(&name), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &vertex_module, - entry_point: Some("main"), - compilation_options: Default::default(), - buffers: &buffers, - }, - primitive: Default::default(), - depth_stencil: None, - multisample: Default::default(), - fragment: Some(wgpu::FragmentState { - module: &fragment_module, - entry_point: Some("main"), - compilation_options: Default::default(), - targets: &target_states, - }), - multiview_mask: None, - cache: None, - }); - let uniform = block.as_ref().map(|(size, _)| { - ( - *size, - device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Java shader pack uniforms"), - size: u64::from(*size).max(16), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }), - ) - }); - Ok(Self { - program: name, - pipeline, - layout, - samplers, - uniforms: block.map(|(_, members)| members).unwrap_or_default(), - uniform, - attributes, - draw_buffers: if screen.is_some() { - Vec::new() - } else { - program.draw_buffers.clone() - }, - reads_alt: program.pass.reads_alt.clone(), - scale: program.pass.scale.clamp(0.0, 1.0), - flat_pipeline: None, - geometry: None, - shadow: false, - }) - } - - /// A geometry program's pipelines over Horizon's terrain vertices - /// (instanced and flat draws), into its draw buffers on the side the - /// next passes read, tested against the world's depth. - #[allow(clippy::too_many_arguments)] - fn geometry( - device: &wgpu::Device, - program: &Program, - formats: &[wgpu::TextureFormat], - setup: GeometrySetup, - layout: wgpu::BindGroupLayout, - samplers: Vec, - block: Option<(u32, Vec<(String, Vec)>)>, - ) -> Result { - let name = program.pass.program.clone(); - let outputs = &program.fragment.interface.outputs; - let targets: Vec> = program - .draw_buffers - .iter() - .enumerate() - .map(|(l, &i)| { - let format = formats - .get(i as usize) - .copied() - .unwrap_or(wgpu::TextureFormat::Rgba8Unorm); - let blendable = matches!( - sample_type(format), - wgpu::TextureSampleType::Float { filterable: true } - ); - Some(wgpu::ColorTargetState { - format, - blend: setup.blends.get(l).copied().flatten().filter(|_| blendable), - write_mask: if outputs.contains_key(&(l as u32)) { - wgpu::ColorWrites::ALL - } else { - wgpu::ColorWrites::empty() - }, - }) - }) - .collect(); - let vertex = wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &super::TERRAIN_ATTRIBUTES, - }; - let instance_attributes = wgpu::vertex_attr_array![5 => Float32x4]; - let instance = wgpu::VertexBufferLayout { - array_stride: 16, - step_mode: wgpu::VertexStepMode::Instance, - attributes: &instance_attributes, - }; - let fragment_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some(&format!("{name}.fsh")), - source: wgpu::ShaderSource::Wgsl(program.fragment.wgsl.as_str().into()), - }); - let pipeline = |vertex_program: &horizon_shaderpack::translate::Translated, - flat: Option<&wgpu::BindGroupLayout>| { - let vertex_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some(&format!("{name}.vsh")), - source: wgpu::ShaderSource::Wgsl(vertex_program.wgsl.as_str().into()), - }); - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some(&name), - bind_group_layouts: &[None, flat, Some(&layout), Some(setup.layouts.page)], - ..Default::default() - }); - let buffers = if flat.is_some() { - vec![Some(vertex.clone())] - } else { - vec![Some(vertex.clone()), Some(instance.clone())] - }; - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some(&name), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &vertex_module, - entry_point: Some("main"), - compilation_options: Default::default(), - buffers: &buffers, - }, - // The y flip into GL's layout turns the faces around. The - // shadow map culls none (a mountain's back casts shadows). - primitive: wgpu::PrimitiveState { - cull_mode: (!setup.shadow).then_some(wgpu::Face::Back), - front_face: wgpu::FrontFace::Cw, - ..Default::default() - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: wgpu::TextureFormat::Depth32Float, - depth_write_enabled: Some(true), - depth_compare: Some(wgpu::CompareFunction::LessEqual), - stencil: Default::default(), - bias: Default::default(), - }), - multisample: Default::default(), - fragment: Some(wgpu::FragmentState { - module: &fragment_module, - entry_point: Some("main"), - compilation_options: Default::default(), - targets: &targets, - }), - multiview_mask: None, - cache: None, - }) - }; - let instanced = pipeline(&program.vertex, None); - let flat_pipeline = setup - .layouts - .flat - .map(|flat| pipeline(setup.flat_vertex, Some(flat))); - let uniform = block.as_ref().map(|(size, _)| { - ( - *size, - device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Java shader pack uniforms"), - size: u64::from(*size).max(16), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }), - ) - }); - // It reads the side the next passes read, but the other side of the - // targets it writes: wgpu can't read and write a texture at once. - let flipped = program.pass.reads_alt.clone(); - let written: BTreeSet = program.draw_buffers.iter().copied().collect(); - let reads_alt = (0..horizon_shaderpack::pipeline::MAX_COLOR_BUFFERS) - .filter(|i| flipped.contains(i) != written.contains(i)) - .collect(); - Ok(Self { - program: name, - pipeline: instanced, - layout, - samplers, - uniforms: block.map(|(_, members)| members).unwrap_or_default(), - uniform, - attributes: Vec::new(), - draw_buffers: program.draw_buffers.clone(), - reads_alt, - scale: 1.0, - flat_pipeline, - geometry: Some(flipped), - shadow: setup.shadow, - }) - } - - /// The pass's bind group over `targets` and `views`. - fn bind( - &self, - device: &wgpu::Device, - queue: &wgpu::Queue, - targets: &BTreeMap, - textures: &Views, - common: &Common, - blank: &mut Vec, - ) -> wgpu::BindGroup { - let depth = textures.depth; - let mut views = Vec::new(); - for s in &self.samplers { - let custom = (!s.compare && s.dimension == wgpu::TextureViewDimension::D2) - .then(|| { - common - .custom_textures - .get(&texture_key(&self.program, &s.name)) - }) - .flatten(); - let shadow = textures - .shadow - .filter(|_| s.dimension == wgpu::TextureViewDimension::D2); - let view = if let Some(custom) = custom { - custom.clone() - } else { - match (s.compare, s.source) { - (true, Source::ShadowDepth(i)) if let Some(sh) = shadow => { - if i == 0 { - sh.depth.clone() - } else { - sh.depth_solid.clone() - } - } - (true, _) => blank_view(device, queue, blank, s, BlankKind::FarDepth), - (false, Source::ShadowDepth(i)) if let Some(sh) = shadow => { - if i == 0 { - sh.float.clone() - } else { - sh.float_solid.clone() - } - } - (false, Source::ShadowColor(i)) - if let Some(view) = - shadow.and_then(|sh| sh.colors.get(i as usize)?.as_ref()) => - { - view.clone() - } - (false, Source::Target(i)) if s.dimension == wgpu::TextureViewDimension::D2 => { - match targets.get(&i) { - Some(t) if self.reads_alt.contains(&i) => t.alt_view.clone(), - Some(t) => t.main_view.clone(), - None => blank_view(device, queue, blank, s, BlankKind::Color(false)), - } - } - (false, Source::Depth) if s.dimension == wgpu::TextureViewDimension::D2 => { - depth.clone() - } - (false, Source::DepthSolid) - if s.dimension == wgpu::TextureViewDimension::D2 => - { - textures.depth_solid.clone() - } - (false, Source::Atlas) if s.dimension == wgpu::TextureViewDimension::D2 => { - match textures.atlas { - Some(v) => v.clone(), - None => blank_view(device, queue, blank, s, BlankKind::Color(true)), - } - } - (false, Source::Lightmap) if s.dimension == wgpu::TextureViewDimension::D2 => { - match textures.lightmap { - Some(v) => v.clone(), - None => blank_view(device, queue, blank, s, BlankKind::Color(true)), - } - } - (false, Source::FlatNormal) => { - blank_view(device, queue, blank, s, BlankKind::FlatNormal) - } - (false, Source::ShadowDepth(_) | Source::Far) => { - blank_view(device, queue, blank, s, BlankKind::Far) - } - (false, Source::ShadowColor(_)) => { - blank_view(device, queue, blank, s, BlankKind::Color(true)) - } - (false, Source::Noise) if s.dimension == wgpu::TextureViewDimension::D2 => { - common.noise.clone() - } - (false, Source::Blank(white)) => { - blank_view(device, queue, blank, s, BlankKind::Color(white)) - } - _ => blank_view(device, queue, blank, s, BlankKind::Color(false)), - } - }; - let sampler = match (s.compare, s.source) { - _ if s.shared.is_some() => match s.shared.as_deref() { - Some("iris_compare") => &common.compare, - Some("iris_nearest") => &common.nearest, - Some("iris_repeat") => &common.repeat, - _ => &common.linear, - }, - (true, _) => &common.compare, - _ if s.signed.is_some() => &common.nearest, - (false, Source::Noise) => &common.repeat, - ( - false, - Source::Depth | Source::DepthSolid | Source::ShadowDepth(_) | Source::Far, - ) => &common.nearest, - (false, Source::Atlas) => &common.atlas, - (false, Source::Target(i)) => { - match targets.get(&i).map(|t| sample_type(t.format)) { - Some(wgpu::TextureSampleType::Float { filterable: true }) | None => { - &common.linear - } - _ => &common.nearest, - } - } - _ => &common.linear, - }; - views.push((s.texture, view, s.sampler, sampler)); - } - let mut entries = Vec::new(); - if let Some((_, buffer)) = &self.uniform { - entries.push(wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }); - } - let mut sampler_bindings = BTreeSet::new(); - for (texture, view, sampler_binding, sampler) in &views { - entries.push(wgpu::BindGroupEntry { - binding: *texture, - resource: wgpu::BindingResource::TextureView(view), - }); - if !sampler_bindings.insert(*sampler_binding) { - continue; - } - entries.push(wgpu::BindGroupEntry { - binding: *sampler_binding, - resource: wgpu::BindingResource::Sampler(sampler), - }); - } - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some(&self.program), - layout: &self.layout, - entries: &entries, - }) - } - - /// This frame's values of its uniforms. - fn write_uniforms(&self, queue: &wgpu::Queue, inputs: &Inputs) { - let Some((block_size, buffer)) = &self.uniform else { - return; - }; - let mut bytes = vec![0u8; *block_size as usize]; - for (name, slots) in &self.uniforms { - let values = if self.shadow { - inputs.shadow(name).or_else(|| inputs.value(name, true)) - } else { - inputs.value(name, self.geometry.is_some()) - }; - if let Some(values) = values { - for (&(offset, int), value) in slots.iter().zip(values) { - let at = offset as usize..offset as usize + 4; - bytes[at].copy_from_slice(&if int { - (value as i32).to_ne_bytes() - } else { - value.to_ne_bytes() - }); - } - } - } - queue.write_buffer(buffer, 0, &bytes); - } - - /// A geometry program: Horizon's terrain (solid or `translucent`) into - /// its draw buffers on the side the next passes read, against `depth`. - #[allow(clippy::too_many_arguments)] - fn draw_geometry( - &self, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - bound: &Bound, - group: &wgpu::BindGroup, - depth: &wgpu::TextureView, - inputs: &Inputs, - world: &JavaWorld, - translucent: bool, - ) { - let Some(flipped) = &self.geometry else { - return; - }; - self.write_uniforms(queue, inputs); - // Targets of another size than the world's can't be drawn with it. - let size = wgpu::Extent3d { - width: bound.size[0].max(1), - height: bound.size[1].max(1), - depth_or_array_layers: 1, - }; - let attachments: Vec> = self - .draw_buffers - .iter() - .map(|i| { - let t = bound.targets.get(i).filter(|t| t.main.size() == size)?; - let side = if flipped.contains(i) { - &t.alt_view - } else { - &t.main_view - }; - Some(color_attachment(side, None)) - }) - .collect(); - let pipelines = TerrainPipelines { - instanced: &self.pipeline, - flat: self.flat_pipeline.as_ref(), - group, - }; - let draw = |encoder: &mut wgpu::CommandEncoder, layer| { - let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(&self.program), - color_attachments: &attachments, - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: depth, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - ..Default::default() - }); - (world.draw)(&mut render, layer, &pipelines); - }; - if translucent { - draw(encoder, Layer::Translucent); - return; - } - draw(encoder, Layer::Solid); - // Occlusion culling: the sections left out, tested against what - // phase 1 drew, then the ones in sight. - if let Some(test) = world.occlusion { - test(encoder, depth); - draw(encoder, Layer::Revealed); - } - } - - /// Draw into the targets of `draw_buffers`, on the side it doesn't read. - fn draw( - &self, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - bound: &Bound, - group: &wgpu::BindGroup, - common: &Common, - inputs: &Inputs, - ) { - let views: Vec> = self - .draw_buffers - .iter() - .map(|i| { - bound.targets.get(i).map(|t| { - if self.reads_alt.contains(i) { - &t.main_view - } else { - &t.alt_view - } - }) - }) - .collect(); - let size = self - .draw_buffers - .first() - .and_then(|i| bound.targets.get(i)) - .map_or(bound.size, |t| { - let s = t.main.size(); - [s.width, s.height] - }); - self.draw_to(queue, encoder, &views, size, group, common, inputs); - } - - #[allow(clippy::too_many_arguments)] - fn draw_to( - &self, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - views: &[Option<&wgpu::TextureView>], - size: [u32; 2], - group: &wgpu::BindGroup, - common: &Common, - inputs: &Inputs, - ) { - self.write_uniforms(queue, inputs); - let attachments: Vec> = views - .iter() - .map(|v| v.map(|v| color_attachment(v, None))) - .collect(); - let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(&self.program), - color_attachments: &attachments, - ..Default::default() - }); - render.set_pipeline(&self.pipeline); - render.set_bind_group(0, group, &[]); - for (slot, &data) in self.attributes.iter().enumerate() { - render.set_vertex_buffer(slot as u32, common.vertices[data].slice(..)); - } - let [w, h] = size.map(|v| (v as f32 * self.scale).max(1.0)); - render.set_viewport(0.0, 0.0, w, h, 0.0, 1.0); - render.draw(0..3, 0..1); - } -} - -enum BlankKind { - Color(bool), - /// A normal map's flat normal, (0.5, 0.5, 1). - FlatNormal, - /// An `R32Float` 1.0: the far plane. - Far, - /// A depth texture at the far plane, for comparison samplers. - FarDepth, -} - -/// A one-texel stand-in of the dimension `s` needs. -fn blank_view( - device: &wgpu::Device, - queue: &wgpu::Queue, - keep: &mut Vec, - s: &SamplerBinding, - kind: BlankKind, -) -> wgpu::TextureView { - use wgpu::TextureViewDimension as D; - let (format, bytes): (wgpu::TextureFormat, Vec) = match (kind, s.signed) { - (BlankKind::Color(_), Some(true)) => (wgpu::TextureFormat::R32Sint, vec![0; 4]), - (BlankKind::Color(_), Some(false)) => (wgpu::TextureFormat::R32Uint, vec![0; 4]), - (BlankKind::Color(white), None) => ( - wgpu::TextureFormat::Rgba8Unorm, - vec![if white { 255 } else { 0 }; 4], - ), - (BlankKind::FlatNormal, _) => (wgpu::TextureFormat::Rgba8Unorm, vec![128, 128, 255, 255]), - (BlankKind::Far, _) => (wgpu::TextureFormat::R32Float, 1.0f32.to_ne_bytes().to_vec()), - (BlankKind::FarDepth, _) => (wgpu::TextureFormat::Depth32Float, Vec::new()), - }; - let layers = match s.dimension { - D::Cube | D::CubeArray => 6, - _ => 1, - }; - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("Java shader pack blank"), - size: wgpu::Extent3d { - width: 1, - height: 1, - depth_or_array_layers: layers, - }, - mip_level_count: 1, - sample_count: 1, - dimension: match s.dimension { - D::D1 => wgpu::TextureDimension::D1, - D::D3 => wgpu::TextureDimension::D3, - _ => wgpu::TextureDimension::D2, - }, - format, - usage: wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }); - if bytes.is_empty() { - // Depth can't be written by a copy: clear it to the far plane. - let view = texture.create_view(&Default::default()); - let mut encoder = device.create_command_encoder(&Default::default()); - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Java shader pack blank"), - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - ..Default::default() - }); - queue.submit([encoder.finish()]); - } else { - for layer in 0..layers { - queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &texture, - mip_level: 0, - origin: wgpu::Origin3d { - x: 0, - y: 0, - z: layer, - }, - aspect: wgpu::TextureAspect::All, - }, - &bytes, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(bytes.len() as u32), - rows_per_image: None, - }, - wgpu::Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }, - ); - } - } - let view = texture.create_view(&wgpu::TextureViewDescriptor { - dimension: Some(s.dimension), - ..Default::default() - }); - keep.push(texture); - view -} - -/// The uniform block of a translated program: its size and each member's -/// scalar slots. -fn uniform_block(module: &naga::Module) -> Option<(u32, Vec<(String, Vec)>)> { - let (_, var) = module - .global_variables - .iter() - .find(|(_, v)| v.space == naga::AddressSpace::Uniform)?; - let naga::TypeInner::Struct { members, span } = &module.types[var.ty].inner else { - return None; - }; - let members = members - .iter() - .map(|m| { - let mut slots = Vec::new(); - scalar_slots(module, m.ty, m.offset, &mut slots); - (m.name.clone().unwrap_or_default(), slots) - }) - .collect(); - Some((*span, members)) -} - -/// The scalars of a value of type `ty` at `offset`: vectors in order, -/// matrices column by column, arrays element by element. -fn scalar_slots( - module: &naga::Module, - ty: naga::Handle, - offset: u32, - out: &mut Vec, -) { - let int = |s: naga::Scalar| s.kind != naga::ScalarKind::Float; - match &module.types[ty].inner { - naga::TypeInner::Scalar(s) => out.push((offset, int(*s))), - naga::TypeInner::Vector { size, scalar } => { - out.extend((0..*size as u32).map(|i| (offset + 4 * i, int(*scalar)))) - } - naga::TypeInner::Matrix { columns, rows, .. } => { - let stride = if *rows == naga::VectorSize::Bi { 8 } else { 16 }; - for c in 0..*columns as u32 { - out.extend((0..*rows as u32).map(|r| (offset + c * stride + 4 * r, false))); - } - } - naga::TypeInner::Array { - base, - size: naga::ArraySize::Constant(n), - stride, - } => { - for i in 0..n.get() { - scalar_slots(module, *base, offset + i * stride, out); - } - } - _ => {} - } -} - -/// The vertex entry point's inputs: name, location and a vertex format -/// matching their scalar kind. -fn vertex_inputs(module: &naga::Module) -> Vec<(String, u32, wgpu::VertexFormat)> { - let Some(entry) = module.entry_points.first() else { - return Vec::new(); - }; - let format = |ty: naga::Handle| { - let kind = match &module.types[ty].inner { - naga::TypeInner::Scalar(s) | naga::TypeInner::Vector { scalar: s, .. } => s.kind, - _ => naga::ScalarKind::Float, - }; - match kind { - naga::ScalarKind::Sint => wgpu::VertexFormat::Sint32x4, - naga::ScalarKind::Uint => wgpu::VertexFormat::Uint32x4, - _ => wgpu::VertexFormat::Float32x4, - } - }; - let mut out = Vec::new(); - for arg in &entry.function.arguments { - match (&arg.binding, &module.types[arg.ty].inner) { - (Some(naga::Binding::Location { location, .. }), _) => out.push(( - arg.name.clone().unwrap_or_default(), - *location, - format(arg.ty), - )), - (None, naga::TypeInner::Struct { members, .. }) => { - for m in members { - if let Some(naga::Binding::Location { location, .. }) = &m.binding { - out.push((m.name.clone().unwrap_or_default(), *location, format(m.ty))); - } - } - } - _ => {} - } - } - out -} - -/// The game state Iris's uniforms describe, this frame. -struct Inputs { - view: Mat4, - /// GL's: near at -1, far at 1. - projection: Mat4, - camera: Vec3, - previous: (Mat4, Mat4, Vec3), - size: [f32; 2], - time: f32, - frame_time: f32, - frames: u32, - frame: FrameUniform, - /// `eyeBrightness` and `eyeBrightnessSmooth`: block, sky light × 16. - eye: [f32; 2], - eye_smooth: [f32; 2], - /// `wetness` (`JavaStages::wetness_smooth`). - wetness: f32, - /// `CelestialUniforms`, in view space. - sun_position: Vec3, - moon_position: Vec3, - up_position: Vec3, - /// `sunAngle` and `shadowAngle`, 0..1. - sun_angle: f32, - shadow_angle: f32, - is_day: bool, - /// The pack's custom uniforms this frame. - custom: HashMap>, - /// `atlasSize`, in geometry programs. - atlas_size: [f32; 2], - /// `shadowModelView` and `shadowProjection` ([`shadow_matrices`]). - shadow_view: Mat4, - shadow_projection: Mat4, -} - -/// `shadowAngle`: the sun's angle by day, the moon's by night, 0..1. -fn shadow_angle(world: &JavaFrame) -> f32 { - let sun = celestial_angle(world.sun_angle); - if sun < 180.0 { - sun / 360.0 - } else { - celestial_angle(world.moon_angle) / 360.0 - } -} - -/// `CelestialUniforms.getSunAngle`: the body's angle plus 90, in 0..360 -/// (0 at sunrise for the sun). -fn celestial_angle(degrees: f32) -> f32 { - let c = degrees + 90.0; - if c < 0.0 { - c + 360.0 - } else if c > 360.0 { - c - 360.0 - } else { - c - } -} - -impl Inputs { - fn new( - frame: &FrameUniform, - size: [u32; 2], - time: f32, - frame_time: f32, - frames: u32, - previous: Option<(Mat4, Mat4, Vec3)>, - ) -> Self { - let view = Mat4::from_cols_array_2d(&frame.view); - // Horizon's reversed depth (z/w from 1 near to 0 far) to GL's - // (-1 near, 1 far): z' = w - 2z. - let to_gl = Mat4::from_cols( - Vec4::X, - Vec4::Y, - Vec4::new(0.0, 0.0, -2.0, 0.0), - Vec4::new(0.0, 0.0, 1.0, 1.0), - ); - let projection = to_gl * Mat4::from_cols_array_2d(&frame.projection); - let camera = Vec4::from(frame.camera_position).truncate(); - let mut out = Self { - view, - projection, - camera, - previous: previous.unwrap_or((view, projection, camera)), - size: size.map(|v| v as f32), - time, - frame_time, - frames, - frame: *frame, - eye: [0.0; 2], - eye_smooth: [0.0; 2], - wetness: frame.weather[0], - sun_position: Vec3::ZERO, - moon_position: Vec3::ZERO, - up_position: Vec3::ZERO, - sun_angle: 0.0, - shadow_angle: 0.0, - is_day: true, - custom: HashMap::new(), - atlas_size: [0.0; 2], - shadow_view: Mat4::IDENTITY, - shadow_projection: Mat4::IDENTITY, - }; - out.set_world(&JavaFrame::default(), 0.0); - out - } - - /// The light at the eye and the sun and moon, tilted by the pack's - /// `sunPathRotation` as Iris tilts them. - fn set_world(&mut self, world: &JavaFrame, sun_path_rotation: f32) { - self.eye = world.eye_light.map(|l| f32::from(l) * 16.0); - self.eye_smooth = self.eye; - // CelestialUniforms.getCelestialPosition: the sky's Y -90, the - // tilt, the body's angle, then (0, 100, 0) as a point. - let sky = self.view * Mat4::from_rotation_y((-90f32).to_radians()); - let body = |angle: f32| { - let m = sky - * Mat4::from_rotation_z(sun_path_rotation.to_radians()) - * Mat4::from_rotation_x(angle.to_radians()); - m.transform_point3(Vec3::new(0.0, 100.0, 0.0)) - }; - self.sun_position = body(world.sun_angle); - self.moon_position = body(world.moon_angle); - self.up_position = sky.transform_vector3(Vec3::new(0.0, 100.0, 0.0)); - let sun = celestial_angle(world.sun_angle); - self.is_day = sun < 180.0; - self.sun_angle = sun / 360.0; - self.shadow_angle = shadow_angle(world); - } - - /// The value of uniform `name`, as its scalars in order: Iris's own, - /// else the pack's custom uniform; `None` for one Horizon doesn't - /// provide (it stays zero). - fn value(&self, name: &str, geometry: bool) -> Option> { - geometry - .then(|| self.geometry(name)) - .flatten() - .or_else(|| self.builtin(name)) - .or_else(|| self.custom.get(name).cloned()) - } - - /// The uniforms whose value differs in geometry programs: the - /// camera's matrices (camera-relative positions, as Iris's terrain), - /// the lightmap's texture matrix, the atlas's size. - fn geometry(&self, name: &str) -> Option> { - let mat = |m: Mat4| m.to_cols_array().to_vec(); - Some(match name { - "iris_ModelViewMatrix" | "modelViewMatrix" => mat(self.view), - "iris_ModelViewMatrixInverse" | "modelViewMatrixInverse" => mat(self.view.inverse()), - "iris_ProjectionMatrix" | "projectionMatrix" => mat(self.projection), - "iris_ProjectionMatrixInverse" | "projectionMatrixInverse" => { - mat(self.projection.inverse()) - } - "iris_ModelViewProjectionMatrix" => mat(self.projection * self.view), - "iris_NormalMatrix" | "normalMatrix" => Mat3::from_mat4(self.view) - .inverse() - .transpose() - .to_cols_array() - .to_vec(), - // Unit 1, the lightmap: 0..240 to texel centres (Iris's - // `LIGHTMAP_TEXTURE_MATRIX`). - "iris_TextureMatrix" => { - let lightmap = Mat4::from_scale(Vec3::splat(1.0 / 256.0)) - * Mat4::from_translation(Vec3::splat(8.0)); - (0..8) - .flat_map(|i| mat(if i == 1 { lightmap } else { Mat4::IDENTITY })) - .collect() - } - "textureMatrix" => mat(Mat4::IDENTITY), - "chunkOffset" => vec![0.0; 3], - "alphaTestRef" => vec![0.1], - "atlasSize" => self.atlas_size.to_vec(), - "entityId" | "blockEntityId" | "currentRenderedItemId" => vec![-1.0], - _ => return None, - }) - } - - /// The uniforms whose value differs in shadow programs: the light's - /// matrices instead of the camera's. - fn shadow(&self, name: &str) -> Option> { - let mat = |m: Mat4| m.to_cols_array().to_vec(); - let (view, projection) = (self.shadow_view, self.shadow_projection); - Some(match name { - "iris_ModelViewMatrix" | "modelViewMatrix" => mat(view), - "iris_ModelViewMatrixInverse" | "modelViewMatrixInverse" => mat(view.inverse()), - "iris_ProjectionMatrix" | "projectionMatrix" => mat(projection), - "iris_ProjectionMatrixInverse" | "projectionMatrixInverse" => mat(projection.inverse()), - "iris_ModelViewProjectionMatrix" => mat(projection * view), - "iris_NormalMatrix" | "normalMatrix" => Mat3::from_mat4(view) - .inverse() - .transpose() - .to_cols_array() - .to_vec(), - _ => return None, - }) - } - - /// The value of Iris uniform `name`; `None` for one Horizon doesn't - /// provide. - fn builtin(&self, name: &str) -> Option> { - if let Some(flag) = name.strip_prefix("iris_bool_") { - return self.value(flag, false); - } - let f = &self.frame; - let mat = |m: Mat4| m.to_cols_array().to_vec(); - let vec3 = |v: Vec3| v.to_array().to_vec(); - let day = f.time[2].rem_euclid(1.0); - // The full-screen passes' own matrices: 0..1 onto the screen. - let screen = Mat4::orthographic_rh_gl(0.0, 1.0, 0.0, 1.0, -1.0, 1.0); - Some(match name { - "gbufferModelView" => mat(self.view), - "gbufferModelViewInverse" => mat(self.view.inverse()), - "gbufferProjection" => mat(self.projection), - "gbufferProjectionInverse" => mat(self.projection.inverse()), - "gbufferPreviousModelView" => mat(self.previous.0), - "gbufferPreviousProjection" => mat(self.previous.1), - "cameraPosition" => vec3(self.camera), - "previousCameraPosition" => vec3(self.previous.2), - "cameraPositionInt" => vec3(self.camera.floor()), - "cameraPositionFract" => vec3(self.camera - self.camera.floor()), - "eyeAltitude" => vec![self.camera.y], - "viewWidth" => vec![self.size[0]], - "viewHeight" => vec![self.size[1]], - "aspectRatio" => vec![self.size[0] / self.size[1].max(1.0)], - "near" => vec![f.view_params[0]], - "far" => vec![f.view_params[2]], - "frameTimeCounter" => vec![self.time % 3600.0], - "frameTime" => vec![self.frame_time], - "frameCounter" => vec![(self.frames % 720_720) as f32], - "worldTime" => vec![(day * 24000.0).floor()], - "worldDay" => vec![0.0], - "sunAngle" => vec![self.sun_angle], - "shadowAngle" => vec![self.shadow_angle], - "sunPosition" => vec3(self.sun_position), - "moonPosition" => vec3(self.moon_position), - "shadowLightPosition" => vec3(if self.is_day { - self.sun_position - } else { - self.moon_position - }), - "upPosition" => vec3(self.up_position), - "moonPhase" => vec![f.moon[3]], - "rainStrength" => vec![f.weather[0]], - "wetness" => vec![self.wetness], - "thunderStrength" => vec![f.weather[1]], - "fogColor" => f.fog_color[..3].to_vec(), - "skyColor" => f.sky_color[..3].to_vec(), - "isEyeInWater" => vec![f.camera_state[0]], - "nightVision" => vec![f.camera_state[1]], - "darknessFactor" => vec![f.camera_state[2]], - "screenBrightness" => vec![1.0], - "eyeBrightness" => self.eye.to_vec(), - // An ivec2: the smoothed value truncated. - "eyeBrightnessSmooth" => self.eye_smooth.map(f32::trunc).to_vec(), - "hasSkylight" => vec![f.dimension[1]], - "iris_ModelViewMatrix" | "iris_ModelViewMatrixInverse" => mat(Mat4::IDENTITY), - "iris_ProjectionMatrix" | "iris_ModelViewProjectionMatrix" => mat(screen), - "iris_ProjectionMatrixInverse" => mat(screen.inverse()), - "iris_TextureMatrix" => (0..8).flat_map(|_| mat(Mat4::IDENTITY)).collect(), - "iris_NormalMatrix" => Mat3::IDENTITY.to_cols_array().to_vec(), - "shadowModelView" => mat(self.shadow_view), - "shadowModelViewInverse" => mat(self.shadow_view.inverse()), - "shadowProjection" => mat(self.shadow_projection), - "shadowProjectionInverse" => mat(self.shadow_projection.inverse()), - _ => return None, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn iris_formats_become_renderable_wgpu_formats() { - use wgpu::TextureFormat as F; - for (iris, wgpu) in [ - ("RGBA", F::Rgba8Unorm), - ("RGB8", F::Rgba8Unorm), - ("R8", F::R8Unorm), - ("RG16F", F::Rg16Float), - ("R11F_G11F_B10F", F::Rgba16Float), - ("RGB8_SNORM", F::Rgba16Float), - ("RGBA16", F::Rgba16Float), - ("R32F", F::R32Float), - ("RGBA32F", F::Rgba32Float), - ("RGB10_A2", F::Rgb10a2Unorm), - ("R32UI", F::R32Uint), - ("RGBA16I", F::Rgba32Sint), - ] { - assert_eq!(target_format(iris, false), wgpu, "{iris}"); - } - assert_eq!(target_format("RGBA16", true), F::Rgba16Unorm); - assert_eq!(target_format("RG16_SNORM", true), F::Rg16Snorm); - assert_eq!(target_format("RGBA16F", true), F::Rgba16Float); - assert_eq!( - sample_type(F::R32Float), - wgpu::TextureSampleType::Float { filterable: false } - ); - } - - #[test] - fn shadow_matrices_are_iris_s() { - use horizon_shaderpack::pipeline::ShadowDirectives; - let close = |a: Mat4, b: Mat4| { - for (x, y) in a.to_cols_array().iter().zip(b.to_cols_array()) { - assert!((x - y).abs() < 1e-5, "{a} != {b}"); - } - }; - // ShadowMatrices.Tests: shadowDistance 32, planes 0.05 and 256. - let directives = Directives { - shadow: ShadowDirectives { - distance: 32.0, - near: 0.05, - far: 256.0, - interval: 0.0, - ..ShadowDirectives::default() - }, - ..Directives::default() - }; - let (view, projection) = shadow_matrices(&directives, 0.25, Vec3::ZERO); - close( - projection, - Mat4::from_cols_array(&[ - 0.03125, - 0.0, - 0.0, - 0.0, - 0.0, - 0.03125, - 0.0, - 0.0, - 0.0, - 0.0, - -0.007_814_026, - 0.0, - 0.0, - 0.0, - -1.000_390_6, - 1.0, - ]), - ); - // Noon: the light looks straight down (up is toward it, +z). - assert!((view.transform_vector3(Vec3::Y) - Vec3::Z).length() < 1e-5); - // Snapped to the interval: x 5.5 % 2 = 1.5 further than 0 % 2. - let directives = Directives::default(); - let (snapped, _) = shadow_matrices(&directives, 0.25, Vec3::new(5.5, 0.0, 0.0)); - let (plain, _) = shadow_matrices(&directives, 0.25, Vec3::ZERO); - close( - snapped, - plain * Mat4::from_translation(Vec3::new(1.5, 0.0, 0.0)), - ); - } - - #[test] - fn samplers_read_what_iris_binds() { - assert_eq!(source("colortex4", false), Source::Target(4)); - assert_eq!(source("gaux1", false), Source::Target(4)); - assert_eq!(source("gdepthtex", false), Source::Depth); - assert_eq!(source("shadowtex1", false), Source::ShadowDepth(1)); - assert_eq!(source("shadow", false), Source::ShadowDepth(0)); - assert_eq!(source("dhDepthTex0", false), Source::Far); - assert_eq!(source("shadowcolor1", false), Source::ShadowColor(1)); - assert_eq!(source("shadowcolor", false), Source::ShadowColor(0)); - assert_eq!(source("cloudTexture", false), Source::Blank(false)); - // Before translucent terrain; geometry programs' own textures. - assert_eq!(source("depthtex1", false), Source::DepthSolid); - assert_eq!(source("gtexture", true), Source::Atlas); - assert_eq!(source("texture", true), Source::Atlas); - assert_eq!(source("lightmap", true), Source::Lightmap); - assert_eq!(source("normals", true), Source::FlatNormal); - assert_eq!(source("colortex1", true), Source::Target(1)); - let (d, signed, shadow) = sampler_kind("usampler2D").unwrap(); - assert_eq!( - (d, signed, shadow), - (wgpu::TextureViewDimension::D2, Some(false), false) - ); - assert!(sampler_kind("sampler2DShadow").unwrap().2); - assert_eq!( - sampler_kind("sampler3D").unwrap().0, - wgpu::TextureViewDimension::D3 - ); - } - - #[test] - fn celestial_and_eye_uniforms_follow_iris() { - let frame = FrameUniform { - view: Mat4::IDENTITY.to_cols_array_2d(), - ..Default::default() - }; - let close = |a: Option>, b: [f32; 3]| { - let a = a.unwrap(); - assert!( - a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-3), - "{a:?} != {b:?}" - ); - }; - let mut inputs = Inputs::new(&frame, [300, 200], 0.0, 0.0, 0, None); - // Sunrise: the sun in the east, at angle 0; the moon in the west. - let world = |sun_angle: f32| JavaFrame { - eye_light: [3, 15], - sun_angle, - moon_angle: sun_angle + 180.0, - }; - inputs.set_world(&world(-90.0), 0.0); - close(inputs.value("sunPosition", false), [100.0, 0.0, 0.0]); - close(inputs.value("moonPosition", false), [-100.0, 0.0, 0.0]); - close(inputs.value("upPosition", false), [0.0, 100.0, 0.0]); - assert_eq!(inputs.value("sunAngle", false), Some(vec![0.0])); - assert_eq!( - inputs.value("eyeBrightness", false), - Some(vec![48.0, 240.0]) - ); - // Complementary Unbound's sunPathRotation of -40 at noon: the sun - // where its GetSunVector puts it, (0, cos 40, sin 40). - let tilt = (-40f32).to_radians(); - inputs.set_world(&world(0.0), -40.0); - close( - inputs.value("sunPosition", false), - [0.0, 100.0 * tilt.cos(), -100.0 * tilt.sin()], - ); - assert_eq!(inputs.value("sunAngle", false), Some(vec![0.25])); - close(inputs.value("shadowLightPosition", false), { - let s = inputs.sun_position; - [s.x, s.y, s.z] - }); - // Night: the moon casts the shadows. - inputs.set_world(&world(150.0), 0.0); - assert!(!inputs.is_day); - assert_eq!( - inputs.value("shadowLightPosition", false), - inputs.value("moonPosition", false) - ); - assert!((inputs.value("shadowAngle", false).unwrap()[0] - 60.0 / 360.0).abs() < 1e-6); - // Custom uniforms come after Iris's own. - inputs.custom.insert("eyeBrightnessM".into(), vec![0.5]); - inputs.custom.insert("sunAngle".into(), vec![9.0]); - assert_eq!(inputs.value("eyeBrightnessM", false), Some(vec![0.5])); - assert_ne!(inputs.value("sunAngle", false), Some(vec![9.0])); - } - - #[test] - fn gl_projection_from_reversed_depth() { - // A point at the near plane is GL's -1, at the far plane 1. - let (near, far) = (0.05, 512.0); - let frame = FrameUniform { - view: Mat4::IDENTITY.to_cols_array_2d(), - projection: Mat4::perspective_rh(1.2, 1.5, far, near).to_cols_array_2d(), - ..Default::default() - }; - let inputs = Inputs::new(&frame, [300, 200], 0.0, 0.0, 0, None); - let depth = |d: f32| { - let clip = inputs.projection * Vec4::new(0.0, 0.0, -d, 1.0); - clip.z / clip.w - }; - assert!((depth(near) + 1.0).abs() < 1e-4, "{}", depth(near)); - assert!((depth(far) - 1.0).abs() < 1e-3, "{}", depth(far)); - assert_eq!(inputs.value("viewWidth", false), Some(vec![300.0])); - assert_eq!( - inputs.value("iris_bool_hasSkylight", false), - inputs.value("hasSkylight", false) - ); - } -} diff --git a/crates/horizon-client/src/gfx/java_terrain.rs b/crates/horizon-client/src/gfx/java_terrain.rs deleted file mode 100644 index eb71959c9..000000000 --- a/crates/horizon-client/src/gfx/java_terrain.rs +++ /dev/null @@ -1,667 +0,0 @@ -//! Horizon's terrain drawn with a Java pack's `gbuffers_terrain` and -//! `gbuffers_water` (docs/shader-packs.md). The pack's vertex program runs -//! after a glue that rebuilds what Iris would feed it from Horizon's -//! `TerrainVertex`: the camera-relative position, the quad's normal and -//! tangent (from its four corners, read from the arena page as storage), -//! the colour without vanilla's directional shade, the lightmap -//! coordinates, the sprite's place in a block atlas laid out from Horizon's -//! sprite arrays, and `mc_Entity` from the pack's `block.properties`, by -//! sprite name. -use horizon_shaderpack::translate::Geometry; -use std::collections::BTreeMap; - -/// `TerrainVertex` in 32-bit words. -const VERTEX_WORDS: u32 = 6; - -/// The glue's declarations and `hz_vertex`; `HZ_ORIGIN_DECL` and -/// `HZ_ORIGIN` are the variant's (instanced or flat draws). -const GLUE: &str = r#" -layout(location = 0) in vec3 hz_position; -layout(location = 1) in uvec2 hz_uv; -layout(location = 2) in uint hz_layer; -layout(location = 3) in vec4 hz_color; -layout(location = 4) in uvec2 hz_light; -HZ_ORIGIN_DECL -layout(std430, set = 3, binding = 0) readonly buffer HzVertices { uint hz_vertices[]; }; -// [0]: atlas columns, [1]: 1 in the Nether, then each sprite layer's block id. -layout(std430, set = 3, binding = 1) readonly buffer HzTable { int hz_table[]; }; -vec4 hz_pos; -vec3 hz_normal; -vec4 hz_tangent; -vec4 hz_tint; -vec2 hz_atlas; -vec2 hz_mid; -vec2 hz_lightmap; -float hz_block; -vec3 hz_mid_block; -vec3 hz_corner(uint v) { - uint b = v * HZ_WORDSu; - return vec3(uintBitsToFloat(hz_vertices[b]), uintBitsToFloat(hz_vertices[b + 1u]), - uintBitsToFloat(hz_vertices[b + 2u])); -} -vec2 hz_corner_uv(uint v) { - uint w = hz_vertices[v * HZ_WORDSu + 3u]; - return vec2(float(w & 65535u), float(w >> 16u)); -} -void hz_vertex() { - vec4 origin = HZ_ORIGIN; - hz_pos = vec4(hz_position + origin.xyz, 1.0); - // The quad's corners: quads are four vertices from a multiple of four. - uint q = uint(gl_VertexIndex) & 4294967292u; - vec3 p0 = hz_corner(q); - vec3 e1 = hz_corner(q + 1u) - p0; - vec3 e2 = hz_corner(q + 2u) - p0; - vec3 n = cross(e1, e2); - if (dot(n, n) < 1e-12) { - n = cross(e2, hz_corner(q + 3u) - p0); - } - hz_normal = dot(n, n) > 0.0 ? normalize(n) : vec3(0.0, 1.0, 0.0); - vec2 t0 = hz_corner_uv(q); - vec2 d1 = hz_corner_uv(q + 1u) - t0; - vec2 d2 = hz_corner_uv(q + 2u) - t0; - float s = d1.x * d2.y - d2.x * d1.y < 0.0 ? -1.0 : 1.0; - vec3 t = (e1 * d2.y - e2 * d1.y) * s; - vec3 b = (e2 * d1.x - e1 * d2.x) * s; - t = t - hz_normal * dot(hz_normal, t); - t = dot(t, t) > 0.0 ? normalize(t) : vec3(1.0, 0.0, 0.0); - hz_tangent = vec4(t, dot(cross(hz_normal, t), b) < 0.0 ? -1.0 : 1.0); - // Iris turns vanilla's directional shade off: undo it on axis faces. - vec3 a = abs(hz_normal); - bool nether = hz_table[1] != 0; - float shade = 1.0; - if (a.y > 0.999) { - shade = nether ? 0.9 : (hz_normal.y > 0.0 ? 1.0 : 0.5); - } else if (a.z > 0.999) { - shade = 0.8; - } else if (a.x > 0.999) { - shade = 0.6; - } - hz_tint = vec4(min(hz_color.rgb / shade, vec3(1.0)), hz_color.a); - // The sprite's cell in the atlas, columns of square cells. - int layer = int(hz_layer & 16383u); - int cols = max(hz_table[0], 1); - vec2 cell = vec2(float(layer % cols), float(layer / cols)); - vec2 uv = clamp(vec2(hz_uv) * origin.w, 0.0, 1.0); - hz_atlas = (cell + uv) / float(cols); - hz_mid = (cell + 0.5) / float(cols); - // Vanilla's UV2: block light, then sky light, 0..240. - hz_lightmap = vec2(float(hz_light.y), float(hz_light.x)); - hz_block = float(hz_table[2 + layer]); - hz_mid_block = (floor(hz_position - hz_normal * 0.01) + 0.5 - hz_position) * 64.0; -} -"#; - -/// The glue of one draw path: instanced section draws (the origin is an -/// instance attribute) or flat ones (`gfx/flat.rs`: from the block's slot). -pub fn glue(flat: bool, alpha_test: Option) -> Geometry { - let (decl, origin) = if flat { - ( - "layout(std430, set = 1, binding = 0) readonly buffer HzBlockSlots { uint hz_block_slots[]; };\n\ - layout(std430, set = 1, binding = 1) readonly buffer HzSlotOrigins { vec4 hz_slot_origins[]; };", - "hz_slot_origins[hz_block_slots[uint(gl_VertexIndex) >> 8u]]", - ) - } else { - ("layout(location = 5) in vec4 hz_origin;", "hz_origin") - }; - let vertex_glue = GLUE - .replace("HZ_ORIGIN_DECL", decl) - .replace("HZ_ORIGIN", origin) - .replace("HZ_WORDS", &VERTEX_WORDS.to_string()); - let lightmap = "vec4(hz_lightmap, 0.0, 1.0)"; - let attributes: BTreeMap = [ - ("iris_Vertex", "hz_pos"), - ("vaPosition", "hz_pos"), - ("iris_Normal", "vec4(hz_normal, 0.0)"), - ("vaNormal", "vec4(hz_normal, 0.0)"), - ("iris_Color", "hz_tint"), - ("vaColor", "hz_tint"), - ("iris_MultiTexCoord0", "vec4(hz_atlas, 0.0, 1.0)"), - ("vaUV0", "vec4(hz_atlas, 0.0, 1.0)"), - ("iris_MultiTexCoord1", lightmap), - ("iris_MultiTexCoord2", lightmap), - ("vaUV2", lightmap), - // OverlayTexture.NO_OVERLAY. - ("vaUV1", "vec4(0.0, 10.0, 0.0, 0.0)"), - ("mc_Entity", "vec4(hz_block, -1.0, 0.0, 0.0)"), - ("mc_midTexCoord", "vec4(hz_mid, 0.0, 1.0)"), - ("at_tangent", "hz_tangent"), - ("at_midBlock", "vec4(hz_mid_block, 0.0)"), - ] - .into_iter() - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .collect(); - Geometry { - vertex_glue, - attributes, - alpha_test, - } -} - -/// The glue's table: atlas columns, the Nether flag, then each sprite -/// layer's block id from `block.properties` (-1 for blocks it doesn't -/// list, as Iris). -pub fn table(sprite_layers: &BTreeMap, block_ids: &BTreeMap) -> Vec { - let layers = sprite_layers.values().max().map_or(0, |&m| m as usize + 1); - let mut out = vec![-1; layers + 2]; - out[0] = atlas_columns(layers as u32) as i32; - out[1] = 0; - for (name, &layer) in sprite_layers { - if let Some(id) = horizon_shaderpack::properties::sprite_block_id(name, block_ids) { - out[layer as usize + 2] = id; - } - } - out -} - -/// The atlas's columns (and rows) of sprite cells for `layers` sprites. -pub fn atlas_columns(layers: u32) -> u32 { - (f64::from(layers.max(1)).sqrt().ceil() as u32).max(1) -} - -/// Whether `device` renders to and filters the 16-bit normalized formats -/// a Java pack's `RGBA16` targets become (`java_stages::target_format`). -pub fn norm16_targets(adapter: &wgpu::Adapter, device: &wgpu::Device) -> bool { - use wgpu::TextureFormat as F; - let wanted = wgpu::Features::TEXTURE_FORMAT_16BIT_NORM - | wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES; - device.features().contains(wanted) - && [ - F::R16Unorm, - F::Rg16Unorm, - F::Rgba16Unorm, - F::R16Snorm, - F::Rg16Snorm, - F::Rgba16Snorm, - ] - .into_iter() - .all(|f| { - let features = adapter.get_texture_format_features(f); - features - .allowed_usages - .contains(wgpu::TextureUsages::RENDER_ATTACHMENT) - && features - .flags - .contains(wgpu::TextureFormatFeatureFlags::FILTERABLE) - }) -} - -/// The bind group layout of a page group: the page's vertices and the glue's -/// table, read by the vertex stage. -pub fn page_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { - let storage = |binding| wgpu::BindGroupLayoutEntry { - binding, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { read_only: true }, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }; - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Java shader pack terrain page"), - entries: &[storage(0), storage(1)], - }) -} - -/// Which of Horizon's terrain draws a pack's program draws. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Layer { - /// The solid sections: all of them, or with occlusion culling - /// ([`JavaWorld::occlusion`]) the ones the last test saw (phase 1). - Solid, - /// With occlusion culling, the solid sections the test after phase 1 - /// found in sight (phase 2). - Revealed, - Translucent, - /// The shadow map's sections ([`super::Renderer`]'s shadow list), solid - /// then translucent. - ShadowSolid, - ShadowTranslucent, -} - -/// Draws Horizon's terrain with a pack's pipelines. Set by the renderer, -/// which owns the sections. -pub type DrawTerrain<'a> = dyn Fn(&mut wgpu::RenderPass<'_>, Layer, &TerrainPipelines<'_>) + 'a; - -/// Occlusion culling's test between [`Layer::Solid`] and -/// [`Layer::Revealed`], against the pack's depth (OpenGL's layout). -pub type OcclusionTest<'a> = dyn Fn(&mut wgpu::CommandEncoder, &wgpu::TextureView) + 'a; - -/// What the renderer binds to draw with a pack's geometry program: the -/// pipeline of its draw path and the pack's group (group 2). Group 1 is the -/// flat path's, group 3 the page's ([`page_layout`]). `flat` is `None` when -/// the renderer has no flat draws. -pub struct TerrainPipelines<'a> { - pub instanced: &'a wgpu::RenderPipeline, - pub flat: Option<&'a wgpu::RenderPipeline>, - pub group: &'a wgpu::BindGroup, -} - -/// What a Java pack's geometry reads from the renderer each frame. -pub struct JavaWorld<'a> { - /// The block sprite arrays, [`super::SPRITE_SPLIT`]-layer each. - pub sprites: &'a [wgpu::Texture], - pub split: u32, - /// Mipmap levels the terrain samples. - pub levels: u32, - /// The sprite layers and mip levels animation rewrote this frame. - pub animated: &'a [(u32, u32)], - pub lightmap: &'a wgpu::TextureView, - pub draw: &'a DrawTerrain<'a>, - /// Set when the frame culls solid sections by occlusion. - pub occlusion: Option<&'a OcclusionTest<'a>>, -} - -const ATLAS: &str = r#" -struct Atlas { cols: u32, layers: u32, split: u32, pad: u32 } -@group(0) @binding(0) var atlas: Atlas; -@group(0) @binding(1) var s0: texture_2d_array; -@group(0) @binding(2) var s1: texture_2d_array; -@group(0) @binding(3) var s2: texture_2d_array; -@group(0) @binding(4) var s3: texture_2d_array; -@group(0) @binding(5) var s4: texture_2d_array; -@group(0) @binding(6) var s5: texture_2d_array; -@group(0) @binding(7) var s6: texture_2d_array; -@group(0) @binding(8) var s7: texture_2d_array; - -struct Out { - @builtin(position) p: vec4f, - @location(0) @interpolate(flat) mip: u32, -} - -@vertex -fn vs(@builtin(vertex_index) i: u32, @builtin(instance_index) mip: u32) -> Out { - let uv = vec2f(f32((i << 1u) & 2u), f32(i & 2u)); - return Out(vec4f(uv * 2.0 - 1.0, 0.0, 1.0), mip); -} - -// Each sprite layer into its cell, at mip level `mip`. -@fragment -fn fs(in: Out) -> @location(0) vec4f { - let cell = max(textureDimensions(s0, in.mip).x, 1u); - let px = vec2u(in.p.xy); - let c = px / cell; - let layer = c.y * atlas.cols + c.x; - if c.x >= atlas.cols || layer >= atlas.layers { - return vec4f(0.0); - } - let at = px % cell; - let l = i32(layer % atlas.split); - let m = i32(in.mip); - switch layer / atlas.split { - case 0u: { return textureLoad(s0, at, l, m); } - case 1u: { return textureLoad(s1, at, l, m); } - case 2u: { return textureLoad(s2, at, l, m); } - case 3u: { return textureLoad(s3, at, l, m); } - case 4u: { return textureLoad(s4, at, l, m); } - case 5u: { return textureLoad(s5, at, l, m); } - case 6u: { return textureLoad(s6, at, l, m); } - default: { return textureLoad(s7, at, l, m); } - } -} -"#; - -/// Horizon's block sprites as one 2D atlas, as Iris binds `gtexture`: -/// square cells, [`atlas_columns`] to a row. Drawn from the sprite arrays -/// once, then animated sprites are copied in as they change. -pub struct Atlas { - texture: wgpu::Texture, - pub view: wgpu::TextureView, - mips: Vec, - pipeline: wgpu::RenderPipeline, - layout: wgpu::BindGroupLayout, - uniform: wgpu::Buffer, - /// Layers, sprite size, mip levels it was made for. - key: (u32, u32, u32), - /// The first sprite array it was drawn from (resource reloads make - /// new ones). - source: wgpu::Texture, - drawn: bool, -} - -impl Atlas { - /// The atlas for `world`'s sprites; `None` without any. - pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, world: &JavaWorld) -> Option { - let key = Self::key(world)?; - let (layers, size, levels) = key; - let cols = atlas_columns(layers); - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("Java shader pack block atlas"), - size: wgpu::Extent3d { - width: cols * size, - height: cols * size, - depth_or_array_layers: 1, - }, - mip_level_count: levels, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::COPY_DST, - view_formats: &[], - }); - let mips = (0..levels) - .map(|level| { - texture.create_view(&wgpu::TextureViewDescriptor { - base_mip_level: level, - mip_level_count: Some(1), - ..Default::default() - }) - }) - .collect(); - let mut entries = vec![wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }]; - entries.extend((1..=8).map(|binding| wgpu::BindGroupLayoutEntry { - binding, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: false }, - view_dimension: wgpu::TextureViewDimension::D2Array, - multisampled: false, - }, - count: None, - })); - let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Java shader pack block atlas"), - entries: &entries, - }); - let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Java shader pack block atlas"), - source: wgpu::ShaderSource::Wgsl(ATLAS.into()), - }); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Java shader pack block atlas"), - layout: Some( - &device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Java shader pack block atlas"), - bind_group_layouts: &[Some(&layout)], - ..Default::default() - }), - ), - vertex: wgpu::VertexState { - module: &module, - entry_point: Some("vs"), - compilation_options: Default::default(), - buffers: &[], - }, - primitive: Default::default(), - depth_stencil: None, - multisample: Default::default(), - fragment: Some(wgpu::FragmentState { - module: &module, - entry_point: Some("fs"), - compilation_options: Default::default(), - targets: &[Some(wgpu::TextureFormat::Rgba8Unorm.into())], - }), - multiview_mask: None, - cache: None, - }); - let uniform = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Java shader pack block atlas"), - size: 16, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - queue.write_buffer( - &uniform, - 0, - bytemuck::cast_slice(&[cols, layers, world.split.max(1), 0]), - ); - Some(Self { - view: texture.create_view(&Default::default()), - texture, - mips, - pipeline, - layout, - uniform, - key, - source: world.sprites[0].clone(), - drawn: false, - }) - } - - fn key(world: &JavaWorld) -> Option<(u32, u32, u32)> { - let first = world.sprites.first()?; - let layers = world - .sprites - .iter() - .map(|t| t.depth_or_array_layers()) - .sum(); - let levels = (world.levels + 1).min(first.mip_level_count()).max(1); - Some((layers, first.width(), levels)) - } - - /// Whether it was made for `world`'s sprites. - pub fn fits(&self, world: &JavaWorld) -> bool { - Self::key(world) == Some(self.key) && world.sprites.first() == Some(&self.source) - } - - /// Bring it up to date: drawn the first time, then the sprites - /// animation rewrote copied into their cells. - pub fn update( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - world: &JavaWorld, - ) { - if !self.drawn { - self.draw(device, encoder, world); - self.drawn = true; - return; - } - let (layers, size, levels) = self.key; - let cols = atlas_columns(layers); - let split = world.split.max(1); - for &(layer, level) in world.animated { - let Some(array) = world.sprites.get((layer / split) as usize) else { - continue; - }; - if level >= levels || layer >= layers { - continue; - } - let edge = (size >> level).max(1); - encoder.copy_texture_to_texture( - wgpu::TexelCopyTextureInfo { - texture: array, - mip_level: level, - origin: wgpu::Origin3d { - x: 0, - y: 0, - z: layer % split, - }, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyTextureInfo { - texture: &self.texture, - mip_level: level, - origin: wgpu::Origin3d { - x: (layer % cols) * edge, - y: (layer / cols) * edge, - z: 0, - }, - aspect: wgpu::TextureAspect::All, - }, - wgpu::Extent3d { - width: edge, - height: edge, - depth_or_array_layers: 1, - }, - ); - } - } - - /// The atlas's size in pixels (`atlasSize`). - pub fn size(&self) -> [u32; 2] { - [self.texture.width(), self.texture.height()] - } - - /// Draw the sprites into the atlas, every mip level. - fn draw(&self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, world: &JavaWorld) { - let views: Vec = (0..8) - .map(|i| { - let t = world.sprites.get(i).unwrap_or(&world.sprites[0]); - t.create_view(&wgpu::TextureViewDescriptor { - dimension: Some(wgpu::TextureViewDimension::D2Array), - mip_level_count: Some(self.key.2), - ..Default::default() - }) - }) - .collect(); - let mut entries = vec![wgpu::BindGroupEntry { - binding: 0, - resource: self.uniform.as_entire_binding(), - }]; - entries.extend(views.iter().enumerate().map(|(i, v)| wgpu::BindGroupEntry { - binding: i as u32 + 1, - resource: wgpu::BindingResource::TextureView(v), - })); - let group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Java shader pack block atlas"), - layout: &self.layout, - entries: &entries, - }); - for (mip, view) in self.mips.iter().enumerate() { - let attachments = [Some(wgpu::RenderPassColorAttachment { - view, - resolve_target: None, - depth_slice: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - store: wgpu::StoreOp::Store, - }, - })]; - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Java shader pack block atlas"), - color_attachments: &attachments, - ..Default::default() - }); - pass.set_pipeline(&self.pipeline); - pass.set_bind_group(0, &group, &[]); - pass.draw(0..3, mip as u32..mip as u32 + 1); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// `module` validates and translates for Metal, Vulkan and DirectX 12. - fn backends(module: &naga::Module, label: &str) { - let info = naga::valid::Validator::new( - naga::valid::ValidationFlags::all(), - naga::valid::Capabilities::all(), - ) - .validate(module) - .unwrap_or_else(|e| panic!("{label}: {e:?}")); - naga::back::msl::write_string( - module, - &info, - // wgpu's Metal backend asks for MSL 2.x. - &naga::back::msl::Options { - lang_version: (2, 4), - ..Default::default() - }, - &naga::back::msl::PipelineOptions::default(), - ) - .unwrap_or_else(|e| panic!("{label} (MSL): {e}")); - naga::back::spv::write_vec(module, &info, &Default::default(), None) - .unwrap_or_else(|e| panic!("{label} (SPIR-V): {e}")); - for entry in &module.entry_points { - let mut hlsl = String::new(); - naga::back::hlsl::Writer::new( - &mut hlsl, - &naga::back::hlsl::Options::default(), - &naga::back::hlsl::PipelineOptions { - entry_point: Some((entry.stage, entry.name.clone())), - }, - ) - .write(module, &info, None) - .unwrap_or_else(|e| panic!("{label} {} (HLSL): {e}", entry.name)); - } - } - - /// A terrain program reading every attribute the glue fills, run after - /// each draw path's glue, and the atlas shader, on every backend. - #[test] - fn the_glue_feeds_a_terrain_program_on_every_backend() { - let program = "#version 120 -attribute vec4 mc_Entity; -attribute vec4 mc_midTexCoord; -attribute vec4 at_tangent; -attribute vec3 at_midBlock; -varying vec4 color; -varying vec2 uv; -varying vec2 lm; -varying vec3 normal; -varying float id; -void main() { - vec4 p = gl_ModelViewMatrix * gl_Vertex; - p.xyz += at_midBlock / 64.0 * 0.0 + at_tangent.xyz * 0.0; - gl_Position = gl_ProjectionMatrix * p; - color = gl_Color; - uv = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy + mc_midTexCoord.xy * 0.0; - lm = (gl_TextureMatrix[1] * gl_MultiTexCoord1).xy; - normal = gl_NormalMatrix * gl_Normal; - id = mc_Entity.x; -} -"; - for flat in [false, true] { - let mut link = horizon_shaderpack::translate::Link { - set: 2, - geometry: Some(glue(flat, Some(0.1))), - ..Default::default() - }; - let t = horizon_shaderpack::translate::translate( - horizon_shaderpack::Stage::Vertex, - program, - &mut link, - &|l| format!("line {l}"), - ) - .unwrap_or_else(|e| panic!("flat {flat}: {e}")); - for wanted in ["hz_vertex();", "mc_Entity = vec4(vec4(hz_block"] { - assert!(t.glsl.contains(wanted), "{wanted}\n{}", t.glsl); - } - // The glue's inputs are Horizon's vertex layout. - let inputs = t.module.entry_points[0].function.arguments.len(); - assert_eq!(inputs, if flat { 6 } else { 7 }, "{}", t.wgsl); - let reparsed = naga::front::wgsl::parse_str(&t.wgsl).unwrap(); - backends(&reparsed, &format!("terrain glue (flat {flat})")); - } - backends(&naga::front::wgsl::parse_str(ATLAS).unwrap(), "atlas"); - backends( - &naga::front::wgsl::parse_str(super::super::java_stages::BLIT).unwrap(), - "Java shader pack blits", - ); - } - - #[test] - fn sprites_take_their_blocks_ids() { - let layers: BTreeMap = [ - ("minecraft:block/oak_leaves", 0), - ("minecraft:block/water_still", 1), - ("minecraft:block/stone", 2), - ("minecraft:block/oak_log_top", 3), - ] - .into_iter() - .map(|(n, l)| (n.to_owned(), l)) - .collect(); - let ids: BTreeMap = [("oak_leaves", 10003), ("water", 8), ("oak_log", 7)] - .into_iter() - .map(|(n, i)| (n.to_owned(), i)) - .collect(); - assert_eq!(table(&layers, &ids), [2, 0, 10003, 8, -1, 7]); - assert_eq!(atlas_columns(5), 3); - assert_eq!(atlas_columns(0), 1); - } -} diff --git a/crates/horizon-client/src/gfx/pack_stages.rs b/crates/horizon-client/src/gfx/pack_stages.rs deleted file mode 100644 index a067812e8..000000000 --- a/crates/horizon-client/src/gfx/pack_stages.rs +++ /dev/null @@ -1,552 +0,0 @@ -//! A Horizon shader pack on the GPU (docs/shader-packs.md): each stage -//! (`shadow`, `gbuffer`, `deferred`, `composite`, `final`) is a full-screen -//! pass that reads the world's colour, depth, opaque depth, G-buffer normals -//! and materials, shadow map, noise texture and the previous stage's output, -//! and writes its own target; the last target goes to the screen through -//! `world_post` (copy or FXAA) under the hand and HUD. Only exists while a -//! pack is selected. -use crate::shader_pack::{Rgba, ShaderPack, VERTEX_ENTRY}; -use horizon_shaderpack::uniforms::SmoothFloat; - -/// The `Frame` uniform of the prelude (`shader_pack_prelude.wgsl`). -#[repr(C)] -#[derive(Clone, Copy, Default, bytemuck::Pod, bytemuck::Zeroable)] -pub struct FrameUniform { - pub view: [[f32; 4]; 4], - pub projection: [[f32; 4]; 4], - pub inverse_view: [[f32; 4]; 4], - pub inverse_projection: [[f32; 4]; 4], - pub camera_position: [f32; 4], - pub resolution: [f32; 4], - pub time: [f32; 4], - pub sun: [f32; 4], - pub moon: [f32; 4], - pub sky_color: [f32; 4], - pub fog_color: [f32; 4], - pub fog: [f32; 4], - pub weather: [f32; 4], - pub camera_state: [f32; 4], - pub view_params: [f32; 4], - pub dimension: [f32; 4], -} - -/// The API 2 `FrameExt` uniform of the prelude (`shader_pack_prelude.wgsl`). -#[repr(C)] -#[derive(Clone, Copy, Default, bytemuck::Pod, bytemuck::Zeroable)] -pub struct FrameExtUniform { - pub previous_view: [[f32; 4]; 4], - pub previous_projection: [[f32; 4]; 4], - pub shadow_view: [[f32; 4]; 4], - pub shadow_projection: [[f32; 4]; 4], - pub shadow_inverse_view: [[f32; 4]; 4], - pub shadow_inverse_projection: [[f32; 4]; 4], - pub previous_camera: [f32; 4], - pub shadow_params: [f32; 4], - pub environment: [f32; 4], -} - -/// Iris's `noisetex` without a `texture.noise`: 256² random RGB, opaque. -/// Horizon packs get the same one unless `[textures] noise` replaces it. -pub(super) fn noise() -> Rgba { - const SIZE: u32 = 256; - let mut seed = 0x2545_f491_u32; - let pixels = (0..SIZE * SIZE * 4) - .map(|i| { - seed ^= seed << 13; - seed ^= seed >> 17; - seed ^= seed << 5; - if i % 4 == 3 { 255 } else { (seed >> 24) as u8 } - }) - .collect(); - Rgba { - width: SIZE, - height: SIZE, - pixels, - } -} - -/// `image` as a sampled RGBA8 texture. -pub(super) fn rgba_view( - device: &wgpu::Device, - queue: &wgpu::Queue, - label: &str, - image: &Rgba, -) -> wgpu::TextureView { - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some(label), - size: wgpu::Extent3d { - width: image.width, - height: image.height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - view_formats: &[], - }); - queue.write_texture( - texture.as_image_copy(), - &image.pixels, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(image.width * 4), - rows_per_image: None, - }, - texture.size(), - ); - texture.create_view(&Default::default()) -} - -struct Pass { - pipeline: wgpu::RenderPipeline, - scale: f32, - target_format: wgpu::TextureFormat, - uniform: wgpu::Buffer, - uniform_ext: wgpu::Buffer, -} -/// A stage's output and the bindings that read the world into it. -struct Target { - size: [u32; 2], - view: wgpu::TextureView, - group: wgpu::BindGroup, -} - -pub struct PackStages { - pub id: String, - pub needs_shadow: bool, - /// The stages read the G-buffer (`build_gbuffer`) or the depth before - /// translucent geometry (`capture_opaque_depth`). - pub needs_gbuffer: bool, - pub needs_opaque_depth: bool, - pub shadow_res: u32, - pub shadow_dist: f32, - layout: wgpu::BindGroupLayout, - linear: wgpu::Sampler, - nearest: wgpu::Sampler, - repeat: wgpu::Sampler, - shadow_sampler: wgpu::Sampler, - noise_view: wgpu::TextureView, - passes: Vec, - /// Built for this world target size and `world_post` generation. - targets: Vec, - /// `world_post`'s bindings of the last stage's output. - output: Option, - world_size: [u32; 2], - post_generation: u64, - started: std::time::Instant, - last: Option, - frames: u32, - previous: Option<([[f32; 4]; 4], [[f32; 4]; 4], [f32; 3])>, - wetness: SmoothFloat, -} - -fn stage_format(name: Option<&str>, default: wgpu::TextureFormat) -> wgpu::TextureFormat { - match name.map(str::to_lowercase).as_deref() { - Some("rgba16float" | "rgba16f") => wgpu::TextureFormat::Rgba16Float, - Some("rgba8unorm" | "rgba8" | "rgba") => wgpu::TextureFormat::Rgba8Unorm, - Some("rgb10a2unorm" | "rgb10_a2") => wgpu::TextureFormat::Rgb10a2Unorm, - _ => default, - } -} - -impl PackStages { - /// The pipelines of a loaded (already validated) pack, or wgpu's error. - pub fn new( - device: &wgpu::Device, - queue: &wgpu::Queue, - format: wgpu::TextureFormat, - pack: &ShaderPack, - ) -> Result { - let texture = |binding, sample_type| wgpu::BindGroupLayoutEntry { - binding, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }; - let float = wgpu::TextureSampleType::Float { filterable: true }; - let sampler = |binding, ty| wgpu::BindGroupLayoutEntry { - binding, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(ty), - count: None, - }; - let uniform_entry = |binding| wgpu::BindGroupLayoutEntry { - binding, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }; - let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Shader pack stage"), - entries: &[ - texture(0, float), - texture(1, wgpu::TextureSampleType::Depth), - texture(2, float), - sampler(3, wgpu::SamplerBindingType::Filtering), - sampler(4, wgpu::SamplerBindingType::NonFiltering), - uniform_entry(5), - uniform_entry(6), - texture(7, float), - texture(8, float), - texture(9, wgpu::TextureSampleType::Depth), - texture(10, wgpu::TextureSampleType::Depth), - sampler(11, wgpu::SamplerBindingType::Comparison), - texture(12, float), - sampler(13, wgpu::SamplerBindingType::Filtering), - ], - }); - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Shader pack stage"), - bind_group_layouts: &[Some(&layout)], - ..Default::default() - }); - let constants = pack.constants(); - let scope = device.push_error_scope(wgpu::ErrorFilter::Validation); - let stage_count = pack.stages.len(); - let passes = pack - .stages - .iter() - .enumerate() - .map(|(idx, stage)| { - let is_last = idx + 1 == stage_count; - let target_format = if is_last { - format - } else { - stage_format(stage.format.as_deref(), format) - }; - let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some(&stage.file), - source: wgpu::ShaderSource::Wgsl(stage.source.as_str().into()), - }); - // Only the overrides a stage declares may be passed to it. - let constants: Vec<(&str, f64)> = constants - .iter() - .copied() - .filter(|(key, _)| declares_override(&stage.source, key)) - .collect(); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some(&stage.file), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &module, - entry_point: Some(VERTEX_ENTRY), - compilation_options: Default::default(), - buffers: &[], - }, - primitive: Default::default(), - depth_stencil: None, - multisample: Default::default(), - fragment: Some(wgpu::FragmentState { - module: &module, - entry_point: Some(&stage.entry), - compilation_options: wgpu::PipelineCompilationOptions { - constants: &constants, - ..Default::default() - }, - targets: &[Some(wgpu::ColorTargetState { - format: target_format, - blend: None, - write_mask: wgpu::ColorWrites::ALL, - })], - }), - multiview_mask: None, - cache: None, - }); - Pass { - pipeline, - scale: stage.scale, - target_format, - uniform: device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Shader pack frame"), - size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }), - uniform_ext: device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Shader pack frame ext"), - size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }), - } - }) - .collect(); - if let Some(error) = pollster::block_on(scope.pop()) { - return Err(error.to_string()); - } - let make_sampler = |filter, address, compare| { - device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("Shader pack"), - address_mode_u: address, - address_mode_v: address, - address_mode_w: address, - mag_filter: filter, - min_filter: filter, - compare, - ..Default::default() - }) - }; - let noise_view = match &pack.custom_noise { - Some(image) => rgba_view(device, queue, "Shader pack noise", image), - None => rgba_view(device, queue, "Shader pack noise", &noise()), - }; - let (shadow_res, shadow_dist) = pack.shadow_config(); - Ok(Self { - id: pack.id.clone(), - needs_shadow: pack.needs_shadow(), - needs_gbuffer: pack.needs_gbuffer(), - needs_opaque_depth: pack.needs_opaque_depth(), - shadow_res, - shadow_dist, - layout, - linear: make_sampler( - wgpu::FilterMode::Linear, - wgpu::AddressMode::ClampToEdge, - None, - ), - nearest: make_sampler( - wgpu::FilterMode::Nearest, - wgpu::AddressMode::ClampToEdge, - None, - ), - repeat: make_sampler(wgpu::FilterMode::Linear, wgpu::AddressMode::Repeat, None), - shadow_sampler: make_sampler( - wgpu::FilterMode::Linear, - wgpu::AddressMode::ClampToEdge, - Some(wgpu::CompareFunction::GreaterEqual), - ), - noise_view, - passes, - targets: Vec::new(), - output: None, - world_size: [0, 0], - post_generation: u64::MAX, - started: std::time::Instant::now(), - last: None, - frames: 0, - previous: None, - wetness: SmoothFloat::default(), - }) - } - - /// Run every stage over the world's `color` and `depth` (`size`) and - /// return `post`'s bindings of the last stage's output. - #[allow(clippy::too_many_arguments)] - pub fn run( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - color: &wgpu::TextureView, - depth: &wgpu::TextureView, - size: [u32; 2], - post: &super::world_post::Post, - mut uniform: FrameUniform, - eye_light: [u8; 2], - ) -> &wgpu::BindGroup { - if self.world_size != size - || self.post_generation != post.generation() - || self.targets.len() != self.passes.len() - { - self.world_size = size; - self.post_generation = post.generation(); - self.targets.clear(); - for pass in &self.passes { - let target_size = size.map(|v| ((v as f32 * pass.scale).round() as u32).max(1)); - let view = device - .create_texture(&wgpu::TextureDescriptor { - label: Some("Shader pack stage"), - size: wgpu::Extent3d { - width: target_size[0], - height: target_size[1], - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: pass.target_format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }) - .create_view(&Default::default()); - let previous = self.targets.last().map_or(color, |t: &Target| &t.view); - let group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Shader pack stage"), - layout: &self.layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(color), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(depth), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::TextureView(previous), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::Sampler(&self.linear), - }, - wgpu::BindGroupEntry { - binding: 4, - resource: wgpu::BindingResource::Sampler(&self.nearest), - }, - wgpu::BindGroupEntry { - binding: 5, - resource: pass.uniform.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 6, - resource: pass.uniform_ext.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 7, - resource: wgpu::BindingResource::TextureView(post.normal()), - }, - wgpu::BindGroupEntry { - binding: 8, - resource: wgpu::BindingResource::TextureView(post.material()), - }, - wgpu::BindGroupEntry { - binding: 9, - resource: wgpu::BindingResource::TextureView(post.opaque_depth()), - }, - wgpu::BindGroupEntry { - binding: 10, - resource: wgpu::BindingResource::TextureView(post.shadow_depth()), - }, - wgpu::BindGroupEntry { - binding: 11, - resource: wgpu::BindingResource::Sampler(&self.shadow_sampler), - }, - wgpu::BindGroupEntry { - binding: 12, - resource: wgpu::BindingResource::TextureView(&self.noise_view), - }, - wgpu::BindGroupEntry { - binding: 13, - resource: wgpu::BindingResource::Sampler(&self.repeat), - }, - ], - }); - self.targets.push(Target { - size: target_size, - view, - group, - }); - } - let last = &self.targets.last().expect("a pack has a stage").view; - self.output = Some(post.bind(device, last)); - } - let now = std::time::Instant::now(); - let dt = self.last.map_or(0.0, |t| (now - t).as_secs_f32()); - self.last = Some(now); - let wet = self.wetness.update(uniform.weather[0], 60.0, 120.0, dt); - let cam = [ - uniform.camera_position[0], - uniform.camera_position[1], - uniform.camera_position[2], - ]; - let (prev_view, prev_proj, prev_cam) = - self.previous - .unwrap_or((uniform.view, uniform.projection, cam)); - self.previous = Some((uniform.view, uniform.projection, cam)); - let shadow_view = post.shadow_view; - let shadow_proj = post.shadow_proj; - let ext = FrameExtUniform { - previous_view: prev_view, - previous_projection: prev_proj, - shadow_view: shadow_view.to_cols_array_2d(), - shadow_projection: shadow_proj.to_cols_array_2d(), - shadow_inverse_view: shadow_view.inverse().to_cols_array_2d(), - shadow_inverse_projection: shadow_proj.inverse().to_cols_array_2d(), - previous_camera: [prev_cam[0], prev_cam[1], prev_cam[2], dt], - shadow_params: [ - post.shadow_resolution as f32, - post.shadow_distance, - 0.0, - if post.shadow_active { 1.0 } else { 0.0 }, - ], - environment: [ - f32::from(eye_light[0]) * 16.0, - f32::from(eye_light[1]) * 16.0, - wet, - 0.0, - ], - }; - uniform.time[0] = self.started.elapsed().as_secs_f32(); - uniform.time[1] = self.frames as f32; - self.frames = self.frames.wrapping_add(1); - for (pass, target) in self.passes.iter().zip(&self.targets) { - let [w, h] = target.size.map(|v| v as f32); - uniform.resolution = [w, h, 1.0 / w, 1.0 / h]; - queue.write_buffer(&pass.uniform, 0, bytemuck::bytes_of(&uniform)); - queue.write_buffer(&pass.uniform_ext, 0, bytemuck::bytes_of(&ext)); - let attachments = [Some(wgpu::RenderPassColorAttachment { - view: &target.view, - resolve_target: None, - depth_slice: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - })]; - let mut render = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Shader pack stage"), - color_attachments: &attachments, - ..Default::default() - }); - render.set_pipeline(&pass.pipeline); - render.set_bind_group(0, &target.group, &[]); - render.draw(0..3, 0..1); - } - self.output.as_ref().expect("bound with the targets") - } -} - -/// `override ` appears in the source (a declaration, not just a use). -fn declares_override(source: &str, key: &str) -> bool { - source.lines().any(|line| { - line.trim_start() - .strip_prefix("override") - .and_then(|rest| rest.trim_start().strip_prefix(key)) - .is_some_and(|rest| rest.trim_start().starts_with([':', '='])) - }) -} - -#[cfg(test)] -mod tests { - #[test] - fn the_frame_uniform_matches_the_prelude() { - // 4 matrices and 12 vectors, as `struct Frame` in the prelude. - assert_eq!(std::mem::size_of::(), 4 * 64 + 12 * 16); - let prelude = crate::shader_pack::PRELUDE; - let body = prelude.split("struct Frame {").nth(1).unwrap(); - let body = &body[..body.find('}').unwrap()]; - assert_eq!(body.matches("mat4x4").count(), 4); - assert_eq!(body.matches("vec4").count(), 12); - } - #[test] - fn only_declared_overrides_are_passed() { - let source = - "override saturation: f32 = 1.0;\noverride vignette = true;\nlet x = saturation_2;"; - assert!(super::declares_override(source, "saturation")); - assert!(super::declares_override(source, "vignette")); - assert!(!super::declares_override(source, "saturation_2")); - assert!(!super::declares_override(source, "haze")); - } -} diff --git a/crates/horizon-client/src/gfx/world_post.rs b/crates/horizon-client/src/gfx/world_post.rs index cb3e4b41c..934fed6d6 100644 --- a/crates/horizon-client/src/gfx/world_post.rs +++ b/crates/horizon-client/src/gfx/world_post.rs @@ -1,163 +1,25 @@ -//! Resolution scale, FXAA, and shader-pack G-buffer/shadow targets (Horizon -//! settings): the world renders into its own target at the scaled size, then -//! one full-screen pass copies or anti-aliases it onto the screen, under the -//! hand and HUD. For a shader pack that reads them, `Post` also keeps: -//! - `opaque_depth`: the depth before translucent terrain (water, stained -//! glass, ice) draws, a Java pack's `depthtex1`/`depthtex2`; -//! - `normal` and `material`, an api 2 pack's `scene_normal` and -//! `scene_material`: surface normals and material properties -//! reconstructed from depth and scene colour; -//! - `shadow_depth`, an api 2 pack's: the sun/moon shadow map and its -//! light-space `shadow_view` and `shadow_proj` matrices. -//! -//! With none of them on, the world renders straight to the screen as before. - -use super::pack_stages::FrameUniform; -use glam::{Mat4, Vec3}; - -const GBUFFER_WGSL: &str = r#" -struct GbufferParams { - inverse_view: mat4x4, - inverse_projection: mat4x4, - resolution: vec4, - // x: eye block light (0..1), y: eye sky light (0..1), z: wetness (0..1), w: sun elevation - env: vec4, -} - -@group(0) @binding(0) var world_color: texture_2d; -@group(0) @binding(1) var world_depth: texture_depth_2d; -@group(0) @binding(2) var opaque_depth: texture_depth_2d; -@group(0) @binding(3) var nearest_sampler: sampler; -@group(0) @binding(4) var params: GbufferParams; - -struct VsOut { - @builtin(position) clip: vec4, - @location(0) uv: vec2, -} - -@vertex -fn vs_gbuffer(@builtin(vertex_index) i: u32) -> VsOut { - let p = vec2(f32((i << 1u) & 2u), f32(i & 2u)); - var out: VsOut; - out.clip = vec4(p * 2.0 - 1.0, 0.0, 1.0); - out.uv = vec2(p.x, 1.0 - p.y); - return out; -} - -struct GbufferOut { - @location(0) normal: vec4, - @location(1) material: vec4, -} - -fn world_pos(uv: vec2, d: f32) -> vec3 { - let v = params.inverse_projection * vec4(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, d, 1.0); - let view = v.xyz / max(v.w, 1e-6); - return (params.inverse_view * vec4(view, 1.0)).xyz; -} - -@fragment -fn fs_gbuffer(in: VsOut) -> GbufferOut { - let d_all = textureSampleLevel(world_depth, nearest_sampler, in.uv, 0); - let d_opq = textureSampleLevel(opaque_depth, nearest_sampler, in.uv, 0); - let color = textureSampleLevel(world_color, nearest_sampler, in.uv, 0.0); - var out: GbufferOut; - if d_all <= 0.0 && d_opq <= 0.0 { - out.normal = vec4(0.5, 1.0, 0.5, 0.0); - out.material = vec4(0.0, params.env.y, 0.0, 1.0); - return out; - } - let d = select(d_all, d_opq, d_opq > 0.0); - let step_uv = params.resolution.zw; - let p0 = world_pos(in.uv, d); - let uv_r = clamp(in.uv + vec2(step_uv.x, 0.0), vec2(0.0), vec2(1.0)); - let uv_l = clamp(in.uv - vec2(step_uv.x, 0.0), vec2(0.0), vec2(1.0)); - let uv_d = clamp(in.uv + vec2(0.0, step_uv.y), vec2(0.0), vec2(1.0)); - let uv_u = clamp(in.uv - vec2(0.0, step_uv.y), vec2(0.0), vec2(1.0)); - let dr = textureSampleLevel(opaque_depth, nearest_sampler, uv_r, 0); - let dl = textureSampleLevel(opaque_depth, nearest_sampler, uv_l, 0); - let dd = textureSampleLevel(opaque_depth, nearest_sampler, uv_d, 0); - let du = textureSampleLevel(opaque_depth, nearest_sampler, uv_u, 0); - let dx = select(world_pos(uv_r, dr) - p0, p0 - world_pos(uv_l, dl), abs(dl - d) < abs(dr - d)); - let dy = select(world_pos(uv_d, dd) - p0, p0 - world_pos(uv_u, du), abs(du - d) < abs(dd - d)); - let raw_n = cross(dy, dx); - let n_len = length(raw_n); - let n = select(vec3(0.0, 1.0, 0.0), raw_n / max(n_len, 1e-6), n_len > 1e-6); - // Reversed depth: closer translucent geometry has larger depth than opaque geometry. - let is_translucent = select(0.0, 1.0, d_all > d_opq + 1e-5); - let luma = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)); - let block_light = clamp(max(params.env.x, smoothstep(0.65, 0.98, luma)), 0.0, 1.0); - let roughness = select(clamp(0.85 - params.env.z * 0.45, 0.1, 1.0), 0.05, is_translucent > 0.5); - out.normal = vec4(n * 0.5 + 0.5, 1.0); - out.material = vec4(block_light, params.env.y, is_translucent, roughness); - return out; -} -"#; - -#[repr(C)] -#[derive(Clone, Copy, Default, bytemuck::Pod, bytemuck::Zeroable)] -struct GbufferUniform { - inverse_view: [[f32; 4]; 4], - inverse_projection: [[f32; 4]; 4], - resolution: [f32; 4], - env: [f32; 4], -} - -/// What the frame's shader pack reads beyond the world's colour and depth: -/// `Post` keeps these targets only while one does. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct Extras { - pub opaque_depth: bool, - pub gbuffer: bool, -} +//! Resolution scale and FXAA (Horizon settings): the world renders into its +//! own target at the scaled size, then one full-screen pass copies or +//! anti-aliases it onto the screen, under the hand and HUD. With neither on, +//! the world renders straight to the screen. struct WorldTarget { size: [u32; 2], - extras: Extras, color: wgpu::TextureView, - depth_tex: wgpu::Texture, depth: wgpu::TextureView, - /// With `extras.opaque_depth` or `extras.gbuffer` (which reads it). - opaque_depth: Option<(wgpu::Texture, wgpu::TextureView)>, - gbuffer: Option, group: wgpu::BindGroup, - opaque_captured: bool, -} - -struct Gbuffer { - normal: wgpu::TextureView, - material: wgpu::TextureView, - group: wgpu::BindGroup, -} - -struct ShadowTarget { - resolution: u32, - depth: wgpu::TextureView, } pub struct Post { layout: wgpu::BindGroupLayout, sampler: wgpu::Sampler, - nearest: wgpu::Sampler, copy: wgpu::RenderPipeline, fxaa: wgpu::RenderPipeline, - gbuffer_layout: wgpu::BindGroupLayout, - gbuffer_pipeline: wgpu::RenderPipeline, - gbuffer_uniform: wgpu::Buffer, format: wgpu::TextureFormat, /// World resolution relative to the window (0.5..=2.0). pub scale: f32, pub antialias: bool, target: Option, - shadow: Option, - /// Bumped whenever a target is made: bindings of the old ones are stale. - generation: u64, - fallback_shadow: wgpu::TextureView, - fallback_color: wgpu::TextureView, - pub shadow_view: Mat4, - pub shadow_proj: Mat4, - pub shadow_distance: f32, - pub shadow_resolution: u32, - pub shadow_active: bool, } impl Post { @@ -225,192 +87,31 @@ impl Post { min_filter: wgpu::FilterMode::Linear, ..Default::default() }); - let nearest = device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("Horizon post nearest"), - mag_filter: wgpu::FilterMode::Nearest, - min_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - - let gbuffer_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Horizon gbuffer"), - source: wgpu::ShaderSource::Wgsl(GBUFFER_WGSL.into()), - }); - let gbuffer_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Horizon gbuffer layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Depth, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Depth, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 3, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 4, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - ], - }); - let gbuffer_pipe_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Horizon gbuffer"), - bind_group_layouts: &[Some(&gbuffer_layout)], - ..Default::default() - }); - let gbuffer_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Horizon gbuffer"), - layout: Some(&gbuffer_pipe_layout), - vertex: wgpu::VertexState { - module: &gbuffer_shader, - entry_point: Some("vs_gbuffer"), - compilation_options: Default::default(), - buffers: &[], - }, - primitive: Default::default(), - depth_stencil: None, - multisample: Default::default(), - fragment: Some(wgpu::FragmentState { - module: &gbuffer_shader, - entry_point: Some("fs_gbuffer"), - compilation_options: Default::default(), - targets: &[ - Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba8Unorm, - blend: None, - write_mask: wgpu::ColorWrites::ALL, - }), - Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba8Unorm, - blend: None, - write_mask: wgpu::ColorWrites::ALL, - }), - ], - }), - multiview_mask: None, - cache: None, - }); - let gbuffer_uniform = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Horizon gbuffer uniform"), - size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let fallback_shadow = device - .create_texture(&wgpu::TextureDescriptor { - label: Some("Horizon fallback shadow"), - size: wgpu::Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Depth32Float, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }) - .create_view(&Default::default()); - let fallback_color = device - .create_texture(&wgpu::TextureDescriptor { - label: Some("Horizon fallback gbuffer"), - size: wgpu::Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }) - .create_view(&Default::default()); Self { copy: pipeline("fs_copy"), fxaa: pipeline("fs_fxaa"), layout, sampler, - nearest, - gbuffer_layout, - gbuffer_pipeline, - gbuffer_uniform, format, scale: 1.0, antialias: false, target: None, - shadow: None, - generation: 0, - fallback_shadow, - fallback_color, - shadow_view: Mat4::IDENTITY, - shadow_proj: Mat4::IDENTITY, - shadow_distance: 128.0, - shadow_resolution: 2048, - shadow_active: false, } } /// The world's colour and depth targets this frame, or `None` to draw - /// it straight to the screen. `pack`: a shader pack runs this frame, - /// reading `extras`. + /// it straight to the screen. pub fn prepare( &mut self, device: &wgpu::Device, window: [u32; 2], - pack: bool, - extras: Extras, ) -> Option<(wgpu::TextureView, wgpu::TextureView)> { - if self.scale == 1.0 && !self.antialias && !pack { + if self.scale == 1.0 && !self.antialias { self.target = None; - self.shadow_active = false; return None; } let size = window.map(|v| ((v as f32 * self.scale).round() as u32).clamp(1, 8192)); - if self - .target - .as_ref() - .is_none_or(|t| t.size != size || t.extras != extras) - { - self.generation += 1; + if self.target.as_ref().is_none_or(|t| t.size != size) { let extent = wgpu::Extent3d { width: size[0], height: size[1], @@ -434,273 +135,26 @@ impl Post { wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, ) .create_view(&Default::default()); - let depth_tex = make_tex( + // TEXTURE_BINDING: occlusion culling reads it. + let depth = make_tex( "Scaled world depth", wgpu::TextureFormat::Depth32Float, - wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::COPY_SRC, - ); - let depth = depth_tex.create_view(&Default::default()); - let opaque_depth = (extras.opaque_depth || extras.gbuffer).then(|| { - let tex = make_tex( - "Opaque world depth", - wgpu::TextureFormat::Depth32Float, - wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - ); - let view = tex.create_view(&Default::default()); - (tex, view) - }); - let gbuffer = opaque_depth - .as_ref() - .filter(|_| extras.gbuffer) - .map(|(_, opaque)| { - let color_target = |label| { - make_tex( - label, - wgpu::TextureFormat::Rgba8Unorm, - wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - ) - .create_view(&Default::default()) - }; - let group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Horizon gbuffer"), - layout: &self.gbuffer_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&color), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&depth), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::TextureView(opaque), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::Sampler(&self.nearest), - }, - wgpu::BindGroupEntry { - binding: 4, - resource: self.gbuffer_uniform.as_entire_binding(), - }, - ], - }); - Gbuffer { - normal: color_target("World G-buffer normal"), - material: color_target("World G-buffer material"), - group, - } - }); + wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + ) + .create_view(&Default::default()); let group = self.bind(device, &color); self.target = Some(WorldTarget { size, - extras, color, - depth_tex, depth, - opaque_depth, - gbuffer, group, - opaque_captured: false, }); } - if let Some(t) = &mut self.target { - t.opaque_captured = false; - } self.target .as_ref() .map(|t| (t.color.clone(), t.depth.clone())) } - /// Prepare the directional sun/moon shadow map target and compute its - /// camera-relative orthographic `shadow_view` and `shadow_proj` matrices. - pub fn prepare_shadow( - &mut self, - device: &wgpu::Device, - resolution: u32, - distance: f32, - sun_dir: Vec3, - moon_dir: Vec3, - ) -> &wgpu::TextureView { - let resolution = resolution.clamp(256, 4096); - let distance = distance.clamp(16.0, 512.0); - if self - .shadow - .as_ref() - .is_none_or(|s| s.resolution != resolution) - { - let depth = device - .create_texture(&wgpu::TextureDescriptor { - label: Some("Horizon shadow map"), - size: wgpu::Extent3d { - width: resolution, - height: resolution, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Depth32Float, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }) - .create_view(&Default::default()); - self.shadow = Some(ShadowTarget { resolution, depth }); - self.generation += 1; - } - let light_dir = if sun_dir.y >= -0.05 { - sun_dir.normalize_or_zero() - } else { - moon_dir.normalize_or_zero() - }; - let dir = if light_dir.length_squared() > 1e-4 { - light_dir - } else { - Vec3::Y - }; - let up = if dir.y.abs() > 0.99 { Vec3::Z } else { Vec3::Y }; - self.shadow_view = Mat4::look_at_rh(dir * distance, Vec3::ZERO, up); - // Reversed-depth orthographic projection for the world's GreaterEqual - // pipelines (near maps to 1.0, far maps to 0.0), and standard 0..1 - // projection for shader sampling. - self.shadow_proj = Mat4::orthographic_rh( - -distance, - distance, - -distance, - distance, - distance * 2.5, - 0.05, - ); - self.shadow_distance = distance; - self.shadow_resolution = resolution; - self.shadow_active = true; - &self.shadow.as_ref().expect("created above").depth - } - - /// Snapshot `world_depth` into `opaque_depth` (`depthtex1`/`depthtex2`) - /// right before translucent water and weather draw. - pub fn capture_opaque_depth(&mut self, encoder: &mut wgpu::CommandEncoder) { - let Some(t) = &mut self.target else { return }; - let Some((opaque, _)) = &t.opaque_depth else { - return; - }; - encoder.copy_texture_to_texture( - t.depth_tex.as_image_copy(), - opaque.as_image_copy(), - t.depth_tex.size(), - ); - t.opaque_captured = true; - } - - /// Reconstruct the G-buffer `normal` and `material` textures after the - /// world pass finishes (with `Extras::gbuffer`). - pub fn build_gbuffer( - &mut self, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - frame: &FrameUniform, - eye_light: [u8; 2], - wetness: f32, - ) { - if !self.target.as_ref().is_some_and(|t| t.opaque_captured) { - self.capture_opaque_depth(encoder); - } - let Some(t) = &self.target else { return }; - let Some(gbuffer) = &t.gbuffer else { return }; - let [w, h] = t.size.map(|v| v.max(1) as f32); - let uniform = GbufferUniform { - inverse_view: frame.inverse_view, - inverse_projection: frame.inverse_projection, - resolution: [w, h, 1.0 / w, 1.0 / h], - env: [ - f32::from(eye_light[0]) / 15.0, - f32::from(eye_light[1]) / 15.0, - wetness, - frame.sun[1], - ], - }; - queue.write_buffer(&self.gbuffer_uniform, 0, bytemuck::bytes_of(&uniform)); - let attachments = [ - Some(wgpu::RenderPassColorAttachment { - view: &gbuffer.normal, - resolve_target: None, - depth_slice: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - }), - Some(wgpu::RenderPassColorAttachment { - view: &gbuffer.material, - resolve_target: None, - depth_slice: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - }), - ]; - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("Horizon gbuffer"), - color_attachments: &attachments, - ..Default::default() - }); - pass.set_pipeline(&self.gbuffer_pipeline); - pass.set_bind_group(0, &gbuffer.group, &[]); - pass.draw(0..3, 0..1); - } - - /// The world target's size, once `prepare` made one. - pub fn size(&self) -> Option<[u32; 2]> { - self.target.as_ref().map(|t| t.size) - } - - /// Changes whenever `prepare` or `prepare_shadow` makes new targets: - /// bindings made from the views below must be made again. - pub fn generation(&self) -> u64 { - self.generation - } - - /// The depth before translucent geometry (`capture_opaque_depth`), or - /// the world's depth when nothing reads it. - pub fn opaque_depth(&self) -> &wgpu::TextureView { - match &self.target { - Some(WorldTarget { - opaque_depth: Some((_, view)), - .. - }) => view, - Some(t) => &t.depth, - None => &self.fallback_shadow, - } - } - - /// Reconstructed world-space normals (`scene_normal`). - pub fn normal(&self) -> &wgpu::TextureView { - self.gbuffer().map_or(&self.fallback_color, |g| &g.normal) - } - - /// Reconstructed material and light (`scene_material`). - pub fn material(&self) -> &wgpu::TextureView { - self.gbuffer().map_or(&self.fallback_color, |g| &g.material) - } - - fn gbuffer(&self) -> Option<&Gbuffer> { - self.target.as_ref()?.gbuffer.as_ref() - } - - /// The sun/moon shadow map (`shadow_depth` of api 2 packs). - pub fn shadow_depth(&self) -> &wgpu::TextureView { - self.shadow - .as_ref() - .map_or(&self.fallback_shadow, |s| &s.depth) - } - /// Bindings that copy (or anti-alias) `view` onto the screen in `finish_with`. pub fn bind(&self, device: &wgpu::Device, view: &wgpu::TextureView) -> wgpu::BindGroup { device.create_bind_group(&wgpu::BindGroupDescriptor { diff --git a/crates/horizon-client/src/graphics.rs b/crates/horizon-client/src/graphics.rs index ba492ece8..9eb79a477 100644 --- a/crates/horizon-client/src/graphics.rs +++ b/crates/horizon-client/src/graphics.rs @@ -252,9 +252,6 @@ pub struct Graphics { /// or behind other windows, and half the worker threads. Off by default; /// it changes when frames are drawn, never what they show or the ticks. pub battery: bool, - /// The Horizon shader pack in `shaderpacks/` (its folder name); empty: - /// none, the vanilla picture (docs/shader-packs.md). - pub shader_pack: String, /// The graphics API (Windows only; see `GraphicsApi`). pub api: GraphicsApi, } @@ -280,7 +277,6 @@ impl Default for Graphics { resolution_scale: 100, fxaa: false, battery: false, - shader_pack: String::new(), api: GraphicsApi::Auto, } } @@ -332,7 +328,6 @@ impl Graphics { "resolutionScale" => g.resolution_scale = number(g.resolution_scale).clamp(50, 200), "fxaa" => g.fxaa = flag(g.fxaa), "batteryMode" => g.battery = flag(g.battery), - "shaderPack" => g.shader_pack = value.to_string(), "graphicsApi" => g.api = GraphicsApi::from_name(value).unwrap_or_default(), "overlay" => { g.overlay = match value { @@ -348,7 +343,7 @@ impl Graphics { } pub fn serialize(&self) -> String { format!( - "preset:{}\nlodDistance:{}\nfarDetail:{}\ncaveCulling:{}\ncullLeaves:{}\nentityCulling:{}\nchunkUpdates:{}\nuploadBudgetKiB:{}\nsky:{}\nfog:{}\nweather:{}\nanimatedTextures:{}\npresentMode:{}\nframePacing:{}\nworkers:{}\noverlay:{}\nresolutionScale:{}\nfxaa:{}\nbatteryMode:{}\nshaderPack:{}\ngraphicsApi:{}\n", + "preset:{}\nlodDistance:{}\nfarDetail:{}\ncaveCulling:{}\ncullLeaves:{}\nentityCulling:{}\nchunkUpdates:{}\nuploadBudgetKiB:{}\nsky:{}\nfog:{}\nweather:{}\nanimatedTextures:{}\npresentMode:{}\nframePacing:{}\nworkers:{}\noverlay:{}\nresolutionScale:{}\nfxaa:{}\nbatteryMode:{}\ngraphicsApi:{}\n", self.preset.name(), self.lod_distance, self.far_detail.name(), @@ -375,7 +370,6 @@ impl Graphics { self.resolution_scale, bool_text(self.fxaa), bool_text(self.battery), - self.shader_pack, self.api.name(), ) } @@ -469,13 +463,17 @@ mod tests { resolution_scale: 75, fxaa: true, battery: true, - shader_pack: "horizon-sample".into(), api: GraphicsApi::Dx12, }; assert_eq!(Graphics::parse(&g.serialize()), g); assert_eq!(Graphics::parse(""), Graphics::default()); - // The old menus' Simple / Advanced choice is ignored. + // The old menus' Simple / Advanced choice is ignored, and so is the + // shader pack older versions saved. assert_eq!(Graphics::parse("level:advanced\n"), Graphics::default()); + assert_eq!( + Graphics::parse("shaderPack:Pack.zip\n"), + Graphics::default() + ); assert_eq!( Graphics::parse("workers:abc\nsky:maybe\ncullLeaves:maybe\nentityCulling:maybe\n"), Graphics::default() diff --git a/crates/horizon-client/src/graphics_ui.rs b/crates/horizon-client/src/graphics_ui.rs index 6617ef009..d9ea62c72 100644 --- a/crates/horizon-client/src/graphics_ui.rs +++ b/crates/horizon-client/src/graphics_ui.rs @@ -39,7 +39,6 @@ pub enum H { ResolutionScale, Antialiasing, BatteryMode, - ShaderPack, GraphicsApi, } @@ -112,18 +111,13 @@ pub fn rows(level: Level) -> Vec { "Waits for the display's refresh: no tearing, a little latency.", ); let gui = v(Opt::GuiScale, None, "Size of menus and the HUD."); - let shader_pack = h( - H::ShaderPack, - High, - "Redraws the world with a pack from the shaderpacks folder. Off is vanilla.", - ); let preset = h( H::Preset, None, "Sets the options below together. Changing one makes it Custom.", ); let mut rows = match level { - Level::Simple => vec![preset, render, lod, far, fov, fps, vsync, gui, shader_pack], + Level::Simple => vec![preset, render, lod, far, fov, fps, vsync, gui], Level::Advanced => vec![ Row::Header("Rendering"), render, @@ -251,7 +245,6 @@ pub fn rows(level: Level) -> Vec { None, "Lowers the frame rate when minimised, or idle too.", ), - shader_pack, Row::Header("Preferences"), v(Opt::ViewBobbing, None, "Sways the view while walking."), v(Opt::ChunkFade, None, "Fades newly loaded chunks in."), @@ -381,10 +374,6 @@ impl H { H::Antialiasing => labelled("Anti-aliasing", if g.fxaa { "FXAA" } else { "Off" }), H::BatteryMode => labelled("Battery Mode", on_off(g.battery)), H::GraphicsApi => labelled("Graphics API", g.api.label()), - H::ShaderPack => match g.shader_pack.as_str() { - "" => labelled("Shader Pack", "Off"), - id => labelled("Shader Pack", id), - }, H::Overlay => labelled( "Performance Overlay", match g.overlay { @@ -527,7 +516,6 @@ impl Item { | H::PresentMode | H::Workers | H::BatteryMode - | H::ShaderPack | H::GraphicsApi ) | Item::Vanilla( Opt::GuiScale @@ -557,8 +545,6 @@ pub enum Action { Changed, OpenAdvanced, OpenSimple, - /// The Shader Pack row: the Shader Packs page (where packs are picked). - OpenShaderPacks, Done, } @@ -784,9 +770,6 @@ impl GraphicsUi { } return match wd.target { Target::Button(action) => vec![Action::ButtonClick, action], - Target::Item(Item::Horizon(H::ShaderPack)) => { - vec![Action::ButtonClick, Action::OpenShaderPacks] - } Target::Item(item) => { if item.slider(s).is_some() { self.dragging = Some(item); @@ -994,44 +977,6 @@ mod tests { ); } - #[test] - fn the_shader_pack_row_is_off_by_default_and_outside_presets() { - let mut s = Settings::default(); - let row = Item::Horizon(H::ShaderPack); - assert_eq!(row.label(&s).plain(), "Shader Pack: Off"); - assert!( - rows(Level::Advanced) - .iter() - .any(|r| matches!(r, Row::Setting(x) if x.item == row)) - ); - s.horizon.shader_pack = "horizon-sample".into(); - assert_eq!(row.label(&s).plain(), "Shader Pack: horizon-sample"); - // The row opens the Shader Packs page, where packs are picked. - let mut ui = GraphicsUi::default(); - let find = |ui: &mut GraphicsUi| { - let (widgets, ..) = ui.layout(Level::Advanced, 854.0, 480.0); - widgets - .into_iter() - .find(|w| matches!(w.target, Target::Item(i) if i == row)) - .unwrap() - }; - ui.scroll = find(&mut ui).y - GraphicsUi::bounds(480.0).0 - 4.0; - let w = find(&mut ui); - assert_eq!( - ui.click( - Level::Advanced, - &mut s, - (w.x + 1.0, w.y + 1.0), - (854.0, 480.0), - [854, 480], - false, - false - ), - [Action::ButtonClick, Action::OpenShaderPacks] - ); - assert_eq!(s.horizon.shader_pack, "horizon-sample"); - } - #[test] fn presets_set_vanilla_and_horizon_options() { let mut s = Settings::default(); diff --git a/crates/horizon-client/src/java_pack.rs b/crates/horizon-client/src/java_pack.rs deleted file mode 100644 index 9ad60cf9c..000000000 --- a/crates/horizon-client/src/java_pack.rs +++ /dev/null @@ -1,472 +0,0 @@ -//! Java (OptiFine/Iris) shader packs in `shaderpacks/` (docs/shader-packs.md): -//! a folder holding `shaders/`, or a `.zip`. `horizon-shaderpack` translates -//! their full-screen programs (`begin`, `prepare`, `deferred`, `composite`, -//! `final`) to WGSL, and `gfx/java_stages.rs` runs them over Horizon's world -//! as Iris runs them over its own. Of the geometry programs, the terrain's -//! (`gbuffers_terrain`, `gbuffers_water` and their fallbacks) draw Horizon's -//! terrain (`gfx/java_terrain.rs`); the rest of the world is Horizon's. -use horizon_shaderpack::{ - JavaPack, - pipeline::{Directives, GeometryProgram, Pass, Program, TargetSettings}, - preprocess::Environment, - properties::{Blend, SkyDirectives}, - uniforms::CustomUniforms, -}; -use std::{ - collections::{BTreeMap, BTreeSet}, - path::Path, -}; - -/// The passes of one program folder (`world0`, or `""` for the pack's root). -pub struct JavaFolder { - pub folder: String, - pub targets: Vec, - /// `begin*`, `prepare*`, `deferred*`, `composite*`, in order. - pub passes: Vec, - /// How many of `passes` run before the world (`begin*`, `prepare*`): the - /// world's colour and depth enter `world_target` and `depthtex*` after them. - pub world_at: usize, - /// The colour target the world goes into, where the pack's terrain - /// program draws (MakeUp: colortex1). - pub world_target: u32, - /// The targets whose alternate side the world is written to. - pub flipped_after_prepare: BTreeSet, - pub final_pass: Option, - /// The targets whose alternate side holds the frame's last write. - pub flipped_at_end: BTreeSet, - /// `sunPathRotation`, `eyeBrightnessHalflife`, `wetnessHalflife` and - /// `drynessHalflife`. - pub directives: Directives, - /// The programs Horizon's solid and translucent terrain are drawn with, - /// each translated for instanced and flat draws (`variants[0]`). - pub terrain: Option, - pub water: Option, - /// How many of `passes` run before translucent terrain (`begin*`, - /// `prepare*`, `deferred*`). - pub translucent_at: usize, - pub flipped_after_translucent: BTreeSet, - /// Each draw buffer's blending in `terrain` and `water`. - pub terrain_blends: Vec>, - pub water_blends: Vec>, - /// The shadow map's programs over Horizon's solid and translucent - /// terrain (instanced draws only), drawn after `begin*`, into - /// `shadowcolor` targets set by `shadow_targets`. - pub shadow: Option, - pub shadow_water: Option, - pub shadow_targets: Vec, - pub shadow_blends: Vec>, - pub shadow_water_blends: Vec>, - /// How many of `passes` run before the shadow map (`begin*`). - pub shadow_at: usize, -} - -/// The key of `texture.noise` (`noisetex`) in [`JavaPasses::custom_textures`]. -pub const NOISE_KEY: &str = "noise"; - -/// The key of `texture..` in [`JavaPasses::custom_textures`] -/// for a pass of `program` (`world0/composite2`): Iris's texture stages, where -/// `final` reads the `composite` ones. -pub fn texture_key(program: &str, sampler: &str) -> String { - let name = program.rsplit('/').next().unwrap_or(program); - let stage = match name.trim_end_matches(|c: char| c.is_ascii_digit()) { - "final" => "composite", - stage => stage, - }; - format!("{stage}.{sampler}") -} - -/// A loaded Java pack. -pub struct JavaPasses { - pub folders: Vec, - /// `uniform.*` / `variable.*` from `shaders.properties`. - pub uniforms: CustomUniforms, - /// What the pack keeps of the vanilla sky (`clouds`, `sun`, `moon`). - pub sky: SkyDirectives, - /// `block.properties`: block name → `mc_Entity` id. - pub block_ids: BTreeMap, - /// `texture..` and `texture.noise` images, by - /// `"."` and [`NOISE_KEY`]. - pub custom_textures: BTreeMap, -} - -impl JavaPasses { - /// The folder Iris uses in `dimension` (0 overworld, -1 the Nether, 1 the - /// End): `world` when the pack has one, else `world0`, else the root. - pub fn folder_for(&self, dimension: i8) -> Option { - let find = |name: &str| self.folders.iter().position(|f| f.folder == name); - find(&format!("world{dimension}")) - .or_else(|| find("world0")) - .or_else(|| find("")) - } -} - -/// A folder holding `shaders/`, or a zip. -pub fn is_java_pack(path: &Path) -> bool { - path.join("shaders").is_dir() - || path.is_file() - && path - .extension() - .is_some_and(|e| e.eq_ignore_ascii_case("zip")) -} - -/// The `MC_GL_VENDOR_*` name of a wgpu adapter (`AdapterInfo::vendor`, a -/// PCI id; Apple's GPUs report 0). -pub fn vendor_name(vendor: u32, adapter: &str) -> &'static str { - match vendor { - 0x10de => "NVIDIA Corporation", - 0x1002 | 0x1022 => "AMD", - 0x8086 => "Intel", - _ if adapter.starts_with("Apple") => "Apple", - _ => "Unknown", - } -} - -/// Open the pack at `path` and translate its full-screen passes with the -/// player's `values`; `vendor` and `renderer` name the GPU to the pack. -pub fn load( - path: &Path, - values: &BTreeMap, - vendor: &str, - renderer: &str, -) -> Result { - let pack = JavaPack::open(path)?; - let values = pack.values(values); - let env = Environment::iris(vendor, renderer, 4); - let properties = pack.properties(&values, &env)?; - let mut names: Vec<&str> = ["world0", "world-1", "world1"] - .into_iter() - .filter(|d| { - pack.programs - .iter() - .any(|p| p.starts_with(&format!("{d}/"))) - }) - .collect(); - if pack.programs.iter().any(|p| !p.contains('/')) { - names.push(""); - } - let mut folders = Vec::new(); - for name in names { - let plan = pack.passes(name, &values, &env, &properties)?; - let compile = |pass| { - let program = pack.compile(pass, &plan.targets, &values, &env, &properties)?; - if let Some((image, _)) = program.fragment.interface.images.first() { - return Err(format!( - "{}: images ({image}) aren't supported yet", - program.pass.program - )); - } - Ok::<_, String>(program) - }; - let early = plan.begin.iter().chain(&plan.prepare); - let passes = early - .clone() - .chain(&plan.deferred) - .chain(&plan.composite) - .map(compile) - .collect::, _>>()?; - let final_pass = plan.final_pass.as_ref().map(compile).transpose()?; - if passes.is_empty() && final_pass.is_none() { - continue; - } - // A geometry program that doesn't translate leaves that terrain to - // Horizon, with a warning. - let geometry_with = |pass: &Option, - alpha_test, - flat: &[bool]| { - let pass = pass.as_ref()?; - let glues: Vec<_> = flat - .iter() - .map(|&flat| crate::gfx::java_terrain::glue(flat, alpha_test)) - .collect(); - match pack.compile_geometry(pass, &plan.targets, &values, &env, &properties, &glues) { - Ok(program) => Some(program), - Err(e) => { - log::warn!("Horizon: shader pack {}: {e}", pass.program); - None - } - } - }; - let geometry = |pass: &Option, alpha_test| { - geometry_with(pass, alpha_test, &[false, true]) - }; - // Iris's alpha test of cutout terrain (`AlphaTests.ONE_TENTH_ALPHA`). - let terrain = geometry(&plan.terrain, Some(0.1)); - let water = terrain.as_ref().and_then(|_| geometry(&plan.water, None)); - // The shadow map draws only with the pack's terrain, and only from - // instanced draws (the flat draws hold what the camera sees). - let shadow = terrain - .as_ref() - .and_then(|_| geometry_with(&plan.shadow, Some(0.1), &[false])); - let shadow_water = shadow - .as_ref() - .and_then(|_| geometry_with(&plan.shadow_water, None, &[false])); - let translucent_at = early.clone().count() + plan.deferred.len(); - let blends = |g: &Option, default| { - g.as_ref().map_or_else(Vec::new, |g| { - pass_blends(&g.program.pass, &g.program.draw_buffers, default) - }) - }; - let terrain_blends = blends(&terrain, None); - // Iris's shadow programs don't blend (`BlendModeOverride.OFF`). - let shadow_blends = blends(&shadow, None); - let shadow_water_blends = blends(&shadow_water, None); - // Vanilla's translucent blending (`BlendFunction.TRANSLUCENT`). - let water_blends = blends( - &water, - Some(wgpu::BlendState { - color: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - }), - ); - folders.push(JavaFolder { - folder: name.to_owned(), - targets: plan.targets, - world_at: early.count(), - world_target: plan.world_target, - passes, - flipped_after_prepare: plan.flipped_after_prepare, - final_pass, - flipped_at_end: plan.flipped_at_end, - directives: plan.directives, - terrain, - water, - translucent_at, - flipped_after_translucent: plan.flipped_after_translucent, - terrain_blends, - water_blends, - shadow, - shadow_water, - shadow_targets: plan.shadow_targets, - shadow_blends, - shadow_water_blends, - shadow_at: plan.begin.len(), - }); - } - if folders.is_empty() { - return Err("no deferred, composite or final programs".into()); - } - let mut custom_textures = BTreeMap::new(); - for tex in properties.custom_textures() { - let key = match tex.stage { - Some(stage) => format!("{stage}.{}", tex.sampler), - None if tex.sampler == NOISE_KEY => NOISE_KEY.to_owned(), - None => continue, - }; - // Only PNGs for now: resource locations and raw `.dat` textures are - // skipped and the sampler reads what it would without them. - let image = pack - .files - .read_bytes(&tex.path) - .ok_or_else(|| "not in the pack".to_owned()) - .and_then(|bytes| crate::shader_pack::decode_png(&bytes)); - match image { - Ok(image) => { - custom_textures.insert(key, image); - } - Err(e) => log::warn!("Horizon: shader pack texture {}: {e}", tex.path), - } - } - let uniforms = properties.custom_uniforms(); - for problem in &uniforms.problems { - log::warn!("Horizon: shader pack custom uniform {problem}"); - } - let block_ids = pack.block_ids(&values, &env).unwrap_or_else(|e| { - log::warn!("Horizon: shader pack {e}"); - BTreeMap::new() - }); - Ok(JavaPasses { - folders, - uniforms, - sky: properties.sky(), - custom_textures, - block_ids, - }) -} - -/// Each draw buffer's blending, as Iris's `BlendModeOverride`: -/// `blend..`, else `blend.`, else `default` -/// (none for full-screen passes, vanilla's for the geometry programs). -pub(crate) fn pass_blends( - pass: &Pass, - draw_buffers: &[u32], - default: Option, -) -> Vec> { - let all = pass.blend.as_ref().map_or(default, blend_state); - draw_buffers - .iter() - .map(|i| { - pass.buffer_blends - .iter() - .find(|(b, _)| b == i) - .map_or(all, |(_, b)| blend_state(b)) - }) - .collect() -} - -/// A `blend.*` value as wgpu's blending (`off`: none). The factors are the -/// GL ones `ShaderProperties` accepts. -pub(crate) fn blend_state(blend: &Blend) -> Option { - let Blend::Factors(factors) = blend else { - return None; - }; - let factor = |name: &str| { - use wgpu::BlendFactor as F; - match name { - "ZERO" => F::Zero, - "SRC_COLOR" => F::Src, - "ONE_MINUS_SRC_COLOR" => F::OneMinusSrc, - "DST_COLOR" => F::Dst, - "ONE_MINUS_DST_COLOR" => F::OneMinusDst, - "SRC_ALPHA" => F::SrcAlpha, - "ONE_MINUS_SRC_ALPHA" => F::OneMinusSrcAlpha, - "DST_ALPHA" => F::DstAlpha, - "ONE_MINUS_DST_ALPHA" => F::OneMinusDstAlpha, - "SRC_ALPHA_SATURATE" => F::SrcAlphaSaturated, - _ => F::One, - } - }; - let component = |src: &str, dst: &str| wgpu::BlendComponent { - src_factor: factor(src), - dst_factor: factor(dst), - operation: wgpu::BlendOperation::Add, - }; - Some(wgpu::BlendState { - color: component(&factors[0], &factors[1]), - alpha: component(&factors[2], &factors[3]), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn custom_textures_belong_to_iris_s_stages() { - assert_eq!(texture_key("world0/composite2", "gaux1"), "composite.gaux1"); - assert_eq!(texture_key("final", "colortex4"), "composite.colortex4"); - assert_eq!( - texture_key("world-1/deferred10", "noisetex"), - "deferred.noisetex" - ); - assert_eq!(texture_key("prepare", "depthtex2"), "prepare.depthtex2"); - } - - #[test] - fn loads_the_full_screen_passes_of_a_java_pack() { - let dir = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../horizon-shaderpack/tests/fixtures/passes"); - assert!(is_java_pack(&dir)); - let pack = load(&dir, &BTreeMap::new(), "Apple", "Apple M4 Pro").unwrap(); - let [folder] = &pack.folders[..] else { - panic!("one folder, the root"); - }; - assert_eq!(folder.folder, ""); - assert_eq!( - folder - .passes - .iter() - .map(|p| p.pass.program.as_str()) - .collect::>(), - ["deferred", "composite", "composite1"] - ); - assert_eq!(folder.world_at, 0); - assert!(folder.final_pass.is_some()); - // The overworld, the Nether and the End all use the root. - assert_eq!([-1, 0, 1].map(|d| pack.folder_for(d)), [Some(0); 3]); - assert_eq!( - vendor_name(0x10de, "NVIDIA GeForce RTX 4080"), - "NVIDIA Corporation" - ); - assert_eq!(vendor_name(0, "Apple M4 Pro"), "Apple"); - } - - /// wgpu is given the WGSL text: it must parse again. - fn reparse_error(pack: &JavaPasses) -> Option { - let passes = pack - .folders - .iter() - .flat_map(|f| f.passes.iter().chain(&f.final_pass)); - let geometry = pack.folders.iter().flat_map(|f| { - f.terrain - .iter() - .chain(&f.water) - .chain(&f.shadow) - .chain(&f.shadow_water) - }); - passes - .flat_map(|p| [(p, &p.vertex, "vsh"), (p, &p.fragment, "fsh")]) - .chain(geometry.flat_map(|g| { - let p = &g.program; - [(p, &p.vertex, "vsh"), (p, &p.fragment, "fsh")] - .into_iter() - .chain(g.variants.iter().map(move |v| (p, v, "vsh (flat)"))) - })) - .find_map(|(p, t, ext)| { - let e = naga::front::wgsl::parse_str(&t.wgsl).err()?; - Some(format!( - "{}.{ext}: {}", - p.pass.program, - e.emit_to_string(&t.wgsl) - )) - }) - } - - /// The packs in `HORIZON_TEST_SHADERPACKS` (folders or zips) load as the - /// game loads them, with their default options: the compatibility table - /// of docs/shader-packs.md. Skipped when the variable isn't set. - #[test] - fn real_packs_load() { - let Some(dir) = std::env::var_os("HORIZON_TEST_SHADERPACKS") else { - return; - }; - let mut entries: Vec<_> = std::fs::read_dir(dir) - .unwrap() - .flatten() - .map(|e| e.path()) - .filter(|p| is_java_pack(p)) - .collect(); - entries.sort(); - let mut failures = Vec::new(); - for path in entries { - let name = path.file_name().unwrap().to_string_lossy().into_owned(); - match load(&path, &BTreeMap::new(), "Apple", "Apple M4 Pro") { - Ok(pack) if let Some(e) = reparse_error(&pack) => { - println!("{name}: {e}"); - failures.push(name); - } - Ok(pack) => { - let folders: Vec = pack - .folders - .iter() - .map(|f| { - let n = f.passes.len() + usize::from(f.final_pass.is_some()); - let geometry: Vec<&str> = f - .terrain - .iter() - .chain(&f.water) - .chain(&f.shadow) - .chain(&f.shadow_water) - .map(|g| g.program.pass.program.as_str()) - .collect(); - format!("{} {n} passes, terrain {geometry:?}", f.folder) - }) - .collect(); - println!("{name}: loads ({})", folders.join(", ")); - } - Err(e) => { - println!("{name}: {e}"); - failures.push(name); - } - } - } - assert!(failures.is_empty(), "{failures:?}"); - } -} diff --git a/crates/horizon-client/src/lib.rs b/crates/horizon-client/src/lib.rs index dddf87c0d..3c741e963 100644 --- a/crates/horizon-client/src/lib.rs +++ b/crates/horizon-client/src/lib.rs @@ -95,7 +95,6 @@ pub mod item_data; pub mod item_render; pub mod item_tooltip; pub mod items; -pub mod java_pack; pub mod java_text; pub mod key_names; pub mod keybinds; @@ -159,7 +158,6 @@ pub mod screen_layout; pub mod sculk; pub use horizon_server_files as server; pub mod settings; -pub mod shader_pack; pub mod shadows; pub mod signs; pub mod skins; diff --git a/crates/horizon-client/src/options_ui.rs b/crates/horizon-client/src/options_ui.rs index 66ceb93e1..352ebf26e 100644 --- a/crates/horizon-client/src/options_ui.rs +++ b/crates/horizon-client/src/options_ui.rs @@ -10,7 +10,6 @@ use crate::{ keybinds::{self, CATEGORIES, KeyBinds, MAPPINGS}, lang::{self, LanguageInfo}, settings::{SOUND_SOURCES, Settings}, - shader_pack::PackConfig, text::{self, Style, Text}, ui, }; @@ -863,10 +862,6 @@ pub enum Page { Font, Packs, Mods, - /// The shader packs in `shaderpacks/`: pick one, open the folder. - ShaderPacks, - /// Interactive options and profiles for the active shader pack. - ShaderPackOptions, /// Horizon's graphics settings (`graphics_ui`). Graphics, GraphicsAdvanced, @@ -898,17 +893,9 @@ enum Act { ResetPadKeys, ToggleMod(usize), OpenModFolder, - /// Pick shader pack `i` of `OptionsUi::shader_packs` (again: off). - ToggleShaderPack(usize), - OpenShaderPackFolder, /// Credits & Attribution's Credits button (WinScreen without the poem). Credits, CopyAgentDetails, - ShaderPackProfile, - ShaderPackSubscreen(String), - ShaderPackBackScreen, - ShaderPackOption(usize), - ResetShaderPackOptions, } enum WKind { Button { @@ -921,11 +908,6 @@ enum WKind { value: f32, opt: Opt, }, - ShaderOptionSlider { - label: Text, - value: f32, - index: usize, - }, Text { text: Text, color: u32, @@ -961,15 +943,10 @@ pub enum Event { Packs(Vec), OpenPackFolder, OpenModFolder, - /// Create `shaderpacks/` if needed and open it. - OpenShaderPackFolder, /// Roll the credits (`CreditsAndAttributionScreen.openCreditsScreen`). Credits, /// Copy how an agent connects (the AI Agent page). CopyAgentDetails, - /// The shader pack settings page closed with changes: they are saved, - /// the pack is built again with them. - ReloadShaderPack, } /// `SliderableValueSet.applyValueImmediately`: these apply 600 ms after /// the last change (`OptionInstanceSliderButton.delayedApplyAt`). @@ -1021,19 +998,8 @@ pub struct OptionsUi { /// Scroll the selected language into the middle on the next layout. center_language: bool, pub graphics: crate::graphics_ui::GraphicsUi, - /// `shaderpacks/` and its packs, read while the Shader Packs page shows. - pub shader_packs: crate::shader_pack::Folder, - /// The pack that failed to load last and why (set by the game). - pub shader_pack_error: Option<(String, String)>, /// The AI Agent page's status lines (set by the game while it shows). pub agent_status: Vec, - /// Loaded interactive configuration for the currently selected shader pack. - pub shader_pack_config: Option, - /// The shader pack option slider being dragged. - pub(crate) dragging_shader_option: Option, - /// `shader_pack_config` changed since the settings page opened: leaving - /// it saves the values and reloads the pack. - shader_pack_edited: bool, } impl Default for OptionsUi { fn default() -> Self { @@ -1062,12 +1028,7 @@ impl Default for OptionsUi { in_world: false, center_language: false, graphics: Default::default(), - shader_packs: Default::default(), - shader_pack_error: None, agent_status: Vec::new(), - shader_pack_config: None, - dragging_shader_option: None, - shader_pack_edited: false, } } } @@ -1114,14 +1075,11 @@ fn option_widget(s: &Settings, opt: Opt, x: f32, y: f32, w: f32) -> Widget { /// What a button opening one of Horizon's pages says on hover. fn horizon_page_tooltip(page: Page) -> Option<&'static str> { Some(match page { - Page::Horizon => { - "Horizon's own settings: graphics, shader packs, controllers, extras and mods." - } + Page::Horizon => "Horizon's own settings: graphics, controllers, extras and mods.", Page::Graphics => "Graphics, simply: each setting says what it costs.", Page::GraphicsAdvanced => { "Every graphics setting, Horizon's included, with what each costs." } - Page::ShaderPacks => "Packs that redraw the world. Off is the vanilla look.", Page::Controller => "Controllers: their buttons, sticks, vibration and menu navigation.", Page::Extras => { "Optional additions, all off by default: the game stays vanilla until you turn one on." @@ -1196,105 +1154,6 @@ impl OptionsUi { self.graphics.reset(); } } - /// Pick shader pack `i` of the folder, or turn it off when it is the - /// one running. - pub(crate) fn toggle_shader_pack(&mut self, s: &mut Settings, i: usize) { - let Some((id, Ok(_))) = self.shader_packs.packs.get(i) else { - return; - }; - let g = &mut s.horizon; - g.shader_pack = if g.shader_pack == *id { - String::new() - } else { - id.clone() - }; - self.sync_shader_pack_config(s); - } - /// Ensure `shader_pack_config` matches `s.horizon.shader_pack`. - pub fn sync_shader_pack_config(&mut self, s: &Settings) { - let id = s.horizon.shader_pack.as_str(); - if id.is_empty() { - self.shader_pack_config = None; - return; - } - if self - .shader_pack_config - .as_ref() - .is_some_and(|cfg| cfg.id == id) - { - return; - } - // A pack whose options can't be read has none, rather than being - // read again every frame. - let config = PackConfig::load(&self.shader_pack_dir(), id).unwrap_or_else(|e| { - log::warn!("Horizon: shader pack {id} options: {e}"); - PackConfig { - id: id.to_owned(), - ..Default::default() - } - }); - self.shader_pack_config = Some(config); - self.shader_pack_edited = false; - } - /// The picked pack has options or profiles to show. - fn shader_pack_has_settings(&self, s: &Settings) -> bool { - self.shader_pack_config.as_ref().is_some_and(|c| { - !s.horizon.shader_pack.is_empty() - && c.id == s.horizon.shader_pack - && (!c.options.is_empty() || !c.profiles.is_empty()) - }) - } - fn shader_pack_dir(&self) -> std::path::PathBuf { - if self.shader_packs.dir.as_os_str().is_empty() { - crate::shader_pack::folder() - } else { - self.shader_packs.dir.clone() - } - } - /// A file or folder dropped on the window: on the Shader Packs page (as - /// Iris's `ShaderPackScreen.onFilesDrop`), a pack copied into - /// `shaderpacks/` and picked. `None` elsewhere; the caller saves the - /// settings and loads the pack on `Some(Ok)`. - pub fn drop_shader_pack( - &mut self, - s: &mut Settings, - path: &std::path::Path, - ) -> Option> { - if self.page() != Page::ShaderPacks { - return None; - } - let dir = self.shader_pack_dir(); - Some(crate::shader_pack::import_pack(path, &dir).map(|id| { - self.shader_packs = Default::default(); - self.shader_packs.poll(dir); - s.horizon.shader_pack = id; - self.sync_shader_pack_config(s); - })) - } - /// Change the shader pack's options, applied when its page closes. - fn edit_shader_pack(&mut self, edit: impl FnOnce(&mut PackConfig)) { - if let Some(cfg) = &mut self.shader_pack_config { - edit(cfg); - self.shader_pack_edited = true; - } - } - /// Save the active shader pack options to `shaderpacks/.txt`. - fn save_shader_pack_config(&self) { - if let Some(cfg) = &self.shader_pack_config - && let Err(e) = cfg.save(&self.shader_pack_dir()) - { - log::warn!("Horizon: shader pack {} options: {e}", cfg.id); - } - } - pub(crate) fn shader_pack_failed(&self, id: &str) -> bool { - self.shader_pack_error - .as_ref() - .is_some_and(|(e, _)| e == id) - } - /// Whether the Shader Packs page shows. - fn shader_packs_shown(&self) -> bool { - self.page() == Page::ShaderPacks - } /// The graphics screen level of `page`, if it is one. fn graphics_level(page: Page) -> Option { match page { @@ -1317,8 +1176,6 @@ impl OptionsUi { Page::Font => "options.font.title", Page::Packs => "resourcePack.title", Page::Mods => "Mods", - Page::ShaderPacks => "Shader Packs", - Page::ShaderPackOptions => "Shader Pack Settings", Page::Graphics | Page::GraphicsAdvanced => "options.videoTitle", Page::Controller => "Controller Settings", Page::PadBinds => "Controller Binds", @@ -1434,10 +1291,7 @@ impl OptionsUi { open("Graphics...", Page::Graphics), Some(open("Advanced Graphics...", Page::GraphicsAdvanced)), ), - Row::Buttons( - open("Shader Packs...", Page::ShaderPacks), - Some(open("Controller Settings...", Page::Controller)), - ), + Row::Buttons(open("Controller Settings...", Page::Controller), None), Row::Buttons( open("Extras...", Page::Extras), Some(open("Mods...", Page::Mods)), @@ -1485,16 +1339,6 @@ impl OptionsUi { Act::Push(Page::GraphicsAdvanced), )), )]; - rows.push(Row::Buttons( - button( - 0.0, - 0.0, - 310.0, - Text::literal("Shader Packs..."), - Act::Push(Page::ShaderPacks), - ), - None, - )); rows.push(Row::Header("options.video.display.header")); rows.extend(pairs(&[ Opt::MaxFps, @@ -1667,113 +1511,6 @@ impl OptionsUi { }) .collect() } - Page::ShaderPacks => { - let folder = &self.shader_packs; - let mut rows = vec![Row::Note(folder.dir.display().to_string())]; - if folder.packs.is_empty() { - rows.push(Row::Header( - "No shader packs: put a pack's folder or .zip in this folder", - )); - } - for (i, (id, info)) in folder.packs.iter().enumerate() { - let on = s.horizon.shader_pack == *id; - let mut label = Text::literal(format!("{id}: ")).append(on_off(on)); - if info.is_err() || self.shader_pack_failed(id) { - label = label.append(" (Failed)"); - } - let widget = button(0.0, 0.0, 310.0, label, Act::ToggleShaderPack(i)); - rows.push(Row::Buttons( - if info.is_ok() { - widget - } else { - inactive(widget) - }, - None, - )); - } - rows - } - Page::ShaderPackOptions => { - let Some(cfg) = &self.shader_pack_config else { - return vec![Row::Header("No configurable options for this shader pack")]; - }; - let screen = cfg.current_screen(); - let mut rows = vec![Row::Note(format!("{} — {}", cfg.id, screen.label))]; - if cfg.active_screen.is_some() { - rows.push(Row::Buttons( - button( - 0.0, - 0.0, - 310.0, - Text::literal("Back to Main Screen"), - Act::ShaderPackBackScreen, - ), - None, - )); - } - let mut widgets = Vec::new(); - for item in &screen.items { - match item { - crate::shader_pack::ScreenItem::Empty => {} - crate::shader_pack::ScreenItem::Profile => { - let label = - Text::literal(format!("Profile: {}", cfg.active_profile_label())); - widgets.push(button(0.0, 0.0, 150.0, label, Act::ShaderPackProfile)); - } - crate::shader_pack::ScreenItem::Subscreen(sub, title) => { - let label = Text::literal(format!("{title}...")); - widgets.push(button( - 0.0, - 0.0, - 150.0, - label, - Act::ShaderPackSubscreen(sub.clone()), - )); - } - crate::shader_pack::ScreenItem::Option(idx) => { - if let Some(opt) = cfg.options.get(*idx) { - let label = Text::literal(format!( - "{}: {}", - opt.label, - opt.display_value() - )); - if let Some(value) = opt.slider_pos() { - widgets.push(Widget { - x: 0.0, - y: 0.0, - w: 150.0, - h: 20.0, - kind: WKind::ShaderOptionSlider { - label, - value, - index: *idx, - }, - list: false, - }); - } else { - widgets.push(button( - 0.0, - 0.0, - 150.0, - label, - Act::ShaderPackOption(*idx), - )); - } - } - } - } - } - let mut iter = widgets.into_iter(); - while let Some(mut first) = iter.next() { - if let Some(second) = iter.next() { - rows.push(Row::Buttons(first, Some(second))); - } else { - first.w = 310.0; - rows.push(Row::Buttons(first, None)); - } - } - rows - } _ => { let _ = s; Vec::new() @@ -1784,9 +1521,6 @@ impl OptionsUi { fn layout(&mut self, s: &Settings, w: f32, h: f32) -> (Vec, f32, f32, f32) { let mut out = Vec::new(); let page = self.page(); - if matches!(page, Page::ShaderPacks | Page::ShaderPackOptions) { - self.sync_shader_pack_config(s); - } let cx = (w / 2.0).floor(); if matches!(page, Page::Video | Page::Controls | Page::KeyBinds) { self.settings_search.x = cx - 100.0; @@ -2176,10 +1910,6 @@ impl OptionsUi { if matches!(page, Page::Video | Page::Controls) { top = 38.0; } - if page == Page::ShaderPacks { - // Two footer rows, as Iris's ShaderPackScreen. - bottom = h - 57.0; - } out.push(Widget { x: 0.0, y: if matches!(page, Page::Video | Page::Controls) { @@ -2266,42 +1996,13 @@ impl OptionsUi { } content = y + self.scroll - top + 2.0; out[list_start..].iter_mut().for_each(|w| w.list = true); - let folder = match page { - Page::Mods => Some(("Open Mod Folder", Act::OpenModFolder)), - Page::ShaderPackOptions => { - Some(("Reset to Defaults", Act::ResetShaderPackOptions)) - } - _ => None, - }; - if page == Page::ShaderPacks { - // Iris: the folder and the pack's settings above Done. - out.push(button( - cx - 154.0, - h - 51.0, - 150.0, - Text::literal("Open Shader Pack Folder"), - Act::OpenShaderPackFolder, - )); - let settings = button( - cx + 4.0, - h - 51.0, - 150.0, - Text::literal("Shader Pack Settings..."), - Act::Push(Page::ShaderPackOptions), - ); - out.push(if self.shader_pack_has_settings(s) { - settings - } else { - inactive(settings) - }); - out.push(done(h - 27.0)); - } else if let Some((label, act)) = folder { + if page == Page::Mods { out.push(button( cx - 154.0, h - 27.0, 150.0, - Text::literal(label), - act, + Text::literal("Open Mod Folder"), + Act::OpenModFolder, )); out.push(button(cx + 4.0, h - 27.0, 150.0, tr("gui.done"), Act::Done)); } else { @@ -2331,9 +2032,6 @@ impl OptionsUi { atlas: &HudAtlas, cursor: (f32, f32), ) { - if self.shader_packs_shown() { - self.shader_packs.poll(crate::shader_pack::folder()); - } let (w, h) = (batch.width, batch.height); if let Some(error) = &self.resource_error { ui::menu_background(batch, atlas, 0.0, 0.0, w, h, self.in_world); @@ -2462,16 +2160,6 @@ impl OptionsUi { 170.0, )); } - if let WKind::Button { - act: Act::ShaderPackOption(i), - .. - } = &wd.kind - && let Some(cfg) = &self.shader_pack_config - && let Some(opt) = cfg.options.get(*i) - && !opt.description.is_empty() - { - tooltip = Some(atlas.font.split(&Text::literal(&opt.description), 170.0)); - } } match &wd.kind { WKind::Button { label, active, act } => { @@ -2512,31 +2200,6 @@ impl OptionsUi { true, ); } - WKind::ShaderOptionSlider { - label, - value, - index, - } => { - let dragging = self.dragging_shader_option == Some(*index); - if hover - && let Some(cfg) = &self.shader_pack_config - && let Some(opt) = cfg.options.get(*index) - && !opt.description.is_empty() - { - tooltip = Some(atlas.font.split(&Text::literal(&opt.description), 170.0)); - } - ui::slider( - batch, - atlas, - wd.x, - wd.y, - wd.w, - label, - *value, - hover || dragging, - true, - ); - } WKind::Text { text, color, @@ -2669,7 +2332,6 @@ impl OptionsUi { Action::ButtonClick => events.push(Event::ButtonClick), Action::Changed => events.push(Event::Changed), Action::OpenAdvanced => self.push(Page::GraphicsAdvanced), - Action::OpenShaderPacks => self.push(Page::ShaderPacks), Action::OpenSimple => { if self.stack.len() >= 2 && self.stack[self.stack.len() - 2] == Page::Graphics @@ -2751,19 +2413,6 @@ impl OptionsUi { set_slider(opt, s, v); events.push(Event::Changed); } - if let Some(idx) = self.dragging_shader_option - && let Some(wd) = widgets.iter().find(|wd| { - matches!(&wd.kind, WKind::ShaderOptionSlider { index, .. } if *index == idx) - }) - { - let v = ui::slider_value(wd.x, wd.w, x); - self.edit_shader_pack(|cfg| { - if let Some(opt) = cfg.options.get_mut(idx) { - opt.set_slider_pos(v); - } - }); - events.push(Event::Changed); - } return events; } // Controller Binds: a click cancels the wait. @@ -2844,41 +2493,8 @@ impl OptionsUi { events.push(Event::Changed); } Act::OpenModFolder => events.push(Event::OpenModFolder), - Act::ToggleShaderPack(i) => { - self.toggle_shader_pack(s, i); - events.push(Event::Changed); - } - Act::OpenShaderPackFolder => events.push(Event::OpenShaderPackFolder), Act::Credits => events.push(Event::Credits), Act::CopyAgentDetails => events.push(Event::CopyAgentDetails), - Act::ShaderPackProfile => { - self.edit_shader_pack(|cfg| cfg.cycle_profile(false)); - events.push(Event::Changed); - } - Act::ShaderPackSubscreen(sub) => { - if let Some(cfg) = &mut self.shader_pack_config { - cfg.active_screen = Some(sub); - } - self.scroll = 0.0; - } - Act::ShaderPackBackScreen => { - if let Some(cfg) = &mut self.shader_pack_config { - cfg.active_screen = None; - } - self.scroll = 0.0; - } - Act::ShaderPackOption(i) => { - self.edit_shader_pack(|cfg| { - if let Some(opt) = cfg.options.get_mut(i) { - opt.cycle(false); - } - }); - events.push(Event::Changed); - } - Act::ResetShaderPackOptions => { - self.edit_shader_pack(PackConfig::reset_all); - events.push(Event::Changed); - } Act::None => {} } } @@ -2889,16 +2505,6 @@ impl OptionsUi { set_slider(opt, s, v); events.push(Event::Changed); } - WKind::ShaderOptionSlider { index, .. } => { - self.dragging_shader_option = Some(index); - let v = ui::slider_value(wd.x, wd.w, x); - self.edit_shader_pack(|cfg| { - if let Some(opt) = cfg.options.get_mut(index) { - opt.set_slider_pos(v); - } - }); - events.push(Event::Changed); - } WKind::LanguageRow { code, .. } => { if self.language_choice.as_deref() == Some(code.as_str()) { events.extend(self.back(s)); @@ -2927,7 +2533,7 @@ impl OptionsUi { } // AbstractSliderButton suppresses the press sound and plays once on // release, even if the pointer has left the widget or its value stayed. - let slider = self.dragging.take().is_some() || self.dragging_shader_option.take().is_some(); + let slider = self.dragging.take().is_some(); let graphics_slider = self.graphics.release(); if slider || graphics_slider { vec![Event::ButtonClick] @@ -2983,14 +2589,6 @@ impl OptionsUi { /// Escape or Done: leave the current page, applying page results. pub fn back(&mut self, s: &mut Settings) -> Vec { let mut events = Vec::new(); - if self.page() == Page::ShaderPackOptions - && let Some(cfg) = &mut self.shader_pack_config - && cfg.active_screen.is_some() - { - cfg.active_screen = None; - self.scroll = 0.0; - return events; - } let page = self.stack.pop().unwrap_or(Page::Main); self.scroll = 0.0; self.scrollbar_drag_y = None; @@ -3007,12 +2605,6 @@ impl OptionsUi { events.push(Event::Language(code)); } } - // Iris's ShaderPackScreen applies its edits on Done, so the - // pack is rebuilt once, not on every click or slider step. - Page::ShaderPackOptions if std::mem::take(&mut self.shader_pack_edited) => { - self.save_shader_pack_config(); - events.push(Event::ReloadShaderPack); - } Page::Packs => { // PackSelectionScreen.onClose: PackSelectionModel.commit. if let Some(p) = self.pack_screen.take() @@ -3337,177 +2929,6 @@ mod tests { assert!(ui.resource_error.is_none()); } - /// The Shader Packs page (Video Settings > Shader Packs..., or the - /// Shader Pack row of Advanced graphics): the folder's path, Open Shader - /// Pack Folder, and a switch per pack (#941, #925). - #[test] - fn classic_shader_packs_page_shows_the_folder_and_picks_a_pack() { - let atlas = atlas(); - let mut s = Settings::default(); - let mut ui = OptionsUi::default(); - let size = (480.0, 240.0); - let dir = std::env::temp_dir().join(format!("horizon-shaderpacks-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - let find = |ui: &mut OptionsUi, s: &Settings, act: &dyn Fn(&Act) -> bool| { - ui.layout(s, size.0, size.1) - .0 - .into_iter() - .find(|w| matches!(&w.kind, WKind::Button { act: a, active: true, .. } if act(a))) - }; - ui.show(Page::Video); - let open = find(&mut ui, &s, &|a| *a == Act::Push(Page::ShaderPacks)).unwrap(); - ui.click( - &mut s, - &atlas, - open.x + 1.0, - open.y + 1.0, - size, - [480, 240], - false, - false, - ); - assert_eq!(ui.page(), Page::ShaderPacks); - - ui.shader_packs.poll(dir.clone()); - let empty = ui.layout(&s, size.0, size.1).0; - let path = dir.display().to_string(); - for line in [path.as_str(), "No shader packs"] { - assert!( - empty.iter().any( - |w| matches!(&w.kind, WKind::Text { text, .. } if text.plain().contains(line)) - ), - "{line}" - ); - } - let folder = find(&mut ui, &s, &|a| *a == Act::OpenShaderPackFolder).unwrap(); - assert_eq!( - ui.click( - &mut s, - &atlas, - folder.x + 1.0, - folder.y + 1.0, - size, - [480, 240], - false, - false - ), - vec![Event::ButtonClick, Event::OpenShaderPackFolder] - ); - - std::fs::write(dir.join("Pack.zip"), b"").unwrap(); - ui.shader_packs = Default::default(); - ui.shader_packs.poll(dir.clone()); - let pack = find(&mut ui, &s, &|a| *a == Act::ToggleShaderPack(0)).unwrap(); - let events = ui.click( - &mut s, - &atlas, - pack.x + 1.0, - pack.y + 1.0, - size, - [480, 240], - false, - false, - ); - assert!(events.contains(&Event::Changed)); - assert_eq!(s.horizon.shader_pack, "Pack.zip"); - ui.shader_pack_error = Some(("Pack.zip".into(), "broken".into())); - let label = |ui: &mut OptionsUi, s: &Settings| match find(ui, s, &|a| { - *a == Act::ToggleShaderPack(0) - }) - .unwrap() - .kind - { - WKind::Button { label, .. } => label.plain(), - _ => unreachable!(), - }; - assert!( - label(&mut ui, &s).ends_with("(Failed)"), - "{}", - label(&mut ui, &s) - ); - ui.click( - &mut s, - &atlas, - pack.x + 1.0, - pack.y + 1.0, - size, - [480, 240], - false, - false, - ); - assert_eq!(s.horizon.shader_pack, "", "a second click turns it off"); - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn a_dropped_pack_is_picked_and_its_settings_apply_on_done() { - let atlas = atlas(); - let mut s = Settings::default(); - let mut ui = OptionsUi::default(); - let size = (480.0, 240.0); - let dir = - std::env::temp_dir().join(format!("horizon-shaderpack-drop-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - ui.shader_packs.poll(dir.clone()); - let sample = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../examples/shaderpacks/horizon-sample"); - assert_eq!( - ui.drop_shader_pack(&mut s, &sample), - None, - "only on its page" - ); - ui.show(Page::ShaderPacks); - assert_eq!(ui.drop_shader_pack(&mut s, &sample), Some(Ok(()))); - assert_eq!(s.horizon.shader_pack, "horizon-sample"); - assert!(dir.join("horizon-sample/pack.toml").is_file()); - let again = ui.drop_shader_pack(&mut s, &sample).unwrap().unwrap_err(); - assert!(again.contains("already"), "{again}"); - - let click = |ui: &mut OptionsUi, s: &mut Settings, act: &dyn Fn(&Act) -> bool| { - let w = ui - .layout(s, size.0, size.1) - .0 - .into_iter() - .find(|w| matches!(&w.kind, WKind::Button { act: a, active: true, .. } if act(a))) - .unwrap(); - ui.click( - s, - &atlas, - w.x + 1.0, - w.y + 1.0, - size, - [480, 240], - false, - false, - ) - }; - click(&mut ui, &mut s, &|a| { - *a == Act::Push(Page::ShaderPackOptions) - }); - assert_eq!(ui.page(), Page::ShaderPackOptions); - for _ in 0..3 { - let events = click(&mut ui, &mut s, &|a| matches!(a, Act::ShaderPackOption(_))); - assert!(!events.contains(&Event::ReloadShaderPack), "{events:?}"); - } - let txt = dir.join("horizon-sample.txt"); - assert!(!txt.exists(), "nothing is saved before Done"); - let events = click(&mut ui, &mut s, &|a| *a == Act::Done); - assert!(events.contains(&Event::ReloadShaderPack), "{events:?}"); - assert_eq!(ui.page(), Page::ShaderPacks); - assert!(txt.is_file(), "Done saves the values"); - click(&mut ui, &mut s, &|a| { - *a == Act::Push(Page::ShaderPackOptions) - }); - let events = click(&mut ui, &mut s, &|a| *a == Act::Done); - assert!( - !events.contains(&Event::ReloadShaderPack), - "no change: no reload" - ); - let _ = std::fs::remove_dir_all(&dir); - } - #[test] fn classic_mods_page_toggles_mods_and_keeps_dev_and_invalid_entries_inactive() { let atlas = atlas(); @@ -4618,7 +4039,6 @@ mod tests { [ Page::Graphics, Page::GraphicsAdvanced, - Page::ShaderPacks, Page::Controller, Page::Extras, Page::Mods, @@ -4652,7 +4072,7 @@ mod tests { // Vanilla's own pages keep the Horizon pages they had. ui.stack = vec![Page::Main, Page::Video]; let video = pushes(&mut ui); - for page in [Page::Graphics, Page::GraphicsAdvanced, Page::ShaderPacks] { + for page in [Page::Graphics, Page::GraphicsAdvanced] { assert!(video.contains(&page), "{page:?}"); } ui.stack = vec![Page::Main, Page::Controls]; diff --git a/crates/horizon-client/src/shader_pack.rs b/crates/horizon-client/src/shader_pack.rs deleted file mode 100644 index b1be9d3ea..000000000 --- a/crates/horizon-client/src/shader_pack.rs +++ /dev/null @@ -1,1488 +0,0 @@ -//! Horizon shader packs (docs/shader-packs.md): folders in `shaderpacks/` -//! holding a `pack.toml` and WGSL stages, plus Java (OptiFine/Iris) shader -//! packs in folders or `.zip` archives. Loading reads the manifest and the -//! player's option values, puts the prelude before each stage's source and -//! has naga parse and validate it, so a broken pack is reported (file, line, -//! column) and never reaches the GPU. -use horizon_shaderpack::{JavaPack, preprocess::Environment, properties::MenuElement}; -use serde::Deserialize; -use std::{ - collections::{BTreeMap, BTreeSet}, - fs, - path::{Path, PathBuf}, - time::SystemTime, -}; - -/// The stage and uniform contract this build provides. -pub const API: u32 = 2; -/// The folder beside `options.txt` that holds the packs. -pub const DIR: &str = "shaderpacks"; -pub const PRELUDE: &str = include_str!("shader_pack_prelude.wgsl"); -/// What api 2 adds after [`PRELUDE`] (`api = 2` packs only). -pub const PRELUDE_API2: &str = include_str!("shader_pack_prelude_api2.wgsl"); -/// [`PRELUDE_API2`]'s bindings that cost a pass when a stage reads them. -const BINDING_SCENE_NORMAL: u32 = 7; -const BINDING_SCENE_MATERIAL: u32 = 8; -const BINDING_OPAQUE_DEPTH: u32 = 9; -const BINDING_SHADOW_DEPTH: u32 = 10; -/// The prelude's vertex entry point, shared by every stage. -pub const VERTEX_ENTRY: &str = "horizon_stage_vertex"; -const MAX_STAGES: usize = 16; -const MAX_OPTIONS: usize = 64; - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct Manifest { - pack: Info, - #[serde(default)] - stage: Vec, - #[serde(default)] - options: BTreeMap, - #[serde(default)] - profiles: BTreeMap>, - #[serde(default)] - textures: BTreeMap, -} -#[derive(Clone, Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct Info { - pub name: String, - #[serde(default)] - pub description: String, - #[serde(default)] - pub authors: Vec, - #[serde(default)] - pub version: String, - #[serde(default)] - pub license: String, - pub api: u32, -} -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct StageEntry { - kind: String, - shader: String, - entry: Option, - scale: Option, - format: Option, -} -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct OptionEntry { - label: Option, - #[serde(default)] - description: String, - default: toml::Value, - min: Option, - max: Option, - step: Option, - values: Option>, -} - -/// Where a stage runs: `composite` only so far (docs/shader-packs.md, Stages). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum StageKind { - /// After the world pass, before the hand: reads the scene's colour and depth. - Composite, -} -#[derive(Clone, Debug)] -pub struct Stage { - pub kind: StageKind, - /// The stage's file, relative to the pack. - pub file: String, - /// The prelude followed by the stage's source. - pub source: String, - pub entry: String, - /// Target size relative to the world's. - pub scale: f32, - /// The target's format ([`STAGE_FORMATS`]); `None`: the screen's. - pub format: Option, - /// The prelude bindings (group 0) the stage reads. - pub bindings: BTreeSet, -} -/// The `format`s a stage's target can have. -pub const STAGE_FORMATS: [&str; 3] = ["rgba8unorm", "rgba16float", "rgb10a2unorm"]; -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum OptionKind { - Bool, - /// An `i32`, cycling through `values`. - Int, - /// An `f32` slider. - Float { - min: f64, - max: f64, - step: f64, - }, - /// A Java `#define` / `const` string option cycling through `choices` - /// (or drawn as a slider when `slider` is true and choices are numeric). - Choice { - slider: bool, - }, -} -#[derive(Clone, Debug, PartialEq)] -pub struct PackOption { - /// The WGSL `override` or GLSL `#define`/`const` name it sets. - pub key: String, - pub label: String, - pub description: String, - pub kind: OptionKind, - pub values: Vec, - pub default: f64, - pub value: f64, - /// Allowed `(raw_value, display_label)` pairs for `OptionKind::Choice`. - pub choices: Vec<(String, String)>, - pub default_choice: String, - pub current_choice: String, -} - -impl PackOption { - /// Formatted current value for UI buttons and labels. - pub fn display_value(&self) -> String { - match self.kind { - OptionKind::Bool => { - if self.value != 0.0 { - "ON".into() - } else { - "OFF".into() - } - } - OptionKind::Int => format!("{}", self.value.round() as i64), - OptionKind::Float { step, .. } => { - if step >= 1.0 { - format!("{:.0}", self.value) - } else if step >= 0.1 { - format!("{:.1}", self.value) - } else { - format!("{:.2}", self.value) - } - } - OptionKind::Choice { .. } => self - .choices - .iter() - .find(|(raw, _)| *raw == self.current_choice) - .map(|(_, label)| label.clone()) - .unwrap_or_else(|| self.current_choice.clone()), - } - } - - /// Normalized `0.0..=1.0` slider position if this option is a slider. - pub fn slider_pos(&self) -> Option { - match self.kind { - OptionKind::Float { min, max, .. } if max > min => { - Some(((self.value - min) / (max - min)).clamp(0.0, 1.0) as f32) - } - OptionKind::Choice { slider: true } if self.choices.len() > 1 => { - let idx = self - .choices - .iter() - .position(|(raw, _)| *raw == self.current_choice) - .unwrap_or(0); - Some(idx as f32 / (self.choices.len() - 1) as f32) - } - _ => None, - } - } - - /// Update the option from a normalized `0.0..=1.0` slider drag. - pub fn set_slider_pos(&mut self, pos: f32) { - let pos = f64::from(pos.clamp(0.0, 1.0)); - match self.kind { - OptionKind::Float { min, max, step } => { - let mut v = min + pos * (max - min); - if step > 0.0 { - v = (v / step).round() * step; - } - self.value = v.clamp(min, max); - } - OptionKind::Choice { slider: true } if !self.choices.is_empty() => { - let idx = (pos * (self.choices.len() - 1) as f64).round() as usize; - if let Some((raw, _)) = self.choices.get(idx.min(self.choices.len() - 1)) { - self.current_choice = raw.clone(); - } - } - _ => {} - } - } - - /// Cycle or toggle the option (`reverse`: right-click steps backwards). - pub fn cycle(&mut self, reverse: bool) { - match self.kind { - OptionKind::Bool => { - self.value = if self.value == 0.0 { 1.0 } else { 0.0 }; - } - OptionKind::Int if !self.values.is_empty() => { - let cur = self.value.round() as i64; - let idx = self.values.iter().position(|&v| v == cur).unwrap_or(0); - let next = if reverse { - (idx + self.values.len() - 1) % self.values.len() - } else { - (idx + 1) % self.values.len() - }; - self.value = self.values[next] as f64; - } - OptionKind::Float { min, max, step } => { - let delta = if step > 0.0 { step } else { (max - min) * 0.05 }; - let next = if reverse { - self.value - delta - } else { - self.value + delta - }; - self.value = next.clamp(min, max); - } - OptionKind::Choice { .. } if !self.choices.is_empty() => { - let idx = self - .choices - .iter() - .position(|(raw, _)| *raw == self.current_choice) - .unwrap_or(0); - let next = if reverse { - (idx + self.choices.len() - 1) % self.choices.len() - } else { - (idx + 1) % self.choices.len() - }; - self.current_choice = self.choices[next].0.clone(); - } - _ => {} - } - } - - /// Serialized string value for `shaderpacks/.txt`, or `None` if at default. - pub fn saved_string(&self) -> Option { - match self.kind { - OptionKind::Bool => { - let b = self.value != 0.0; - let def = self.default != 0.0; - (b != def).then(|| b.to_string()) - } - OptionKind::Int => { - let i = self.value.round() as i64; - let def = self.default.round() as i64; - (i != def).then(|| i.to_string()) - } - OptionKind::Float { .. } => { - ((self.value - self.default).abs() > 1e-6).then(|| format!("{}", self.value)) - } - OptionKind::Choice { .. } => { - (self.current_choice != self.default_choice).then(|| self.current_choice.clone()) - } - } - } - - /// Reset to the pack's default value. - pub fn reset(&mut self) { - self.value = self.default; - self.current_choice = self.default_choice.clone(); - } -} - -/// A named preset profile in a Horizon or Java shader pack (`Low`, `High`, `Ultra`). -#[derive(Clone, Debug, PartialEq)] -pub struct PackProfile { - pub name: String, - pub label: String, - pub values: BTreeMap, -} - -/// An item on a shader pack option screen (`shaders.properties` `screen`). -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ScreenItem { - Empty, - Profile, - Subscreen(String, String), - Option(usize), -} - -/// A laid-out option screen (main screen or sub-screen). -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct PackScreen { - pub name: String, - pub label: String, - pub columns: u32, - pub items: Vec, -} - -/// Interactive configuration metadata and current values for a shader pack, -/// usable directly by the Options UI without a GPU device. -#[derive(Clone, Debug, Default)] -pub struct PackConfig { - pub id: String, - pub options: Vec, - pub profiles: Vec, - pub main_screen: PackScreen, - pub subscreens: Vec, - /// Active subscreen name (`None` = main screen). - pub active_screen: Option, -} - -impl PackConfig { - /// Load the configurable options, profiles, and screens for pack `id` in `dir`. - pub fn load(dir: &Path, id: &str) -> Result { - if id.is_empty() { - return Err("no shader pack selected".into()); - } - let path = dir.join(id); - let saved = load_values(dir, id); - if path.join("pack.toml").is_file() { - let manifest = manifest(&path)?; - let mut options = Vec::new(); - for (key, entry) in manifest.options { - options.push(option(key, entry, &saved)?); - } - let mut profiles = Vec::new(); - for (name, map) in manifest.profiles { - let mut values = BTreeMap::new(); - for (k, v) in map { - let s = match v { - toml::Value::Boolean(b) => b.to_string(), - toml::Value::Integer(i) => i.to_string(), - toml::Value::Float(f) => f.to_string(), - toml::Value::String(s) => s, - _ => continue, - }; - values.insert(k, s); - } - profiles.push(PackProfile { - label: humanize_identifier(&name), - name, - values, - }); - } - let mut items = Vec::new(); - if !profiles.is_empty() { - items.push(ScreenItem::Profile); - } - items.extend((0..options.len()).map(ScreenItem::Option)); - return Ok(Self { - id: id.to_owned(), - options, - profiles, - main_screen: PackScreen { - name: String::new(), - label: manifest.pack.name, - columns: 2, - items, - }, - subscreens: Vec::new(), - active_screen: None, - }); - } - if crate::java_pack::is_java_pack(&path) { - return Self::load_java(&path, id, &saved); - } - Err("not a valid shader pack".into()) - } - - fn load_java(path: &Path, id: &str, saved: &BTreeMap) -> Result { - let pack = JavaPack::open(path)?; - let values = pack.values(saved); - // The menus don't know the GPU: `shaders.properties` is read as on an - // unknown one (the screens and profiles of the packs tested don't - // depend on it). - let env = Environment::iris("Unknown", "", 4); - let props = pack.properties(&values, &env).unwrap_or_default(); - let lang = pack.translations("en_us"); - let sliders: BTreeSet<&str> = props.sliders().into_iter().collect(); - - // Iris's names: the `.lang` entry, else the name itself; the tooltip - // from `.lang`, else the option's comment. - let named = |key: String, name: &str| lang.get(&key).cloned().unwrap_or(name.to_owned()); - let option_label = |name: &str| named(format!("option.{name}"), name); - let option_desc = |name: &str, comment: Option<&str>| -> String { - lang.get(&format!("option.{name}.comment")) - .cloned() - .or_else(|| comment.map(str::to_owned)) - .unwrap_or_default() - }; - - let mut options = Vec::new(); - let mut by_name = BTreeMap::new(); - for (name, b) in &pack.options.bools { - let current = values.bools.get(name).copied().unwrap_or(b.default); - let idx = options.len(); - options.push(PackOption { - key: name.clone(), - label: option_label(name), - description: option_desc(name, b.comment.as_deref()), - kind: OptionKind::Bool, - values: Vec::new(), - default: f64::from(u8::from(b.default)), - value: f64::from(u8::from(current)), - choices: Vec::new(), - default_choice: b.default.to_string(), - current_choice: current.to_string(), - }); - by_name.insert(name.clone(), idx); - } - for (name, s) in &pack.options.strings { - let current = values - .strings - .get(name) - .cloned() - .unwrap_or_else(|| s.default.clone()); - let choices: Vec<(String, String)> = s - .allowed - .iter() - .map(|raw| (raw.clone(), named(format!("value.{name}.{raw}"), raw))) - .collect(); - let idx = options.len(); - options.push(PackOption { - key: name.clone(), - label: option_label(name), - description: option_desc(name, s.comment.as_deref()), - kind: OptionKind::Choice { - slider: sliders.contains(name.as_str()), - }, - values: Vec::new(), - default: 0.0, - value: 0.0, - choices, - default_choice: s.default.clone(), - current_choice: current, - }); - by_name.insert(name.clone(), idx); - } - - let profiles: Vec = props - .profiles(&pack.options) - .unwrap_or_default() - .into_iter() - .map(|p| { - let label = named(format!("profile.{}", p.name), &p.name); - PackProfile { - name: p.name, - label, - values: p.values, - } - }) - .collect(); - - let referenced: BTreeSet = std::iter::once(None) - .chain(props.screens().into_iter().map(Some)) - .flat_map(|s| props.screen(s).unwrap_or_default()) - .filter_map(|e| match e { - MenuElement::Option(o) => Some(o), - _ => None, - }) - .collect(); - - let build_screen = |screen_name: Option<&str>| -> PackScreen { - let elements = props - .screen(screen_name) - .unwrap_or_else(|| vec![MenuElement::Rest]); - let mut items = Vec::new(); - if screen_name.is_none() - && !profiles.is_empty() - && !elements.contains(&MenuElement::Profile) - { - items.push(ScreenItem::Profile); - } - for el in elements { - match el { - MenuElement::Empty => items.push(ScreenItem::Empty), - MenuElement::Profile => { - if !profiles.is_empty() { - items.push(ScreenItem::Profile); - } - } - MenuElement::Screen(sub) => { - let label = named(format!("screen.{sub}"), &sub); - items.push(ScreenItem::Subscreen(sub, label)); - } - MenuElement::Option(opt) => { - if let Some(&idx) = by_name.get(&opt) { - items.push(ScreenItem::Option(idx)); - } - } - MenuElement::Rest => { - for (idx, opt) in options.iter().enumerate() { - if !referenced.contains(&opt.key) { - items.push(ScreenItem::Option(idx)); - } - } - } - } - } - let name_str = screen_name.unwrap_or("").to_owned(); - let label = match screen_name { - Some(n) => named(format!("screen.{n}"), n), - None => id.to_owned(), - }; - PackScreen { - name: name_str, - label, - columns: props.columns(screen_name).unwrap_or(2), - items, - } - }; - - let main_screen = build_screen(None); - let subscreens = props - .screens() - .into_iter() - .map(|s| build_screen(Some(s))) - .collect(); - - Ok(Self { - id: id.to_owned(), - options, - profiles, - main_screen, - subscreens, - active_screen: None, - }) - } - - /// Currently active screen (main or a sub-screen). - pub fn current_screen(&self) -> &PackScreen { - if let Some(name) = &self.active_screen - && let Some(sub) = self.subscreens.iter().find(|s| s.name == *name) - { - return sub; - } - &self.main_screen - } - - /// Name of the profile matching current option values, or `"Custom"`. - pub fn active_profile_label(&self) -> String { - let map = self.to_map(); - for profile in &self.profiles { - let matches = profile.values.iter().all(|(k, v)| { - if let Some(cur) = map.get(k) { - return cur == v; - } - if let Some(opt) = self.options.iter().find(|o| o.key == *k) { - return match opt.kind { - OptionKind::Bool => (opt.default != 0.0).to_string() == *v, - OptionKind::Int => (opt.default.round() as i64).to_string() == *v, - OptionKind::Float { .. } => format!("{}", opt.default) == *v, - OptionKind::Choice { .. } => opt.default_choice == *v, - }; - } - true - }); - if matches { - return profile.label.clone(); - } - } - "Custom".into() - } - - /// Cycle to the next (or previous) profile and apply its option values. - pub fn cycle_profile(&mut self, reverse: bool) { - if self.profiles.is_empty() { - return; - } - let current = self.active_profile_label(); - let idx = self - .profiles - .iter() - .position(|p| p.label == current) - .map(|i| { - if reverse { - (i + self.profiles.len() - 1) % self.profiles.len() - } else { - (i + 1) % self.profiles.len() - } - }) - .unwrap_or(0); - let values = self.profiles[idx].values.clone(); - for opt in &mut self.options { - if let Some(v) = values.get(&opt.key) { - match opt.kind { - OptionKind::Bool => { - if let Ok(b) = v.parse::() { - opt.value = f64::from(u8::from(b)); - } - } - OptionKind::Int => { - if let Ok(i) = v.parse::() { - opt.value = i as f64; - } - } - OptionKind::Float { min, max, .. } => { - if let Ok(f) = v.parse::() { - opt.value = f.clamp(min, max); - } - } - OptionKind::Choice { .. } => { - opt.current_choice = v.clone(); - } - } - } - } - } - - /// Reset all options to their defaults. - pub fn reset_all(&mut self) { - for opt in &mut self.options { - opt.reset(); - } - } - - /// Export non-default option values as a `key=value` map. - pub fn to_map(&self) -> BTreeMap { - let mut out = BTreeMap::new(); - for opt in &self.options { - if let Some(val) = opt.saved_string() { - out.insert(opt.key.clone(), val); - } - } - out - } - - /// Save the current non-default option values to `shaderpacks/.txt`. - pub fn save(&self, dir: &Path) -> Result<(), String> { - save_values(dir, &self.id, &self.to_map()) - } -} - -fn humanize_identifier(raw: &str) -> String { - let mut words = Vec::new(); - let mut current = String::new(); - for ch in raw.chars() { - if ch == '_' || ch == '-' { - if !current.is_empty() { - words.push(std::mem::take(&mut current)); - } - } else if ch.is_uppercase() - && !current.is_empty() - && current.chars().last().is_some_and(char::is_lowercase) - { - words.push(std::mem::take(&mut current)); - current.push(ch); - } else { - current.push(ch); - } - } - if !current.is_empty() { - words.push(current); - } - words - .into_iter() - .map(|w| { - let lower = w.to_lowercase(); - let mut chars = lower.chars(); - match chars.next() { - Some(first) => format!("{}{}", first.to_uppercase(), chars.as_str()), - None => String::new(), - } - }) - .collect::>() - .join(" ") -} - -pub struct ShaderPack { - /// The pack's folder name. - pub id: String, - pub info: Info, - pub stages: Vec, - pub options: Vec, - /// Optional custom noise/LUT texture bytes (RGBA8, width, height) from `[textures]`. - pub custom_noise: Option, - /// A Java (OptiFine/Iris) pack's translated passes, instead of `stages`. - pub java: Option, - dir: PathBuf, - stamp: Stamp, -} - -/// Every file's path, length and modification time: a changed stamp means -/// the pack or its `.txt` option file was edited (hot reload). -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Stamp(Vec<(PathBuf, u64, Option)>); -impl Stamp { - pub fn of(dir: &Path) -> Self { - let mut files = Vec::new(); - if let Ok(meta) = fs::metadata(dir) - && meta.is_file() - { - files.push((dir.to_path_buf(), meta.len(), meta.modified().ok())); - } - // Also watch the sibling `.txt` options file for live option reloads. - if let Some(name) = dir.file_name() - && let Some(parent) = dir.parent() - { - let txt = parent.join(format!("{}.txt", name.to_string_lossy())); - if let Ok(meta) = fs::metadata(&txt) - && meta.is_file() - { - files.push((txt, meta.len(), meta.modified().ok())); - } - } - let mut stack = vec![dir.to_path_buf()]; - while let Some(d) = stack.pop() { - let Ok(entries) = fs::read_dir(&d) else { - continue; - }; - for entry in entries.flatten() { - let path = entry.path(); - let Ok(meta) = entry.metadata() else { continue }; - if meta.is_dir() { - stack.push(path); - } else { - files.push((path, meta.len(), meta.modified().ok())); - } - } - } - files.sort(); - Self(files) - } -} - -/// The packs in `dir` (`shaderpacks/`): folders holding a `pack.toml`, by -/// folder name, with their manifest or why it can't be read, and Java packs -/// (folders holding `shaders/`, zips). -pub fn list(dir: &Path) -> Vec<(String, Result)> { - let mut packs: Vec<_> = fs::read_dir(dir) - .into_iter() - .flatten() - .flatten() - .filter_map(|e| { - let id = e.file_name().to_string_lossy().into_owned(); - let path = e.path(); - if path.join("pack.toml").is_file() { - Some((id, manifest(&path).map(|m| m.pack))) - } else if crate::java_pack::is_java_pack(&path) { - Some((id.clone(), Ok(java_info(id)))) - } else { - None - } - }) - .collect(); - packs.sort_by(|a, b| a.0.cmp(&b.0)); - packs -} - -/// `shaderpacks/` beside `options.txt` (the game's working folder: the game -/// home, not the launcher's version folder). -pub fn folder() -> PathBuf { - std::env::current_dir().unwrap_or_default().join(DIR) -} - -/// Import a `.zip` archive or shader pack folder from `source` into -/// `target_dir` (`shaderpacks/`), validating that it holds `pack.toml` or -/// `shaders/`. Returns the imported pack's folder/zip name (`id`). -pub fn import_pack(source: &Path, target_dir: &Path) -> Result { - let source = source - .canonicalize() - .unwrap_or_else(|_| source.to_path_buf()); - if !source.exists() { - return Err(format!("{}: file or folder not found", source.display())); - } - let name = source - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .filter(|n| !n.is_empty()) - .ok_or_else(|| "invalid shader pack path".to_string())?; - let is_horizon = source.is_dir() && source.join("pack.toml").is_file(); - let is_java = crate::java_pack::is_java_pack(&source); - if !is_horizon && !is_java { - return Err(format!( - "{name}: not a shader pack (needs pack.toml or shaders/)" - )); - } - if is_java && source.is_file() { - // Verify the zip actually contains `shaders/` or `pack.toml` at its root. - JavaPack::open(&source)?; - } else if is_horizon { - manifest(&source)?; - } - fs::create_dir_all(target_dir).map_err(|e| format!("{}: {e}", target_dir.display()))?; - let dest = target_dir.join(&name); - if dest.canonicalize().ok() == source.canonicalize().ok() { - return Ok(name); - } - if dest.exists() { - return Err(format!("{name} is already in {DIR}/")); - } - if source.is_file() { - fs::copy(&source, &dest).map_err(|e| format!("failed to copy {name}: {e}"))?; - } else { - copy_dir_recursive(&source, &dest)?; - } - Ok(name) -} - -fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), String> { - fs::create_dir_all(dst).map_err(|e| format!("{}: {e}", dst.display()))?; - let entries = fs::read_dir(src).map_err(|e| format!("{}: {e}", src.display()))?; - for entry in entries.flatten() { - let path = entry.path(); - let target = dst.join(entry.file_name()); - // Links aren't followed: one pointing up the tree would never end. - match entry.file_type() { - Ok(t) if t.is_dir() => copy_dir_recursive(&path, &target)?, - Ok(t) if t.is_file() => { - fs::copy(&path, &target).map_err(|e| format!("{}: {e}", target.display()))?; - } - _ => {} - } - } - Ok(()) -} - -/// `shaderpacks/` and its packs as the Shader Packs page lists them, read -/// again at most once a second while the page is open, so a pack dropped -/// into the folder shows up. -#[derive(Debug, Default)] -pub struct Folder { - pub dir: PathBuf, - pub packs: Vec<(String, Result)>, - read: Option, -} - -impl Folder { - pub fn poll(&mut self, dir: PathBuf) { - let fresh = self - .read - .is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(1)); - if fresh && self.dir == dir { - return; - } - self.packs = list(&dir); - self.dir = dir; - self.read = Some(std::time::Instant::now()); - } -} - -/// A Java pack has no manifest: its name is its file name. -fn java_info(name: String) -> Info { - Info { - name, - description: "Iris/OptiFine pack: its full-screen passes run.".into(), - authors: Vec::new(), - version: String::new(), - license: String::new(), - api: API, - } -} - -fn manifest(dir: &Path) -> Result { - let text = fs::read_to_string(dir.join("pack.toml")).map_err(|e| format!("pack.toml: {e}"))?; - let manifest: Manifest = toml::from_str(&text).map_err(|e| format!("pack.toml: {e}"))?; - if manifest.pack.api == 0 || manifest.pack.api > API { - return Err(format!( - "pack.toml: needs shader pack api {}, this Horizon has {API}", - manifest.pack.api - )); - } - Ok(manifest) -} - -/// An RGBA8 image (`decode_png`). -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Rgba { - pub width: u32, - pub height: u32, - pub pixels: Vec, -} - -/// A PNG of any colour type as [`Rgba`]. -pub(crate) fn decode_png(bytes: &[u8]) -> Result { - let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes)); - decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16); - let mut reader = decoder.read_info().map_err(|e| e.to_string())?; - let mut buf = vec![0u8; reader.output_buffer_size().unwrap_or(0)]; - let info = reader.next_frame(&mut buf).map_err(|e| e.to_string())?; - buf.truncate(info.buffer_size()); - let (w, h) = (info.width.max(1), info.height.max(1)); - let rgba = match info.color_type { - png::ColorType::Rgba => buf, - png::ColorType::Rgb => buf - .as_chunks::<3>() - .0 - .iter() - .flat_map(|p| [p[0], p[1], p[2], 255]) - .collect(), - png::ColorType::GrayscaleAlpha => buf - .as_chunks::<2>() - .0 - .iter() - .flat_map(|p| [p[0], p[0], p[0], p[1]]) - .collect(), - png::ColorType::Grayscale => buf.into_iter().flat_map(|g| [g, g, g, 255]).collect(), - _ => return Err("unsupported PNG color format".into()), - }; - Ok(Rgba { - width: w, - height: h, - pixels: rgba, - }) -} - -impl ShaderPack { - /// Load and validate the pack in `dir` with the option values in - /// `values` (missing ones take their default). - pub fn load(dir: &Path, values: &BTreeMap) -> Result { - let stamp = Stamp::of(dir); - let manifest = manifest(dir)?; - let pack_api = manifest.pack.api; - if manifest.stage.is_empty() || manifest.stage.len() > MAX_STAGES { - return Err(format!("pack.toml: 1 to {MAX_STAGES} stages")); - } - if manifest.options.len() > MAX_OPTIONS { - return Err(format!("pack.toml: at most {MAX_OPTIONS} options")); - } - let options = manifest - .options - .into_iter() - .map(|(key, entry)| option(key, entry, values)) - .collect::, _>>()?; - let mut stages = Vec::new(); - for entry in manifest.stage { - let kind = match entry.kind.as_str() { - "composite" => StageKind::Composite, - other => { - return Err(format!( - "pack.toml: stage kind `{other}` isn't supported (api {API} has `composite`)" - )); - } - }; - if pack_api == 1 && entry.format.is_some() { - return Err(format!("pack.toml: {}: `format` needs api 2", entry.shader)); - } - if let Some(format) = entry.format.as_deref() - && !STAGE_FORMATS.contains(&format) - { - return Err(format!( - "pack.toml: stage format `{format}` isn't one of {}", - STAGE_FORMATS.join(", ") - )); - } - let scale = entry.scale.unwrap_or(1.0); - if !(0.25..=2.0).contains(&scale) { - return Err(format!( - "pack.toml: {}: scale must be 0.25 to 2", - entry.shader - )); - } - let path = dir.join(&entry.shader); - if !path.starts_with(dir) || entry.shader.contains("..") { - return Err(format!("pack.toml: {} is outside the pack", entry.shader)); - } - let code = fs::read_to_string(&path).map_err(|e| format!("{}: {e}", entry.shader))?; - let entry_point = entry.entry.unwrap_or_else(|| "fs_main".into()); - let prelude = match pack_api { - 1 => PRELUDE.to_owned(), - _ => format!("{PRELUDE}\n{PRELUDE_API2}"), - }; - let source = format!("{prelude}\n{code}"); - let bindings = validate( - &source, - prelude.lines().count(), - &entry.shader, - &entry_point, - )?; - stages.push(Stage { - kind, - file: entry.shader, - source, - entry: entry_point, - scale, - format: entry.format, - bindings, - }); - } - let mut custom_noise = None; - for (name, rel) in &manifest.textures { - if name != "noise" { - return Err(format!( - "pack.toml: texture `{name}` isn't supported (api {API} has `noise`)" - )); - } - if rel.contains("..") { - return Err(format!("pack.toml: texture {rel} is outside the pack")); - } - let bytes = fs::read(dir.join(rel)).map_err(|e| format!("{rel}: {e}"))?; - custom_noise = Some(decode_png(&bytes).map_err(|e| format!("{rel}: {e}"))?); - } - Ok(Self { - id: dir - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_default(), - info: manifest.pack, - stages, - options, - custom_noise, - java: None, - dir: dir.to_path_buf(), - stamp, - }) - } - /// Load the pack at `path` (a folder or a zip): a Horizon pack, or a - /// Java pack translated for the GPU `vendor` and `renderer` names. - pub fn load_any( - path: &Path, - values: &BTreeMap, - vendor: &str, - renderer: &str, - ) -> Result { - if path.join("pack.toml").is_file() || !crate::java_pack::is_java_pack(path) { - return Self::load(path, values); - } - let stamp = Stamp::of(path); - let java = crate::java_pack::load(path, values, vendor, renderer)?; - let id = path - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_default(); - Ok(Self { - info: java_info(id.clone()), - id, - stages: Vec::new(), - options: Vec::new(), - custom_noise: None, - java: Some(java), - dir: path.to_path_buf(), - stamp, - }) - } - /// Whether a stage reads one of the prelude's `bindings`. - fn reads(&self, bindings: &[u32]) -> bool { - self.stages - .iter() - .any(|s| bindings.iter().any(|b| s.bindings.contains(b))) - } - /// Whether a stage reads the shadow map (`shadow_depth`, `sample_shadow`). - pub fn needs_shadow(&self) -> bool { - self.reads(&[BINDING_SHADOW_DEPTH]) - } - /// Whether a stage reads the G-buffer (`scene_normal`, `scene_material`). - pub fn needs_gbuffer(&self) -> bool { - self.reads(&[BINDING_SCENE_NORMAL, BINDING_SCENE_MATERIAL]) - } - /// Whether the world's depth before translucent geometry is kept - /// (`opaque_depth`, which the G-buffer is also built from). - pub fn needs_opaque_depth(&self) -> bool { - self.needs_gbuffer() || self.reads(&[BINDING_OPAQUE_DEPTH]) - } - /// The shadow map's resolution and distance in blocks. - pub fn shadow_config(&self) -> (u32, f32) { - let res = self - .options - .iter() - .find(|o| o.key == "shadow_resolution" || o.key == "shadowMapResolution") - .map(|o| (o.value.round() as u32).clamp(256, 4096)) - .unwrap_or(2048); - let dist = self - .options - .iter() - .find(|o| o.key == "shadow_distance" || o.key == "shadowDistance") - .map(|o| (o.value as f32).clamp(16.0, 512.0)) - .unwrap_or(128.0); - (res, dist) - } - /// The pipeline-overridable constants every stage gets: the option values. - pub fn constants(&self) -> Vec<(&str, f64)> { - self.options - .iter() - .map(|o| (o.key.as_str(), o.value)) - .collect() - } - /// The pack's files changed since it loaded. - pub fn changed(&self) -> bool { - Stamp::of(&self.dir) != self.stamp - } - pub fn dir(&self) -> &Path { - &self.dir - } -} - -fn option( - key: String, - entry: OptionEntry, - values: &BTreeMap, -) -> Result { - let bad = |what: &str| format!("pack.toml: option `{key}`: {what}"); - let (kind, default) = match entry.default { - toml::Value::Boolean(b) => (OptionKind::Bool, f64::from(u8::from(b))), - toml::Value::Integer(i) => (OptionKind::Int, i as f64), - toml::Value::Float(f) => ( - OptionKind::Float { - min: entry.min.ok_or_else(|| bad("a number needs min"))?, - max: entry.max.ok_or_else(|| bad("a number needs max"))?, - step: entry.step.unwrap_or(0.0), - }, - f, - ), - _ => return Err(bad("default must be true/false, an integer or a number")), - }; - let values_list = entry.values.unwrap_or_default(); - if kind == OptionKind::Int && !values_list.contains(&(default as i64)) { - return Err(bad("an integer needs `values` holding its default")); - } - let value = values - .get(&key) - .and_then(|v| match kind { - OptionKind::Bool => v.parse::().ok().map(|b| f64::from(u8::from(b))), - OptionKind::Int => v - .parse::() - .ok() - .filter(|i| values_list.contains(i)) - .map(|i| i as f64), - OptionKind::Float { min, max, .. } => v - .parse::() - .ok() - .filter(|f| f.is_finite()) - .map(|f| f.clamp(min, max)), - OptionKind::Choice { .. } => None, - }) - .unwrap_or(default); - Ok(PackOption { - label: entry.label.unwrap_or_else(|| humanize_identifier(&key)), - description: entry.description, - key, - kind, - values: values_list, - default, - value, - choices: Vec::new(), - default_choice: String::new(), - current_choice: String::new(), - }) -} - -/// naga parses and validates the stage (prelude included) and checks its -/// fragment entry point; errors point into the pack's own file. -/// Parse and validate a stage: the prelude's bindings (group 0) its entry -/// point uses, or the error at the stage's own line. -fn validate( - source: &str, - prelude_lines: usize, - file: &str, - entry: &str, -) -> Result, String> { - let prelude_lines = prelude_lines as u32 + 1; - let at = |location: Option| match location { - Some(l) if l.line_number > prelude_lines => { - format!( - "{file}:{}:{}", - l.line_number - prelude_lines, - l.line_position - ) - } - _ => file.to_string(), - }; - let module = naga::front::wgsl::parse_str(source) - .map_err(|e| format!("{}: {}", at(e.location(source)), e.message()))?; - let info = naga::valid::Validator::new( - naga::valid::ValidationFlags::all(), - naga::valid::Capabilities::default(), - ) - .validate(&module) - .map_err(|e| format!("{}: {}", at(e.location(source)), e.as_inner()))?; - let Some(index) = module - .entry_points - .iter() - .position(|e| e.name == entry && e.stage == naga::ShaderStage::Fragment) - else { - return Err(format!("{file}: no fragment entry point `{entry}`")); - }; - let uses = info.get_entry_point(index); - Ok(module - .global_variables - .iter() - .filter(|(handle, _)| !uses[*handle].is_empty()) - .filter_map(|(_, var)| var.binding.as_ref()) - .filter(|binding| binding.group == 0) - .map(|binding| binding.binding) - .collect()) -} - -/// The option values saved for a pack (`shaderpacks/.txt`, `key=value`). -pub fn load_values(dir: &Path, id: &str) -> BTreeMap { - fs::read_to_string(dir.join(format!("{id}.txt"))) - .unwrap_or_default() - .lines() - .filter_map(|l| l.split_once('=')) - .map(|(k, v)| (k.trim().to_string(), v.trim().to_string())) - .collect() -} - -/// Save option values for a pack (`shaderpacks/.txt`, `key=value`). -/// When `values` is empty, removes `.txt` if present. -pub fn save_values(dir: &Path, id: &str, values: &BTreeMap) -> Result<(), String> { - if id.is_empty() { - return Ok(()); - } - let path = dir.join(format!("{id}.txt")); - if values.is_empty() { - let _ = fs::remove_file(path); - return Ok(()); - } - fs::create_dir_all(dir).map_err(|e| e.to_string())?; - let mut text = String::new(); - for (k, v) in values { - text.push_str(&format!("{k}={v}\n")); - } - fs::write(&path, text).map_err(|e| format!("{}: {e}", path.display())) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn scratch() -> PathBuf { - static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); - let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let dir = - std::env::temp_dir().join(format!("horizon-shader-pack-{}-{n}", std::process::id())); - let _ = fs::remove_dir_all(&dir); - fs::create_dir_all(&dir).unwrap(); - dir - } - fn pack(files: &[(&str, &str)]) -> PathBuf { - let dir = scratch(); - for (name, text) in files { - let path = dir.join(name); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(path, text).unwrap(); - } - dir - } - const MANIFEST: &str = r#" -[pack] -name = "Test" -api = 1 - -[[stage]] -kind = "composite" -shader = "shaders/grade.wgsl" - -[options.strength] -label = "Strength" -default = 0.5 -min = 0.0 -max = 1.0 -step = 0.1 - -[options.invert] -default = false - -[options.mode] -default = 1 -values = [0, 1, 2] -"#; - const GRADE: &str = r#" -override strength: f32 = 0.5; -override invert: bool = false; -override mode: i32 = 1; -@fragment fn fs_main(in: StageIn) -> @location(0) vec4 { - var c = textureSampleLevel(previous, linear_sampler, in.uv, 0.0).rgb; - let d = linear_depth(depth_at(in.uv)); - c = mix(c, vec3(luminance(c)), strength) * f32(mode) + d * frame.fog_color.rgb * 0.0; - if invert { c = 1.0 - c; } - return vec4(c, 1.0); -} -"#; - - #[test] - fn the_sample_pack_loads() { - let dir = - Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/shaderpacks/horizon-sample"); - let pack = ShaderPack::load(&dir, &BTreeMap::new()).unwrap(); - assert_eq!(pack.id, "horizon-sample"); - assert!(!pack.stages.is_empty()); - assert!(!pack.changed()); - } - - #[test] - fn options_take_saved_values_within_their_range() { - let dir = pack(&[("pack.toml", MANIFEST), ("shaders/grade.wgsl", GRADE)]); - let values = BTreeMap::from([ - ("strength".to_string(), "7".to_string()), - ("invert".to_string(), "true".to_string()), - ("mode".to_string(), "5".to_string()), - ]); - let pack = ShaderPack::load(&dir, &values).unwrap(); - assert_eq!( - pack.constants(), - [("invert", 1.0), ("mode", 1.0), ("strength", 1.0)] - ); - assert_eq!(pack.stages[0].entry, "fs_main"); - assert_eq!(pack.stages[0].kind, StageKind::Composite); - } - - #[test] - fn errors_point_into_the_pack_file() { - let broken = "\n@fragment fn fs_main(in: StageIn) -> @location(0) vec4 {\n return vec4(nope, 1.0);\n}\n"; - let dir = pack(&[("pack.toml", MANIFEST), ("shaders/grade.wgsl", broken)]); - let error = ShaderPack::load(&dir, &BTreeMap::new()).err().unwrap(); - assert!(error.starts_with("shaders/grade.wgsl:3:"), "{error}"); - let wrong_entry = GRADE.replace("fs_main", "other"); - let dir = pack(&[ - ("pack.toml", MANIFEST), - ("shaders/grade.wgsl", &wrong_entry), - ]); - let error = ShaderPack::load(&dir, &BTreeMap::new()).err().unwrap(); - assert_eq!( - error, - "shaders/grade.wgsl: no fragment entry point `fs_main`" - ); - } - - #[test] - fn manifests_are_checked() { - for (manifest, expected) in [ - ( - MANIFEST.replace("api = 1", "api = 3"), - "needs shader pack api 3", - ), - ( - MANIFEST.replace("\"composite\"", "\"shadow\""), - "stage kind `shadow`", - ), - ( - MANIFEST.replace( - "kind = \"composite\"", - "kind = \"composite\"\nformat = \"rgba16float\"", - ), - "`format` needs api 2", - ), - ( - MANIFEST.replace("api = 1", "api = 2").replace( - "kind = \"composite\"", - "kind = \"composite\"\nformat = \"rgba32float\"", - ), - "stage format `rgba32float`", - ), - ( - MANIFEST.replace("grade.wgsl", "../grade.wgsl"), - "outside the pack", - ), - (MANIFEST.replace("min = 0.0\n", ""), "a number needs min"), - ( - MANIFEST.replace("values = [0, 1, 2]", "values = [0]"), - "holding its default", - ), - ( - MANIFEST.replace("name = \"Test\"", "name = \"Test\"\ncolour = 1"), - "unknown field", - ), - ] { - let dir = pack(&[("pack.toml", &manifest), ("shaders/grade.wgsl", GRADE)]); - let error = ShaderPack::load(&dir, &BTreeMap::new()).err().unwrap(); - assert!(error.contains(expected), "{error}"); - } - } - - #[test] - fn api_2_stages_and_pack_config_work() { - let manifest_v2 = MANIFEST.replace("api = 1", "api = 2").replace( - "kind = \"composite\"", - "kind = \"composite\"\nformat = \"rgba16float\"", - ); - let dir = pack(&[("pack.toml", &manifest_v2), ("shaders/grade.wgsl", GRADE)]); - let loaded = ShaderPack::load(&dir, &BTreeMap::new()).unwrap(); - assert_eq!(loaded.stages[0].format.as_deref(), Some("rgba16float")); - assert!( - !loaded.needs_shadow() && !loaded.needs_gbuffer() && !loaded.needs_opaque_depth(), - "reading none of them costs no pass" - ); - let opaque = GRADE.replace("depth_at(in.uv)", "opaque_depth_at(in.uv)"); - let reads = pack(&[("pack.toml", &manifest_v2), ("shaders/grade.wgsl", &opaque)]); - let reads = ShaderPack::load(&reads, &BTreeMap::new()).unwrap(); - assert!(reads.needs_opaque_depth() && !reads.needs_gbuffer()); - let api_1 = pack(&[("pack.toml", MANIFEST), ("shaders/grade.wgsl", &opaque)]); - let error = ShaderPack::load(&api_1, &BTreeMap::new()).err().unwrap(); - assert!( - error.contains("opaque_depth_at"), - "api 1 stages don't see api 2's names: {error}" - ); - let parent = dir.parent().unwrap(); - let id = dir.file_name().unwrap().to_str().unwrap(); - let mut cfg = PackConfig::load(parent, id).unwrap(); - assert_eq!(cfg.options.len(), 3); - cfg.options[0].cycle(false); - cfg.save(parent).unwrap(); - assert!(loaded.changed(), "saving .txt updates the stamp"); - } - - #[test] - fn edits_change_the_stamp_and_the_list_shows_every_pack() { - let root = scratch(); - let dir = root.join("mine"); - fs::create_dir_all(dir.join("shaders")).unwrap(); - fs::write(dir.join("pack.toml"), MANIFEST).unwrap(); - fs::write(dir.join("shaders/grade.wgsl"), GRADE).unwrap(); - fs::create_dir_all(root.join("broken")).unwrap(); - fs::write(root.join("broken/pack.toml"), "[pack]\n").unwrap(); - fs::create_dir_all(root.join("not-a-pack")).unwrap(); - let pack = ShaderPack::load(&dir, &BTreeMap::new()).unwrap(); - assert!(!pack.changed()); - fs::write(dir.join("shaders/grade.wgsl"), format!("{GRADE}\n")).unwrap(); - assert!(pack.changed()); - let listed = list(&root); - assert_eq!(listed.len(), 2); - assert_eq!(listed[0].0, "broken"); - assert!(listed[0].1.is_err()); - assert_eq!(listed[1].1.as_ref().unwrap().name, "Test"); - } - - #[test] - fn saved_values_are_read_per_pack() { - let dir = scratch(); - fs::write(dir.join("mine.txt"), "strength = 0.25\ninvert=true\n").unwrap(); - let values = load_values(&dir, "mine"); - assert_eq!(values["strength"], "0.25"); - assert_eq!(values["invert"], "true"); - assert!(load_values(&dir, "other").is_empty()); - } - - /// Dropping a pack on the Shader Packs page: zips and folders, Java - /// and Horizon, and the things that aren't packs. - #[test] - fn imports_zips_and_folders_and_refuses_the_rest() { - let shaderpacks = scratch(); - let java = pack(&[("shaders/composite.fsh", "void main() {}\n")]); - let zip_path = scratch().join("Java Pack.zip"); - let mut zip = zip::ZipWriter::new(fs::File::create(&zip_path).unwrap()); - zip.start_file( - "Java Pack/shaders/final.fsh", - zip::write::SimpleFileOptions::default(), - ) - .unwrap(); - std::io::Write::write_all(&mut zip, b"void main() {}\n").unwrap(); - zip.finish().unwrap(); - - assert_eq!( - import_pack(&zip_path, &shaderpacks).unwrap(), - "Java Pack.zip" - ); - assert!(shaderpacks.join("Java Pack.zip").is_file()); - let folder = import_pack(&java, &shaderpacks).unwrap(); - assert!( - shaderpacks - .join(&folder) - .join("shaders/composite.fsh") - .is_file() - ); - // One already there is refused; one dropped from shaderpacks/ itself - // is just picked. - let again = import_pack(&zip_path, &shaderpacks).unwrap_err(); - assert!(again.contains("already"), "{again}"); - assert_eq!( - import_pack(&shaderpacks.join("Java Pack.zip"), &shaderpacks).unwrap(), - "Java Pack.zip" - ); - - let not_a_pack = pack(&[("readme.txt", "hello")]); - assert!( - import_pack(¬_a_pack, &shaderpacks) - .unwrap_err() - .contains("not a shader pack") - ); - let fake_zip = scratch().join("broken.zip"); - fs::write(&fake_zip, "not a zip").unwrap(); - assert!(import_pack(&fake_zip, &shaderpacks).is_err()); - let broken_horizon = pack(&[("pack.toml", "name = ")]); - assert!(import_pack(&broken_horizon, &shaderpacks).is_err()); - assert!(import_pack(&shaderpacks.join("missing"), &shaderpacks).is_err()); - let left: Vec<_> = fs::read_dir(&shaderpacks) - .unwrap() - .flatten() - .map(|e| e.file_name()) - .collect(); - assert_eq!(left.len(), 2, "nothing copied for refused packs: {left:?}"); - } -} diff --git a/crates/horizon-client/src/shader_pack_prelude.wgsl b/crates/horizon-client/src/shader_pack_prelude.wgsl deleted file mode 100644 index 7b16690f9..000000000 --- a/crates/horizon-client/src/shader_pack_prelude.wgsl +++ /dev/null @@ -1,71 +0,0 @@ -// Horizon shader pack prelude, api 1 (docs/shader-packs.md): placed before -// every stage's source. Bindings, the Frame uniform, the full-screen vertex -// shader and helpers that hide the renderer's reversed depth. -struct Frame { - view: mat4x4, - projection: mat4x4, - inverse_view: mat4x4, - inverse_projection: mat4x4, - camera_position: vec4, - resolution: vec4, - time: vec4, - sun: vec4, - moon: vec4, - sky_color: vec4, - fog_color: vec4, - fog: vec4, - weather: vec4, - camera_state: vec4, - view_params: vec4, - dimension: vec4, -} -@group(0) @binding(0) var scene_color: texture_2d; -@group(0) @binding(1) var scene_depth: texture_depth_2d; -@group(0) @binding(2) var previous: texture_2d; -@group(0) @binding(3) var linear_sampler: sampler; -@group(0) @binding(4) var nearest_sampler: sampler; -@group(0) @binding(5) var frame: Frame; - -struct StageIn { - @builtin(position) position: vec4, - @location(0) uv: vec2, -} -@vertex fn horizon_stage_vertex(@builtin(vertex_index) i: u32) -> StageIn { - let p = vec2(f32((i << 1u) & 2u), f32(i & 2u)); - var out: StageIn; - out.position = vec4(p * 2.0 - 1.0, 0.0, 1.0); - out.uv = vec2(p.x, 1.0 - p.y); - return out; -} - -// The world's depth at `uv`: 1 at the near plane, 0 for the sky. -fn depth_at(uv: vec2) -> f32 { - return textureSampleLevel(scene_depth, nearest_sampler, uv, 0); -} -fn is_sky(depth: f32) -> bool { - return depth <= 0.0; -} -// Blocks from the eye along the view axis. -fn linear_depth(depth: f32) -> f32 { - let near = frame.view_params.x; - let far = frame.view_params.y; - return near * far / ((far - near) * depth + near); -} -fn view_position(uv: vec2, depth: f32) -> vec3 { - let v = frame.inverse_projection * vec4(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0); - return v.xyz / v.w; -} -// World position relative to the eye (add frame.camera_position.xyz for the -// absolute one). -fn world_offset(uv: vec2, depth: f32) -> vec3 { - return (frame.inverse_view * vec4(view_position(uv, depth), 1.0)).xyz; -} -fn luminance(c: vec3) -> f32 { - return dot(c, vec3(0.2126, 0.7152, 0.0722)); -} -fn srgb_to_linear(c: vec3) -> vec3 { - return select(pow((c + 0.055) / 1.055, vec3(2.4)), c / 12.92, c <= vec3(0.04045)); -} -fn linear_to_srgb(c: vec3) -> vec3 { - return select(1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, c * 12.92, c <= vec3(0.0031308)); -} diff --git a/crates/horizon-client/src/shader_pack_prelude_api2.wgsl b/crates/horizon-client/src/shader_pack_prelude_api2.wgsl deleted file mode 100644 index 80e9a205e..000000000 --- a/crates/horizon-client/src/shader_pack_prelude_api2.wgsl +++ /dev/null @@ -1,108 +0,0 @@ -// Horizon shader pack prelude, api 2 additions (docs/shader-packs.md): placed -// after the api 1 prelude before the stages of an `api = 2` pack, so api 1 -// packs keep every name they may have declared themselves. -struct FrameExt { - previous_view: mat4x4, - previous_projection: mat4x4, - shadow_view: mat4x4, - shadow_projection: mat4x4, - shadow_inverse_view: mat4x4, - shadow_inverse_projection: mat4x4, - // xyz: previous camera world position, w: frame delta time (seconds) - previous_camera: vec4, - // x: shadow map resolution, y: shadow distance (blocks), z: 0, - // w: 1 while the shadow map is drawn this frame, else 0 - shadow_params: vec4, - // x: eye block light (0..240), y: eye sky light (0..240), z: smoothed - // rain (0..1), w: 0 - environment: vec4, -} - -@group(0) @binding(6) var frame_ext: FrameExt; -@group(0) @binding(7) var scene_normal: texture_2d; -@group(0) @binding(8) var scene_material: texture_2d; -@group(0) @binding(9) var opaque_depth: texture_depth_2d; -@group(0) @binding(10) var shadow_depth: texture_depth_2d; -@group(0) @binding(11) var shadow_sampler: sampler_comparison; -@group(0) @binding(12) var noise_texture: texture_2d; -@group(0) @binding(13) var repeat_sampler: sampler; - -// The opaque world's depth at `uv` (before translucent water and stained glass). -fn opaque_depth_at(uv: vec2) -> f32 { - return textureSampleLevel(opaque_depth, nearest_sampler, uv, 0); -} - -// World-space surface normal at `uv` from the G-buffer (or reconstructed from depth). -fn world_normal_at(uv: vec2) -> vec3 { - let encoded = textureSampleLevel(scene_normal, nearest_sampler, uv, 0.0); - if encoded.w > 0.001 { - return normalize(encoded.xyz * 2.0 - 1.0); - } - let step_uv = frame.resolution.zw; - let d0 = depth_at(uv); - let p0 = world_offset(uv, d0); - let px = world_offset(uv + vec2(step_uv.x, 0.0), depth_at(uv + vec2(step_uv.x, 0.0))) - p0; - let py = world_offset(uv + vec2(0.0, step_uv.y), depth_at(uv + vec2(0.0, step_uv.y))) - p0; - let n = cross(py, px); - let len = length(n); - if len > 1e-6 { - return n / len; - } - return vec3(0.0, 1.0, 0.0); -} -// G-buffer material data at `uv`: r = block light (0..1), g = sky light (0..1), -// b = water/translucent mask (0..1), a = roughness (0..1). -fn material_at(uv: vec2) -> vec4 { - return textureSampleLevel(scene_material, nearest_sampler, uv, 0.0); -} -// Reproject a camera-relative world position into previous-frame UV and clip depth. -fn reproject_uv(world_rel: vec3) -> vec3 { - let camera_delta = frame.camera_position.xyz - frame_ext.previous_camera.xyz; - let prev_rel = world_rel + camera_delta; - let clip = frame_ext.previous_projection * (frame_ext.previous_view * vec4(prev_rel, 1.0)); - if clip.w <= 1e-5 { - return vec3(-1.0); - } - let ndc = clip.xyz / clip.w; - return vec3(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5, ndc.z); -} -// Sample the directional sun/moon shadow map with 5-tap PCF and normal offset bias. -// Returns 1.0 in full light and 0.0 in full shadow. -fn sample_shadow(world_rel: vec3, world_normal: vec3) -> f32 { - if frame_ext.shadow_params.w <= 0.001 { - return 1.0; - } - let dist = length(world_rel); - let max_dist = max(frame_ext.shadow_params.y, 16.0); - if dist >= max_dist { - return 1.0; - } - let light_dir = normalize(select(frame.moon.xyz, frame.sun.xyz, frame.sun.y >= -0.05)); - let ndotl = max(dot(world_normal, light_dir), 0.0); - let bias_pos = world_rel + world_normal * (0.06 + 0.12 * (1.0 - ndotl)); - let light_clip = frame_ext.shadow_projection * (frame_ext.shadow_view * vec4(bias_pos, 1.0)); - let ndc = light_clip.xyz / max(light_clip.w, 1e-5); - let shadow_uv = vec2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5); - if any(shadow_uv < vec2(0.002)) || any(shadow_uv > vec2(0.998)) || ndc.z <= 0.0 || ndc.z >= 1.0 { - return 1.0; - } - let texel = 1.0 / max(frame_ext.shadow_params.x, 512.0); - let ref_z = clamp(ndc.z + 0.0008, 0.0, 1.0); - var sum = textureSampleCompareLevel(shadow_depth, shadow_sampler, shadow_uv, ref_z); - sum += textureSampleCompareLevel(shadow_depth, shadow_sampler, shadow_uv + vec2(texel, 0.0), ref_z); - sum += textureSampleCompareLevel(shadow_depth, shadow_sampler, shadow_uv - vec2(texel, 0.0), ref_z); - sum += textureSampleCompareLevel(shadow_depth, shadow_sampler, shadow_uv + vec2(0.0, texel), ref_z); - sum += textureSampleCompareLevel(shadow_depth, shadow_sampler, shadow_uv - vec2(0.0, texel), ref_z); - let fade = smoothstep(max_dist, max_dist * 0.82, dist); - return mix(1.0, sum * 0.2, fade * frame_ext.shadow_params.w); -} - -// ACES fitted filmic tonemapping curve (linear HDR -> linear 0..1). -fn aces_tonemap(x: vec3) -> vec3 { - let a = 2.51; - let b = 0.03; - let c = 2.43; - let d = 0.59; - let e = 0.14; - return clamp((x * (a * x + b)) / (x * (c * x + d) + e), vec3(0.0), vec3(1.0)); -} diff --git a/crates/horizon-shaderpack/Cargo.toml b/crates/horizon-shaderpack/Cargo.toml deleted file mode 100644 index 91a2251bc..000000000 --- a/crates/horizon-shaderpack/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "horizon-shaderpack" -version = "0.1.0" -edition.workspace = true -license.workspace = true -rust-version.workspace = true -description = "Java (OptiFine/Iris) shader packs for Horizon: Iris's preprocessor and the GLSL to WGSL translation" - -[dependencies] -glsl-lang = "0.8.1" -naga = { version = "30.0.1", features = ["glsl-in", "wgsl-in", "wgsl-out"] } -# The C preprocessor naga's GLSL front end uses. -pp-rs = "0.2.1" -zip = { version = "2", default-features = false, features = ["deflate"] } - -[lints] -workspace = true - -[dev-dependencies] -zip = { version = "2", default-features = false, features = ["deflate"] } diff --git a/crates/horizon-shaderpack/src/include.rs b/crates/horizon-shaderpack/src/include.rs deleted file mode 100644 index 34455a88e..000000000 --- a/crates/horizon-shaderpack/src/include.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! `#include` the way Iris resolves it (`IncludeGraph`, `IncludeProcessor`): -//! before any other directive, so include guards don't apply; `/lib/x.glsl` -//! from `shaders/`, `x.glsl` from the including file's folder. Every file -//! reachable from the programs is read once into a graph, and a program is -//! the graph inlined from its file. - -use crate::source::PackFiles; -use std::collections::{BTreeMap, BTreeSet}; - -/// One file of the graph: its lines and the include on each `#include` line. -pub struct FileNode { - pub lines: Vec, - /// Line index → included file (a normalised path relative to `shaders/`). - pub includes: BTreeMap, -} - -pub struct IncludeGraph { - pub files: BTreeMap, - /// Files that couldn't be read or include themselves, with the reason. - pub failures: BTreeMap, -} - -/// A program after inlining: the source and, for each of its lines, the -/// file and 1-based line it came from (to report errors where they are). -pub struct Inlined { - pub text: String, - pub origin: Vec<(String, u32)>, -} - -impl Inlined { - /// `file:line` of 1-based `line` of the inlined text. - pub fn locate(&self, line: u32) -> String { - match self.origin.get(line.saturating_sub(1) as usize) { - Some((file, at)) => format!("{file}:{at}"), - None => format!("line {line}"), - } - } -} - -/// `target` from an `#include` in `from`, as a path relative to `shaders/` -/// with `.` and `..` resolved (Iris's `AbsolutePackPath`). -pub fn resolve(from: &str, target: &str) -> String { - let joined = match target.strip_prefix('/') { - Some(absolute) => absolute.to_owned(), - None => match from.rsplit_once('/') { - Some((dir, _)) => format!("{dir}/{target}"), - None => target.to_owned(), - }, - }; - let mut parts: Vec<&str> = Vec::new(); - for segment in joined.split('/') { - match segment { - "" | "." => {} - ".." => { - parts.pop(); - } - s => parts.push(s), - } - } - parts.join("/") -} - -/// The target of an `#include` line, or `None` for any other line. -fn include_target(line: &str) -> Option<&str> { - let t = line.trim(); - if !t.starts_with("#include") { - return None; - } - let target = t.get("#include ".len()..).unwrap_or("").trim(); - let target = target.strip_prefix('"').unwrap_or(target); - Some(target.strip_suffix('"').unwrap_or(target)) -} - -impl IncludeGraph { - /// Read `starts` and every file they include, transitively. - pub fn build(files: &PackFiles, starts: &[String]) -> Self { - let mut graph = Self { - files: BTreeMap::new(), - failures: BTreeMap::new(), - }; - let mut seen: BTreeSet = starts.iter().cloned().collect(); - let mut queue: Vec<(String, Option<(String, usize)>)> = - starts.iter().map(|s| (s.clone(), None)).collect(); - while let Some((path, from)) = queue.pop() { - let Some(text) = files.read(&path) else { - let why = match from { - Some((file, line)) => { - format!("{file}:{}: #include \"{path}\": file not found", line + 1) - } - None => format!("{path}: file not found"), - }; - graph.failures.insert(path, why); - continue; - }; - let lines: Vec = text.lines().map(str::to_owned).collect(); - let includes: BTreeMap = lines - .iter() - .enumerate() - .filter_map(|(i, l)| Some((i, resolve(&path, include_target(l)?)))) - .collect(); - if let Some((line, _)) = includes.iter().find(|(_, target)| **target == path) { - graph.failures.insert( - path.clone(), - format!("{path}:{}: file includes itself", line + 1), - ); - continue; - } - for (&line, target) in &includes { - if seen.insert(target.clone()) { - queue.push((target.clone(), Some((path.clone(), line)))); - } - } - graph.files.insert(path, FileNode { lines, includes }); - } - graph - } - - /// The graph's weakly connected components (files linked by includes in - /// either direction), each sorted. Iris discovers options per component. - pub fn components(&self) -> Vec> { - let names: Vec<&String> = self.files.keys().collect(); - let index: BTreeMap<&str, usize> = names - .iter() - .enumerate() - .map(|(i, n)| (n.as_str(), i)) - .collect(); - let mut parent: Vec = (0..names.len()).collect(); - fn root(parent: &mut [usize], mut i: usize) -> usize { - while parent[i] != i { - parent[i] = parent[parent[i]]; - i = parent[i]; - } - i - } - for (i, node) in self.files.values().enumerate() { - for target in node.includes.values() { - if let Some(&j) = index.get(target.as_str()) { - let (a, b) = (root(&mut parent, i), root(&mut parent, j)); - parent[a] = b; - } - } - } - let mut groups: BTreeMap> = BTreeMap::new(); - for (i, name) in names.iter().enumerate() { - let r = root(&mut parent, i); - groups.entry(r).or_default().push((*name).clone()); - } - groups.into_values().collect() - } - - /// Inline `start`, passing every line that isn't an `#include` through - /// `edit(file, line index, text)` first (Iris applies option values there). - pub fn inline( - &self, - start: &str, - edit: &dyn Fn(&str, usize, &str) -> String, - ) -> Result { - let mut out = Inlined { - text: String::new(), - origin: Vec::new(), - }; - let mut stack = Vec::new(); - self.inline_into(start, edit, &mut stack, &mut out)?; - Ok(out) - } - - fn inline_into( - &self, - path: &str, - edit: &dyn Fn(&str, usize, &str) -> String, - stack: &mut Vec, - out: &mut Inlined, - ) -> Result<(), String> { - if let Some(why) = self.failures.get(path) { - return Err(why.clone()); - } - if stack.iter().any(|p| p == path) { - return Err(format!("#include cycle: {} -> {path}", stack.join(" -> "))); - } - let node = self - .files - .get(path) - .ok_or_else(|| format!("{path}: file not found"))?; - stack.push(path.to_owned()); - for (i, line) in node.lines.iter().enumerate() { - match node.includes.get(&i) { - Some(target) => self.inline_into(target, edit, stack, out)?, - None => { - out.text.push_str(&edit(path, i, line)); - out.text.push('\n'); - out.origin.push((path.to_owned(), i as u32 + 1)); - } - } - } - stack.pop(); - Ok(()) - } -} diff --git a/crates/horizon-shaderpack/src/lib.rs b/crates/horizon-shaderpack/src/lib.rs deleted file mode 100644 index 552105d9e..000000000 --- a/crates/horizon-shaderpack/src/lib.rs +++ /dev/null @@ -1,205 +0,0 @@ -//! Java shader packs (OptiFine/Iris) for Horizon (docs/shader-packs.md, -//! "Java shader packs"). This crate does the part that needs no GPU: open a -//! pack, find its programs and options, and turn a program into plain GLSL -//! the way Iris does before compiling it (`#include`s inlined, the player's -//! option values written into the source, Iris's macros expanded), read its -//! `shaders.properties`, and translate programs to WGSL. - -pub mod include; -pub mod options; -pub mod pipeline; -pub mod preprocess; -pub mod properties; -pub mod source; -pub mod translate; -pub mod uniforms; - -use include::{IncludeGraph, Inlined}; -use options::{Annotated, OptionSet, OptionValues}; -use preprocess::{Environment, Preprocessed}; -use properties::{Properties, ShaderProperties}; -use source::PackFiles; -use std::{ - collections::{BTreeMap, BTreeSet}, - path::Path, -}; -use translate::{Link, Translated}; - -/// A program's pipeline stage, from its file extension. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Stage { - Vertex, - Fragment, - Geometry, - Compute, - TessControl, - TessEval, -} - -impl Stage { - /// The stage of `composite1.fsh` and the like; `None` for other files. - pub fn of(program: &str) -> Option { - Some(match program.rsplit_once('.')?.1 { - "vsh" => Self::Vertex, - "fsh" => Self::Fragment, - "gsh" => Self::Geometry, - "csh" => Self::Compute, - "tcs" => Self::TessControl, - "tes" => Self::TessEval, - _ => return None, - }) - } -} - -/// An opened Java shader pack. -pub struct JavaPack { - pub files: PackFiles, - /// Every program source file (`composite1.fsh`, `world-1/final.vsh`). - pub programs: Vec, - pub graph: IncludeGraph, - annotations: BTreeMap, - /// The options the player can set (`ShaderPackOptions`). - pub options: OptionSet, -} - -impl JavaPack { - /// Open the pack folder or zip at `path` and read every program and file - /// they include. - pub fn open(path: &Path) -> Result { - let files = PackFiles::open(path)?; - let programs = files.programs(); - if programs.is_empty() { - return Err("no programs in shaders/".into()); - } - let graph = IncludeGraph::build(&files, &programs); - let annotations: BTreeMap = graph - .files - .iter() - .map(|(path, node)| (path.clone(), Annotated::parse(&node.lines))) - .collect(); - // A boolean #define is an option when an #ifdef in its component - // refers to it. - let mut options = OptionSet::default(); - for component in graph.components() { - let refs: BTreeSet<&String> = component - .iter() - .flat_map(|f| &annotations[f].ifdef_refs) - .collect(); - for file in &component { - let a = &annotations[file]; - a.bools - .values() - .filter(|o| refs.contains(&o.name)) - .for_each(|o| options.add_bool(o)); - a.strings.values().for_each(|o| options.add_string(o)); - } - } - Ok(Self { - files, - programs, - graph, - annotations, - options, - }) - } - - /// The player's values from saved `name=value` pairs (`shaderpacks/.txt`). - pub fn values(&self, saved: &BTreeMap) -> OptionValues { - OptionValues::new(&self.options, saved) - } - - /// `program` with its includes inlined and `values` applied. - pub fn source(&self, program: &str, values: &OptionValues) -> Result { - self.graph - .inline(program, &|file, i, line| match self.annotations.get(file) { - Some(a) => a.edit(values, i, line), - None => line.to_owned(), - }) - } - - /// `program` as plain GLSL: [`JavaPack::source`], then the preprocessor. - pub fn preprocess( - &self, - program: &str, - values: &OptionValues, - env: &Environment, - ) -> Result { - preprocess::preprocess(&self.source(program, values)?, env) - } - - /// `program` translated to WGSL. The programs of one pass (its `.vsh`, - /// then its `.fsh`) share `link`. - pub fn translate( - &self, - program: &str, - values: &OptionValues, - env: &Environment, - link: &mut Link, - ) -> Result { - let stage = Stage::of(program).ok_or_else(|| format!("{program}: not a program"))?; - let source = self.source(program, values)?; - let pre = preprocess::preprocess(&source, env)?; - translate::translate(stage, &pre.text, link, &|line| source.locate(line)) - .map_err(|e| format!("{program}: {e}")) - } - - /// The pack's `shaders.properties` with `values` (empty when it has none). - pub fn properties( - &self, - values: &OptionValues, - env: &Environment, - ) -> Result { - match self.files.read("shaders.properties") { - Some(text) => ShaderProperties::new(&text, env, &self.options, values), - None => Ok(ShaderProperties::default()), - } - } - - /// The pack's language translations (`shaders/lang/.lang` or - /// `en_US.lang`), mapping keys such as `option.SHADOW_QUALITY`, - /// `value.SHADOW_QUALITY.2`, `screen.LIGHTING`, and `profile.HIGH` to their - /// display labels. - pub fn translations(&self, lang_code: &str) -> BTreeMap { - let mut out = BTreeMap::new(); - let lower = lang_code.to_lowercase(); - let upper = match lower.split_once('_') { - Some((l, r)) => format!("{l}_{}", r.to_uppercase()), - None => lower.clone(), - }; - let mut candidates = vec!["lang/en_US.lang".to_owned(), "lang/en_us.lang".to_owned()]; - if lower != "en_us" { - candidates.push(format!("lang/{lower}.lang")); - candidates.push(format!("lang/{upper}.lang")); - } - for path in candidates { - if let Some(text) = self.files.read(&path) { - for (k, v) in Properties::parse(&text).iter() { - out.insert(k.to_owned(), v.to_owned()); - } - } - } - out - } -} - -impl JavaPack { - /// The block ids of the pack's `block.properties` (empty without one), - /// [`properties::block_ids`]. - pub fn block_ids( - &self, - values: &OptionValues, - env: &Environment, - ) -> Result, String> { - match self.files.read("block.properties") { - Some(text) => properties::block_ids(&text, env, &self.options, values) - .map_err(|e| format!("block.properties: {e}")), - None => Ok(BTreeMap::new()), - } - } -} - -/// A program's name in `shaders.properties` (`world-1/composite1.fsh` is -/// `world-1/composite1`). -pub fn program_name(program: &str) -> &str { - program.rsplit_once('.').map_or(program, |(name, _)| name) -} diff --git a/crates/horizon-shaderpack/src/options.rs b/crates/horizon-shaderpack/src/options.rs deleted file mode 100644 index 3158b12cf..000000000 --- a/crates/horizon-shaderpack/src/options.rs +++ /dev/null @@ -1,669 +0,0 @@ -//! A pack's options, found in its source the way Iris finds them -//! (`OptionAnnotatedSource`, `ParsedString`, `OptionSet`), and the player's -//! values written back into the lines they came from. -//! -//! - `#define NAME` / `//#define NAME` (optionally `// comment`): a boolean, -//! on unless commented out, and an option only if some `#ifdef NAME` / -//! `#ifndef NAME` in the same include component refers to it. -//! - `#define NAME VALUE // comment [A B C]`: a value with its allowed list. -//! - `const int|float|bool NAME = VALUE; // [A B C]`: only for the constants -//! Iris lets players configure ([`CONST_OPTIONS`]). - -use std::collections::{BTreeMap, BTreeSet}; - -/// The `const` names Iris configures (`VALID_CONST_OPTION_NAMES`), apart from -/// the per-buffer ones [`is_const_option`] adds. -pub const CONST_OPTIONS: &[&str] = &[ - "shadowMapResolution", - "shadowDistance", - "voxelDistance", - "shadowDistanceRenderMul", - "entityShadowDistanceMul", - "shadowIntervalSize", - "generateShadowMipmap", - "generateShadowColorMipmap", - "shadowHardwareFiltering", - "shadowtex0Mipmap", - "shadowtexMipmap", - "shadowtex1Mipmap", - "shadowtex0Nearest", - "shadowtexNearest", - "shadow0MinMagNearest", - "shadowtex1Nearest", - "shadow1MinMagNearest", - "wetnessHalflife", - "drynessHalflife", - "eyeBrightnessHalflife", - "centerDepthHalflife", - "sunPathRotation", - "ambientOcclusionLevel", - "superSamplingLevel", - "noiseTextureResolution", -]; - -/// Iris's shadow colour buffer count (`MAX_SHADOW_COLOR_BUFFERS_IRIS`). -const SHADOW_COLOR_BUFFERS: u32 = 8; - -pub fn is_const_option(name: &str) -> bool { - CONST_OPTIONS.contains(&name) - || (0..SHADOW_COLOR_BUFFERS).any(|i| { - [ - format!("shadowcolor{i}Mipmap"), - format!("shadowColor{i}Mipmap"), - format!("shadowcolor{i}Nearest"), - format!("shadowColor{i}Nearest"), - format!("shadowcolor{i}MinMagNearest"), - format!("shadowColor{i}MinMagNearest"), - format!("shadowHardwareFiltering{i}"), - ] - .iter() - .any(|n| n == name) - }) -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum OptionKind { - Define, - Const, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct BoolOption { - pub kind: OptionKind, - pub name: String, - pub comment: Option, - pub default: bool, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct StringOption { - pub kind: OptionKind, - pub name: String, - pub comment: Option, - pub default: String, - /// The bracketed list, with the default added at the end when missing. - pub allowed: Vec, -} - -impl StringOption { - /// `StringOption.create`: `None` without a `[...]` list in the comment. - fn create(kind: OptionKind, name: &str, comment: Option<&str>, default: &str) -> Option { - let comment = comment?; - let open = comment.find('[')?; - let close = open + comment[open..].find(']')?; - // Java's split(" "): empty strings between doubled spaces stay, - // trailing ones go. - let mut allowed: Vec = comment[open + 1..close] - .split(' ') - .map(str::to_owned) - .collect(); - while allowed.last().is_some_and(String::is_empty) { - allowed.pop(); - } - if !allowed.iter().any(|v| v == default) { - allowed.push(default.to_owned()); - } - let rest = format!("{}{}", &comment[..open], &comment[close + 1..]); - Some(Self { - kind, - name: name.to_owned(), - comment: non_empty(&rest), - default: default.to_owned(), - allowed, - }) - } -} - -/// An option's comment; empty is none (`BaseOption`). -fn non_empty(comment: &str) -> Option { - let c = comment.trim(); - (!c.is_empty()).then(|| c.to_owned()) -} - -/// Iris's `ParsedString`: a cursor over a trimmed line. -struct Cursor(Vec); - -impl Cursor { - fn new(s: &str) -> Self { - Self(s.chars().collect()) - } - fn literal(&mut self, token: &str) -> bool { - let t: Vec = token.chars().collect(); - if self.0.starts_with(&t) { - self.0.drain(..t.len()); - true - } else { - false - } - } - /// Whitespace first, then trims both ends (as Iris does). - fn whitespace(&mut self) -> bool { - if !self.0.first().is_some_and(|c| c.is_whitespace()) { - return false; - } - let s: String = self.0.iter().collect(); - self.0 = s.trim().chars().collect(); - true - } - /// `//` and any further slashes. - fn comments(&mut self) -> bool { - if !self.literal("//") { - return false; - } - while self.literal("/") {} - true - } - fn contains(&self, s: &str) -> bool { - self.rest().contains(s) - } - fn is_end(&self) -> bool { - self.0.is_empty() - } - fn rest(&self) -> String { - self.0.iter().collect() - } - fn take(&mut self, n: usize) -> String { - self.0.drain(..n).collect() - } - fn word(&mut self) -> Option { - let n = self - .0 - .iter() - .take_while(|c| c.is_alphanumeric() || **c == '_') - .count(); - (n > 0).then(|| self.take(n)) - } - /// Iris's `takeNumber`: digits and any character next to a digit, an `f` - /// suffix, and the whole must parse as a Java float. - fn number(&mut self) -> Option { - let c = &self.0; - let mut n = 0; - while n < c.len() { - if n + 1 < c.len() { - if !c[n].is_ascii_digit() && !c[n + 1].is_ascii_digit() { - break; - } - } else if !c[n].is_ascii_digit() { - break; - } - n += 1; - } - if n > 0 && n + 1 < c.len() && (c[n] == 'f' || c[n] == 'F') { - n += 1; - } - let text: String = c[..n].iter().collect(); - java_float(&text).then(|| self.take(n)) - } - fn word_or_number(&mut self) -> Option { - self.number().or_else(|| self.word()) - } -} - -/// Whether Java's `Float.parseFloat` accepts `s` (decimal forms; a trailing -/// `f`/`d` is allowed). -fn java_float(s: &str) -> bool { - let s = s.trim(); - let s = s.strip_suffix(['f', 'F', 'd', 'D']).unwrap_or(s); - !s.is_empty() && s.parse::().is_ok() -} - -/// The options and `#ifdef` references of one file, by line index. -#[derive(Default, Debug)] -pub struct Annotated { - pub bools: BTreeMap, - pub strings: BTreeMap, - /// Names tested by a bare `#ifdef NAME` / `#ifndef NAME` line. - pub ifdef_refs: BTreeSet, - /// Why a line that looks like an option isn't one. - pub diagnostics: BTreeMap, -} - -impl Annotated { - pub fn parse(lines: &[String]) -> Self { - let mut out = Self::default(); - for (i, line) in lines.iter().enumerate() { - if !["#define", "const", "#ifdef", "#ifndef"] - .iter() - .any(|k| line.contains(k)) - { - continue; - } - let mut c = Cursor::new(line.trim()); - if c.literal("#ifdef") || c.literal("#ifndef") { - if !c.whitespace() { - continue; - } - let name = c.word(); - c.whitespace(); - if let (Some(name), true) = (name, c.is_end()) { - out.ifdef_refs.insert(name); - } - } else if c.literal("const") { - if let Err(why) = out.parse_const(i, &mut c) { - out.diagnostics.insert(i, why.into()); - } - } else if c.contains("#define") { - if let Err(why) = out.parse_define(i, &mut c) { - out.diagnostics.insert(i, why.into()); - } - } - } - out - } - - fn parse_const(&mut self, i: usize, c: &mut Cursor) -> Result<(), &'static str> { - if !c.whitespace() { - return Err("expected whitespace after const"); - } - let is_bool = if c.literal("int") || c.literal("float") { - false - } else if c.literal("bool") { - true - } else { - return Err("only int, float and bool consts are options"); - }; - if !c.whitespace() { - return Err("expected whitespace after the type"); - } - let name = c.word().ok_or("expected a name after the type")?; - c.whitespace(); - if !c.literal("=") { - return Err("expected = after the name"); - } - c.whitespace(); - let value = c - .word_or_number() - .ok_or("expected a word or number after =")?; - c.whitespace(); - if !c.literal(";") { - return Err("expected ; after the value"); - } - c.whitespace(); - let comment = if c.comments() { - non_empty(&c.rest()) - } else if !c.is_end() { - return Err("only a comment may follow the ;"); - } else { - None - }; - if !is_const_option(&name) { - return Err("not one of the configurable consts"); - } - if is_bool { - let default = match value.as_str() { - "true" => true, - "false" => false, - _ => return Err("a bool const option must be true or false"), - }; - self.bools.insert( - i, - BoolOption { - kind: OptionKind::Const, - name, - comment, - default, - }, - ); - return Ok(()); - } - let option = StringOption::create(OptionKind::Const, &name, comment.as_deref(), &value) - .ok_or("a const option needs a [...] list of values in its comment")?; - self.strings.insert(i, option); - Ok(()) - } - - fn parse_define(&mut self, i: usize, c: &mut Cursor) -> Result<(), &'static str> { - let commented = c.comments(); - c.whitespace(); - if !c.literal("#define") { - return Err("#define isn't where an option's would be"); - } - if !c.whitespace() { - return Err("expected whitespace after #define"); - } - let name = c.word().ok_or("expected a name after #define")?; - let mut spaced = c.whitespace(); - if c.is_end() { - self.bools.insert( - i, - BoolOption { - kind: OptionKind::Define, - name, - comment: None, - default: !commented, - }, - ); - return Ok(()); - } - if c.comments() { - let comment = non_empty(&c.rest()); - self.bools.insert( - i, - BoolOption { - kind: OptionKind::Define, - name, - comment, - default: !commented, - }, - ); - return Ok(()); - } - if !spaced { - return Err("only letters, digits and _ may be in an option's name"); - } - if commented { - return Err("only boolean #define options may be commented out"); - } - let value = c - .word_or_number() - .ok_or("the value is neither a number nor a word")?; - spaced = c.whitespace(); - if c.is_end() { - return Err("a value #define needs a comment with a [...] list of values"); - } - if !c.comments() { - return Err(if spaced { - "only a comment may follow the value" - } else { - "unexpected characters after the value" - }); - } - let comment = c.rest().trim().to_owned(); - let option = StringOption::create(OptionKind::Define, &name, Some(&comment), &value) - .ok_or("a value #define needs a [...] list of values in its comment")?; - self.strings.insert(i, option); - Ok(()) - } - - /// Line `i` (text `line`) with the player's values applied - /// (`OptionAnnotatedSource.edit`). - pub fn edit(&self, values: &OptionValues, i: usize, line: &str) -> String { - if let Some(o) = self.bools.get(&i) { - let value = values.bools.get(&o.name).copied(); - return match o.kind { - OptionKind::Define => { - let on = value.unwrap_or(o.default); - let commented = line.trim().starts_with("//"); - if commented && on { - let mut c = Cursor::new(line); - c.whitespace(); - c.comments(); - c.rest() - } else if !on { - format!("//{line}") - } else { - line.to_owned() - } - } - OptionKind::Const => match value { - Some(v) => edit_const(line, &o.default.to_string(), &v.to_string()), - None => line.to_owned(), - }, - }; - } - if let Some(o) = self.strings.get(&i) { - if let Some(v) = values.strings.get(&o.name) { - return match o.kind { - OptionKind::Define => format!( - "#define {} {v} // OptionAnnotatedSource: Changed option", - o.name - ), - OptionKind::Const => edit_const(line, &o.default, v), - }; - } - } - line.to_owned() - } -} - -/// The first `current` after the `=` replaced by `new`. -fn edit_const(line: &str, current: &str, new: &str) -> String { - match line.find('=') { - Some(eq) => format!("{}{}", &line[..eq], line[eq..].replacen(current, new, 1)), - None => line.to_owned(), - } -} - -/// Every option of a pack by name. Two definitions with different defaults -/// make the name ambiguous: Iris drops it. -#[derive(Default, Debug)] -pub struct OptionSet { - pub bools: BTreeMap, - pub strings: BTreeMap, -} - -impl OptionSet { - pub fn add_bool(&mut self, o: &BoolOption) { - match self.bools.get_mut(&o.name) { - Some(existing) if existing.default != o.default => { - self.bools.remove(&o.name); - } - Some(existing) => { - if existing.comment.is_none() { - *existing = o.clone(); - } - } - None => { - self.bools.insert(o.name.clone(), o.clone()); - } - } - } - - pub fn add_string(&mut self, o: &StringOption) { - match self.strings.get_mut(&o.name) { - Some(existing) if existing.default != o.default => { - self.strings.remove(&o.name); - } - Some(existing) => { - if existing.comment.is_none() { - *existing = o.clone(); - } - } - None => { - self.strings.insert(o.name.clone(), o.clone()); - } - } - } -} - -/// The player's values that differ from the defaults (`MutableOptionValues`). -#[derive(Default, Debug, Clone, PartialEq)] -pub struct OptionValues { - pub bools: BTreeMap, - pub strings: BTreeMap, -} - -impl OptionValues { - /// From saved `name=value` pairs. A boolean other than `true`/`false` is - /// ignored; a value outside an option's allowed list is kept (profiles and - /// hand-edited files may set one, as in OptiFine). - pub fn new(options: &OptionSet, saved: &BTreeMap) -> Self { - let mut out = Self::default(); - for (name, o) in &options.bools { - let value = match saved.get(name).map(String::as_str) { - Some("true") => true, - Some("false") => false, - _ => continue, - }; - if value != o.default { - out.bools.insert(name.clone(), value); - } - } - // A saved value the option doesn't list (an old or hand-edited - // file) is dropped, as OptiFine's `ShaderOption.setValue` refuses it. - for (name, o) in &options.strings { - if let Some(v) = saved.get(name) - && *v != o.default - && o.allowed.contains(v) - { - out.strings.insert(name.clone(), v.clone()); - } - } - out - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn one(line: &str) -> Annotated { - Annotated::parse(&[line.to_owned()]) - } - - fn string(line: &str) -> Option<(String, String, Vec)> { - one(line) - .strings - .into_values() - .next() - .map(|o| (o.name, o.default, o.allowed)) - } - - #[test] - fn value_defines_need_an_allowed_list() { - // Iris's OptionDiscoverTest cases. - let s = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::>(); - assert_eq!( - string("#define SECRET 0 //[0]"), - Some(("SECRET".into(), "0".into(), s(&["0"]))) - ); - assert_eq!( - string("#define SECRET 0//[0]"), - Some(("SECRET".into(), "0".into(), s(&["0"]))) - ); - assert_eq!( - string("#define OPT 1 //[0]"), - Some(("OPT".into(), "1".into(), s(&["0", "1"]))) - ); - for none in [ - "#define PI 3.14 // It's PI.", - "#define PI 3.14", - "const int noiseTextureResolution = 512;", - "const int noiseTextureResolution = 512; // Default noise texture size [", - ] { - let a = one(none); - assert!(a.strings.is_empty() && a.bools.is_empty(), "{none}"); - } - assert_eq!( - string(" #define SHADOW_QUALITY 2.0f // Shadows [1.0f 2.0f 3.0f]"), - Some(( - "SHADOW_QUALITY".into(), - "2.0f".into(), - s(&["1.0f", "2.0f", "3.0f"]) - )) - ); - assert_eq!( - string("#define SKY_STYLE SKY_REALISTIC // [SKY_REALISTIC SKY_STYLISED]") - .unwrap() - .1, - "SKY_REALISTIC" - ); - assert_eq!( - string("const int shadowMapResolution = 2048; // [1024 2048 4096]") - .unwrap() - .0, - "shadowMapResolution" - ); - assert!(string("const int myResolution = 2048; // [1024 2048 4096]").is_none()); - assert!(string("//#define SHADOW_QUALITY 2 // [1 2 3]").is_none()); - } - - #[test] - fn boolean_defines_and_references() { - let a = Annotated::parse(&[ - "#define BLOOM // Bloom".into(), - "//#define DOF".into(), - "// #define MOTION_BLUR // Blur".into(), - "#define FOG(x) x".into(), - "#ifdef BLOOM".into(), - "#ifndef DOF // trailing text: not a reference".into(), - "#if defined MOTION_BLUR".into(), - "const bool shadowHardwareFiltering = true; // Filtering".into(), - ]); - let b: Vec<(&str, bool, Option<&str>)> = a - .bools - .values() - .map(|o| (o.name.as_str(), o.default, o.comment.as_deref())) - .collect(); - assert_eq!( - b, - [ - ("BLOOM", true, Some("Bloom")), - ("DOF", false, None), - ("MOTION_BLUR", false, Some("Blur")), - ("shadowHardwareFiltering", true, Some("Filtering")) - ] - ); - assert_eq!(a.ifdef_refs.iter().collect::>(), ["BLOOM"]); - } - - #[test] - fn values_are_written_into_the_lines() { - let lines: Vec = [ - "#define BLOOM", - "//#define DOF", - "#define QUALITY 2 // [1 2 3]", - "const float sunPathRotation = -40.0; // [-40.0 0.0 40.0]", - "const bool shadowHardwareFiltering = false;", - ] - .map(String::from) - .into(); - let a = Annotated::parse(&lines); - let mut set = OptionSet::default(); - a.bools.values().for_each(|o| set.add_bool(o)); - a.strings.values().for_each(|o| set.add_string(o)); - let saved: BTreeMap = [ - ("BLOOM", "false"), - ("DOF", "true"), - ("QUALITY", "3"), - ("sunPathRotation", "0.0"), - ("shadowHardwareFiltering", "yes"), - ] - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .into(); - let values = OptionValues::new(&set, &saved); - assert!( - !values.bools.contains_key("shadowHardwareFiltering"), - "not a boolean: ignored" - ); - let stale = [("QUALITY", "abc"), ("sunPathRotation", "")] - .map(|(k, v)| (k.to_owned(), v.to_owned())) - .into(); - assert!( - OptionValues::new(&set, &stale).strings.is_empty(), - "values the options don't list: ignored" - ); - let edited: Vec = lines - .iter() - .enumerate() - .map(|(i, l)| a.edit(&values, i, l)) - .collect(); - assert_eq!( - edited, - [ - "//#define BLOOM", - "#define DOF", - "#define QUALITY 3 // OptionAnnotatedSource: Changed option", - "const float sunPathRotation = 0.0; // [-40.0 0.0 40.0]", - "const bool shadowHardwareFiltering = false;", - ] - ); - } - - #[test] - fn conflicting_defaults_drop_the_option() { - let mut set = OptionSet::default(); - let a = Annotated::parse(&["#define Q 1 // [1 2]".into()]); - let b = Annotated::parse(&["#define Q 2 // Quality [1 2]".into()]); - let c = Annotated::parse(&["#define Q 1 // Quality [1 2]".into()]); - set.add_string(&a.strings[&0]); - set.add_string(&c.strings[&0]); - assert_eq!( - set.strings["Q"].comment.as_deref(), - Some("Quality"), - "the commented one wins" - ); - set.add_string(&b.strings[&0]); - assert!(set.strings.is_empty()); - } -} diff --git a/crates/horizon-shaderpack/src/pipeline.rs b/crates/horizon-shaderpack/src/pipeline.rs deleted file mode 100644 index f26597e09..000000000 --- a/crates/horizon-shaderpack/src/pipeline.rs +++ /dev/null @@ -1,1075 +0,0 @@ -//! Iris's full-screen passes and the colour targets they draw to: which -//! programs run (`begin*`, `prepare*`, `deferred*`, `composite*`, `final`), -//! the targets each one writes (`/* DRAWBUFFERS:01 */`, `/* RENDERTARGETS: -//! 0,1 */`), the targets' formats and clearing (`const int colortex0Format -//! = RGBA16F;` and friends), and which side of each ping-ponged target a -//! pass reads (`BufferFlipper`). Read from the preprocessed sources with -//! their comments, as Iris's `ProgramDirectives`, `PackRenderTargetDirectives` -//! and `CompositeRenderer` do. - -use crate::{ - JavaPack, - options::OptionValues, - preprocess::Environment, - properties::{Blend, LEGACY_TARGETS, ShaderProperties}, - translate::{Geometry, Link, Translated}, -}; -use std::collections::{BTreeMap, BTreeSet}; - -/// Colour targets Iris supports (`IrisLimits.MAX_COLOR_BUFFERS`). -pub const MAX_COLOR_BUFFERS: u32 = 32; - -/// The formats `const int colortexNFormat` may name (`InternalTextureFormat`). -pub const TEXTURE_FORMATS: &[&str] = &[ - "RGBA", - "R8", - "RG8", - "RGB8", - "RGBA8", - "R8_SNORM", - "RG8_SNORM", - "RGB8_SNORM", - "RGBA8_SNORM", - "R16", - "RG16", - "RGB16", - "RGBA16", - "R16_SNORM", - "RG16_SNORM", - "RGB16_SNORM", - "RGBA16_SNORM", - "R16F", - "RG16F", - "RGB16F", - "RGBA16F", - "R32F", - "RG32F", - "RGB32F", - "RGBA32F", - "R8I", - "RG8I", - "RGB8I", - "RGBA8I", - "R8UI", - "RG8UI", - "RGB8UI", - "RGBA8UI", - "R16I", - "RG16I", - "RGB16I", - "RGBA16I", - "R16UI", - "RG16UI", - "RGB16UI", - "RGBA16UI", - "R32I", - "RG32I", - "RGB32I", - "RGBA32I", - "R32UI", - "RG32UI", - "RGB32UI", - "RGBA32UI", - "RGBA2", - "RGBA4", - "R3_G3_B2", - "RGB5_A1", - "RGB565", - "RGB10_A2", - "RGB10_A2UI", - "R11F_G11F_B10F", - "RGB9_E5", -]; - -/// The value of the last `/* NAME:value */` in `text` and where it starts -/// (`CommentDirectiveParser.findDirective`). -pub fn comment_directive(text: &str, name: &str) -> Option<(usize, String)> { - let prefix = format!("{name}:"); - let at = text.rfind(&prefix)?; - if !text[..at].trim_end().ends_with("/*") { - return None; - } - let rest = &text[at + prefix.len()..]; - let end = rest.find("*/")?; - Some((at, rest[..end].trim().to_owned())) -} - -/// A `const = ;` line (`ConstDirectiveParser`). -#[derive(Clone, Debug, PartialEq)] -pub struct ConstDirective { - /// `int`, `float`, `vec2`, `ivec3`, `vec4` or `bool`. - pub ty: &'static str, - pub key: String, - pub value: String, -} - -/// Every line of `text` that starts with a `const` Iris reads, in order. -pub fn const_directives(text: &str) -> Vec { - text.lines().filter_map(const_directive).collect() -} - -fn const_directive(line: &str) -> Option { - if !line.contains("const") || !line.contains('=') || !line.contains(';') { - return None; - } - let rest = line.trim().strip_prefix("const")?; - if !rest.starts_with(char::is_whitespace) { - return None; - } - let rest = rest.trim_start(); - // In Iris's order: `ivec3` before `vec4` doesn't matter, but `int` must - // not take `ivec3`'s place, and it can't (`ivec3` starts with `i`, `v`). - let ty = ["int", "float", "vec2", "ivec3", "vec4", "bool"] - .into_iter() - .find(|t| rest.starts_with(t))?; - let rest = &rest[ty.len()..]; - if !rest.starts_with(char::is_whitespace) { - return None; - } - let (key, value) = rest.split_once('=')?; - let key = key.trim(); - if key.is_empty() || !key.chars().all(|c| c.is_alphanumeric() || c == '_') { - return None; - } - let value = value.split_once(';')?.0.trim(); - Some(ConstDirective { - ty, - key: key.to_owned(), - value: value.to_owned(), - }) -} - -/// The targets a fragment program writes, by output location: the later of -/// its `DRAWBUFFERS` and `RENDERTARGETS` directives, or `None` without one -/// (Iris then takes the `gl_FragData` indices it writes). -pub fn draw_buffers(fragment: &str) -> Result>, String> { - let draw = comment_directive(fragment, "DRAWBUFFERS"); - let render = comment_directive(fragment, "RENDERTARGETS"); - let (digits, value) = match (draw, render) { - (Some(d), Some(r)) if d.0 > r.0 => (true, d.1), - (_, Some(r)) => (false, r.1), - (Some(d), None) => (true, d.1), - (None, None) => return Ok(None), - }; - let bad = || format!("invalid render targets `{value}`"); - let targets: Vec = if digits { - value - .chars() - .map(|c| c.to_digit(10).ok_or_else(bad)) - .collect::>()? - } else { - value - .split(',') - .map(|t| t.trim().parse().map_err(|_| bad())) - .collect::>()? - }; - match targets.iter().find(|&&t| t >= MAX_COLOR_BUFFERS) { - Some(t) => Err(format!( - "render target {t} is not supported, only {MAX_COLOR_BUFFERS} are" - )), - None => Ok(Some(targets)), - } -} - -/// The index of a target name, `colortex` or a legacy one (`gaux1`). -pub fn target_index(name: &str) -> Option { - match LEGACY_TARGETS.iter().position(|t| *t == name) { - Some(i) => Some(i as u32), - None => name - .strip_prefix("colortex")? - .parse() - .ok() - .filter(|&i| i < MAX_COLOR_BUFFERS), - } -} - -/// A key naming a target and a suffix (`colortex4Format`, `gaux1Clear`). -fn target_key(key: &str, suffix: &str) -> Option { - target_index(key.strip_suffix(suffix)?) -} - -/// A colour target's settings (`RenderTargetSettings`, `size.buffer.*`). -#[derive(Clone, Debug, PartialEq)] -pub struct TargetSettings { - /// One of [`TEXTURE_FORMATS`]; `RGBA` (8 bits per channel) by default. - pub format: String, - /// Cleared every frame (the default), or kept for the next frame. - pub clear: bool, - /// Iris clears to black, or `colortex0` to the fog colour, without one. - pub clear_color: Option<[f32; 4]>, - /// `size.buffer.`: a fraction of the screen or a size in pixels. - pub size: Option, -} - -impl Default for TargetSettings { - fn default() -> Self { - Self { - format: "RGBA".into(), - clear: true, - clear_color: None, - size: None, - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum TargetSize { - Relative(f32, f32), - Pixels(u32, u32), -} - -/// Every target's settings from the consts of `fragments`, read in order so -/// the last one wins, then `size.buffer.*` from the properties. -pub fn target_settings<'a>( - fragments: impl IntoIterator, - properties: &ShaderProperties, -) -> Vec { - let mut targets = vec![TargetSettings::default(); MAX_COLOR_BUFFERS as usize]; - for fragment in fragments { - for c in const_directives(fragment) { - match c.ty { - "int" => { - if let Some(i) = target_key(&c.key, "Format") { - let format = c.value.to_uppercase(); - if TEXTURE_FORMATS.contains(&format.as_str()) { - targets[i as usize].format = format; - } - } - } - "bool" => { - if let (Some(i), Ok(clear)) = (target_key(&c.key, "Clear"), c.value.parse()) { - targets[i as usize].clear = clear; - } - } - "vec4" => { - if let (Some(i), Some(color)) = - (target_key(&c.key, "ClearColor"), parse_vec4(&c.value)) - { - targets[i as usize].clear_color = Some(color); - } - } - _ => {} - } - } - } - for (key, value) in properties.preprocessed.iter() { - let Some(i) = key.strip_prefix("size.buffer.").and_then(target_index) else { - continue; - }; - let parts: Vec<&str> = value.split_whitespace().collect(); - if let [w, h] = parts[..] { - targets[i as usize].size = if w.contains('.') || h.contains('.') { - w.parse() - .ok() - .zip(h.parse().ok()) - .map(|(w, h)| TargetSize::Relative(w, h)) - } else { - w.parse() - .ok() - .zip(h.parse().ok()) - .map(|(w, h)| TargetSize::Pixels(w, h)) - }; - } - } - targets -} - -fn parse_vec4(value: &str) -> Option<[f32; 4]> { - let args = value.strip_prefix("vec4")?.trim(); - let args = args.strip_prefix('(')?.strip_suffix(')')?; - let parts: Vec = args - .split(',') - .map(|p| p.trim().parse().ok()) - .collect::>()?; - parts.try_into().ok() -} - -/// One full-screen pass. -#[derive(Clone, Debug, PartialEq)] -pub struct Pass { - /// The program's path without extension (`world0/composite1`). - pub program: String, - /// What it writes, by output location; `None`: the outputs it writes. - pub draw_buffers: Option>, - /// Targets it reads from their alternate side (it writes the other side). - pub reads_alt: BTreeSet, - /// Targets whose mipmaps it wants generated before it runs. - pub mipmapped: BTreeSet, - /// `scale.`: the fraction of the screen it draws. - pub scale: f32, - /// `blend.` override for all outputs of this pass. - pub blend: Option, - /// `blend..` overrides per colour target index. - pub buffer_blends: Vec<(u32, Blend)>, -} - -/// The full-screen passes of one program folder, in order, and the -/// flipping state between them. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Passes { - pub targets: Vec, - pub begin: Vec, - pub prepare: Vec, - /// The targets gbuffers programs (and Horizon's world) write on their - /// alternate side, before and after translucent geometry. - pub flipped_after_prepare: BTreeSet, - /// The target Horizon's world goes into: the first one `gbuffers_terrain` - /// (or the program Iris falls back to for terrain) draws to, `colortex0` - /// without one. - pub world_target: u32, - pub deferred: Vec, - pub flipped_after_translucent: BTreeSet, - pub composite: Vec, - /// `final`, which writes the screen; `None` without one (Iris then - /// copies `colortex0` to the screen). - pub final_pass: Option, - /// After `final`, these targets' alternate side is copied back so the - /// next frame starts unflipped. - pub flipped_at_end: BTreeSet, - pub directives: Directives, - /// The program Horizon's solid terrain is drawn with - /// ([`TERRAIN_PROGRAMS`]), before `deferred`. - pub terrain: Option, - /// The program its translucent terrain is drawn with - /// ([`WATER_PROGRAMS`]), after `deferred`. - pub water: Option, - /// The programs the shadow map's solid and translucent terrain are - /// drawn with ([`SHADOW_PROGRAMS`], [`SHADOW_WATER_PROGRAMS`]), after - /// `begin`. Their draw buffers are `shadowcolor` targets. - pub shadow: Option, - pub shadow_water: Option, - /// `shadowcolor0..7`: white and cleared each frame unless the pack - /// says otherwise. - pub shadow_targets: Vec, -} - -/// The programs Iris draws the shadow map's solid terrain with. Horizon's -/// solid layer holds cutout blocks too, so `shadow_cutout` (with its alpha -/// test) comes first. -pub const SHADOW_PROGRAMS: &[&str] = &["shadow_cutout", "shadow", "shadow_solid"]; - -/// The programs Iris draws the shadow map's translucent terrain with. -pub const SHADOW_WATER_PROGRAMS: &[&str] = &["shadow_water", "shadow"]; - -/// How many `shadowcolor` targets Iris has. -pub const MAX_SHADOW_COLOR_BUFFERS: u32 = 8; - -/// `shadowcolorFormat`, `Clear` and `ClearColor` from `fragments`. -pub fn shadow_target_settings<'a>( - fragments: impl IntoIterator, -) -> Vec { - let mut targets = vec![ - TargetSettings { - clear_color: Some([1.0; 4]), - ..TargetSettings::default() - }; - MAX_SHADOW_COLOR_BUFFERS as usize - ]; - let key = |key: &str, suffix: &str| { - let i: u32 = key - .strip_prefix("shadowcolor")? - .strip_suffix(suffix)? - .parse() - .ok()?; - (i < MAX_SHADOW_COLOR_BUFFERS).then_some(i as usize) - }; - for fragment in fragments { - for c in const_directives(fragment) { - match c.ty { - "int" => { - if let Some(i) = key(&c.key, "Format") { - let format = c.value.to_uppercase(); - if TEXTURE_FORMATS.contains(&format.as_str()) { - targets[i].format = format; - } - } - } - "bool" => { - if let (Some(i), Ok(clear)) = (key(&c.key, "Clear"), c.value.parse()) { - targets[i].clear = clear; - } - } - "vec4" => { - if let (Some(i), Some(color)) = - (key(&c.key, "ClearColor"), parse_vec4(&c.value)) - { - targets[i].clear_color = Some(color); - } - } - _ => {} - } - } - } - targets -} - -/// The programs Iris draws solid terrain with, the first the pack has -/// (`ProgramId.Terrain`'s fallbacks). -pub const TERRAIN_PROGRAMS: &[&str] = &[ - "gbuffers_terrain", - "gbuffers_textured_lit", - "gbuffers_textured", - "gbuffers_basic", -]; - -/// The programs Iris draws translucent terrain with (`ProgramId.Water`'s -/// fallbacks). -pub const WATER_PROGRAMS: &[&str] = &[ - "gbuffers_water", - "gbuffers_terrain", - "gbuffers_textured_lit", - "gbuffers_textured", - "gbuffers_basic", -]; - -/// The `const` directives of a program folder that aren't about targets -/// (`PackDirectives`), read from the same programs in the same order. -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Directives { - /// `sunPathRotation`, degrees: how far the sun and moon's path tilts - /// toward the north or south (a Z rotation between the sky's Y -90 and - /// its angle, `CelestialUniforms`). - pub sun_path_rotation: f32, - /// `eyeBrightnessHalflife`, tenths of a second: `eyeBrightnessSmooth`. - pub eye_brightness_half_life: f32, - /// `wetnessHalflife`, tenths of a second: `wetness` smoothing when raining. - pub wetness_half_life: f32, - /// `drynessHalflife`, tenths of a second: `wetness` smoothing when dry. - pub dryness_half_life: f32, - /// The shadow map's directives. - pub shadow: ShadowDirectives, -} - -impl Default for Directives { - fn default() -> Self { - Self { - sun_path_rotation: 0.0, - eye_brightness_half_life: 10.0, - wetness_half_life: 600.0, - dryness_half_life: 200.0, - shadow: ShadowDirectives::default(), - } - } -} - -/// The shadow map's `const` directives (`PackShadowDirectives`), with -/// Iris's defaults. -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct ShadowDirectives { - /// `shadowMapResolution`: the shadow targets' width and height. - pub resolution: u32, - /// `shadowDistance`: half the side of the orthographic projection. - pub distance: f32, - /// `shadowNearPlane` and `shadowFarPlane` (`ShadowMatrices.NEAR`/`FAR`). - pub near: f32, - pub far: f32, - /// `shadowIntervalSize`: the light's view snaps to a grid this size as - /// the camera moves, so shadows don't shimmer. - pub interval: f32, -} - -impl Default for ShadowDirectives { - fn default() -> Self { - Self { - resolution: 1024, - distance: 160.0, - near: -100.05, - far: 156.0, - interval: 2.0, - } - } -} - -impl Directives { - /// Read from `fragments` in order, the last one winning. - pub fn read<'a>(fragments: impl IntoIterator) -> Self { - let mut out = Self::default(); - for fragment in fragments { - for c in const_directives(fragment) { - // Java's `Float.parseFloat` takes a trailing `f`. - let value = c.value.trim_end_matches(['f', 'F']).parse::(); - match (c.ty, c.key.as_str(), value) { - ("float", "sunPathRotation", Ok(v)) => out.sun_path_rotation = v, - ("float", "eyeBrightnessHalflife", Ok(v)) => out.eye_brightness_half_life = v, - ("int", "shadowMapResolution", Ok(v)) if v >= 1.0 => { - out.shadow.resolution = v as u32; - } - ("float", "shadowDistance", Ok(v)) => out.shadow.distance = v, - ("float", "shadowNearPlane", Ok(v)) => out.shadow.near = v, - ("float", "shadowFarPlane", Ok(v)) => out.shadow.far = v, - ("float", "shadowIntervalSize", Ok(v)) => out.shadow.interval = v, - ("float", "wetnessHalflife", Ok(v)) => out.wetness_half_life = v, - ("float", "drynessHalflife", Ok(v)) => out.dryness_half_life = v, - _ => {} - } - } - } - out - } -} - -/// Iris's single programs (`ProgramId`), in order. -const SINGLE_PROGRAMS: &[&str] = &[ - "shadow", - "shadow_solid", - "shadow_cutout", - "shadow_water", - "shadow_entities", - "shadow_lightning", - "shadow_block", - "gbuffers_basic", - "gbuffers_line", - "gbuffers_textured", - "gbuffers_textured_lit", - "gbuffers_skybasic", - "gbuffers_skytextured", - "gbuffers_clouds", - "gbuffers_terrain", - "gbuffers_terrain_solid", - "gbuffers_terrain_cutout", - "gbuffers_damagedblock", - "gbuffers_block", - "gbuffers_block_translucent", - "gbuffers_beaconbeam", - "gbuffers_item", - "gbuffers_entities", - "gbuffers_entities_translucent", - "gbuffers_lightning", - "gbuffers_particles", - "gbuffers_particles_translucent", - "gbuffers_entities_glowing", - "gbuffers_armor_glint", - "gbuffers_spidereyes", - "gbuffers_hand", - "gbuffers_weather", - "gbuffers_water", - "gbuffers_hand_water", - "dh_terrain", - "dh_water", - "dh_generic", - "dh_shadow", - "final", -]; - -/// The program groups that run as full-screen passes, in Iris's order. -const GROUPS: &[&str] = &["begin", "prepare", "deferred", "composite"]; - -impl JavaPack { - /// The full-screen passes of the programs in `dir` (`world0`, or `""` - /// for the pack's root), with the player's `values`. - pub fn passes( - &self, - dir: &str, - values: &OptionValues, - env: &Environment, - properties: &ShaderProperties, - ) -> Result { - let path = |name: &str| { - if dir.is_empty() { - name.to_owned() - } else { - format!("{dir}/{name}") - } - }; - // A pass needs both halves, and its `program.*.enabled` to hold. - let exists = |name: &str| { - let p = path(name); - self.programs.contains(&format!("{p}.vsh")) - && self.programs.contains(&format!("{p}.fsh")) - && properties.program_enabled(&p, &self.options, values) - }; - let fragment = |name: &str| -> Result { - let file = format!("{}.fsh", path(name)); - Ok(self.preprocess(&file, values, env)?.with_comments()) - }; - let group = |base: &str| -> Vec { - (0..100) - .map(|i| { - if i == 0 { - base.to_owned() - } else { - format!("{base}{i}") - } - }) - .filter(|name| exists(name)) - .collect() - }; - let groups: Vec> = GROUPS.iter().map(|g| group(g)).collect(); - - // Target settings come from the fragments of every pass, in Iris's - // order (`ProgramSet.locateDirectives`): some packs declare them in - // `final` only. - let mut sources: Vec = Vec::new(); - for name in group("shadowcomp") - .iter() - .chain(&groups[0]) - .chain(&groups[1]) - { - sources.push(fragment(name)?); - } - for name in SINGLE_PROGRAMS { - if exists(name) { - sources.push(fragment(name)?); - } - } - let mut fragments: BTreeMap = BTreeMap::new(); - for name in groups[2].iter().chain(&groups[3]) { - let text = fragment(name)?; - sources.push(text.clone()); - fragments.insert(name.clone(), text); - } - for name in &groups[0..2].concat() { - fragments.insert(name.clone(), fragment(name)?); - } - let mut out = Passes { - targets: target_settings(sources.iter().map(String::as_str), properties), - shadow_targets: shadow_target_settings(sources.iter().map(String::as_str)), - directives: Directives::read(sources.iter().map(String::as_str)), - ..Passes::default() - }; - - let resolve_blends = |prog: &str| { - let blend = properties.blend(prog).and_then(Result::ok); - let buffer_blends: Vec<(u32, Blend)> = properties - .buffer_blends(prog) - .into_iter() - .filter_map(|(i, b)| Some((i, b.ok()?))) - .collect(); - (blend, buffer_blends) - }; - - let flips = explicit_flips(properties); - let run = |group: &[String], pre: &str, flipped: &mut BTreeSet| { - *flipped = flips_pre(&flips, pre, flipped); - let mut passes = Vec::new(); - for name in group { - let text = &fragments[name]; - let prog = path(name); - let (blend, buffer_blends) = resolve_blends(&prog); - let pass = Pass { - program: prog.clone(), - draw_buffers: draw_buffers(text).map_err(|e| format!("{prog}: {e}"))?, - reads_alt: flipped.clone(), - mipmapped: mipmapped(text), - scale: properties - .preprocessed - .get(&format!("scale.{name}")) - .and_then(|v| v.split_whitespace().next()?.parse().ok()) - .unwrap_or(1.0), - blend, - buffer_blends, - }; - *flipped = flip_set(&pass, &flips); - passes.push(pass); - } - Ok::<_, String>(passes) - }; - let mut flipped = BTreeSet::new(); - out.begin = run(&groups[0], "begin_pre", &mut flipped)?; - let flipped_after_begin = flipped.clone(); - out.prepare = run(&groups[1], "prepare_pre", &mut flipped)?; - out.flipped_after_prepare = flipped.clone(); - // Iris's fallbacks for terrain (`ProgramFallbackResolver`). - let terrain = [ - "gbuffers_terrain", - "gbuffers_textured_lit", - "gbuffers_textured", - "gbuffers_basic", - ] - .into_iter() - .find(|name| exists(name)); - if let Some(name) = terrain { - let buffers = - draw_buffers(&fragment(name)?).map_err(|e| format!("{}: {e}", path(name)))?; - out.world_target = buffers.and_then(|b| b.first().copied()).unwrap_or(0); - } - out.deferred = run(&groups[2], "deferred_pre", &mut flipped)?; - out.flipped_after_translucent = flipped.clone(); - out.composite = run(&groups[3], "composite_pre", &mut flipped)?; - out.flipped_at_end = flipped; - // Geometry writes the side the next passes read. - let geometry = |chain: &[&str], flipped: &BTreeSet| { - let Some(name) = chain.iter().find(|n| exists(n)) else { - return Ok::<_, String>(None); - }; - let text = fragment(name)?; - let (blend, buffer_blends) = resolve_blends(&path(name)); - Ok(Some(Pass { - program: path(name), - draw_buffers: draw_buffers(&text).map_err(|e| format!("{}: {e}", path(name)))?, - reads_alt: flipped.clone(), - mipmapped: BTreeSet::new(), - scale: 1.0, - blend, - buffer_blends, - })) - }; - out.terrain = geometry(TERRAIN_PROGRAMS, &out.flipped_after_prepare)?; - out.water = geometry(WATER_PROGRAMS, &out.flipped_after_translucent)?; - out.shadow = geometry(SHADOW_PROGRAMS, &flipped_after_begin)?; - out.shadow_water = geometry(SHADOW_WATER_PROGRAMS, &flipped_after_begin)?; - if exists("final") { - let prog = path("final"); - let (blend, buffer_blends) = resolve_blends(&prog); - out.final_pass = Some(Pass { - program: prog, - draw_buffers: None, - reads_alt: out.flipped_at_end.clone(), - mipmapped: mipmapped(&fragment("final")?), - scale: 1.0, - blend, - buffer_blends, - }); - } - Ok(out) - } -} - -/// A pass's programs translated, with the targets it draws to. -pub struct Program { - pub pass: Pass, - pub vertex: Translated, - pub fragment: Translated, - /// The target each output location writes: the pass's directive, or - /// as Iris does without one, location `i` to target `i`. - pub draw_buffers: Vec, - /// The bindings both programs share. - pub link: Link, -} - -impl JavaPack { - /// Translate `pass`'s `.vsh` and `.fsh`. - pub fn compile( - &self, - pass: &Pass, - targets: &[TargetSettings], - values: &OptionValues, - env: &Environment, - properties: &ShaderProperties, - ) -> Result { - let mut link = Link { - image_formats: properties.image_formats(), - shared_samplers: shared_samplers(targets), - ..Link::default() - }; - let vertex = self.translate(&format!("{}.vsh", pass.program), values, env, &mut link)?; - let fragment = self.translate(&format!("{}.fsh", pass.program), values, env, &mut link)?; - let draw_buffers = match &pass.draw_buffers { - Some(buffers) => buffers.clone(), - None => (0..fragment.interface.outputs.keys().max().map_or(1, |m| m + 1)).collect(), - }; - Ok(Program { - pass: pass.clone(), - vertex, - fragment, - draw_buffers, - link, - }) - } -} - -/// A geometry program translated: the program (its vertex with the first -/// glue), and its vertex with each other glue. -pub struct GeometryProgram { - pub program: Program, - pub variants: Vec, -} - -impl JavaPack { - /// Translate geometry program `pass` (`gbuffers_*`) to run over - /// Horizon's geometry: its bindings in group 2, its attributes from - /// each of `glues` (the vertex program is translated once per glue). - #[allow(clippy::too_many_arguments)] - pub fn compile_geometry( - &self, - pass: &Pass, - targets: &[TargetSettings], - values: &OptionValues, - env: &Environment, - properties: &ShaderProperties, - glues: &[Geometry], - ) -> Result { - let (first, rest) = glues - .split_first() - .ok_or("a geometry program needs a glue")?; - let mut link = Link { - image_formats: properties.image_formats(), - shared_samplers: shared_samplers(targets), - set: 2, - geometry: Some(first.clone()), - ..Link::default() - }; - let vertex = self.translate(&format!("{}.vsh", pass.program), values, env, &mut link)?; - let fragment = self.translate(&format!("{}.fsh", pass.program), values, env, &mut link)?; - let mut variants = Vec::new(); - for glue in rest { - let mut other = link.clone(); - other.geometry = Some(glue.clone()); - variants.push(self.translate( - &format!("{}.vsh", pass.program), - values, - env, - &mut other, - )?); - if other.uniforms != link.uniforms || other.bindings != link.bindings { - return Err(format!("{}: its variants bind differently", pass.program)); - } - } - let draw_buffers = match &pass.draw_buffers { - Some(buffers) => buffers.clone(), - None => (0..fragment.interface.outputs.keys().max().map_or(1, |m| m + 1)).collect(), - }; - Ok(GeometryProgram { - program: Program { - pass: pass.clone(), - vertex, - fragment, - draw_buffers, - link, - }, - variants, - }) - } -} - -/// Iris's sampler states for the textures it binds: colour targets -/// linear (nearest for integer formats, and for 32-bit floats, which wgpu -/// can't filter everywhere), depth nearest, noise repeating. For -/// [`Link::shared_samplers`]. -pub fn shared_samplers(targets: &[TargetSettings]) -> BTreeMap { - let mut out = BTreeMap::new(); - for (i, t) in targets.iter().enumerate() { - let f = &t.format; - let class = if f.ends_with("32F") || f.ends_with('I') { - "iris_nearest" - } else { - "iris_linear" - }; - out.insert(format!("colortex{i}"), class.to_owned()); - if let Some(legacy) = LEGACY_TARGETS.get(i) { - out.insert((*legacy).to_owned(), class.to_owned()); - } - } - for name in [ - "depthtex0", - "depthtex1", - "depthtex2", - "gdepthtex", - "shadowtex0", - "shadowtex1", - "shadow", - "watershadow", - "dhDepthTex", - "dhDepthTex0", - "dhDepthTex1", - "vxDepthTexOpaque", - "vxDepthTexTrans", - ] { - out.insert(name.to_owned(), "iris_nearest".to_owned()); - } - for name in ["shadowcolor", "shadowcolor0", "shadowcolor1"] { - out.insert(name.to_owned(), "iris_linear".to_owned()); - } - out.insert("noisetex".to_owned(), "iris_repeat".to_owned()); - out -} - -fn toggle(set: &mut BTreeSet, buffer: u32) { - if !set.remove(&buffer) { - set.insert(buffer); - } -} - -/// `flip.. = true|false`, by pass and target index. -fn explicit_flips(properties: &ShaderProperties) -> BTreeMap<&str, BTreeMap> { - let mut out: BTreeMap<&str, BTreeMap> = BTreeMap::new(); - for (key, value) in properties.preprocessed.iter() { - let Some((pass, buffer)) = key.strip_prefix("flip.").and_then(|k| k.split_once('.')) else { - continue; - }; - if let (Some(i), Ok(flip)) = (target_index(buffer), value.trim().parse()) { - out.entry(pass).or_default().insert(i, flip); - } - } - out -} - -/// The flipped set after `pass` ran: what it read, flipped by its writes -/// and its explicit flips. -fn flip_set(pass: &Pass, flips: &BTreeMap<&str, BTreeMap>) -> BTreeSet { - let name = pass.program.rsplit('/').next().unwrap_or(&pass.program); - let explicit = flips.get(name); - let mut set = pass.reads_alt.clone(); - for &buffer in pass.draw_buffers.as_deref().unwrap_or(&[0]) { - if explicit.and_then(|f| f.get(&buffer)) != Some(&false) { - toggle(&mut set, buffer); - } - } - for (&buffer, &flip) in explicit.into_iter().flatten() { - if flip { - toggle(&mut set, buffer); - } - } - set -} - -fn flips_pre( - flips: &BTreeMap<&str, BTreeMap>, - pre: &str, - before: &BTreeSet, -) -> BTreeSet { - let mut set = before.clone(); - for (&buffer, &flip) in flips.get(pre).into_iter().flatten() { - if flip { - toggle(&mut set, buffer); - } - } - set -} - -/// `const bool colortexNMipmapEnabled = true;` in a program's fragment. -fn mipmapped(fragment: &str) -> BTreeSet { - let mut out = BTreeSet::new(); - for c in const_directives(fragment) { - if c.ty != "bool" { - continue; - } - if let Some(i) = target_key(&c.key, "MipmapEnabled") { - match c.value.as_str() { - "true" => out.insert(i), - "false" => out.remove(&i), - _ => false, - }; - } - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn comment_directives_as_iris_finds_them() { - // Iris's own cases (`CommentDirectiveParser.Tests`). - let find = |text: &str, name: &str| comment_directive(text, name).map(|d| d.1); - let draw = |text: &str| find(text, "DRAWBUFFERS"); - assert_eq!( - draw("Some normal text that doesn't contain a DRAWBUFFERS directive"), - None - ); - assert_eq!( - draw("doesn't contain a /* DRAWBUFFERS: directive of any sort"), - None - ); - assert_eq!( - draw("/*DRAWBUFFERS:321*/ OptiFine will detect this").as_deref(), - Some("321") - ); - assert_eq!( - draw("a directive: /* DRAWBUFFERS:321 */").as_deref(), - Some("321") - ); - assert_eq!( - draw("This is a line /* DRAWBUFFERS:31 */ containing").as_deref(), - Some("31") - ); - assert_eq!( - draw("/* DRAWBUFFERS: */ invalid but matching").as_deref(), - Some("") - ); - assert_eq!(draw(" /*DRAWBUFFERS:12*/").as_deref(), Some("12")); - let lines = "/* RENDERTARGETS:Duplicate handling? */\nuniform sampler2D test;\n\ - /* RENDERTARGETS:Within a line? */ Let's see /* RENDERTARGETS:It works */\n"; - assert_eq!(find(lines, "RENDERTARGETS").as_deref(), Some("It works")); - - // The later directive wins, whichever kind it is. - assert_eq!( - draw_buffers("/* RENDERTARGETS: 4,10 */ /* DRAWBUFFERS:02 */"), - Ok(Some(vec![0, 2])) - ); - assert_eq!( - draw_buffers("/* DRAWBUFFERS:02 */ /* RENDERTARGETS: 4,10 */"), - Ok(Some(vec![4, 10])) - ); - assert_eq!(draw_buffers("void main() {}"), Ok(None)); - assert!(draw_buffers("/* RENDERTARGETS: 40 */").is_err()); - assert!(draw_buffers("/* DRAWBUFFERS:0a */").is_err()); - } - - #[test] - fn sky_directives_last_one_wins() { - assert_eq!(Directives::read([]), Directives::default()); - assert_eq!(Directives::default().eye_brightness_half_life, 10.0); - let d = Directives::read([ - "const float sunPathRotation = 30.0;\nconst float eyeBrightnessHalflife = 4.0f;\nconst float wetnessHalflife = 300.0;", - "const float sunPathRotation = -40.0; // Complementary Unbound", - "const int sunPathRotation = 5;", - ]); - assert_eq!(d.sun_path_rotation, -40.0); - assert_eq!(d.eye_brightness_half_life, 4.0); - assert_eq!(d.wetness_half_life, 300.0); - assert_eq!(d.dryness_half_life, 200.0, "the default"); - } - - #[test] - fn shadow_directives_and_targets() { - let text = "\ -const int shadowMapResolution = 2048; -const float shadowDistance = 128.0; -const float shadowIntervalSize = 3.0; -const bool shadowcolor0Clear = false; -const int shadowcolor1Format = RGBA16; -const vec4 shadowcolor1ClearColor = vec4(0.0, 0.0, 0.0, 0.0); -"; - let d = Directives::read([text]); - assert_eq!(d.shadow.resolution, 2048); - assert_eq!(d.shadow.distance, 128.0); - assert_eq!(d.shadow.interval, 3.0); - assert_eq!(d.shadow.near, ShadowDirectives::default().near); - let targets = shadow_target_settings([text]); - assert!(!targets[0].clear); - assert_eq!(targets[1].format, "RGBA16"); - assert_eq!(targets[1].clear_color, Some([0.0; 4])); - // Iris clears the others to white. - assert_eq!(targets[2].clear_color, Some([1.0; 4])); - assert!(targets[2].clear); - } - - #[test] - fn const_directives_and_target_settings() { - let text = "\ -const int colortex4Format = rgba16f; - const bool gaux2Clear = false; // kept between frames -const vec4 colortex0ClearColor = vec4(0.5, 0.25, 0.0, 1.0); -const int colortex5Format = NOT_A_FORMAT; -uniform const int x = 1; -const int colortex6Format=RGBA8; -const intx colortex7Format = RGBA8; -const ivec3 workGroups = ivec3(16, 8, 16); -"; - let consts = const_directives(text); - assert_eq!( - consts - .iter() - .map(|c| (c.ty, c.key.as_str(), c.value.as_str())) - .collect::>(), - [ - ("int", "colortex4Format", "rgba16f"), - ("bool", "gaux2Clear", "false"), - ("vec4", "colortex0ClearColor", "vec4(0.5, 0.25, 0.0, 1.0)"), - ("int", "colortex5Format", "NOT_A_FORMAT"), - ("int", "colortex6Format", "RGBA8"), - ("ivec3", "workGroups", "ivec3(16, 8, 16)"), - ], - "a type needs a space after it, the name doesn't" - ); - let targets = target_settings([text], &ShaderProperties::default()); - assert_eq!(targets.len(), MAX_COLOR_BUFFERS as usize); - assert_eq!(targets[4].format, "RGBA16F"); - assert_eq!(targets[5].format, "RGBA", "unknown formats are ignored"); - assert!(!targets[5].clear, "gaux2 is colortex5"); - assert_eq!(targets[0].clear_color, Some([0.5, 0.25, 0.0, 1.0])); - assert_eq!(targets[1], TargetSettings::default()); - } -} diff --git a/crates/horizon-shaderpack/src/preprocess.rs b/crates/horizon-shaderpack/src/preprocess.rs deleted file mode 100644 index 8a0c4e694..000000000 --- a/crates/horizon-shaderpack/src/preprocess.rs +++ /dev/null @@ -1,594 +0,0 @@ -//! The C preprocessor pass: Iris's standard macros (`StandardMacros`, -//! `IrisDefines`) defined, then every directive and macro expanded by pp-rs, -//! the preprocessor naga's GLSL front end uses. The output keeps the inlined -//! source's line numbers, so an error later on still names `file:line`. - -use crate::include::Inlined; -use pp_rs::{ - pp::Preprocessor, - token::{PreprocessorError, Punct, TokenValue}, -}; -use std::collections::HashMap; - -/// The version of Minecraft packs are told they run on (`MC_VERSION`, -/// Iris's `formatVersionString("26.3")`). -pub const MC_VERSION: &str = "260300"; -/// The Iris release whose behaviour the translation follows (`IRIS_VERSION`, 1.10.0). -pub const IRIS_VERSION: &str = "11000"; - -/// Iris's `WorldRenderingPhase`, in order: `MC_RENDER_STAGE_` is the index. -pub const RENDER_STAGES: &[&str] = &[ - "NONE", - "SKY", - "SUNSET", - "CUSTOM_SKY", - "SUN", - "MOON", - "STARS", - "VOID", - "TERRAIN_SOLID", - "TERRAIN_CUTOUT_MIPPED", - "TERRAIN_CUTOUT", - "ENTITIES", - "BLOCK_ENTITIES", - "DESTROY", - "OUTLINE", - "DEBUG", - "HAND_SOLID", - "TERRAIN_TRANSLUCENT", - "TRIPWIRE", - "PARTICLES", - "CLOUDS", - "RAIN_SNOW", - "WORLD_BORDER", - "HAND_TRANSLUCENT", -]; - -/// Iris's `BiomeCategories`, in order: `CAT_` is the index. -pub const BIOME_CATEGORIES: &[&str] = &[ - "NONE", - "TAIGA", - "EXTREME_HILLS", - "JUNGLE", - "MESA", - "PLAINS", - "SAVANNA", - "ICY", - "THE_END", - "BEACH", - "FOREST", - "OCEAN", - "DESERT", - "RIVER", - "SWAMP", - "MUSHROOM", - "NETHER", - "MOUNTAIN", - "UNDERGROUND", -]; - -/// Distant Horizons block kinds Iris always defines (`DH_BLOCK_`). -const DH_BLOCKS: &[&str] = &[ - "UNKNOWN", - "LEAVES", - "STONE", - "WOOD", - "METAL", - "DIRT", - "LAVA", - "DEEPSLATE", - "SNOW", - "SAND", - "TERRACOTTA", - "NETHER_STONE", - "WATER", - "GRASS", - "AIR", - "ILLUMINATED", -]; - -/// The macros every program is preprocessed with. -#[derive(Clone, Debug)] -pub struct Environment { - pub defines: Vec<(String, String)>, -} - -impl Environment { - /// What Iris defines (`IrisDefines.createIrisReplacements`) for a GPU - /// whose adapter reports `vendor` and `renderer`, without any GL - /// extension (`MC_GL_*`) or mod macro: packs then take their portable - /// paths. Biomes (`BIOME_*`) and the texture format come from the game: - /// add them with [`Environment::define`]. - pub fn iris(vendor: &str, renderer: &str, mipmap_levels: u32) -> Self { - let mut env = Self { - defines: Vec::new(), - }; - env.define("MC_VERSION", MC_VERSION); - env.define("MC_MIPMAP_LEVEL", &mipmap_levels.to_string()); - env.define("IRIS_VERSION", IRIS_VERSION); - // Horizon's renderer stands in for GL 4.6 / GLSL 4.60. - env.define("MC_GL_VERSION", "460"); - env.define("MC_GLSL_VERSION", "460"); - env.define(os_macro(), ""); - env.define(vendor_macro(vendor), ""); - env.define(renderer_macro(renderer), ""); - for flag in [ - "IS_IRIS", - "IRIS_REQUIRES_SEPARATE_ENTITY_DRAWS", - "IRIS_HAS_TRANSLUCENCY_SORTING", - ] { - env.define(flag, ""); - } - env.define("MAX_COLOR_BUFFERS", "32"); - env.define("IRIS_TAG_SUPPORT", "2"); - for (i, name) in DH_BLOCKS.iter().enumerate() { - env.define(&format!("DH_BLOCK_{name}"), &i.to_string()); - } - env.define("MC_NORMAL_MAP", ""); - env.define("MC_SPECULAR_MAP", ""); - env.define("MC_RENDER_QUALITY", "1.0"); - env.define("MC_SHADOW_QUALITY", "1.0"); - env.define("MC_HAND_DEPTH", "0.125"); - for (i, name) in RENDER_STAGES.iter().enumerate() { - env.define(&format!("MC_RENDER_STAGE_{name}"), &i.to_string()); - } - for (i, name) in BIOME_CATEGORIES.iter().enumerate() { - env.define(&format!("CAT_{name}"), &i.to_string()); - } - env.define("PPT_NONE", "0"); - env.define("PPT_RAIN", "1"); - env.define("PPT_SNOW", "2"); - env - } - - /// Define `name` (replacing an earlier value). - pub fn define(&mut self, name: &str, value: &str) { - self.defines.retain(|(n, _)| n != name); - self.defines.push((name.to_owned(), value.to_owned())); - } -} - -fn os_macro() -> &'static str { - if cfg!(target_os = "macos") { - "MC_OS_MAC" - } else if cfg!(windows) { - "MC_OS_WINDOWS" - } else { - "MC_OS_LINUX" - } -} - -/// `StandardMacros.getVendor`. -pub fn vendor_macro(vendor: &str) -> &'static str { - let v = vendor.to_lowercase(); - const TABLE: &[(&str, &str)] = &[ - ("ati", "MC_GL_VENDOR_ATI"), - ("intel", "MC_GL_VENDOR_INTEL"), - ("nvidia", "MC_GL_VENDOR_NVIDIA"), - ("amd", "MC_GL_VENDOR_AMD"), - ("x.org", "MC_GL_VENDOR_XORG"), - ]; - TABLE - .iter() - .find(|(prefix, _)| v.starts_with(prefix)) - .map_or("MC_GL_VENDOR_OTHER", |(_, m)| m) -} - -/// `StandardMacros.getRenderer`. -pub fn renderer_macro(renderer: &str) -> &'static str { - let r = renderer.to_lowercase(); - const TABLE: &[(&str, &str)] = &[ - ("amd", "MC_GL_RENDERER_RADEON"), - ("ati", "MC_GL_RENDERER_RADEON"), - ("radeon", "MC_GL_RENDERER_RADEON"), - ("gallium", "MC_GL_RENDERER_GALLIUM"), - ("intel", "MC_GL_RENDERER_INTEL"), - ("geforce", "MC_GL_RENDERER_GEFORCE"), - ("nvidia", "MC_GL_RENDERER_GEFORCE"), - ("quadro", "MC_GL_RENDERER_QUADRO"), - ("nvs", "MC_GL_RENDERER_QUADRO"), - ("mesa", "MC_GL_RENDERER_MESA"), - ("apple", "MC_GL_RENDERER_APPLE"), - ]; - TABLE - .iter() - .find(|(prefix, _)| r.starts_with(prefix)) - .map_or("MC_GL_RENDERER_OTHER", |(_, m)| m) -} - -/// A program after the preprocessor: plain GLSL with no directives. -#[derive(Debug)] -pub struct Preprocessed { - /// The source, one output line per inlined line (macro expansions joined). - pub text: String, - /// The `#version` line's words (`["330", "compatibility"]`), if any. - pub version: Vec, - /// `#extension` lines' words (`["GL_ARB_gpu_shader5", ":", "enable"]`). - pub extensions: Vec>, - /// The comments Iris reads directives from (`/* DRAWBUFFERS:01 */`, - /// `const int colortex0Format = RGBA16F;`) that are outside `#if`'d out - /// blocks, with their output line. Iris keeps comments through its - /// preprocessor; pp-rs drops them. - pub comments: Vec<(u32, String)>, -} - -impl Preprocessed { - /// The text with its kept comments put back at the start of their - /// lines: what Iris's directive parsers read. - pub fn with_comments(&self) -> String { - let mut comments = self.comments.iter().peekable(); - let mut out = String::new(); - for (i, line) in self.text.lines().enumerate() { - while let Some((_, comment)) = comments.next_if(|(at, _)| *at as usize <= i + 1) { - out.push_str(comment); - out.push(' '); - } - out.push_str(line); - out.push('\n'); - } - out - } -} - -/// What a comment must hold for Iris to read a directive from it. -const DIRECTIVE_WORDS: &[&str] = &["DRAWBUFFERS:", "RENDERTARGETS:", "const"]; -/// The identifier a kept comment is replaced by while pp-rs runs. -const COMMENT_MARK: &str = "iris_comment_"; - -/// Replace the comments holding [`DIRECTIVE_WORDS`] with an identifier -/// (and their line breaks), so the ones in `#if`'d out blocks disappear with -/// them. Comments on directive lines are left alone: there the identifier -/// would end up in a macro or an `#if`. -fn mark_comments(text: &str) -> (String, Vec) { - let mut out = String::with_capacity(text.len()); - let mut comments = Vec::new(); - let (mut directive, mut line_start, mut continued) = (false, true, false); - let mut rest = text; - while let Some(c) = rest.chars().next() { - let comment_len = if rest.starts_with("//") { - rest.find('\n').unwrap_or(rest.len()) - } else if let Some(body) = rest.strip_prefix("/*") { - body.find("*/").map_or(rest.len(), |i| i + 4) - } else { - 0 - }; - if comment_len > 0 { - let comment = &rest[..comment_len]; - if !directive && DIRECTIVE_WORDS.iter().any(|w| comment.contains(w)) { - out.push_str(&format!(" {COMMENT_MARK}{} ", comments.len())); - out.extend(std::iter::repeat_n('\n', comment.matches('\n').count())); - comments.push(comment.to_owned()); - } else { - out.push_str(comment); - } - if comment.contains('\n') { - // A comment spanning lines ends a directive like a line break. - directive = false; - } - line_start = false; - rest = &rest[comment_len..]; - continue; - } - match c { - '\n' => { - directive = directive && continued; - line_start = !directive; - } - '#' if line_start => directive = true, - c if c.is_whitespace() => {} - _ => line_start = false, - } - if c != '\r' && !c.is_whitespace() || c == '\n' { - continued = c == '\\'; - } - out.push(c); - rest = &rest[c.len_utf8()..]; - } - (out, comments) -} - -/// Whether trimmed `line` is an `#if` or `#elif` directive. -pub(crate) fn is_condition(line: &str) -> bool { - line.strip_prefix('#') - .map(str::trim_start) - .and_then(|d| d.strip_prefix("elif").or_else(|| d.strip_prefix("if"))) - .is_some_and(|r| !r.starts_with(|c: char| c.is_ascii_alphanumeric() || c == '_')) -} - -/// Whether `word` is a float literal. -pub(crate) fn is_float(word: &str) -> bool { - word.starts_with(|c: char| c.is_ascii_digit() || c == '.') - && !word.starts_with("0x") - && !word.starts_with("0X") - && word.contains(['.', 'e', 'E']) -} - -/// What JCPP's `#if` makes of a number (`Preprocessor.expr` takes -/// `NumericValue.longValue`): a float counts as its integer part, so -/// `0.75` is 0 and `1.5e1` is 10. `None` for a non-number. -pub(crate) fn jcpp_integer(number: &str) -> Option { - let number = number.trim_end_matches(['f', 'F', 'l', 'L', 'u', 'U']); - let (mantissa, exponent) = match number.find(['e', 'E']) { - Some(i) => (&number[..i], number[i + 1..].parse::().ok()?), - None => (number, 0), - }; - let integer = mantissa.split('.').next()?; - let value = if integer.is_empty() { - 0 - } else { - integer.parse::().ok()? - }; - Some((value as f64 * 10f64.powi(exponent)) as i64) -} - -/// `line` with each word (identifier or number, with an exponent's sign: -/// `1e-3`) replaced by what `f` returns for it, if anything. The directive's -/// name and the names after `defined` are kept. -pub(crate) fn rewrite_condition(line: &str, f: impl Fn(&str) -> Option) -> String { - let mut out = String::with_capacity(line.len()); - let (mut rest, mut first, mut after_defined) = (line, true, false); - while let Some(c) = rest.chars().next() { - if !(c.is_ascii_alphanumeric() || c == '_' || c == '.') { - if !c.is_whitespace() && c != '(' && c != '#' { - after_defined = false; - } - out.push(c); - rest = &rest[c.len_utf8()..]; - continue; - } - let bytes = rest.as_bytes(); - let mut len = 0; - while let Some(&b) = bytes.get(len) { - let sign = (b == b'+' || b == b'-') - && bytes[0].is_ascii_digit() - && matches!(bytes[len - 1], b'e' | b'E'); - if !(b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || sign) { - break; - } - len += 1; - } - let word = &rest[..len]; - let kept = first || after_defined; - match (!kept).then(|| f(word)).flatten() { - Some(replacement) => out.push_str(&replacement), - None => out.push_str(word), - } - after_defined = word == "defined"; - first = false; - rest = &rest[len..]; - } - out -} - -/// Make pp-rs read `text` as JCPP, Iris's preprocessor, does: -/// - a `#define` replaces an earlier macro (`Preprocessor.addMacro`; Bloop -/// and Steadfast redefine theirs), where pp-rs refuses a different -/// redefinition: an `#undef` line goes before every `#define`; -/// - in `#if`/`#elif`, a float counts as its integer part, and so does a -/// macro always defined as the same float (an option: `#define LEVEL 0.5`); -/// a macro always defined empty (Iris's `MC_GL_VENDOR_OTHER`, Bloop's -/// `#define TAA_USE_YCOCG`) counts as 0: JCPP reports the missing operand -/// and goes on with 0. pp-rs stops on both; -/// - `#error "..."` (Photon's guard in programs it disables) lexes: pp-rs -/// has no string tokens and fails on the quote, even in a skipped block. -/// -/// Returns the text and the added lines' 1-based numbers in it. -fn jcpp_compatible(text: &str, env: &Environment) -> (String, Vec) { - // A `#define` line's name and body. - let define = |l: &str| { - let rest = l - .trim_start() - .strip_prefix('#')? - .trim_start() - .strip_prefix("define") - .filter(|rest| rest.starts_with([' ', '\t']))? - .trim_start(); - let end = rest - .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_')) - .unwrap_or(rest.len()); - let (name, body) = rest.split_at(end); - let body = body.split("//").next().unwrap_or("").trim(); - (!name.is_empty()).then(|| (name.to_owned(), body.to_owned())) - }; - // Each macro's body, if every definition has the same one. - let mut bodies: HashMap> = HashMap::new(); - let definitions = env - .defines - .iter() - .cloned() - .chain(text.lines().filter_map(define)); - for (name, body) in definitions { - bodies - .entry(name) - .and_modify(|b| { - if b.as_ref() != Some(&body) { - *b = None; - } - }) - .or_insert(Some(body)); - } - let number = |word: &str| -> Option { - let word = match bodies.get(word) { - Some(Some(body)) if body.is_empty() => return Some("0".into()), - Some(Some(body)) => body.as_str(), - Some(None) => return None, - None => word, - }; - is_float(word) - .then(|| jcpp_integer(word)) - .flatten() - .map(|n| n.to_string()) - }; - let mut out = String::with_capacity(text.len() + text.len() / 16); - let mut added = Vec::new(); - let (mut line, mut continued) = (1, false); - for l in text.split_inclusive('\n') { - if !continued && let Some((name, _)) = define(l) { - out.push_str(&format!("#undef {name}\n")); - added.push(line); - line += 1; - } - let directive = l.trim_start().strip_prefix('#').map(str::trim_start); - if !continued - && directive.is_some_and(|d| d.starts_with("error") || d.starts_with("warning")) - { - out.push_str(&l.replace(['"', '\''], "")); - } else if !continued && is_condition(l.trim_start()) { - out.push_str(&rewrite_condition(l, number)); - } else { - out.push_str(l); - } - continued = l.trim_end().ends_with('\\'); - line += 1; - } - (out, added) -} - -/// Expand `source` with `env`'s macros. Errors name the file and line. -pub fn preprocess(source: &Inlined, env: &Environment) -> Result { - let (marked, comments) = mark_comments(&source.text); - let (text, added) = jcpp_compatible(&marked, env); - let text = &text; - let line_starts: Vec = std::iter::once(0) - .chain(text.match_indices('\n').map(|(i, _)| i + 1)) - .collect(); - // Lines of the source: the added `#undef` lines don't count. - let line_of = |offset: u32| { - let line = line_starts.partition_point(|&s| s <= offset as usize) as u32; - line - added.partition_point(|&a| a < line) as u32 - }; - let mut pp = Preprocessor::new(text); - for (name, value) in &env.defines { - pp.add_define(name, value) - .map_err(|e| format!("macro {name}: {:?}", e.0))?; - } - let mut out = Preprocessed { - text: String::new(), - version: Vec::new(), - extensions: Vec::new(), - comments: Vec::new(), - }; - let mut line = 1; - for item in pp { - let token = match item { - // JCPP lets an `#if` run to the end of the input (Bloop's - // taa.fsh never closes its `#ifdef TAA`); pp-rs says so there, - // after the last token. - Err((PreprocessorError::UnfinishedBlock, _)) => break, - item => { - item.map_err(|(e, at)| format!("{}: {e:?}", source.locate(line_of(at.start))))? - } - }; - let at = line_of(token.location.start); - let piece = match token.value { - TokenValue::Version(v) => { - out.version = v.tokens.into_iter().map(|t| spelling(t.value)).collect(); - continue; - } - TokenValue::Extension(e) => { - out.extensions - .push(e.tokens.into_iter().map(|t| spelling(t.value)).collect()); - continue; - } - TokenValue::Pragma(_) => continue, - TokenValue::Ident(ref name) if name.starts_with(COMMENT_MARK) => { - if let Some(comment) = name[COMMENT_MARK.len()..] - .parse::() - .ok() - .and_then(|i| comments.get(i)) - { - out.comments.push((at, comment.clone())); - } - continue; - } - value => spelling(value), - }; - if at > line { - out.text - .extend(std::iter::repeat_n('\n', (at - line) as usize)); - line = at; - } else if !out.text.is_empty() { - out.text.push(' '); - } - out.text.push_str(&piece); - } - out.text.push('\n'); - Ok(out) -} - -/// A token as GLSL source. -fn spelling(value: TokenValue) -> String { - match value { - TokenValue::Ident(s) => s, - TokenValue::Integer(i) => { - let suffix = if i.signed { "" } else { "u" }; - // Bit patterns such as 0xFFFFFFFF stay hexadecimal: as decimals - // they would overflow a signed int. - if i.value > i32::MAX as u64 { - format!("{:#X}{suffix}", i.value) - } else { - format!("{}{suffix}", i.value) - } - } - TokenValue::Float(f) => { - let mut s = format!("{:?}", f.value); - if !s.contains(['.', 'e', 'i', 'N']) { - s.push_str(".0"); - } - s - } - TokenValue::Punct(p) => punct(p).to_owned(), - TokenValue::Version(_) | TokenValue::Extension(_) | TokenValue::Pragma(_) => String::new(), - } -} - -fn punct(p: Punct) -> &'static str { - use Punct::*; - match p { - AddAssign => "+=", - SubAssign => "-=", - MulAssign => "*=", - DivAssign => "/=", - ModAssign => "%=", - LeftShiftAssign => "<<=", - RightShiftAssign => ">>=", - AndAssign => "&=", - XorAssign => "^=", - OrAssign => "|=", - Increment => "++", - Decrement => "--", - LogicalAnd => "&&", - LogicalOr => "||", - LogicalXor => "^^", - LessEqual => "<=", - GreaterEqual => ">=", - EqualEqual => "==", - NotEqual => "!=", - LeftShift => "<<", - RightShift => ">>", - LeftBrace => "{", - RightBrace => "}", - LeftParen => "(", - RightParen => ")", - LeftBracket => "[", - RightBracket => "]", - LeftAngle => "<", - RightAngle => ">", - Semicolon => ";", - Comma => ",", - Colon => ":", - Dot => ".", - Equal => "=", - Bang => "!", - Minus => "-", - Tilde => "~", - Plus => "+", - Star => "*", - Slash => "/", - Percent => "%", - Pipe => "|", - Caret => "^", - Ampersand => "&", - Question => "?", - } -} diff --git a/crates/horizon-shaderpack/src/properties.rs b/crates/horizon-shaderpack/src/properties.rs deleted file mode 100644 index d0c69bbf0..000000000 --- a/crates/horizon-shaderpack/src/properties.rs +++ /dev/null @@ -1,1063 +0,0 @@ -//! `shaders.properties`, read the way Iris reads it (`ShaderProperties`, -//! `PropertiesPreprocessor`): the option screens, sliders, profiles and -//! feature flags come from the file as written; everything else from the file -//! after the C preprocessor, run with Iris's macros and the player's options -//! (an enabled boolean option is defined as `1`, a value option as its value), -//! so `#if` blocks pick the directives that apply. - -use crate::options::{OptionSet, OptionValues}; -use crate::preprocess::{Environment, is_condition, is_float, jcpp_integer, rewrite_condition}; -use pp_rs::{pp::Preprocessor, token::TokenValue}; -use std::collections::BTreeMap; - -/// A Java `.properties` file: keys in the order they first appear, the last -/// value of a repeated key kept (Iris's `OrderBackedProperties`). -#[derive(Debug, Default, Clone)] -pub struct Properties(Vec<(String, String)>); - -impl Properties { - /// `java.util.Properties.load`: `#`/`!` comment lines, `key=value`, - /// `key:value` or `key value`, backslash continuations and escapes. - pub fn parse(text: &str) -> Self { - let mut out = Self::default(); - let mut lines = text.lines(); - while let Some(first) = lines.next() { - let mut line = first.trim_start().to_owned(); - if line.is_empty() || line.starts_with('#') || line.starts_with('!') { - continue; - } - while ends_with_odd_backslashes(&line) { - line.pop(); - match lines.next() { - Some(next) => line.push_str(next.trim_start()), - None => break, - } - } - let (key, value) = split_entry(&line); - out.set(unescape(&key), unescape(&value)); - } - out - } - - fn set(&mut self, key: String, value: String) { - match self.0.iter_mut().find(|(k, _)| *k == key) { - Some(entry) => entry.1 = value, - None => self.0.push((key, value)), - } - } - - pub fn get(&self, key: &str) -> Option<&str> { - self.0 - .iter() - .find(|(k, _)| k == key) - .map(|(_, v)| v.as_str()) - } - - pub fn iter(&self) -> impl Iterator { - self.0.iter().map(|(k, v)| (k.as_str(), v.as_str())) - } -} - -fn ends_with_odd_backslashes(s: &str) -> bool { - s.chars().rev().take_while(|&c| c == '\\').count() % 2 == 1 -} - -/// The key ends at the first unescaped `=`, `:` or whitespace; then -/// whitespace and at most one `=` or `:` separate it from the value. -fn split_entry(line: &str) -> (String, String) { - let chars: Vec = line.chars().collect(); - let mut i = 0; - while i < chars.len() { - match chars[i] { - '\\' => i += 2, - '=' | ':' => break, - c if c.is_whitespace() => break, - _ => i += 1, - } - } - let i = i.min(chars.len()); - let key: String = chars[..i].iter().collect(); - let mut j = i; - while j < chars.len() && chars[j].is_whitespace() { - j += 1; - } - if j < chars.len() && (chars[j] == '=' || chars[j] == ':') { - j += 1; - } - while j < chars.len() && chars[j].is_whitespace() { - j += 1; - } - (key, chars[j..].iter().collect()) -} - -fn unescape(s: &str) -> String { - let mut out = String::with_capacity(s.len()); - let mut chars = s.chars(); - while let Some(c) = chars.next() { - if c != '\\' { - out.push(c); - continue; - } - match chars.next() { - Some('t') => out.push('\t'), - Some('n') => out.push('\n'), - Some('r') => out.push('\r'), - Some('f') => out.push('\u{c}'), - Some('u') => { - let hex: String = chars.by_ref().take(4).collect(); - out.extend(u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32)); - } - Some(other) => out.push(other), - None => {} - } - } - out -} - -/// The directives the C preprocessor knows: other `#` lines are comments. -const DIRECTIVES: &[&str] = &[ - "#define", "#undef", "#ifdef", "#ifndef", "#if", "#elif", "#else", "#endif", "#error", - "#warning", "#pragma", "#line", "#include", -]; - -/// An `#if`/`#elif` line with its floats as JCPP reads them (Steadfast tests -/// `#if ATMOSPHERE_FOG_STRENGTH_NOON == 0.75` with the option at 0.5, which -/// Iris reads as `0 == 0`): float literals and the options set to a float -/// become their integer part. -fn jcpp_condition(line: &str, options: &OptionSet, values: &OptionValues) -> String { - rewrite_condition(line, |word| { - let value = match options.strings.get(word) { - Some(o) => values.strings.get(word).unwrap_or(&o.default).as_str(), - None => word, - }; - is_float(value) - .then(|| jcpp_integer(value)) - .flatten() - .map(|n| n.to_string()) - }) -} - -/// What backslashes are while the macros expand, as in Iris: the -/// preprocessor must not join a `\`-continued value with the directive on -/// the next line (Photon ends an `#if`'d value with `) \` before `#else`). -const BACKSLASH: &str = "IRIS_PASSTHROUGHBACKSLASH"; - -/// `PropertiesPreprocessor.preprocessSource`: comment lines dropped, other -/// `#`s removed, then the macros expanded with the backslashes hidden. -/// Tokens keep their source spelling and spacing (`0.50` stays `0.50`, -/// `` stays one word). -pub fn preprocess( - text: &str, - env: &Environment, - options: &OptionSet, - values: &OptionValues, -) -> Result { - let filtered: String = text - .lines() - .map(str::trim) - .map(|l| match l.starts_with('#') { - // JCPP lets a #define replace a macro (Complementary sets - // COLORED_LIGHTING 0 on macOS); pp-rs refuses, so undefine first. - true if l.starts_with("#define") => { - match l["#define".len()..].split_whitespace().next() { - Some(name) => format!("#undef {}\n{l}", name.split('(').next().unwrap_or(name)), - None => l.to_owned(), - } - } - true if is_condition(l) => jcpp_condition(l, options, values), - true if DIRECTIVES.iter().any(|d| l.starts_with(d)) => l.to_owned(), - true => String::new(), - false => l.replace('#', ""), - }) - .map(|l| l.replace('\\', BACKSLASH) + "\n") - .collect(); - let mut pp = Preprocessor::new(&filtered); - let mut define = |name: &str, value: &str| { - pp.add_define(name, value) - .map_err(|e| format!("{name}: {:?}", e.0)) - }; - for (name, o) in &options.bools { - if values.bools.get(name).copied().unwrap_or(o.default) { - define(name, "1")?; - } - } - for (name, value) in &env.defines { - define(name, if value.is_empty() { "1" } else { value })?; - } - for (name, o) in &options.strings { - define(name, values.strings.get(name).unwrap_or(&o.default))?; - } - let line_starts: Vec = std::iter::once(0) - .chain(filtered.match_indices('\n').map(|(i, _)| i + 1)) - .collect(); - let line_of = |offset: u32| line_starts.partition_point(|&s| s <= offset as usize); - let mut out = String::new(); - let (mut line, mut last_end) = (0, None::); - for item in pp { - let token = - item.map_err(|(e, at)| format!("shaders.properties:{}: {e:?}", line_of(at.start)))?; - let (start, end) = (token.location.start, token.location.end); - let at = line_of(start); - if at > line { - // Lines the preprocessor left empty stay empty lines, so a value - // continued with `\` before an `#else` (Photon's - // `moon_phase_brightness`) ends there, as in Iris. - if line > 0 { - out.extend(std::iter::repeat_n('\n', at - line)); - } - line = at; - } else if last_end != Some(start) { - out.push(' '); - } - last_end = Some(end); - let source = filtered.get(start as usize..end as usize).unwrap_or(""); - out.push_str(&match token.value { - TokenValue::Ident(s) => s, - TokenValue::Integer(i) if source.parse::().ok() == Some(i.value) => { - source.to_owned() - } - TokenValue::Integer(i) => i.value.to_string(), - TokenValue::Float(f) - if source.trim_end_matches(['f', 'F']).parse::().ok() == Some(f.value) => - { - source.to_owned() - } - TokenValue::Float(f) => f.value.to_string(), - TokenValue::Punct(_) => source.to_owned(), - TokenValue::Version(_) | TokenValue::Extension(_) | TokenValue::Pragma(_) => continue, - }); - } - out.push('\n'); - Ok(out.replace(BACKSLASH, "\\")) -} - -/// One element of an option screen (`OptionMenuElement`). -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum MenuElement { - /// ``: a blank cell. - Empty, - /// ``: the profile button (only when the pack has profiles). - Profile, - /// `*`: every option no screen lists. - Rest, - /// `[NAME]`: a button to sub-screen `screen.NAME`. - Screen(String), - /// An option, by name. A name the source doesn't define is shown as an - /// empty cell, as Iris does (packs keep removed options on screens). - Option(String), -} - -impl MenuElement { - fn parse(word: &str) -> Self { - match word { - "" => Self::Empty, - "" => Self::Profile, - "*" => Self::Rest, - w if w.starts_with('[') && w.ends_with(']') && w.len() > 1 => { - Self::Screen(w[1..w.len() - 1].to_owned()) - } - w => Self::Option(w.to_owned()), - } - } -} - -/// A named set of option values (`profile.LOW = SHADOWS:1 !BLOOM`). -#[derive(Clone, Debug, PartialEq)] -pub struct Profile { - pub name: String, - pub values: BTreeMap, - pub disabled_programs: Vec, -} - -impl Profile { - /// Whether the current values are this profile's (`Profile.matches`). - pub fn matches(&self, options: &OptionSet, values: &OptionValues) -> bool { - self.values.iter().all(|(name, value)| { - if let Some(o) = options.bools.get(name) { - let on = values.bools.get(name).copied().unwrap_or(o.default); - return on.to_string() == *value; - } - if let Some(o) = options.strings.get(name) { - return values.strings.get(name).unwrap_or(&o.default) == value; - } - true - }) - } -} - -/// A blend override: `off`, or the four GL factors (source and -/// destination colour, then alpha), as written (`SRC_ALPHA`, ...). -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Blend { - Off, - Factors([String; 4]), -} - -/// The GL blend factors packs may name (`BlendModeFunction`). -pub const BLEND_FACTORS: &[&str] = &[ - "ZERO", - "ONE", - "SRC_COLOR", - "ONE_MINUS_SRC_COLOR", - "DST_COLOR", - "ONE_MINUS_DST_COLOR", - "SRC_ALPHA", - "ONE_MINUS_SRC_ALPHA", - "DST_ALPHA", - "ONE_MINUS_DST_ALPHA", - "SRC_ALPHA_SATURATE", -]; - -/// Colour targets by their pre-`colortex` names (`gcolor` is `colortex0`). -pub const LEGACY_TARGETS: &[&str] = &[ - "gcolor", - "gdepth", - "gnormal", - "composite", - "gaux1", - "gaux2", - "gaux3", - "gaux4", -]; - -fn parse_blend(value: &str) -> Result { - if value == "off" { - return Ok(Blend::Off); - } - let words: Vec<&str> = value.split(' ').collect(); - match words.as_slice() { - [a, b, c, d] if words.iter().all(|w| BLEND_FACTORS.contains(w)) => { - Ok(Blend::Factors([a, b, c, d].map(|w| w.to_string()))) - } - _ => Err(format!("invalid blend mode `{value}`")), - } -} - -/// `clouds = off | fast | fancy`: replaces the player's cloud setting while -/// the pack runs (`ShaderProperties.cloudSetting`). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CloudSetting { - Off, - Fast, - Fancy, -} - -/// The vanilla sky the pack keeps: Iris draws no clouds with `clouds = off`, -/// and no sun or moon with `sun = false` / `moon = false`. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct SkyDirectives { - pub clouds: Option, - pub sun: bool, - pub moon: bool, -} - -impl Default for SkyDirectives { - fn default() -> Self { - Self { - clouds: None, - sun: true, - moon: true, - } - } -} - -/// A custom texture bound by `texture.. = ` in -/// `shaders.properties`. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CustomTexture { - /// Stage family (`composite`, `deferred`, `gbuffers`, `prepare`, `begin`), - /// or `None` when bound across all stages. - pub stage: Option, - /// Sampler uniform name (`noisetex`, `colortex4`, `customTex`, ...). - pub sampler: String, - /// Path relative to `shaders/` (`textures/noise.png`). - pub path: String, -} - -/// A pack's `shaders.properties`. -#[derive(Debug, Default, Clone)] -pub struct ShaderProperties { - /// As written: screens, sliders, profiles, feature flags. - pub original: Properties, - /// After the preprocessor: every other directive. - pub preprocessed: Properties, -} - -impl ShaderProperties { - pub fn new( - text: &str, - env: &Environment, - options: &OptionSet, - values: &OptionValues, - ) -> Result { - Ok(Self { - original: Properties::parse(text), - preprocessed: Properties::parse(&preprocess(text, env, options, values)?), - }) - } - - /// The main screen (`screen`, `None`) or sub-screen `screen.NAME`; the - /// main screen defaults to `*`. - pub fn screen(&self, name: Option<&str>) -> Option> { - let value = match name { - None => self.original.get("screen").or(Some("*")), - Some(n) => self.original.get(&format!("screen.{n}")), - }?; - Some(value.split_whitespace().map(MenuElement::parse).collect()) - } - - /// The sub-screens, by name. - pub fn screens(&self) -> Vec<&str> { - self.original - .iter() - .filter_map(|(k, _)| k.strip_prefix("screen.")) - .filter(|n| !n.ends_with(".columns") && *n != "columns") - .collect() - } - - /// A screen's column count (`screen.columns`, `screen.NAME.columns`). - pub fn columns(&self, name: Option<&str>) -> Option { - let key = name.map_or("screen.columns".to_owned(), |n| { - format!("screen.{n}.columns") - }); - self.original.get(&key)?.trim().parse().ok() - } - - /// Value options shown as sliders rather than buttons. - pub fn sliders(&self) -> Vec<&str> { - self.original - .get("sliders") - .map(|v| v.split_whitespace().collect()) - .unwrap_or_default() - } - - /// `iris.features.required` and `iris.features.optional`. - pub fn features(&self) -> (Vec<&str>, Vec<&str>) { - let list = |k: &str| { - self.original - .get(k) - .map(|v| v.split_whitespace().collect()) - .unwrap_or_default() - }; - ( - list("iris.features.required"), - list("iris.features.optional"), - ) - } - - /// The profiles in file order, `profile.OTHER` references expanded - /// (`ProfileSet.fromTree`). - pub fn profiles(&self, options: &OptionSet) -> Result, String> { - let tree: Vec<(&str, Vec<&str>)> = self - .original - .iter() - .filter_map(|(k, v)| { - Some((k.strip_prefix("profile.")?, v.split_whitespace().collect())) - }) - .collect(); - fn build( - name: &str, - parents: &mut Vec, - tree: &[(&str, Vec<&str>)], - options: &OptionSet, - out: &mut Profile, - ) -> Result<(), String> { - let words = &tree - .iter() - .find(|(n, _)| *n == name) - .ok_or(format!("profile {name} does not exist"))? - .1; - for &w in words { - if let Some(program) = w.strip_prefix("!program.") { - out.disabled_programs.push(program.to_owned()); - } else if let Some(other) = w.strip_prefix("profile.") { - if parents.iter().any(|p| p == other) { - return Err(format!( - "profile {name} includes itself through {}", - parents.join(", ") - )); - } - parents.push(other.to_owned()); - build(other, parents, tree, options, out)?; - } else if let Some(option) = w.strip_prefix('!') { - out.values.insert(option.to_owned(), "false".to_owned()); - } else if let Some((option, value)) = - w.split_once('=').or_else(|| w.split_once(':')) - { - out.values.insert(option.to_owned(), value.to_owned()); - } else if options.bools.contains_key(w) { - out.values.insert(w.to_owned(), "true".to_owned()); - } - } - Ok(()) - } - tree.iter() - .map(|(name, _)| { - let mut p = Profile { - name: name.to_string(), - values: BTreeMap::new(), - disabled_programs: Vec::new(), - }; - build(name, &mut Vec::new(), &tree, options, &mut p)?; - Ok(p) - }) - .collect() - } - - /// The profile the values match, trying those that set the most options - /// first (`ProfileSet.scan`). - pub fn current_profile<'p>( - profiles: &'p [Profile], - options: &OptionSet, - values: &OptionValues, - ) -> Option<&'p Profile> { - let mut sorted: Vec<&Profile> = profiles.iter().collect(); - sorted.sort_by_key(|p| std::cmp::Reverse(p.values.len())); - sorted.into_iter().find(|p| p.matches(options, values)) - } - - /// Whether `program` (`composite1`, `world-1/shadowcomp`) runs: not - /// turned off by `program..enabled` or by the current profile. - pub fn program_enabled( - &self, - program: &str, - options: &OptionSet, - values: &OptionValues, - ) -> bool { - if let Ok(profiles) = self.profiles(options) { - if let Some(p) = Self::current_profile(&profiles, options, values) { - if p.disabled_programs.iter().any(|d| d == program) { - return false; - } - } - } - match self.preprocessed.get(&format!("program.{program}.enabled")) { - Some(expr) => boolean_expression(expr, options, values), - None => true, - } - } - - /// `image. = ...`: each - /// custom image's storage format (`rgba16f`), for [`crate::translate::Link`]. - pub fn image_formats(&self) -> BTreeMap { - self.preprocessed - .iter() - .filter_map(|(k, v)| { - Some(( - k.strip_prefix("image.")?.to_owned(), - v.split(' ').nth(2)?.to_lowercase(), - )) - }) - .collect() - } - - /// `texture.. = ` and `texture. = `: - /// custom textures defined in `shaders.properties`. - pub fn custom_textures(&self) -> Vec { - let mut out = Vec::new(); - for (k, v) in self.preprocessed.iter() { - let Some(rest) = k.strip_prefix("texture.") else { - continue; - }; - let path = v.split_whitespace().next().unwrap_or("").trim(); - if path.is_empty() { - continue; - } - let (stage, sampler) = match rest.split_once('.') { - Some((stage, sampler)) => (Some(stage.to_owned()), sampler.to_owned()), - None => (None, rest.to_owned()), - }; - out.push(CustomTexture { - stage, - sampler, - path: path.to_owned(), - }); - } - out - } - - /// `uniform.*` and `variable.*`: the pack's custom uniforms. - pub fn custom_uniforms(&self) -> crate::uniforms::CustomUniforms { - crate::uniforms::CustomUniforms::parse(self.preprocessed.iter()) - } - - /// `clouds`, `sun` and `moon`: what the pack does with the vanilla sky. - pub fn sky(&self) -> SkyDirectives { - let boolean = |key| { - !matches!( - self.preprocessed.get(key).map(str::trim), - Some("false" | "0") - ) - }; - SkyDirectives { - clouds: match self.preprocessed.get("clouds").map(str::trim) { - Some("off") => Some(CloudSetting::Off), - Some("fast") => Some(CloudSetting::Fast), - Some("fancy") => Some(CloudSetting::Fancy), - _ => None, - }, - sun: boolean("sun"), - moon: boolean("moon"), - } - } - - /// `blend.`: the program's blend override. - pub fn blend(&self, program: &str) -> Option> { - self.preprocessed - .get(&format!("blend.{program}")) - .or_else(|| { - let short = program.rsplit('/').next()?; - self.preprocessed.get(&format!("blend.{short}")) - }) - .map(parse_blend) - } - - /// `blend..`: per colour target overrides, by target index. - pub fn buffer_blends(&self, program: &str) -> Vec<(u32, Result)> { - let short = program.rsplit('/').next().unwrap_or(program); - let prefixes = [format!("blend.{program}."), format!("blend.{short}.")]; - let mut out = Vec::new(); - for (k, v) in self.preprocessed.iter() { - let Some(buffer) = prefixes.iter().find_map(|p| k.strip_prefix(p.as_str())) else { - continue; - }; - let Some(index) = LEGACY_TARGETS - .iter() - .position(|t| *t == buffer) - .map(|i| i as u32) - .or_else(|| buffer.strip_prefix("colortex")?.parse().ok()) - else { - continue; - }; - if let Some(existing) = out.iter_mut().find(|(i, _)| *i == index) { - existing.1 = parse_blend(v); - } else { - out.push((index, parse_blend(v))); - } - } - out - } -} - -/// `block.properties` (`block.=`), preprocessed as -/// `shaders.properties` is: each block's id by its name without namespace -/// (`oak_leaves`). A block listed with a state (`grass_block:snowy=true`) -/// takes that id only when no entry names it without one; numeric ids -/// (before 1.13) are skipped. -pub fn block_ids( - text: &str, - env: &Environment, - options: &OptionSet, - values: &OptionValues, -) -> Result, String> { - let properties = Properties::parse(&preprocess(text, env, options, values)?); - let mut plain = BTreeMap::new(); - let mut with_state = BTreeMap::new(); - for (key, value) in properties.iter() { - let Some(Ok(id)) = key.strip_prefix("block.").map(str::parse::) else { - continue; - }; - for entry in value.split_whitespace() { - let entry = entry.strip_prefix("minecraft:").unwrap_or(entry); - if entry.contains(':') && entry.split(':').nth(1).is_some_and(|s| s.contains('=')) { - let name = entry.split(':').next().unwrap_or(entry); - with_state.entry(name.to_owned()).or_insert(id); - } else if !entry.chars().all(|c| c.is_ascii_digit()) && !entry.contains('=') { - plain.insert(entry.to_owned(), id); - } - } - } - for (name, id) in with_state { - plain.entry(name).or_insert(id); - } - Ok(plain) -} - -/// The block a block sprite (`minecraft:block/oak_log_top`) belongs to in -/// `ids` ([`block_ids`]): the block named like it, else the longest one its -/// name starts with followed by `_` (`oak_log`, `water` for `water_still`). -pub fn sprite_block_id(sprite: &str, ids: &BTreeMap) -> Option { - let name = sprite.rsplit(['/', ':']).next()?; - if let Some(&id) = ids.get(name) { - return Some(id); - } - let mut end = name.len(); - while let Some(cut) = name[..end].rfind('_') { - if let Some(&id) = ids.get(&name[..cut]) { - return Some(id); - } - end = cut; - } - None -} - -/// Iris's `BooleanParser`: options joined by `!`, `&&`, `||` and brackets, -/// `&&` binding tighter. `true`/`1` and `false`/`0` are literals; a name that -/// isn't a boolean option is true; a malformed expression is true. -pub fn boolean_expression(expr: &str, options: &OptionSet, values: &OptionValues) -> bool { - #[derive(Clone, Copy, PartialEq)] - enum Op { - And, - Or, - Not, - Open, - } - let value_of = |name: &str| match name { - "true" | "1" => true, - "false" | "0" => false, - n => match options.bools.get(n) { - Some(o) => values.bools.get(n).copied().unwrap_or(o.default), - None => true, - }, - }; - fn push_value(name: &mut String, ops: &mut Vec, vals: &mut Vec, value: bool) { - name.clear(); - if ops.last() == Some(&Op::Not) { - ops.pop(); - vals.push(!value); - } else { - vals.push(value); - } - } - fn evaluate(ops: &mut Vec, vals: &mut Vec, bracket: bool) -> Option<()> { - let mut value = vals.pop()?; - while let Some(&op) = ops.last() { - if bracket && op == Op::Open { - break; - } - ops.pop(); - value = match op { - Op::And => vals.pop()? && value, - Op::Or => vals.pop()? || value, - Op::Not => !value, - Op::Open => value, - }; - } - if ops.last() == Some(&Op::Open) { - ops.pop(); - if ops.last() == Some(&Op::Not) { - ops.pop(); - value = !value; - } - } - vals.push(value); - Some(()) - } - let run = || -> Option { - let (mut ops, mut vals, mut name) = (Vec::new(), Vec::new(), String::new()); - let chars: Vec = expr.chars().collect(); - let mut i = 0; - while i < chars.len() { - match chars[i] { - '!' => ops.push(Op::Not), - c @ ('&' | '|') => { - if !name.is_empty() { - let v = value_of(&name); - push_value(&mut name, &mut ops, &mut vals, v); - } - if c == '&' { - if ops.last() != Some(&Op::And) { - ops.push(Op::Open); - } - ops.push(Op::And); - } else { - if ops.last() == Some(&Op::And) { - evaluate(&mut ops, &mut vals, true)?; - } - ops.push(Op::Or); - } - i += 1; - } - '(' => ops.push(Op::Open), - ')' => { - if !name.is_empty() { - let v = value_of(&name); - push_value(&mut name, &mut ops, &mut vals, v); - } - if ops.last() == Some(&Op::And) { - evaluate(&mut ops, &mut vals, true)?; - } - evaluate(&mut ops, &mut vals, true)?; - } - ' ' => {} - c => name.push(c), - } - i += 1; - } - if !name.is_empty() { - let v = value_of(&name); - push_value(&mut name, &mut ops, &mut vals, v); - } - evaluate(&mut ops, &mut vals, false)?; - let result = vals.pop()?; - (vals.is_empty() && ops.is_empty()).then_some(result) - }; - run().unwrap_or(true) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::options::{Annotated, OptionSet}; - - fn options(lines: &[&str]) -> OptionSet { - let lines: Vec = lines.iter().map(|l| l.to_string()).collect(); - let a = Annotated::parse(&lines); - let mut set = OptionSet::default(); - a.bools.values().for_each(|o| set.add_bool(o)); - a.strings.values().for_each(|o| set.add_string(o)); - set - } - - /// `block.properties` as Chocapic13' writes it, and sprites matched to - /// its blocks. - #[test] - fn block_ids_and_the_sprites_of_their_blocks() { - let text = "\ -#if MC_VERSION >= 11300 -block.10001= minecraft:wheat minecraft:short_grass -block.10003 = minecraft:oak_leaves \\ - minecraft:vine -block.10006 = minecraft:grass_block:snowy=true minecraft:snow -block.8 = minecraft:water -block.2 = minecraft:grass_block -#else -block.10001= 6 31 -#endif -layer.translucent=minecraft:glass -"; - let env = Environment { - defines: vec![("MC_VERSION".into(), "260300".into())], - }; - let ids = block_ids(text, &env, &OptionSet::default(), &OptionValues::default()).unwrap(); - assert_eq!(ids.get("vine"), Some(&10003)); - // The stateless entry wins over the snowy one. - assert_eq!(ids.get("grass_block"), Some(&2)); - assert!(!ids.contains_key("6") && !ids.contains_key("glass")); - for (sprite, id) in [ - ("minecraft:block/oak_leaves", Some(10003)), - ("minecraft:block/water_still", Some(8)), - ("minecraft:block/grass_block_top", Some(2)), - ("minecraft:block/wheat_stage7", Some(10001)), - ("minecraft:block/stone", None), - ] { - assert_eq!(sprite_block_id(sprite, &ids), id, "{sprite}"); - } - } - - #[test] - fn java_properties_syntax() { - let p = Properties::parse( - "# comment\n! also\n a = 1\nb:2\nc 3\nlong = x \\\n y\nd=\\u0041\\\\z\na=last\nempty\n", - ); - let all: Vec<(&str, &str)> = p.iter().collect(); - assert_eq!( - all, - [ - ("a", "last"), - ("b", "2"), - ("c", "3"), - ("long", "x y"), - ("d", "A\\z"), - ("empty", "") - ] - ); - } - - #[test] - fn floats_in_conditions_count_as_their_integer_part() { - assert_eq!( - [".5", "0.75", "1.0", "2.5f", "1.5e1", "7", "0x10"].map(jcpp_integer), - [Some(0), Some(0), Some(1), Some(2), Some(10), Some(7), None] - ); - // Steadfast's fog strength: the option is 0.5, so Iris takes the - // 0.75 branch (0 == 0). - let set = options(&["#define FOG 0.5 // [0.0 0.5 0.75 1.0]"]); - let text = "\ -#if FOG == 1.0 -a = 1 -#elif FOG == 0.75 -a = 0.75 -#else -a = 0.5 -#endif -#if(FOG<1e1) -b = 1 -#endif -"; - let env = Environment { - defines: Vec::new(), - }; - let values = OptionValues::default(); - let out = preprocess(text, &env, &set, &values).unwrap(); - let lines: Vec<&str> = out.lines().filter(|l| !l.is_empty()).collect(); - assert_eq!(lines, ["a = 0.75", "b = 1"]); - // Photon ends an `#if`'d value with `) \` before `#else`: the value - // stops at the empty line the directive leaves. - let text = "\ -#if FOG == 0 -a = if( \\ - moonPhase == 0, 1.0, \\ - 2.0 \\ -) \\ -#else -a = 1.0 -#endif - -b = 2 -"; - let out = preprocess(text, &env, &set, &values).unwrap(); - let parsed = Properties::parse(&out); - assert_eq!(parsed.get("a"), Some("if( moonPhase == 0, 1.0, 2.0 ) ")); - assert_eq!(parsed.get("b"), Some("2")); - assert_eq!( - jcpp_condition("#elif FOG == 0.75 && X > 2.5", &set, &values), - "#elif 0 == 0 && X > 2" - ); - } - - #[test] - fn boolean_expressions() { - let set = options(&["#define A", "//#define B", "#define C"]); - let values = OptionValues::default(); - let eval = |e: &str| boolean_expression(e, &set, &values); - assert!(eval("A") && !eval("B") && !eval("!A") && eval("!B")); - assert!(!eval("A && B") && eval("A || B") && eval("B || C")); - assert!(eval("A && B || C"), "&& binds tighter"); - assert!(!eval("B || C && B")); - assert!(!eval("!(A && C)") && eval("!(A && B)") && eval("A && !B")); - assert!(eval("(B || C) && A")); - assert!(eval("UNKNOWN") && eval("1") && !eval("0") && !eval("false")); - assert!(eval("A &&"), "malformed: true"); - let mut on = OptionValues::default(); - on.bools.insert("B".into(), true); - assert!(boolean_expression("A && B", &set, &on)); - } - - #[test] - fn preprocessed_and_original_reads() { - let set = options(&["//#define COLORED", "#define QUALITY 2 // [1 2 3]"]); - let text = "\ -screen = QUALITY [LIGHT] * -screen.LIGHT = COLORED -screen.LIGHT.columns = 1 -sliders = QUALITY -profile.LOW = QUALITY:1 !COLORED !program.composite2 -profile.HIGH = profile.LOW QUALITY:3 COLORED -#ifdef MC_OS_MAC -#define QUALITY 5 -#endif -#if QUALITY >= 5 && defined MC_OS_MAC - program.composite1.enabled = false -#else - program.composite1.enabled = true -#endif -program.world0/shadowcomp.enabled = COLORED -blend.gbuffers_water = SRC_ALPHA ONE_MINUS_SRC_ALPHA ONE ZERO -blend.gbuffers_water.gaux2 = off -blend.gbuffers_water.colortex9 = ONE ONE ONE ONE -size.buffer.colortex4 = 0.50 QUALITY -image.lightimg0 = lighttex0 rgba RGBA16F half_float false false 128 64 128 -texture.composite.colortex4 = textures/noise.png -# a comment, not a directive -iris.features.optional = CUSTOM_IMAGES SSBO -"; - let mut env = Environment { - defines: Vec::new(), - }; - env.define("MC_OS_MAC", ""); - let values = OptionValues::default(); - let p = ShaderProperties::new(text, &env, &set, &values).unwrap(); - use MenuElement::*; - assert_eq!( - p.screen(None).unwrap(), - [ - Option("QUALITY".into()), - Empty, - Screen("LIGHT".into()), - Profile, - Rest - ] - ); - assert_eq!(p.screens(), ["LIGHT"]); - assert_eq!((p.columns(None), p.columns(Some("LIGHT"))), (None, Some(1))); - assert_eq!(p.sliders(), ["QUALITY"]); - assert_eq!( - p.image_formats(), - [("lightimg0".to_owned(), "rgba16f".to_owned())].into() - ); - assert_eq!( - p.custom_textures(), - [CustomTexture { - stage: Some("composite".into()), - sampler: "colortex4".into(), - path: "textures/noise.png".into(), - }] - ); - assert_eq!(p.features(), (vec![], vec!["CUSTOM_IMAGES", "SSBO"])); - // The preprocessed read: #if picked, values expanded, spelling kept. - assert!(!p.program_enabled("composite1", &set, &values)); - assert!(!p.program_enabled("world0/shadowcomp", &set, &values)); - assert_eq!( - p.preprocessed.get("size.buffer.colortex4"), - Some("0.50 5"), - "QUALITY redefined on macOS" - ); - assert_eq!( - p.blend("gbuffers_water"), - Some(Ok(Blend::Factors( - ["SRC_ALPHA", "ONE_MINUS_SRC_ALPHA", "ONE", "ZERO"].map(String::from) - ))) - ); - assert_eq!( - p.buffer_blends("gbuffers_water"), - [ - (5, Ok(Blend::Off)), - (9, Ok(Blend::Factors(["ONE"; 4].map(String::from)))) - ] - ); - - let profiles = p.profiles(&set).unwrap(); - assert_eq!( - profiles[1].values.get("QUALITY").map(String::as_str), - Some("3") - ); - assert_eq!(profiles[1].disabled_programs, ["composite2"]); - // Defaults (QUALITY 2) match no profile; LOW's values match LOW, which - // also turns composite2 off. - assert_eq!( - ShaderProperties::current_profile(&profiles, &set, &values), - None - ); - let low = OptionValues::new(&set, &[("QUALITY".to_owned(), "1".to_owned())].into()); - assert_eq!( - ShaderProperties::current_profile(&profiles, &set, &low).map(|p| p.name.as_str()), - Some("LOW") - ); - assert!( - !p.program_enabled("composite2", &set, &low) - && p.program_enabled("composite2", &set, &values) - ); - - // With COLORED on and QUALITY 1, the other branches apply. - let high = OptionValues::new( - &set, - &[ - ("COLORED".to_owned(), "true".to_owned()), - ("QUALITY".to_owned(), "1".to_owned()), - ] - .into(), - ); - let p = ShaderProperties::new( - text, - &Environment { - defines: Vec::new(), - }, - &set, - &high, - ) - .unwrap(); - assert!(p.program_enabled("composite1", &set, &high)); - assert!(p.program_enabled("world0/shadowcomp", &set, &high)); - } -} diff --git a/crates/horizon-shaderpack/src/source.rs b/crates/horizon-shaderpack/src/source.rs deleted file mode 100644 index 4e4bcd779..000000000 --- a/crates/horizon-shaderpack/src/source.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! A Java shader pack's files: a folder or a zip holding a `shaders/` folder, -//! found the way Iris finds it (`Iris.loadExternalZipShaderpack`). - -use std::{ - collections::BTreeMap, - fs, - io::Read, - path::{Path, PathBuf}, -}; - -/// Program source extensions Iris loads (vertex, fragment, geometry, -/// compute, tessellation control and evaluation). -pub const PROGRAM_EXTENSIONS: &[&str] = &["vsh", "fsh", "gsh", "csh", "tcs", "tes"]; - -/// The files under a pack's `shaders/` folder, addressed by `/`-separated -/// paths relative to it (`lib/common.glsl`). -pub enum PackFiles { - Folder(PathBuf), - /// A zip is small (a few MB of text), so it is read once. - Zip(BTreeMap>), -} - -impl PackFiles { - /// Open `path`, a pack folder (with `shaders/` in it) or a `.zip`. - pub fn open(path: &Path) -> Result { - if path.is_dir() { - let shaders = path.join("shaders"); - return if shaders.is_dir() { - Ok(Self::Folder(shaders)) - } else { - Err("no shaders folder".into()) - }; - } - let file = fs::File::open(path).map_err(|e| e.to_string())?; - let mut zip = zip::ZipArchive::new(file).map_err(|e| format!("not a zip: {e}"))?; - let names: Vec = zip.file_names().map(str::to_owned).collect(); - // `shaders/` at the root, else the first folder ending in `shaders` - // (`Pack-main/shaders/`), as Iris walks the zip. - let mut prefixes: Vec<&str> = names - .iter() - .filter_map(|n| { - let at = n.find("shaders/")?; - (at == 0 || n.as_bytes()[at - 1] == b'/').then(|| &n[..at + "shaders/".len()]) - }) - .collect(); - prefixes.sort_by_key(|p| (p.len(), *p)); - let prefix = prefixes.first().ok_or("no shaders folder")?.to_string(); - let mut files = BTreeMap::new(); - for name in &names { - let Some(rel) = name.strip_prefix(&prefix) else { - continue; - }; - if rel.is_empty() || rel.ends_with('/') { - continue; - } - let mut entry = zip.by_name(name).map_err(|e| e.to_string())?; - let mut bytes = Vec::new(); - entry - .read_to_end(&mut bytes) - .map_err(|e| format!("{name}: {e}"))?; - files.insert(rel.to_owned(), bytes); - } - Ok(Self::Zip(files)) - } - - /// Raw bytes of a file relative to `shaders/` (leading `/` or `./` stripped), - /// or `None` when it doesn't exist. - pub fn read_bytes(&self, rel: &str) -> Option> { - let clean = rel - .trim() - .trim_start_matches("./") - .trim_start_matches('/') - .trim_start_matches("shaders/"); - if clean.is_empty() || clean.contains("..") { - return None; - } - match self { - Self::Folder(root) => fs::read(root.join(clean)).ok(), - Self::Zip(files) => files.get(clean).cloned(), - } - } - - /// A text file, or `None` when it doesn't exist. Invalid UTF-8 is - /// replaced, as packs are often saved in Latin-1. - pub fn read(&self, rel: &str) -> Option { - let bytes = self.read_bytes(rel)?; - Some(String::from_utf8_lossy(&bytes).into_owned()) - } - - /// The files directly in folder `dir` (`""` for `shaders/` itself), sorted. - pub fn list(&self, dir: &str) -> Vec { - let prefix = if dir.is_empty() { - String::new() - } else { - format!("{dir}/") - }; - let mut out: Vec = match self { - Self::Folder(root) => fs::read_dir(root.join(dir)) - .into_iter() - .flatten() - .flatten() - .filter(|e| e.file_type().is_ok_and(|t| t.is_file())) - .filter_map(|e| Some(format!("{prefix}{}", e.file_name().to_str()?))) - .collect(), - Self::Zip(files) => files - .keys() - .filter(|k| { - k.strip_prefix(&prefix) - .is_some_and(|rest| !rest.contains('/')) - }) - .cloned() - .collect(), - }; - out.sort(); - out - } - - /// The program folders: `shaders/` itself and the per-dimension folders, - /// from `dimension.properties` (`dimension.world-1=minecraft:the_nether`) - /// or, without one, Iris's defaults `world0`, `world-1` and `world1`. - pub fn program_dirs(&self) -> Vec { - let mut dirs = vec![String::new()]; - match self.read("dimension.properties") { - Some(text) => { - for line in text.lines() { - if let Some((key, _)) = line.trim().split_once('=') { - if let Some(dir) = key.trim().strip_prefix("dimension.") { - dirs.push(dir.to_owned()); - } - } - } - } - None => dirs.extend(["world0", "world-1", "world1"].map(String::from)), - } - dirs.dedup(); - dirs - } - - /// Every program source file of the pack (`composite1.fsh`, - /// `world-1/gbuffers_water.vsh`), sorted. - pub fn programs(&self) -> Vec { - let mut out: Vec = self - .program_dirs() - .iter() - .flat_map(|d| self.list(d)) - .filter(|f| { - f.rsplit_once('.') - .is_some_and(|(_, ext)| PROGRAM_EXTENSIONS.contains(&ext)) - }) - .collect(); - out.sort(); - out.dedup(); - out - } -} diff --git a/crates/horizon-shaderpack/src/translate.rs b/crates/horizon-shaderpack/src/translate.rs deleted file mode 100644 index 1fa3a6c14..000000000 --- a/crates/horizon-shaderpack/src/translate.rs +++ /dev/null @@ -1,2152 +0,0 @@ -//! Preprocessed pack GLSL to WGSL. Packs are written for `#version 120`..`460 -//! compatibility` and lean on the compatibility profile; naga's GLSL front end -//! takes Vulkan-style GLSL 450. The legaliser parses the program into a -//! syntax tree (glsl-lang), rewrites it the way Iris's glsl-transformer -//! patches do for the core profile, prints GLSL 450 and has naga turn that -//! into a validated module and WGSL: -//! -//! - `attribute`/`varying` become `in`/`out` with locations; matrix, nested -//! array and struct varyings are split into vectors (WGSL can pass none of -//! them directly between stages); -//! - loose uniforms go into one std140 block (`bool`s stored as `int`); -//! - `sampler2D` & co. become a texture and a sampler binding, rebuilt as -//! `sampler2D(t, s)` where used, also through function parameters, and not -//! where a local hides them; -//! - compatibility built-ins (`gl_Vertex`, `gl_Color`, `gl_FragData[n]`, -//! `gl_ModelViewMatrix`, `ftransform()`, ...) and Iris's core-profile names -//! (`vaPosition`, `modelViewMatrix`, ...) are declared; -//! - legacy texture calls are renamed, `shadow2D` returns a `vec4` again and a -//! swizzled shadow lookup is widened first (naga has no scalar swizzle); -//! - outside fragment programs, `texture`, `textureProj` and their `Offset` -//! forms become their `Lod` forms at level 0 (no derivatives there; naga -//! refuses the implicit ones), a bias dropped; -//! - global initialisers run at the start of `main` (drivers accept -//! non-constant ones), unsized global arrays are sized from them; -//! - `isnan`, `isinf` and `modf` are helper functions (WGSL has none); -//! - what naga refuses and drivers accept: `%` goes through overloads (naga -//! won't take `ivec2 % int`), `mat2(vec4)` goes through `iris_mat2`, `1 << 31` -//! shifts as unsigned before casting back to `int`, `sqrt` of a negative -//! constant isn't folded, constants naga can't fold (`?:`, `mix`, `<<`) -//! become globals set in `main`, and 16/8-bit types are their 32-bit ones. -//! -//! The programs of one pass share a [`Link`], so their locations, bindings and -//! uniform block agree. - -use crate::Stage; -use glsl_lang::{ - ast::{self, NodeContent}, - parse::DefaultParse, - transpiler::glsl::{ - FormattingState, show_declaration, show_expr, show_external_declaration, - show_type_specifier, - }, -}; -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; - -/// Locations, bindings and uniforms shared by the programs of one pass: a -/// pass's vertex program is translated first, its fragment program second. -#[derive(Clone, Default, Debug)] -pub struct Link { - /// Varying name → location (split matrices and arrays as `name#i`). - pub varyings: BTreeMap, - /// Vertex attribute name → location. - pub attributes: BTreeMap, - /// Texture, sampler, image and block name → binding in group 0 (the - /// uniform block is binding 0). - pub bindings: BTreeMap, - /// The uniform block's members in order (`"mat4 gbufferModelView"`); a - /// later program's block extends an earlier one's, so offsets agree. - pub uniforms: Vec<(String, String)>, - /// Storage formats of images whose declaration has none. - pub image_formats: BTreeMap, - /// Samplers shared by texture name (`colortex0` → `iris_linear`), so a - /// pass binds a few samplers rather than one per texture (Metal allows - /// 16). Once any is named, comparison samplers share `iris_compare` and - /// integer ones `iris_nearest`; textures not named keep `{name}_s`. - pub shared_samplers: BTreeMap, - /// The bind group of the pack's bindings (0; 2 for geometry programs, - /// whose first groups are Horizon's). - pub set: u32, - /// Set for `gbuffers_*` programs drawn over Horizon's own geometry. - pub geometry: Option, -} - -/// How a `gbuffers_*` program runs over Horizon's geometry: its vertex -/// attributes aren't inputs but globals the glue fills from Horizon's -/// vertices before the program's `main`. -#[derive(Clone, Debug, Default)] -pub struct Geometry { - /// GLSL put before the vertex program: Horizon's vertex inputs and - /// buffers, and a `void hz_vertex()` run first in `main`. - pub vertex_glue: String, - /// Each attribute's value as a `vec4` expression of the glue - /// (`iris_Vertex`, `mc_Entity`, `vaUV0`...). Others are zero. - pub attributes: BTreeMap, - /// Iris's alpha test: the fragment is discarded unless output 0's - /// alpha is above it. - pub alpha_test: Option, -} - -impl Link { - fn location(map: &mut BTreeMap, name: &str) -> u32 { - let next = map.len() as u32; - *map.entry(name.to_owned()).or_insert(next) - } - - fn binding(&mut self, name: &str) -> u32 { - let next = self.bindings.len() as u32 + 1; - *self.bindings.entry(name.to_owned()).or_insert(next) - } - - fn uniform(&mut self, name: &str, decl: String) -> Result<(), String> { - match self.uniforms.iter().find(|(n, _)| n == name) { - Some((_, d)) if *d != decl => Err(format!( - "uniform {name} is declared as both `{d}` and `{decl}`" - )), - Some(_) => Ok(()), - None => { - self.uniforms.push((name.to_owned(), decl)); - Ok(()) - } - } - } -} - -/// What a translated program binds and passes, for the pipeline that runs it. -#[derive(Default, Debug)] -pub struct Interface { - /// `(name, kind)` of each sampler the program reads (`colortex0`, - /// `sampler2D`); its bindings are `{name}_t` and its sampler's in the [`Link`]. - pub samplers: Vec<(String, String)>, - /// Each sampler's sampler variable: `{name}_s`, or a shared one - /// ([`Link::shared_samplers`]). - pub sampler_vars: BTreeMap, - /// `(name, format)` of each image. - pub images: Vec<(String, String)>, - /// Uniform names the program declares or uses. - pub uniforms: BTreeSet, - /// Vertex attributes read (vertex programs). - pub attributes: BTreeSet, - /// Fragment outputs: location → name (`iris_FragData0` for `gl_FragData[0]`). - pub outputs: BTreeMap, -} - -pub struct Translated { - /// The legalised GLSL 450 naga read (kept for diagnostics). - pub glsl: String, - pub module: naga::Module, - pub info: naga::valid::ModuleInfo, - pub wgsl: String, - pub interface: Interface, -} - -/// Combined sampler types: (texture type, sampler type, compares depth). -const OPAQUE: &[(&str, &str, &str, bool)] = &[ - ("sampler1D", "texture1D", "sampler", false), - ("sampler2D", "texture2D", "sampler", false), - ("sampler3D", "texture3D", "sampler", false), - ("samplerCube", "textureCube", "sampler", false), - ("sampler2DArray", "texture2DArray", "sampler", false), - ("sampler2DShadow", "texture2D", "samplerShadow", true), - ( - "sampler2DArrayShadow", - "texture2DArray", - "samplerShadow", - true, - ), - ("samplerCubeShadow", "textureCube", "samplerShadow", true), - ("isampler2D", "itexture2D", "sampler", false), - ("usampler2D", "utexture2D", "sampler", false), - ("isampler3D", "itexture3D", "sampler", false), - ("usampler3D", "utexture3D", "sampler", false), - ("isampler2DArray", "itexture2DArray", "sampler", false), - ("usampler2DArray", "utexture2DArray", "sampler", false), -]; - -fn opaque(ty: &str) -> Option<&'static (&'static str, &'static str, &'static str, bool)> { - OPAQUE.iter().find(|o| o.0 == ty) -} - -/// Compatibility-profile and ARB calls and their core names. -const LEGACY_CALLS: &[(&str, &str)] = &[ - ("texture1D", "texture"), - ("texture2D", "texture"), - ("texture3D", "texture"), - ("textureCube", "texture"), - ("texture2DLod", "textureLod"), - ("texture2DLodARB", "textureLod"), - ("texture3DLod", "textureLod"), - ("textureCubeLod", "textureLod"), - ("texture2DGrad", "textureGrad"), - ("texture2DGradARB", "textureGrad"), - ("texture2DProj", "textureProj"), - ("texture2DProjLod", "textureProjLod"), - ("texelFetch2D", "texelFetch"), - ("isnan", "iris_isnan"), - ("isinf", "iris_isinf"), - ("modf", "iris_modf"), -]; - -/// Legacy shadow lookups, which return a `vec4`. -const SHADOW_CALLS: &[(&str, &str)] = &[ - ("shadow2D", "texture"), - ("shadow2DLod", "textureLod"), - ("shadow2DProj", "textureProj"), -]; - -/// Compatibility-profile uniforms: (name, member declaration). -const LEGACY_UNIFORMS: &[(&str, &str)] = &[ - ("gl_ModelViewMatrix", "mat4 iris_ModelViewMatrix"), - ("gl_ProjectionMatrix", "mat4 iris_ProjectionMatrix"), - ( - "gl_ModelViewProjectionMatrix", - "mat4 iris_ModelViewProjectionMatrix", - ), - ("gl_NormalMatrix", "mat3 iris_NormalMatrix"), - ( - "gl_ModelViewMatrixInverse", - "mat4 iris_ModelViewMatrixInverse", - ), - ( - "gl_ProjectionMatrixInverse", - "mat4 iris_ProjectionMatrixInverse", - ), - ("gl_TextureMatrix", "mat4 iris_TextureMatrix[8]"), - ("gl_Fog", "iris_FogParameters iris_Fog"), -]; - -/// Compatibility-profile vertex attributes: (name, type). -const LEGACY_ATTRIBUTES: &[(&str, &str)] = &[ - ("gl_Vertex", "vec4"), - ("gl_Normal", "vec3"), - ("gl_Color", "vec4"), - ("gl_MultiTexCoord0", "vec4"), - ("gl_MultiTexCoord1", "vec4"), - ("gl_MultiTexCoord2", "vec4"), - ("gl_MultiTexCoord3", "vec4"), - ("gl_FogCoord", "float"), -]; - -/// Iris's core-profile names, declared when a program uses them without -/// declaring them: (name, type, is an attribute). -const IRIS_IMPLICIT: &[(&str, &str, bool)] = &[ - ("vaPosition", "vec3", true), - ("vaColor", "vec4", true), - ("vaUV0", "vec2", true), - ("vaUV1", "ivec2", true), - ("vaUV2", "ivec2", true), - ("vaNormal", "vec3", true), - ("modelViewMatrix", "mat4", false), - ("modelViewMatrixInverse", "mat4", false), - ("projectionMatrix", "mat4", false), - ("projectionMatrixInverse", "mat4", false), - ("normalMatrix", "mat3", false), - ("textureMatrix", "mat4", false), - ("chunkOffset", "vec3", false), - ("alphaTestRef", "float", false), -]; - -/// Built-ins naga lacks. `isnan` and `isinf` test the bits: Metal compiles -/// with fast math, which folds `x != x` to false. -const HELPERS: &str = "\ -struct iris_FogParameters { vec4 color; float density; float start; float end; float scale; }; -bool iris_isnan(float x) { return (floatBitsToUint(x) & 0x7fffffffu) > 0x7f800000u; } -bvec2 iris_isnan(vec2 x) { return greaterThan(floatBitsToUint(x) & 0x7fffffffu, uvec2(0x7f800000u)); } -bvec3 iris_isnan(vec3 x) { return greaterThan(floatBitsToUint(x) & 0x7fffffffu, uvec3(0x7f800000u)); } -bvec4 iris_isnan(vec4 x) { return greaterThan(floatBitsToUint(x) & 0x7fffffffu, uvec4(0x7f800000u)); } -bool iris_isinf(float x) { return (floatBitsToUint(x) & 0x7fffffffu) == 0x7f800000u; } -bvec2 iris_isinf(vec2 x) { return equal(floatBitsToUint(x) & 0x7fffffffu, uvec2(0x7f800000u)); } -bvec3 iris_isinf(vec3 x) { return equal(floatBitsToUint(x) & 0x7fffffffu, uvec3(0x7f800000u)); } -bvec4 iris_isinf(vec4 x) { return equal(floatBitsToUint(x) & 0x7fffffffu, uvec4(0x7f800000u)); } -float iris_modf(float x, out float i) { i = trunc(x); return x - i; } -vec2 iris_modf(vec2 x, out vec2 i) { i = trunc(x); return x - i; } -vec3 iris_modf(vec3 x, out vec3 i) { i = trunc(x); return x - i; } -vec4 iris_modf(vec4 x, out vec4 i) { i = trunc(x); return x - i; } -"; - -/// Overloaded `mat2` constructor helper: drivers accept `mat2(vec4)` (Bloop), -/// which naga's GLSL front end refuses. -const MAT2_HELPERS: &str = "\ -mat2 iris_mat2(float x) { return mat2(x); } -mat2 iris_mat2(vec2 a, vec2 b) { return mat2(a, b); } -mat2 iris_mat2(float a, float b, float c, float d) { return mat2(a, b, c, d); } -mat2 iris_mat2(vec4 v) { return mat2(v.xy, v.zw); } -mat2 iris_mat2(mat2 m) { return m; } -mat2 iris_mat2(mat3 m) { return mat2(m); } -mat2 iris_mat2(mat4 m) { return mat2(m); } -"; - -/// A fragment output narrower than four channels (`out vec3 color`, -/// Steadfast's `final`): the four-channel type it is written through and the -/// epilogue line that fills it, with alpha 1 and the rest 0. `None` when the -/// output already has four channels. -fn widen_output(ty: &str, name: &str) -> Option<(String, String)> { - let (wide, one, zero) = match ty { - "float" | "vec2" | "vec3" => ("vec4", "1.0", "0.0"), - "int" | "ivec2" | "ivec3" => ("ivec4", "1", "0"), - "uint" | "uvec2" | "uvec3" => ("uvec4", "1u", "0u"), - _ => return None, - }; - let channels = match ty.chars().last() { - Some('2') => 2, - Some('3') => 3, - _ => 1, - }; - let mut parts = vec![name.to_owned()]; - parts.extend((channels..3).map(|_| zero.to_owned())); - parts.push(one.to_owned()); - Some(( - wide.to_owned(), - format!("iris_out_{name} = {wide}({});", parts.join(", ")), - )) -} - -/// `%` as overloads: naga's GLSL front end splats a scalar operand for -/// `+ - / & ^ |` but not for `%`, and rejects `ivec2(uv) % 256` (Noble). -fn modulo_helpers() -> String { - let mut out = String::new(); - for (s, v) in [("int", "ivec"), ("uint", "uvec")] { - out.push_str(&format!("{s} iris_mod({s} a, {s} b) {{ return a % b; }}\n")); - for n in 2..=4 { - let v = format!("{v}{n}"); - out.push_str(&format!("{v} iris_mod({v} a, {v} b) {{ return a % b; }}\n")); - out.push_str(&format!( - "{v} iris_mod({v} a, {s} b) {{ return a % {v}(b); }}\n" - )); - out.push_str(&format!( - "{v} iris_mod({s} a, {v} b) {{ return {v}(a) % b; }}\n" - )); - } - } - out -} - -/// 16- and 8-bit types (`GL_NV_gpu_shader5`, `GL_EXT_shader_explicit_arithmetic_types`) -/// and their built-ins, as the 32-bit ones WGSL has: Noble declares -/// `u16vec2` & co. when Iris reports an NVIDIA GPU, and falls back to these -/// same aliases elsewhere. -const SMALL_TYPES: &[(&str, &str)] = &[ - ("float16_t", "float"), - ("f16vec2", "vec2"), - ("f16vec3", "vec3"), - ("f16vec4", "vec4"), - ("int16_t", "int"), - ("i16vec2", "ivec2"), - ("i16vec3", "ivec3"), - ("i16vec4", "ivec4"), - ("uint16_t", "uint"), - ("u16vec2", "uvec2"), - ("u16vec3", "uvec3"), - ("u16vec4", "uvec4"), - ("int8_t", "int"), - ("i8vec2", "ivec2"), - ("i8vec3", "ivec3"), - ("i8vec4", "ivec4"), - ("uint8_t", "uint"), - ("u8vec2", "uvec2"), - ("u8vec3", "uvec3"), - ("u8vec4", "uvec4"), - ("packFloat2x16", "packHalf2x16"), - ("unpackFloat2x16", "unpackHalf2x16"), -]; - -/// `source` with [`SMALL_TYPES`] replaced, word by word. -fn widen_small_types(source: &str) -> std::borrow::Cow<'_, str> { - if !SMALL_TYPES.iter().any(|(t, _)| source.contains(t)) { - return source.into(); - } - let mut out = String::with_capacity(source.len()); - let mut rest = source; - while let Some(c) = rest.chars().next() { - let len = if c.is_ascii_alphanumeric() || c == '_' { - rest.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_')) - .unwrap_or(rest.len()) - } else { - c.len_utf8() - }; - let word = &rest[..len]; - out.push_str( - SMALL_TYPES - .iter() - .find(|(t, _)| *t == word) - .map_or(word, |(_, w)| w), - ); - rest = &rest[len..]; - } - out.into() -} - -/// Translate one preprocessed program. `locate` turns a line of `source` -/// into `file:line` for errors. -pub fn translate( - stage: Stage, - source: &str, - link: &mut Link, - locate: &dyn Fn(u32) -> String, -) -> Result { - let naga_stage = match stage { - Stage::Vertex => naga::ShaderStage::Vertex, - Stage::Fragment => naga::ShaderStage::Fragment, - Stage::Compute => naga::ShaderStage::Compute, - _ => return Err(format!("{stage:?} programs are not supported")), - }; - let source = widen_small_types(source); - let mut unit = ast::TranslationUnit::parse(&source).map_err(|e| { - let line = e.line() + 1; - format!("{}: {}", locate(line), e.inner()) - })?; - let mut legaliser = Legaliser::new(stage, link); - legaliser.unit(&mut unit)?; - let glsl = legaliser.finish(&unit)?; - let options = naga::front::glsl::Options { - stage: naga_stage, - defines: Default::default(), - }; - let mut module = naga::front::glsl::Frontend::default() - .parse(&options, &glsl) - .map_err(|e| first_lines(&e.emit_to_string(&glsl)))?; - unpad_private_arrays(&mut module); - end_returning_cases(&mut module); - let info = naga::valid::Validator::new( - naga::valid::ValidationFlags::all(), - naga::valid::Capabilities::all(), - ) - .validate(&module) - .map_err(|e| first_lines(&e.emit_to_string(&glsl)))?; - let wgsl = - naga::back::wgsl::write_string(&module, &info, naga::back::wgsl::WriterFlags::empty()) - .map_err(|e| e.to_string())?; - Ok(Translated { - glsl, - module, - info, - wgsl, - interface: legaliser.interface, - }) -} - -/// A `case` that ends with `return` or `discard` rather than `break` -/// (Photon's displacement) is a fall-through one to naga's GLSL front end, -/// which its WGSL writer refuses: nothing falls through it. -fn end_returning_cases(module: &mut naga::Module) { - fn block(b: &mut naga::Block) { - for s in b.iter_mut() { - match s { - naga::Statement::Block(inner) => block(inner), - naga::Statement::If { accept, reject, .. } => { - block(accept); - block(reject); - } - naga::Statement::Loop { - body, continuing, .. - } => { - block(body); - block(continuing); - } - naga::Statement::Switch { cases, .. } => { - for case in cases { - block(&mut case.body); - if case.fall_through - && matches!( - case.body.last(), - Some(naga::Statement::Return { .. } | naga::Statement::Kill) - ) - { - case.fall_through = false; - } - } - } - _ => {} - } - } - } - for (_, f) in module.functions.iter_mut() { - block(&mut f.body); - } - for e in &mut module.entry_points { - block(&mut e.function.body); - } -} - -/// Built-in functions naga folds in `const` initialisers. -const FOLDABLE: &[&str] = &[ - "radians", - "degrees", - "sin", - "cos", - "tan", - "asin", - "acos", - "atan", - "pow", - "exp", - "log", - "exp2", - "log2", - "sqrt", - "inversesqrt", - "abs", - "sign", - "floor", - "trunc", - "round", - "ceil", - "fract", - "mod", - "min", - "max", - "clamp", - "step", - "length", - "distance", - "dot", - "cross", - "normalize", -]; - -fn constructor(name: &str) -> bool { - ["bool", "int", "uint", "float", "double"].contains(&name) - || ["vec", "ivec", "uvec", "bvec", "dvec", "mat", "dmat"] - .iter() - .any(|p| name.starts_with(p)) -} - -/// Whether `list` has an initialiser naga's constant folder refuses (`?:`, -/// `mix`, `smoothstep`, `1 << 31`, or a reference to a constant already turned -/// into a global). -fn needs_runtime(list: &ast::InitDeclaratorList, runtime: &HashSet) -> bool { - let expr = |e: &ast::Expr| { - fn walk(e: &ast::Expr, f: &dyn Fn(&ast::Expr) -> bool) -> bool { - use ast::ExprData as E; - f(e) || match &e.content { - E::Unary(_, a) | E::PostInc(a) | E::PostDec(a) | E::Dot(a, _) => walk(a, f), - E::Binary(_, a, b) | E::Assignment(a, _, b) | E::Comma(a, b) | E::Bracket(a, b) => { - walk(a, f) || walk(b, f) - } - E::Ternary(a, b, c) => walk(a, f) || walk(b, f) || walk(c, f), - E::FunCall(_, args) => args.iter().any(|a| walk(a, f)), - _ => false, - } - } - walk(e, &|e| match &e.content { - ast::ExprData::Ternary(..) => true, - ast::ExprData::Binary(op, _, _) - if matches!( - op.content, - ast::BinaryOpData::LShift | ast::BinaryOpData::RShift - ) => - { - true - } - ast::ExprData::Variable(v) => runtime.contains(v.as_str()), - ast::ExprData::FunCall(f, _) => { - callee(f).is_some_and(|c| !constructor(&c) && !FOLDABLE.contains(&c.as_str())) - } - _ => false, - }) - }; - fn init(i: &ast::Initializer, expr: &dyn Fn(&ast::Expr) -> bool) -> bool { - match &i.content { - ast::InitializerData::Simple(e) => expr(e), - ast::InitializerData::List(items) => items.iter().any(|i| init(i, expr)), - } - } - let tail = list.tail.iter().filter_map(|t| t.initializer.as_ref()); - list.head - .initializer - .iter() - .chain(tail) - .any(|i| init(i, &expr)) -} - -/// A negative number: `-1.0`, `-1`. -fn negative_constant(e: &ast::Expr) -> bool { - use ast::ExprData as E; - match &e.content { - E::Unary(op, a) if op.content == ast::UnaryOpData::Minus => { - matches!( - a.content, - E::FloatConst(v) if v > 0.0 - ) || matches!(a.content, E::IntConst(v) if v > 0) - || matches!(a.content, E::DoubleConst(v) if v > 0.0) - } - E::FloatConst(v) => *v < 0.0, - _ => false, - } -} - -/// naga's GLSL front end lays every struct out as std140, so an array field -/// of a struct that never reaches a buffer (Photon's ACES spline parameters, -/// `float[6]`) gets a stride of 16, and constructing it from a `float[6](..)` -/// (stride 4) fails validation. Give such fields their natural stride. -fn unpad_private_arrays(module: &mut naga::Module) { - use naga::TypeInner as T; - let mut shared = HashSet::new(); - let mut stack: Vec<_> = module - .global_variables - .iter() - .filter(|(_, v)| { - matches!( - v.space, - naga::AddressSpace::Uniform - | naga::AddressSpace::Storage { .. } - | naga::AddressSpace::Immediate - ) - }) - .map(|(_, v)| v.ty) - .collect(); - while let Some(ty) = stack.pop() { - if !shared.insert(ty) { - continue; - } - match &module.types[ty].inner { - T::Struct { members, .. } => stack.extend(members.iter().map(|m| m.ty)), - T::Array { base, .. } | T::BindingArray { base, .. } => stack.push(*base), - _ => {} - } - } - let mut layouter = naga::proc::Layouter::default(); - if layouter.update(module.to_ctx()).is_err() { - return; - } - let structs: Vec<_> = module - .types - .iter() - .filter(|(h, t)| !shared.contains(h) && matches!(t.inner, T::Struct { .. })) - .map(|(h, _)| h) - .collect(); - for ty in structs { - let mut new = module.types[ty].clone(); - let T::Struct { members, .. } = &mut new.inner else { - continue; - }; - let mut changed = false; - for m in members.iter_mut() { - let T::Array { base, size, stride } = module.types[m.ty].inner else { - continue; - }; - let natural = layouter[base].to_stride(); - if stride != natural { - let span = module.types.get_span(m.ty); - m.ty = module.types.insert( - naga::Type { - name: None, - inner: T::Array { - base, - size, - stride: natural, - }, - }, - span, - ); - changed = true; - } - } - if changed { - module.types.replace(ty, new); - } - } -} - -fn first_lines(s: &str) -> String { - s.lines().take(8).collect::>().join("\n") -} - -/// What a name means inside a function. -#[derive(Clone, Copy)] -enum Local { - Plain, - /// A sampler parameter, split into `{name}_t`, `{name}_s`. - Sampler(&'static str), -} - -/// A struct field `(type_name, field_name, array_suffix)`. -type StructField = (String, String, String); - -struct Legaliser<'a> { - stage: Stage, - link: &'a mut Link, - interface: Interface, - /// Global samplers: name → combined type. - /// Sampler uniforms: their combined type and sampler variable. - samplers: HashMap, - /// Sampler variables declared so far (shared ones once). - sampler_vars: HashSet, - /// User functions → indices of their sampler parameters. - sampler_params: HashMap>, - functions: HashSet, - /// User structs → their fields `(ty, name, array)` (for struct varyings). - structs: HashMap>, - scopes: Vec>, - /// Declarations before the program (bindings, block, inputs, outputs). - header: Vec, - /// Statements run at the start and end of `main`. - prologue: Vec, - epilogue: Vec, - /// Names the program declares at global scope. - declared: HashSet, - /// Identifiers used, for the implicit declarations. - used: HashSet, - errors: Vec, - /// Inside a constant initialiser: no helper calls there. - constant: bool, - /// The program uses `%` ([`modulo_helpers`]), `iris_sqrt` or `iris_mat2`: - /// their helpers go in. - modulo: bool, - nan: bool, - /// `textureGather` was used: [`GATHER`] is added. - gather: bool, - mat2_ctor: bool, - /// Constants turned into globals ([`needs_runtime`]). - runtime_constants: HashSet, - /// Geometry programs: each attribute's type, for the glue. - attribute_types: BTreeMap, -} - -fn var(name: &str) -> ast::Expr { - ast::ExprData::Variable(ast::IdentifierData::from(name).into_node()).into_node() -} - -fn call(name: &str, args: Vec) -> ast::Expr { - ast::ExprData::FunCall( - ast::FunIdentifierData::Expr(Box::new(var(name))).into_node(), - args, - ) - .into_node() -} - -/// `textureGather(sampler2D, uv, comp)`: component `comp` of the four texels -/// bilinear filtering at `uv` would blend, in GL's order, read with -/// `texelFetch` (edges clamped). -const GATHER: &str = "\ -vec4 iris_gather(texture2D t, sampler s, vec2 uv, int c) { - ivec2 hi = textureSize(sampler2D(t, s), 0) - 1; - ivec2 i = ivec2(floor(uv * vec2(hi + 1) - 0.5)); - vec4 a = texelFetch(sampler2D(t, s), clamp(i + ivec2(0, 1), ivec2(0), hi), 0); - vec4 b = texelFetch(sampler2D(t, s), clamp(i + ivec2(1, 1), ivec2(0), hi), 0); - vec4 d = texelFetch(sampler2D(t, s), clamp(i + ivec2(1, 0), ivec2(0), hi), 0); - vec4 e = texelFetch(sampler2D(t, s), clamp(i, ivec2(0), hi), 0); - return vec4(a[c], b[c], d[c], e[c]); -} -"; - -/// GL converts a `bool` given to a numeric constructor (`vec3(r < radius)`, -/// Photon's sun disc); naga refuses. A comparison or logical expression -/// passed to one becomes `(cond ? 1.0 : 0.0)` (or `1`/`1u`). -fn bool_to_number(e: &mut ast::Expr) { - use ast::BinaryOpData as B; - let ast::ExprData::FunCall(f, args) = &mut e.content else { - return; - }; - let Some(name) = callee(f) else { return }; - let base = name.trim_start_matches('d'); - let (one, zero) = if base.starts_with("ivec") || name == "int" { - (ast::ExprData::IntConst(1), ast::ExprData::IntConst(0)) - } else if base.starts_with("uvec") || name == "uint" { - (ast::ExprData::UIntConst(1), ast::ExprData::UIntConst(0)) - } else if base.starts_with("vec") || base.starts_with("mat") || name == "float" { - ( - ast::ExprData::FloatConst(1.0), - ast::ExprData::FloatConst(0.0), - ) - } else { - return; - }; - for a in args.iter_mut() { - let boolean = match &a.content { - ast::ExprData::Binary(op, ..) => matches!( - op.content, - B::Or | B::Xor | B::And | B::Equal | B::NonEqual | B::Lt | B::Gt | B::Lte | B::Gte - ), - ast::ExprData::Unary(op, _) => op.content == ast::UnaryOpData::Not, - _ => false, - }; - if boolean { - let cond = std::mem::replace(a, var("_")); - *a = ast::ExprData::Ternary( - Box::new(cond), - Box::new(one.clone().into_node()), - Box::new(zero.clone().into_node()), - ) - .into_node(); - } - } -} - -/// Outside fragment shaders there are no derivatives, so GL samples level 0 -/// where `texture` and its kin would pick one; naga refuses them there (the -/// vertex programs of Chocapic13', MakeUp and Bliss). Turn them into their -/// `Lod` forms at level 0 (a bias is dropped), also inside `vec4(..)` as -/// legacy shadow lookups are wrapped. -fn explicit_lod(e: &mut ast::Expr) { - let ast::ExprData::FunCall(f, args) = &mut e.content else { - return; - }; - let Some(name) = callee(f) else { return }; - if name == "vec4" && args.len() == 1 { - return explicit_lod(&mut args[0]); - } - // (implicit, explicit, where the level goes, how many arguments without a bias) - let (lod, at, plain) = match name.as_str() { - "texture" => ("textureLod", 2, 2), - "textureProj" => ("textureProjLod", 2, 2), - "textureOffset" => ("textureLodOffset", 2, 3), - "textureProjOffset" => ("textureProjLodOffset", 2, 3), - _ => return, - }; - if args.len() < plain { - return; - } - args.truncate(plain); - let zero = ast::ExprData::FloatConst(0.0).into_node(); - args.insert(at, zero); - *f = ast::FunIdentifierData::Expr(Box::new(var(lod))).into_node(); -} - -/// The name a call is made by: a function, or a constructor's type. -fn callee(f: &ast::FunIdentifier) -> Option { - match &f.content { - ast::FunIdentifierData::Expr(e) => match &e.content { - ast::ExprData::Variable(id) => Some(id.as_str().to_owned()), - _ => None, - }, - ast::FunIdentifierData::TypeSpecifier(t) if t.array_specifier.is_none() => { - Some(type_text(t)) - } - ast::FunIdentifierData::TypeSpecifier(_) => None, - } -} - -fn type_text(t: &ast::TypeSpecifier) -> String { - let mut s = String::new(); - let _ = show_type_specifier(&mut s, t, &mut FormattingState::default()); - s -} - -fn expr_text(e: &ast::Expr) -> String { - let mut s = String::new(); - let _ = show_expr(&mut s, e, &mut FormattingState::default()); - s -} - -fn storage(q: &Option) -> Vec { - q.iter() - .flat_map(|q| &q.qualifiers) - .filter_map(|s| match &s.content { - ast::TypeQualifierSpecData::Storage(s) => Some(s.content.clone()), - _ => None, - }) - .collect() -} - -/// The interpolation and `centroid` words of a declaration. -fn interpolation(q: &Option) -> String { - let mut out = String::new(); - for s in q.iter().flat_map(|q| &q.qualifiers) { - match &s.content { - ast::TypeQualifierSpecData::Interpolation(i) => out.push_str(match i.content { - ast::InterpolationQualifierData::Flat => "flat ", - ast::InterpolationQualifierData::Smooth => "smooth ", - ast::InterpolationQualifierData::NoPerspective => "noperspective ", - }), - ast::TypeQualifierSpecData::Storage(s) - if s.content == ast::StorageQualifierData::Centroid => - { - out.push_str("centroid ") - } - _ => {} - } - } - out -} - -/// `layout(...)` entries of a declaration, as text (`rgba16f`, `location = 0`). -fn layout_ids(q: &Option) -> Vec<(String, Option)> { - q.iter() - .flat_map(|q| &q.qualifiers) - .filter_map(|s| match &s.content { - ast::TypeQualifierSpecData::Layout(l) => Some(l), - _ => None, - }) - .flat_map(|l| &l.ids) - .map(|id| match &id.content { - ast::LayoutQualifierSpecData::Identifier(i, e) => { - (i.as_str().to_owned(), e.as_ref().map(|e| expr_text(e))) - } - ast::LayoutQualifierSpecData::Shared => ("shared".to_owned(), None), - }) - .collect() -} - -/// Array sizes as text (`[3]`), `[]` for unsized ones. -fn array_text(a: &Option) -> String { - a.iter() - .flat_map(|a| &a.dimensions) - .map(|d| match &d.content { - ast::ArraySpecifierDimensionData::Unsized => "[]".to_owned(), - ast::ArraySpecifierDimensionData::ExplicitlySized(e) => format!("[{}]", expr_text(e)), - }) - .collect() -} - -/// Parse one or more `[N]` dimensions (`[9][9]` → `Some(vec![9, 9])`). -fn array_dims(array: &str) -> Option> { - if array.is_empty() { - return Some(Vec::new()); - } - let mut dims = Vec::new(); - let mut rest = array.trim(); - while !rest.is_empty() { - let inner = rest.strip_prefix('[')?; - let (num, tail) = inner.split_once(']')?; - dims.push(num.trim().parse::().ok()?); - rest = tail.trim(); - } - Some(dims) -} - -/// Expand multi-dimensional indices `dims` into `(key_suffix, expr_suffix)` -/// pairs (`[2, 2]` → `("#0#0", "[0][0]")`, `("#0#1", "[0][1]")`, ...). -fn expand_dims(dims: &[usize]) -> Vec<(String, String)> { - let mut out = vec![(String::new(), String::new())]; - for &n in dims { - let mut next = Vec::with_capacity(out.len() * n); - for (k, e) in &out { - for i in 0..n { - next.push((format!("{k}#{i}"), format!("{e}[{i}]"))); - } - } - out = next; - } - out -} - -/// The element count of `T[](a, b, c)`. -fn constructor_len(init: &Option) -> Option { - match &init.as_ref()?.content { - ast::InitializerData::Simple(e) => match &e.content { - ast::ExprData::FunCall(f, args) => match &f.content { - ast::FunIdentifierData::TypeSpecifier(t) if t.array_specifier.is_some() => { - Some(args.len()) - } - _ => None, - }, - _ => None, - }, - ast::InitializerData::List(items) => Some(items.len()), - } -} - -/// Size `[]` in a declaration and in its `T[](...)` initialiser. -fn size_arrays( - len: usize, - decl_array: &mut Option, - ty: &mut ast::TypeSpecifier, - init: &mut Option, -) { - let sized = || { - ast::ArraySpecifierData { - dimensions: vec![ - ast::ArraySpecifierDimensionData::ExplicitlySized(Box::new( - ast::ExprData::IntConst(len as i32).into_node(), - )) - .into_node(), - ], - } - .into_node() - }; - let is_unsized = |a: &Option| { - a.as_ref().is_some_and(|a| { - a.dimensions.len() == 1 - && a.dimensions[0].content == ast::ArraySpecifierDimensionData::Unsized - }) - }; - if is_unsized(decl_array) { - *decl_array = Some(sized()); - } - if is_unsized(&ty.array_specifier) { - ty.array_specifier = Some(sized()); - } - if let Some(ast::InitializerData::Simple(e)) = init.as_mut().map(|i| &mut i.content) { - if let ast::ExprData::FunCall(f, _) = &mut e.content { - if let ast::FunIdentifierData::TypeSpecifier(t) = &mut f.content { - if is_unsized(&t.array_specifier) { - t.array_specifier = Some(sized()); - } - } - } - } -} - -/// Columns of a matrix type (`mat3` → 3 × `vec3`, `mat2x4` → 2 × `vec4`). -fn matrix_columns(ty: &str) -> Option<(usize, String)> { - let rest = ty.strip_prefix("mat").or_else(|| ty.strip_prefix("dmat"))?; - let (cols, rows) = match rest.split_once('x') { - Some((c, r)) => (c.parse().ok()?, r.parse::().ok()?), - None => (rest.parse().ok()?, rest.parse().ok()?), - }; - Some((cols, format!("vec{rows}"))) -} - -fn is_integer(ty: &str) -> bool { - ty.starts_with("int") - || ty.starts_with("uint") - || ty.starts_with("ivec") - || ty.starts_with("uvec") -} - -impl<'a> Legaliser<'a> { - fn new(stage: Stage, link: &'a mut Link) -> Self { - Self { - stage, - link, - interface: Interface::default(), - samplers: HashMap::new(), - sampler_vars: HashSet::new(), - sampler_params: HashMap::new(), - functions: HashSet::new(), - structs: HashMap::new(), - scopes: Vec::new(), - header: Vec::new(), - prologue: Vec::new(), - epilogue: Vec::new(), - declared: HashSet::new(), - used: HashSet::new(), - errors: Vec::new(), - constant: false, - modulo: false, - nan: false, - gather: false, - mat2_ctor: false, - runtime_constants: HashSet::new(), - attribute_types: BTreeMap::new(), - } - } - - fn record_struct(&mut self, spec: &ast::StructSpecifier) { - let Some(name) = &spec.name else { return }; - let mut fields = Vec::new(); - for field in &spec.fields { - let base_ty = type_text(&field.ty); - let ty_arr = array_text(&field.ty.array_specifier); - for id in &field.identifiers { - let arr = format!("{ty_arr}{}", array_text(&id.array_spec)); - fields.push((base_ty.clone(), id.ident.as_str().to_owned(), arr)); - } - } - self.structs.insert(name.as_str().to_owned(), fields); - } - - fn unit(&mut self, unit: &mut ast::TranslationUnit) -> Result<(), String> { - // Functions, their sampler parameters, and struct definitions first. - for ed in &unit.0 { - if let ast::ExternalDeclarationData::Declaration(d) = &ed.content - && let ast::DeclarationData::InitDeclaratorList(list) = &d.content - && let ast::TypeSpecifierNonArrayData::Struct(s) = &list.head.ty.ty.ty.content - { - self.record_struct(s); - } - let proto = match &ed.content { - ast::ExternalDeclarationData::FunctionDefinition(f) => &f.prototype, - ast::ExternalDeclarationData::Declaration(d) => match &d.content { - ast::DeclarationData::FunctionPrototype(p) => p, - _ => continue, - }, - _ => continue, - }; - let name = proto.name.as_str().to_owned(); - for (i, p) in proto.parameters.iter().enumerate() { - let ty = match &p.content { - ast::FunctionParameterDeclarationData::Named(_, d) => &d.ty, - ast::FunctionParameterDeclarationData::Unnamed(_, t) => t, - }; - if opaque(&type_text(ty)).is_some() { - self.sampler_params - .entry(name.clone()) - .or_default() - .insert(i); - } - } - self.functions.insert(name); - } - let mut kept = Vec::with_capacity(unit.0.len()); - for mut ed in std::mem::take(&mut unit.0) { - let keep = match &mut ed.content { - ast::ExternalDeclarationData::Preprocessor(_) => false, - ast::ExternalDeclarationData::FunctionDefinition(f) => { - self.function(f); - true - } - ast::ExternalDeclarationData::Declaration(d) => self.global(d)?, - }; - if keep { - kept.push(ed); - } - } - unit.0 = kept; - if self.errors.is_empty() { - Ok(()) - } else { - Err(self.errors.join("\n")) - } - } - - /// A global declaration; `false` when it moves into the header. - fn global(&mut self, d: &mut ast::Declaration) -> Result { - match &mut d.content { - ast::DeclarationData::FunctionPrototype(p) => { - self.prototype(p); - Ok(true) - } - ast::DeclarationData::Precision(..) => Ok(false), - ast::DeclarationData::Block(b) => { - let binding = self.link.binding(b.name.as_str()); - let buffer = storage(&Some(b.qualifier.clone())) - .contains(&ast::StorageQualifierData::Buffer); - let kind = if buffer { "std430" } else { "std140" }; - b.qualifier - .qualifiers - .retain(|q| !matches!(q.content, ast::TypeQualifierSpecData::Layout(_))); - let mut text = String::new(); - show_declaration(&mut text, d, &mut FormattingState::default()) - .map_err(|e| e.to_string())?; - self.header.push(format!( - "layout({kind}, set = {}, binding = {binding}) {}", - self.link.set, - text.trim() - )); - Ok(false) - } - ast::DeclarationData::Invariant(_) | ast::DeclarationData::TypeOnly(_) => Ok(true), - ast::DeclarationData::InitDeclaratorList(list) => self.global_list(list), - } - } - - fn global_list(&mut self, list: &mut ast::InitDeclaratorList) -> Result { - // If a standalone `struct S { ... };` has no declarator names, keep it - // in the header so varyings of type `S` can declare `S varname;` there. - if list.head.name.is_none() - && list.tail.is_empty() - && matches!( - list.head.ty.ty.ty.content, - ast::TypeSpecifierNonArrayData::Struct(_) - ) - { - let mut text = String::new(); - let decl = ast::DeclarationData::InitDeclaratorList(list.clone()).into_node(); - show_declaration(&mut text, &decl, &mut FormattingState::default()) - .map_err(|e| e.to_string())?; - self.header.push(text.trim().to_owned()); - return Ok(false); - } - let qualifier = list.head.ty.qualifier.clone(); - let storage = storage(&qualifier); - let has = |s: ast::StorageQualifierData| storage.contains(&s); - let ty = type_text(&list.head.ty.ty); - let ty_array = array_text(&list.head.ty.ty.array_specifier); - let mut names: Vec<(String, String)> = Vec::new(); - if let Some(n) = &list.head.name { - names.push(( - n.as_str().to_owned(), - array_text(&list.head.array_specifier), - )); - } - for t in &list.tail { - names.push(( - t.ident.ident.as_str().to_owned(), - array_text(&t.ident.array_spec), - )); - } - self.declared.extend(names.iter().map(|(n, _)| n.clone())); - let input = has(ast::StorageQualifierData::Attribute) - || has(ast::StorageQualifierData::In) && self.stage == Stage::Vertex; - let varying = has(ast::StorageQualifierData::Varying) - || has(ast::StorageQualifierData::Out) && self.stage == Stage::Vertex - || has(ast::StorageQualifierData::In) && self.stage == Stage::Fragment; - if has(ast::StorageQualifierData::Uniform) { - if let Some(&(combined, texture, sampler, compare)) = opaque(&ty) { - for (name, _) in &names { - let t = self.link.binding(&format!("{name}_t")); - self.header.push(format!( - "layout(set = {}, binding = {t}) uniform {texture} {name}_t;", - self.link.set - )); - let shared = &self.link.shared_samplers; - let var = if shared.is_empty() { - format!("{name}_s") - } else if compare { - "iris_compare".to_owned() - } else if combined.starts_with(['i', 'u']) { - "iris_nearest".to_owned() - } else { - shared - .get(name) - .cloned() - .unwrap_or_else(|| format!("{name}_s")) - }; - if self.sampler_vars.insert(var.clone()) { - let s = self.link.binding(&var); - self.header.push(format!( - "layout(set = {}, binding = {s}) uniform {sampler} {var};", - self.link.set - )); - } - self.interface - .sampler_vars - .insert(name.clone(), var.clone()); - self.samplers.insert(name.clone(), (combined, var)); - self.interface - .samplers - .push((name.clone(), combined.to_owned())); - } - } else if ["image", "iimage", "uimage"] - .iter() - .any(|p| ty.starts_with(p)) - { - let mut layout: Vec = Vec::new(); - let ids = layout_ids(&qualifier); - let mut format = ids - .iter() - .find(|(k, v)| v.is_none() && k != "shared") - .map(|(k, _)| k.clone()); - let memory: String = storage - .iter() - .filter_map(|s| match s { - ast::StorageQualifierData::ReadOnly => Some("readonly "), - ast::StorageQualifierData::WriteOnly => Some("writeonly "), - ast::StorageQualifierData::Coherent => Some("coherent "), - ast::StorageQualifierData::Restrict => Some("restrict "), - ast::StorageQualifierData::Volatile => Some("volatile "), - _ => None, - }) - .collect(); - for (name, _) in &names { - let f = format - .take() - .or_else(|| self.link.image_formats.get(name).cloned()) - .unwrap_or_else(|| "rgba8".to_owned()); - let binding = self.link.binding(name); - layout.push(format!( - "layout({f}, set = {}, binding = {binding}) {memory}uniform {ty} {name};", - self.link.set - )); - self.interface.images.push((name.clone(), f)); - } - self.header.extend(layout); - } else { - if ty.starts_with("struct") || ty_array.contains("[]") { - return Err(format!( - "uniform {}: unsupported type {ty}{ty_array}", - names[0].0 - )); - } - for (name, array) in &names { - if ty == "bool" { - self.link - .uniform(name, format!("int iris_bool_{name}{array}"))?; - self.header.push(format!("bool {name}{array};")); - self.prologue - .push(format!("{name} = iris_bool_{name} != 0;")); - } else { - self.link - .uniform(name, format!("{ty}{ty_array} {name}{array}"))?; - } - self.interface.uniforms.insert(name.clone()); - } - } - return Ok(false); - } - if input || varying || has(ast::StorageQualifierData::Out) && self.stage == Stage::Fragment - { - let dir = if input || varying && self.stage == Stage::Fragment { - "in" - } else { - "out" - }; - let interp = interpolation(&qualifier); - let explicit = layout_ids(&qualifier) - .into_iter() - .find(|(k, _)| k == "location") - .and_then(|(_, v)| v?.parse().ok()); - for (i, (name, array)) in names.iter().enumerate() { - let full_array = format!("{ty_array}{array}"); - if input { - let decl = self.attribute(&ty, name, &full_array); - self.header.push(decl); - self.interface.attributes.insert(name.clone()); - } else if !varying { - let at = explicit - .map(|l: u32| l + i as u32) - .unwrap_or_else(|| self.next_output()); - if let (Some(widened), "") = (widen_output(&ty, name), full_array.as_str()) { - // GL fills the missing channels of a narrow output; - // wgpu wants every channel of the target written. - let (wide, fill) = widened; - self.header.push(format!("{ty} {name};")); - self.header.push(format!( - "layout(location = {at}) out {wide} iris_out_{name};" - )); - self.epilogue.push(fill); - } else { - self.header.push(format!( - "layout(location = {at}) out {ty} {name}{full_array};" - )); - } - self.interface.outputs.insert(at, name.clone()); - } else { - self.varying(dir, &interp, &ty, name, &full_array); - } - } - return Ok(false); - } - // A constant naga can't fold becomes a global set at the start of `main`. - if has(ast::StorageQualifierData::Const) && needs_runtime(list, &self.runtime_constants) { - self.runtime_constants - .extend(names.iter().map(|(n, _)| n.clone())); - let q = &mut list.head.ty.qualifier; - if let Some(q) = q { - q.qualifiers.retain(|q| { - !matches!(&q.content, ast::TypeQualifierSpecData::Storage(s) - if s.content == ast::StorageQualifierData::Const) - }); - } - if q.as_ref().is_some_and(|q| q.qualifiers.is_empty()) { - *q = None; - } - } else if has(ast::StorageQualifierData::Const) || has(ast::StorageQualifierData::Shared) { - self.sized(list); - self.constant = true; - self.global_initialisers(list, false); - self.constant = false; - return Ok(true); - } - self.sized(list); - self.global_initialisers(list, true); - Ok(true) - } - - fn next_output(&self) -> u32 { - (0..) - .find(|l| !self.interface.outputs.contains_key(l)) - .unwrap() - } - - /// Emit scalar/vector/matrix varying leaves for an expression `expr_path` - /// with key `key_prefix`. - fn varying_leaf(&mut self, dir: &str, interp: &str, ty: &str, key: &str, expr_path: &str) { - if let Some(fields) = self.structs.get(ty).cloned() { - for (f_ty, f_name, f_arr) in fields { - let sub_key = format!("{key}#{f_name}"); - let sub_expr = format!("{expr_path}.{f_name}"); - self.varying_composite(dir, interp, &f_ty, &sub_key, &sub_expr, &f_arr); - } - return; - } - if let Some((cols, column)) = matrix_columns(ty) { - let flat = if is_integer(&column) && !interp.contains("flat") { - "flat " - } else { - "" - }; - let mut columns = Vec::new(); - for c in 0..cols { - let c_key = format!("{key}#{c}"); - let at = Link::location(&mut self.link.varyings, &c_key); - let part = c_key.replace('#', "_iris"); - self.header.push(format!( - "layout(location = {at}) {flat}{interp}{dir} {column} {part};" - )); - if dir == "out" { - self.epilogue.push(format!("{part} = {expr_path}[{c}];")); - } - columns.push(part); - } - if dir == "in" { - self.prologue - .push(format!("{expr_path} = {ty}({});", columns.join(", "))); - } - return; - } - let flat = if is_integer(ty) && !interp.contains("flat") { - "flat " - } else { - "" - }; - let at = Link::location(&mut self.link.varyings, key); - let part = key.replace('#', "_iris"); - self.header.push(format!( - "layout(location = {at}) {flat}{interp}{dir} {ty} {part};" - )); - if dir == "out" { - self.epilogue.push(format!("{part} = {expr_path};")); - } else { - self.prologue.push(format!("{expr_path} = {part};")); - } - } - - fn varying_composite( - &mut self, - dir: &str, - interp: &str, - ty: &str, - key: &str, - expr_path: &str, - array: &str, - ) { - let Some(dims) = array_dims(array) else { - self.errors - .push(format!("varying {expr_path}: unsupported array `{array}`")); - return; - }; - if dims.is_empty() { - self.varying_leaf(dir, interp, ty, key, expr_path); - } else { - for (k_suf, e_suf) in expand_dims(&dims) { - self.varying_leaf( - dir, - interp, - ty, - &format!("{key}{k_suf}"), - &format!("{expr_path}{e_suf}"), - ); - } - } - } - - /// One varying: matrices, arrays (including multi-dimensional `vec3[9][9]`) - /// and structs (`CloudsParameters`) split into vectors that a private - /// global of the original type is packed from or into. - fn varying(&mut self, dir: &str, interp: &str, ty: &str, name: &str, array: &str) { - if array.is_empty() && matrix_columns(ty).is_none() && !self.structs.contains_key(ty) { - let flat = if is_integer(ty) && !interp.contains("flat") { - "flat " - } else { - "" - }; - let at = Link::location(&mut self.link.varyings, name); - self.header.push(format!( - "layout(location = {at}) {flat}{interp}{dir} {ty} {name};" - )); - return; - } - self.header.push(format!("{ty} {name}{array};")); - self.varying_composite(dir, interp, ty, name, name, array); - } - - fn sized(&mut self, list: &mut ast::InitDeclaratorList) { - let head_ty = list.head.ty.ty.clone(); - let head = &mut list.head.content; - if let Some(len) = constructor_len(&head.initializer) { - size_arrays( - len, - &mut head.array_specifier, - &mut head.ty.content.ty, - &mut head.initializer, - ); - } - for t in &mut list.tail { - let t = &mut t.content; - if let Some(len) = constructor_len(&t.initializer) { - let mut ty = head_ty.clone(); - size_arrays( - len, - &mut t.ident.content.array_spec, - &mut ty, - &mut t.initializer, - ); - } - } - } - - /// Rewrite global initialisers; with `hoist`, move simple ones to the - /// start of `main`. - fn global_initialisers(&mut self, list: &mut ast::InitDeclaratorList, hoist: bool) { - let take = |this: &mut Self, name: &str, init: &mut Option| { - let Some(i) = init else { return }; - match &mut i.content { - ast::InitializerData::Simple(e) => { - this.expr(e); - if hoist { - this.prologue.push(format!("{name} = {};", expr_text(e))); - *init = None; - } - } - ast::InitializerData::List(_) => {} - } - }; - if let Some(n) = list.head.name.clone() { - take(self, n.as_str(), &mut list.head.initializer); - } - for t in &mut list.tail { - let n = t.ident.ident.as_str().to_owned(); - take(self, &n, &mut t.initializer); - } - } - - fn prototype(&mut self, p: &mut ast::FunctionPrototype) -> Vec<(String, Local)> { - if p.name.as_str() == "main" { - p.name = ast::IdentifierData::from("iris_main").into_node(); - } - let mut locals = Vec::new(); - let mut params = Vec::with_capacity(p.parameters.len()); - for mut param in std::mem::take(&mut p.parameters) { - // `const in float x` (Bliss): naga's front end takes no `const` - // on parameters, and it changes nothing for the caller. - match &mut param.content { - ast::FunctionParameterDeclarationData::Named(q, _) - | ast::FunctionParameterDeclarationData::Unnamed(q, _) => { - if let Some(list) = q { - list.qualifiers.retain(|q| { - !matches!(&q.content, ast::TypeQualifierSpecData::Storage(s) - if s.content == ast::StorageQualifierData::Const) - }); - if list.qualifiers.is_empty() { - *q = None; - } - } - } - } - let (ty, name) = match ¶m.content { - ast::FunctionParameterDeclarationData::Named(_, d) => { - (type_text(&d.ty), Some(d.ident.ident.as_str().to_owned())) - } - ast::FunctionParameterDeclarationData::Unnamed(_, t) => (type_text(t), None), - }; - match (opaque(&ty), name) { - (Some(&(combined, texture, sampler, _)), Some(name)) => { - for (t, n) in [ - (texture, format!("{name}_t")), - (sampler, format!("{name}_s")), - ] { - let spec = ast::TypeSpecifierData { - ty: ast::TypeSpecifierNonArrayData::TypeName( - ast::TypeNameData::from(t).into_node(), - ) - .into_node(), - array_specifier: None, - } - .into_node(); - let ident = ast::ArrayedIdentifierData { - ident: ast::IdentifierData::from(n.as_str()).into_node(), - array_spec: None, - } - .into_node(); - let decl = - ast::FunctionParameterDeclaratorData { ty: spec, ident }.into_node(); - params.push( - ast::FunctionParameterDeclarationData::Named(None, decl).into_node(), - ); - } - locals.push((name, Local::Sampler(combined))); - } - (_, name) => { - locals.extend(name.map(|n| (n, Local::Plain))); - params.push(param); - } - } - } - p.parameters = params; - locals - } - - fn function(&mut self, f: &mut ast::FunctionDefinition) { - let locals = self.prototype(&mut f.prototype); - self.scopes.push(locals.into_iter().collect()); - self.statements(&mut f.statement.statement_list); - self.scopes.pop(); - } - - fn declare(&mut self, name: &str) { - if let Some(scope) = self.scopes.last_mut() { - scope.insert(name.to_owned(), Local::Plain); - } - } - - fn local_declaration(&mut self, d: &mut ast::Declaration) { - let ast::DeclarationData::InitDeclaratorList(list) = &mut d.content else { - return; - }; - self.sized(list); - self.constant = - storage(&list.head.ty.qualifier).contains(&ast::StorageQualifierData::Const); - if let Some(i) = &mut list.head.initializer { - self.initializer(i); - } - if let Some(n) = &list.head.name { - let n = n.as_str().to_owned(); - self.declare(&n); - } - for t in &mut list.tail { - if let Some(i) = &mut t.initializer { - self.initializer(i); - } - let n = t.ident.ident.as_str().to_owned(); - self.declare(&n); - } - self.constant = false; - } - - fn initializer(&mut self, i: &mut ast::Initializer) { - match &mut i.content { - ast::InitializerData::Simple(e) => self.expr(e), - ast::InitializerData::List(items) => items.iter_mut().for_each(|i| self.initializer(i)), - } - } - - /// A list of statements, without the dead code after an unconditional - /// `return`, `discard`, `break` or `continue` (up to the next `case`): - /// naga leaves such code unvisited and rejects the function. - fn statements(&mut self, list: &mut Vec) { - let mut dead = false; - list.retain_mut(|s| { - match s.content { - ast::StatementData::CaseLabel(_) => dead = false, - _ if dead => return false, - ast::StatementData::Jump(_) => dead = true, - _ => {} - } - true - }); - list.iter_mut().for_each(|s| self.statement(s)); - } - - fn scoped(&mut self, s: &mut ast::Statement) { - self.scopes.push(HashMap::new()); - self.statement(s); - self.scopes.pop(); - } - - fn statement(&mut self, s: &mut ast::Statement) { - match &mut s.content { - ast::StatementData::Declaration(d) => self.local_declaration(d), - ast::StatementData::Expression(e) => { - if let Some(e) = &mut e.0 { - self.expr(e); - } - } - ast::StatementData::Selection(sel) => { - self.expr(&mut sel.cond); - match &mut sel.rest.content { - ast::SelectionRestStatementData::Statement(a) => self.scoped(a), - ast::SelectionRestStatementData::Else(a, b) => { - self.scoped(a); - self.scoped(b); - } - } - } - ast::StatementData::Switch(sw) => { - self.expr(&mut sw.head); - self.scopes.push(HashMap::new()); - self.statements(&mut sw.body); - self.scopes.pop(); - } - ast::StatementData::CaseLabel(c) => { - if let ast::CaseLabelData::Case(e) = &mut c.content { - self.expr(e); - } - } - ast::StatementData::Iteration(it) => { - self.scopes.push(HashMap::new()); - match &mut it.content { - ast::IterationStatementData::While(c, body) => { - self.condition(c); - self.scoped(body); - } - ast::IterationStatementData::DoWhile(body, e) => { - self.scoped(body); - self.expr(e); - } - ast::IterationStatementData::For(init, rest, body) => { - match &mut init.content { - ast::ForInitStatementData::Expression(Some(e)) => self.expr(e), - ast::ForInitStatementData::Expression(None) => {} - ast::ForInitStatementData::Declaration(d) => self.local_declaration(d), - } - if let Some(c) = &mut rest.condition { - self.condition(c); - } - if let Some(e) = &mut rest.post_expr { - self.expr(e); - } - self.scoped(body); - } - } - self.scopes.pop(); - } - ast::StatementData::Jump(j) => { - if let ast::JumpStatementData::Return(Some(e)) = &mut j.content { - self.expr(e); - } - } - ast::StatementData::Compound(c) => { - self.scopes.push(HashMap::new()); - self.statements(&mut c.statement_list); - self.scopes.pop(); - } - } - } - - fn condition(&mut self, c: &mut ast::Condition) { - match &mut c.content { - ast::ConditionData::Expr(e) => self.expr(e), - ast::ConditionData::Assignment(_, name, init) => { - self.initializer(init); - let n = name.as_str().to_owned(); - self.declare(&n); - } - } - } - - fn local(&self, name: &str) -> Option { - self.scopes.iter().rev().find_map(|s| s.get(name).copied()) - } - - /// What a variable reads, if it must change. - fn variable(&mut self, name: &str) -> Option { - let combined = match self.local(name) { - Some(Local::Sampler(c)) => Some(c), - Some(Local::Plain) => return None, - None => self.samplers.get(name).map(|(c, _)| *c), - }; - if let Some(c) = combined { - let sampler = match self.samplers.get(name) { - Some((_, v)) if self.local(name).is_none() => v.clone(), - _ => format!("{name}_s"), - }; - return Some(call(c, vec![var(&format!("{name}_t")), var(&sampler)])); - } - self.used.insert(name.to_owned()); - if let Some((_, decl)) = LEGACY_UNIFORMS.iter().find(|(n, _)| *n == name) { - let member = decl.split(' ').nth(1).unwrap().split('[').next().unwrap(); - if let Err(e) = self.link.uniform(member, (*decl).to_owned()) { - self.errors.push(e); - } - self.interface.uniforms.insert(member.to_owned()); - return Some(var(member)); - } - let renamed = match (name, self.stage) { - ("gl_FragColor", Stage::Fragment) => return Some(self.frag_data(0)), - ("gl_VertexID", _) => "gl_VertexIndex".to_owned(), - ("gl_InstanceID", _) => "gl_InstanceIndex".to_owned(), - ("gl_FrontColor", Stage::Vertex) | ("gl_Color", Stage::Fragment) => { - self.legacy_varying("iris_FrontColor", "vec4"); - "iris_FrontColor".to_owned() - } - ("gl_FogFragCoord", Stage::Vertex | Stage::Fragment) => { - self.legacy_varying("iris_FogFragCoord", "float"); - "iris_FogFragCoord".to_owned() - } - (n, Stage::Vertex) if LEGACY_ATTRIBUTES.iter().any(|(a, _)| *a == n) => { - let ty = LEGACY_ATTRIBUTES.iter().find(|(a, _)| *a == n).unwrap().1; - let renamed = n.replacen("gl_", "iris_", 1); - if self.interface.attributes.insert(renamed.clone()) { - let decl = self.attribute(ty, &renamed, ""); - self.header.push(decl); - } - renamed - } - _ => return None, - }; - Some(var(&renamed)) - } - - fn legacy_varying(&mut self, name: &str, ty: &str) { - if self.declared.insert(name.to_owned()) { - let dir = if self.stage == Stage::Vertex { - "out" - } else { - "in" - }; - let at = Link::location(&mut self.link.varyings, name); - self.header - .push(format!("layout(location = {at}) {dir} {ty} {name};")); - } - } - - /// A vertex attribute's declaration: an input, or for geometry programs - /// a global the glue fills ([`Geometry`]). - fn attribute(&mut self, ty: &str, name: &str, array: &str) -> String { - match &self.link.geometry { - Some(_) => { - self.attribute_types.insert(name.to_owned(), ty.to_owned()); - format!("{ty} {name}{array};") - } - None => { - let at = Link::location(&mut self.link.attributes, name); - format!("layout(location = {at}) in {ty} {name}{array};") - } - } - } - - fn frag_data(&mut self, n: u32) -> ast::Expr { - let name = format!("iris_FragData{n}"); - if self.interface.outputs.insert(n, name.clone()).is_none() { - self.header - .push(format!("layout(location = {n}) out vec4 {name};")); - } - var(&name) - } - - fn expr(&mut self, e: &mut ast::Expr) { - use ast::ExprData as E; - match &mut e.content { - E::Variable(id) => { - let name = id.as_str().to_owned(); - if let Some(new) = self.variable(&name) { - *e = new; - } - } - E::IntConst(_) - | E::UIntConst(_) - | E::BoolConst(_) - | E::FloatConst(_) - | E::DoubleConst(_) => {} - E::Unary(_, a) | E::PostInc(a) | E::PostDec(a) => self.expr(a), - E::Binary(op, a, b) if op.content == ast::BinaryOpData::Mod && !self.constant => { - self.expr(a); - self.expr(b); - self.modulo = true; - let lhs = std::mem::replace(&mut **a, var("_")); - let rhs = std::mem::replace(&mut **b, var("_")); - *e = call("iris_mod", vec![lhs, rhs]); - } - E::Binary(op, a, b) - if op.content == ast::BinaryOpData::LShift - && matches!(a.content, E::IntConst(_)) - && matches!(b.content, E::IntConst(n) if n >= 31) => - { - self.expr(a); - self.expr(b); - let lhs = std::mem::replace(&mut **a, var("_")); - let rhs = std::mem::replace(&mut **b, var("_")); - let shifted = ast::ExprData::Binary( - ast::BinaryOpData::LShift.into_node(), - Box::new(call("uint", vec![lhs])), - Box::new(call("uint", vec![rhs])), - ) - .into_node(); - *e = call("int", vec![shifted]); - } - E::Assignment(a, op, b) - if op.content == ast::AssignmentOpData::Mod && !self.constant => - { - self.expr(a); - self.expr(b); - self.modulo = true; - let rhs = std::mem::replace(&mut **b, var("_")); - **b = call("iris_mod", vec![(**a).clone(), rhs]); - *op = ast::AssignmentOpData::Equal.into_node(); - } - E::Binary(_, a, b) | E::Assignment(a, _, b) | E::Comma(a, b) => { - self.expr(a); - self.expr(b); - } - E::Ternary(a, b, c) => { - self.expr(a); - self.expr(b); - self.expr(c); - } - E::Bracket(a, i) => { - let array = match &a.content { - E::Variable(id) if self.local(id.as_str()).is_none() => id.as_str().to_owned(), - _ => String::new(), - }; - let index = match i.content { - E::IntConst(n) => Some(n as u32), - E::UIntConst(n) => Some(n), - _ => None, - }; - match (array.as_str(), index) { - ("gl_FragData", Some(n)) if self.stage == Stage::Fragment => { - *e = self.frag_data(n) - } - ("gl_TexCoord", Some(n)) => { - let name = format!("iris_TexCoord{n}"); - self.legacy_varying(&name, "vec4"); - *e = var(&name); - } - ("gl_FragData" | "gl_TexCoord", None) => self.errors.push(format!( - "{array} indexed by a non-constant: {}", - expr_text(e) - )), - _ => { - self.expr(a); - self.expr(i); - } - } - } - E::FunCall(_, args) => { - args.iter_mut().for_each(|a| self.expr(a)); - self.call(e); - } - E::Dot(a, _) => { - self.expr(a); - if self.shadow_lookup(a) { - let inner = std::mem::replace(&mut **a, var("_")); - **a = call("vec4", vec![inner]); - } - } - } - } - - /// Whether `e` is a `texture*` call on a depth-comparing sampler (a float). - fn shadow_lookup(&self, e: &ast::Expr) -> bool { - let ast::ExprData::FunCall(f, args) = &e.content else { - return false; - }; - let Some(name) = callee(f) else { return false }; - const LOOKUPS: &[&str] = &[ - "texture", - "textureLod", - "textureOffset", - "textureLodOffset", - "textureProj", - "textureProjLod", - "textureProjOffset", - "textureGrad", - "textureGradOffset", - ]; - if !LOOKUPS.contains(&name.as_str()) || self.functions.contains(&name) { - return false; - } - let Some(first) = args.first() else { - return false; - }; - match &first.content { - ast::ExprData::FunCall(f, _) => callee(f).and_then(|c| opaque(&c)).is_some_and(|o| o.3), - _ => false, - } - } - - fn call(&mut self, e: &mut ast::Expr) { - let ast::ExprData::FunCall(f, args) = &mut e.content else { - return; - }; - let Some(name) = callee(f) else { return }; - if name == "ftransform" && args.is_empty() && !self.functions.contains(&name) { - let mvp = self.variable("gl_ModelViewProjectionMatrix").unwrap(); - let vertex = self - .variable("gl_Vertex") - .unwrap_or_else(|| var("gl_Vertex")); - *e = ast::ExprData::Binary( - ast::BinaryOpData::Mult.into_node(), - Box::new(mvp), - Box::new(vertex), - ) - .into_node(); - return; - } - if let Some(params) = self.sampler_params.get(&name) { - // sampler2D(a_t, a_s) passed to a split parameter: a_t, a_s. - let mut spliced = Vec::with_capacity(args.len() + params.len()); - for (i, a) in std::mem::take(args).into_iter().enumerate() { - match &a.content { - ast::ExprData::FunCall(g, inner) - if params.contains(&i) - && inner.len() == 2 - && callee(g).is_some_and(|c| opaque(&c).is_some()) => - { - spliced.extend(inner.iter().cloned()) - } - _ => spliced.push(a), - } - } - *args = spliced; - return; - } - if self.functions.contains(&name) { - return; - } - // `mat2(vec4)` (Bloop): naga refuses 1-argument `mat2` from a `vec4`. - if name == "mat2" - && !self.constant - && args.len() == 1 - && !matches!( - args[0].content, - ast::ExprData::FloatConst(_) - | ast::ExprData::IntConst(_) - | ast::ExprData::UIntConst(_) - | ast::ExprData::DoubleConst(_) - ) - { - *f = ast::FunIdentifierData::Expr(Box::new(var("iris_mat2"))).into_node(); - self.mat2_ctor = true; - return; - } - // `sqrt(-1.0)`, Bloop's NaN: naga folds it and fails, drivers give NaN. - if name == "sqrt" && !self.constant && args.len() == 1 && negative_constant(&args[0]) { - *f = ast::FunIdentifierData::Expr(Box::new(var("iris_sqrt"))).into_node(); - self.nan = true; - return; - } - if name == "textureGather" - && (2..=3).contains(&args.len()) - && let ast::ExprData::FunCall(g, inner) = &args[0].content - && inner.len() == 2 - && callee(g).as_deref() == Some("sampler2D") - { - // naga's GLSL front end has no `textureGather`. - let mut call_args = inner.clone(); - call_args.push(args[1].clone()); - call_args.push( - args.get(2) - .cloned() - .unwrap_or_else(|| ast::ExprData::IntConst(0).into_node()), - ); - *e = call("iris_gather", call_args); - self.gather = true; - return; - } - if name == "modf" - && args.len() == 2 - && !matches!(args[1].content, ast::ExprData::Variable(_)) - { - // `modf(x, v.y)` (Photon): WGSL can't point at a vector - // component, so no `out` parameter: `+(v.y = trunc(x), x - trunc(x))` - // (the unary `+` has the comma printed in brackets, also in an - // initialiser). - let (x, whole) = (args[0].clone(), args[1].clone()); - let assign = ast::ExprData::Assignment( - Box::new(whole), - ast::AssignmentOpData::Equal.into_node(), - Box::new(call("trunc", vec![x.clone()])), - ) - .into_node(); - let fraction = ast::ExprData::Binary( - ast::BinaryOpData::Sub.into_node(), - Box::new(x.clone()), - Box::new(call("trunc", vec![x])), - ) - .into_node(); - let comma = ast::ExprData::Comma(Box::new(assign), Box::new(fraction)).into_node(); - *e = ast::ExprData::Unary(ast::UnaryOpData::Add.into_node(), Box::new(comma)) - .into_node(); - return; - } - if let Some((_, new)) = LEGACY_CALLS.iter().find(|(old, _)| *old == name) { - *f = ast::FunIdentifierData::Expr(Box::new(var(new))).into_node(); - } else if let Some((_, new)) = SHADOW_CALLS.iter().find(|(old, _)| *old == name) { - let args = std::mem::take(args); - *e = call("vec4", vec![call(new, args)]); - } - if self.stage != Stage::Fragment { - explicit_lod(e); - } - bool_to_number(e); - } - - /// The legalised source: header, the rewritten program, then a `main` - /// that runs the prologue, the program's own `main` and the epilogue. - fn finish(&mut self, unit: &ast::TranslationUnit) -> Result { - for &(name, ty, attribute) in IRIS_IMPLICIT { - if !self.used.contains(name) || self.declared.contains(name) { - continue; - } - if attribute && self.stage == Stage::Vertex { - let decl = self.attribute(ty, name, ""); - self.header.push(decl); - self.interface.attributes.insert(name.to_owned()); - } else if !attribute { - self.link.uniform(name, format!("{ty} {name}"))?; - self.interface.uniforms.insert(name.to_owned()); - } - } - if !self.errors.is_empty() { - return Err(self.errors.join("\n")); - } - if let Some(geometry) = &self.link.geometry { - match self.stage { - Stage::Vertex => { - // The glue first, then the attributes from it, before - // the global initialisers (which may read them). - let mut first = vec!["hz_vertex();".to_owned()]; - for (name, ty) in &self.attribute_types { - first.push(match geometry.attributes.get(name) { - Some(value) => format!("{name} = {ty}({value});"), - None => format!("{name} = {ty}(0);"), - }); - } - self.prologue.splice(0..0, first); - } - Stage::Fragment => { - if let (Some(reference), Some(name)) = - (geometry.alpha_test, self.interface.outputs.get(&0)) - { - let written = format!("iris_out_{name}"); - let name = if self.declared.contains(&written) - || self - .header - .iter() - .any(|h| h.ends_with(&format!(" {written};"))) - { - written - } else { - name.clone() - }; - self.epilogue - .push(format!("if (!({name}.a > {reference:?})) discard;")); - } - } - _ => {} - } - } - if self.stage == Stage::Vertex { - // GL's clip space to wgpu's: y flipped, so texture row 0 stays - // the bottom of the picture as in GL (texture coordinates and - // `gl_FragCoord` then agree with the pack's), and z from - // [-w, w] to [0, w]. - self.epilogue.push("gl_Position.y = -gl_Position.y;".into()); - self.epilogue - .push("gl_Position.z = (gl_Position.z + gl_Position.w) * 0.5;".into()); - } - let mut out = String::from("#version 450 core\n"); - out.push_str(HELPERS); - if let (Some(geometry), Stage::Vertex) = (&self.link.geometry, self.stage) { - out.push_str(&geometry.vertex_glue); - out.push('\n'); - } - if self.modulo { - out.push_str(&modulo_helpers()); - } - if self.gather { - out.push_str(GATHER); - } - if self.nan { - out.push_str("float iris_sqrt(float x) { return sqrt(x); }\n"); - } - if self.mat2_ctor { - out.push_str(MAT2_HELPERS); - } - if !self.link.uniforms.is_empty() { - out.push_str(&format!( - "layout(std140, set = {}, binding = 0) uniform IrisUniforms {{\n", - self.link.set - )); - for (_, decl) in &self.link.uniforms { - out.push_str(&format!(" {decl};\n")); - } - out.push_str("};\n"); - } - for h in &self.header { - out.push_str(h); - out.push('\n'); - } - let mut state = FormattingState::default(); - for ed in &unit.0 { - show_external_declaration(&mut out, ed, &mut state).map_err(|e| e.to_string())?; - } - out.push_str("\nvoid main() {\n"); - for s in self - .prologue - .iter() - .chain(["iris_main();".to_owned()].iter()) - .chain(&self.epilogue) - { - out.push_str(" "); - out.push_str(s); - out.push('\n'); - } - out.push_str("}\n"); - Ok(out) - } -} diff --git a/crates/horizon-shaderpack/src/uniforms.rs b/crates/horizon-shaderpack/src/uniforms.rs deleted file mode 100644 index 842aeb51f..000000000 --- a/crates/horizon-shaderpack/src/uniforms.rs +++ /dev/null @@ -1,838 +0,0 @@ -//! Custom uniforms: `uniform.. = ` and -//! `variable.. = ` in `shaders.properties`, evaluated -//! once a frame the way Iris does (`CustomUniforms`, the `kroppeb.stareval` -//! parser, `IrisFunctions`, `SmoothFloat`). -//! -//! Values are lists of floats: one for `bool`, `int` and `float` (a bool is 0 -//! or 1), two to four for vectors. Iris types each expression; evaluating in -//! floats gives the same results for what packs write (its only integer -//! operations are `+ - * %`, exact in floats at these sizes; `/` is always a -//! float division). -//! -//! A definition Iris can't resolve (a syntax error, an unknown name or -//! function, a cycle) is left out, and so is every definition using it: the -//! shader's uniform keeps its default, zero. Here a name is unknown when it is -//! neither another definition nor a value the `builtin` lookup gives. - -use std::collections::HashMap; - -/// The custom uniforms and variables of a pack, in evaluation order. -#[derive(Clone, Debug, Default)] -pub struct CustomUniforms { - defs: Vec, - /// Evaluation order (indices into `defs`): dependencies first. - order: Vec, - /// What couldn't be parsed or resolved, for the log. - pub problems: Vec, -} - -#[derive(Clone, Debug)] -struct Def { - name: String, - /// Components of the result: 1, 2, 3 or 4. - size: usize, - /// `bool` (0 or 1) or `int` (truncated) rather than float. - kind: Kind, - /// `uniform.` (given to the shaders) rather than `variable.`. - uniform: bool, - expr: Expr, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -enum Kind { - Bool, - Int, - Float, -} - -#[derive(Clone, Debug, PartialEq)] -enum Expr { - Number(f32), - Name(String), - /// A vector's component (`.x`, `.g`, `.t`, `.2`...). - Access(Box, usize), - Not(Box), - Negate(Box), - Binary(Op, Box, Box), - /// A function; `smooth` calls carry their own state slot. - Call(String, Vec, usize), -} - -#[derive(Clone, Copy, Debug, PartialEq)] -enum Op { - Multiply, - Divide, - Remainder, - Add, - Subtract, - Equals, - NotEquals, - Less, - More, - LessOrEqual, - MoreOrEqual, - And, - Or, -} - -impl Op { - /// `IrisOptions`: lower binds tighter; equal priorities associate left. - fn priority(self) -> u8 { - match self { - Op::Multiply | Op::Divide | Op::Remainder => 0, - Op::Add | Op::Subtract => 1, - Op::Equals - | Op::NotEquals - | Op::Less - | Op::More - | Op::LessOrEqual - | Op::MoreOrEqual => 2, - Op::And | Op::Or => 3, - } - } -} - -/// The per-frame state: `smooth` accumulators, `random`'s generator and the -/// last values. -#[derive(Clone, Debug, Default)] -pub struct UniformState { - smooth: Vec, - seed: u64, - values: HashMap>, -} - -impl UniformState { - /// A uniform's value this frame; `None` for variables and for - /// definitions that couldn't be resolved. - pub fn get(&self, name: &str) -> Option<&[f32]> { - self.values.get(name).map(Vec::as_slice) - } - - /// Every uniform's value this frame, by name. - pub fn values(&self) -> &HashMap> { - &self.values - } -} - -impl CustomUniforms { - /// The definitions in `properties` (key, value), in file order. - pub fn parse<'a>(properties: impl IntoIterator) -> Self { - let mut out = Self::default(); - let mut smooth = 0; - for (key, value) in properties { - let (uniform, rest) = if let Some(rest) = key.strip_prefix("uniform.") { - (true, rest) - } else if let Some(rest) = key.strip_prefix("variable.") { - (false, rest) - } else { - continue; - }; - let Some((ty, name)) = rest.split_once('.') else { - continue; - }; - let (kind, size) = match ty { - "bool" => (Kind::Bool, 1), - "int" => (Kind::Int, 1), - "float" => (Kind::Float, 1), - "vec2" => (Kind::Float, 2), - "vec3" => (Kind::Float, 3), - "vec4" => (Kind::Float, 4), - _ => { - out.problems.push(format!("{key}: unknown type `{ty}`")); - continue; - } - }; - if out.defs.iter().any(|d| d.name == name) { - out.problems.push(format!("{key}: duplicated variable")); - continue; - } - match parse(value, &mut smooth) { - Ok(expr) => out.defs.push(Def { - name: name.to_owned(), - size, - kind, - uniform, - expr, - }), - Err(e) => out.problems.push(format!("{key}: {e} ( = {value})")), - } - } - out.order = out.sort(); - out - } - - /// Whether the pack defines none. - pub fn is_empty(&self) -> bool { - self.defs.is_empty() - } - - /// Dependencies first; a definition in a cycle is left out. - fn sort(&mut self) -> Vec { - let index: HashMap<&str, usize> = self - .defs - .iter() - .enumerate() - .map(|(i, d)| (d.name.as_str(), i)) - .collect(); - // 0 unvisited, 1 in progress, 2 done, 3 broken. - let mut mark = vec![0u8; self.defs.len()]; - let mut order = Vec::new(); - fn visit( - i: usize, - defs: &[Def], - index: &HashMap<&str, usize>, - mark: &mut [u8], - order: &mut Vec, - ) -> bool { - match mark[i] { - 1 | 3 => return false, - 2 => return true, - _ => {} - } - mark[i] = 1; - let mut names = Vec::new(); - defs[i].expr.names(&mut names); - let ok = names.iter().all(|n| match index.get(n.as_str()) { - Some(&j) => visit(j, defs, index, mark, order), - None => true, - }); - mark[i] = if ok { 2 } else { 3 }; - if ok { - order.push(i); - } - ok - } - for i in 0..self.defs.len() { - if !visit(i, &self.defs, &index, &mut mark, &mut order) && mark[i] == 3 { - self.problems - .push(format!("{}: circular reference", self.defs[i].name)); - } - } - order - } - - /// Evaluate every definition for this frame: `builtin` gives Iris's own - /// uniforms, `frame_time` (seconds) drives `smooth`. - pub fn update( - &self, - state: &mut UniformState, - frame_time: f32, - builtin: &dyn Fn(&str) -> Option>, - ) { - let mut values: HashMap<&str, Vec> = HashMap::new(); - let mut broken: Vec<&str> = Vec::new(); - state.values.clear(); - for &i in &self.order { - let def = &self.defs[i]; - let mut eval = Eval { - frame_time, - builtin, - values: &values, - broken: &broken, - smooth: &mut state.smooth, - seed: &mut state.seed, - }; - let Some(mut value) = eval.expr(&def.expr) else { - broken.push(&def.name); - continue; - }; - value.resize(def.size, value.last().copied().unwrap_or(0.0)); - match def.kind { - Kind::Bool => value[0] = f32::from(u8::from(value[0] != 0.0)), - Kind::Int => value[0] = value[0].trunc(), - Kind::Float => {} - } - if def.uniform { - state.values.insert(def.name.clone(), value.clone()); - } - values.insert(&def.name, value); - } - } -} - -impl Expr { - fn names(&self, out: &mut Vec) { - match self { - Expr::Number(_) => {} - Expr::Name(n) => out.push(n.clone()), - Expr::Access(e, _) | Expr::Not(e) | Expr::Negate(e) => e.names(out), - Expr::Binary(_, a, b) => { - a.names(out); - b.names(out); - } - Expr::Call(_, args, _) => args.iter().for_each(|a| a.names(out)), - } - } -} - -struct Eval<'a> { - frame_time: f32, - builtin: &'a dyn Fn(&str) -> Option>, - values: &'a HashMap<&'a str, Vec>, - broken: &'a [&'a str], - smooth: &'a mut Vec, - seed: &'a mut u64, -} - -/// Apply `f` component by component, a scalar standing for every component. -fn zip(args: &[Vec], f: impl Fn(&[f32]) -> f32) -> Vec { - let n = args.iter().map(Vec::len).max().unwrap_or(1); - (0..n) - .map(|i| { - let a: Vec = args - .iter() - .map(|v| { - if v.len() == 1 { - v[0] - } else { - v.get(i).copied().unwrap_or(0.0) - } - }) - .collect(); - f(&a) - }) - .collect() -} - -fn truth(b: bool) -> f32 { - f32::from(u8::from(b)) -} - -impl Eval<'_> { - fn expr(&mut self, e: &Expr) -> Option> { - Some(match e { - Expr::Number(n) => vec![*n], - Expr::Name(n) => self.name(n)?, - Expr::Access(e, i) => vec![*self.expr(e)?.get(*i)?], - Expr::Not(e) => zip(&[self.expr(e)?], |a| truth(a[0] == 0.0)), - Expr::Negate(e) => self.expr(e)?.iter().map(|v| -v).collect(), - Expr::Binary(op, a, b) => { - let a = self.expr(a)?; - let b = self.expr(b)?; - let f = |x: f32, y: f32| match op { - Op::Multiply => x * y, - Op::Divide => x / y, - // Java's `%`, on ints and floats: the sign of x. - Op::Remainder => x % y, - Op::Add => x + y, - Op::Subtract => x - y, - Op::Equals => truth(x == y), - Op::NotEquals => truth(x != y), - Op::Less => truth(x < y), - Op::More => truth(x > y), - Op::LessOrEqual => truth(x <= y), - Op::MoreOrEqual => truth(x >= y), - Op::And => truth(x != 0.0 && y != 0.0), - Op::Or => truth(x != 0.0 || y != 0.0), - }; - zip(&[a, b], |v| f(v[0], v[1])) - } - Expr::Call(name, args, slot) => self.call(name, args, *slot)?, - }) - } - - fn name(&self, n: &str) -> Option> { - if self.broken.contains(&n) { - return None; - } - if let Some(v) = self.values.get(n) { - return Some(v.clone()); - } - match n { - "true" => return Some(vec![1.0]), - "false" => return Some(vec![0.0]), - "pi" => return Some(vec![std::f32::consts::PI]), - _ => {} - } - (self.builtin)(n) - } - - fn random(&mut self) -> f32 { - // xorshift64*, seeded once. - if *self.seed == 0 { - *self.seed = 0x9E37_79B9_7F4A_7C15; - } - let mut x = *self.seed; - x ^= x >> 12; - x ^= x << 25; - x ^= x >> 27; - *self.seed = x; - ((x.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 40) as f32) / (1u64 << 24) as f32 - } - - fn call(&mut self, name: &str, args: &[Expr], slot: usize) -> Option> { - // `if` evaluates only the branch it takes. - if name == "if" { - if args.len() < 3 || args.len() % 2 == 0 { - return None; - } - for pair in args[..args.len() - 1].chunks(2) { - if self.expr(&pair[0])?[0] != 0.0 { - return self.expr(&pair[1]); - } - } - return self.expr(&args[args.len() - 1]); - } - if name == "smooth" { - return self.smooth(args, slot); - } - let v: Vec> = args.iter().map(|a| self.expr(a)).collect::>()?; - let unary = |f: fn(f32) -> f32| { - (v.len() == 1).then(|| v[0].iter().map(|&x| f(x)).collect::>()) - }; - let n = v.len(); - Some(match (name, n) { - ("sin", 1) => unary(f32::sin)?, - ("cos", 1) => unary(f32::cos)?, - ("tan", 1) => unary(f32::tan)?, - ("asin", 1) => unary(f32::asin)?, - ("acos", 1) => unary(f32::acos)?, - ("atan", 1) => unary(f32::atan)?, - ("atan" | "atan2", 2) => zip(&v, |a| a[0].atan2(a[1])), - ("torad" | "radians", 1) => unary(f32::to_radians)?, - ("todeg" | "degrees", 1) => unary(f32::to_degrees)?, - ("pow", 2) => zip(&v, |a| a[0].powf(a[1])), - ("exp", 1) => unary(f32::exp)?, - ("exp2", 1) => unary(f32::exp2)?, - ("exp10", 1) => unary(|x| 10f32.powf(x))?, - ("log", 1) => unary(f32::ln)?, - ("log", 2) => zip(&v, |a| a[1].ln() / a[0].ln()), - ("log2", 1) => unary(f32::log2)?, - ("log10", 1) => unary(f32::log10)?, - ("sqrt", 1) => unary(f32::sqrt)?, - ("abs", 1) => unary(f32::abs)?, - // Math.signum: 0 stays 0. - ("sign" | "signum", 1) => unary(|x| if x == 0.0 { x } else { x.signum() })?, - ("floor", 1) => unary(f32::floor)?, - ("ceil", 1) => unary(f32::ceil)?, - ("frac", 1) => unary(|x| x - x.floor())?, - // Math.round: half up. - ("round", 1) => unary(|x| (x + 0.5).floor())?, - ("min", 2..) => zip(&v, |a| a.iter().copied().fold(f32::INFINITY, f32::min)), - ("max", 2..) => zip(&v, |a| a.iter().copied().fold(f32::NEG_INFINITY, f32::max)), - ("clamp", 3) => zip(&v, |a| a[0].max(a[1]).min(a[2])), - ("mix", 3) => zip(&v, |a| a[0] + (a[1] - a[0]) * a[2]), - ("edge", 2) => zip(&v, |a| truth(a[1] >= a[0])), - ("fmod", 2) => zip(&v, |a| (a[0] % a[1] + a[1]) % a[1]), - ("between", 3) => vec![truth(v[0][0] >= v[1][0] && v[0][0] <= v[2][0])], - ("equals", 3) => vec![truth((v[0][0] - v[1][0]).abs() <= v[2][0])], - ("in", 2..) => vec![truth(v[1..].iter().any(|x| x[0] == v[0][0]))], - ("random", 0) => vec![self.random()], - ("random", 2) => { - let r = self.random(); - vec![v[0][0] + r * (v[1][0] - v[0][0])] - } - ("vec2" | "ivec2" | "bvec2", 2) - | ("vec3" | "ivec3" | "bvec3", 3) - | ("vec4" | "ivec4" | "bvec4", 4) => { - let mut out: Vec = v.iter().map(|a| a[0]).collect(); - if name.starts_with('i') { - out.iter_mut().for_each(|x| *x = x.trunc()); - } else if name.starts_with('b') { - out.iter_mut().for_each(|x| *x = truth(*x != 0.0)); - } - out - } - _ => return None, - }) - } - - /// `smooth([id,] value[, fadeTime[, fadeDownTime]])`: the half-lives, in - /// tenths of a second, default to 1. A constant first argument of two or - /// three is OptiFine's id; with four it always is. - fn smooth(&mut self, args: &[Expr], slot: usize) -> Option> { - let id = matches!(args.first(), Some(Expr::Number(_))) && args.len() >= 2; - let args = if id || args.len() == 4 { - &args[1..] - } else { - args - }; - let value = self.expr(args.first()?)?[0]; - let up = match args.get(1) { - Some(e) => self.expr(e)?[0], - None => 1.0, - }; - let down = match args.get(2) { - Some(e) => self.expr(e)?[0], - None => up, - }; - if args.len() > 3 { - return None; - } - if self.smooth.len() <= slot { - self.smooth.resize(slot + 1, SmoothFloat::default()); - } - Some(vec![self.smooth[slot].update( - value, - up, - down, - self.frame_time, - )]) - } -} - -/// Iris's `SmoothFloat`: exponential smoothing with separate half-lives up -/// and down, in tenths of a second; the first value is taken as is. -#[derive(Clone, Copy, Debug, Default)] -pub struct SmoothFloat { - value: Option, -} - -impl SmoothFloat { - pub fn update(&mut self, target: f32, half_life_up: f32, half_life_down: f32, dt: f32) -> f32 { - let Some(acc) = self.value else { - self.value = Some(target); - return target; - }; - let half_life = if target > acc { - half_life_up - } else { - half_life_down - }; - let next = if half_life == 0.0 { - target - } else { - let decay = std::f32::consts::LN_2 / (half_life * 0.1); - let alpha = 1.0 - (-decay * dt).exp(); - (1.0 - alpha) * acc + alpha * target - }; - self.value = Some(next); - next - } -} - -/// Parse one expression; `smooth` counts the `smooth` calls so far (each -/// keeps its own state). -fn parse(text: &str, smooth: &mut usize) -> Result { - let tokens = tokenize(text)?; - let mut p = Parser { - tokens, - at: 0, - smooth, - }; - let e = p.binary(3)?; - match p.tokens.get(p.at) { - None => Ok(e), - Some(t) => Err(format!("unexpected {t:?}")), - } -} - -#[derive(Clone, Debug, PartialEq)] -enum Token { - Number(f32), - Id(String), - /// `.` followed by an accessor. - Access(String), - Op(String), - Open, - Close, - Comma, -} - -fn tokenize(text: &str) -> Result, String> { - let chars: Vec = text.chars().collect(); - let mut out = Vec::new(); - let mut i = 0; - let read_while = |i: &mut usize, f: &dyn Fn(char) -> bool| { - let start = *i; - while *i < chars.len() && f(chars[*i]) { - *i += 1; - } - chars[start..*i].iter().collect::() - }; - while i < chars.len() { - let c = chars[i]; - if c.is_whitespace() { - i += 1; - } else if c.is_ascii_alphabetic() || c == '_' { - out.push(Token::Id(read_while(&mut i, &|c| { - c.is_ascii_alphanumeric() || c == '_' - }))); - } else if c == '.' - && matches!( - out.last(), - Some(Token::Id(_) | Token::Close | Token::Access(_)) - ) - { - i += 1; - while i < chars.len() && chars[i].is_whitespace() { - i += 1; - } - let access = read_while(&mut i, &|c| c.is_ascii_alphanumeric() || c == '_'); - if access.is_empty() { - return Err("an expression can't end with `.`".into()); - } - out.push(Token::Access(access)); - } else if c.is_ascii_digit() || c == '.' { - let s = read_while(&mut i, &|c| c.is_ascii_alphanumeric() || c == '.'); - let n = s - .parse::() - .or_else(|_| s.trim_end_matches(['f', 'F']).parse::()) - .map_err(|_| format!("bad number `{s}`"))?; - out.push(Token::Number(n)); - } else if c == '(' { - out.push(Token::Open); - i += 1; - } else if c == ')' { - out.push(Token::Close); - i += 1; - } else if c == ',' { - out.push(Token::Comma); - i += 1; - } else { - let two: String = chars[i..(i + 2).min(chars.len())].iter().collect(); - let op = ["==", "!=", "<=", ">=", "&&", "||"] - .into_iter() - .find(|o| two == *o) - .map(str::to_owned) - .or_else(|| "*/%+-<>!≠≤≥".contains(c).then(|| c.to_string())) - .ok_or_else(|| format!("unexpected character `{c}`"))?; - i += op.chars().count(); - out.push(Token::Op(op)); - } - } - Ok(out) -} - -struct Parser<'a> { - tokens: Vec, - at: usize, - smooth: &'a mut usize, -} - -impl Parser<'_> { - fn next(&mut self) -> Option { - let t = self.tokens.get(self.at).cloned(); - self.at += 1; - t - } - - fn peek_op(&self) -> Option { - let Some(Token::Op(o)) = self.tokens.get(self.at) else { - return None; - }; - Some(match o.as_str() { - "*" => Op::Multiply, - "/" => Op::Divide, - "%" => Op::Remainder, - "+" => Op::Add, - "-" => Op::Subtract, - "==" => Op::Equals, - "!=" | "≠" => Op::NotEquals, - "<" => Op::Less, - ">" => Op::More, - "<=" | "≤" => Op::LessOrEqual, - ">=" | "≥" => Op::MoreOrEqual, - "&&" => Op::And, - "||" => Op::Or, - _ => return None, - }) - } - - /// Operators of priority `max` or tighter, left to right. - fn binary(&mut self, max: u8) -> Result { - let mut left = self.unary()?; - while let Some(op) = self.peek_op() { - let p = op.priority(); - if p > max { - break; - } - self.at += 1; - // The right side takes only tighter operators: equal priorities - // associate to the left. - let right = if p == 0 { - self.unary()? - } else { - self.binary(p - 1)? - }; - left = Expr::Binary(op, Box::new(left), Box::new(right)); - } - Ok(left) - } - - /// Prefix `!` and `-`, which bind tighter than any binary operator. - fn unary(&mut self) -> Result { - match self.tokens.get(self.at) { - Some(Token::Op(o)) if o == "!" => { - self.at += 1; - Ok(Expr::Not(Box::new(self.unary()?))) - } - Some(Token::Op(o)) if o == "-" => { - self.at += 1; - Ok(Expr::Negate(Box::new(self.unary()?))) - } - _ => self.postfix(), - } - } - - fn postfix(&mut self) -> Result { - let mut e = match self.next() { - Some(Token::Number(n)) => Expr::Number(n), - Some(Token::Id(id)) if self.tokens.get(self.at) == Some(&Token::Open) => { - self.at += 1; - let mut args = Vec::new(); - if self.tokens.get(self.at) == Some(&Token::Close) { - self.at += 1; - } else { - loop { - args.push(self.binary(3)?); - match self.next() { - Some(Token::Comma) => {} - Some(Token::Close) => break, - t => return Err(format!("expected `,` or `)`, found {t:?}")), - } - } - } - let slot = *self.smooth; - if id == "smooth" { - *self.smooth += 1; - } - Expr::Call(id, args, slot) - } - Some(Token::Id(id)) => Expr::Name(id), - Some(Token::Open) => { - let e = self.binary(3)?; - match self.next() { - Some(Token::Close) => e, - t => return Err(format!("expected `)`, found {t:?}")), - } - } - t => return Err(format!("expected a value, found {t:?}")), - }; - while let Some(Token::Access(a)) = self.tokens.get(self.at) { - let i = match a.as_str() { - "x" | "r" | "s" | "0" => 0, - "y" | "g" | "t" | "1" => 1, - "z" | "b" | "p" | "2" => 2, - "w" | "a" | "q" | "3" => 3, - _ => return Err(format!("unknown accessor `.{a}`")), - }; - self.at += 1; - e = Expr::Access(Box::new(e), i); - } - Ok(e) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn builtins(name: &str) -> Option> { - Some(match name { - "frameCounter" => vec![7.0], - "eyeBrightness" => vec![32.0, 120.0], - "isEyeInWater" => vec![0.0], - "eyeAltitude" => vec![40.0], - "cameraPosition" => vec![1.0, 64.5, -3.0], - "rainStrength" => vec![0.0], - _ => return None, - }) - } - - fn eval(text: &str) -> Option> { - let u = CustomUniforms::parse([("uniform.float.x", text)]); - let mut state = UniformState::default(); - u.update(&mut state, 0.05, &builtins); - state.get("x").map(<[f32]>::to_vec) - } - - #[test] - fn operators_follow_iris_priorities() { - assert_eq!(eval("1 + 2 * 3"), Some(vec![7.0])); - assert_eq!(eval("10 - 4 - 3"), Some(vec![3.0])); - assert_eq!(eval("2 * 3 % 4"), Some(vec![2.0])); - assert_eq!(eval("-2 * 3"), Some(vec![-6.0])); - assert_eq!(eval("1 < 2 && 3 > 4 || 1"), Some(vec![1.0])); - assert_eq!(eval("!(1 == 1)"), Some(vec![0.0])); - assert_eq!(eval("frameCounter % 2"), Some(vec![1.0])); - assert_eq!(eval("7 / 2"), Some(vec![3.5])); - assert_eq!(eval("-7 % 4"), Some(vec![-3.0])); - assert_eq!(eval("eyeBrightness.y / 240.0"), Some(vec![0.5])); - assert_eq!(eval("cameraPosition.y - 0.5"), Some(vec![64.0])); - assert_eq!(eval("2.5f * 2"), Some(vec![5.0])); - } - - #[test] - fn functions() { - assert_eq!(eval("if(eyeBrightness.y > 239.0, 1, 0)"), Some(vec![0.0])); - assert_eq!(eval("if(0, 1, 1 == 1, 2, 3)"), Some(vec![2.0])); - assert_eq!( - eval("clamp(5, 0, 1) + max(1, 4, 2) + min(3, -1)"), - Some(vec![4.0]) - ); - assert_eq!(eval("fmod(-1, 4)"), Some(vec![3.0])); - assert_eq!(eval("in(3, 1, 2, 3)"), Some(vec![1.0])); - assert_eq!( - eval("between(2, 1, 3) && equals(1.0, 1.05, 0.1)"), - Some(vec![1.0]) - ); - assert_eq!(eval("frac(2.25) + floor(-0.5) + abs(-2)"), Some(vec![1.25])); - assert_eq!(eval("vec2(1, 2).y"), Some(vec![2.0])); - // An unknown name or function leaves the uniform out (zero). - assert_eq!(eval("biome_precipitation + 1"), None); - assert_eq!(eval("nope(1)"), None); - assert_eq!(eval("1 +"), None); - } - - #[test] - fn variables_resolve_in_dependency_order() { - let u = CustomUniforms::parse([ - ("uniform.float.b", "a * 2"), - ("variable.float.a", "frameCounter + 1"), - ("uniform.vec2.v", "vec2(a, b)"), - ("uniform.bool.flag", "b > 10"), - ("uniform.float.loop1", "loop2"), - ("uniform.float.loop2", "loop1"), - ("uniform.float.bad", "unknown"), - ("uniform.float.uses_bad", "bad + 1"), - ]); - let mut state = UniformState::default(); - u.update(&mut state, 0.05, &builtins); - assert_eq!(state.get("b"), Some(&[16.0][..])); - assert_eq!(state.get("a"), None, "variables aren't uniforms"); - assert_eq!(state.get("v"), Some(&[8.0, 16.0][..])); - assert_eq!(state.get("flag"), Some(&[1.0][..])); - assert_eq!(state.get("loop1"), None); - assert_eq!(state.get("bad"), None); - assert_eq!(state.get("uses_bad"), None); - assert!(u.problems.iter().any(|p| p.contains("circular"))); - } - - #[test] - fn smooth_is_iris_exponential_smoothing() { - // Complementary's `eyeBrightnessM` and `isEyeInCave`. - let u = CustomUniforms::parse([ - ( - "uniform.float.m", - "smooth(4, eyeBrightness.y / 240.0, 5, 5)", - ), - ( - "uniform.float.cave", - "if(isEyeInWater == 0, 1.0 - smooth(202, if(eyeAltitude < 5.0, eyeBrightness.y / 240.0, 1.0), 6, 12), 0.0)", - ), - ("uniform.float.once", "smooth(frameCounter)"), - ]); - assert!(u.problems.is_empty(), "{:?}", u.problems); - let mut state = UniformState::default(); - u.update(&mut state, 0.05, &builtins); - // The first value isn't smoothed. - assert_eq!(state.get("m"), Some(&[0.5][..])); - assert_eq!(state.get("cave"), Some(&[0.0][..])); - // Half a second at a half-life of 5 tenths: halfway to the target. - let dark = |name: &str| -> Option> { - match name { - "eyeBrightness" => Some(vec![0.0, 0.0]), - _ => builtins(name), - } - }; - u.update(&mut state, 0.5, &dark); - let m = state.get("m").unwrap()[0]; - assert!((m - 0.25).abs() < 1e-5, "{m}"); - let mut s = SmoothFloat::default(); - s.update(0.0, 0.0, 10.0, 0.1); - assert_eq!(s.update(1.0, 0.0, 10.0, 0.1), 1.0, "a half-life of 0 jumps"); - let down = s.update(0.0, 0.0, 10.0, 1.0); - assert!((down - 0.5).abs() < 1e-5, "{down}"); - } -} diff --git a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/composite.fsh b/crates/horizon-shaderpack/tests/fixtures/mini/shaders/composite.fsh deleted file mode 100644 index 948ee25c6..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/composite.fsh +++ /dev/null @@ -1,10 +0,0 @@ -#version 120 -#extension GL_ARB_shader_texture_lod : enable -#include "/lib/common.glsl" -uniform sampler2D colortex0; -varying vec2 texcoord; -void main() { -#if MC_VERSION >= 260300 && defined IS_IRIS - gl_FragData[0] = texture2D(colortex0, texcoord) * float(MC_RENDER_STAGE_TERRAIN_SOLID); -#endif -} diff --git a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/composite.vsh b/crates/horizon-shaderpack/tests/fixtures/mini/shaders/composite.vsh deleted file mode 100644 index eb261aadd..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/composite.vsh +++ /dev/null @@ -1,3 +0,0 @@ -#version 120 -varying vec2 texcoord; -void main() { gl_Position = ftransform(); texcoord = gl_MultiTexCoord0.xy; } diff --git a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/lib/common.glsl b/crates/horizon-shaderpack/tests/fixtures/mini/shaders/lib/common.glsl deleted file mode 100644 index bb88b82c3..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/lib/common.glsl +++ /dev/null @@ -1,7 +0,0 @@ -#include "settings.glsl" -#ifdef BLOOM -float bloomStrength = 0.5 * QUALITY; -#endif -#ifdef DOF -float dofStrength = 1.0; -#endif diff --git a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/lib/settings.glsl b/crates/horizon-shaderpack/tests/fixtures/mini/shaders/lib/settings.glsl deleted file mode 100644 index 0d5b4b1c9..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/lib/settings.glsl +++ /dev/null @@ -1,7 +0,0 @@ -// Options, as packs write them. -#define BLOOM // Bloom on bright light -//#define DOF // Depth of field -//#define UNREFERENCED -#define QUALITY 2 // Quality [1 2 3] -#define NOT_AN_OPTION 3 -const float sunPathRotation = -40.0; // [-40.0 0.0 40.0] diff --git a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/notes.txt b/crates/horizon-shaderpack/tests/fixtures/mini/shaders/notes.txt deleted file mode 100644 index 528b86abe..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/notes.txt +++ /dev/null @@ -1 +0,0 @@ -Not a program. diff --git a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/world-1/composite.fsh b/crates/horizon-shaderpack/tests/fixtures/mini/shaders/world-1/composite.fsh deleted file mode 100644 index 7d2f794da..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/mini/shaders/world-1/composite.fsh +++ /dev/null @@ -1,4 +0,0 @@ -#version 120 -#include "../lib/common.glsl" -#include "/lib/missing.glsl" -void main() {} diff --git a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite.fsh b/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite.fsh deleted file mode 100644 index c6304265e..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite.fsh +++ /dev/null @@ -1,17 +0,0 @@ -#version 120 -uniform sampler2D colortex3; -varying vec2 texcoord; -#ifdef BLOOM -/* -const int colortex1Format = R8; -*/ -#endif -const bool colortex3MipmapEnabled = true; // read with mipmaps -void main() { -#if 0 - /* DRAWBUFFERS:12 */ -#endif - /* RENDERTARGETS: 0,1 */ - gl_FragData[0] = texture2D(colortex3, texcoord); - gl_FragData[1] = vec4(1.0); -} diff --git a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite.vsh b/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite.vsh deleted file mode 100644 index eb261aadd..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite.vsh +++ /dev/null @@ -1,3 +0,0 @@ -#version 120 -varying vec2 texcoord; -void main() { gl_Position = ftransform(); texcoord = gl_MultiTexCoord0.xy; } diff --git a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite1.fsh b/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite1.fsh deleted file mode 100644 index 783c79b52..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite1.fsh +++ /dev/null @@ -1,7 +0,0 @@ -#version 120 -uniform sampler2D colortex0; -varying vec2 texcoord; -void main() { - /* DRAWBUFFERS:0 */ - gl_FragData[0] = texture2D(colortex0, texcoord); -} diff --git a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite1.vsh b/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite1.vsh deleted file mode 100644 index eb261aadd..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite1.vsh +++ /dev/null @@ -1,3 +0,0 @@ -#version 120 -varying vec2 texcoord; -void main() { gl_Position = ftransform(); texcoord = gl_MultiTexCoord0.xy; } diff --git a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite2.fsh b/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite2.fsh deleted file mode 100644 index d105112b9..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite2.fsh +++ /dev/null @@ -1,5 +0,0 @@ -#version 120 -void main() { - /* DRAWBUFFERS:7 */ - gl_FragData[0] = vec4(1.0); -} diff --git a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite2.vsh b/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite2.vsh deleted file mode 100644 index eb261aadd..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/composite2.vsh +++ /dev/null @@ -1,3 +0,0 @@ -#version 120 -varying vec2 texcoord; -void main() { gl_Position = ftransform(); texcoord = gl_MultiTexCoord0.xy; } diff --git a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/deferred.fsh b/crates/horizon-shaderpack/tests/fixtures/passes/shaders/deferred.fsh deleted file mode 100644 index 6a24357c6..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/deferred.fsh +++ /dev/null @@ -1,11 +0,0 @@ -#version 120 -varying vec2 texcoord; -/* -const int colortex1Format = RGBA16F; -const bool colortex2Clear = false; -const vec4 colortex3ClearColor = vec4(1.0, 0.0, 0.0, 1.0); -*/ -void main() { - /* DRAWBUFFERS:3 */ - gl_FragData[0] = vec4(texcoord, 0.0, 1.0); -} diff --git a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/deferred.vsh b/crates/horizon-shaderpack/tests/fixtures/passes/shaders/deferred.vsh deleted file mode 100644 index eb261aadd..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/deferred.vsh +++ /dev/null @@ -1,3 +0,0 @@ -#version 120 -varying vec2 texcoord; -void main() { gl_Position = ftransform(); texcoord = gl_MultiTexCoord0.xy; } diff --git a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/final.fsh b/crates/horizon-shaderpack/tests/fixtures/passes/shaders/final.fsh deleted file mode 100644 index 65c6b0195..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/final.fsh +++ /dev/null @@ -1,6 +0,0 @@ -#version 120 -uniform sampler2D colortex0; -varying vec2 texcoord; -void main() { - gl_FragColor = texture2D(colortex0, texcoord); -} diff --git a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/final.vsh b/crates/horizon-shaderpack/tests/fixtures/passes/shaders/final.vsh deleted file mode 100644 index eb261aadd..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/final.vsh +++ /dev/null @@ -1,3 +0,0 @@ -#version 120 -varying vec2 texcoord; -void main() { gl_Position = ftransform(); texcoord = gl_MultiTexCoord0.xy; } diff --git a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/shaders.properties b/crates/horizon-shaderpack/tests/fixtures/passes/shaders/shaders.properties deleted file mode 100644 index 0a9996c4c..000000000 --- a/crates/horizon-shaderpack/tests/fixtures/passes/shaders/shaders.properties +++ /dev/null @@ -1,6 +0,0 @@ -# Full-screen passes: a smaller composite1 that also flips colortex2, -# composite2 off, colortex3 at half size. -scale.composite1 = 0.5 -flip.composite1.colortex2 = true -program.composite2.enabled = false -size.buffer.colortex3 = 0.5 0.5 diff --git a/crates/horizon-shaderpack/tests/pack.rs b/crates/horizon-shaderpack/tests/pack.rs deleted file mode 100644 index 1e2ee8158..000000000 --- a/crates/horizon-shaderpack/tests/pack.rs +++ /dev/null @@ -1,505 +0,0 @@ -//! The preprocessing half of the Java shader pack translation on a small -//! hand-written pack (`tests/fixtures/mini`), and on real packs when -//! `HORIZON_TEST_SHADERPACKS` names a folder of them (development only: -//! third-party packs are never committed). - -use horizon_shaderpack::{ - JavaPack, Stage, - pipeline::{TargetSettings, TargetSize}, - preprocess::Environment, -}; -use std::{ - collections::BTreeMap, - io::Write, - path::{Path, PathBuf}, -}; - -fn mini() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mini") -} - -fn env() -> Environment { - Environment::iris("Apple", "Apple M4 Pro", 4) -} - -fn saved(pairs: &[(&str, &str)]) -> BTreeMap { - pairs - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect() -} - -#[test] -fn finds_programs_and_options() { - let pack = JavaPack::open(&mini()).unwrap(); - assert_eq!( - pack.programs, - ["composite.fsh", "composite.vsh", "world-1/composite.fsh"] - ); - assert_eq!(Stage::of(&pack.programs[1]), Some(Stage::Vertex)); - let bools: Vec<(&str, bool)> = pack - .options - .bools - .values() - .map(|o| (o.name.as_str(), o.default)) - .collect(); - assert_eq!( - bools, - [("BLOOM", true), ("DOF", false)], - "UNREFERENCED has no #ifdef" - ); - let strings: Vec<(&str, &str)> = pack - .options - .strings - .values() - .map(|o| (o.name.as_str(), o.default.as_str())) - .collect(); - assert_eq!(strings, [("QUALITY", "2"), ("sunPathRotation", "-40.0")]); - assert_eq!( - pack.options.strings["QUALITY"].comment.as_deref(), - Some("Quality") - ); -} - -#[test] -fn preprocesses_with_defaults_and_with_values() { - let pack = JavaPack::open(&mini()).unwrap(); - let defaults = pack - .preprocess("composite.fsh", &pack.values(&BTreeMap::new()), &env()) - .unwrap(); - assert_eq!(defaults.version, ["120"]); - assert_eq!( - defaults.extensions, - [["GL_ARB_shader_texture_lod", ":", "enable"]] - ); - let text = defaults - .text - .split_whitespace() - .collect::>() - .join(" "); - assert!(text.contains("float bloomStrength = 0.5 * 2 ;"), "{text}"); - assert!(!text.contains("dofStrength"), "{text}"); - assert!( - text.contains("float ( 8 )"), - "Iris's render stage macros: {text}" - ); - assert!(!text.contains('#'), "no directive is left: {text}"); - - let values = pack.values(&saved(&[ - ("BLOOM", "false"), - ("DOF", "true"), - ("QUALITY", "3"), - ])); - let changed = pack - .preprocess("composite.fsh", &values, &env()) - .unwrap() - .text; - assert!( - !changed.contains("bloomStrength") && changed.contains("dofStrength"), - "{changed}" - ); - - let mut source = pack.source("composite.fsh", &values).unwrap(); - assert!( - source - .text - .contains("#define QUALITY 3 // OptionAnnotatedSource: Changed option") - ); - // Output lines are the inlined lines: errors can name file:line. - let line = defaults - .text - .lines() - .position(|l| l.contains("bloomStrength")) - .unwrap() as u32 - + 1; - assert_eq!(source.locate(line), "lib/common.glsl:3"); - source.text = "#if\n".into(); - assert!( - horizon_shaderpack::preprocess::preprocess(&source, &env()) - .unwrap_err() - .starts_with("composite.fsh:1:") - ); -} - -/// JCPP lets a `#define` replace a macro (Bloop's `TILT_ANGLE`); the added -/// `#undef`s don't move the lines errors name. -#[test] -fn a_define_replaces_an_earlier_one() { - use horizon_shaderpack::{include::Inlined, preprocess::preprocess}; - let text = "#define TILT 5.0\nfloat a = TILT;\n # define TILT 1.5 // radians\nfloat b = TILT;\n#define F(x) x\n#define F(x) (x * 2)\nfloat c = F(1);\n#if\n"; - let source = Inlined { - text: text.into(), - origin: (1..=8).map(|l| ("lib/specular.glsl".into(), l)).collect(), - }; - let error = preprocess(&source, &env()).unwrap_err(); - assert!(error.starts_with("lib/specular.glsl:8:"), "{error}"); - let source = Inlined { - text: text.replace("#if\n", ""), - ..source - }; - let out = preprocess(&source, &env()).unwrap().text; - let lines: Vec<&str> = out.lines().collect(); - assert_eq!(lines[1], "float a = 5.0 ;"); - assert_eq!(lines[3], "float b = 1.5 ;"); - assert_eq!(lines[6], "float c = ( 1 * 2 ) ;"); -} - -/// Conditions as JCPP reads them: an empty macro counts as 0 (Bloop's -/// `#if MC_GL_VENDOR_INTEL || MC_GL_VENDOR_OTHER` with Iris's empty vendor -/// macro, its `#define TAA_USE_YCOCG`), a float as its integer part; a -/// quoted `#error` lexes, and an `#if` may run to the end. -#[test] -fn conditions_read_as_jcpp_does() { - use horizon_shaderpack::{include::Inlined, preprocess::preprocess}; - let mut env = env(); - env.define("MC_GL_VENDOR_OTHER", ""); - let text = "#define TAA_USE_YCOCG -#define LEVEL 0.5 -#if MC_GL_VENDOR_INTEL || MC_GL_VENDOR_OTHER -float gather = 0.0; -#else -float gather = 1.0; -#endif -#if TAA_USE_YCOCG -float ycocg = 1.0; -#endif -#if defined TAA_USE_YCOCG && defined(MC_GL_VENDOR_OTHER) && LEVEL == 0 -float both = 1.0; -#endif -#ifndef TAA_USE_YCOCG -#error \"This program should be disabled\" -#endif -#ifdef TAA_USE_YCOCG -float open = 1.0; -"; - let source = Inlined { - text: text.into(), - origin: Vec::new(), - }; - let out = preprocess(&source, &env).unwrap().text; - let words: Vec<&str> = out.split_whitespace().collect(); - assert_eq!( - words.join(" "), - "float gather = 1.0 ; float both = 1.0 ; float open = 1.0 ;" - ); -} - -#[test] -fn reports_missing_includes_and_cycles() { - let pack = JavaPack::open(&mini()).unwrap(); - let err = pack - .source("world-1/composite.fsh", &Default::default()) - .err() - .unwrap(); - assert_eq!( - err, - "world-1/composite.fsh:3: #include \"lib/missing.glsl\": file not found" - ); - - let dir = scratch("cycle"); - std::fs::create_dir_all(dir.join("shaders/lib")).unwrap(); - std::fs::write( - dir.join("shaders/final.fsh"), - "#include \"/lib/a.glsl\"\nvoid main() {}\n", - ) - .unwrap(); - std::fs::write(dir.join("shaders/lib/a.glsl"), "#include \"b.glsl\"\n").unwrap(); - std::fs::write(dir.join("shaders/lib/b.glsl"), "#include \"a.glsl\"\n").unwrap(); - let pack = JavaPack::open(&dir).unwrap(); - let err = pack.source("final.fsh", &Default::default()).err().unwrap(); - assert_eq!( - err, - "#include cycle: final.fsh -> lib/a.glsl -> lib/b.glsl -> lib/a.glsl" - ); - std::fs::remove_dir_all(dir).unwrap(); -} - -#[test] -fn opens_zips_with_shaders_in_a_subfolder() { - let dir = scratch("zip"); - std::fs::create_dir_all(&dir).unwrap(); - let zip_path = dir.join("Mini-main.zip"); - let mut zip = zip::ZipWriter::new(std::fs::File::create(&zip_path).unwrap()); - let options = zip::write::SimpleFileOptions::default(); - let root = mini().join("shaders"); - for rel in [ - "composite.fsh", - "composite.vsh", - "lib/common.glsl", - "lib/settings.glsl", - "world-1/composite.fsh", - ] { - zip.start_file(format!("Mini-main/shaders/{rel}"), options) - .unwrap(); - zip.write_all(&std::fs::read(root.join(rel)).unwrap()) - .unwrap(); - } - zip.start_file("Mini-main/docs/shaders/readme.txt", options) - .unwrap(); - zip.finish().unwrap(); - let zipped = JavaPack::open(&zip_path).unwrap(); - let folder = JavaPack::open(&mini()).unwrap(); - assert_eq!(zipped.programs, folder.programs); - let values = zipped.values(&BTreeMap::new()); - assert_eq!( - zipped - .preprocess("composite.fsh", &values, &env()) - .unwrap() - .text, - folder - .preprocess("composite.fsh", &values, &env()) - .unwrap() - .text - ); - std::fs::remove_dir_all(dir).unwrap(); -} - -#[test] -fn plans_the_full_screen_passes() { - let pack = JavaPack::open(&Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/passes")) - .unwrap(); - let values = pack.values(&BTreeMap::new()); - let properties = pack.properties(&values, &env()).unwrap(); - let passes = pack.passes("", &values, &env(), &properties).unwrap(); - - // Settings from the active comments only: colortex1's R8 is #ifdef'd out. - assert_eq!(passes.targets[1].format, "RGBA16F"); - assert!(!passes.targets[2].clear); - assert_eq!(passes.targets[3].clear_color, Some([1.0, 0.0, 0.0, 1.0])); - assert_eq!(passes.targets[3].size, Some(TargetSize::Relative(0.5, 0.5))); - assert_eq!(passes.targets[4], TargetSettings::default()); - - let summary = |p: &horizon_shaderpack::pipeline::Pass| { - ( - p.program.clone(), - p.draw_buffers.clone(), - p.reads_alt.iter().copied().collect::>(), - ) - }; - assert_eq!( - passes.deferred.iter().map(summary).collect::>(), - [("deferred".into(), Some(vec![3]), vec![])] - ); - // composite2 is off; RENDERTARGETS in `#if 0` doesn't count; each pass - // reads the side its predecessors wrote, and composite1 flips colortex2. - assert_eq!( - passes.composite.iter().map(summary).collect::>(), - [ - ("composite".into(), Some(vec![0, 1]), vec![3]), - ("composite1".into(), Some(vec![0]), vec![0, 1, 3]), - ] - ); - assert_eq!(passes.composite[0].mipmapped, [3].into()); - assert_eq!(passes.composite[1].scale, 0.5); - assert_eq!(passes.flipped_after_prepare, [].into()); - // No gbuffers programs: the world goes into colortex0. - assert_eq!(passes.world_target, 0); - assert_eq!(passes.flipped_after_translucent, [3].into()); - assert_eq!(passes.flipped_at_end, [1, 2, 3].into()); - let final_pass = passes.final_pass.unwrap(); - assert_eq!( - (final_pass.program.as_str(), final_pass.draw_buffers), - ("final", None) - ); - assert_eq!(final_pass.reads_alt, [1, 2, 3].into()); -} - -/// The world enters the target terrain draws to first (MakeUp's colortex1), -/// through Iris's fallbacks when `gbuffers_terrain` is missing or disabled. -#[test] -fn the_world_goes_where_terrain_draws() { - let dir = scratch("world-target"); - let shaders = dir.join("shaders"); - std::fs::create_dir_all(&shaders).unwrap(); - let write = |name: &str, text: &str| std::fs::write(shaders.join(name), text).unwrap(); - for program in ["composite", "gbuffers_terrain", "gbuffers_textured"] { - write(&format!("{program}.vsh"), "void main() {}\n"); - } - write("composite.fsh", "void main() {}\n"); - write( - "gbuffers_textured.fsh", - "void main() {\n/* DRAWBUFFERS:12 */\n gl_FragData[0] = vec4(1.0);\n}\n", - ); - write( - "gbuffers_terrain.fsh", - "void main() {\n#ifdef GRASS\n/* DRAWBUFFERS:42 */\n#else\n/* DRAWBUFFERS:3 */\n#endif\n gl_FragData[0] = vec4(1.0);\n}\n", - ); - let target = |properties: &str| { - write("shaders.properties", properties); - let pack = JavaPack::open(&dir).unwrap(); - let values = pack.values(&BTreeMap::new()); - let properties = pack.properties(&values, &env()).unwrap(); - pack.passes("", &values, &env(), &properties) - .unwrap() - .world_target - }; - assert_eq!(target(""), 3); - assert_eq!(target("program.gbuffers_terrain.enabled = false\n"), 1); - std::fs::remove_dir_all(dir).unwrap(); -} - -fn scratch(name: &str) -> PathBuf { - std::env::temp_dir().join(format!("horizon-shaderpack-{name}-{}", std::process::id())) -} - -/// Every program of the packs in `HORIZON_TEST_SHADERPACKS` (folders or -/// zips) preprocesses. Skipped when the variable isn't set. -#[test] -fn real_packs_preprocess() { - let Some(dir) = std::env::var_os("HORIZON_TEST_SHADERPACKS") else { - return; - }; - let mut failures = Vec::new(); - for entry in std::fs::read_dir(dir).unwrap().flatten() { - let Ok(pack) = JavaPack::open(&entry.path()) else { - continue; - }; - let values = pack.values(&BTreeMap::new()); - let mut ok = 0; - for program in &pack.programs { - match pack.preprocess(program, &values, &env()) { - Ok(_) => ok += 1, - Err(e) => failures.push(format!("{}: {program}: {e}", entry.path().display())), - } - } - println!( - "{}: {ok}/{} programs, {} boolean and {} value options", - entry.file_name().to_string_lossy(), - pack.programs.len(), - pack.options.bools.len(), - pack.options.strings.len() - ); - } - assert!(failures.is_empty(), "{}", failures.join("\n")); -} - -/// The `shaders.properties` of the packs in `HORIZON_TEST_SHADERPACKS` read -/// with their defaults. Skipped when the variable isn't set. -#[test] -fn real_packs_properties() { - let Some(dir) = std::env::var_os("HORIZON_TEST_SHADERPACKS") else { - return; - }; - for entry in std::fs::read_dir(dir).unwrap().flatten() { - let Ok(pack) = JavaPack::open(&entry.path()) else { - continue; - }; - let values = pack.values(&BTreeMap::new()); - let properties = pack.properties(&values, &env()).unwrap(); - let profiles = properties.profiles(&pack.options).unwrap(); - let current = horizon_shaderpack::properties::ShaderProperties::current_profile( - &profiles, - &pack.options, - &values, - ); - let disabled: std::collections::BTreeSet<&str> = pack - .programs - .iter() - .map(|p| horizon_shaderpack::program_name(p)) - .filter(|p| !properties.program_enabled(p, &pack.options, &values)) - .collect(); - let main = properties.screen(None).unwrap(); - println!( - "{}: {} main screen elements, {} sub-screens, {} sliders, profiles {:?} (current {:?}), off: {}", - entry.file_name().to_string_lossy(), - main.len(), - properties.screens().len(), - properties.sliders().len(), - profiles.iter().map(|p| p.name.as_str()).collect::>(), - current.map(|p| p.name.as_str()), - disabled.into_iter().collect::>().join(" ") - ); - // Screens name the pack's options (a few stale names are tolerated: - // Iris shows them as empty cells). - let mut unknown = Vec::new(); - for screen in std::iter::once(None).chain(properties.screens().into_iter().map(Some)) { - for e in properties.screen(screen).unwrap() { - if let horizon_shaderpack::properties::MenuElement::Option(o) = e { - if !pack.options.bools.contains_key(&o) - && !pack.options.strings.contains_key(&o) - { - unknown.push(o); - } - } - } - } - println!(" unknown screen options: {unknown:?}"); - assert!(unknown.len() <= 2, "{unknown:?}"); - // Every custom uniform parses. - let uniforms = properties.custom_uniforms(); - println!( - " custom uniforms: {:?}, sky {:?}", - uniforms.problems, - properties.sky() - ); - assert!(uniforms.problems.is_empty(), "{:?}", uniforms.problems); - } -} - -/// The overworld's full-screen passes for the packs in -/// `HORIZON_TEST_SHADERPACKS`. Skipped when the variable isn't set. -#[test] -fn real_packs_passes() { - let Some(dir) = std::env::var_os("HORIZON_TEST_SHADERPACKS") else { - return; - }; - for entry in std::fs::read_dir(dir).unwrap().flatten() { - let Ok(pack) = JavaPack::open(&entry.path()) else { - continue; - }; - let values = pack.values(&BTreeMap::new()); - let properties = pack.properties(&values, &env()).unwrap(); - let folder = if pack.programs.iter().any(|p| p.starts_with("world0/")) { - "world0" - } else { - "" - }; - let passes = pack.passes(folder, &values, &env(), &properties).unwrap(); - println!("{}:", entry.file_name().to_string_lossy()); - for (i, t) in passes.targets.iter().enumerate() { - if *t != TargetSettings::default() { - println!(" colortex{i}: {t:?}"); - } - } - for p in passes - .begin - .iter() - .chain(&passes.prepare) - .chain(&passes.deferred) - .chain(&passes.composite) - .chain(&passes.final_pass) - { - println!( - " {} writes {:?} reads alt {:?} mipmaps {:?} scale {}", - p.program, p.draw_buffers, p.reads_alt, p.mipmapped, p.scale - ); - } - assert!(passes.final_pass.is_some() || !passes.composite.is_empty()); - // Every planned pass translates, its outputs within its targets. - for p in passes - .begin - .iter() - .chain(&passes.prepare) - .chain(&passes.deferred) - .chain(&passes.composite) - .chain(&passes.final_pass) - { - let program = pack - .compile(p, &passes.targets, &values, &env(), &properties) - .unwrap(); - assert!( - program - .fragment - .interface - .outputs - .keys() - .all(|&l| (l as usize) < program.draw_buffers.len()) - || p.program.ends_with("final"), - "{}: outputs {:?}, targets {:?}", - p.program, - program.fragment.interface.outputs, - program.draw_buffers - ); - } - } -} diff --git a/crates/horizon-shaderpack/tests/translate.rs b/crates/horizon-shaderpack/tests/translate.rs deleted file mode 100644 index 251bb7fb5..000000000 --- a/crates/horizon-shaderpack/tests/translate.rs +++ /dev/null @@ -1,702 +0,0 @@ -//! The GLSL → WGSL translation on hand-written compatibility-profile -//! programs, checked through naga's IR (locations, bindings, uniform block), -//! and on real packs when `HORIZON_TEST_SHADERPACKS` names a folder of them. - -use horizon_shaderpack::{ - JavaPack, Stage, - include::Inlined, - preprocess::{Environment, preprocess}, - program_name, - translate::{Geometry, Link, Translated, translate}, -}; -use std::collections::{BTreeMap, BTreeSet}; - -fn env() -> Environment { - Environment::iris("Apple", "Apple M4 Pro", 4) -} - -fn run(stage: Stage, source: &str, link: &mut Link) -> Result { - let inlined = Inlined { - text: source.to_owned(), - origin: Vec::new(), - }; - let pre = preprocess(&inlined, &env())?; - translate(stage, &pre.text, link, &|line| format!("line {line}")) -} - -/// `(name, location)` of an entry point's inputs (`result: false`) or outputs. -fn io(t: &Translated, result: bool) -> BTreeMap { - let f = &t.module.entry_points[0].function; - let mut out = BTreeMap::new(); - let mut add = - |name: Option<&String>, binding: &Option, ty: naga::Handle| { - match (binding, &t.module.types[ty].inner) { - (Some(naga::Binding::Location { location, .. }), _) => { - out.insert(name.cloned().unwrap_or_default(), *location); - } - (None, naga::TypeInner::Struct { members, .. }) => { - for m in members { - if let Some(naga::Binding::Location { location, .. }) = &m.binding { - out.insert(m.name.clone().unwrap_or_default(), *location); - } - } - } - _ => {} - } - }; - if result { - if let Some(r) = &f.result { - add(None, &r.binding, r.ty); - } - } else { - for a in &f.arguments { - add(a.name.as_ref(), &a.binding, a.ty); - } - } - out -} - -/// Group 0 bindings of the module's resources, by variable name. -fn bindings(t: &Translated) -> BTreeMap { - t.module - .global_variables - .iter() - .filter_map(|(_, v)| Some((v.name.clone()?, v.binding.as_ref()?.binding))) - .collect() -} - -/// The uniform block's member names, in order. -fn block_members(t: &Translated) -> Vec { - t.module - .global_variables - .iter() - .find(|(_, v)| v.space == naga::AddressSpace::Uniform) - .map(|(_, v)| match &t.module.types[v.ty].inner { - naga::TypeInner::Struct { members, .. } => { - members.iter().filter_map(|m| m.name.clone()).collect() - } - _ => Vec::new(), - }) - .unwrap_or_default() -} - -const VSH: &str = r#" -#version 120 -attribute vec4 mc_Entity; -uniform mat4 gbufferModelView; -uniform bool hasSkylight; -varying vec2 texcoord; -varying vec4 color; -varying mat3 tbn; -varying float entity; -void main() { - gl_Position = ftransform(); - texcoord = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy; - color = gl_Color; - gl_FrontColor = gl_Color; - tbn = mat3(gbufferModelView) * gl_NormalMatrix; - entity = hasSkylight ? mc_Entity.x : 0.0; -} -"#; - -const FSH: &str = r#" -#version 120 -uniform sampler2D texture; -uniform sampler2DShadow shadowtex0; -uniform float frameTimeCounter; -varying vec2 texcoord; -varying vec4 color; -varying mat3 tbn; -float phase = frameTimeCounter * 2.0; -const float weights[] = float[](0.25, 0.5, 0.25); -vec4 sampleTwice(sampler2D tex, vec2 uv) { - return texture2D(tex, uv) + texture2D(tex, uv + vec2(phase)); -} -float shade(vec3 p) { - float texture = shadow2D(shadowtex0, p).x; - return texture * weights[1]; -} -void main() { - vec4 albedo = sampleTwice(texture, texcoord) * color; - if (isnan(albedo.r)) { - discard; - albedo = vec4(0.0); - } - gl_FragData[0] = albedo * shade(tbn[2]) * gl_Color; - gl_FragData[1] = vec4(texture(shadowtex0, vec3(texcoord, 0.5)).x); -} -"#; - -#[test] -fn translates_a_compatibility_profile_pass() { - let mut link = Link::default(); - let vs = run(Stage::Vertex, VSH, &mut link).unwrap(); - let fs = run(Stage::Fragment, FSH, &mut link).unwrap(); - - // The varyings meet at the same locations, the mat3 as three columns. - let outs = io(&vs, true); - let ins = io(&fs, false); - for name in [ - "texcoord", - "color", - "tbn_iris0", - "tbn_iris1", - "tbn_iris2", - "iris_FrontColor", - ] { - assert_eq!( - outs.get(name), - ins.get(name), - "{name}: vs {outs:?} fs {ins:?}" - ); - assert!(outs.contains_key(name), "{name} in {outs:?}"); - } - assert!(outs.contains_key("entity") && !ins.contains_key("entity")); - // Attributes: the pack's, then the compatibility ones it reads. - let attributes: Vec = io(&vs, false).into_keys().collect(); - assert_eq!( - attributes, - [ - "iris_Color", - "iris_MultiTexCoord0", - "iris_Vertex", - "mc_Entity" - ] - ); - // gl_FragData[n] are the outputs at location n. - assert_eq!( - fs.interface.outputs.keys().copied().collect::>(), - [0, 1] - ); - assert_eq!( - io(&fs, true).values().copied().collect::>(), - [0, 1].into() - ); - - // One block whose members extend across the pass: the fragment program's - // starts with the vertex program's, so offsets agree. - let vs_block = block_members(&vs); - let fs_block = block_members(&fs); - assert_eq!( - vs_block, - [ - "gbufferModelView", - "iris_bool_hasSkylight", - "iris_ModelViewProjectionMatrix", - "iris_TextureMatrix", - "iris_NormalMatrix" - ] - ); - assert_eq!(fs_block[..vs_block.len()], vs_block[..]); - assert_eq!(fs_block[vs_block.len()..], ["frameTimeCounter"]); - - // Samplers split into texture + sampler bindings, shared by the pass. - let b = bindings(&fs); - let block = fs - .module - .global_variables - .iter() - .find(|(_, v)| v.space == naga::AddressSpace::Uniform) - .unwrap() - .1; - assert_eq!( - block.binding.as_ref().map(|b| (b.group, b.binding)), - Some((0, 0)) - ); - for name in ["texture_t", "texture_s", "shadowtex0_t", "shadowtex0_s"] { - assert_eq!(b[name], link.bindings[name], "{name}"); - } - assert_eq!( - fs.interface.samplers, - [ - ("texture".into(), "sampler2D".into()), - ("shadowtex0".into(), "sampler2DShadow".into()) - ] - ); - // A local named `texture` hides the sampler; the sampler parameter is split. - assert!( - fs.glsl.contains( - "float texture = vec4(texture(sampler2DShadow(shadowtex0_t, shadowtex0_s), p)).x;" - ), - "{}", - fs.glsl - ); - assert!( - fs.glsl - .contains("vec4 sampleTwice(texture2D tex_t, sampler tex_s, vec2 uv)"), - "{}", - fs.glsl - ); - assert!( - fs.glsl - .contains("sampleTwice(texture_t, texture_s, texcoord)"), - "{}", - fs.glsl - ); - // Global initialisers run first; the unsized array is sized. - assert!( - fs.glsl.contains("phase = frameTimeCounter * 2.;"), - "{}", - fs.glsl - ); - assert!( - fs.glsl - .contains("const float weights[3] = float[3](0.25, 0.5, 0.25);"), - "{}", - fs.glsl - ); - // The code after `discard` is gone, isnan is a helper. - assert!(!fs.glsl.contains("albedo = vec4(0.);"), "{}", fs.glsl); - assert!(fs.glsl.contains("iris_isnan(albedo.r)"), "{}", fs.glsl); - assert!(fs.wgsl.contains("@fragment")); - // GL's clip space becomes wgpu's after the program's own `main`. - assert!( - vs.glsl - .contains("gl_Position.y = -gl_Position.y;\n gl_Position.z = (gl_Position.z + gl_Position.w) * 0.5;\n}"), - "{}", - vs.glsl - ); -} - -#[test] -fn reports_errors_with_their_place() { - let mut link = Link::default(); - let err = run( - Stage::Fragment, - "void main() {\n float x = ;\n}\n", - &mut link, - ) - .err() - .unwrap(); - assert!(err.starts_with("line 2:"), "{err}"); - let err = run(Stage::Geometry, "void main() {}", &mut link) - .err() - .unwrap(); - assert_eq!(err, "Geometry programs are not supported"); - let mut link = Link::default(); - run( - Stage::Vertex, - "uniform float a;\nvoid main() { gl_Position = vec4(a); }", - &mut link, - ) - .unwrap(); - let err = run( - Stage::Fragment, - "uniform int a;\nvoid main() { gl_FragColor = vec4(a); }", - &mut link, - ) - .err() - .unwrap(); - assert_eq!(err, "uniform a is declared as both `float a` and `int a`"); -} - -/// What naga refuses and drivers accept: `vec % scalar` (Noble), `sqrt` of -/// a negative constant (Bloop's NaN), constants naga can't fold (Bliss's -/// `?:`, `mix`), 16-bit types (Noble on NVIDIA), `mat2(vec4)` (Bloop), -/// `1 << 31` (Rethinking Voxels), vertex `texture2D` without LOD (Bliss), -/// nested array varyings (Noble), and struct varyings (Photon). -#[test] -fn legalises_what_naga_refuses() { - let mut link = Link::default(); - let vs = run( - Stage::Vertex, - "uniform sampler2D colortex0; -struct CloudsParameters { - vec3 color; - float density; -}; -varying CloudsParameters clouds; -varying vec3 grid[2][2]; -void main() { - vec4 s = texture2D(colortex0, vec2(0.5)); - clouds.color = s.rgb; - clouds.density = s.a; - grid[0][0] = s.rgb; - grid[0][1] = s.gbr; - grid[1][0] = s.brg; - grid[1][1] = vec3(1.0); - gl_Position = vec4(0.0, 0.0, 0.0, 1.0); -} -", - &mut link, - ) - .unwrap(); - reparse(&vs.wgsl).unwrap(); - - let fs = run( - Stage::Fragment, - "uniform vec2 uv; -struct CloudsParameters { - vec3 color; - float density; -}; -varying CloudsParameters clouds; -varying vec3 grid[2][2]; -const int RES = 256 % 100; -const int SIGN_BIT = 1 << 31; -const vec3 TARGET = length(vec3(1.0)) < 0.001 ? vec3(0.0) : normalize(vec3(1.0)); -const vec3 TINT = mix(TARGET, vec3(1.0), 0.5); -const vec3 HALF = TINT * 0.5; -u16vec2 pack(uint v) { return u16vec2(v & 65535u, v >> 16u); } -float nan() { return sqrt(-1.0); } -void main() { - ivec2 texel = ivec2(uv) % RES; - texel %= 7; - uint packed = packFloat2x16(f16vec2(HALF.xy)); - mat2 rot = mat2(vec4(1.0, 0.0, 0.0, 1.0)); - vec2 r = rot * vec2(clouds.density, float(SIGN_BIT != 0)); - gl_FragColor = vec4(vec2(texel) + r + clouds.color.xy + grid[1][0].xy, float(pack(packed).x), nan()); -} -", - &mut link, - ) - .unwrap(); - for wanted in [ - "const int RES = 256 % 100;", - "vec3 TARGET;", - "vec3 TINT;", - "vec3 HALF;", - "ivec2 texel = iris_mod(ivec2(uv), RES);", - "texel = iris_mod(texel, 7);", - "uvec2 pack(uint v)", - "return iris_sqrt(-1.);", - "uint packed = packHalf2x16(vec2(HALF.xy));", - "iris_mat2(", - ] { - assert!(fs.glsl.contains(wanted), "{wanted}\n{}", fs.glsl); - } - reparse(&fs.wgsl).unwrap(); -} - -/// What Photon, Chocapic13', MakeUp Ultra Fast and Bliss write and GL takes: -/// lookups without a level in a vertex shader, a struct passed between -/// stages, `textureGather`, a `bool` given to `vec3(..)`, `const in` -/// parameters and an array field in a struct built by its constructor. -#[test] -fn legalises_what_photon_and_chocapic_write() { - let mut link = Link::default(); - let header = "uniform sampler2D colortex0; -uniform sampler2DShadow shadowtex0; -struct Clouds { float coverage; vec2 wind; mat3 turn; int layers; }; -"; - let vs = run( - Stage::Vertex, - &format!( - "{header}flat varying Clouds clouds; -void main() {{ - clouds.coverage = texture2D(colortex0, vec2(0.5)).r + shadow2D(shadowtex0, vec3(0.5)).r; - clouds.wind = textureOffset(colortex0, vec2(0.5), ivec2(1, 0), 2.0).rg; - clouds.turn = mat3(1.0); - clouds.layers = 2; - gl_Position = ftransform(); -}} -" - ), - &mut link, - ) - .unwrap(); - for wanted in [ - "textureLod(sampler2D(colortex0_t", - "vec2(0.5), 0.)", - "textureLodOffset(sampler2D(colortex0_t", - "clouds_iriscoverage = clouds.coverage;", - "clouds_iristurn_iris1 = clouds.turn[1];", - ] { - assert!(vs.glsl.contains(wanted), "{wanted}\n{}", vs.glsl); - } - reparse(&vs.wgsl).unwrap(); - let fs = run( - Stage::Fragment, - &format!( - "{header}flat varying Clouds clouds; -struct Spline {{ float slope; float[3] coeffs; }}; -float edge(const in float x, const float r) {{ return float(x < r); }} -void main() {{ - const Spline spline = Spline(0.0, float[3](1.0, 2.0, 3.0)); - vec4 four = textureGather(colortex0, vec2(0.5), 1); - vec3 disc = vec3(clouds.coverage < 0.5); - vec2 cell = vec2(0.0); - float part = modf(clouds.coverage * 4.0, cell.y); - gl_FragColor = vec4(disc * edge(four.x, spline.coeffs[1]), clouds.wind.x + float(clouds.layers) + part + cell.y); -}} -" - ), - &mut link, - ) - .unwrap(); - for wanted in [ - "iris_gather(colortex0_t", - "clouds.coverage = clouds_iriscoverage;", - "float edge(in float x, float r)", - "cell.y = trunc(", - ] { - assert!(fs.glsl.contains(wanted), "{wanted}\n{}", fs.glsl); - } - reparse(&fs.wgsl).unwrap(); - // The vertex program's outputs are the fragment program's inputs. - assert_eq!( - io(&vs, true).get("clouds_iriswind"), - io(&fs, false).get("clouds_iriswind") - ); -} - -/// A `gbuffers_*` program over Horizon's geometry: its attributes are -/// globals the glue fills before `main`, its bindings in group 2, and its -/// fragment discards below Iris's alpha test. A `case` ending in `return` -/// (Photon's displacement) isn't a fall-through for naga. -#[test] -fn geometry_programs_take_their_attributes_from_the_glue() { - let glue = Geometry { - vertex_glue: "layout(location = 0) in vec3 hz_position; -layout(std430, set = 3, binding = 0) readonly buffer HzTable { int hz_table[]; }; -vec4 hz_pos; -void hz_vertex() { hz_pos = vec4(hz_position, 1.0) + float(hz_table[0]); }" - .into(), - attributes: [ - ("iris_Vertex".to_owned(), "hz_pos".to_owned()), - ( - "mc_Entity".to_owned(), - "vec4(8.0, -1.0, 0.0, 0.0)".to_owned(), - ), - ] - .into(), - alpha_test: Some(0.1), - }; - let mut link = Link { - set: 2, - geometry: Some(glue), - ..Link::default() - }; - let vs = run( - Stage::Vertex, - "uniform sampler2D noisetex; -attribute vec4 mc_Entity; -attribute vec2 mc_midTexCoord; -varying vec2 uv; -vec3 sway(vec3 p, int id) { - switch (id) { - case 8: - return p + 1.0; - case 9: - case 10: - return p - 1.0; - default: - return p; - } -} -void main() { - uv = gl_MultiTexCoord0.xy + mc_midTexCoord; - gl_Position = gl_ModelViewProjectionMatrix * vec4(sway(gl_Vertex.xyz, int(mc_Entity.x)), 1.0); -} -", - &mut link, - ) - .unwrap(); - for wanted in [ - "hz_vertex();", - "mc_Entity = vec4(vec4(8.0, -1.0, 0.0, 0.0));", - "iris_Vertex = vec4(hz_pos);", - "mc_midTexCoord = vec2(0);", - "set = 2, binding = 0) uniform IrisUniforms", - ] { - assert!(vs.glsl.contains(wanted), "{wanted}\n{}", vs.glsl); - } - // Horizon's vertex input is the only one. - assert_eq!( - io(&vs, false).into_keys().collect::>(), - ["hz_position"] - ); - reparse(&vs.wgsl).unwrap(); - let fs = run( - Stage::Fragment, - "uniform sampler2D gtexture; -varying vec2 uv; -void main() { - /* DRAWBUFFERS:02 */ - gl_FragData[0] = texture2D(gtexture, uv); - gl_FragData[1] = vec4(1.0); -} -", - &mut link, - ) - .unwrap(); - assert!( - fs.glsl.contains("if (!(iris_FragData0.a > 0.1)) discard;"), - "{}", - fs.glsl - ); - assert!(fs.glsl.contains("set = 2, binding = "), "{}", fs.glsl); - reparse(&fs.wgsl).unwrap(); -} - -/// A fragment output narrower than its four-channel target (Steadfast's -/// `final` writes `out vec3 color`) is written through a `vec4` with alpha 1: -/// GL fills the missing channels, wgpu refuses the pipeline. -#[test] -fn narrow_fragment_outputs_fill_four_channels() { - let mut link = Link::default(); - let fs = run( - Stage::Fragment, - "uniform vec3 tint; -layout(location = 0) out vec3 color; -layout(location = 1) out float depth; -out vec4 extra; -void main() { - color = tint; - depth = 0.5; - extra = vec4(tint, 0.25); -} -", - &mut link, - ) - .unwrap(); - for wanted in [ - "layout(location = 0) out vec4 iris_out_color;", - "iris_out_color = vec4(color, 1.0);", - "layout(location = 1) out vec4 iris_out_depth;", - "iris_out_depth = vec4(depth, 0.0, 0.0, 1.0);", - "layout(location = 2) out vec4 extra;", - ] { - assert!(fs.glsl.contains(wanted), "{wanted}\n{}", fs.glsl); - } - assert_eq!( - fs.interface.outputs.values().collect::>(), - ["color", "depth", "extra"] - ); - let module = naga::front::wgsl::parse_str(&fs.wgsl).unwrap(); - let entry = &module.entry_points[0]; - let result = entry.function.result.as_ref().unwrap(); - let naga::TypeInner::Struct { members, .. } = &module.types[result.ty].inner else { - panic!("{}", fs.wgsl); - }; - for m in members { - assert!( - matches!( - module.types[m.ty].inner, - naga::TypeInner::Vector { - size: naga::VectorSize::Quad, - .. - } - ), - "{}", - fs.wgsl - ); - } - reparse(&fs.wgsl).unwrap(); -} - -/// The written WGSL, read back and validated: what wgpu is given. -fn reparse(wgsl: &str) -> Result<(), String> { - let module = naga::front::wgsl::parse_str(wgsl).map_err(|e| e.emit_to_string(wgsl))?; - naga::valid::Validator::new( - naga::valid::ValidationFlags::all(), - naga::valid::Capabilities::all(), - ) - .validate(&module) - .map(|_| ()) - .map_err(|e| e.emit_to_string(wgsl)) -} - -/// Every program the packs in `HORIZON_TEST_SHADERPACKS` enable by default -/// (`program.*.enabled` in their shaders.properties) -/// translates to WGSL that reads back (each pass's `.vsh` before its `.fsh`). Skipped when the -/// variable isn't set. Distant Horizons, Colorwheel and Voxy programs are -/// left out: Iris loads them only with those mods. -#[test] -fn real_packs_translate() { - let Some(dir) = std::env::var_os("HORIZON_TEST_SHADERPACKS") else { - return; - }; - let mut failures = Vec::new(); - for entry in std::fs::read_dir(dir).unwrap().flatten() { - let Ok(pack) = JavaPack::open(&entry.path()) else { - continue; - }; - let values = pack.values(&BTreeMap::new()); - let properties = pack.properties(&values, &env()).unwrap(); - let mut passes: BTreeMap<&str, Vec<&String>> = BTreeMap::new(); - for p in &pack.programs { - let file = p.rsplit('/').next().unwrap(); - if ["dh_", "clrwl_", "voxy"] - .iter() - .any(|m| file.starts_with(m)) - || !properties.program_enabled(program_name(p), &pack.options, &values) - { - continue; - } - passes - .entry(p.rsplit_once('.').unwrap().0) - .or_default() - .push(p); - } - let (mut ok, mut total) = (0, 0); - for programs in passes.values_mut() { - programs.sort_by_key(|p| !p.ends_with(".vsh")); - let mut link = Link { - image_formats: properties.image_formats(), - ..Link::default() - }; - for p in programs.iter() { - total += 1; - match pack - .translate(p, &values, &env(), &mut link) - .and_then(|t| reparse(&t.wgsl)) - { - Ok(()) => ok += 1, - Err(e) => { - failures.push(format!("{}: {e}", entry.file_name().to_string_lossy())) - } - } - } - } - println!( - "{}: {ok}/{total} programs translated", - entry.file_name().to_string_lossy() - ); - } - assert!(failures.is_empty(), "{}", failures.join("\n\n")); -} - -#[test] -fn textures_share_samplers_by_name() { - let mut link = Link { - shared_samplers: [("colortex0", "iris_linear"), ("colortex1", "iris_linear")] - .map(|(a, b)| (a.to_owned(), b.to_owned())) - .into(), - ..Link::default() - }; - let fs = run( - Stage::Fragment, - "uniform sampler2D colortex0;\nuniform sampler2D colortex1;\nuniform sampler2D custom;\n\ - uniform sampler2DShadow shadowtex0;\nvarying vec2 uv;\n\ - void main() { gl_FragColor = texture2D(colortex0, uv) + texture2D(colortex1, uv)\n\ - + texture2D(custom, uv) + shadow2D(shadowtex0, vec3(uv, 0.5)); }", - &mut link, - ) - .unwrap(); - assert_eq!( - fs.glsl.matches("uniform sampler iris_linear;").count(), - 1, - "{}", - fs.glsl - ); - assert!( - fs.glsl.contains("sampler2D(colortex1_t, iris_linear)"), - "{}", - fs.glsl - ); - assert!(fs.glsl.contains("uniform sampler custom_s;"), "{}", fs.glsl); - assert!( - fs.glsl.contains("uniform samplerShadow iris_compare;"), - "{}", - fs.glsl - ); - assert_eq!(fs.interface.sampler_vars["colortex0"], "iris_linear"); - // Three sampler bindings for four textures. - assert_eq!( - bindings(&fs).keys().filter(|k| !k.ends_with("_t")).count(), - 3, - "{:?}", - bindings(&fs) - ); -} diff --git a/docs/README.md b/docs/README.md index 2e06b6e89..00460d12d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,7 +12,6 @@ To play, use [the launcher](../launcher/README.md); the project site is [playhor - [Agent control](agent-control.md): what AI agents can drive in the launcher and the game, and the design of the control socket, `horizon-ctl` and MCP. - [Graphics](graphics.md): the Simple and Advanced graphics settings, the graphics benchmark and what it measured. - [Benchmark](benchmark.md): Horizon against vanilla 26.3 and vanilla with performance mods, on the same machine, world, path and settings: what is measured, how it is kept fair, how to reproduce it. -- [Shader packs](shader-packs.md): how the renderer draws a frame, the Horizon WGSL shader pack format (stages, uniforms, `pack.toml`, hot reload), and the plan to translate OptiFine/Iris packs. - [Extras](extras.md): optional zoom, minimap, FPS and keystroke overlays and auto-reconnect, all off by default. - [Mods](mods.md): Horizon's own mod system (Rust, sandboxed WebAssembly), how to make and install mods, and [what carries over from Java mods](research/java-mod-compat.md). - [Modpacks](modpacks.md): porting whole packs (ATM10 and smaller): import, inventory, carry-over, an agent per mod with a harness against the Java original, coverage, and how "10x" is measured. @@ -21,7 +20,7 @@ To play, use [the launcher](../launcher/README.md); the project site is [playhor - [Resource packs](resource-packs.md): everything a 26.3 resource pack can change, whether Horizon honours it, and the proof. - [Opt-in data collection](telemetry.md): the consent, exactly what is sent, how it is stored and sent. - [Benchmark report format](benchmark-report.md): the versioned JSON every benchmark writes (game, launcher, `tools/perf_bench.py`), the local log and, with consent, the upload. -- Menus: every screen is vanilla's (title, world list, create world, multiplayer, options, pause). Horizon's own settings hang off vanilla's Options the way mods add theirs: **Horizon Settings...** and **Mods...** in the last two slots of the Options grid (Horizon Settings opens Graphics, Advanced Graphics, Shader Packs, Controller Settings, Extras and Mods, each a vanilla options list), with Horizon Graphics, Advanced Graphics and Shader Packs also at the top of Video Settings and Controller Settings at the bottom of Controls. The title screen's and pause menu's Friends buttons open Join a Friend and Play with Friends ([play-with-friends.md](play-with-friends.md)). +- Menus: every screen is vanilla's (title, world list, create world, multiplayer, options, pause). Horizon's own settings hang off vanilla's Options the way mods add theirs: **Horizon Settings...** and **Mods...** in the last two slots of the Options grid (Horizon Settings opens Graphics, Advanced Graphics, Controller Settings, Extras and Mods, each a vanilla options list), with Horizon Graphics and Advanced Graphics also at the top of Video Settings and Controller Settings at the bottom of Controls. The title screen's and pause menu's Friends buttons open Join a Friend and Play with Friends ([play-with-friends.md](play-with-friends.md)). ## Parity diff --git a/docs/agent-control.md b/docs/agent-control.md index 9d5db2597..903a48b2f 100644 --- a/docs/agent-control.md +++ b/docs/agent-control.md @@ -78,7 +78,7 @@ passes on without checking it). | Patch notes, links | Patch notes, footer | yes | yes | yes | yes | | | Game log, crash reports | Settings | yes | yes | yes | yes | `logs [--crash]` | | Data collection: status, allow, deny, show, delete, send, bug, benchmark upload | Settings > Data | yes | part | yes | yes | sending was missing in MCP; `data_delete` needs `confirm` | -| Mod browser: search, check, install, update, remove, list (mods, resource packs, shader packs, modpacks) | Mods | yes | no | yes | yes | `mods ...`; `mods_remove` needs `confirm` | +| Mod browser: search, check, install, update, remove, list (mods, resource packs, modpacks) | Mods | yes | no | yes | yes | `mods ...`; `mods_remove` needs `confirm` | | Modpacks: import, port, play, remove, list | Modpacks | yes | no | yes | yes | `modpack ...`; `modpack_remove` needs `confirm` | | Launcher benchmark | (none) | yes | no | yes | yes | | | The game it started | (the game) | (horizon-ctl) | yes | (horizon-ctl) | yes | `game_*` | @@ -101,10 +101,10 @@ passes on without checking it). | Open to LAN | Pause > Open to LAN | part | no | yes | yes | `lan open\|close\|status` | | Play with Friends: host, stop, status | Pause | part | no | yes | yes | `friends host\|stop\|status` | | Vanilla options, every page | Options | yes | part | yes | yes | `options-get`, `options-set` (all of options.txt, typed); `options PAGE` was socket-only | -| Horizon Settings: Graphics, Shader Packs, Controller, Extras, Mods, AI Agent | Horizon Settings | part | part | yes | yes | `options horizon\|graphics\|extras\|agent...`, then `ui`/`click`/`set` | +| Horizon Settings: Graphics, Controller, Extras, Mods, AI Agent | Horizon Settings | part | part | yes | yes | `options horizon\|graphics\|extras\|agent...`, then `ui`/`click`/`set` | | Agent control on or off, status, connection details | Horizon Settings > AI Agent | no | no | yes | yes | new page (#1353) | | Key binds | Controls > Key Binds | yes | yes | yes | yes | `options-set key_key.jump key.keyboard.j` | -| Resource packs, shader packs | Options > Resource Packs, Shader Packs | part | part | yes | yes | `options packs`, `options shader-packs`, then `ui` rows and `click`; `options-set resourcePacks` | +| Resource packs | Options > Resource Packs | part | part | yes | yes | `options packs`, then `ui` rows and `click`; `options-set resourcePacks` | | Chat and commands | Chat | yes | yes | yes | yes | `chat`, `command`, `messages`, `chat-log` | | Inventory, containers, stations | Inventory, Chest... | yes | part | yes | yes | `inventory`, `screen`, `click-slot`; `container X Y Z` was socket-only | | Crafting, recipe book | Crafting | part | part | yes | yes | the grid by `click-slot`; the recipe book by `ui`/`click` | diff --git a/docs/design-system.md b/docs/design-system.md index bd4b635cb..e1417f749 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -137,8 +137,8 @@ Geist (400, 500, 600) and Geist Mono (400), bundled under the SIL Open Font Lice - **Launcher** (`launcher/`): drawn in the Blocks look below. A top bar (wordmark, the pages as tabs, the account chip, Settings), a sidebar (the instances, the hosted servers, the launcher's own version and channel at its foot), and the page over a stone texture. Home opens on a pixel - landscape banner with the hero, the version picker and Play; Mods holds the resource pack, - shader pack and modpack tabs; Versions and Patch notes are lists of panels; Settings is + landscape banner with the hero, the version picker and Play; Mods holds the mod, resource pack + and modpack tabs; Versions and Patch notes are lists of panels; Settings is grouped panels of rows. Dark by default, Light or System in Settings (Appearance). - **Website** (`website/`): the Blocks look below. The same page structure as before (what it is, screenshots, parity, launcher, patch notes, community), headings in Pixelify Sans, lists as diff --git a/docs/extras.md b/docs/extras.md index e27e142ea..26ddbe161 100644 --- a/docs/extras.md +++ b/docs/extras.md @@ -49,7 +49,7 @@ Horizon category in Controls, and adding one would put a non-vanilla mapping int ### Dynamic lights (`dynamicLights`, default off) - A light source held by a player or mob (main or off hand), dropped as an item, or an entity on - fire lights the blocks, entities and hand around it, as LambDynamicLights and OptiFine do. Not + fire lights the blocks, entities and hand around it, as dynamic-light mods do. Not in vanilla; the idea is ported, not a mod's code (issue #814). - An item glows like its block's default state (`getLightEmission`): torches 14, lanterns, glowstone, sea lanterns and jack o'lanterns 15, soul torches 10, a redstone torch 7; a lava bucket diff --git a/docs/graphics.md b/docs/graphics.md index 7629ad47d..d4261df81 100644 --- a/docs/graphics.md +++ b/docs/graphics.md @@ -2,7 +2,7 @@ ## Settings -Horizon's graphics settings are two screens built from vanilla's widgets, opened from the top of **Video Settings** (**Horizon Graphics...** and **Advanced Graphics...**, beside **Shader Packs...**, where Sodium and Iris put theirs) or from **Options → Horizon Settings...**: +Horizon's graphics settings are two screens built from vanilla's widgets, opened from the top of **Video Settings** (**Horizon Graphics...** and **Advanced Graphics...**) or from **Options → Horizon Settings...**: - **Graphics** (Simple): a preset (Performance, Balanced, Quality, Ultra or Custom) and LOD distance. - **Advanced Graphics**: every Horizon setting, grouped under Rendering, Quality, Display, Preferences, Performance and Overlay. @@ -45,8 +45,6 @@ Coverage and detail improve progressively; a large selected radius does not mean Ordinary Overworld atmospheric fog extends with the selected LOD distance. Fog Off disables distance haze at large ranges too; fluid, darkness and boss fog retain their limits. -LOD terrain enters the existing world colour and depth passes, so supported shader-pack postprocessing can see it. This does not add Iris material passes or shadow maps: Java packs still have the [existing fullscreen-pass limitations](shader-packs.md). Shader appearance and transitions need testing with the selected pack. - The setting is stored as `lodDistance` in `horizon-graphics.txt`; `0` disables it. Existing `renderDistance` in `options.txt` keeps its normal meaning for full chunks. Not implemented: anisotropic filtering (wgpu needs linear magnification for it, which blurs close blocks), MSAA (FXAA instead) and exclusive fullscreen (fullscreen is borderless). diff --git a/docs/horizon-ctl-commands.md b/docs/horizon-ctl-commands.md index 4bc4b2c34..394aac5bf 100644 --- a/docs/horizon-ctl-commands.md +++ b/docs/horizon-ctl-commands.md @@ -94,7 +94,7 @@ Every command `horizon-ctl` takes, its MCP tool in `horizon-ctl mcp` (the launch | `respawn` | `respawn` | Respawn from the death screen | none | | `pause` | `pause` | Open the pause menu | none | | `resume` | `resume` | Leave the pause menu | none | -| `options [PAGE]` | `options` | Open the options screen, or one of its pages (Horizon's too) | `page` (main, video, sound, controls, keybinds, controller, controller-binds, mouse, chat, accessibility, language, font, packs, mods, graphics, graphics-advanced, extras, horizon, shader-packs, agent): the page (default: the main options) | +| `options [PAGE]` | `options` | Open the options screen, or one of its pages (Horizon's too) | `page` (main, video, sound, controls, keybinds, controller, controller-binds, mouse, chat, accessibility, language, font, packs, mods, graphics, graphics-advanced, extras, horizon, agent): the page (default: the main options) | | `book [next\|back\|take\|done]` | `book` | The open book or lectern: its page text, or press one of its buttons | `action` (next, back, take, done): a button to press | | `signedit X Y Z [front\|back]` | `signedit` | Open the sign editor of the sign at X Y Z (then type and close) | `x` (integer, required): block X
`y` (integer, required): block Y
`z` (integer, required): block Z
`side` (front, back): which side | | `mount-menu` | `mount_menu` | Open the inventory of the horse, donkey, mule or llama being ridden | none | diff --git a/docs/java-packs.md b/docs/java-packs.md deleted file mode 100644 index 66ee402d3..000000000 --- a/docs/java-packs.md +++ /dev/null @@ -1,109 +0,0 @@ -# Java performance packs (no longer developed) - -Before the Rust rewrite, Horizon was a lab for Minecraft Java client performance: pinned Fabric performance packs, distant terrain, an original frame and tick recorder, and client and server starter downloads (the server starter is gone: [horizon-server](hosting.md) replaces it). This track is **no longer developed**. No release downloads are published; build the packs and starters locally with `python3 tools/build_release.py` (see [Build and run](#build-and-run)). This page keeps its documentation. - -## Java packs: Mac starter (out of scope) - -Build **horizon-mac-starter-0.21.0.zip** with `python3 tools/build_release.py` (it lands in `dist/`), extract it and open `Mac-Start.command`. It verifies and installs official universal Prism Launcher in `~/Applications` if needed, then opens the balanced pack import. Sign into Microsoft **inside Prism yourself**. This project needs no credentials. - -Use native **Java 25 ARM64**, maximum heap **6144 MiB**, minimum **512 MiB** on a 16 GB M2 Pro Mac. Prism can download Java automatically. Start at 1280×800 or 1920×1200; full Retina resolution is a larger GPU workload. [START-HERE.txt](../packs/START-HERE.txt) has details. The same `.mrpack` files import into Prism on Windows and Linux. - -Core and balanced passed macOS title and demo-world rendering probes on the hosted -ARM64 software renderer, including readback, saves and clean exits. Physical M2 -GPU performance and macOS 27 were never tested. - -| Profile | Full-detail chunks | Distant LOD radius | Suggested maximum heap | -| --- | ---: | ---: | ---: | -| baseline | 12 | none | 6144 MiB | -| core-performance | 12 | none | 6144 MiB | -| m2-balanced | 12 | 128 | 6144 MiB | -| adaptive (experiment) | 12 | 128 | 6144 MiB | -| worker-pacing (experiment) | 12 | 128 | 6144 MiB | -| adaptive-pacing (experiment) | 12 | 128 | 6144 MiB | -| queue-wait (experimental) | 12 | 128 | 6144 MiB | -| adaptive-queue (experimental) | 12 | 128 | 6144 MiB | -| dispatch-retry (experimental) | 12 | 128 | 6144 MiB | -| tick-dedup (experimental) | 12 | 128 | 6144 MiB | -| culling-empty (experimental) | 12 | 128 | 6144 MiB | -| quad-reuse (experimental) | 12 | 128 | 6144 MiB | -| terrain-position (experimental) | 12 | 128 | 6144 MiB | -| culling-rays (experimental) | 12 | 128 | 6144 MiB | -| foreground-first (combined experiment) | 12 | 128 | 6144 MiB | -| foreground-vulkan (combined experiment) | 12 | 128 | 6144 MiB | -| low-memory | 8 | none | 3072 MiB | -| far-distance | 12 | 256 | 8192 MiB, with sufficient physical RAM | -| cinematic | 12 | 128 | 6144 MiB | -| vulkan | 12 | 128 | 6144 MiB | - -Balanced combines [Sodium](https://modrinth.com/mod/sodium), [Lithium](https://modrinth.com/mod/lithium), [FerriteCore](https://modrinth.com/mod/ferrite-core), [ImmediatelyFast](https://modrinth.com/mod/immediatelyfast), [EntityCulling](https://modrinth.com/mod/entityculling), and [Distant Horizons](https://modrinth.com/mod/distanthorizons). Cinematic adds [Iris](https://modrinth.com/mod/iris) and [Complementary Reimagined](https://modrinth.com/shader/complementary-reimagined), initially disabled. Vulkan independently experiments with the game's own backend and fallback, without Iris. - -Distant LOD is simplified terrain. Nearby blocks retain full detail. Initial generation costs CPU/disk/memory and needs warm-up. DH adjusts clouds, chunk fade and transparency to blend its renderer; assess appearance as well as speed. Shaders cost GPU time and have [Mac-specific limits](https://shaders.properties/current/reference/miscellaneous/macos/). Universal CPU/GPU support and exact vanilla equivalence remain validation goals. Simulation distance stays 12 in every profile; normal 20 TPS rules are retained. - -For the first comparison at the same view distance, use `baseline` versus -`core-performance`: both select 12 full-detail chunks and no distant LOD or shaders. -The latter adds only the pinned core performance mods and leaves every original -controller disabled. Then test balanced separately for its additional distant -terrain. Matching captured settings does not prove pixel-identical rendering. - -The profiles marked *experiment* each isolate one change to Distant Horizons, -EntityCulling or the JVM (adaptive terrain throttling, precise worker pacing, -debounce-queue waits, rejected-dispatch retries, empty culling whitelists, -culling ray scratch vectors, terrain quad reuse, terrain position arithmetic, -tick membership storage, a smaller heap, compact object headers). Each kept the -upstream behaviour it replaced and was measured only on Linux software rendering, -where none showed a gain large enough to enable by default; balanced keeps them -all off. Their write-ups were removed once the Rust rewrite replaced this track; -they remain in Git history. Matched software-rendered pairs showed `core-performance` -14–21% faster than `baseline`, and Vulkan about 1.5% more FPS for about 12% more -CPU; neither is evidence for a Mac GPU. - -To try the game's own Vulkan backend on a Mac, see [Vulkan on your Mac](vulkan.md); -to compare garbage collectors, see [collector comparisons](collectors.md). - -## Measure on your hardware - -Press **F8** during gameplay to start a 120-second recording, or again to stop early. Rebind in Controls if needed. This also works with restricted chat accounts. Alternatively: `/horizon bench