refactor: migrate sparse shaders to WESL (#1767)

Migrate the sparse-strip shaders from WGSL source files to
[WESL](https://wesl-lang.dev/) while preserving their existing behavior.
The build now links each WESL root to WGSL and retains optional GLSL
generation for WebGL.

This establishes the foundation for future specialized shader variants,
reusable imported shader modules, reduced duplication, and conditional
shader composition. It also exposes linked shaders for shared linting
and tooling while keeping stripping disabled during the initial
migration.

Created with assistance from GPT-5.6 Sol.
diff --git a/Cargo.lock b/Cargo.lock
index ac95a65..7df92f0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -119,6 +119,17 @@
 checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
 
 [[package]]
+name = "annotate-snippets"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1"
+dependencies = [
+ "anstyle",
+ "memchr",
+ "unicode-width",
+]
+
+[[package]]
 name = "anstream"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -193,6 +204,15 @@
 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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -284,15 +304,30 @@
 
 [[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.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd"
 dependencies = [
- "bit-vec",
+ "bit-vec 0.9.1",
 ]
 
 [[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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
@@ -322,6 +357,15 @@
 ]
 
 [[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
 name = "block2"
 version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -610,6 +654,15 @@
 ]
 
 [[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 = "core-foundation"
 version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -659,6 +712,15 @@
 ]
 
 [[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
 name = "crc32fast"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -679,7 +741,7 @@
  "clap",
  "criterion-plot",
  "is-terminal",
- "itertools",
+ "itertools 0.10.5",
  "num-traits",
  "once_cell",
  "oorandom",
@@ -698,7 +760,7 @@
 checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
 dependencies = [
  "cast",
- "itertools",
+ "itertools 0.10.5",
 ]
 
 [[package]]
@@ -742,6 +804,16 @@
 checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
 
 [[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
 name = "ctor"
 version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -763,12 +835,45 @@
 checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
 
 [[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",
+ "unicode-xid",
+]
+
+[[package]]
 name = "devserver_lib"
 version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "edf215dbb8cb1409cca7645aaed35f9e39fb0a21855bba1ac48bc0334903bf66"
 
 [[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
 name = "dispatch"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -884,6 +989,15 @@
 checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
 
 [[package]]
+name = "ena"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1"
+dependencies = [
+ "log",
+]
+
+[[package]]
 name = "encode_unicode"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -993,6 +1107,12 @@
 checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
 
 [[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
+[[package]]
 name = "flate2"
 version = "1.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1009,6 +1129,12 @@
 checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
 
 [[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
 name = "foldhash"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1170,6 +1296,16 @@
 ]
 
 [[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
 name = "gethostname"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1727,6 +1863,15 @@
 ]
 
 [[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
 name = "itoa"
 version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1851,6 +1996,15 @@
 ]
 
 [[package]]
+name = "keccak"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
+dependencies = [
+ "cpufeatures",
+]
+
+[[package]]
 name = "khronos-egl"
 version = "6.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1947,6 +2101,37 @@
 ]
 
 [[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 0.14.0",
+ "lalrpop-util",
+ "petgraph",
+ "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 = "lazy_static"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1965,6 +2150,72 @@
 checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
 
 [[package]]
+name = "lexical"
+version = "7.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc8a009b2ff1f419ccc62706f04fe0ca6e67b37460513964a3dfdb919bb37d6"
+dependencies = [
+ "lexical-core",
+]
+
+[[package]]
+name = "lexical-core"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594"
+dependencies = [
+ "lexical-parse-float",
+ "lexical-parse-integer",
+ "lexical-util",
+ "lexical-write-float",
+ "lexical-write-integer",
+]
+
+[[package]]
+name = "lexical-parse-float"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56"
+dependencies = [
+ "lexical-parse-integer",
+ "lexical-util",
+]
+
+[[package]]
+name = "lexical-parse-integer"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34"
+dependencies = [
+ "lexical-util",
+]
+
+[[package]]
+name = "lexical-util"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17"
+
+[[package]]
+name = "lexical-write-float"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361"
+dependencies = [
+ "lexical-util",
+ "lexical-write-integer",
+]
+
+[[package]]
+name = "lexical-write-integer"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df"
+dependencies = [
+ "lexical-util",
+]
+
+[[package]]
 name = "libc"
 version = "0.2.186"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2068,6 +2319,38 @@
 checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
 
 [[package]]
+name = "logos"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f"
+dependencies = [
+ "logos-derive",
+]
+
+[[package]]
+name = "logos-codegen"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970"
+dependencies = [
+ "fnv",
+ "proc-macro2",
+ "quote",
+ "regex-automata",
+ "regex-syntax",
+ "syn",
+]
+
+[[package]]
+name = "logos-derive"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31"
+dependencies = [
+ "logos-codegen",
+]
+
+[[package]]
 name = "matchit"
 version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2165,7 +2448,7 @@
 checksum = "0dd91265cc2454558f659b3b4b9640f0ddb8cc6521277f166b8a8c181c898079"
 dependencies = [
  "arrayvec",
- "bit-set",
+ "bit-set 0.9.1",
  "bitflags 2.11.1",
  "cfg-if",
  "cfg_aliases",
@@ -2232,6 +2515,12 @@
 ]
 
 [[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
 name = "notify"
 version = "8.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2770,6 +3059,25 @@
 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 = "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"
@@ -2892,6 +3200,12 @@
 ]
 
 [[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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3403,6 +3717,16 @@
 ]
 
 [[package]]
+name = "sha3"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874"
+dependencies = [
+ "digest",
+ "keccak",
+]
+
+[[package]]
 name = "sharded-slab"
 version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3603,6 +3927,18 @@
 ]
 
 [[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",
+]
+
+[[package]]
 name = "strsim"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3665,6 +4001,15 @@
 checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
 
 [[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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3959,6 +4304,12 @@
 ]
 
 [[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
 name = "unicode-bidi"
 version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4013,6 +4364,12 @@
 checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
 
 [[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
 name = "unit-prefix"
 version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4224,6 +4581,7 @@
 version = "0.0.9"
 dependencies = [
  "naga",
+ "wesl",
 ]
 
 [[package]]
@@ -4654,6 +5012,35 @@
 ]
 
 [[package]]
+name = "wesl"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc3857a39e7220245e4ec1e2f6230e1b5ccd3542545bc1ee5c13d79597e6d193"
+dependencies = [
+ "annotate-snippets",
+ "derive_more",
+ "half",
+ "itertools 0.14.0",
+ "num-traits",
+ "thiserror 2.0.18",
+ "wesl-macros",
+ "wgsl-parse",
+ "wgsl-types",
+]
+
+[[package]]
+name = "wesl-macros"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8662ee0b2ef199c31f486b869e5272ac364898c09d352483370d37b8c3abf9f9"
+dependencies = [
+ "itertools 0.14.0",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "wgpu"
 version = "29.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4690,8 +5077,8 @@
 checksum = "02da3ad1b568337f25513b317870960ef87073ea0945502e44b864b67a8c77b7"
 dependencies = [
  "arrayvec",
- "bit-set",
- "bit-vec",
+ "bit-set 0.9.1",
+ "bit-vec 0.9.1",
  "bitflags 2.11.1",
  "bytemuck",
  "cfg_aliases",
@@ -4762,7 +5149,7 @@
  "android_system_properties",
  "arrayvec",
  "ash",
- "bit-set",
+ "bit-set 0.9.1",
  "bitflags 2.11.1",
  "block2 0.6.2",
  "bytemuck",
@@ -4861,6 +5248,34 @@
 ]
 
 [[package]]
+name = "wgsl-parse"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2141e2425fbb5aefd13e875adc7247e5dc889a3bbbe5a016dd1546211405f89f"
+dependencies = [
+ "annotate-snippets",
+ "derive_more",
+ "itertools 0.14.0",
+ "lalrpop",
+ "lalrpop-util",
+ "lexical",
+ "logos",
+ "thiserror 2.0.18",
+ "wgsl-types",
+]
+
+[[package]]
+name = "wgsl-types"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3cf8623d173060d5a9e1465b89954ea42f3ac21af2f533c7975d003f8d82b98"
+dependencies = [
+ "half",
+ "itertools 0.14.0",
+ "num-traits",
+]
+
+[[package]]
 name = "winapi-util"
 version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/sparse_strips/vello_common/src/filter/gaussian_blur.rs b/sparse_strips/vello_common/src/filter/gaussian_blur.rs
index 4c807d5..0b68f6a 100644
--- a/sparse_strips/vello_common/src/filter/gaussian_blur.rs
+++ b/sparse_strips/vello_common/src/filter/gaussian_blur.rs
@@ -37,7 +37,7 @@
 /// The multi-scale decimation algorithm guarantees that kernel size never exceeds this value.
 /// Decimation stops when remaining variance ≤ 4.0 (σ ≤ 2.0), which produces kernels of size
 /// at most 13 (radius = ceil(3σ) = 6, size = 1 + 2×6 = 13).
-// Keep in sync with MAX_KERNEL_SIZE in vello_sparse_shaders/shaders/filter.wgsl
+// Keep in sync with MAX_KERNEL_SIZE in vello_sparse_shaders/shaders/filter.wesl
 pub const MAX_KERNEL_SIZE: usize = 13;
 
 #[cfg(test)]
diff --git a/sparse_strips/vello_common/src/math.rs b/sparse_strips/vello_common/src/math.rs
index 7c98d68..21760ba 100644
--- a/sparse_strips/vello_common/src/math.rs
+++ b/sparse_strips/vello_common/src/math.rs
@@ -29,7 +29,7 @@
 }
 
 // From <https://github.com/linebender/tiny-skia/blob/68b198a7210a6bbf752b43d6bc4db62445730313/path/src/scalar.rs#L12>
-// Note: If this value changes, also update NEARLY_ZERO_TOLERANCE in render.wgsl
+// Note: If this value changes, also update NEARLY_ZERO_TOLERANCE in render.wesl
 // @see {@link https://github.com/linebender/vello/blob/58b80d660e2fc5aef3bd32b24af3f95e973aab95/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl#L63}
 const SCALAR_NEARLY_ZERO: f32 = 1.0 / (1 << 12) as f32;
 
diff --git a/sparse_strips/vello_hybrid/src/filter.rs b/sparse_strips/vello_hybrid/src/filter.rs
index 93b69e0..5a850af 100644
--- a/sparse_strips/vello_hybrid/src/filter.rs
+++ b/sparse_strips/vello_hybrid/src/filter.rs
@@ -23,11 +23,11 @@
 /// do use checked accesses for the offset filter, the bottleneck is formed by the gaussian blur
 /// convolution.
 ///
-/// Keep this in sync with `FILTER_ATLAS_PADDING` in `filter.wgsl`!
+/// Keep this in sync with `FILTER_ATLAS_PADDING` in `filter.wesl`!
 #[expect(clippy::cast_possible_truncation, reason = "safe in this case")]
 pub(crate) const FILTER_ATLAS_PADDING: u16 = MAX_KERNEL_SIZE as u16 / 2;
 
-// Note: Keep these variables and struct layouts in sync with `filter.wgsl`!
+// Note: Keep these variables and struct layouts in sync with `filter.wesl`!
 
 // Since we store in RGBA32 texture.
 const BYTES_PER_TEXEL: usize = 16;
diff --git a/sparse_strips/vello_hybrid/src/paint.rs b/sparse_strips/vello_hybrid/src/paint.rs
index 4545aa2..f0a2aaa 100644
--- a/sparse_strips/vello_hybrid/src/paint.rs
+++ b/sparse_strips/vello_hybrid/src/paint.rs
@@ -18,7 +18,7 @@
 const PAINT_TYPE_SWEEP_GRADIENT: u32 = 4;
 const PAINT_TYPE_BLURRED_ROUNDED_RECT: u32 = 5;
 
-// See the layout information in `render.wgsl`.
+// See the layout information in `render.wesl`.
 pub(crate) const COLOR_SOURCE_SHIFT: u32 = 29;
 const PAINT_TYPE_SHIFT: u32 = 26;
 const PAINT_TEXTURE_INDEX_MASK: u32 = (1 << PAINT_TYPE_SHIFT) - 1;
diff --git a/sparse_strips/vello_hybrid/src/render/common.rs b/sparse_strips/vello_hybrid/src/render/common.rs
index 919315e..fdc814b 100644
--- a/sparse_strips/vello_hybrid/src/render/common.rs
+++ b/sparse_strips/vello_hybrid/src/render/common.rs
@@ -356,23 +356,23 @@
 /// A GPU strip instance for rendering.
 ///
 /// This struct corresponds to the `StripInstance` struct in the shader.
-/// See the `StripInstance` documentation in `render.wgsl` for detailed field descriptions.
+/// See the `StripInstance` documentation in `render.wesl` for detailed field descriptions.
 #[repr(C)]
 #[derive(Debug, Clone, Copy, Zeroable, Pod)]
 pub struct GpuStrip {
-    /// See `StripInstance::xy` documentation in `render.wgsl`.
+    /// See `StripInstance::xy` documentation in `render.wesl`.
     pub x: u16,
-    /// See `StripInstance::xy` documentation in `render.wgsl`.
+    /// See `StripInstance::xy` documentation in `render.wesl`.
     pub y: u16,
-    /// See `StripInstance::dense_width_or_rect_height` documentation in `render.wgsl`.
+    /// See `StripInstance::dense_width_or_rect_height` documentation in `render.wesl`.
     pub width: u16,
-    /// See `StripInstance::dense_width_or_rect_height` documentation in `render.wgsl`.
+    /// See `StripInstance::dense_width_or_rect_height` documentation in `render.wesl`.
     pub dense_width_or_rect_height: u16,
-    /// See `StripInstance::col_idx_or_rect_frac` documentation in `render.wgsl`.
+    /// See `StripInstance::col_idx_or_rect_frac` documentation in `render.wesl`.
     pub col_idx_or_rect_frac: u32,
-    /// See `StripInstance::payload` documentation in `render.wgsl`.
+    /// See `StripInstance::payload` documentation in `render.wesl`.
     pub payload: u32,
-    /// See `StripInstance::paint_and_rect_flag` documentation in `render.wgsl`.
+    /// See `StripInstance::paint_and_rect_flag` documentation in `render.wesl`.
     pub paint_and_rect_flag: u32,
     /// Painter's-order index used to compute z-depth for early-z rejection in shader.
     /// In other words, the back-most draw has index 0 and every additional draw in front
diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs
index ae90916..a977055 100644
--- a/sparse_strips/vello_hybrid/src/scene.rs
+++ b/sparse_strips/vello_hybrid/src/scene.rs
@@ -505,7 +505,7 @@
         rects: impl IntoIterator<Item = SampleRect>,
     ) {
         // This API currently doesn't take extend mode parameters: as of writing, the
-        // `render.wgsl` shader does not use extend modes to sample across boundaries, i.e.,
+        // `render.wesl` shader does not use extend modes to sample across boundaries, i.e.,
         // sampling near a boundary doesn't take extend modes into account when determining where
         // the sample should be taken.
         //
diff --git a/sparse_strips/vello_sparse_shaders/Cargo.toml b/sparse_strips/vello_sparse_shaders/Cargo.toml
index 208af6d..0d8f04b 100644
--- a/sparse_strips/vello_sparse_shaders/Cargo.toml
+++ b/sparse_strips/vello_sparse_shaders/Cargo.toml
@@ -2,7 +2,7 @@
 name = "vello_sparse_shaders"
 # When updating, also update the version in the workspace dependency in the root Cargo.toml
 version = "0.0.9"
-description = "Provide compilation of wgsl to glsl to support the WebGL `vello_hybrid` backend."
+description = "Provide WESL shaders linked to WGSL and GLSL for the `vello_hybrid` backends."
 categories = ["rendering", "graphics"]
 keywords = ["2d", "vector-graphics"]
 edition.workspace = true
@@ -21,6 +21,7 @@
 
 [build-dependencies]
 naga = { workspace = true, features = ["wgsl-in", "glsl-out"], optional = true }
+wesl = "0.4.0"
 
 [features]
 glsl = ["dep:naga"]
diff --git a/sparse_strips/vello_sparse_shaders/README.md b/sparse_strips/vello_sparse_shaders/README.md
index 87f3b92..dbcabf3 100644
--- a/sparse_strips/vello_sparse_shaders/README.md
+++ b/sparse_strips/vello_sparse_shaders/README.md
@@ -9,19 +9,20 @@
 
 </div>
 
-This crate contains the WGSL programs and optionally generated GLSL shader programs used by the
-Vello Hybrid renderer.
+This crate contains the WESL programs and linked WGSL output, plus optionally generated GLSL
+shader programs, used by the Vello Hybrid renderer.
 
 ## Features
-- Single source of truth authored WGSL programs.
-- Automated build step for compiling GLSL shaders using [naga](https://github.com/gfx-rs/wgpu/tree/trunk/naga).
+- Single source of truth authored as [WESL](https://wesl-lang.dev/) programs.
+- Automated build step for linking WESL to WGSL and compiling GLSL shaders using
+  [naga](https://github.com/gfx-rs/wgpu/tree/trunk/naga).
 
 ## Usage
-This crate provides the WGSL programs and build step for GLSL programs that are used by the
-optimized hybrid rendering engine.
+This crate provides linked WGSL programs and the build step for GLSL programs used by the optimized
+hybrid rendering engine.
 
-Whenever the WGSL shaders are updated, the build script will automatically regenerate the GLSL
-programs used by the crate itself.
+Whenever the WESL shaders are updated, the build script automatically relinks the WGSL and
+regenerates the GLSL programs used by the crate itself.
 
 If you want to inspect the generated WebGL GLSL output directly, you can also create a "local copy" that is
 easier to inspect by running:
diff --git a/sparse_strips/vello_sparse_shaders/build.rs b/sparse_strips/vello_sparse_shaders/build.rs
index 8811532..7ac2ab6 100644
--- a/sparse_strips/vello_sparse_shaders/build.rs
+++ b/sparse_strips/vello_sparse_shaders/build.rs
@@ -7,6 +7,7 @@
 use std::fmt::Write;
 use std::fs;
 use std::path::{Path, PathBuf};
+use wesl::Wesl;
 
 #[allow(warnings)]
 #[cfg(feature = "glsl")]
@@ -17,13 +18,15 @@
 #[path = "src/lint/mod.rs"]
 mod lint;
 #[allow(warnings)]
-#[path = "src/shader_info.rs"]
-mod shader_info;
-#[allow(warnings)]
 #[cfg(feature = "glsl")]
 #[path = "src/types.rs"]
 mod types;
 
+struct ShaderInfo {
+    name: String,
+    wgsl_source: String,
+}
+
 // TODO: Format the generated code via `rustfmt`.
 // TODO: Use `quote` instead of string concatenation to generate code.
 fn main() {
@@ -34,13 +37,54 @@
     // metadata.
     let dest_path = Path::new(&out_dir).join("compiled_shaders.rs");
 
-    // Get paths to WGSL files
+    // Link each WESL root module to WGSL.
     let shader_dir = PathBuf::from("shaders");
-    let shader_infos = shader_info::load_shader_infos(&shader_dir).expect("Unable to read shaders");
+    let shader_infos = load_shader_infos(&shader_dir);
     fs::write(dest_path, generate_compiled_shaders_module(&shader_infos)).unwrap();
 }
 
-fn generate_compiled_shaders_module(shader_infos: &[shader_info::ShaderInfo]) -> String {
+fn load_shader_infos(shader_dir: &Path) -> Vec<ShaderInfo> {
+    let shader_names = load_shader_names(shader_dir);
+    let mut compiler = Wesl::new(shader_dir);
+
+    // Keep the initial migration behavior-preserving.
+    compiler.use_stripping(false);
+
+    shader_names
+        .into_iter()
+        .map(|name| compile_shader(&compiler, name))
+        .collect()
+}
+
+fn load_shader_names(shader_dir: &Path) -> Vec<String> {
+    let mut shader_names = fs::read_dir(shader_dir)
+        .expect("Unable to discover WESL shaders")
+        .filter_map(|entry| {
+            let path = entry.ok()?.path();
+            if path.extension()?.to_str()? == "wesl" {
+                Some(path.file_stem()?.to_str()?.to_owned())
+            } else {
+                None
+            }
+        })
+        .collect::<Vec<_>>();
+    shader_names.sort();
+    shader_names
+}
+
+fn compile_shader<R: wesl::Resolver>(compiler: &Wesl<R>, name: String) -> ShaderInfo {
+    let module_path = format!("package::{name}")
+        .parse()
+        .expect("generated WESL module path should be valid");
+    let wgsl_source = compiler
+        .compile(&module_path)
+        .unwrap_or_else(|error| panic!("Unable to compile `{name}.wesl`: {error}"))
+        .to_string();
+
+    ShaderInfo { name, wgsl_source }
+}
+
+fn generate_compiled_shaders_module(shader_infos: &[ShaderInfo]) -> String {
     let mut buf = String::new();
     writeln!(
         buf,
@@ -48,12 +92,23 @@
     )
     .unwrap();
 
-    writeln!(buf, "/// Re-exporting wgsl shader source code.").unwrap();
+    writeln!(buf, "/// WGSL shader sources linked from WESL modules.").unwrap();
 
     writeln!(buf, "pub mod wgsl {{").unwrap();
     for shader_info in shader_infos {
         generate_wgsl_shader_module(&mut buf, shader_info).unwrap();
     }
+    writeln!(
+        buf,
+        "    /// All linked WGSL shader sources, keyed by WESL root module name."
+    )
+    .unwrap();
+    writeln!(buf, "    pub const ALL: &[(&str, &str)] = &[").unwrap();
+    for shader_info in shader_infos {
+        let const_name = shader_info.name.to_uppercase();
+        writeln!(buf, "        (\"{}\", {const_name}),", shader_info.name).unwrap();
+    }
+    writeln!(buf, "    ];").unwrap();
     writeln!(buf, "}}").unwrap();
 
     // Implementation for creating a CompiledGlsl struct per shader assuming the standard entry
@@ -62,7 +117,7 @@
     {
         writeln!(
             buf,
-            "/// Build time GLSL shaders derived from wgsl shaders."
+            "/// Build-time GLSL shaders derived from linked WESL modules."
         )
         .unwrap();
 
@@ -81,12 +136,13 @@
     buf
 }
 
-fn generate_wgsl_shader_module(
-    buf: &mut String,
-    shader_info: &shader_info::ShaderInfo,
-) -> std::fmt::Result {
+fn generate_wgsl_shader_module(buf: &mut String, shader_info: &ShaderInfo) -> std::fmt::Result {
     let const_name = shader_info.name.to_uppercase();
-    writeln!(buf, "    /// Source for `{}.wgsl`", shader_info.name)?;
+    writeln!(
+        buf,
+        "    /// Linked WGSL source for `{}.wesl`.",
+        shader_info.name
+    )?;
     writeln!(
         buf,
         "    pub const {const_name}: &str = r###\"{}\"###;",
diff --git a/sparse_strips/vello_sparse_shaders/shaders/blend.wgsl b/sparse_strips/vello_sparse_shaders/shaders/blend.wesl
similarity index 100%
rename from sparse_strips/vello_sparse_shaders/shaders/blend.wgsl
rename to sparse_strips/vello_sparse_shaders/shaders/blend.wesl
diff --git a/sparse_strips/vello_sparse_shaders/shaders/clear.wgsl b/sparse_strips/vello_sparse_shaders/shaders/clear.wesl
similarity index 100%
rename from sparse_strips/vello_sparse_shaders/shaders/clear.wgsl
rename to sparse_strips/vello_sparse_shaders/shaders/clear.wesl
diff --git a/sparse_strips/vello_sparse_shaders/shaders/copy.wgsl b/sparse_strips/vello_sparse_shaders/shaders/copy.wesl
similarity index 100%
rename from sparse_strips/vello_sparse_shaders/shaders/copy.wgsl
rename to sparse_strips/vello_sparse_shaders/shaders/copy.wesl
diff --git a/sparse_strips/vello_sparse_shaders/shaders/filter.wgsl b/sparse_strips/vello_sparse_shaders/shaders/filter.wesl
similarity index 100%
rename from sparse_strips/vello_sparse_shaders/shaders/filter.wgsl
rename to sparse_strips/vello_sparse_shaders/shaders/filter.wesl
diff --git a/sparse_strips/vello_sparse_shaders/shaders/render.wgsl b/sparse_strips/vello_sparse_shaders/shaders/render.wesl
similarity index 100%
rename from sparse_strips/vello_sparse_shaders/shaders/render.wgsl
rename to sparse_strips/vello_sparse_shaders/shaders/render.wesl
diff --git a/sparse_strips/vello_sparse_shaders/src/lib.rs b/sparse_strips/vello_sparse_shaders/src/lib.rs
index 2b50c23..aa7fe02 100644
--- a/sparse_strips/vello_sparse_shaders/src/lib.rs
+++ b/sparse_strips/vello_sparse_shaders/src/lib.rs
@@ -1,15 +1,32 @@
 // Copyright 2025 the Vello Authors
 // SPDX-License-Identifier: Apache-2.0 OR MIT
 
-//! This is a utility library to help integrate `vello_hybrid` WebGPU wgsl shaders into glsl.
+//! WESL shader sources linked to WGSL and optionally compiled to GLSL for `vello_hybrid`.
 
 #[cfg(feature = "glsl")]
 mod compile;
 #[cfg(feature = "glsl")]
 mod lint;
-#[cfg(all(test, feature = "glsl"))]
-mod shader_info;
 #[cfg(feature = "glsl")]
 mod types;
 
 include!(concat!(env!("OUT_DIR"), "/compiled_shaders.rs"));
+
+#[cfg(all(test, feature = "glsl"))]
+mod tests {
+    use naga::front::wgsl;
+
+    use crate::lint::lint;
+
+    #[test]
+    fn every_shipped_shader_passes_the_lint() {
+        assert!(
+            !crate::wgsl::ALL.is_empty(),
+            "expected at least one linked WESL shader"
+        );
+        for &(name, source) in crate::wgsl::ALL {
+            let module = wgsl::parse_str(source).expect("linked WGSL parses");
+            lint(name, &module);
+        }
+    }
+}
diff --git a/sparse_strips/vello_sparse_shaders/src/lint/mod.rs b/sparse_strips/vello_sparse_shaders/src/lint/mod.rs
index 0faa1ff..f76bdf8 100644
--- a/sparse_strips/vello_sparse_shaders/src/lint/mod.rs
+++ b/sparse_strips/vello_sparse_shaders/src/lint/mod.rs
@@ -1,7 +1,7 @@
 // Copyright 2026 the Vello Authors
 // SPDX-License-Identifier: Apache-2.0 OR MIT
 
-//! Lints for the WGSL shaders.
+//! Lints for the WGSL output linked from WESL shaders.
 //!
 //! [`lint`] is the entry point. Each individual pass lives in its own submodule
 //! under `lint::*` and exposes a `check(module: &Module) -> Option<LintReport>`.
@@ -32,7 +32,7 @@
         return;
     }
 
-    let mut message = format!("`{shader_name}.wgsl` failed shader lints:\n");
+    let mut message = format!("`{shader_name}.wesl` failed shader lints:\n");
     for report in &reports {
         use std::fmt::Write as _;
         write!(
@@ -49,26 +49,3 @@
     }
     panic!("{message}");
 }
-
-#[cfg(test)]
-mod tests {
-    use naga::front::wgsl;
-
-    use super::*;
-
-    #[test]
-    fn every_shipped_shader_passes_the_lint() {
-        let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
-        let shader_dir = manifest_dir.join("shaders");
-        let shaders =
-            crate::shader_info::load_shader_infos(&shader_dir).expect("load WGSL shaders");
-        assert!(
-            !shaders.is_empty(),
-            "expected at least one shader in {shader_dir:?}"
-        );
-        for shader in shaders {
-            let module = wgsl::parse_str(&shader.wgsl_source).expect("WGSL parses");
-            lint(&shader.name, &module);
-        }
-    }
-}
diff --git a/sparse_strips/vello_sparse_shaders/src/main.rs b/sparse_strips/vello_sparse_shaders/src/main.rs
index 989f4d6..f689608 100644
--- a/sparse_strips/vello_sparse_shaders/src/main.rs
+++ b/sparse_strips/vello_sparse_shaders/src/main.rs
@@ -8,8 +8,6 @@
 #[cfg(feature = "glsl")]
 mod lint;
 #[cfg(feature = "glsl")]
-mod shader_info;
-#[cfg(feature = "glsl")]
 mod types;
 
 #[cfg(feature = "glsl")]
@@ -18,22 +16,15 @@
     let output_dir = manifest_dir.join("generated_glsl");
     std::fs::create_dir_all(&output_dir).unwrap();
 
-    let shader_infos = shader_info::load_shader_infos(&manifest_dir.join("shaders")).unwrap();
-
-    for shader_info in shader_infos {
-        let shader = compile::compile_wgsl_shader(
-            &shader_info.wgsl_source,
-            &shader_info.name,
-            "vs_main",
-            "fs_main",
-        );
+    for &(name, wgsl_source) in vello_sparse_shaders::wgsl::ALL {
+        let shader = compile::compile_wgsl_shader(wgsl_source, name, "vs_main", "fs_main");
         std::fs::write(
-            output_dir.join(format!("{}.vert.glsl", shader_info.name)),
+            output_dir.join(format!("{name}.vert.glsl")),
             shader.vertex.source,
         )
         .unwrap();
         std::fs::write(
-            output_dir.join(format!("{}.frag.glsl", shader_info.name)),
+            output_dir.join(format!("{name}.frag.glsl")),
             shader.fragment.source,
         )
         .unwrap();
diff --git a/sparse_strips/vello_sparse_shaders/src/shader_info.rs b/sparse_strips/vello_sparse_shaders/src/shader_info.rs
deleted file mode 100644
index eb4aa94..0000000
--- a/sparse_strips/vello_sparse_shaders/src/shader_info.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2026 the Vello Authors
-// SPDX-License-Identifier: Apache-2.0 OR MIT
-
-use std::fs;
-use std::io;
-use std::path::Path;
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct ShaderInfo {
-    pub(crate) name: String,
-    pub(crate) wgsl_source: String,
-}
-
-pub(crate) fn load_shader_infos(shader_dir: &Path) -> io::Result<Vec<ShaderInfo>> {
-    let shader_paths = fs::read_dir(shader_dir)?
-        .filter_map(|entry| {
-            let entry = entry.ok()?;
-            let path = entry.path();
-            if path.extension()?.to_str()? == "wgsl" {
-                Some(path)
-            } else {
-                None
-            }
-        })
-        .collect::<Vec<_>>();
-
-    let mut shader_infos = Vec::new();
-    for path in shader_paths {
-        let file_stem = path.file_stem().unwrap().to_str().unwrap().to_owned();
-        let source = fs::read_to_string(&path)?;
-        shader_infos.push(ShaderInfo {
-            name: file_stem,
-            wgsl_source: source,
-        });
-    }
-    shader_infos.sort_by(|a, b| a.name.cmp(&b.name));
-    Ok(shader_infos)
-}
diff --git a/sparse_strips/vello_sparse_shaders/src/types.rs b/sparse_strips/vello_sparse_shaders/src/types.rs
index 7c8c632..8071485 100644
--- a/sparse_strips/vello_sparse_shaders/src/types.rs
+++ b/sparse_strips/vello_sparse_shaders/src/types.rs
@@ -77,7 +77,7 @@
     /// identifier mappings are also generated from the wgsl identifier to the compiled naga
     /// identifier.
     pub(crate) fn to_generated_code(&self, shader_name: &str) -> String {
-        let mut code = format!("/// Compiled glsl for `{shader_name}.wgsl`\n");
+        let mut code = format!("/// Compiled GLSL for `{shader_name}.wesl`.\n");
         code.push_str(&format!("pub mod {shader_name} {{\n"));
         code.push_str(r#"    #![allow(missing_docs, reason="No metadata to generate precise documentation for generated code.")]"#);
         code.push_str("\n\n");
diff --git a/sparse_strips/vello_sparse_shaders/wesl.toml b/sparse_strips/vello_sparse_shaders/wesl.toml
new file mode 100644
index 0000000..cf8bf37
--- /dev/null
+++ b/sparse_strips/vello_sparse_shaders/wesl.toml
@@ -0,0 +1,3 @@
+edition = "2026_pre"
+root = "./shaders"
+include = ["./shaders/**/*.wesl"]