From f54620f1c5072c0cd738cc540347e2be196f4599 Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Fri, 29 Aug 2025 16:16:49 +1200 Subject: [PATCH 01/22] added xtask webserver for helping WASM tests --- .cargo/config.toml | 1 + Cargo.lock | 1171 ++++++++++++++++- Cargo.toml | 8 +- crates/example-wasm/Cargo.toml | 15 +- crates/example-wasm/index.html | 4 + crates/example-wasm/src/event.rs | 1 - crates/example-wasm/src/lib.rs | 137 +- .../example-wasm/src/req_animation_frame.rs | 80 ++ crates/example/src/camera.rs | 15 +- crates/example/src/lib.rs | 9 +- crates/example/src/main.rs | 4 +- crates/example/src/utils.rs | 2 +- crates/img-diff/src/lib.rs | 26 +- crates/loading-bytes/Cargo.toml | 2 + crates/loading-bytes/src/lib.rs | 194 ++- crates/renderling/Cargo.toml | 5 + crates/renderling/src/bvol.rs | 22 + crates/renderling/src/context.rs | 67 +- crates/renderling/src/cull/cpu.rs | 14 +- crates/renderling/src/draw/cpu.rs | 7 + crates/renderling/src/lib.rs | 2 +- crates/renderling/src/math.rs | 2 +- crates/renderling/src/pbr.rs | 2 +- crates/renderling/src/sdf.rs | 14 +- crates/renderling/src/stage/cpu.rs | 12 + crates/renderling/src/stage/gltf_support.rs | 24 + .../src/stage/gltf_support/anime.rs | 3 +- crates/renderling/src/ui.rs | 2 + crates/renderling/src/ui/cpu.rs | 14 +- crates/renderling/src/ui/cpu/path.rs | 5 +- crates/renderling/src/ui/cpu/text.rs | 2 +- crates/renderling/src/ui/sdf.rs | 23 + crates/renderling/tests/wasm.rs | 122 ++ crates/sandbox/src/main.rs | 5 +- crates/wire-types/Cargo.toml | 7 + crates/wire-types/src/lib.rs | 20 + crates/xtask/Cargo.toml | 7 + crates/xtask/src/main.rs | 35 +- crates/xtask/src/server.rs | 115 ++ 39 files changed, 2018 insertions(+), 182 deletions(-) create mode 100644 crates/example-wasm/src/req_animation_frame.rs create mode 100644 crates/renderling/src/ui/sdf.rs create mode 100644 crates/renderling/tests/wasm.rs create mode 100644 crates/wire-types/Cargo.toml create mode 100644 crates/wire-types/src/lib.rs create mode 100644 crates/xtask/src/server.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 148828e0..b0c3b61d 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,6 +2,7 @@ xtask = "run --package xtask --" shaders = "xtask compile-shaders" linkage = "xtask generate-linkage" +test-wasm = "xtask test-wasm" [build] rustflags = ["--cfg=web_sys_unstable_apis"] diff --git a/Cargo.lock b/Cargo.lock index 3ca1ef03..7e1d7140 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169" +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -341,12 +350,87 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.8.0" @@ -719,7 +803,7 @@ dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", "core-graphics-types 0.1.3", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -753,7 +837,7 @@ checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" dependencies = [ "core-foundation 0.9.4", "core-graphics", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -912,6 +996,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "dlib" version = "0.5.2" @@ -972,6 +1067,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -1109,6 +1213,7 @@ dependencies = [ "wasm-bindgen-test", "web-sys", "wgpu", + "winit", ] [[package]] @@ -1187,6 +1292,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" +[[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" @@ -1218,6 +1329,15 @@ dependencies = [ "yeslogic-fontconfig-sys", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1225,7 +1345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1239,12 +1359,27 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "freetype-sys" version = "0.20.1" @@ -1262,6 +1397,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.31" @@ -1299,6 +1443,46 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -1352,6 +1536,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "gl_generator" version = "0.14.0" @@ -1390,7 +1580,7 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7" dependencies = [ - "base64", + "base64 0.13.1", "byteorder", "gltf-json", "image 0.25.6", @@ -1521,6 +1711,25 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.6.0" @@ -1576,6 +1785,52 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "human-repr" version = "1.1.0" @@ -1588,6 +1843,87 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -1626,6 +1962,113 @@ dependencies = [ "serde_json", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "image" version = "0.24.9" @@ -1726,30 +2169,57 @@ dependencies = [ ] [[package]] -name = "is-terminal" -version = "0.4.16" +name = "io-uring" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "hermit-abi 0.5.2", + "bitflags 2.9.1", + "cfg-if", "libc", - "windows-sys 0.59.0", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.1" +name = "ipnet" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] -name = "itertools" -version = "0.12.1" +name = "iri-string" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" dependencies = [ - "either", -] + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.2", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] [[package]] name = "itoa" @@ -1895,6 +2365,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "litrs" version = "0.4.1" @@ -1908,6 +2384,8 @@ dependencies = [ "async-fs", "js-sys", "send_wrapper", + "serde", + "serde_json", "snafu 0.8.6", "wasm-bindgen", "wasm-bindgen-futures", @@ -2000,6 +2478,12 @@ dependencies = [ "libc", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -2034,12 +2518,18 @@ dependencies = [ "bitflags 2.9.1", "block", "core-graphics-types 0.2.0", - "foreign-types", + "foreign-types 0.5.0", "log", "objc", "paste", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minicov" version = "0.3.7" @@ -2066,6 +2556,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + [[package]] name = "naga" version = "26.0.0" @@ -2093,6 +2594,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2129,6 +2647,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "new_mime_guess" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a2dfb3559d53e90b709376af1c379462f7fb3085a0177deb73e6ea0d99eff4" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "nom" version = "7.1.3" @@ -2439,6 +2967,15 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2451,6 +2988,50 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2587,6 +3168,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "piper" version = "0.2.4" @@ -2693,6 +3280,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3165,9 +3761,13 @@ dependencies = [ "snafu 0.8.6", "spirv-std", "ttf-parser 0.20.0", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", "wgpu", "wgpu-core", "winit", + "wire-types", ] [[package]] @@ -3181,12 +3781,75 @@ dependencies = [ "serde_json", ] +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + [[package]] name = "rgb" version = "0.8.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a457e416a0f90d246a4c3288bd7a25b2304ca727f253f95be383dd17af56be8f" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -3248,6 +3911,39 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -3270,12 +3966,12 @@ dependencies = [ ] [[package]] -name = "sandbox" -version = "0.1.0" +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "renderling", - "wgpu", - "winit", + "windows-sys 0.59.0", ] [[package]] @@ -3303,6 +3999,29 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.26" @@ -3347,6 +4066,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -3356,12 +4085,33 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3487,6 +4237,16 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -3525,6 +4285,12 @@ name = "spirv-std-types" version = "0.9.0" source = "git+https://github.com/LegNeato/rust-gpu.git?rev=425328a#425328a3ac7f1f18db914d24b3d4754bf13bb7ac" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -3555,6 +4321,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -3577,6 +4349,47 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3596,6 +4409,19 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand 2.3.0", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.60.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -3690,6 +4516,80 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -3724,12 +4624,59 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3765,6 +4712,12 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "ttf-parser" version = "0.20.0" @@ -3786,6 +4739,12 @@ dependencies = [ "rand 0.9.1", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -3804,12 +4763,35 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "urlencoding" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -3827,6 +4809,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -3861,6 +4849,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3971,6 +4968,19 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wayland-backend" version = "0.3.10" @@ -4373,6 +5383,17 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -4758,6 +5779,13 @@ dependencies = [ "winapi", ] +[[package]] +name = "wire-types" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -4767,6 +5795,12 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "x11-dl" version = "2.21.0" @@ -4840,10 +5874,17 @@ checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" name = "xtask" version = "0.1.0" dependencies = [ + "axum", "clap 4.5.40", "env_logger", + "image 0.25.6", + "img-diff", "log", + "new_mime_guess", "renderling_build", + "reqwest", + "tokio", + "wire-types", ] [[package]] @@ -4863,6 +5904,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.26" @@ -4883,6 +5948,66 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 818a2189..2618873d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,8 @@ members = [ "crates/loading-bytes", "crates/renderling", "crates/renderling-build", - "crates/sandbox", + "crates/wire-types", + # "crates/sandbox", "crates/xtask" ] @@ -17,6 +18,7 @@ resolver = "2" [workspace.dependencies] assert_approx_eq = "1.1.0" async-channel = "1.8" +axum = "0.8.4" bytemuck = { version = "1.19.0", features = ["derive"] } cfg_aliases = "0.2" clap = { version = "4.5.23", features = ["derive"] } @@ -35,9 +37,11 @@ log = "0.4" loading-bytes = { path = "crates/loading-bytes", version = "0.1.1" } lyon = "1.0.1" naga = { version = "26.0", features = ["spv-in", "wgsl-out", "wgsl-in", "msl-out"] } +new_mime_guess = "4.0.4" pretty_assertions = "1.4.0" proc-macro2 = { version = "1.0", features = ["span-locations"] } quote = "1.0" +reqwest = "0.12.23" rustc-hash = "1.1" serde = {version = "1.0", features = ["derive"]} serde_json = "1.0.117" @@ -47,9 +51,11 @@ snafu = "0.8" spirv-std = { git = "https://github.com/LegNeato/rust-gpu.git", rev = "425328a" } spirv-std-macros = { git = "https://github.com/LegNeato/rust-gpu.git", rev = "425328a" } syn = { version = "2.0.49", features = ["full", "extra-traits", "parsing"] } +tokio = "1.47.1" tracing = "0.1.41" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" +wasm-bindgen-test = "0.3" web-sys = "0.3" winit = { version = "0.30" } wgpu = { version = "26.0" } diff --git a/crates/example-wasm/Cargo.toml b/crates/example-wasm/Cargo.toml index 94a8867f..988fd3c6 100644 --- a/crates/example-wasm/Cargo.toml +++ b/crates/example-wasm/Cargo.toml @@ -13,12 +13,13 @@ console_log = "^0.2" console_error_panic_hook = "^0.1" example = { path = "../example" } fern = "0.6" -futures-lite = { workspace = true } -gltf = { workspace = true } -log = { workspace = true } -renderling = { path = "../renderling", features = ["wasm"] } -wasm-bindgen = { workspace = true } -wasm-bindgen-futures = { workspace = true } +futures-lite.workspace = true +gltf.workspace = true +log.workspace = true +renderling = { path = "../renderling", features = ["wasm", "winit"] } +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true wasm-bindgen-test = "^0.3" web-sys = { workspace = true, features = ["Document", "Element", "Event", "HtmlCanvasElement", "HtmlElement", "Window"] } -wgpu = { workspace = true } +wgpu.workspace = true +winit.workspace = true diff --git a/crates/example-wasm/index.html b/crates/example-wasm/index.html index 24dba6a1..64064e1c 100644 --- a/crates/example-wasm/index.html +++ b/crates/example-wasm/index.html @@ -63,6 +63,10 @@ margin: 0 0.25em 0.5em 0.25em; } + + + +
diff --git a/crates/example-wasm/src/event.rs b/crates/example-wasm/src/event.rs index 5384221c..1aa2554d 100644 --- a/crates/example-wasm/src/event.rs +++ b/crates/example-wasm/src/event.rs @@ -31,7 +31,6 @@ impl Drop for WebCallback { closure.as_ref().unchecked_ref(), ) .unwrap(); - log::trace!("dropping event {}", self.name); } } } diff --git a/crates/example-wasm/src/lib.rs b/crates/example-wasm/src/lib.rs index 33e879ef..cbc9c087 100644 --- a/crates/example-wasm/src/lib.rs +++ b/crates/example-wasm/src/lib.rs @@ -1,9 +1,14 @@ use futures_lite::StreamExt; -use renderling::Context; +use glam::{Vec2, Vec3, Vec4}; +use renderling::{prelude::*, ui::prelude::*}; use wasm_bindgen::prelude::*; use web_sys::HtmlCanvasElement; mod event; +mod req_animation_frame; + +// const HDR_IMAGE_BYTES: &[u8] = include_bytes!("../../../img/hdr/helipad.hdr"); +// const GLTF_FOX_BYTES: &[u8] = include_bytes!("../../../gltf/Fox.glb"); fn surface_from_canvas(_canvas: HtmlCanvasElement) -> Option> { #[cfg(target_arch = "wasm32")] @@ -16,14 +21,58 @@ fn surface_from_canvas(_canvas: HtmlCanvasElement) -> Option, + // text: UiText, +} + +impl App { + fn tick(&self) { + let frame = self.ctx.get_next_frame().unwrap(); + self.ui.render(&frame.view()); + // // self.stage.render(&frame.view()); + // let mut encoder = self + // .ctx + // .get_device() + // .create_command_encoder(&Default::default()); + // { + // let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + // color_attachments: &[Some(wgpu::RenderPassColorAttachment { + // view: &frame.view(), + // depth_slice: None, + // resolve_target: None, + // ops: wgpu::Operations { + // load: wgpu::LoadOp::Clear(wgpu::Color::RED), + // store: wgpu::StoreOp::Store, + // }, + // })], + // depth_stencil_attachment: None, + // ..Default::default() + // }); + // render_pass. + // } + + frame.present(); + self.ctx + .get_device() + .poll(wgpu::PollType::Wait) + .expect_throw("Error polling"); + } +} + #[wasm_bindgen(start)] pub async fn main() { std::panic::set_hook(Box::new(console_error_panic_hook::hook)); fern::Dispatch::new() - .level(log::LevelFilter::Trace) - .level_for("wgpu", log::LevelFilter::Error) - .level_for("naga", log::LevelFilter::Error) - .level_for("renderling", log::LevelFilter::Debug) + .level(log::LevelFilter::Info) + .level_for("wgpu", log::LevelFilter::Warn) + .level_for("naga", log::LevelFilter::Trace) + .level_for("renderling::draw", log::LevelFilter::Trace) .chain(fern::Output::call(console_log::log)) .apply() .unwrap(); @@ -31,32 +80,80 @@ pub async fn main() { log::info!("Starting example-wasm"); let dom_window = web_sys::window().unwrap(); - let ww = dom_window.inner_width().unwrap().as_f64().unwrap() as u32; - let wh = dom_window.inner_height().unwrap().as_f64().unwrap() as u32; let dom_doc = dom_window.document().unwrap(); - let viewport_canvas = dom_doc + let canvas = dom_doc .query_selector("main canvas") .unwrap() .unwrap() .dyn_into::() .unwrap(); - viewport_canvas.set_width(ww); - viewport_canvas.set_height(wh); + canvas.set_width(800); + canvas.set_height(600); - let surface = surface_from_canvas(viewport_canvas.clone()).unwrap(); - let ctx = Context::try_from_raw_window(ww, wh, None, surface) + let surface = surface_from_canvas(canvas.clone()).unwrap(); + let ctx = Context::try_new_with_surface(800, 600, None, surface) .await .unwrap(); - let app = example::App::new(&ctx, example::camera::CameraControl::Turntable); - let window_resize = event::event_stream("resize", &dom_window); - let mut all_events = window_resize; + let ui = ctx.new_ui(); + let path = ui + .new_path() + .with_circle(Vec2::splat(100.0), 20.0) + .with_fill_color(Vec4::new(1.0, 1.0, 0.0, 1.0)) + .fill(); + // let _ = ui + // .load_font("Recursive Mn Lnr St Med Nerd Font Complete.ttf") + // .await + // .expect_throw("Could not load font"); + // let text = ui + // .new_text() + // .with_color( + // // white + // Vec4::ONE, + // ) + // .with_section(Section::default().add_text(Text::new("WASM example").with_scale(24.0))) + // .build(); - while let Some(event) = all_events.next().await { - log::trace!("event: {event:#?}"); - let frame = ctx.get_next_frame().unwrap(); - app.stage.render(&frame.view()); - frame.present(); + // let stage = ctx + // .new_stage() + // .with_background_color( + // // black + // // Vec3::ZERO.extend(1.0), + // Vec4::new(1.0, 0.0, 0.0, 1.0), + // ) + // .with_lighting(false); + + // let skybox = stage.new_skybox_from_bytes(HDR_IMAGE_BYTES).unwrap(); + // stage.set_skybox(skybox); + + // let fox = stage.load_gltf_document_from_bytes(GLTF_FOX_BYTES).unwrap(); + // log::info!("fox aabb: {:?}", fox.bounding_volume()); + + // let camera = stage.new_camera(Camera::default_perspective(800.0, 600.0)); + + let app = App { + ctx, + ui, + path, + // stage, + // doc: fox, + // camera, + // text, + }; + app.tick(); + + // let mut app = example::App::new(&ctx, example::camera::CameraControl::Turntable); + // app.load_hdr_skybox(HDR_IMAGE_BYTES.to_vec()); + // app.load_default_model(); + // app.tick(); + // app.render(&ctx); + + // let window_resize = event::event_stream("resize", &dom_window); + // let mut all_events = window_resize; + + loop { + let _ = req_animation_frame::next_animation_frame().await; + app.tick(); } } diff --git a/crates/example-wasm/src/req_animation_frame.rs b/crates/example-wasm/src/req_animation_frame.rs new file mode 100644 index 00000000..5f5b519a --- /dev/null +++ b/crates/example-wasm/src/req_animation_frame.rs @@ -0,0 +1,80 @@ +//! Request animation frame helpers, taken from [mogwai](https://crates.io/crates/mogwai). +use std::{cell::RefCell, rc::Rc}; + +use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt}; + +fn req_animation_frame(f: &Closure) { + web_sys::window() + .expect_throw("could not get window") + .request_animation_frame(f.as_ref().unchecked_ref()) + .expect_throw("should register `requestAnimationFrame` OK"); +} + +/// Sets a static rust closure to be called with `window.requestAnimationFrame`. +/// +/// The static rust closure takes one parameter which is +/// a timestamp representing the number of milliseconds since the application's +/// load. See +/// for more info. +fn request_animation_frame(mut f: impl FnMut(JsValue) + 'static) { + let wrapper = Rc::new(RefCell::new(None)); + let callback = Box::new({ + let wrapper = wrapper.clone(); + move |jsval| { + f(jsval); + wrapper.borrow_mut().take(); + } + }) as Box; + let closure: Closure = Closure::wrap(callback); + *wrapper.borrow_mut() = Some(closure); + req_animation_frame(wrapper.borrow().as_ref().unwrap_throw()); +} + +#[derive(Clone, Default)] +#[expect(clippy::type_complexity, reason = "not too complex")] +struct NextFrame { + closure: Rc>>>, + ts: Rc>>, + waker: Rc>>, +} + +impl std::future::Future for NextFrame { + type Output = f64; + + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + if let Some(ts) = self.ts.borrow_mut().take() { + std::task::Poll::Ready(ts) + } else { + *self.waker.borrow_mut() = Some(cx.waker().clone()); + std::task::Poll::Pending + } + } +} + +/// Creates a future that will resolve on the next animation frame. +/// +/// The future's output is a timestamp representing the number of +/// milliseconds since the application's load. +/// See +/// for more info. +pub fn next_animation_frame() -> impl std::future::Future { + // https://rustwasm.github.io/wasm-bindgen/examples/request-animation-frame.html#srclibrs + let frame = NextFrame::default(); + + *frame.closure.borrow_mut() = Some(Closure::wrap(Box::new({ + let frame = frame.clone(); + move |ts_val: JsValue| { + *frame.ts.borrow_mut() = Some(ts_val.as_f64().unwrap_or(0.0)); + if let Some(waker) = frame.waker.borrow_mut().take() { + waker.wake(); + } + } + }) as Box)); + + req_animation_frame(frame.closure.borrow().as_ref().unwrap_throw()); + + frame +} diff --git a/crates/example/src/camera.rs b/crates/example/src/camera.rs index 19700b88..785b365e 100644 --- a/crates/example/src/camera.rs +++ b/crates/example/src/camera.rs @@ -2,11 +2,8 @@ use std::str::FromStr; use craballoc::prelude::Hybrid; -use renderling::{ - bvol::Aabb, - camera::Camera, - math::{Mat4, Quat, UVec2, Vec2, Vec3}, -}; +use renderling::prelude::glam::{Mat4, Quat, UVec2, Vec2, Vec3}; +use renderling::{bvol::Aabb, camera::Camera}; use winit::{event::KeyEvent, keyboard::Key}; const RADIUS_SCROLL_DAMPENING: f32 = 0.001; @@ -198,8 +195,12 @@ impl CameraController for WasdMouseCameraController { } fn update_camera(&self, UVec2 { x: w, y: h }: UVec2, camera: &Hybrid) { - let camera_rotation = - Quat::from_euler(renderling::math::EulerRot::XYZ, self.phi, self.theta, 0.0); + let camera_rotation = Quat::from_euler( + renderling::prelude::glam::EulerRot::XYZ, + self.phi, + self.theta, + 0.0, + ); let projection = Mat4::perspective_infinite_rh(std::f32::consts::FRAC_PI_4, w as f32 / h as f32, 0.01); let view = Mat4::from_quat(camera_rotation) * Mat4::from_translation(-self.position); diff --git a/crates/example/src/lib.rs b/crates/example/src/lib.rs index 0592840b..46784893 100644 --- a/crates/example/src/lib.rs +++ b/crates/example/src/lib.rs @@ -6,12 +6,13 @@ use std::{ }; use craballoc::prelude::{GpuArray, Hybrid}; +use glam::{Mat4, UVec2, Vec2, Vec3, Vec4}; use renderling::{ atlas::AtlasImage, bvol::{Aabb, BoundingSphere}, camera::Camera, light::{AnalyticalLight, DirectionalLightDescriptor}, - math::{Mat4, UVec2, Vec2, Vec3, Vec4}, + prelude::*, skybox::Skybox, stage::{Animator, GltfDocument, Renderlet, Stage, Vertex}, ui::{FontArc, Section, Text, Ui, UiPath, UiText}, @@ -201,10 +202,12 @@ impl App { } pub fn render(&self, ctx: &Context) { + log::info!("render"); let frame = ctx.get_next_frame().unwrap(); self.stage.render(&frame.view()); self.ui.ui.render(&frame.view()); frame.present(); + log::info!("render done"); } pub fn update_view(&mut self) { @@ -212,7 +215,8 @@ impl App { .update_camera(self.stage.get_size(), &self.camera); } - fn load_hdr_skybox(&mut self, bytes: Vec) { + pub fn load_hdr_skybox(&mut self, bytes: Vec) { + log::info!("loading skybox"); let img = AtlasImage::from_hdr_bytes(&bytes).unwrap(); let skybox = Skybox::new(self.stage.runtime(), img); self.skybox_image_bytes = Some(bytes); @@ -220,6 +224,7 @@ impl App { } pub fn load_default_model(&mut self) { + log::info!("loading default model"); let mut min = Vec3::splat(f32::INFINITY); let mut max = Vec3::splat(f32::NEG_INFINITY); diff --git a/crates/example/src/main.rs b/crates/example/src/main.rs index be2e603c..af18313e 100644 --- a/crates/example/src/main.rs +++ b/crates/example/src/main.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use clap::Parser; use example::{camera::CameraControl, App}; use renderling::{ - math::{UVec2, Vec2}, + prelude::glam::{UVec2, Vec2}, Context, }; use winit::{application::ApplicationHandler, event::WindowEvent, window::WindowAttributes}; @@ -86,7 +86,7 @@ impl ApplicationHandler for OuterApp { let window = Arc::new(event_loop.create_window(attributes).unwrap()); // Set up a new renderling context - let ctx = Context::try_from_window(None, window.clone()).unwrap(); + let ctx = futures_lite::future::block_on(Context::from_winit_window(None, window.clone())); let mut app = App::new(&ctx, self.cli.camera_control); if let Some(file) = self.cli.model.as_ref() { log::info!("loading model '{file}'"); diff --git a/crates/example/src/utils.rs b/crates/example/src/utils.rs index 422b1914..46ce02b7 100644 --- a/crates/example/src/utils.rs +++ b/crates/example/src/utils.rs @@ -73,7 +73,7 @@ impl winit::application::ApplicationHandler for TestApp { ) .unwrap(), ); - let ctx = Context::try_from_window(None, window.clone()).unwrap(); + let ctx = futures_lite::future::block_on(Context::from_winit_window(None, window.clone())); let mut app = T::new(event_loop, window, &ctx); app.resumed(event_loop); self.inner = Some(InnerTestApp { app, ctx }); diff --git a/crates/img-diff/src/lib.rs b/crates/img-diff/src/lib.rs index efbd3b9f..83509883 100644 --- a/crates/img-diff/src/lib.rs +++ b/crates/img-diff/src/lib.rs @@ -2,7 +2,7 @@ use glam::{Vec3, Vec4, Vec4Swizzles}; use image::{DynamicImage, Luma, Rgb, Rgb32FImage, Rgba32FImage}; use snafu::prelude::*; -use std::path::Path; +use std::{path::Path, sync::LazyLock}; const TEST_IMG_DIR: &str = concat!(std::env!("CARGO_WORKSPACE_DIR"), "test_img"); const TEST_OUTPUT_DIR: &str = concat!(std::env!("CARGO_WORKSPACE_DIR"), "test_output"); @@ -138,7 +138,7 @@ pub fn assert_eq_cfg( lhs: impl Into, rhs: impl Into, cfg: DiffCfg, -) { +) -> Result<(), String> { let lhs = lhs.into(); let lhs = lhs.into_rgba32f(); let rhs = rhs.into().into_rgba32f(); @@ -149,7 +149,7 @@ pub fn assert_eq_cfg( } = cfg; let results = match get_results(&lhs, &rhs, pixel_threshold) { Ok(maybe_diff) => maybe_diff, - Err(e) => panic!("Asserting {filename} failed: {e}"), + Err(e) => return Err(format!("Asserting {filename} failed: {e}")), }; if let Some(DiffResults { num_pixels: diffs, @@ -168,7 +168,7 @@ pub fn assert_eq_cfg( let percent_diff = diffs as f32 / (lhs.width() * lhs.height()) as f32; println!("{filename}'s image is {percent_diff} different (threshold={image_threshold})"); if percent_diff < image_threshold { - return; + return Ok(()); } let mut dir = Path::new(TEST_OUTPUT_DIR).join(test_name.unwrap_or(filename)); @@ -192,28 +192,38 @@ pub fn assert_eq_cfg( mask_image .save_with_format(&mask, image::ImageFormat::Png) .expect("can't save diff mask"); - panic!( + Err(format!( "{} has >= {} differences above the threshold\nexpected: {}\nseen: {}\ndiff: {}", filename, diffs, expected.display(), seen.display(), diff.display() - ); + )) + } else { + Ok(()) } } pub fn assert_eq(filename: &str, lhs: impl Into, rhs: impl Into) { - assert_eq_cfg(filename, lhs, rhs, DiffCfg::default()) + assert_eq_cfg(filename, lhs, rhs, DiffCfg::default()).unwrap() } -pub fn assert_img_eq_cfg(filename: &str, seen: impl Into, cfg: DiffCfg) { +pub fn assert_img_eq_cfg_result( + filename: &str, + seen: impl Into, + cfg: DiffCfg, +) -> Result<(), String> { let path = Path::new(TEST_IMG_DIR).join(filename); let lhs = image::open(&path) .unwrap_or_else(|e| panic!("can't open expected image '{}': {e}", path.display(),)); assert_eq_cfg(filename, lhs, seen, cfg) } +pub fn assert_img_eq_cfg(filename: &str, seen: impl Into, cfg: DiffCfg) { + assert_img_eq_cfg_result(filename, seen, cfg).unwrap() +} + pub fn assert_img_eq(filename: &str, seen: impl Into) { assert_img_eq_cfg(filename, seen, DiffCfg::default()) } diff --git a/crates/loading-bytes/Cargo.toml b/crates/loading-bytes/Cargo.toml index 572fcce8..05646d5e 100644 --- a/crates/loading-bytes/Cargo.toml +++ b/crates/loading-bytes/Cargo.toml @@ -15,6 +15,8 @@ readme = "README.md" async-fs = "1.6" js-sys = "0.3" send_wrapper = {workspace=true} +serde.workspace = true +serde_json.workspace = true snafu = {workspace = true} wasm-bindgen = {workspace = true} wasm-bindgen-futures = {workspace = true} diff --git a/crates/loading-bytes/src/lib.rs b/crates/loading-bytes/src/lib.rs index fdaef2eb..8aafe561 100644 --- a/crates/loading-bytes/src/lib.rs +++ b/crates/loading-bytes/src/lib.rs @@ -1,14 +1,49 @@ //! Abstraction over loading bytes on WASM or other. use snafu::prelude::*; +use wasm_bindgen::UnwrapThrowExt; -/// Enumeration of all errors this library may result in. #[derive(Debug, Snafu)] -pub enum LoadingBytesError { - #[snafu(display("loading '{path}' by WASM error: {msg:#?}"))] - Wasm { +pub enum WasmError { + #[snafu(display("Could not create request to load '{path}': {msg:#?}"))] + CreateRequest { + path: String, + msg: send_wrapper::SendWrapper, + }, + + #[snafu(display("Fetch failed to load '{path}' by WASM error: {msg:#?}"))] + Fetch { path: String, msg: send_wrapper::SendWrapper, }, + + #[snafu(display("Fetching '{path}' returned something that was not a Response: {msg:#?}"))] + NotAResponse { + path: String, + msg: send_wrapper::SendWrapper, + }, + + #[snafu(display("Could not get response array buffer '{path}': {msg:#?}"))] + Array { + path: String, + msg: send_wrapper::SendWrapper, + }, + + #[snafu(display("Could not get buffer from array '{path}': {msg:#?}"))] + Buffer { + path: String, + msg: send_wrapper::SendWrapper, + }, + + #[snafu(display("{other}"))] + Other { other: String }, +} + +/// Enumeration of all errors this library may result in. +#[derive(Debug, Snafu)] +pub enum LoadingBytesError { + #[snafu(display("{source}"))] + Wasm { source: WasmError }, + #[snafu(display("loading '{path}' by filesystem from CWD '{}' error: {source}", cwd.display()))] Fs { path: String, @@ -17,50 +52,135 @@ pub enum LoadingBytesError { }, } -/// Load the file at the given url fragment or path and return it as a vector of bytes, if -/// possible. -pub async fn load(path: &str) -> Result, LoadingBytesError> { - #[cfg(target_arch = "wasm32")] - { - use wasm_bindgen::JsCast; +impl From for LoadingBytesError { + fn from(value: WasmError) -> Self { + LoadingBytesError::Wasm { source: value } + } +} + +pub async fn load_wasm(path: &str) -> Result, WasmError> { + use wasm_bindgen::JsCast; - let path = path.to_string(); - let mut opts = web_sys::RequestInit::new(); - opts.method("GET"); - let request = web_sys::Request::new_with_str_and_init(&path, &opts).map_err(|msg| { - LoadingBytesError::Wasm { + let path = path.to_string(); + let opts = web_sys::RequestInit::new(); + opts.set_method("GET"); + let request = web_sys::Request::new_with_str_and_init(&path, &opts).map_err(|msg| { + CreateRequestSnafu { + path: path.clone(), + msg: send_wrapper::SendWrapper::new(msg), + } + .build() + })?; + let window = web_sys::window().unwrap(); + let resp_value = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request)) + .await + .map_err(|msg| { + FetchSnafu { path: path.clone(), msg: send_wrapper::SendWrapper::new(msg), } + .build() })?; - let window = web_sys::window().unwrap(); - let resp_value = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request)) - .await - .map_err(|msg| LoadingBytesError::Wasm { + let resp: web_sys::Response = resp_value.dyn_into().map_err(|msg| { + NotAResponseSnafu { + path: path.clone(), + msg: send_wrapper::SendWrapper::new(msg), + } + .build() + })?; + let array_promise = resp.array_buffer().map_err(|msg| { + ArraySnafu { + path: path.clone(), + msg: send_wrapper::SendWrapper::new(msg), + } + .build() + })?; + let buffer = wasm_bindgen_futures::JsFuture::from(array_promise) + .await + .map_err(|msg| { + BufferSnafu { path: path.clone(), msg: send_wrapper::SendWrapper::new(msg), - })?; - let resp: web_sys::Response = - resp_value - .dyn_into() - .map_err(|msg| LoadingBytesError::Wasm { - path: path.clone(), - msg: send_wrapper::SendWrapper::new(msg), - })?; - let array_promise = resp.array_buffer().map_err(|msg| LoadingBytesError::Wasm { + } + .build() + })?; + assert!(buffer.is_instance_of::()); + let array: js_sys::Uint8Array = js_sys::Uint8Array::new(&buffer); + let mut bytes: Vec = vec![0; array.length() as usize]; + array.copy_to(&mut bytes); + Ok(bytes) +} + +pub async fn post_json_wasm( + path: &str, + data: &str, +) -> Result { + use js_sys::JsString; + use wasm_bindgen::JsCast; + + let path = path.to_string(); + let opts = web_sys::RequestInit::new(); + opts.set_method("POST"); + let headers = js_sys::Object::new(); + js_sys::Reflect::set( + &headers, + &JsString::from("content-type"), + &JsString::from("application/json"), + ) + .unwrap(); + opts.set_headers(&headers); + let body = js_sys::JsString::from(data); + opts.set_body(&body.into()); + let request = web_sys::Request::new_with_str_and_init(&path, &opts).map_err(|msg| { + CreateRequestSnafu { path: path.clone(), msg: send_wrapper::SendWrapper::new(msg), - })?; - let buffer = wasm_bindgen_futures::JsFuture::from(array_promise) - .await - .map_err(|msg| LoadingBytesError::Wasm { + } + .build() + })?; + let window = web_sys::window().unwrap(); + let resp_value = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request)) + .await + .map_err(|msg| { + FetchSnafu { path: path.clone(), msg: send_wrapper::SendWrapper::new(msg), - })?; - assert!(buffer.is_instance_of::()); - let array: js_sys::Uint8Array = js_sys::Uint8Array::new(&buffer); - let mut bytes: Vec = vec![0; array.length() as usize]; - array.copy_to(&mut bytes); + } + .build() + })?; + let resp: web_sys::Response = resp_value.dyn_into().map_err(|msg| { + NotAResponseSnafu { + path: path.clone(), + msg: send_wrapper::SendWrapper::new(msg), + } + .build() + })?; + + snafu::ensure!( + resp.ok(), + OtherSnafu { + other: wasm_bindgen_futures::JsFuture::from(resp.text().unwrap()) + .await + .unwrap() + .as_string() + .unwrap() + } + ); + + let value = wasm_bindgen_futures::JsFuture::from(resp.text().unwrap_throw()) + .await + .unwrap_throw(); + let s = value.as_string().expect_throw(&format!("{value:#?}")); + let t = serde_json::from_str::(&s).unwrap_throw(); + Ok(t) +} + +/// Load the file at the given url fragment or path and return it as a vector of bytes, if +/// possible. +pub async fn load(path: &str) -> Result, LoadingBytesError> { + #[cfg(target_arch = "wasm32")] + { + let bytes = load_wasm(path).await?; Ok(bytes) } #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/renderling/Cargo.toml b/crates/renderling/Cargo.toml index c2fc3152..9469a1e7 100644 --- a/crates/renderling/Cargo.toml +++ b/crates/renderling/Cargo.toml @@ -30,6 +30,7 @@ ui = ["dep:glyph_brush", "dep:loading-bytes", "dep:lyon"] wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"] debug-slab = [] light-tiling-stats = [ "dep:plotters" ] +test-helpers = [] [build-dependencies] cfg_aliases.workspace = true @@ -86,8 +87,12 @@ icosahedron = "0.1" img-diff = { path = "../img-diff" } naga.workspace = true ttf-parser = "0.20.0" +wasm-bindgen.workspace = true +wasm-bindgen-test.workspace = true +web-sys.workspace = true wgpu-core.workspace = true winit.workspace = true +wire-types = { path = "../wire-types" } [target.'cfg(not(target_arch = "spirv"))'.dev-dependencies] glam = { workspace = true, features = ["std", "debug-glam-assert"] } diff --git a/crates/renderling/src/bvol.rs b/crates/renderling/src/bvol.rs index a63236b9..5d8339fc 100644 --- a/crates/renderling/src/bvol.rs +++ b/crates/renderling/src/bvol.rs @@ -141,6 +141,14 @@ impl Aabb { self.min == self.max } + /// Returns the union of the two [`Aabbs`]. + pub fn union(a: Self, b: Self) -> Self { + Aabb { + min: a.min.min(a.max).min(b.min).min(b.max), + max: a.max.max(a.min).max(b.max).max(b.min), + } + } + /// Determines whether this `Aabb` can be seen by `camera` after being /// transformed by `transform`. pub fn is_outside_camera_view(&self, camera: &Camera, transform: Transform) -> bool { @@ -720,4 +728,18 @@ mod test { assert!(a.intersects_aabb(&b)); assert!(b.intersects_aabb(&a)); } + + #[test] + fn aabb_union() { + let a = Aabb::new(Vec3::splat(4.0), Vec3::splat(5.0)); + let b = Aabb::new(Vec3::ZERO, Vec3::ONE); + let c = Aabb::union(a, b); + assert_eq!( + Aabb { + min: Vec3::ZERO, + max: Vec3::splat(5.0) + }, + c + ); + } } diff --git a/crates/renderling/src/context.rs b/crates/renderling/src/context.rs index 459270cd..7860925a 100644 --- a/crates/renderling/src/context.rs +++ b/crates/renderling/src/context.rs @@ -12,6 +12,7 @@ use snafu::prelude::*; use crate::{ stage::Stage, texture::{BufferDimensions, CopiedTextureBuffer, Texture, TextureError}, + ui::Ui, }; enum RenderTargetInner { @@ -115,12 +116,14 @@ async fn adapter( ); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), + power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface, - force_fallback_adapter: false, + force_fallback_adapter: true, }) .await .context(CannotCreateAdaptorSnafu)?; + + log::info!("Adapter selected: {:?}", adapter.get_info()); let info = adapter.get_info(); log::info!( "using adapter: '{}' backend:{:?} driver:'{}'", @@ -147,6 +150,7 @@ async fn device( let unsupported_features = wanted_features.difference(supported_features); if !unsupported_features.is_empty() { log::error!("requested but unsupported features: {unsupported_features:#?}"); + log::warn!("requested and supported features: {supported_features:#?}"); } let limits = adapter.limits(); log::info!("adapter limits: {limits:#?}"); @@ -162,7 +166,7 @@ async fn device( } fn new_instance(backends: Option) -> wgpu::Instance { - log::trace!( + log::info!( "creating instance - available backends: {:#?}", wgpu::Instance::enabled_backend_features() ); @@ -509,7 +513,7 @@ impl Context { Ok(Self::new(target, adapter, device, queue)) } - pub async fn try_from_raw_window( + pub async fn try_new_with_surface( width: u32, height: u32, backends: Option, @@ -522,55 +526,27 @@ impl Context { } #[cfg(feature = "winit")] - pub async fn from_window_async( - backends: Option, - window: Arc, - ) -> Self { - let inner_size = window.inner_size(); - Self::try_from_raw_window(inner_size.width, inner_size.height, backends, window) - .await - .unwrap() - } - - #[cfg(all(feature = "winit", not(target_arch = "wasm32")))] - /// Create a new context from a `winit::window::Window`, blocking until it - /// is created. + /// Create a [`Context`] from an existing [`winit::window::Window`]. /// /// ## Panics - /// Panics if the context cannot be created. - pub fn from_window( + /// Panics if the context could not be created. + pub async fn from_winit_window( backends: Option, window: Arc, ) -> Self { - futures_lite::future::block_on(Self::from_window_async(backends, window)) - } - - #[cfg(not(target_arch = "wasm32"))] - pub fn try_from_raw_window_handle( - window_handle: impl Into>, - width: u32, - height: u32, - backends: Option, - ) -> Result { - futures_lite::future::block_on(Self::try_from_raw_window( - width, - height, - backends, - window_handle, - )) - } - - #[cfg(all(feature = "winit", not(target_arch = "wasm32")))] - pub fn try_from_window( - backends: Option, - window: Arc, - ) -> Result { let inner_size = window.inner_size(); - Self::try_from_raw_window_handle(window, inner_size.width, inner_size.height, backends) + Self::try_new_with_surface(inner_size.width, inner_size.height, backends, window) + .await + .unwrap() } /// Create a new headless renderer. /// + /// Immediately blocks on [`Context::try_new_headless`]. + /// + /// ## Note + /// Only available on native. + /// /// ## Panics /// This function will panic if an adapter cannot be found. For example this /// would happen on machines without a GPU. @@ -754,4 +730,9 @@ impl Context { pub fn new_stage(&self) -> Stage { Stage::new(self) } + + /// Create and return a new [`Ui`] renderer. + pub fn new_ui(&self) -> Ui { + Ui::new(self) + } } diff --git a/crates/renderling/src/cull/cpu.rs b/crates/renderling/src/cull/cpu.rs index edb86349..c962f7b9 100644 --- a/crates/renderling/src/cull/cpu.rs +++ b/crates/renderling/src/cull/cpu.rs @@ -262,7 +262,7 @@ impl DepthPyramid { } pub fn resize(&mut self, size: UVec2) { - log::info!("resizing depth pyramid to {size}"); + log::trace!("resizing depth pyramid to {size}"); // drop the buffers let mip = self.slab.new_array(vec![]); self.mip_data = vec![]; @@ -303,8 +303,8 @@ impl DepthPyramid { crate::color::f32_to_u8(depth) }) .collect(); - log::info!("min: {min}"); - log::info!("max: {max}"); + log::trace!("min: {min}"); + log::trace!("max: {max}"); let width = size.x >> i; let height = size.y >> i; let image = image::GrayImage::from_raw(width, height, depth_data) @@ -329,12 +329,12 @@ impl ComputeCopyDepth { fn create_bindgroup_layout(device: &wgpu::Device, sample_count: u32) -> wgpu::BindGroupLayout { if sample_count > 1 { - log::info!( + log::trace!( "creating bindgroup layout with {sample_count} multisampled depth for {}", Self::LABEL.unwrap() ); } else { - log::info!( + log::trace!( "creating bindgroup layout without multisampling for {}", Self::LABEL.unwrap() ); @@ -374,10 +374,10 @@ impl ComputeCopyDepth { multisampled: bool, ) -> wgpu::ComputePipeline { let linkage = if multisampled { - log::info!("creating multisampled shader for {}", Self::LABEL.unwrap()); + log::trace!("creating multisampled shader for {}", Self::LABEL.unwrap()); crate::linkage::compute_copy_depth_to_pyramid_multisampled::linkage(device) } else { - log::info!( + log::trace!( "creating shader without multisampling for {}", Self::LABEL.unwrap() ); diff --git a/crates/renderling/src/draw/cpu.rs b/crates/renderling/src/draw/cpu.rs index 08ebc0a4..2ffafe0a 100644 --- a/crates/renderling/src/draw/cpu.rs +++ b/crates/renderling/src/draw/cpu.rs @@ -168,6 +168,8 @@ impl DrawCalls { stage_slab_buffer: &SlabBuffer, depth_texture: &Texture, ) -> Self { + let supported_features = ctx.get_adapter().features(); + log::trace!("supported features: {supported_features:#?}"); let can_use_multi_draw_indirect = ctx.get_adapter().features().contains( wgpu::Features::INDIRECT_FIRST_INSTANCE | wgpu::Features::MULTI_DRAW_INDIRECT, ); @@ -330,6 +332,9 @@ impl DrawCalls { /// Draw into the given `RenderPass` by directly calling each draw. pub fn draw_direct(&self, render_pass: &mut wgpu::RenderPass) { + if self.internal_renderlets.is_empty() { + log::warn!("no internal renderlets, nothing to draw"); + } for ir in self.internal_renderlets.iter() { // UNWRAP: panic on purpose if let Some(hr) = ir.inner.upgrade() { @@ -362,6 +367,8 @@ impl DrawCalls { log::trace!("drawing {num_draw_calls} renderlets using direct"); self.draw_direct(render_pass); } + } else { + log::warn!("zero draw calls"); } } } diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index 0f50d760..d59b9882 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -222,7 +222,7 @@ mod test { use pretty_assertions::assert_eq; use stage::Stage; - #[ctor::ctor] + #[cfg_attr(not(target_arch = "wasm32"), ctor::ctor)] fn init_logging() { let _ = env_logger::builder().is_test(true).try_init(); log::info!("logging is on"); diff --git a/crates/renderling/src/math.rs b/crates/renderling/src/math.rs index 63bf1c88..25029b75 100644 --- a/crates/renderling/src/math.rs +++ b/crates/renderling/src/math.rs @@ -12,7 +12,7 @@ use spirv_std::{ Image, Sampler, }; -pub use glam::*; +use glam::*; pub use spirv_std::num_traits::{clamp, Float, Zero}; pub trait Fetch { diff --git a/crates/renderling/src/pbr.rs b/crates/renderling/src/pbr.rs index 2bbdc0f0..4c20f695 100644 --- a/crates/renderling/src/pbr.rs +++ b/crates/renderling/src/pbr.rs @@ -717,8 +717,8 @@ mod test { use crate::{ atlas::AtlasImage, camera::Camera, - math::{Vec3, Vec4}, pbr::Material, + prelude::glam::{Vec3, Vec4}, stage::Vertex, transform::Transform, }; diff --git a/crates/renderling/src/sdf.rs b/crates/renderling/src/sdf.rs index 80cabc2b..27b5d2ec 100644 --- a/crates/renderling/src/sdf.rs +++ b/crates/renderling/src/sdf.rs @@ -2,17 +2,23 @@ //! //! For more info, see these great articles: //! - - use crabslab::SlabItem; use glam::Vec2; +// use spirv_std::spirv; + +// #[spirv(vertex)] +// pub fn vertex_sdf_circle( +// #[spirv(instance_index)] circle_id: Id, +// #[spirv(vertex_index)] vertex_index: u32, +// ) #[derive(Clone, Copy, SlabItem)] -pub struct Circle { +pub struct CircleDescriptor { pub center: Vec2, pub radius: f32, } -impl Circle { +impl CircleDescriptor { pub fn distance(&self, point: Vec2) -> f32 { let p = point - self.center; p.length() - self.radius @@ -46,7 +52,7 @@ mod test { fn sdf_circle_sanity() { let mut img = image::ImageBuffer::, Vec>::new(32, 32); - let circle = Circle { + let circle = CircleDescriptor { center: Vec2::new(12.0, 12.0), radius: 4.0, }; diff --git a/crates/renderling/src/stage/cpu.rs b/crates/renderling/src/stage/cpu.rs index 1b70cb9a..9fa96930 100644 --- a/crates/renderling/src/stage/cpu.rs +++ b/crates/renderling/src/stage/cpu.rs @@ -2,6 +2,8 @@ //! //! The `Stage` object contains a slab buffer and a render pipeline. //! It is used to stage [`Renderlet`]s for rendering. +#[cfg(test)] +use core::ops::Deref; use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; use craballoc::prelude::*; use crabslab::Id; @@ -768,6 +770,11 @@ impl Stage { &self.runtime().queue } + #[cfg(feature = "test-helpers")] + pub fn hdr_texture(&self) -> impl Deref + '_ { + self.hdr_texture.read().unwrap() + } + pub fn builder(&self) -> RenderletBuilder<'_, ()> { RenderletBuilder::new(self) } @@ -1430,6 +1437,11 @@ impl Stage { Ok(Skybox::new(self.runtime(), hdr)) } + pub fn new_skybox_from_bytes(&self, bytes: &[u8]) -> Result { + let hdr = AtlasImage::from_hdr_bytes(bytes)?; + Ok(Skybox::new(self.runtime(), hdr)) + } + /// Create a new [`NestedTransform`]. pub fn new_nested_transform(&self) -> NestedTransform { NestedTransform::new(self.geometry.slab_allocator()) diff --git a/crates/renderling/src/stage/gltf_support.rs b/crates/renderling/src/stage/gltf_support.rs index 94083efc..169d8bbc 100644 --- a/crates/renderling/src/stage/gltf_support.rs +++ b/crates/renderling/src/stage/gltf_support.rs @@ -9,6 +9,7 @@ use snafu::{OptionExt, ResultExt, Snafu}; use crate::{ atlas::{AtlasError, AtlasImage, AtlasTexture, TextureAddressMode, TextureModes}, + bvol::Aabb, camera::Camera, light::{ AnalyticalLight, DirectionalLightDescriptor, LightStyle, PointLightDescriptor, @@ -1170,6 +1171,29 @@ impl GltfDocument { } nodes.into_iter() } + + /// Returns the bounding volume of this document, if possible. + /// + /// This function will return `None` if this document does not contain meshes. + pub fn bounding_volume(&self) -> Option { + let mut aabbs = vec![]; + for node in self.nodes.iter() { + if let Some(mesh_index) = node.mesh { + let mesh = self.meshes.get(mesh_index)?; + for prim in mesh.primitives.iter() { + let (prim_min, prim_max) = prim.bounding_box; + let prim_aabb = Aabb::new(prim_min, prim_max); + aabbs.push(prim_aabb); + } + } + } + let mut aabbs = aabbs.into_iter(); + let mut aabb = aabbs.next()?; + for next_aabb in aabbs { + aabb = Aabb::union(aabb, next_aabb); + } + Some(aabb) + } } impl Stage { diff --git a/crates/renderling/src/stage/gltf_support/anime.rs b/crates/renderling/src/stage/gltf_support/anime.rs index 0ac9def6..ccdc75e1 100644 --- a/crates/renderling/src/stage/gltf_support/anime.rs +++ b/crates/renderling/src/stage/gltf_support/anime.rs @@ -771,7 +771,8 @@ impl Animator { #[cfg(test)] mod test { - use crate::{camera::Camera, math::Vec3, stage::Animator, Context}; + use crate::{camera::Camera, stage::Animator, Context}; + use glam::Vec3; #[test] fn gltf_simple_animation() { diff --git a/crates/renderling/src/ui.rs b/crates/renderling/src/ui.rs index 3b37eb21..c7622c6e 100644 --- a/crates/renderling/src/ui.rs +++ b/crates/renderling/src/ui.rs @@ -25,3 +25,5 @@ mod cpu; #[cfg(cpu)] pub use cpu::*; + +pub mod sdf; diff --git a/crates/renderling/src/ui/cpu.rs b/crates/renderling/src/ui/cpu.rs index a9cc3af8..454ef0e3 100644 --- a/crates/renderling/src/ui/cpu.rs +++ b/crates/renderling/src/ui/cpu.rs @@ -6,13 +6,13 @@ use crate::{ atlas::AtlasTexture, camera::Camera, geometry::Geometry, - math::{Quat, UVec2, Vec2, Vec3Swizzles, Vec4}, stage::{NestedTransform, Renderlet, Stage}, transform::Transform, Context, }; use craballoc::prelude::{Hybrid, SourceId}; use crabslab::Id; +use glam::{Quat, UVec2, Vec2, Vec3Swizzles, Vec4}; use glyph_brush::ab_glyph; use rustc_hash::FxHashMap; use snafu::{prelude::*, ResultExt}; @@ -105,7 +105,7 @@ impl UiTransform { self.transform .get() .rotation - .to_euler(crate::math::EulerRot::XYZ) + .to_euler(glam::EulerRot::XYZ) .2 } @@ -152,7 +152,8 @@ impl Ui { .with_background_color(Vec4::ONE) .with_lighting(false) .with_bloom(false) - .with_msaa_sample_count(4); + .with_msaa_sample_count(4) + .with_frustum_culling(false); let camera = stage.new_camera(Camera::default_ortho2d(x as f32, y as f32)); Ui { camera, @@ -342,12 +343,7 @@ impl Ui { #[cfg(test)] pub(crate) mod test { - use crate::{color::rgb_hex_color, math::Vec4}; - - #[ctor::ctor] - fn init_logging() { - let _ = env_logger::builder().is_test(true).try_init(); - } + use crate::{color::rgb_hex_color, prelude::glam::Vec4}; pub struct Colors(std::iter::Cycle>); diff --git a/crates/renderling/src/ui/cpu/path.rs b/crates/renderling/src/ui/cpu/path.rs index 4dbe3fa4..6e337097 100644 --- a/crates/renderling/src/ui/cpu/path.rs +++ b/crates/renderling/src/ui/cpu/path.rs @@ -2,12 +2,12 @@ //! //! Path colors are sRGB. use crate::{ - math::{Vec2, Vec3, Vec3Swizzles, Vec4}, pbr::Material, stage::{Renderlet, Vertex}, }; use craballoc::prelude::{GpuArray, Hybrid}; use crabslab::Id; +use glam::{Vec2, Vec3, Vec3Swizzles, Vec4}; use lyon::{ path::traits::PathBuilder, tessellation::{ @@ -524,13 +524,14 @@ impl UiPathBuilder { #[cfg(test)] mod test { use crate::{ - math::{hex_to_vec4, Vec2}, + math::hex_to_vec4, ui::{ test::{cute_beach_palette, Colors}, Ui, }, Context, }; + use glam::Vec2; use super::*; diff --git a/crates/renderling/src/ui/cpu/text.rs b/crates/renderling/src/ui/cpu/text.rs index 56f3b070..027cdb47 100644 --- a/crates/renderling/src/ui/cpu/text.rs +++ b/crates/renderling/src/ui/cpu/text.rs @@ -9,6 +9,7 @@ use std::{ use ab_glyph::Rect; use craballoc::prelude::{GpuArray, Hybrid}; +use glam::{Vec2, Vec4}; use glyph_brush::*; pub use ab_glyph::FontArc; @@ -16,7 +17,6 @@ pub use glyph_brush::{Section, Text}; use crate::{ atlas::AtlasTexture, - math::{Vec2, Vec4}, pbr::Material, stage::{Renderlet, Vertex}, }; diff --git a/crates/renderling/src/ui/sdf.rs b/crates/renderling/src/ui/sdf.rs new file mode 100644 index 00000000..0ccad0ba --- /dev/null +++ b/crates/renderling/src/ui/sdf.rs @@ -0,0 +1,23 @@ +//! 2d signed distance fields. +use glam::Vec2; + +/// Returns the distance to the edge of a circle of radius `r` with center at `p`. +fn distance_circle(p: Vec2, r: f32) -> f32 { + p.length() - r +} + +pub struct Circle { + origin: Vec2, + radius: f32, +} + +impl Circle { + pub fn distance(&self) -> f32 { + distance_circle(self.origin, self.radius) + } +} + +// #[spirv_std::spirv(vertex)] +// pub fn vertex_circle( + +// ) diff --git a/crates/renderling/tests/wasm.rs b/crates/renderling/tests/wasm.rs new file mode 100644 index 00000000..86c961eb --- /dev/null +++ b/crates/renderling/tests/wasm.rs @@ -0,0 +1,122 @@ +//! WASM tests. +#![allow(dead_code)] + +use glam::Vec4; +use image::DynamicImage; +use renderling::prelude::*; +use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; +use web_sys::wasm_bindgen::UnwrapThrowExt; +use wire_types::{Error, PixelType}; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +async fn can_create_headless_ctx() { + let _ctx = renderling::Context::try_new_headless(256, 256, None) + .await + .unwrap_throw(); +} + +#[wasm_bindgen_test] +async fn stage_creation() { + let ctx = renderling::Context::try_new_headless(256, 256, None) + .await + .unwrap_throw(); + let _stage = ctx.new_stage(); +} + +fn image_from_bytes(bytes: &[u8]) -> image::DynamicImage { + image::ImageReader::new(std::io::Cursor::new(bytes)) + .with_guessed_format() + .expect_throw("could not guess format") + .decode() + .expect_throw("could not decode") +} + +async fn load_test_img(path: &str) -> image::DynamicImage { + let result = loading_bytes::load(&format!("http://127.0.0.1:4000/test_img/{path}")).await; + let bytes = match result { + Ok(bytes) => bytes, + Err(e) => panic!("{e}"), + }; + image_from_bytes(&bytes) +} + +async fn assert_img_eq(filename: &str, seen: impl Into) { + let img: DynamicImage = seen.into(); + let width = img.width(); + let height = img.height(); + let (pixel, bytes) = match img { + DynamicImage::ImageRgb8(image_buffer) => (PixelType::Rgb8, image_buffer.to_vec()), + DynamicImage::ImageRgba8(image_buffer) => (PixelType::Rgba8, image_buffer.to_vec()), + _ => panic!("Image type is not yet supported in the WASM tests"), + }; + let wire_data = wire_types::Image { + width, + height, + bytes, + pixel, + }; + let data = serde_json::to_string(&wire_data).unwrap(); + let result = loading_bytes::post_json_wasm::>( + &format!("http://127.0.0.1:4000/assert_img_eq/{filename}"), + &data, + ) + .await + .unwrap(); + + if let Err(Error { description }) = result { + panic!("{description}"); + } +} + +#[wasm_bindgen_test] +async fn can_load_image() { + let _img = load_test_img("jolt.png").await; +} + +#[wasm_bindgen_test] +async fn can_img_diff() { + let a = load_test_img("jolt.png").await; + assert_img_eq("jolt.png", a).await; + + let b = load_test_img("cmy_triangle/hdr.png").await; + assert_img_eq("cmy_triangle/hdr.png", b).await; +} + +#[wasm_bindgen_test] +async fn can_clear_background() { + let ctx = Context::try_new_headless(100, 100, None).await.unwrap(); + let stage = ctx + .new_stage() + .with_background_color(Vec4::new(1.0, 0.0, 0.0, 1.0)); + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let seen = frame.read_image().unwrap(); + assert_img_eq("cmy_triangle/hdr.png", seen).await; +} + +// #[wasm_bindgen_test] +// #[should_panic] +// async fn can_save_wrong_diffs() { +// let img = load_test_img("jolt.png").await; +// assert_img_eq("cmy_triangle/hdr.png", img).await; +// } + +// #[wasm_bindgen_test] +// async fn can_render_hello_triangle() { +// // This is a wasm version of cmy_triangle_sanity +// let ctx = Context::try_new_headless(100, 100, None).await.unwrap(); +// let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); +// let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); +// let _rez = stage +// .builder() +// .with_vertices(renderling::::right_tri_vertices()) +// .build(); + +// let frame = ctx.get_next_frame().unwrap(); +// stage.render(&frame.view()); +// frame.present(); + +// let hdr_img = stage.hdr_texture().read_hdr_image(&ctx).unwrap(); +// } diff --git a/crates/sandbox/src/main.rs b/crates/sandbox/src/main.rs index 36377619..f1ff0465 100644 --- a/crates/sandbox/src/main.rs +++ b/crates/sandbox/src/main.rs @@ -2,7 +2,8 @@ //! //! This program will change on a whim and does not contain anything all that //! useful. -use renderling::{math::UVec2, stage::Stage, Context}; +use glam::UVec2; +use renderling::{stage::Stage, Context}; use std::sync::Arc; use winit::{ dpi::LogicalSize, @@ -59,7 +60,7 @@ impl winit::application::ApplicationHandler for App { }) .with_title("renderling gltf viewer"); let window = Arc::new(event_loop.create_window(attributes).unwrap()); - let ctx = Context::from_window(None, window.clone()); + let ctx = futures_lite::future::block_on(Context::from_winit_window(None, window.clone())); let stage = ctx.new_stage(); self.example = Some(Example { ctx, window, stage }); } diff --git a/crates/wire-types/Cargo.toml b/crates/wire-types/Cargo.toml new file mode 100644 index 00000000..b076c6f8 --- /dev/null +++ b/crates/wire-types/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "wire-types" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde.workspace = true diff --git a/crates/wire-types/src/lib.rs b/crates/wire-types/src/lib.rs new file mode 100644 index 00000000..3fec866b --- /dev/null +++ b/crates/wire-types/src/lib.rs @@ -0,0 +1,20 @@ +/// Supported pixel type. +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub enum PixelType { + Rgb8, + Rgba8, +} + +/// Wire type for an RGB8 image. +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct Image { + pub width: u32, + pub height: u32, + pub bytes: Vec, + pub pixel: PixelType, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct Error { + pub description: String, +} diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml index 45ea47ea..9d265715 100644 --- a/crates/xtask/Cargo.toml +++ b/crates/xtask/Cargo.toml @@ -4,7 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] +axum.workspace = true clap.workspace = true env_logger.workspace = true +image.workspace = true +img-diff = { path = "../img-diff" } log.workspace = true +new_mime_guess.workspace = true renderling_build = { path = "../renderling-build" } +reqwest = { workspace = true, features = ["stream"] } +tokio = { workspace = true, features = ["full"] } +wire-types = { path = "../wire-types" } diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 59e85cc0..6e33bf05 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -1,6 +1,8 @@ //! A build helper for the `renderling` project. use clap::{Parser, Subcommand}; +mod server; + #[derive(Subcommand)] enum Command { /// Compile the `renderling` library into multiple SPIR-V shader entry points. @@ -19,6 +21,10 @@ enum Command { #[clap(long)] from_cargo: bool, }, + /// Run the webdriver proxy server + WebdriverProxy, + /// Compile for WASM and run headless browser tests + TestWasm, } #[derive(Parser)] @@ -29,9 +35,12 @@ struct Cli { subcommand: Command, } -fn main() { +#[tokio::main] +async fn main() { env_logger::builder().init(); + log::info!("running xtask"); + let cli = Cli::parse(); match cli.subcommand { Command::CompileShaders => { @@ -59,5 +68,29 @@ fn main() { let paths = renderling_build::RenderlingPaths::new().unwrap(); paths.generate_linkage(from_cargo, wgsl, only_fn_with_name); } + Command::TestWasm => { + log::info!("testing WASM"); + let _proxy_handle = tokio::spawn(server::serve()); + let mut test_handle = tokio::process::Command::new("wasm-pack") + .args([ + "test", + "--headless", + "--firefox", + "crates/renderling", + "--features", + "wasm", + "--jobs", + "1", + ]) + .spawn() + .unwrap(); + let status = test_handle.wait().await.unwrap(); + if !status.success() { + panic!("Testing WASM failed :("); + } + } + Command::WebdriverProxy => { + server::serve().await; + } } } diff --git a/crates/xtask/src/server.rs b/crates/xtask/src/server.rs new file mode 100644 index 00000000..e24f6527 --- /dev/null +++ b/crates/xtask/src/server.rs @@ -0,0 +1,115 @@ +//! Axum web server for running the webdriver proxy. +//! +//! This proxy server allows the WASM tests to request static assets, +//! as well as report test failures in a (hopefully) nice way. + +use axum::{ + body::{Body, Bytes}, + extract::{Path, Request}, + http::{HeaderMap, StatusCode}, + response::{IntoResponse, Response}, + routing::{any, get, options, post}, + Json, Router, +}; +use image::DynamicImage; +use wire_types::Error; + +pub async fn serve() { + log::info!("starting the webdriver proxy"); + let app = Router::new() + .route("/test_img/{*path}", get(static_file)) + .route("/assert_img_eq/{*filename}", options(accept)) + .route("/assert_img_eq/{*filename}", post(assert_img_eq)) + .route("/{*rest}", any(accept)); + let listener = tokio::net::TcpListener::bind("127.0.0.1:4000") + .await + .unwrap(); + axum::serve(listener, app).await.unwrap(); +} + +async fn static_file(Path(path): Path) -> Result { + log::info!("requested static '{path}'"); + let test_img = std::path::PathBuf::from(std::env!("CARGO_WORKSPACE_DIR")).join("test_img"); + let path = test_img.join(path); + if path.exists() { + let bytes = tokio::fs::read(&path).await.map_err(|e| { + log::error!("could not read path '{path:?}': {e}"); + StatusCode::BAD_REQUEST + })?; + let mime = new_mime_guess::from_path(path); + let mimetype = if let Some(mt) = mime.first() { + mt.to_string() + } else { + "application/octet-stream".to_owned() + }; + let resp = Response::builder() + .status(StatusCode::OK) + .header("content-type", mimetype) + .header("access-control-allow-origin", "*") + .body(Body::from(Bytes::copy_from_slice(&bytes))) + .map_err(|e| { + log::error!("colud not create response: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + Ok(resp) + } else { + log::error!("{path:?} not found"); + Err(StatusCode::NOT_FOUND) + } +} + +async fn assert_img_eq_inner( + filename: &str, + img: wire_types::Image, +) -> Result<(), wire_types::Error> { + let seen = match img.pixel { + wire_types::PixelType::Rgb8 => { + image::RgbImage::from_raw(img.width, img.height, img.bytes).map(DynamicImage::from) + } + wire_types::PixelType::Rgba8 => { + image::RgbaImage::from_raw(img.width, img.height, img.bytes).map(DynamicImage::from) + } + } + .ok_or_else(|| { + let description = "could not construct image".to_owned(); + log::error!("{description}"); + Error { description } + })?; + + img_diff::assert_img_eq_cfg_result(filename, seen, Default::default()).map_err(|description| { + log::error!("{description}"); + Error { description } + }) +} + +async fn assert_img_eq( + headers: HeaderMap, + Path(parts): Path>, + Json(img): Json, +) -> Response { + let filename = parts.join("/"); + log::info!("asserting '{filename}'"); + log::info!("headers: {headers:#?}"); + + let result = assert_img_eq_inner(&filename, img).await; + Response::builder() + .status(StatusCode::OK) + .header("accept", "*/*") + .header("access-control-allow-origin", "*") + .header("access-control-allow-methods", "*") + .header("access-control-allow-headers", "*") + .body(Json(result).into_response().into_body()) + .unwrap() +} + +async fn accept(request: Request) -> Response { + log::info!("accept: {request:#?}"); + Response::builder() + .status(StatusCode::OK) + .header("accept", "*/*") + .header("access-control-allow-origin", "*") + .header("access-control-allow-methods", "*") + .header("access-control-allow-headers", "*") + .body(Body::default()) + .unwrap() +} From 53334405183140dd60db079d5aa45e7a734ea54c Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Sat, 30 Aug 2025 09:10:45 +1200 Subject: [PATCH 02/22] only use futures_lite::future::block_on in tests --- crates/renderling/Cargo.toml | 2 +- crates/renderling/src/atlas/cpu.rs | 40 +++-- crates/renderling/src/bloom/cpu.rs | 8 +- crates/renderling/src/bvol.rs | 6 +- crates/renderling/src/context.rs | 40 +++-- crates/renderling/src/cubemap/cpu.rs | 7 +- crates/renderling/src/cull/cpu.rs | 19 +- crates/renderling/src/lib.rs | 106 +++++++---- crates/renderling/src/light/cpu/test.rs | 28 +-- crates/renderling/src/light/shadow_map.rs | 29 +-- crates/renderling/src/pbr.rs | 5 +- crates/renderling/src/skybox/cpu.rs | 14 +- crates/renderling/src/stage/cpu.rs | 13 +- crates/renderling/src/stage/gltf_support.rs | 33 ++-- .../src/stage/gltf_support/anime.rs | 8 +- crates/renderling/src/texture.rs | 166 ++++++++++++------ crates/renderling/src/ui.rs | 2 +- crates/renderling/src/ui/cpu/path.rs | 15 +- crates/renderling/src/ui/cpu/text.rs | 24 ++- crates/renderling/tests/wasm.rs | 2 +- 20 files changed, 339 insertions(+), 228 deletions(-) diff --git a/crates/renderling/Cargo.toml b/crates/renderling/Cargo.toml index 9469a1e7..9214b4c3 100644 --- a/crates/renderling/Cargo.toml +++ b/crates/renderling/Cargo.toml @@ -59,7 +59,6 @@ craballoc.workspace = true crabslab = { workspace = true, features = ["default"] } dagga = {workspace=true} crunch = "0.5" -futures-lite = {workspace=true} glam = { workspace = true, features = ["std"] } gltf = {workspace = true, optional = true} glyph_brush = {workspace = true, optional = true} @@ -82,6 +81,7 @@ ctor = "0.2.2" env_logger.workspace = true example = { path = "../example" } fastrand = "2.1.1" +futures-lite.workspace = true human-repr = "1.1.0" icosahedron = "0.1" img-diff = { path = "../img-diff" } diff --git a/crates/renderling/src/atlas/cpu.rs b/crates/renderling/src/atlas/cpu.rs index b0a92e36..8a0e1916 100644 --- a/crates/renderling/src/atlas/cpu.rs +++ b/crates/renderling/src/atlas/cpu.rs @@ -436,16 +436,18 @@ impl Atlas { /// ## Panics /// Panics if the pixels read from the GPU cannot be converted into an /// `RgbaImage`. - pub fn atlas_img(&self, runtime: impl AsRef, layer: u32) -> RgbaImage { + pub async fn atlas_img(&self, runtime: impl AsRef, layer: u32) -> RgbaImage { let runtime = runtime.as_ref(); let buffer = self.atlas_img_buffer(runtime, layer); - buffer.into_linear_rgba(&runtime.device).unwrap() + buffer.into_linear_rgba(&runtime.device).await.unwrap() } - pub fn read_images(&self, runtime: impl AsRef) -> Vec { + // It's ok to hold this lock because this is just for testing. + #[allow(clippy::await_holding_lock)] + pub async fn read_images(&self, runtime: impl AsRef) -> Vec { let mut images = vec![]; for i in 0..self.layers.read().unwrap().len() { - images.push(self.atlas_img(runtime.as_ref(), i as u32)); + images.push(self.atlas_img(runtime.as_ref(), i as u32).await); } images } @@ -936,6 +938,7 @@ mod test { material::Materials, pbr::Material, stage::Vertex, + test::BlockOnFuture, transform::Transform, Context, }; @@ -947,8 +950,9 @@ mod test { // Ensures that textures are packed and rendered correctly. fn atlas_uv_mapping() { log::info!("{:?}", std::env::current_dir()); - let ctx = - Context::headless(32, 32).with_default_atlas_texture_size(UVec3::new(1024, 1024, 2)); + let ctx = Context::headless(32, 32) + .block() + .with_default_atlas_texture_size(UVec3::new(1024, 1024, 2)); let stage = ctx .new_stage() .with_background_color(Vec3::splat(0.0).extend(1.0)) @@ -995,7 +999,7 @@ mod test { log::info!("rendering"); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("atlas/uv_mapping.png", img); } @@ -1008,7 +1012,7 @@ mod test { let sheet_h = icon_h * 3; let w = sheet_w * 3 + 2; let h = sheet_h; - let ctx = Context::headless(w, h); + let ctx = Context::headless(w, h).block(); let stage = ctx .new_stage() .with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0)); @@ -1085,7 +1089,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("atlas/uv_wrapping.png", img); } @@ -1098,7 +1102,7 @@ mod test { let sheet_h = icon_h * 3; let w = sheet_w * 3 + 2; let h = sheet_h; - let ctx = Context::headless(w, h); + let ctx = Context::headless(w, h).block(); let stage = ctx .new_stage() .with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0)); @@ -1178,7 +1182,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("atlas/negative_uv_wrapping.png", img); } @@ -1202,8 +1206,9 @@ mod test { #[test] fn can_load_and_read_atlas_texture_array() { // tests that the atlas lays out textures in the way we expect - let ctx = - Context::headless(100, 100).with_default_atlas_texture_size(UVec3::new(512, 512, 2)); + let ctx = Context::headless(100, 100) + .block() + .with_default_atlas_texture_size(UVec3::new(512, 512, 2)); let stage = ctx.new_stage(); let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap(); let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap(); @@ -1213,17 +1218,18 @@ mod test { .set_images([dirt, sandstone, cheetah, texels]) .unwrap(); let materials: &Materials = stage.as_ref(); - let img = materials.atlas().atlas_img(&ctx, 0); + let img = materials.atlas().atlas_img(&ctx, 0).block(); img_diff::assert_img_eq("atlas/array0.png", img); - let img = materials.atlas().atlas_img(&ctx, 1); + let img = materials.atlas().atlas_img(&ctx, 1).block(); img_diff::assert_img_eq("atlas/array1.png", img); } #[test] fn upkeep_trims_the_atlas() { // tests that Atlas::upkeep trims out unused images and repacks the atlas - let ctx = - Context::headless(100, 100).with_default_atlas_texture_size(UVec3::new(512, 512, 2)); + let ctx = Context::headless(100, 100) + .block() + .with_default_atlas_texture_size(UVec3::new(512, 512, 2)); let stage = ctx.new_stage(); let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap(); let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap(); diff --git a/crates/renderling/src/bloom/cpu.rs b/crates/renderling/src/bloom/cpu.rs index fb1d28d7..b8f1d355 100644 --- a/crates/renderling/src/bloom/cpu.rs +++ b/crates/renderling/src/bloom/cpu.rs @@ -701,7 +701,7 @@ impl Bloom { mod test { use glam::Vec3; - use crate::{camera::Camera, Context}; + use crate::{camera::Camera, test::BlockOnFuture, Context}; use super::*; @@ -747,7 +747,7 @@ mod test { fn bloom_sanity() { let width = 256; let height = 128; - let ctx = Context::headless(width, height); + let ctx = Context::headless(width, height).block(); let stage = ctx.new_stage().with_bloom(false); // .with_frustum_culling(false) // .with_occlusion_culling(false); @@ -766,7 +766,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("bloom/without.png", img); frame.present(); @@ -776,7 +776,7 @@ mod test { stage.set_bloom_filter_radius(2.0); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("bloom/with.png", img); } } diff --git a/crates/renderling/src/bvol.rs b/crates/renderling/src/bvol.rs index 5d8339fc..e76345fc 100644 --- a/crates/renderling/src/bvol.rs +++ b/crates/renderling/src/bvol.rs @@ -595,7 +595,7 @@ impl BVol for Aabb { mod test { use glam::{Mat4, Quat}; - use crate::{pbr::Material, stage::Vertex, Context}; + use crate::{pbr::Material, stage::Vertex, test::BlockOnFuture, Context}; use super::*; @@ -649,7 +649,7 @@ mod test { #[test] fn bounding_box_from_min_max() { - let ctx = Context::headless(256, 256); + let ctx = Context::headless(256, 256).block(); let stage = ctx .new_stage() .with_background_color(Vec4::ZERO) @@ -717,7 +717,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("bvol/bounding_box/get_mesh.png", img); } diff --git a/crates/renderling/src/context.rs b/crates/renderling/src/context.rs index 7860925a..3f2f4a08 100644 --- a/crates/renderling/src/context.rs +++ b/crates/renderling/src/context.rs @@ -116,9 +116,9 @@ async fn adapter( ); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, + power_preference: wgpu::PowerPreference::default(), compatible_surface, - force_fallback_adapter: true, + force_fallback_adapter: false, }) .await .context(CannotCreateAdaptorSnafu)?; @@ -378,14 +378,14 @@ impl Frame { /// /// ## Note /// This operation can take a long time, depending on how big the screen is. - pub fn read_image(&self) -> Result { + pub async fn read_image(&self) -> Result { let size = self.get_size(); let buffer = self.copy_to_buffer(size.x, size.y); let is_srgb = self.texture().format().is_srgb(); let img = if is_srgb { - buffer.into_srgba(&self.runtime.device)? + buffer.into_srgba(&self.runtime.device).await? } else { - buffer.into_linear_rgba(&self.runtime.device)? + buffer.into_linear_rgba(&self.runtime.device).await? }; Ok(img) } @@ -399,11 +399,11 @@ impl Frame { /// /// ## Note /// This operation can take a long time, depending on how big the screen is. - pub fn read_srgb_image(&self) -> Result { + pub async fn read_srgb_image(&self) -> Result { let size = self.get_size(); let buffer = self.copy_to_buffer(size.x, size.y); log::trace!("read image has the format: {:?}", buffer.format); - buffer.into_srgba(&self.runtime.device) + buffer.into_srgba(&self.runtime.device).await } /// Read the frame into an image. /// @@ -414,10 +414,10 @@ impl Frame { /// /// ## Note /// This operation can take a long time, depending on how big the screen is. - pub fn read_linear_image(&self) -> Result { + pub async fn read_linear_image(&self) -> Result { let size = self.get_size(); let buffer = self.copy_to_buffer(size.x, size.y); - buffer.into_linear_rgba(&self.runtime.device) + buffer.into_linear_rgba(&self.runtime.device).await } /// If self is `TargetFrame::Surface` this presents the surface frame. @@ -439,7 +439,7 @@ pub(crate) struct GlobalStageConfig { pub(crate) use_compute_culling: bool, } -/// Contains the adapter, device, queue, [`RenderTarget`] and initial atlas sizing. +/// Contains the adapter, device, queue, [`RenderTarget`] and configuration. /// /// A `Context` is created to initialize rendering to a window, canvas or /// texture. @@ -447,7 +447,7 @@ pub(crate) struct GlobalStageConfig { /// ``` /// use renderling::Context; /// -/// let ctx = Context::headless(100, 100); +/// let ctx = futures_lite::future::block_on(Context::headless(100, 100)); /// ``` pub struct Context { runtime: WgpuRuntime, @@ -542,16 +542,22 @@ impl Context { /// Create a new headless renderer. /// - /// Immediately blocks on [`Context::try_new_headless`]. - /// - /// ## Note - /// Only available on native. + /// Immediately proxies to [`Context::try_new_headless`] and unwraps. /// /// ## Panics /// This function will panic if an adapter cannot be found. For example this /// would happen on machines without a GPU. - pub fn headless(width: u32, height: u32) -> Self { - futures_lite::future::block_on(Self::try_new_headless(width, height, None)).unwrap() + pub async fn headless(width: u32, height: u32) -> Self { + let result = Self::try_new_headless(width, height, None).await; + #[cfg(target_arch = "wasm32")] + { + use wasm_bindgen::UnwrapThrowExt; + result.expect_throw("Could not create context") + } + #[cfg(not(target_arch = "wasm32"))] + { + result.expect("Could not create context") + } } pub fn get_size(&self) -> UVec2 { diff --git a/crates/renderling/src/cubemap/cpu.rs b/crates/renderling/src/cubemap/cpu.rs index 09734d32..618a59ba 100644 --- a/crates/renderling/src/cubemap/cpu.rs +++ b/crates/renderling/src/cubemap/cpu.rs @@ -272,6 +272,7 @@ mod test { use crate::{ math::{UNIT_INDICES, UNIT_POINTS}, stage::Vertex, + test::BlockOnFuture, }; use super::*; @@ -280,7 +281,7 @@ mod test { fn hand_rolled_cubemap_sampling() { let width = 256; let height = 256; - let ctx = crate::Context::headless(width, height); + let ctx = crate::Context::headless(width, height).block(); let stage = ctx .new_stage() .with_background_color(Vec4::ZERO) @@ -306,7 +307,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("cubemap/hand_rolled_cubemap_sampling/cube.png", img); frame.present(); @@ -487,6 +488,7 @@ mod test { let img = Texture::read(&ctx, &render_target, 1, 1, 4, 1) .into_image::>(ctx.get_device()) + .block() .unwrap(); let image::Rgba([r, g, b, a]) = img.get_pixel(0, 0); Vec4::new( @@ -522,6 +524,7 @@ mod test { Some(wgpu::Origin3d { x: 0, y: 0, z: i }), ) .into_image::>(ctx.get_device()) + .block() .unwrap(); img_diff::assert_img_eq( diff --git a/crates/renderling/src/cull/cpu.rs b/crates/renderling/src/cull/cpu.rs index c962f7b9..5accb2bd 100644 --- a/crates/renderling/src/cull/cpu.rs +++ b/crates/renderling/src/cull/cpu.rs @@ -674,14 +674,14 @@ mod test { use crate::{ bvol::BoundingSphere, cull::DepthPyramidDescriptor, draw::DrawIndirectArgs, - geometry::Geometry, math::hex_to_vec4, prelude::*, + geometry::Geometry, math::hex_to_vec4, prelude::*, test::BlockOnFuture, }; use crabslab::{GrowableSlab, Slab}; use glam::{Mat4, Quat, UVec2, UVec3, Vec2, Vec3, Vec4}; #[test] fn occlusion_culling_sanity() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); let camera_position = Vec3::new(0.0, 9.0, 9.0); let _camera = stage.new_camera(Camera::new( @@ -704,12 +704,12 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::save("cull/pyramid/frame.png", img); frame.present(); let depth_texture = stage.get_depth_texture(); - let depth_img = depth_texture.read_image().unwrap().unwrap(); + let depth_img = depth_texture.read_image().block().unwrap().unwrap(); img_diff::save("cull/pyramid/depth.png", depth_img); let pyramid_images = futures_lite::future::block_on( @@ -776,7 +776,7 @@ mod test { #[test] fn occlusion_culling_debugging() { - let ctx = Context::headless(128, 128); + let ctx = Context::headless(128, 128).block(); let stage = ctx .new_stage() .with_lighting(false) @@ -796,7 +796,7 @@ mod test { let save_render = |s: &str| { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::save(format!("cull/debugging_{s}.png"), img); frame.present(); }; @@ -918,7 +918,12 @@ mod test { save_render("3_purple_cube"); // save the normalized depth image - let mut depth_img = stage.get_depth_texture().read_image().unwrap().unwrap(); + let mut depth_img = stage + .get_depth_texture() + .read_image() + .block() + .unwrap() + .unwrap(); img_diff::normalize_gray_img(&mut depth_img); img_diff::save("cull/debugging_4_depth.png", depth_img); diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index d59b9882..1b688baa 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -19,7 +19,7 @@ //! use renderling::prelude::*; //! //! // create a headless context with dimensions 100, 100. -//! let ctx = Context::headless(100, 100); +//! let ctx = futures_lite::future::block_on(Context::headless(100, 100)); //! ``` //! //! [`Context::headless`] creates a `Context` that renders to a texture. @@ -30,7 +30,7 @@ //! //! ``` //! # use renderling::prelude::*; -//! # let ctx = Context::headless(100, 100); +//! # let ctx = futures_lite::future::block_on(Context::headless(100, 100)); //! let stage: Stage = ctx //! .new_stage() //! .with_background_color([1.0, 1.0, 1.0, 1.0]) @@ -61,7 +61,7 @@ //! //! ``` //! # use renderling::prelude::*; -//! # let ctx = Context::headless(100, 100); +//! # let ctx = futures_lite::future::block_on(Context::headless(100, 100)); //! # let stage: Stage = ctx.new_stage(); //! //! let camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); @@ -98,7 +98,7 @@ //! //! ``` //! # use renderling::prelude::*; -//! # let ctx = Context::headless(100, 100); +//! # let ctx = futures_lite::future::block_on(Context::headless(100, 100)); //! # let stage = ctx.new_stage(); //! # let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); //! # let _rez = stage.builder().with_vertices([ @@ -115,7 +115,7 @@ //! //! let frame = ctx.get_next_frame().unwrap(); //! stage.render(&frame.view()); -//! let img = frame.read_image().unwrap(); +//! let img = futures_lite::future::block_on(frame.read_image()).unwrap(); //! frame.present(); //! ``` //! @@ -237,6 +237,28 @@ mod test { workspace_dir().join("test_output") } + /// Marker trait to block on futures in synchronous code. + /// + /// This is a simple convenience. + /// Many of the tests in this crate render something and then read a + /// texture in order to perform a diff on the result using a known image. + /// Since reading from the GPU is async, this trait helps cut down + /// boilerplate. + pub trait BlockOnFuture { + type Output; + + /// Block on the future using [`futures_util::future::block_on`]. + fn block(self) -> Self::Output; + } + + impl BlockOnFuture for T { + type Output = ::Output; + + fn block(self) -> Self::Output { + futures_lite::future::block_on(self) + } + } + pub fn make_two_directional_light_setup(stage: &Stage) -> (AnalyticalLight, AnalyticalLight) { let sunlight_a = stage.new_analytical_light(DirectionalLightDescriptor { direction: Vec3::new(-0.8, -1.0, 0.5).normalize(), @@ -333,7 +355,7 @@ mod test { #[test] // This tests our ability to draw a CMYK triangle in the top left corner. fn cmy_triangle_sanity() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); let _rez = stage.builder().with_vertices(right_tri_vertices()).build(); @@ -343,7 +365,7 @@ mod test { frame.present(); let depth_texture = stage.get_depth_texture(); - let depth_img = depth_texture.read_image().unwrap().unwrap(); + let depth_img = depth_texture.read_image().block().unwrap().unwrap(); img_diff::assert_img_eq("cmy_triangle/depth.png", depth_img); let hdr_img = stage @@ -351,10 +373,16 @@ mod test { .read() .unwrap() .read_hdr_image(&ctx) + .block() .unwrap(); img_diff::assert_img_eq("cmy_triangle/hdr.png", hdr_img); - let bloom_mix = stage.bloom.get_mix_texture().read_hdr_image(&ctx).unwrap(); + let bloom_mix = stage + .bloom + .get_mix_texture() + .read_hdr_image(&ctx) + .block() + .unwrap(); img_diff::assert_img_eq("cmy_triangle/bloom_mix.png", bloom_mix); } @@ -364,7 +392,7 @@ mod test { fn cmy_triangle_backface() { use img_diff::DiffCfg; - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); let _rez = stage @@ -378,7 +406,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_linear_image().unwrap(); + let img = frame.read_linear_image().block().unwrap(); img_diff::assert_img_eq_cfg( "cmy_triangle/hdr.png", img, @@ -394,7 +422,7 @@ mod test { // has already been sent to the GPU. // We do this by writing over the previous transform in the stage. fn cmy_triangle_update_transform() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); let (_vertices, transform, _renderlet) = stage @@ -413,7 +441,7 @@ mod test { }); stage.render(&frame.view()); - let img = frame.read_linear_image().unwrap(); + let img = frame.read_linear_image().block().unwrap(); img_diff::assert_img_eq("cmy_triangle/update_transform.png", img); } @@ -459,7 +487,7 @@ mod test { #[test] // Tests our ability to draw a CMYK cube. fn cmy_cube_sanity() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); let camera_position = Vec3::new(0.0, 12.0, 20.0); let _camera = stage.new_camera(Camera::new( @@ -478,14 +506,14 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("cmy_cube/sanity.png", img); } #[test] // Tests our ability to draw a CMYK cube using indexed geometry. fn cmy_cube_indices() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); let camera_position = Vec3::new(0.0, 12.0, 20.0); let _camera = stage.new_camera(Camera::new( @@ -505,7 +533,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq_cfg( "cmy_cube/sanity.png", img, @@ -520,7 +548,7 @@ mod test { // Test our ability to create two cubes and toggle the visibility of one of // them. fn cmy_cube_visible() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); let (projection, view) = camera::default_perspective(100.0, 100.0); let _camera = stage.new_camera(Camera::new(projection, view)); @@ -547,7 +575,7 @@ mod test { // we should see two colored cubes let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("cmy_cube/visible_before.png", img.clone()); let img_before = img; frame.present(); @@ -558,7 +586,7 @@ mod test { // we should see only one colored cube let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("cmy_cube/visible_after.png", img); frame.present(); @@ -568,7 +596,7 @@ mod test { // we should see two colored cubes again let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_eq("cmy_cube/visible_before_again.png", img_before, img); } @@ -576,7 +604,7 @@ mod test { // Tests the ability to specify indexed vertices, as well as the ability to // update a field within a struct stored on the slab by using a `Hybrid`. fn cmy_cube_remesh() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx .new_stage() .with_lighting(false) @@ -595,7 +623,7 @@ mod test { // we should see a cube (in sRGB color space) let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("cmy_cube/remesh_before.png", img); frame.present(); @@ -609,7 +637,7 @@ mod test { // we should see a pyramid (in sRGB color space) let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("cmy_cube/remesh_after.png", img); } @@ -663,7 +691,7 @@ mod test { // Tests that updating the material actually updates the rendering of an unlit // mesh fn unlit_textured_cube_material() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(0.0)); let (projection, view) = camera::default_perspective(100.0, 100.0); let _camera = stage.new_camera(Camera::new(projection, view)); @@ -690,7 +718,7 @@ mod test { // we should see a cube with a stoney texture let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("unlit_textured_cube_material_before.png", img); frame.present(); @@ -700,7 +728,7 @@ mod test { // we should see a cube with a dirty texture let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("unlit_textured_cube_material_after.png", img); // let size = stage.atlas.get_size(); @@ -717,7 +745,7 @@ mod test { // Ensures that we can render multiple nodes with mesh primitives // that share the same geometry, but have different materials. fn multi_node_scene() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx .new_stage() .with_background_color(Vec3::splat(0.0).extend(1.0)); @@ -773,7 +801,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("stage/shared_node_with_different_materials.png", img); } @@ -782,7 +810,7 @@ mod test { fn scene_cube_directional() { use crate::light::{DirectionalLightDescriptor, Light, LightStyle}; - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx .new_stage() .with_bloom(false) @@ -842,9 +870,9 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); let depth_texture = stage.get_depth_texture(); - let depth_img = depth_texture.read_image().unwrap().unwrap(); + let depth_img = depth_texture.read_image().block().unwrap().unwrap(); img_diff::assert_img_eq("stage/cube_directional_depth.png", depth_img); img_diff::assert_img_eq("stage/cube_directional.png", img); } @@ -890,7 +918,7 @@ mod test { // shows how to "nest" children to make them appear transformed by their // parent's transform fn scene_parent_sanity() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage().with_background_color(Vec4::splat(0.0)); let (projection, view) = camera::default_ortho2d(100.0, 100.0); let _camera = stage.new_camera(Camera::new(projection, view)); @@ -966,7 +994,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("scene_parent_sanity.png", img); } @@ -983,7 +1011,7 @@ mod test { #[test] fn can_resize_context_and_stage() { let size = UVec2::new(100, 100); - let mut ctx = Context::headless(size.x, size.y); + let mut ctx = Context::headless(size.x, size.y).block(); let stage = ctx.new_stage(); // create the CMY cube @@ -1004,7 +1032,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); assert_eq!(size, UVec2::new(img.width(), img.height())); img_diff::assert_img_eq("stage/resize_100.png", img); frame.present(); @@ -1015,7 +1043,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); assert_eq!(new_size, UVec2::new(img.width(), img.height())); img_diff::assert_img_eq("stage/resize_200.png", img); frame.present(); @@ -1024,7 +1052,9 @@ mod test { #[test] fn can_direct_draw_cube() { let size = UVec2::new(100, 100); - let ctx = Context::headless(size.x, size.y).with_use_direct_draw(true); + let ctx = Context::headless(size.x, size.y) + .block() + .with_use_direct_draw(true); let stage = ctx.new_stage(); // create the CMY cube @@ -1045,7 +1075,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); assert_eq!(size, UVec2::new(img.width(), img.height())); img_diff::assert_img_eq("stage/resize_100.png", img); frame.present(); diff --git a/crates/renderling/src/light/cpu/test.rs b/crates/renderling/src/light/cpu/test.rs index f1174b8b..76a2fb44 100644 --- a/crates/renderling/src/light/cpu/test.rs +++ b/crates/renderling/src/light/cpu/test.rs @@ -13,7 +13,7 @@ use crate::{ math::GpuRng, pbr::Material, prelude::Transform, - stage::{Renderlet, RenderletPbrVertexInfo, Stage, Vertex}, + stage::{Renderlet, RenderletPbrVertexInfo, Stage, Vertex}, test::BlockOnFuture, }; use super::*; @@ -84,7 +84,7 @@ fn spot_one_calc() { fn spot_one_frame() { let m = 32.0; let (w, h) = (16.0f32 * m, 9.0 * m); - let ctx = crate::Context::headless(w as u32, h as u32); + let ctx = crate::Context::headless(w as u32, h as u32).block(); let stage = ctx.new_stage().with_msaa_sample_count(4); let doc = stage .load_gltf_document_from_path( @@ -101,7 +101,7 @@ fn spot_one_frame() { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("light/spot_lights/one.png", img); frame.present(); } @@ -114,7 +114,7 @@ fn spot_one_frame() { fn spot_lights() { let w = 800.0; let h = 800.0; - let ctx = crate::Context::headless(w as u32, h as u32); + let ctx = crate::Context::headless(w as u32, h as u32).block(); let stage = ctx .new_stage() .with_lighting(true) @@ -141,7 +141,7 @@ fn spot_lights() { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("light/spot_lights/frame.png", img); frame.present(); } @@ -151,7 +151,7 @@ fn light_tiling_light_bounds() { let magnification = 8; let w = 16.0 * 2.0f32.powi(magnification); let h = 9.0 * 2.0f32.powi(magnification); - let ctx = crate::Context::headless(w as u32, h as u32); + let ctx = crate::Context::headless(w as u32, h as u32).block(); let stage = ctx.new_stage().with_msaa_sample_count(4); let doc = stage .load_gltf_document_from_path( @@ -219,7 +219,7 @@ fn light_tiling_light_bounds() { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::save("light/tiling/bounds.png", img); frame.present(); } @@ -339,7 +339,7 @@ fn clear_tiles_sanity() { let _ = env_logger::builder().is_test(true).try_init(); let s = 256; let depth_texture_size = UVec2::splat(s); - let ctx = crate::Context::headless(s, s); + let ctx = crate::Context::headless(s, s).block(); let stage = ctx.new_stage(); let lighting: &Lighting = stage.as_ref(); let tiling_config = LightTilingConfig::default(); @@ -410,7 +410,7 @@ fn min_max_depth_sanity() { let _ = env_logger::builder().is_test(true).try_init(); let s = 256; let depth_texture_size = UVec2::splat(s); - let ctx = crate::Context::headless(s, s); + let ctx = crate::Context::headless(s, s).block(); let stage = ctx.new_stage(); let _doc = stage .load_gltf_document_from_path( @@ -462,7 +462,7 @@ fn light_bins_sanity() { let _ = env_logger::builder().is_test(true).try_init(); let s = 256; let depth_texture_size = UVec2::splat(s); - let ctx = crate::Context::headless(s, s); + let ctx = crate::Context::headless(s, s).block(); let stage = ctx.new_stage(); let doc = stage .load_gltf_document_from_path( @@ -532,7 +532,7 @@ fn light_bins_sanity() { // Ensures point lights are being binned properly. #[test] fn light_bins_point() { - let ctx = crate::Context::headless(256, 256); + let ctx = crate::Context::headless(256, 256).block(); let stage = ctx .new_stage() .with_msaa_sample_count(1) @@ -605,7 +605,7 @@ fn tiling_e2e_sanity_with( minimum_illuminance: {minimum_illuminance}" ); let size = size(); - let ctx = crate::Context::headless(size.x, size.y); + let ctx = crate::Context::headless(size.x, size.y).block(); let stage = ctx .new_stage() .with_bloom(true) @@ -783,7 +783,7 @@ fn snapshot(ctx: &crate::Context, stage: &Stage, path: &str, save: bool) { stage.render(&frame.view()); let elapsed = start.elapsed(); log::info!("shapshot: {}s '{path}'", elapsed.as_secs_f32()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); if save { img_diff::save(path, img); } else { @@ -943,7 +943,7 @@ mod stats { /// In other words, light w/ nested transform is the same as light with /// that same transform pre-applied. fn pedestal() { - let ctx = crate::Context::headless(256, 256); + let ctx = crate::Context::headless(256, 256).block(); let stage = ctx .new_stage() .with_lighting(false) diff --git a/crates/renderling/src/light/shadow_map.rs b/crates/renderling/src/light/shadow_map.rs index 91b73f79..1e24e9c0 100644 --- a/crates/renderling/src/light/shadow_map.rs +++ b/crates/renderling/src/light/shadow_map.rs @@ -368,7 +368,7 @@ impl ShadowMap { #[cfg(test)] #[allow(clippy::unused_enumerate_index)] mod test { - use crate::camera::Camera; + use crate::{camera::Camera, test::BlockOnFuture}; use super::super::*; @@ -376,7 +376,7 @@ mod test { fn shadow_mapping_just_cuboid() { let w = 800.0; let h = 800.0; - let ctx = crate::Context::headless(w as u32, h as u32); + let ctx = crate::Context::headless(w as u32, h as u32).block(); let stage = ctx .new_stage() .with_lighting(true) @@ -403,7 +403,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); frame.present(); // Rendering the scene without shadows as a sanity check @@ -421,7 +421,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("shadows/shadow_mapping_just_cuboid/scene_after.png", img); frame.present(); } @@ -430,7 +430,7 @@ mod test { fn shadow_mapping_just_cuboid_red_and_blue() { let w = 800.0; let h = 800.0; - let ctx = crate::Context::headless(w as u32, h as u32); + let ctx = crate::Context::headless(w as u32, h as u32).block(); let stage = ctx .new_stage() .with_lighting(true) @@ -471,7 +471,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq( "shadows/shadow_mapping_just_cuboid/red_and_blue/frame.png", img, @@ -484,6 +484,7 @@ mod test { let w = 800.0; let h = 800.0; let ctx = crate::Context::headless(w as u32, h as u32) + .block() .with_shadow_mapping_atlas_texture_size([1024, 1024, 2]); let stage = ctx.new_stage().with_lighting(true); @@ -502,7 +503,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); frame.present(); // Rendering the scene without shadows as a sanity check @@ -553,7 +554,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); frame.present(); img_diff::assert_img_eq_cfg( "shadows/shadow_mapping_sanity/stage_render.png", @@ -569,7 +570,7 @@ mod test { fn shadow_mapping_spot_lights() { let w = 800.0; let h = 800.0; - let ctx = crate::Context::headless(w as u32, h as u32); + let ctx = crate::Context::headless(w as u32, h as u32).block(); let stage = ctx .new_stage() .with_lighting(true) @@ -603,7 +604,7 @@ mod test { camera.as_ref().set(Camera::new(p, v)); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let _img = frame.read_image().unwrap(); + let _img = frame.read_image().block().unwrap(); // img_diff::assert_img_eq( // &format!("shadows/shadow_mapping_spots/light_pov_{i}.png"), // img, @@ -625,7 +626,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("shadows/shadow_mapping_spots/frame.png", img); frame.present(); } @@ -634,7 +635,7 @@ mod test { fn shadow_mapping_point_lights() { let w = 800.0; let h = 800.0; - let ctx = crate::Context::headless(w as u32, h as u32); + let ctx = crate::Context::headless(w as u32, h as u32).block(); let stage = ctx .new_stage() .with_lighting(true) @@ -670,7 +671,7 @@ mod test { camera.as_ref().set(Camera::new(p, v)); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let _img = frame.read_image().unwrap(); + let _img = frame.read_image().block().unwrap(); // img_diff::assert_img_eq( // &format!("shadows/shadow_mapping_points/light_{i}_pov_{j}.png"), // img, @@ -693,7 +694,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("shadows/shadow_mapping_points/frame.png", img); frame.present(); } diff --git a/crates/renderling/src/pbr.rs b/crates/renderling/src/pbr.rs index 4c20f695..26d6d038 100644 --- a/crates/renderling/src/pbr.rs +++ b/crates/renderling/src/pbr.rs @@ -720,6 +720,7 @@ mod test { pbr::Material, prelude::glam::{Vec3, Vec4}, stage::Vertex, + test::BlockOnFuture, transform::Transform, }; @@ -730,7 +731,7 @@ mod test { // see https://learnopengl.com/PBR/Lighting fn pbr_metallic_roughness_spheres() { let ss = 600; - let ctx = crate::Context::headless(ss, ss); + let ctx = crate::Context::headless(ss, ss).block(); let stage = ctx.new_stage(); let radius = 0.5; @@ -807,7 +808,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("pbr/metallic_roughness_spheres.png", img); } } diff --git a/crates/renderling/src/skybox/cpu.rs b/crates/renderling/src/skybox/cpu.rs index 0281556e..773431e0 100644 --- a/crates/renderling/src/skybox/cpu.rs +++ b/crates/renderling/src/skybox/cpu.rs @@ -650,11 +650,11 @@ mod test { use glam::Vec3; use super::*; - use crate::Context; + use crate::{test::BlockOnFuture, Context}; #[test] fn hdr_skybox_scene() { - let ctx = Context::headless(600, 400); + let ctx = Context::headless(600, 400).block(); let proj = crate::camera::perspective(600.0, 400.0); let view = crate::camera::look_at(Vec3::new(0.0, 0.0, 2.0), Vec3::ZERO, Vec3::Y); @@ -687,7 +687,7 @@ mod test { 0, Some(wgpu::Origin3d { x: 0, y: 0, z: i }), ); - let pixels = copied_buffer.pixels(ctx.get_device()).unwrap(); + let pixels = copied_buffer.pixels(ctx.get_device()).block().unwrap(); let pixels = bytemuck::cast_slice::(pixels.as_slice()) .iter() .map(|p| half::f16::from_bits(*p).to_f32()) @@ -710,7 +710,7 @@ mod test { mip_level, Some(wgpu::Origin3d { x: 0, y: 0, z: i }), ); - let pixels = copied_buffer.pixels(ctx.get_device()).unwrap(); + let pixels = copied_buffer.pixels(ctx.get_device()).block().unwrap(); let pixels = bytemuck::cast_slice::(pixels.as_slice()) .iter() .map(|p| half::f16::from_bits(*p).to_f32()) @@ -731,18 +731,18 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_linear_image().unwrap(); + let img = frame.read_linear_image().block().unwrap(); img_diff::assert_img_eq("skybox/hdr.png", img); } #[test] fn precomputed_brdf() { assert_eq!(2, std::mem::size_of::()); - let r = Context::headless(32, 32); + let r = Context::headless(32, 32).block(); let brdf_lut = Skybox::create_precomputed_brdf_texture(&r); assert_eq!(wgpu::TextureFormat::Rg16Float, brdf_lut.texture.format()); let copied_buffer = Texture::read(&r, &brdf_lut.texture, 512, 512, 2, 2); - let pixels = copied_buffer.pixels(r.get_device()).unwrap(); + let pixels = copied_buffer.pixels(r.get_device()).block().unwrap(); let pixels: Vec = bytemuck::cast_slice::(pixels.as_slice()) .iter() .copied() diff --git a/crates/renderling/src/stage/cpu.rs b/crates/renderling/src/stage/cpu.rs index 9fa96930..cb15d724 100644 --- a/crates/renderling/src/stage/cpu.rs +++ b/crates/renderling/src/stage/cpu.rs @@ -2,7 +2,6 @@ //! //! The `Stage` object contains a slab buffer and a render pipeline. //! It is used to stage [`Renderlet`]s for rendering. -#[cfg(test)] use core::ops::Deref; use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; use craballoc::prelude::*; @@ -770,7 +769,6 @@ impl Stage { &self.runtime().queue } - #[cfg(feature = "test-helpers")] pub fn hdr_texture(&self) -> impl Deref + '_ { self.hdr_texture.read().unwrap() } @@ -1717,6 +1715,7 @@ mod test { camera::Camera, geometry::{Geometry, GeometryDescriptor}, stage::{cpu::SlabAllocator, NestedTransform, Renderlet, Vertex}, + test::BlockOnFuture, transform::Transform, Context, }; @@ -1791,7 +1790,7 @@ mod test { #[test] fn can_msaa() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx .new_stage() .with_background_color([1.0, 1.0, 1.0, 1.0]) @@ -1815,7 +1814,7 @@ mod test { log::debug!("rendering without msaa"); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq_cfg( "msaa/without.png", img, @@ -1831,7 +1830,7 @@ mod test { log::debug!("rendering with msaa"); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq_cfg( "msaa/with.png", img, @@ -1845,7 +1844,7 @@ mod test { #[test] fn has_consistent_stage_renderlet_strong_count() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage(); let r = stage.new_renderlet(Renderlet::default()); assert_eq!(1, r.ref_count()); @@ -1858,7 +1857,7 @@ mod test { /// Tests that the PBR descriptor is written to slot 0 of the geometry buffer, /// and that it contains what we think it contains. fn stage_geometry_desc_sanity() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage(); let _ = stage.commit(); diff --git a/crates/renderling/src/stage/gltf_support.rs b/crates/renderling/src/stage/gltf_support.rs index 169d8bbc..c7a16f0a 100644 --- a/crates/renderling/src/stage/gltf_support.rs +++ b/crates/renderling/src/stage/gltf_support.rs @@ -1216,7 +1216,10 @@ impl Stage { #[cfg(test)] mod test { - use crate::{camera::Camera, pbr::Material, stage::Vertex, transform::Transform, Context}; + use crate::{ + camera::Camera, pbr::Material, stage::Vertex, test::BlockOnFuture, transform::Transform, + Context, + }; use glam::{Vec3, Vec4}; #[test] @@ -1241,7 +1244,7 @@ mod test { // * support primitives w/ positions and normal attributes // * support transforming nodes (T * R * S) fn stage_gltf_simple_meshes() { - let ctx = Context::headless(100, 50); + let ctx = Context::headless(100, 50).block(); let projection = crate::camera::perspective(100.0, 50.0); let position = Vec3::new(1.0, 0.5, 1.5); let view = crate::camera::look_at(position, Vec3::new(1.0, 0.5, 0.0), Vec3::Y); @@ -1257,14 +1260,14 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("gltf/simple_meshes.png", img); } #[test] // Ensures we can read a minimal gltf file with a simple triangle mesh. fn minimal_mesh() { - let ctx = Context::headless(20, 20); + let ctx = Context::headless(20, 20).block(); let stage = ctx .new_stage() .with_lighting(false) @@ -1282,7 +1285,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("gltf/minimal_mesh.png", img); } @@ -1291,7 +1294,7 @@ mod test { // // This ensures we are decoding images correctly. fn gltf_images() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx .new_stage() .with_lighting(false) @@ -1333,14 +1336,14 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_linear_image().unwrap(); + let img = frame.read_linear_image().block().unwrap(); img_diff::assert_img_eq("gltf/images.png", img); } #[test] fn simple_texture() { let size = 100; - let ctx = Context::headless(size, size); + let ctx = Context::headless(size, size).block(); let stage = ctx .new_stage() .with_background_color(Vec3::splat(0.0).extend(1.0)) @@ -1359,7 +1362,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("gltf/simple_texture.png", img); } @@ -1367,7 +1370,7 @@ mod test { // Demonstrates how to load and render a gltf file containing lighting and a // normal map. fn normal_mapping_brick_sphere() { - let ctx = Context::headless(1920, 1080); + let ctx = Context::headless(1920, 1080).block(); let stage = ctx .new_stage() .with_lighting(true) @@ -1379,13 +1382,13 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("gltf/normal_mapping_brick_sphere.png", img); } #[test] fn rigged_fox() { - let ctx = Context::headless(256, 256); + let ctx = Context::headless(256, 256).block(); let stage = ctx .new_stage() .with_lighting(false) @@ -1413,14 +1416,14 @@ mod test { // render a frame without vertex skinning as a baseline let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("gltf/skinning/rigged_fox_no_skinning.png", img); // render a frame with vertex skinning to ensure our rigging is correct stage.set_has_vertex_skinning(true); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq_cfg( "gltf/skinning/rigged_fox_no_skinning.png", img, @@ -1469,7 +1472,7 @@ mod test { // Test that the camera has the expected translation, // taking into account that the gltf files may have been // saved with Y up, or with Z up - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let stage = ctx.new_stage(); let doc = stage .load_gltf_document_from_path( diff --git a/crates/renderling/src/stage/gltf_support/anime.rs b/crates/renderling/src/stage/gltf_support/anime.rs index ccdc75e1..72958a41 100644 --- a/crates/renderling/src/stage/gltf_support/anime.rs +++ b/crates/renderling/src/stage/gltf_support/anime.rs @@ -771,12 +771,12 @@ impl Animator { #[cfg(test)] mod test { - use crate::{camera::Camera, stage::Animator, Context}; + use crate::{camera::Camera, stage::Animator, test::BlockOnFuture, Context}; use glam::Vec3; #[test] fn gltf_simple_animation() { - let ctx = Context::headless(16, 16); + let ctx = Context::headless(16, 16).block(); let stage = ctx .new_stage() .with_bloom(false) @@ -798,7 +798,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::save("animation/triangle.png", img); frame.present(); @@ -807,7 +807,7 @@ mod test { animator.progress(dt).unwrap(); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::save(format!("animation/triangle{i}.png"), img); frame.present(); } diff --git a/crates/renderling/src/texture.rs b/crates/renderling/src/texture.rs index 3fc4e5c6..4a9918ab 100644 --- a/crates/renderling/src/texture.rs +++ b/crates/renderling/src/texture.rs @@ -2,7 +2,7 @@ use core::sync::atomic::AtomicUsize; use std::{ ops::Deref, - sync::{Arc, LazyLock}, + sync::{Arc, LazyLock, Mutex}, }; use craballoc::runtime::WgpuRuntime; @@ -42,6 +42,9 @@ pub enum TextureError { #[snafu(display("Unsupported format"))] UnsupportedFormat, + #[snafu(display("Buffer async error: {source}"))] + BufferAsync { source: wgpu::BufferAsyncError }, + #[snafu(display("Driver poll error: {source}"))] Poll { source: wgpu::PollError }, } @@ -668,7 +671,7 @@ impl Texture { } } - pub fn read_hdr_image( + pub async fn read_hdr_image( &self, runtime: impl AsRef, ) -> Result { @@ -684,7 +687,7 @@ impl Texture { 2, ); - let pixels = copied.pixels(&runtime.device)?; + let pixels = copied.pixels(&runtime.device).await?; let pixels = bytemuck::cast_slice::(pixels.as_slice()) .iter() .map(|p| half::f16::from_bits(*p).to_f32()) @@ -762,14 +765,14 @@ impl Texture { } } -pub fn read_depth_texture_to_image( +pub async fn read_depth_texture_to_image( runtime: impl AsRef, width: usize, height: usize, texture: &wgpu::Texture, ) -> Result> { let depth_copied_buffer = Texture::read(runtime.as_ref(), texture, width, height, 1, 4); - let pixels = depth_copied_buffer.pixels(&runtime.as_ref().device)?; + let pixels = depth_copied_buffer.pixels(&runtime.as_ref().device).await?; let pixels = bytemuck::cast_slice::(&pixels) .iter() .copied() @@ -785,14 +788,14 @@ pub fn read_depth_texture_to_image( )) } -pub fn read_depth_texture_f32( +pub async fn read_depth_texture_f32( runtime: impl AsRef, width: usize, height: usize, texture: &wgpu::Texture, ) -> Result, Vec>>> { let depth_copied_buffer = Texture::read(runtime.as_ref(), texture, width, height, 1, 4); - let pixels = depth_copied_buffer.pixels(&runtime.as_ref().device)?; + let pixels = depth_copied_buffer.pixels(&runtime.as_ref().device).await?; let pixels = bytemuck::cast_slice::(&pixels).to_vec(); Ok(image::ImageBuffer::from_raw( width as u32, @@ -844,18 +847,19 @@ impl DepthTexture { /// ## Panics /// This may panic if the depth texture has a multisample count greater than /// 1. - pub fn read_image(&self) -> Result> { - // TODO: impl AsRef + pub async fn read_image(&self) -> Result> { read_depth_texture_to_image( &self.runtime, self.width() as usize, self.height() as usize, &self.texture, ) + .await } } /// Helper for retreiving an image from a texture. +#[derive(Clone, Copy)] pub struct BufferDimensions { pub width: usize, pub height: usize, @@ -879,6 +883,44 @@ impl BufferDimensions { } } +/// A buffer that is being mapped. +/// +/// This implements `Future>`. +pub struct MappedBuffer<'a> { + waker: Arc>>, + result: Arc>>>, + dimensions: BufferDimensions, + buffer_slice: wgpu::BufferSlice<'a>, +} + +impl std::future::Future for MappedBuffer<'_> { + type Output = Result, wgpu::BufferAsyncError>; + + fn poll( + self: core::pin::Pin<&mut Self>, + cx: &mut core::task::Context<'_>, + ) -> core::task::Poll { + let this = self.deref(); + if let Some(result) = this.result.lock().unwrap().take() { + std::task::Poll::Ready(result.map(|()| { + let padded_buffer = this.buffer_slice.get_mapped_range(); + let mut unpadded_buffer = vec![]; + // from the padded_buffer we write just the unpadded bytes into the + // unpadded_buffer + for chunk in padded_buffer.chunks(self.dimensions.padded_bytes_per_row) { + unpadded_buffer + .extend_from_slice(&chunk[..self.dimensions.unpadded_bytes_per_row]); + } + unpadded_buffer + })) + } else { + let waker = cx.waker().clone(); + *this.waker.lock().unwrap() = Some(waker); + std::task::Poll::Pending + } + } +} + /// Helper for retreiving a rendered frame. pub struct CopiedTextureBuffer { pub format: wgpu::TextureFormat, @@ -887,52 +929,48 @@ pub struct CopiedTextureBuffer { } impl CopiedTextureBuffer { - /// Access the raw unpadded pixels of the buffer. - pub fn pixels(&self, device: &wgpu::Device) -> Result> { - let buffer_slice = self.buffer.slice(..); - buffer_slice.map_async(wgpu::MapMode::Read, |_| {}); - device.poll(wgpu::PollType::Wait).context(PollSnafu)?; - - let padded_buffer = buffer_slice.get_mapped_range(); - let mut unpadded_buffer = vec![]; - // from the padded_buffer we write just the unpadded bytes into the - // unpadded_buffer - for chunk in padded_buffer.chunks(self.dimensions.padded_bytes_per_row) { - unpadded_buffer.extend_from_slice(&chunk[..self.dimensions.unpadded_bytes_per_row]); - } - Ok(unpadded_buffer) - } - - /// Convert the post render buffer into an RgbaImage. - pub async fn convert_to_rgba(self) -> Result { + /// Return a mapped buffer that can be `await`ed for data from the GPU. + fn get_mapped_buffer(&self) -> MappedBuffer<'_> { let buffer_slice = self.buffer.slice(..); - let (tx, rx) = std::sync::mpsc::channel(); + let waker: Arc>> = Default::default(); + let result = Arc::new(Mutex::new(None)); buffer_slice.map_async(wgpu::MapMode::Read, { - move |result| { - tx.send(result).unwrap(); + let waker = waker.clone(); + let result = result.clone(); + move |res| { + let mut result = result.lock().unwrap(); + *result = Some(res); + if let Some(waker) = waker.lock().unwrap().take() { + waker.wake(); + } } }); - loop { - if let Ok(result) = rx.try_recv() { - result.context(CouldNotMapBufferSnafu)?; - break; - } else { - futures_lite::future::yield_now().await; - } + MappedBuffer { + result, + waker, + buffer_slice, + dimensions: self.dimensions, } + } - let padded_buffer = buffer_slice.get_mapped_range(); - let mut unpadded_buffer = vec![]; - // from the padded_buffer we write just the unpadded bytes into the - // unpadded_buffer - for chunk in padded_buffer.chunks(self.dimensions.padded_bytes_per_row) { - unpadded_buffer.extend_from_slice(&chunk[..self.dimensions.unpadded_bytes_per_row]); - } + /// Access the raw unpadded pixels of the buffer. + /// + /// This calls `wgpu::Device::poll`. + pub async fn pixels(&self, device: &wgpu::Device) -> Result> { + let buffer = self.get_mapped_buffer(); + device.poll(wgpu::PollType::Wait).context(PollSnafu)?; + buffer.await.context(BufferAsyncSnafu) + } + + /// Convert the post render buffer into an RgbaImage. + pub async fn convert_to_rgba(self) -> Result { + let fut_buffer = self.get_mapped_buffer(); + let pixels = fut_buffer.await.context(BufferAsyncSnafu)?; let mut img_buffer: image::ImageBuffer, Vec> = image::ImageBuffer::from_raw( self.dimensions.width as u32, self.dimensions.height as u32, - unpadded_buffer, + pixels, ) .context(CouldNotConvertImageBufferSnafu)?; if self.format.is_srgb() { @@ -953,7 +991,7 @@ impl CopiedTextureBuffer { /// `Sp` is the sub-pixel type. eg, `u8` or `f32` /// /// `P` is the pixel type. eg, `Rgba` or `Luma` - pub fn into_image( + pub async fn into_image( self, device: &wgpu::Device, ) -> Result @@ -962,7 +1000,7 @@ impl CopiedTextureBuffer { P: image::Pixel, image::DynamicImage: From>>, { - let pixels = self.pixels(device)?; + let pixels = self.pixels(device).await?; let coerced_pixels: &[Sp] = bytemuck::cast_slice(&pixels); let img_buffer: image::ImageBuffer> = image::ImageBuffer::from_raw( self.dimensions.width as u32, @@ -974,8 +1012,8 @@ impl CopiedTextureBuffer { } /// Convert the post render buffer into an internal-format [`AtlasImage`]. - pub fn into_atlas_image(self, device: &wgpu::Device) -> Result { - let pixels = self.pixels(device)?; + pub async fn into_atlas_image(self, device: &wgpu::Device) -> Result { + let pixels = self.pixels(device).await?; let img = AtlasImage { pixels, size: UVec2::new(self.dimensions.width as u32, self.dimensions.height as u32), @@ -992,7 +1030,7 @@ impl CopiedTextureBuffer { /// correct transfer function if needed. /// /// Assumes the texture is in `Rgba8` format. - pub fn into_rgba( + pub async fn into_rgba( self, device: &wgpu::Device, // `true` - the resulting image will be in a linear color space @@ -1000,7 +1038,10 @@ impl CopiedTextureBuffer { linear: bool, ) -> Result { let format = self.format; - let mut img_buffer = self.into_image::>(device)?.into_rgba8(); + let mut img_buffer = self + .into_image::>(device) + .await? + .into_rgba8(); let linear_xfer = format.is_srgb() && linear; let opto_xfer = !format.is_srgb() && !linear; let should_xfer = linear_xfer || opto_xfer; @@ -1033,9 +1074,15 @@ impl CopiedTextureBuffer { /// /// Ensures that the pixels are in a linear color space by applying the /// linear transfer if the texture this buffer was copied from was sRGB. - pub fn into_linear_rgba(self, device: &wgpu::Device) -> Result { + pub async fn into_linear_rgba( + self, + device: &wgpu::Device, + ) -> Result { let format = self.format; - let mut img_buffer = self.into_image::>(device)?.into_rgba8(); + let mut img_buffer = self + .into_image::>(device) + .await? + .into_rgba8(); if format.is_srgb() { log::trace!( "converting by applying linear transfer fn to srgb pixels (sRGB -> linear)" @@ -1056,9 +1103,12 @@ impl CopiedTextureBuffer { /// /// Ensures that the pixels are in a linear color space by applying the /// linear transfer if the texture this buffer was copied from was sRGB. - pub fn into_srgba(self, device: &wgpu::Device) -> Result { + pub async fn into_srgba(self, device: &wgpu::Device) -> Result { let format = self.format; - let mut img_buffer = self.into_image::>(device)?.into_rgba8(); + let mut img_buffer = self + .into_image::>(device) + .await? + .into_rgba8(); if !format.is_srgb() { log::trace!( "converting by applying opto transfer fn to linear pixels (linear -> sRGB)" @@ -1078,13 +1128,13 @@ impl CopiedTextureBuffer { #[cfg(test)] mod test { - use crate::Context; + use crate::{test::BlockOnFuture, Context}; use super::Texture; #[test] fn generate_mipmaps() { - let r = Context::headless(10, 10); + let r = Context::headless(10, 10).block(); let img = image::open("../../img/sandstone.png").unwrap(); let width = img.width(); let height = img.height(); @@ -1119,7 +1169,7 @@ mod test { 0, None, ); - let pixels = copied_buffer.pixels(r.get_device()).unwrap(); + let pixels = copied_buffer.pixels(r.get_device()).block().unwrap(); assert_eq!((mip_width * mip_height * 4) as usize, pixels.len()); let img: image::RgbaImage = image::ImageBuffer::from_vec(mip_width, mip_height, pixels).unwrap(); diff --git a/crates/renderling/src/ui.rs b/crates/renderling/src/ui.rs index c7622c6e..bffe3ef7 100644 --- a/crates/renderling/src/ui.rs +++ b/crates/renderling/src/ui.rs @@ -8,7 +8,7 @@ //! use renderling::ui::prelude::*; //! use glam::Vec2; //! -//! let ctx = Context::headless(100, 100); +//! let ctx = futures_lite::future::block_on(Context::headless(100, 100)); //! let mut ui = Ui::new(&ctx); //! //! let _path = ui diff --git a/crates/renderling/src/ui/cpu/path.rs b/crates/renderling/src/ui/cpu/path.rs index 6e337097..1d610b05 100644 --- a/crates/renderling/src/ui/cpu/path.rs +++ b/crates/renderling/src/ui/cpu/path.rs @@ -525,6 +525,7 @@ impl UiPathBuilder { mod test { use crate::{ math::hex_to_vec4, + test::BlockOnFuture, ui::{ test::{cute_beach_palette, Colors}, Ui, @@ -556,7 +557,7 @@ mod test { #[test] fn can_build_path_sanity() { - let ctx = Context::headless(100, 100); + let ctx = Context::headless(100, 100).block(); let ui = Ui::new(&ctx).with_antialiasing(false); let builder = ui .new_path() @@ -570,7 +571,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); ui.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("ui/path/sanity.png", img); } @@ -582,7 +583,7 @@ mod test { let _resources = builder.fill_and_stroke(); let frame = ctx.get_next_frame().unwrap(); ui.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq_cfg( "ui/path/sanity.png", img, @@ -596,7 +597,7 @@ mod test { #[test] fn can_draw_shapes() { - let ctx = Context::headless(256, 48); + let ctx = Context::headless(256, 48).block(); let ui = Ui::new(&ctx).with_default_stroke_options(StrokeOptions { line_width: 4.0, ..Default::default() @@ -672,14 +673,14 @@ mod test { let frame = ctx.get_next_frame().unwrap(); ui.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("ui/path/shapes.png", img); } #[test] fn can_fill_image() { let w = 150.0; - let ctx = Context::headless(w as u32, w as u32); + let ctx = Context::headless(w as u32, w as u32).block(); let ui = Ui::new(&ctx); let image_id = futures_lite::future::block_on(ui.load_image("../../img/dirt.jpg")).unwrap(); let center = Vec2::splat(w / 2.0); @@ -706,7 +707,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); ui.render(&frame.view()); - let mut img = frame.read_srgb_image().unwrap(); + let mut img = frame.read_srgb_image().block().unwrap(); img.pixels_mut().for_each(|p| { crate::color::opto_xfer_u8(&mut p.0[0]); crate::color::opto_xfer_u8(&mut p.0[1]); diff --git a/crates/renderling/src/ui/cpu/text.rs b/crates/renderling/src/ui/cpu/text.rs index 027cdb47..fa2325cd 100644 --- a/crates/renderling/src/ui/cpu/text.rs +++ b/crates/renderling/src/ui/cpu/text.rs @@ -330,7 +330,7 @@ impl GlyphCache { #[cfg(test)] mod test { - use crate::{ui::Ui, Context}; + use crate::{test::BlockOnFuture, ui::Ui, Context}; use glyph_brush::Section; use super::*; @@ -342,7 +342,7 @@ mod test { std::fs::read("../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf").unwrap(); let font = FontArc::try_from_vec(bytes).unwrap(); - let ctx = Context::headless(455, 145); + let ctx = Context::headless(455, 145).block(); let ui = Ui::new(&ctx); let _font_id = ui.add_font(font); let _text = ui @@ -374,7 +374,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); ui.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("ui/text/can_display.png", img); } @@ -384,7 +384,7 @@ mod test { fn text_overlayed() { log::info!("{:#?}", std::env::current_dir()); - let ctx = Context::headless(500, 253); + let ctx = Context::headless(500, 253).block(); let ui = Ui::new(&ctx).with_antialiasing(false); let font_id = futures_lite::future::block_on( ui.load_font("../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf"), @@ -432,15 +432,21 @@ mod test { let frame = ctx.get_next_frame().unwrap(); ui.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); img_diff::assert_img_eq("ui/text/overlay.png", img); - let depth_img = ui.stage.get_depth_texture().read_image().unwrap().unwrap(); + let depth_img = ui + .stage + .get_depth_texture() + .read_image() + .block() + .unwrap() + .unwrap(); img_diff::assert_img_eq("ui/text/overlay_depth.png", depth_img); } #[test] fn recreate_text() { - let ctx = Context::headless(50, 50); + let ctx = Context::headless(50, 50).block(); let ui = Ui::new(&ctx).with_antialiasing(true); let _font_id = futures_lite::future::block_on( ui.load_font("../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf"), @@ -461,7 +467,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); ui.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); frame.present(); img_diff::assert_img_eq("ui/text/can_recreate_0.png", img); @@ -481,7 +487,7 @@ mod test { let frame = ctx.get_next_frame().unwrap(); ui.render(&frame.view()); - let img = frame.read_image().unwrap(); + let img = frame.read_image().block().unwrap(); frame.present(); img_diff::assert_img_eq("ui/text/can_recreate_1.png", img); } diff --git a/crates/renderling/tests/wasm.rs b/crates/renderling/tests/wasm.rs index 86c961eb..8be99ed6 100644 --- a/crates/renderling/tests/wasm.rs +++ b/crates/renderling/tests/wasm.rs @@ -92,7 +92,7 @@ async fn can_clear_background() { .with_background_color(Vec4::new(1.0, 0.0, 0.0, 1.0)); let frame = ctx.get_next_frame().unwrap(); stage.render(&frame.view()); - let seen = frame.read_image().unwrap(); + let seen = frame.read_image().await.unwrap(); assert_img_eq("cmy_triangle/hdr.png", seen).await; } From aeffb7f04cb1be5233af19c290398f3e88498ee0 Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Sat, 30 Aug 2025 11:03:34 +1200 Subject: [PATCH 03/22] can save WASM comparison images, moved internal context creation stuff to internal module to use in WASM tests --- crates/img-diff/src/lib.rs | 28 +++- crates/renderling/Cargo.toml | 6 +- crates/renderling/src/atlas/cpu.rs | 6 +- crates/renderling/src/context.rs | 177 +++--------------------- crates/renderling/src/cubemap/cpu.rs | 3 +- crates/renderling/src/internal.rs | 186 ++++++++++++++++++++++++++ crates/renderling/src/lib.rs | 1 + crates/renderling/src/skybox/cpu.rs | 6 +- crates/renderling/src/texture.rs | 181 ++++++++++++++----------- crates/renderling/src/texture/mips.rs | 4 +- crates/renderling/tests/wasm.rs | 84 ++++++++++-- crates/xtask/src/server.rs | 54 +++++++- test_img/clear.png | Bin 0 -> 86 bytes 13 files changed, 463 insertions(+), 273 deletions(-) create mode 100644 crates/renderling/src/internal.rs create mode 100644 test_img/clear.png diff --git a/crates/img-diff/src/lib.rs b/crates/img-diff/src/lib.rs index 83509883..e3cd62c8 100644 --- a/crates/img-diff/src/lib.rs +++ b/crates/img-diff/src/lib.rs @@ -2,10 +2,12 @@ use glam::{Vec3, Vec4, Vec4Swizzles}; use image::{DynamicImage, Luma, Rgb, Rgb32FImage, Rgba32FImage}; use snafu::prelude::*; -use std::{path::Path, sync::LazyLock}; +use std::path::Path; -const TEST_IMG_DIR: &str = concat!(std::env!("CARGO_WORKSPACE_DIR"), "test_img"); -const TEST_OUTPUT_DIR: &str = concat!(std::env!("CARGO_WORKSPACE_DIR"), "test_output"); +pub const TEST_IMG_DIR: &str = concat!(std::env!("CARGO_WORKSPACE_DIR"), "test_img"); +pub const TEST_OUTPUT_DIR: &str = concat!(std::env!("CARGO_WORKSPACE_DIR"), "test_output"); +pub const WASM_TEST_OUTPUT_DIR: &str = + concat!(std::env!("CARGO_WORKSPACE_DIR"), "test_output/wasm"); const PIXEL_MAGNITUDE_THRESHOLD: f32 = 0.1; pub const LOW_PIXEL_THRESHOLD: f32 = 0.02; const IMAGE_DIFF_THRESHOLD: f32 = 0.05; @@ -41,6 +43,8 @@ pub struct DiffCfg { pub image_threshold: f32, /// The name of the test. pub test_name: Option<&'static str>, + /// The output directory to store comparisons in. + pub output_dir: &'static str, } impl Default for DiffCfg { @@ -49,6 +53,7 @@ impl Default for DiffCfg { pixel_threshold: PIXEL_MAGNITUDE_THRESHOLD, image_threshold: IMAGE_DIFF_THRESHOLD, test_name: None, + output_dir: TEST_OUTPUT_DIR, } } } @@ -124,13 +129,21 @@ fn get_results( } } -pub fn save(filename: impl AsRef, seen: impl Into) { - let path = Path::new(TEST_OUTPUT_DIR).join(filename); +pub fn save_to( + dir: impl AsRef, + filename: impl AsRef, + seen: impl Into, +) -> Result<(), String> { + let path = dir.as_ref().join(filename); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); let img: DynamicImage = seen.into(); let img_buffer = img.into_rgba8(); let img = DynamicImage::from(img_buffer); - img.save(path).unwrap(); + img.save(path).map_err(|e| e.to_string()) +} + +pub fn save(filename: impl AsRef, seen: impl Into) { + save_to(TEST_OUTPUT_DIR, filename, seen).unwrap() } pub fn assert_eq_cfg( @@ -146,6 +159,7 @@ pub fn assert_eq_cfg( pixel_threshold, image_threshold, test_name, + output_dir, } = cfg; let results = match get_results(&lhs, &rhs, pixel_threshold) { Ok(maybe_diff) => maybe_diff, @@ -171,7 +185,7 @@ pub fn assert_eq_cfg( return Ok(()); } - let mut dir = Path::new(TEST_OUTPUT_DIR).join(test_name.unwrap_or(filename)); + let mut dir = Path::new(output_dir).join(test_name.unwrap_or(filename)); dir.set_extension(""); std::fs::create_dir_all(&dir).expect("cannot create test output dir"); let expected = dir.join("expected.png"); diff --git a/crates/renderling/Cargo.toml b/crates/renderling/Cargo.toml index 9214b4c3..aebd9c97 100644 --- a/crates/renderling/Cargo.toml +++ b/crates/renderling/Cargo.toml @@ -30,7 +30,6 @@ ui = ["dep:glyph_brush", "dep:loading-bytes", "dep:lyon"] wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"] debug-slab = [] light-tiling-stats = [ "dep:plotters" ] -test-helpers = [] [build-dependencies] cfg_aliases.workspace = true @@ -75,6 +74,10 @@ snafu = {workspace = true} wgpu = { workspace = true, features = ["spirv"] } winit = { workspace = true, optional = true } +# dependencies for WASM CPU code +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen.workspace = true + [dev-dependencies] assert_approx_eq.workspace = true ctor = "0.2.2" @@ -87,7 +90,6 @@ icosahedron = "0.1" img-diff = { path = "../img-diff" } naga.workspace = true ttf-parser = "0.20.0" -wasm-bindgen.workspace = true wasm-bindgen-test.workspace = true web-sys.workspace = true wgpu-core.workspace = true diff --git a/crates/renderling/src/atlas/cpu.rs b/crates/renderling/src/atlas/cpu.rs index 8a0e1916..23a14c82 100644 --- a/crates/renderling/src/atlas/cpu.rs +++ b/crates/renderling/src/atlas/cpu.rs @@ -407,11 +407,13 @@ impl Atlas { let tex = self.get_texture(); let size = tex.texture.size(); let (channels, subpixel_bytes) = - crate::texture::wgpu_texture_format_channels_and_subpixel_bytes(tex.texture.format()); + crate::texture::wgpu_texture_format_channels_and_subpixel_bytes_todo( + tex.texture.format(), + ); log::info!("atlas_texture_format: {:#?}", tex.texture.format()); log::info!("atlas_texture_channels: {channels:#?}"); log::info!("atlas_texture_subpixel_bytes: {subpixel_bytes:#?}"); - Texture::read_from( + CopiedTextureBuffer::read_from( runtime, &tex.texture, size.width as usize, diff --git a/crates/renderling/src/context.rs b/crates/renderling/src/context.rs index 3f2f4a08..2b4adfa2 100644 --- a/crates/renderling/src/context.rs +++ b/crates/renderling/src/context.rs @@ -15,7 +15,7 @@ use crate::{ ui::Ui, }; -enum RenderTargetInner { +pub(crate) enum RenderTargetInner { Surface { surface: wgpu::Surface<'static>, surface_config: wgpu::SurfaceConfiguration, @@ -31,7 +31,7 @@ enum RenderTargetInner { /// Will be a surface if the context was created with a window or canvas. /// /// Will be a texture if the context is headless. -pub struct RenderTarget(RenderTargetInner); +pub struct RenderTarget(pub(crate) RenderTargetInner); impl From for RenderTarget { fn from(value: wgpu::Texture) -> Self { @@ -88,6 +88,14 @@ impl RenderTarget { } } + /// Return the underlying target as a texture, if possible. + pub fn as_texture(&self) -> Option<&wgpu::Texture> { + match &self.0 { + RenderTargetInner::Surface { .. } => None, + RenderTargetInner::Texture { texture } => Some(texture), + } + } + pub fn get_size(&self) -> UVec2 { match &self.0 { RenderTargetInner::Surface { @@ -102,162 +110,8 @@ impl RenderTarget { } } -async fn adapter( - instance: &wgpu::Instance, - compatible_surface: Option<&wgpu::Surface<'_>>, -) -> Result { - log::trace!( - "creating adapter for a {} context", - if compatible_surface.is_none() { - "headless" - } else { - "surface-based" - } - ); - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - compatible_surface, - force_fallback_adapter: false, - }) - .await - .context(CannotCreateAdaptorSnafu)?; - - log::info!("Adapter selected: {:?}", adapter.get_info()); - let info = adapter.get_info(); - log::info!( - "using adapter: '{}' backend:{:?} driver:'{}'", - info.name, - info.backend, - info.driver - ); - Ok(adapter) -} - -async fn device( - adapter: &wgpu::Adapter, -) -> Result<(wgpu::Device, wgpu::Queue), wgpu::RequestDeviceError> { - let wanted_features = wgpu::Features::INDIRECT_FIRST_INSTANCE - | wgpu::Features::MULTI_DRAW_INDIRECT - //// when debugging rust-gpu shader miscompilation it's nice to have this - //| wgpu::Features::SPIRV_SHADER_PASSTHROUGH - // this one is a funny requirement, it seems it is needed if using storage buffers in - // vertex shaders, even if those shaders are read-only - | wgpu::Features::VERTEX_WRITABLE_STORAGE - | wgpu::Features::CLEAR_TEXTURE; - let supported_features = adapter.features(); - let required_features = wanted_features.intersection(supported_features); - let unsupported_features = wanted_features.difference(supported_features); - if !unsupported_features.is_empty() { - log::error!("requested but unsupported features: {unsupported_features:#?}"); - log::warn!("requested and supported features: {supported_features:#?}"); - } - let limits = adapter.limits(); - log::info!("adapter limits: {limits:#?}"); - adapter - .request_device(&wgpu::DeviceDescriptor { - required_features, - required_limits: adapter.limits(), - label: None, - memory_hints: wgpu::MemoryHints::default(), - trace: wgpu::Trace::Off, - }) - .await -} - -fn new_instance(backends: Option) -> wgpu::Instance { - log::info!( - "creating instance - available backends: {:#?}", - wgpu::Instance::enabled_backend_features() - ); - // BackendBit::PRIMARY => Vulkan + Metal + DX12 + Browser WebGPU - let backends = backends.unwrap_or(wgpu::Backends::PRIMARY); - let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { - backends, - ..Default::default() - }); - - #[cfg(not(target_arch = "wasm32"))] - { - let adapters = instance.enumerate_adapters(backends); - log::trace!("available adapters: {adapters:#?}"); - } - - instance -} - -async fn new_windowed_adapter_device_queue( - width: u32, - height: u32, - instance: &wgpu::Instance, - window: impl Into>, -) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, RenderTarget), ContextError> { - let surface = instance - .create_surface(window) - .map_err(|e| ContextError::CreateSurface { source: e })?; - let adapter = adapter(instance, Some(&surface)).await?; - let surface_caps = surface.get_capabilities(&adapter); - let fmt = if surface_caps - .formats - .contains(&wgpu::TextureFormat::Rgba8UnormSrgb) - { - wgpu::TextureFormat::Rgba8UnormSrgb - } else { - surface_caps - .formats - .iter() - .copied() - .find(|f| f.is_srgb()) - .unwrap_or(surface_caps.formats[0]) - }; - let view_fmts = if fmt.is_srgb() { - vec![] - } else { - vec![fmt.add_srgb_suffix()] - }; - log::info!("surface capabilities: {surface_caps:#?}"); - let mut surface_config = surface - .get_default_config(&adapter, width, height) - .context(IncompatibleSurfaceSnafu)?; - surface_config.view_formats = view_fmts; - let (device, queue) = device(&adapter).await.context(CannotRequestDeviceSnafu)?; - surface.configure(&device, &surface_config); - let target = RenderTarget(RenderTargetInner::Surface { - surface, - surface_config, - }); - Ok((adapter, device, queue, target)) -} - -async fn new_headless_device_queue_and_target( - width: u32, - height: u32, - instance: &wgpu::Instance, -) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, RenderTarget), ContextError> { - let adapter = adapter(instance, None).await?; - let texture_desc = wgpu::TextureDescriptor { - size: wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, - usage: wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - label: None, - view_formats: &[], - }; - let (device, queue) = device(&adapter).await.context(CannotRequestDeviceSnafu)?; - let texture = Arc::new(device.create_texture(&texture_desc)); - let target = RenderTarget(RenderTargetInner::Texture { texture }); - Ok((adapter, device, queue, target)) -} - #[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] pub enum ContextError { #[snafu(display("missing surface texture: {}", source))] Surface { source: wgpu::SurfaceError }, @@ -507,9 +361,9 @@ impl Context { backends: Option, ) -> Result { log::trace!("creating headless context of size ({width}, {height})"); - let instance = new_instance(backends); + let instance = crate::internal::new_instance(backends); let (adapter, device, queue, target) = - new_headless_device_queue_and_target(width, height, &instance).await?; + crate::internal::new_headless_device_queue_and_target(width, height, &instance).await?; Ok(Self::new(target, adapter, device, queue)) } @@ -519,9 +373,10 @@ impl Context { backends: Option, window: impl Into>, ) -> Result { - let instance = new_instance(backends); + let instance = crate::internal::new_instance(backends); let (adapter, device, queue, target) = - new_windowed_adapter_device_queue(width, height, &instance, window).await?; + crate::internal::new_windowed_adapter_device_queue(width, height, &instance, window) + .await?; Ok(Self::new(target, adapter, device, queue)) } diff --git a/crates/renderling/src/cubemap/cpu.rs b/crates/renderling/src/cubemap/cpu.rs index 618a59ba..663314aa 100644 --- a/crates/renderling/src/cubemap/cpu.rs +++ b/crates/renderling/src/cubemap/cpu.rs @@ -273,6 +273,7 @@ mod test { math::{UNIT_INDICES, UNIT_POINTS}, stage::Vertex, test::BlockOnFuture, + texture::CopiedTextureBuffer, }; use super::*; @@ -513,7 +514,7 @@ mod test { let mut cpu_cubemap = vec![]; for i in 0..6 { - let img = Texture::read_from( + let img = CopiedTextureBuffer::read_from( &ctx, &scene_cubemap.cubemap_texture, width as usize, diff --git a/crates/renderling/src/internal.rs b/crates/renderling/src/internal.rs new file mode 100644 index 00000000..512ecbf2 --- /dev/null +++ b/crates/renderling/src/internal.rs @@ -0,0 +1,186 @@ +//! Internal types and functions. +//! +//! ## Note +//! The types and functions exposed by this module are used internally, and +//! are _not_ required to be used by users of this library. +//! +//! They are public here because they are needed for integration tests, and +//! on the off-chance that somebody wants to build something with them. + +use std::sync::Arc; + +use snafu::{OptionExt, ResultExt}; + +use crate::{ + CannotCreateAdaptorSnafu, CannotRequestDeviceSnafu, ContextError, IncompatibleSurfaceSnafu, + RenderTarget, RenderTargetInner, +}; + +/// Create a new [`wgpu::Adapter`]. +pub async fn adapter( + instance: &wgpu::Instance, + compatible_surface: Option<&wgpu::Surface<'_>>, +) -> Result { + log::trace!( + "creating adapter for a {} context", + if compatible_surface.is_none() { + "headless" + } else { + "surface-based" + } + ); + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface, + force_fallback_adapter: false, + }) + .await + .context(CannotCreateAdaptorSnafu)?; + + log::info!("Adapter selected: {:?}", adapter.get_info()); + let info = adapter.get_info(); + log::info!( + "using adapter: '{}' backend:{:?} driver:'{}'", + info.name, + info.backend, + info.driver + ); + Ok(adapter) +} + +/// Create a new [`wgpu::Device`]. +pub async fn device( + adapter: &wgpu::Adapter, +) -> Result<(wgpu::Device, wgpu::Queue), wgpu::RequestDeviceError> { + let wanted_features = wgpu::Features::INDIRECT_FIRST_INSTANCE + | wgpu::Features::MULTI_DRAW_INDIRECT + //// when debugging rust-gpu shader miscompilation it's nice to have this + //| wgpu::Features::SPIRV_SHADER_PASSTHROUGH + // this one is a funny requirement, it seems it is needed if using storage buffers in + // vertex shaders, even if those shaders are read-only + | wgpu::Features::VERTEX_WRITABLE_STORAGE + | wgpu::Features::CLEAR_TEXTURE; + let supported_features = adapter.features(); + let required_features = wanted_features.intersection(supported_features); + let unsupported_features = wanted_features.difference(supported_features); + if !unsupported_features.is_empty() { + log::error!("requested but unsupported features: {unsupported_features:#?}"); + log::warn!("requested and supported features: {supported_features:#?}"); + } + let limits = adapter.limits(); + log::info!("adapter limits: {limits:#?}"); + adapter + .request_device(&wgpu::DeviceDescriptor { + required_features, + required_limits: adapter.limits(), + label: None, + memory_hints: wgpu::MemoryHints::default(), + trace: wgpu::Trace::Off, + }) + .await +} + +/// Create a new instance. +/// +/// This is for internal use. It is not necessary to create your own `wgpu` +/// instance to use this library. +pub fn new_instance(backends: Option) -> wgpu::Instance { + log::info!( + "creating instance - available backends: {:#?}", + wgpu::Instance::enabled_backend_features() + ); + // BackendBit::PRIMARY => Vulkan + Metal + DX12 + Browser WebGPU + let backends = backends.unwrap_or(wgpu::Backends::PRIMARY); + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends, + ..Default::default() + }); + + #[cfg(not(target_arch = "wasm32"))] + { + let adapters = instance.enumerate_adapters(backends); + log::trace!("available adapters: {adapters:#?}"); + } + + instance +} + +/// Create a new suite of `wgpu` machinery using a window or canvas. +/// +/// ## Note +/// This function is used internally. +pub async fn new_windowed_adapter_device_queue( + width: u32, + height: u32, + instance: &wgpu::Instance, + window: impl Into>, +) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, RenderTarget), ContextError> { + let surface = instance + .create_surface(window) + .map_err(|e| ContextError::CreateSurface { source: e })?; + let adapter = adapter(instance, Some(&surface)).await?; + let surface_caps = surface.get_capabilities(&adapter); + let fmt = if surface_caps + .formats + .contains(&wgpu::TextureFormat::Rgba8UnormSrgb) + { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + surface_caps + .formats + .iter() + .copied() + .find(|f| f.is_srgb()) + .unwrap_or(surface_caps.formats[0]) + }; + let view_fmts = if fmt.is_srgb() { + vec![] + } else { + vec![fmt.add_srgb_suffix()] + }; + log::info!("surface capabilities: {surface_caps:#?}"); + let mut surface_config = surface + .get_default_config(&adapter, width, height) + .context(IncompatibleSurfaceSnafu)?; + surface_config.view_formats = view_fmts; + let (device, queue) = device(&adapter).await.context(CannotRequestDeviceSnafu)?; + surface.configure(&device, &surface_config); + let target = RenderTarget(RenderTargetInner::Surface { + surface, + surface_config, + }); + Ok((adapter, device, queue, target)) +} + +/// Create a new suite of `wgpu` machinery that renders to a texture. +/// +/// ## Note +/// This function is used internally. +pub async fn new_headless_device_queue_and_target( + width: u32, + height: u32, + instance: &wgpu::Instance, +) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue, RenderTarget), ContextError> { + let adapter = adapter(instance, None).await?; + let texture_desc = wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + label: None, + view_formats: &[], + }; + let (device, queue) = device(&adapter).await.context(CannotRequestDeviceSnafu)?; + let texture = Arc::new(device.create_texture(&texture_desc)); + let target = RenderTarget(RenderTargetInner::Texture { texture }); + Ok((adapter, device, queue, target)) +} diff --git a/crates/renderling/src/lib.rs b/crates/renderling/src/lib.rs index 1b688baa..18a7842a 100644 --- a/crates/renderling/src/lib.rs +++ b/crates/renderling/src/lib.rs @@ -160,6 +160,7 @@ pub mod debug; pub mod draw; pub mod geometry; pub mod ibl; +pub mod internal; pub mod light; #[cfg(cpu)] mod linkage; diff --git a/crates/renderling/src/skybox/cpu.rs b/crates/renderling/src/skybox/cpu.rs index 773431e0..f3d929e6 100644 --- a/crates/renderling/src/skybox/cpu.rs +++ b/crates/renderling/src/skybox/cpu.rs @@ -650,7 +650,7 @@ mod test { use glam::Vec3; use super::*; - use crate::{test::BlockOnFuture, Context}; + use crate::{test::BlockOnFuture, texture::CopiedTextureBuffer, Context}; #[test] fn hdr_skybox_scene() { @@ -677,7 +677,7 @@ mod test { for i in 0..6 { // save out the irradiance face - let copied_buffer = Texture::read_from( + let copied_buffer = CopiedTextureBuffer::read_from( &ctx, &skybox.irradiance_cubemap.texture, 32, @@ -700,7 +700,7 @@ mod test { for mip_level in 0..5 { let mip_size = 128u32 >> mip_level; // save out the prefiltered environment faces' mips - let copied_buffer = Texture::read_from( + let copied_buffer = CopiedTextureBuffer::read_from( &ctx, &skybox.prefiltered_environment_cubemap.texture, mip_size as usize, diff --git a/crates/renderling/src/texture.rs b/crates/renderling/src/texture.rs index 4a9918ab..dbc3e655 100644 --- a/crates/renderling/src/texture.rs +++ b/crates/renderling/src/texture.rs @@ -39,8 +39,8 @@ pub enum TextureError { #[snafu(display("Could not create an image buffer"))] CouldNotCreateImageBuffer, - #[snafu(display("Unsupported format"))] - UnsupportedFormat, + #[snafu(display("Unsupported format: {format:#?}"))] + UnsupportedFormat { format: wgpu::TextureFormat }, #[snafu(display("Buffer async error: {source}"))] BufferAsync { source: wgpu::BufferAsyncError }, @@ -51,8 +51,10 @@ pub enum TextureError { type Result = std::result::Result; -pub fn wgpu_texture_format_channels_and_subpixel_bytes(format: wgpu::TextureFormat) -> (u32, u32) { - match format { +pub fn wgpu_texture_format_channels_and_subpixel_bytes( + format: wgpu::TextureFormat, +) -> Result<(u32, u32)> { + Ok(match format { wgpu::TextureFormat::Depth32Float => (1, 4), wgpu::TextureFormat::R32Float => (1, 4), wgpu::TextureFormat::Rg16Float => (2, 2), @@ -61,8 +63,15 @@ pub fn wgpu_texture_format_channels_and_subpixel_bytes(format: wgpu::TextureForm wgpu::TextureFormat::Rgba32Float => (4, 4), wgpu::TextureFormat::Rgba8UnormSrgb => (4, 1), wgpu::TextureFormat::R8Unorm => (1, 1), - _ => todo!("temporarily unsupported format '{format:?}'"), - } + f => UnsupportedFormatSnafu { format: f }.fail()?, + }) +} + +/// ## Panics +pub fn wgpu_texture_format_channels_and_subpixel_bytes_todo( + format: wgpu::TextureFormat, +) -> (u32, u32) { + wgpu_texture_format_channels_and_subpixel_bytes(format).unwrap() } static NEXT_TEXTURE_ID: LazyLock> = LazyLock::new(|| Arc::new(0.into())); @@ -598,7 +607,7 @@ impl Texture { channels: usize, subpixel_bytes: usize, ) -> CopiedTextureBuffer { - Self::read_from( + CopiedTextureBuffer::read_from( runtime, texture, width, @@ -610,67 +619,6 @@ impl Texture { ) } - /// Read the texture from the GPU. - /// - /// To read the texture you must provide the width, height, the number of - /// color/alpha channels and the number of bytes in the underlying - /// subpixel type (usually u8=1, u16=2 or f32=4). - #[allow(clippy::too_many_arguments)] - pub fn read_from( - runtime: impl AsRef, - texture: &wgpu::Texture, - width: usize, - height: usize, - channels: usize, - subpixel_bytes: usize, - mip_level: u32, - origin: Option, - ) -> CopiedTextureBuffer { - let runtime = runtime.as_ref(); - let device = &runtime.device; - let queue = &runtime.queue; - let dimensions = BufferDimensions::new(channels, subpixel_bytes, width, height); - // The output buffer lets us retrieve the self as an array - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Texture::read buffer"), - size: (dimensions.padded_bytes_per_row * dimensions.height) as u64, - usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("post render screen capture encoder"), - }); - let mut source = texture.as_image_copy(); - source.mip_level = mip_level; - if let Some(origin) = origin { - source.origin = origin; - } - // Copy the data from the surface texture to the buffer - encoder.copy_texture_to_buffer( - source, - wgpu::TexelCopyBufferInfo { - buffer: &buffer, - layout: wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(dimensions.padded_bytes_per_row as u32), - rows_per_image: None, - }, - }, - wgpu::Extent3d { - width: dimensions.width as u32, - height: dimensions.height as u32, - depth_or_array_layers: 1, - }, - ); - queue.submit(std::iter::once(encoder.finish())); - - CopiedTextureBuffer { - dimensions, - buffer, - format: texture.format(), - } - } - pub async fn read_hdr_image( &self, runtime: impl AsRef, @@ -830,8 +778,9 @@ impl DepthTexture { runtime: impl AsRef, value: Texture, ) -> Result { - if value.texture.format() != wgpu::TextureFormat::Depth32Float { - return UnsupportedFormatSnafu.fail(); + let format = value.texture.format(); + if format != wgpu::TextureFormat::Depth32Float { + return UnsupportedFormatSnafu { format }.fail(); } Ok(Self { @@ -1017,8 +966,11 @@ impl CopiedTextureBuffer { let img = AtlasImage { pixels, size: UVec2::new(self.dimensions.width as u32, self.dimensions.height as u32), - format: AtlasImageFormat::from_wgpu_texture_format(self.format) - .context(UnsupportedFormatSnafu)?, + format: AtlasImageFormat::from_wgpu_texture_format(self.format).context( + UnsupportedFormatSnafu { + format: self.format, + }, + )?, apply_linear_transfer: false, }; Ok(img) @@ -1124,11 +1076,90 @@ impl CopiedTextureBuffer { Ok(img_buffer) } + + /// Read the texture from the GPU. + /// + /// To read the texture you must provide the width, height, the number of + /// color/alpha channels and the number of bytes in the underlying + /// subpixel type (usually u8=1, u16=2 or f32=4). + #[allow(clippy::too_many_arguments)] + pub fn read_from( + runtime: impl AsRef, + texture: &wgpu::Texture, + width: usize, + height: usize, + channels: usize, + subpixel_bytes: usize, + mip_level: u32, + origin: Option, + ) -> CopiedTextureBuffer { + let runtime = runtime.as_ref(); + let device = &runtime.device; + let queue = &runtime.queue; + let dimensions = BufferDimensions::new(channels, subpixel_bytes, width, height); + // The output buffer lets us retrieve the self as an array + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Texture::read buffer"), + size: (dimensions.padded_bytes_per_row * dimensions.height) as u64, + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("post render screen capture encoder"), + }); + let mut source = texture.as_image_copy(); + source.mip_level = mip_level; + if let Some(origin) = origin { + source.origin = origin; + } + // Copy the data from the surface texture to the buffer + encoder.copy_texture_to_buffer( + source, + wgpu::TexelCopyBufferInfo { + buffer: &buffer, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(dimensions.padded_bytes_per_row as u32), + rows_per_image: None, + }, + }, + wgpu::Extent3d { + width: dimensions.width as u32, + height: dimensions.height as u32, + depth_or_array_layers: 1, + }, + ); + queue.submit(std::iter::once(encoder.finish())); + + CopiedTextureBuffer { + dimensions, + buffer, + format: texture.format(), + } + } + + /// Copy the entire texture into a buffer, at mip `0`. + /// + /// Attempts to figure out the parameters to [`CopiedTextureBuffer::read_from`]. + pub fn new(runtime: impl AsRef, texture: &wgpu::Texture) -> Result { + let (channels, subpixel_bytes) = + wgpu_texture_format_channels_and_subpixel_bytes(texture.format())?; + Ok(Self::read_from( + runtime, + texture, + texture.width() as usize, + texture.height() as usize, + channels as usize, + subpixel_bytes as usize, + 0, + None, + )) + } } #[cfg(test)] mod test { - use crate::{test::BlockOnFuture, Context}; + use crate::{test::BlockOnFuture, texture::CopiedTextureBuffer, Context}; use super::Texture; @@ -1153,13 +1184,13 @@ mod test { let mips = texture.generate_mips(&r, None, mip_level_count); let (channels, subpixel_bytes) = - super::wgpu_texture_format_channels_and_subpixel_bytes(texture.texture.format()); + super::wgpu_texture_format_channels_and_subpixel_bytes_todo(texture.texture.format()); for (level, mip) in mips.into_iter().enumerate() { let mip_level = level + 1; let mip_width = width >> mip_level; let mip_height = height >> mip_level; // save out the mips - let copied_buffer = Texture::read_from( + let copied_buffer = CopiedTextureBuffer::read_from( &r, &mip.texture, mip_width as usize, diff --git a/crates/renderling/src/texture/mips.rs b/crates/renderling/src/texture/mips.rs index b4d83be5..867945de 100644 --- a/crates/renderling/src/texture/mips.rs +++ b/crates/renderling/src/texture/mips.rs @@ -4,7 +4,7 @@ use crate::texture::Texture; use craballoc::runtime::WgpuRuntime; use snafu::Snafu; -use super::wgpu_texture_format_channels_and_subpixel_bytes; +use super::wgpu_texture_format_channels_and_subpixel_bytes_todo; const LABEL: Option<&str> = Some("mip-map-generator"); @@ -118,7 +118,7 @@ impl MipMapGenerator { let mip_levels = 1.max(mip_levels); let (color_channels, subpixel_bytes) = - wgpu_texture_format_channels_and_subpixel_bytes(self.format); + wgpu_texture_format_channels_and_subpixel_bytes_todo(self.format); let size = texture.texture.size(); let mut mips: Vec = vec![]; diff --git a/crates/renderling/tests/wasm.rs b/crates/renderling/tests/wasm.rs index 8be99ed6..2ba8163d 100644 --- a/crates/renderling/tests/wasm.rs +++ b/crates/renderling/tests/wasm.rs @@ -3,7 +3,7 @@ use glam::Vec4; use image::DynamicImage; -use renderling::prelude::*; +use renderling::{prelude::*, texture::CopiedTextureBuffer}; use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; use web_sys::wasm_bindgen::UnwrapThrowExt; use wire_types::{Error, PixelType}; @@ -42,7 +42,7 @@ async fn load_test_img(path: &str) -> image::DynamicImage { image_from_bytes(&bytes) } -async fn assert_img_eq(filename: &str, seen: impl Into) { +fn image_to_wire(seen: impl Into) -> wire_types::Image { let img: DynamicImage = seen.into(); let width = img.width(); let height = img.height(); @@ -51,12 +51,16 @@ async fn assert_img_eq(filename: &str, seen: impl Into) { DynamicImage::ImageRgba8(image_buffer) => (PixelType::Rgba8, image_buffer.to_vec()), _ => panic!("Image type is not yet supported in the WASM tests"), }; - let wire_data = wire_types::Image { + wire_types::Image { width, height, bytes, pixel, - }; + } +} + +async fn assert_img_eq(filename: &str, seen: impl Into) { + let wire_data = image_to_wire(seen); let data = serde_json::to_string(&wire_data).unwrap(); let result = loading_bytes::post_json_wasm::>( &format!("http://127.0.0.1:4000/assert_img_eq/{filename}"), @@ -70,6 +74,21 @@ async fn assert_img_eq(filename: &str, seen: impl Into) { } } +async fn save(filename: &str, seen: impl Into) { + let wire_data = image_to_wire(seen); + let data = serde_json::to_string(&wire_data).unwrap(); + let result = loading_bytes::post_json_wasm::>( + &format!("http://127.0.0.1:4000/save/{filename}"), + &data, + ) + .await + .unwrap(); + + if let Err(Error { description }) = result { + panic!("{description}"); + } +} + #[wasm_bindgen_test] async fn can_load_image() { let _img = load_test_img("jolt.png").await; @@ -84,18 +103,57 @@ async fn can_img_diff() { assert_img_eq("cmy_triangle/hdr.png", b).await; } +/// Performs a clearing render pass with internal context machinery. +/// +/// This tests that the context setup is correct. #[wasm_bindgen_test] -async fn can_clear_background() { - let ctx = Context::try_new_headless(100, 100, None).await.unwrap(); - let stage = ctx - .new_stage() - .with_background_color(Vec4::new(1.0, 0.0, 0.0, 1.0)); - let frame = ctx.get_next_frame().unwrap(); - stage.render(&frame.view()); - let seen = frame.read_image().await.unwrap(); - assert_img_eq("cmy_triangle/hdr.png", seen).await; +async fn can_clear_background_sanity() { + let instance = renderling::internal::new_instance(None); + let (_adapter, device, queue, target) = + renderling::internal::new_headless_device_queue_and_target(2, 2, &instance) + .await + .unwrap(); + let texture = target.as_texture().expect("unexpected RenderTarget"); + let view = texture.create_view(&Default::default()); + + let mut encoder = device.create_command_encoder(&Default::default()); + { + let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::RED), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + resolve_target: None, + })], + ..Default::default() + }); + } + let _index = queue.submit(Some(encoder.finish())); + + let runtime = WgpuRuntime { + device: device.into(), + queue: queue.into(), + }; + let buffer = CopiedTextureBuffer::new(&runtime, texture).unwrap(); + let img = buffer.convert_to_rgba().await.unwrap(); + assert_img_eq("clear.png", img).await; } +// #[wasm_bindgen_test] +// async fn can_clear_background() { +// let ctx = Context::try_new_headless(100, 100, None).await.unwrap(); +// let stage = ctx +// .new_stage() +// .with_background_color(Vec4::new(1.0, 0.0, 0.0, 1.0)); +// let frame = ctx.get_next_frame().unwrap(); +// stage.render(&frame.view()); +// let seen = frame.read_image().await.unwrap(); +// assert_img_eq("cmy_triangle/hdr.png", seen).await; +// } + // #[wasm_bindgen_test] // #[should_panic] // async fn can_save_wrong_diffs() { diff --git a/crates/xtask/src/server.rs b/crates/xtask/src/server.rs index e24f6527..e7e87856 100644 --- a/crates/xtask/src/server.rs +++ b/crates/xtask/src/server.rs @@ -12,6 +12,7 @@ use axum::{ Json, Router, }; use image::DynamicImage; +use img_diff::DiffCfg; use wire_types::Error; pub async fn serve() { @@ -20,6 +21,8 @@ pub async fn serve() { .route("/test_img/{*path}", get(static_file)) .route("/assert_img_eq/{*filename}", options(accept)) .route("/assert_img_eq/{*filename}", post(assert_img_eq)) + .route("/save/{*filename}", options(accept)) + .route("/save/{*filename}", post(save)) .route("/{*rest}", any(accept)); let listener = tokio::net::TcpListener::bind("127.0.0.1:4000") .await @@ -58,11 +61,8 @@ async fn static_file(Path(path): Path) -> Result { } } -async fn assert_img_eq_inner( - filename: &str, - img: wire_types::Image, -) -> Result<(), wire_types::Error> { - let seen = match img.pixel { +fn image_from_wire(img: wire_types::Image) -> Result { + match img.pixel { wire_types::PixelType::Rgb8 => { image::RgbImage::from_raw(img.width, img.height, img.bytes).map(DynamicImage::from) } @@ -74,9 +74,24 @@ async fn assert_img_eq_inner( let description = "could not construct image".to_owned(); log::error!("{description}"); Error { description } - })?; + }) +} - img_diff::assert_img_eq_cfg_result(filename, seen, Default::default()).map_err(|description| { +async fn assert_img_eq_inner( + filename: &str, + img: wire_types::Image, +) -> Result<(), wire_types::Error> { + let seen = image_from_wire(img)?; + + img_diff::assert_img_eq_cfg_result( + filename, + seen, + DiffCfg { + output_dir: img_diff::WASM_TEST_OUTPUT_DIR, + ..Default::default() + }, + ) + .map_err(|description| { log::error!("{description}"); Error { description } }) @@ -102,6 +117,31 @@ async fn assert_img_eq( .unwrap() } +async fn save_inner(filename: &str, img: wire_types::Image) -> Result<(), Error> { + let img = image_from_wire(img)?; + img_diff::save_to(img_diff::WASM_TEST_OUTPUT_DIR, filename, img) + .map_err(|description| Error { description }) +} + +async fn save( + headers: HeaderMap, + Path(parts): Path>, + Json(img): Json, +) -> Response { + let filename = parts.join("/"); + log::info!("asserting '{filename}'"); + log::info!("headers: {headers:#?}"); + let result = save_inner(&filename, img).await; + Response::builder() + .status(StatusCode::OK) + .header("accept", "*/*") + .header("access-control-allow-origin", "*") + .header("access-control-allow-methods", "*") + .header("access-control-allow-headers", "*") + .body(Json(result).into_response().into_body()) + .unwrap() +} + async fn accept(request: Request) -> Response { log::info!("accept: {request:#?}"); Response::builder() diff --git a/test_img/clear.png b/test_img/clear.png new file mode 100644 index 0000000000000000000000000000000000000000..17dadd45dca1bfcbe18f475c1d013601957879d6 GIT binary patch literal 86 zcmeAS@N?(olHy`uVBq!ia0vp^Od!m`1|*BN@u~nRSx* Date: Sat, 30 Aug 2025 11:05:08 +1200 Subject: [PATCH 04/22] wire-types: edition _2021_ --- crates/wire-types/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wire-types/Cargo.toml b/crates/wire-types/Cargo.toml index b076c6f8..7e53c2cb 100644 --- a/crates/wire-types/Cargo.toml +++ b/crates/wire-types/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "wire-types" version = "0.1.0" -edition = "2024" +edition = "2021" [dependencies] serde.workspace = true From 7abdd82330a84c04e4dea0903e87e8cd2351b32c Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Sun, 31 Aug 2025 11:12:55 +1200 Subject: [PATCH 05/22] add tutorial shaders back for WASM tests, renderling-build: when generating WGSL don't flip y --- crates/renderling-build/src/lib.rs | 5 +- crates/renderling/shaders/manifest.json | 25 + .../tutorial-implicit_isosceles_vertex.spv | Bin 0 -> 712 bytes .../shaders/tutorial-passthru_fragment.spv | Bin 0 -> 316 bytes .../shaders/tutorial-slabbed_renderlet.spv | Bin 0 -> 44200 bytes .../shaders/tutorial-slabbed_vertices.spv | Bin 0 -> 5708 bytes .../tutorial-slabbed_vertices_no_instance.spv | Bin 0 -> 5084 bytes crates/renderling/src/lib.rs | 4 +- crates/renderling/src/light.rs | 13 +- crates/renderling/src/linkage.rs | 16 +- .../src/linkage/implicit_isosceles_vertex.rs | 37 + .../src/linkage/passthru_fragment.rs | 34 + .../src/linkage/slabbed_renderlet.rs | 34 + .../src/linkage/slabbed_vertices.rs | 34 + .../linkage/slabbed_vertices_no_instance.rs | 40 + crates/renderling/src/stage.rs | 34 +- crates/renderling/src/tutorial.rs | 115 +++ .../tutorial/implicit_isosceles_vertex.wgsl | 13 + crates/renderling/src/tutorial/passthru.wgsl | 5 + crates/renderling/tests/wasm.rs | 906 +++++++++++++++++- crates/xtask/src/main.rs | 40 +- 21 files changed, 1314 insertions(+), 41 deletions(-) create mode 100644 crates/renderling/shaders/tutorial-implicit_isosceles_vertex.spv create mode 100644 crates/renderling/shaders/tutorial-passthru_fragment.spv create mode 100644 crates/renderling/shaders/tutorial-slabbed_renderlet.spv create mode 100644 crates/renderling/shaders/tutorial-slabbed_vertices.spv create mode 100644 crates/renderling/shaders/tutorial-slabbed_vertices_no_instance.spv create mode 100644 crates/renderling/src/linkage/implicit_isosceles_vertex.rs create mode 100644 crates/renderling/src/linkage/passthru_fragment.rs create mode 100644 crates/renderling/src/linkage/slabbed_renderlet.rs create mode 100644 crates/renderling/src/linkage/slabbed_vertices.rs create mode 100644 crates/renderling/src/linkage/slabbed_vertices_no_instance.rs create mode 100644 crates/renderling/src/tutorial.rs create mode 100644 crates/renderling/src/tutorial/implicit_isosceles_vertex.wgsl create mode 100644 crates/renderling/src/tutorial/passthru.wgsl diff --git a/crates/renderling-build/src/lib.rs b/crates/renderling-build/src/lib.rs index dd548123..6834a9c7 100644 --- a/crates/renderling-build/src/lib.rs +++ b/crates/renderling-build/src/lib.rs @@ -94,7 +94,10 @@ fn wgsl(spv_filepath: impl AsRef, destination: impl AsRefbx#{B4$Gt|6Dobx1~tL)rN~ZFnuOR#h z@1gHdv4QkMZ<%99$}I4?a@b{83jd`X@Lu59`CA-y_odX~F0=6NAn@_sp)ee``3$xA muJFts^N!`iQ~xpS>*UjySlstk7vUz{0>ApkW`DZmmGmFyFC*Xp literal 0 HcmV?d00001 diff --git a/crates/renderling/shaders/tutorial-passthru_fragment.spv b/crates/renderling/shaders/tutorial-passthru_fragment.spv new file mode 100644 index 0000000000000000000000000000000000000000..1169e6301eb5a72c82f34cbd982f666746417b87 GIT binary patch literal 316 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 literal 0 HcmV?d00001 diff --git a/crates/renderling/shaders/tutorial-slabbed_vertices.spv b/crates/renderling/shaders/tutorial-slabbed_vertices.spv new file mode 100644 index 0000000000000000000000000000000000000000..8b54483bf36e5fd0d7bfd5ce4a315172a5de2556 GIT binary patch literal 5708 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 N>)!j)_Ya;H::new(0)); let shadow_desc = light_slab.read_unchecked(lighting_desc.update_shadow_map_id); @@ -811,8 +815,7 @@ pub fn light_tiling_depth_pre_pass( .read_unchecked(Id::::new(0) + GeometryDescriptor::OFFSET_OF_CAMERA_ID); let camera = geometry_slab.read_unchecked(camera_id); - let (_vertex, _transform, _model_matrix, world_pos) = - renderlet.get_vertex_info(vertex_index, geometry_slab); + let VertexInfo { world_pos, .. } = renderlet.get_vertex_info(vertex_index, geometry_slab); *out_clip_pos = camera.view_projection() * world_pos.extend(1.0); } diff --git a/crates/renderling/src/linkage.rs b/crates/renderling/src/linkage.rs index 8cc47c8b..55941dd6 100644 --- a/crates/renderling/src/linkage.rs +++ b/crates/renderling/src/linkage.rs @@ -1,9 +1,10 @@ //! Provides convenient wrappers around renderling shader linkage. //! -//! # Warning! -//! Please don't put anything in `crates/renderling/src/linkage/*`. -//! The files there are all auto-generated by the shader compilation machinery. -//! It is common to delete everything in that directory and regenerate. +//! For internal use. +// # Warning! +// Please don't put anything in `crates/renderling/src/linkage/*`. +// The files there are all auto-generated by the shader compilation machinery. +// It is common to delete everything in that directory and regenerate. use std::sync::Arc; pub mod atlas_blit_fragment; @@ -43,6 +44,13 @@ pub mod skybox_vertex; pub mod tonemapping_fragment; pub mod tonemapping_vertex; +// Tutorial shaders +pub mod implicit_isosceles_vertex; +pub mod passthru_fragment; +pub mod slabbed_renderlet; +pub mod slabbed_vertices; +pub mod slabbed_vertices_no_instance; + pub struct ShaderLinkage { pub module: Arc, pub entry_point: &'static str, diff --git a/crates/renderling/src/linkage/implicit_isosceles_vertex.rs b/crates/renderling/src/linkage/implicit_isosceles_vertex.rs new file mode 100644 index 00000000..1a73746f --- /dev/null +++ b/crates/renderling/src/linkage/implicit_isosceles_vertex.rs @@ -0,0 +1,37 @@ +#![allow(dead_code)] +//! Automatically generated by Renderling's `build.rs`. +use crate::linkage::ShaderLinkage; +#[cfg(not(target_arch = "wasm32"))] +mod target { + pub const ENTRY_POINT: &str = "tutorial::implicit_isosceles_vertex"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_spirv!("../../shaders/tutorial-implicit_isosceles_vertex.spv") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!( + "creating native linkage for {}", + "implicit_isosceles_vertex" + ); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +#[cfg(target_arch = "wasm32")] +mod target { + pub const ENTRY_POINT: &str = "tutorialimplicit_isosceles_vertex"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_wgsl!("../../shaders/tutorial-implicit_isosceles_vertex.wgsl") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!("creating web linkage for {}", "implicit_isosceles_vertex"); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +pub fn linkage(device: &wgpu::Device) -> ShaderLinkage { + target::linkage(device) +} diff --git a/crates/renderling/src/linkage/passthru_fragment.rs b/crates/renderling/src/linkage/passthru_fragment.rs new file mode 100644 index 00000000..2737c79e --- /dev/null +++ b/crates/renderling/src/linkage/passthru_fragment.rs @@ -0,0 +1,34 @@ +#![allow(dead_code)] +//! Automatically generated by Renderling's `build.rs`. +use crate::linkage::ShaderLinkage; +#[cfg(not(target_arch = "wasm32"))] +mod target { + pub const ENTRY_POINT: &str = "tutorial::passthru_fragment"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_spirv!("../../shaders/tutorial-passthru_fragment.spv") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!("creating native linkage for {}", "passthru_fragment"); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +#[cfg(target_arch = "wasm32")] +mod target { + pub const ENTRY_POINT: &str = "tutorialpassthru_fragment"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_wgsl!("../../shaders/tutorial-passthru_fragment.wgsl") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!("creating web linkage for {}", "passthru_fragment"); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +pub fn linkage(device: &wgpu::Device) -> ShaderLinkage { + target::linkage(device) +} diff --git a/crates/renderling/src/linkage/slabbed_renderlet.rs b/crates/renderling/src/linkage/slabbed_renderlet.rs new file mode 100644 index 00000000..c6b94d2e --- /dev/null +++ b/crates/renderling/src/linkage/slabbed_renderlet.rs @@ -0,0 +1,34 @@ +#![allow(dead_code)] +//! Automatically generated by Renderling's `build.rs`. +use crate::linkage::ShaderLinkage; +#[cfg(not(target_arch = "wasm32"))] +mod target { + pub const ENTRY_POINT: &str = "tutorial::slabbed_renderlet"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_spirv!("../../shaders/tutorial-slabbed_renderlet.spv") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!("creating native linkage for {}", "slabbed_renderlet"); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +#[cfg(target_arch = "wasm32")] +mod target { + pub const ENTRY_POINT: &str = "tutorialslabbed_renderlet"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_wgsl!("../../shaders/tutorial-slabbed_renderlet.wgsl") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!("creating web linkage for {}", "slabbed_renderlet"); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +pub fn linkage(device: &wgpu::Device) -> ShaderLinkage { + target::linkage(device) +} diff --git a/crates/renderling/src/linkage/slabbed_vertices.rs b/crates/renderling/src/linkage/slabbed_vertices.rs new file mode 100644 index 00000000..040cc9a2 --- /dev/null +++ b/crates/renderling/src/linkage/slabbed_vertices.rs @@ -0,0 +1,34 @@ +#![allow(dead_code)] +//! Automatically generated by Renderling's `build.rs`. +use crate::linkage::ShaderLinkage; +#[cfg(not(target_arch = "wasm32"))] +mod target { + pub const ENTRY_POINT: &str = "tutorial::slabbed_vertices"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_spirv!("../../shaders/tutorial-slabbed_vertices.spv") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!("creating native linkage for {}", "slabbed_vertices"); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +#[cfg(target_arch = "wasm32")] +mod target { + pub const ENTRY_POINT: &str = "tutorialslabbed_vertices"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_wgsl!("../../shaders/tutorial-slabbed_vertices.wgsl") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!("creating web linkage for {}", "slabbed_vertices"); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +pub fn linkage(device: &wgpu::Device) -> ShaderLinkage { + target::linkage(device) +} diff --git a/crates/renderling/src/linkage/slabbed_vertices_no_instance.rs b/crates/renderling/src/linkage/slabbed_vertices_no_instance.rs new file mode 100644 index 00000000..2cae6dae --- /dev/null +++ b/crates/renderling/src/linkage/slabbed_vertices_no_instance.rs @@ -0,0 +1,40 @@ +#![allow(dead_code)] +//! Automatically generated by Renderling's `build.rs`. +use crate::linkage::ShaderLinkage; +#[cfg(not(target_arch = "wasm32"))] +mod target { + pub const ENTRY_POINT: &str = "tutorial::slabbed_vertices_no_instance"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_spirv!("../../shaders/tutorial-slabbed_vertices_no_instance.spv") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!( + "creating native linkage for {}", + "slabbed_vertices_no_instance" + ); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +#[cfg(target_arch = "wasm32")] +mod target { + pub const ENTRY_POINT: &str = "tutorialslabbed_vertices_no_instance"; + pub fn descriptor() -> wgpu::ShaderModuleDescriptor<'static> { + wgpu::include_wgsl!("../../shaders/tutorial-slabbed_vertices_no_instance.wgsl") + } + pub fn linkage(device: &wgpu::Device) -> super::ShaderLinkage { + log::debug!( + "creating web linkage for {}", + "slabbed_vertices_no_instance" + ); + super::ShaderLinkage { + entry_point: ENTRY_POINT, + module: device.create_shader_module(descriptor()).into(), + } + } +} +pub fn linkage(device: &wgpu::Device) -> ShaderLinkage { + target::linkage(device) +} diff --git a/crates/renderling/src/stage.rs b/crates/renderling/src/stage.rs index 486f7f4e..a47e897e 100644 --- a/crates/renderling/src/stage.rs +++ b/crates/renderling/src/stage.rs @@ -90,9 +90,16 @@ pub struct MorphTarget { // I think this would take a contribution to the `gltf` crate. } +/// Returned by [`Renderlet::get_vertex_info`]. +pub struct VertexInfo { + pub vertex: Vertex, + pub transform: Transform, + pub model_matrix: Mat4, + pub world_pos: Vec3, +} + /// A vertex in a mesh. -#[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, Copy, PartialEq, SlabItem)] +#[derive(Clone, Copy, core::fmt::Debug, PartialEq, SlabItem)] pub struct Vertex { pub position: Vec3, pub color: Vec4, @@ -242,16 +249,17 @@ impl Renderlet { /// Returns the vertex at the given index and its related values. /// /// These values are often used in shaders, so they are grouped together. - pub fn get_vertex_info( - &self, - vertex_index: u32, - geometry_slab: &[u32], - ) -> (Vertex, Transform, Mat4, Vec3) { + pub fn get_vertex_info(&self, vertex_index: u32, geometry_slab: &[u32]) -> VertexInfo { let vertex = self.get_vertex(vertex_index, geometry_slab); let transform = self.get_transform(vertex, geometry_slab); let model_matrix = Mat4::from(transform); let world_pos = model_matrix.transform_point3(vertex.position); - (vertex, transform, model_matrix, world_pos) + VertexInfo { + vertex, + transform, + model_matrix, + world_pos, + } } /// Retrieve the transform of this `Renderlet`. /// @@ -329,6 +337,8 @@ pub fn renderlet_vertex( #[spirv(flat)] out_renderlet: &mut Id, // TODO: Think about placing all these out values in a G-Buffer + // But do we have enough buffers + enough space on web? + // ...and can we write to buffers from vertex shaders on web? out_color: &mut Vec4, out_uv0: &mut Vec2, out_uv1: &mut Vec2, @@ -349,8 +359,12 @@ pub fn renderlet_vertex( *out_renderlet = renderlet_id; - let (vertex, transform, model_matrix, world_pos) = - renderlet.get_vertex_info(vertex_index, geometry_slab); + let VertexInfo { + vertex, + transform, + model_matrix, + world_pos, + } = renderlet.get_vertex_info(vertex_index, geometry_slab); *out_color = vertex.color; *out_uv0 = vertex.uv0; *out_uv1 = vertex.uv1; diff --git a/crates/renderling/src/tutorial.rs b/crates/renderling/src/tutorial.rs new file mode 100644 index 00000000..8b2fb816 --- /dev/null +++ b/crates/renderling/src/tutorial.rs @@ -0,0 +1,115 @@ +//! Shaders used in the intro tutorial and in WASM tests. + +use crabslab::{Array, Id, Slab, SlabItem}; +use glam::{Vec3Swizzles, Vec4}; +use spirv_std::spirv; + +use crate::{ + geometry::GeometryDescriptor, + stage::{Renderlet, Vertex, VertexInfo}, +}; + +/// Simple fragment shader that writes the input color to the output color. +// Inline pragma needed so this shader doesn't get optimized away: +// See +#[inline(never)] +#[spirv(fragment)] +pub fn passthru_fragment(in_color: Vec4, output: &mut Vec4) { + *output = in_color; +} + +/// Simple vertex shader with an implicit isosceles triangle. +/// +/// This shader gets run with three indices and draws a triangle without +/// using any other data from the CPU. +#[spirv(vertex)] +pub fn implicit_isosceles_vertex( + // Which vertex within the render unit are we rendering + #[spirv(vertex_index)] vertex_index: u32, + + out_color: &mut Vec4, + #[spirv(position)] clip_pos: &mut Vec4, +) { + let pos = { + let x = (1 - vertex_index as i32) as f32 * 0.5; + let y = (((vertex_index & 1) as f32 * 2.0) - 1.0) * 0.5; + Vec4::new(x, y, 0.0, 1.0) + }; + *out_color = Vec4::new(1.0, 0.0, 0.0, 1.0); + *clip_pos = pos; +} + +/// This shader uses the vertex index as a slab [`Id`]. The [`Id`] is used to +/// read the vertex from the slab. The vertex's position and color are written +/// to the output. +#[spirv(vertex)] +pub fn slabbed_vertices_no_instance( + // Which vertex within the render unit are we rendering + #[spirv(vertex_index)] vertex_index: u32, + + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + + out_color: &mut Vec4, + #[spirv(position)] clip_pos: &mut Vec4, +) { + let vertex_id = Id::::from(vertex_index as usize * Vertex::SLAB_SIZE); + let vertex = slab.read(vertex_id); + *clip_pos = vertex.position.extend(1.0); + *out_color = vertex.color; +} + +/// This shader uses the `instance_index` as a slab [`Id`]. +/// The `instance_index` is the [`Id`] of an [`Array`] of [`Vertex`]s. The +/// `vertex_index` is the index of a [`Vertex`] within the [`Array`]. +#[spirv(vertex)] +pub fn slabbed_vertices( + // Id of the array of vertices we are rendering + #[spirv(instance_index)] instance_index: u32, + // Which vertex within the render unit are we rendering + #[spirv(vertex_index)] vertex_index: u32, + + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + + out_color: &mut Vec4, + #[spirv(position)] clip_pos: &mut Vec4, +) { + let array_id = Id::>::from(instance_index); + let array = slab.read(array_id); + let vertex_id = array.at(vertex_index as usize); + let vertex = slab.read(vertex_id); + *clip_pos = vertex.position.extend(1.0); + *out_color = vertex.color; +} + +// TODO: fix all this documentation +/// This shader uses the `instance_index` as a slab [`Id`]. +/// The `instance_index` is the [`Id`] of a [`RenderUnit`]. +/// The [`RenderUnit`] contains an [`Array`] of [`Vertex`]s +/// as its mesh, the [`Id`]s of a [`Material`] and [`Camera`], +/// and TRS transforms. +/// The `vertex_index` is the index of a [`Vertex`] within the +/// [`RenderUnit`]'s `vertices` [`Array`]. +#[spirv(vertex)] +pub fn slabbed_renderlet( + // Id of the array of vertices we are rendering + #[spirv(instance_index)] renderlet_id: Id, + // Which vertex within the render unit are we rendering + #[spirv(vertex_index)] vertex_index: u32, + + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32], + + out_color: &mut Vec4, + #[spirv(position)] clip_pos: &mut Vec4, +) { + let renderlet = slab.read(renderlet_id); + let VertexInfo { + vertex, + model_matrix, + .. + } = renderlet.get_vertex_info(vertex_index, slab); + let camera_id = slab + .read_unchecked(renderlet.geometry_descriptor_id + GeometryDescriptor::OFFSET_OF_CAMERA_ID); + let camera = slab.read(camera_id); + *clip_pos = camera.view_projection() * model_matrix * vertex.position.xyz().extend(1.0); + *out_color = vertex.color; +} diff --git a/crates/renderling/src/tutorial/implicit_isosceles_vertex.wgsl b/crates/renderling/src/tutorial/implicit_isosceles_vertex.wgsl new file mode 100644 index 00000000..1a8296aa --- /dev/null +++ b/crates/renderling/src/tutorial/implicit_isosceles_vertex.wgsl @@ -0,0 +1,13 @@ +struct VertexOutput { + @location(0) color: vec4, + @builtin(position) clip_pos: vec4, +} +@vertex +fn main(@builtin(vertex_index) index: u32) -> VertexOutput { + let x = f32(1i - bitcast(index)) * 0.5f; + let y = (f32(index & 1u) * 2f - 1f) * 0.5f; + let position = vec4(x, y, 0f, 1f); + + let color = vec4(1f, 0f, 0f, 1f); + return VertexOutput(color, position); +} diff --git a/crates/renderling/src/tutorial/passthru.wgsl b/crates/renderling/src/tutorial/passthru.wgsl new file mode 100644 index 00000000..4fe7cd2b --- /dev/null +++ b/crates/renderling/src/tutorial/passthru.wgsl @@ -0,0 +1,5 @@ +// Pass-through fragment shader that copies in color to out. +@fragment +fn main(@location(0) color:vec4) -> @location(0) vec4 { + return color; +} diff --git a/crates/renderling/tests/wasm.rs b/crates/renderling/tests/wasm.rs index 2ba8163d..3f3cb3e1 100644 --- a/crates/renderling/tests/wasm.rs +++ b/crates/renderling/tests/wasm.rs @@ -1,11 +1,11 @@ //! WASM tests. #![allow(dead_code)] -use glam::Vec4; +use glam::{Vec3, Vec4}; use image::DynamicImage; use renderling::{prelude::*, texture::CopiedTextureBuffer}; use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; -use web_sys::wasm_bindgen::UnwrapThrowExt; +use web_sys::wasm_bindgen::prelude::*; use wire_types::{Error, PixelType}; wasm_bindgen_test_configure!(run_in_browser); @@ -142,6 +142,908 @@ async fn can_clear_background_sanity() { assert_img_eq("clear.png", img).await; } +/// Test rendering a triangle using no mesh geometry. +#[wasm_bindgen_test] +async fn implicit_isosceles_triangle() { + let instance = renderling::internal::new_instance(None); + let (_adapter, device, queue, target) = + renderling::internal::new_headless_device_queue_and_target(100, 100, &instance) + .await + .unwrap(); + let runtime = WgpuRuntime { + device: device.into(), + queue: queue.into(), + }; + let label = Some("implicit isosceles triangle"); + + // The first time through render with handwritten WGSL to ensure the setup works + let mut hand_written_wgsl_pipeline = { + let vertex = runtime.device.create_shader_module(wgpu::include_wgsl!( + "../src/tutorial/implicit_isosceles_vertex.wgsl" + )); + let fragment = runtime + .device + .create_shader_module(wgpu::include_wgsl!("../src/tutorial/passthru.wgsl")); + runtime + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label, + layout: None, + vertex: wgpu::VertexState { + module: &vertex, + entry_point: Some("main"), + compilation_options: wgpu::PipelineCompilationOptions::default(), + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &fragment, + entry_point: Some("main"), + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + multiview: None, + cache: None, + }) + }; + // The second time render with WGSL that is transpiled from Rust code and pulled in through + // the renderling linkage machinery. + let linkage_pipeline = { + let vertex = renderling::linkage::implicit_isosceles_vertex::linkage(&runtime.device); + let fragment = renderling::linkage::passthru_fragment::linkage(&runtime.device); + runtime + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label, + layout: None, + vertex: wgpu::VertexState { + module: &vertex.module, + entry_point: Some(vertex.entry_point), + compilation_options: wgpu::PipelineCompilationOptions::default(), + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &fragment.module, + entry_point: Some(fragment.entry_point), + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + multiview: None, + cache: None, + }) + }; + + async fn render(runtime: &WgpuRuntime, target: &RenderTarget, pipeline: wgpu::RenderPipeline) { + let texture = target.as_texture().expect("unexpected RenderTarget"); + let view = texture.create_view(&Default::default()); + let mut encoder = runtime + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::GREEN), + store: wgpu::StoreOp::Store, + }, + })], + ..Default::default() + }); + render_pass.set_pipeline(&pipeline); + render_pass.draw(0..3, 0..1); + } + let _index = runtime.queue.submit(std::iter::once(encoder.finish())); + + let buffer = CopiedTextureBuffer::new(runtime, texture).unwrap(); + let img = buffer.convert_to_rgba().await.unwrap(); + assert_img_eq("tutorial/implicit_isosceles_triangle.png", img).await; + } + + render(&runtime, &target, hand_written_wgsl_pipeline).await; + render(&runtime, &target, linkage_pipeline).await; +} + +// #[test] +// fn slabbed_isosceles_triangle_no_instance() { +// let mut r = Renderling::headless(100, 100).unwrap(); +// let (device, queue) = r.get_device_and_queue_owned(); + +// // Create our geometry on the slab. +// // Don't worry too much about capacity, it can grow. +// let slab = crate::slab::SlabBuffer::new(&device, 16); +// let vertices = slab.append_slice( +// &device, +// &queue, +// &[ +// Vertex { +// position: Vec4::new(0.5, -0.5, 0.0, 1.0), +// color: Vec4::new(1.0, 0.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(0.0, 0.5, 0.0, 1.0), +// color: Vec4::new(0.0, 1.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(-0.5, -0.5, 0.0, 1.0), +// color: Vec4::new(0.0, 0.0, 1.0, 1.0), +// ..Default::default() +// }, +// ], +// ); +// assert_eq!(3, vertices.len()); + +// // Create a bindgroup for the slab so our shader can read out the types. +// let label = Some("slabbed isosceles triangle"); +// let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { +// label, +// entries: &[wgpu::BindGroupLayoutEntry { +// binding: 0, +// 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, +// }], +// }); +// let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { +// label, +// bind_group_layouts: &[&bindgroup_layout], +// push_constant_ranges: &[], +// }); + +// let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { +// label, +// layout: Some(&pipeline_layout), +// vertex: wgpu::VertexState { +// module: &device.create_shader_module(wgpu::include_spirv!( +// "linkage/tutorial-slabbed_vertices_no_instance.spv" +// )), +// entry_point: "tutorial::slabbed_vertices_no_instance", +// buffers: &[], +// }, +// primitive: wgpu::PrimitiveState { +// topology: wgpu::PrimitiveTopology::TriangleList, +// strip_index_format: None, +// front_face: wgpu::FrontFace::Ccw, +// cull_mode: None, +// unclipped_depth: false, +// polygon_mode: wgpu::PolygonMode::Fill, +// conservative: false, +// }, +// depth_stencil: Some(wgpu::DepthStencilState { +// format: wgpu::TextureFormat::Depth32Float, +// depth_write_enabled: true, +// depth_compare: wgpu::CompareFunction::Less, +// stencil: wgpu::StencilState::default(), +// bias: wgpu::DepthBiasState::default(), +// }), +// multisample: wgpu::MultisampleState { +// mask: !0, +// alpha_to_coverage_enabled: false, +// count: 1, +// }, +// fragment: Some(wgpu::FragmentState { +// module: &device.create_shader_module(wgpu::include_spirv!( +// "linkage/tutorial-passthru_fragment.spv" +// )), +// entry_point: "tutorial::passthru_fragment", +// targets: &[Some(wgpu::ColorTargetState { +// format: wgpu::TextureFormat::Rgba8UnormSrgb, +// blend: Some(wgpu::BlendState::ALPHA_BLENDING), +// write_mask: wgpu::ColorWrites::ALL, +// })], +// }), +// multiview: None, +// }); + +// let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { +// label, +// layout: &bindgroup_layout, +// entries: &[wgpu::BindGroupEntry { +// binding: 0, +// resource: slab.get_buffer().as_entire_binding(), +// }], +// }); + +// struct App { +// pipeline: wgpu::RenderPipeline, +// bindgroup: wgpu::BindGroup, +// vertices: Array, +// } + +// let app = App { +// pipeline, +// bindgroup, +// vertices, +// }; +// r.graph.add_resource(app); + +// fn render( +// (device, queue, app, frame, depth): ( +// View, +// View, +// View, +// View, +// View, +// ), +// ) -> Result<(), GraphError> { +// let label = Some("slabbed isosceles triangle"); +// let mut encoder = +// device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); +// { +// let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { +// label, +// color_attachments: &[Some(wgpu::RenderPassColorAttachment { +// view: &frame.view, +// resolve_target: None, +// ops: wgpu::Operations { +// load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), +// store: true, +// }, +// })], +// depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { +// view: &depth.view, +// depth_ops: Some(wgpu::Operations { +// load: wgpu::LoadOp::Load, +// store: true, +// }), +// stencil_ops: None, +// }), +// }); +// render_pass.set_pipeline(&app.pipeline); +// render_pass.set_bind_group(0, &app.bindgroup, &[]); +// render_pass.draw(0..app.vertices.len() as u32, 0..1); +// } +// queue.submit(std::iter::once(encoder.finish())); +// Ok(()) +// } + +// use crate::frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}; +// r.graph.add_subgraph(graph!( +// create_frame +// < clear_frame_and_depth +// < render +// < copy_frame_to_post +// < present +// )); + +// let img = r.render_image().unwrap(); +// img_diff::assert_img_eq("tutorial/slabbed_isosceles_triangle_no_instance.png", img); +// } + +// #[test] +// fn slabbed_isosceles_triangle() { +// let mut r = Renderling::headless(100, 100).unwrap(); +// let (device, queue) = r.get_device_and_queue_owned(); + +// // Create our geometry on the slab. +// // Don't worry too much about capacity, it can grow. +// let slab = crate::slab::SlabBuffer::new(&device, 16); +// let geometry = vec![ +// Vertex { +// position: Vec4::new(0.5, -0.5, 0.0, 1.0), +// color: Vec4::new(1.0, 0.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(0.0, 0.5, 0.0, 1.0), +// color: Vec4::new(0.0, 1.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(-0.5, -0.5, 0.0, 1.0), +// color: Vec4::new(0.0, 0.0, 1.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(-1.0, 1.0, 0.0, 1.0), +// color: Vec4::new(1.0, 0.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(-1.0, 0.0, 0.0, 1.0), +// color: Vec4::new(0.0, 1.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(0.0, 1.0, 0.0, 1.0), +// color: Vec4::new(0.0, 0.0, 1.0, 1.0), +// ..Default::default() +// }, +// ]; +// let vertices = slab.append_slice(&device, &queue, &geometry); +// let vertices_id = slab.append(&device, &queue, &vertices); + +// // Create a bindgroup for the slab so our shader can read out the types. +// let label = Some("slabbed isosceles triangle"); +// let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { +// label, +// entries: &[wgpu::BindGroupLayoutEntry { +// binding: 0, +// 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, +// }], +// }); +// let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { +// label, +// bind_group_layouts: &[&bindgroup_layout], +// push_constant_ranges: &[], +// }); + +// let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { +// label, +// layout: Some(&pipeline_layout), +// vertex: wgpu::VertexState { +// module: &device.create_shader_module(wgpu::include_spirv!( +// "linkage/tutorial-slabbed_vertices.spv" +// )), +// entry_point: "tutorial::slabbed_vertices", +// buffers: &[], +// }, +// primitive: wgpu::PrimitiveState { +// topology: wgpu::PrimitiveTopology::TriangleList, +// strip_index_format: None, +// front_face: wgpu::FrontFace::Ccw, +// cull_mode: None, +// unclipped_depth: false, +// polygon_mode: wgpu::PolygonMode::Fill, +// conservative: false, +// }, +// depth_stencil: Some(wgpu::DepthStencilState { +// format: wgpu::TextureFormat::Depth32Float, +// depth_write_enabled: true, +// depth_compare: wgpu::CompareFunction::Less, +// stencil: wgpu::StencilState::default(), +// bias: wgpu::DepthBiasState::default(), +// }), +// multisample: wgpu::MultisampleState { +// mask: !0, +// alpha_to_coverage_enabled: false, +// count: 1, +// }, +// fragment: Some(wgpu::FragmentState { +// module: &device.create_shader_module(wgpu::include_spirv!( +// "linkage/tutorial-passthru_fragment.spv" +// )), +// entry_point: "tutorial::passthru_fragment", +// targets: &[Some(wgpu::ColorTargetState { +// format: wgpu::TextureFormat::Rgba8UnormSrgb, +// blend: Some(wgpu::BlendState::ALPHA_BLENDING), +// write_mask: wgpu::ColorWrites::ALL, +// })], +// }), +// multiview: None, +// }); + +// let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { +// label, +// layout: &bindgroup_layout, +// entries: &[wgpu::BindGroupEntry { +// binding: 0, +// resource: slab.get_buffer().as_entire_binding(), +// }], +// }); + +// struct App { +// pipeline: wgpu::RenderPipeline, +// bindgroup: wgpu::BindGroup, +// vertices_id: Id>, +// vertices: Array, +// } + +// let app = App { +// pipeline, +// bindgroup, +// vertices_id, +// vertices, +// }; +// r.graph.add_resource(app); + +// fn render( +// (device, queue, app, frame, depth): ( +// View, +// View, +// View, +// View, +// View, +// ), +// ) -> Result<(), GraphError> { +// let label = Some("slabbed isosceles triangle"); +// let mut encoder = +// device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); +// { +// let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { +// label, +// color_attachments: &[Some(wgpu::RenderPassColorAttachment { +// view: &frame.view, +// resolve_target: None, +// ops: wgpu::Operations { +// load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), +// store: true, +// }, +// })], +// depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { +// view: &depth.view, +// depth_ops: Some(wgpu::Operations { +// load: wgpu::LoadOp::Load, +// store: true, +// }), +// stencil_ops: None, +// }), +// }); +// render_pass.set_pipeline(&app.pipeline); +// render_pass.set_bind_group(0, &app.bindgroup, &[]); +// render_pass.draw( +// 0..app.vertices.len() as u32, +// app.vertices_id.inner()..app.vertices_id.inner() + 1, +// ); +// } +// queue.submit(std::iter::once(encoder.finish())); +// Ok(()) +// } + +// use crate::frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}; +// r.graph.add_subgraph(graph!( +// create_frame +// < clear_frame_and_depth +// < render +// < copy_frame_to_post +// < present +// )); + +// let img = r.render_image().unwrap(); +// img_diff::assert_img_eq("tutorial/slabbed_isosceles_triangle.png", img); +// } + +// #[test] +// fn slabbed_render_unit() { +// let mut r = Renderling::headless(100, 100).unwrap(); +// let (device, queue) = r.get_device_and_queue_owned(); + +// // Create our geometry on the slab. +// // Don't worry too much about capacity, it can grow. +// let slab = crate::slab::SlabBuffer::new(&device, 16); +// let geometry = vec![ +// Vertex { +// position: Vec4::new(0.5, -0.5, 0.0, 1.0), +// color: Vec4::new(1.0, 0.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(0.0, 0.5, 0.0, 1.0), +// color: Vec4::new(0.0, 1.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(-0.5, -0.5, 0.0, 1.0), +// color: Vec4::new(0.0, 0.0, 1.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(-1.0, 1.0, 0.0, 1.0), +// color: Vec4::new(1.0, 0.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(-1.0, 0.0, 0.0, 1.0), +// color: Vec4::new(0.0, 1.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(0.0, 1.0, 0.0, 1.0), +// color: Vec4::new(0.0, 0.0, 1.0, 1.0), +// ..Default::default() +// }, +// ]; +// let vertices = slab.append_slice(&device, &queue, &geometry); +// let unit = RenderUnit { +// vertices, +// ..Default::default() +// }; +// let unit_id = slab.append(&device, &queue, &unit); + +// // Create a bindgroup for the slab so our shader can read out the types. +// let label = Some("slabbed isosceles triangle"); +// let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { +// label, +// entries: &[wgpu::BindGroupLayoutEntry { +// binding: 0, +// 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, +// }], +// }); +// let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { +// label, +// bind_group_layouts: &[&bindgroup_layout], +// push_constant_ranges: &[], +// }); + +// let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { +// label, +// layout: Some(&pipeline_layout), +// vertex: wgpu::VertexState { +// module: &device.create_shader_module(wgpu::include_spirv!( +// "linkage/tutorial-slabbed_render_unit.spv" +// )), +// entry_point: "tutorial::slabbed_render_unit", +// buffers: &[], +// }, +// primitive: wgpu::PrimitiveState { +// topology: wgpu::PrimitiveTopology::TriangleList, +// strip_index_format: None, +// front_face: wgpu::FrontFace::Ccw, +// cull_mode: None, +// unclipped_depth: false, +// polygon_mode: wgpu::PolygonMode::Fill, +// conservative: false, +// }, +// depth_stencil: Some(wgpu::DepthStencilState { +// format: wgpu::TextureFormat::Depth32Float, +// depth_write_enabled: true, +// depth_compare: wgpu::CompareFunction::Less, +// stencil: wgpu::StencilState::default(), +// bias: wgpu::DepthBiasState::default(), +// }), +// multisample: wgpu::MultisampleState { +// mask: !0, +// alpha_to_coverage_enabled: false, +// count: 1, +// }, +// fragment: Some(wgpu::FragmentState { +// module: &device.create_shader_module(wgpu::include_spirv!( +// "linkage/tutorial-passthru_fragment.spv" +// )), +// entry_point: "tutorial::passthru_fragment", +// targets: &[Some(wgpu::ColorTargetState { +// format: wgpu::TextureFormat::Rgba8UnormSrgb, +// blend: Some(wgpu::BlendState::ALPHA_BLENDING), +// write_mask: wgpu::ColorWrites::ALL, +// })], +// }), +// multiview: None, +// }); + +// let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { +// label, +// layout: &bindgroup_layout, +// entries: &[wgpu::BindGroupEntry { +// binding: 0, +// resource: slab.get_buffer().as_entire_binding(), +// }], +// }); + +// struct App { +// pipeline: wgpu::RenderPipeline, +// bindgroup: wgpu::BindGroup, +// unit_id: Id, +// unit: RenderUnit, +// } + +// let app = App { +// pipeline, +// bindgroup, +// unit_id, +// unit, +// }; +// r.graph.add_resource(app); + +// fn render( +// (device, queue, app, frame, depth): ( +// View, +// View, +// View, +// View, +// View, +// ), +// ) -> Result<(), GraphError> { +// let label = Some("slabbed isosceles triangle"); +// let mut encoder = +// device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); +// { +// let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { +// label, +// color_attachments: &[Some(wgpu::RenderPassColorAttachment { +// view: &frame.view, +// resolve_target: None, +// ops: wgpu::Operations { +// load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), +// store: true, +// }, +// })], +// depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { +// view: &depth.view, +// depth_ops: Some(wgpu::Operations { +// load: wgpu::LoadOp::Load, +// store: true, +// }), +// stencil_ops: None, +// }), +// }); +// render_pass.set_pipeline(&app.pipeline); +// render_pass.set_bind_group(0, &app.bindgroup, &[]); +// render_pass.draw( +// 0..app.unit.vertices.len() as u32, +// app.unit_id.inner()..app.unit_id.inner() + 1, +// ); +// } +// queue.submit(std::iter::once(encoder.finish())); +// Ok(()) +// } + +// use crate::frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}; +// r.graph.add_subgraph(graph!( +// create_frame +// < clear_frame_and_depth +// < render +// < copy_frame_to_post +// < present +// )); + +// let img = r.render_image().unwrap(); +// img_diff::assert_img_eq("tutorial/slabbed_render_unit.png", img); +// } + +// #[test] +// fn slabbed_render_unit_camera() { +// let mut r = Renderling::headless(100, 100).unwrap(); +// let (device, queue) = r.get_device_and_queue_owned(); + +// // Create our geometry on the slab. +// // Don't worry too much about capacity, it can grow. +// let slab = crate::slab::SlabBuffer::new(&device, 16); +// let geometry = vec![ +// Vertex { +// position: Vec4::new(0.5, -0.5, 0.0, 1.0), +// color: Vec4::new(1.0, 0.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(0.0, 0.5, 0.0, 1.0), +// color: Vec4::new(0.0, 1.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(-0.5, -0.5, 0.0, 1.0), +// color: Vec4::new(0.0, 0.0, 1.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(-1.0, 1.0, 0.0, 1.0), +// color: Vec4::new(1.0, 0.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(-1.0, 0.0, 0.0, 1.0), +// color: Vec4::new(0.0, 1.0, 0.0, 1.0), +// ..Default::default() +// }, +// Vertex { +// position: Vec4::new(0.0, 1.0, 0.0, 1.0), +// color: Vec4::new(0.0, 0.0, 1.0, 1.0), +// ..Default::default() +// }, +// ]; +// let vertices = slab.append_slice(&device, &queue, &geometry); +// let (projection, view) = crate::default_ortho2d(100.0, 100.0); +// let camera_id = slab.append( +// &device, +// &queue, +// &Camera { +// projection, +// view, +// ..Default::default() +// }, +// ); +// let unit = RenderUnit { +// vertices, +// camera: camera_id, +// position: Vec3::new(50.0, 50.0, 0.0), +// scale: Vec3::new(50.0, 50.0, 1.0), +// ..Default::default() +// }; +// let unit_id = slab.append(&device, &queue, &unit); + +// // Create a bindgroup for the slab so our shader can read out the types. +// let label = Some("slabbed isosceles triangle"); +// let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { +// label, +// entries: &[wgpu::BindGroupLayoutEntry { +// binding: 0, +// 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, +// }], +// }); +// let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { +// label, +// bind_group_layouts: &[&bindgroup_layout], +// push_constant_ranges: &[], +// }); + +// let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { +// label, +// layout: Some(&pipeline_layout), +// vertex: wgpu::VertexState { +// module: &device.create_shader_module(wgpu::include_spirv!( +// "linkage/tutorial-slabbed_render_unit.spv" +// )), +// entry_point: "tutorial::slabbed_render_unit", +// buffers: &[], +// }, +// primitive: wgpu::PrimitiveState { +// topology: wgpu::PrimitiveTopology::TriangleList, +// strip_index_format: None, +// front_face: wgpu::FrontFace::Ccw, +// cull_mode: None, +// unclipped_depth: false, +// polygon_mode: wgpu::PolygonMode::Fill, +// conservative: false, +// }, +// depth_stencil: Some(wgpu::DepthStencilState { +// format: wgpu::TextureFormat::Depth32Float, +// depth_write_enabled: true, +// depth_compare: wgpu::CompareFunction::Less, +// stencil: wgpu::StencilState::default(), +// bias: wgpu::DepthBiasState::default(), +// }), +// multisample: wgpu::MultisampleState { +// mask: !0, +// alpha_to_coverage_enabled: false, +// count: 1, +// }, +// fragment: Some(wgpu::FragmentState { +// module: &device.create_shader_module(wgpu::include_spirv!( +// "linkage/tutorial-passthru_fragment.spv" +// )), +// entry_point: "tutorial::passthru_fragment", +// targets: &[Some(wgpu::ColorTargetState { +// format: wgpu::TextureFormat::Rgba8UnormSrgb, +// blend: Some(wgpu::BlendState::ALPHA_BLENDING), +// write_mask: wgpu::ColorWrites::ALL, +// })], +// }), +// multiview: None, +// }); + +// let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { +// label, +// layout: &bindgroup_layout, +// entries: &[wgpu::BindGroupEntry { +// binding: 0, +// resource: slab.get_buffer().as_entire_binding(), +// }], +// }); + +// struct App { +// pipeline: wgpu::RenderPipeline, +// bindgroup: wgpu::BindGroup, +// unit_id: Id, +// unit: RenderUnit, +// } + +// let app = App { +// pipeline, +// bindgroup, +// unit_id, +// unit, +// }; +// r.graph.add_resource(app); + +// fn render( +// (device, queue, app, frame, depth): ( +// View, +// View, +// View, +// View, +// View, +// ), +// ) -> Result<(), GraphError> { +// let label = Some("slabbed isosceles triangle"); +// let mut encoder = +// device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); +// { +// let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { +// label, +// color_attachments: &[Some(wgpu::RenderPassColorAttachment { +// view: &frame.view, +// resolve_target: None, +// ops: wgpu::Operations { +// load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), +// store: true, +// }, +// })], +// depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { +// view: &depth.view, +// depth_ops: Some(wgpu::Operations { +// load: wgpu::LoadOp::Load, +// store: true, +// }), +// stencil_ops: None, +// }), +// }); +// render_pass.set_pipeline(&app.pipeline); +// render_pass.set_bind_group(0, &app.bindgroup, &[]); +// render_pass.draw( +// 0..app.unit.vertices.len() as u32, +// app.unit_id.inner()..app.unit_id.inner() + 1, +// ); +// } +// queue.submit(std::iter::once(encoder.finish())); +// Ok(()) +// } + +// use crate::frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}; +// r.graph.add_subgraph(graph!( +// create_frame +// < clear_frame_and_depth +// < render +// < copy_frame_to_post +// < present +// )); + +// let img = r.render_image().unwrap(); +// img_diff::assert_img_eq("tutorial/slabbed_render_unit_camera.png", img); +// } +// } + // #[wasm_bindgen_test] // async fn can_clear_background() { // let ctx = Context::try_new_headless(100, 100, None).await.unwrap(); diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 6e33bf05..f69dc3ff 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -21,10 +21,14 @@ enum Command { #[clap(long)] from_cargo: bool, }, - /// Run the webdriver proxy server - WebdriverProxy, + /// Run the WASM test server + WasmServer, /// Compile for WASM and run headless browser tests - TestWasm, + TestWasm { + /// Cargo args. + #[clap(last = true)] + args: Vec, + }, } #[derive(Parser)] @@ -68,28 +72,28 @@ async fn main() { let paths = renderling_build::RenderlingPaths::new().unwrap(); paths.generate_linkage(from_cargo, wgsl, only_fn_with_name); } - Command::TestWasm => { + Command::TestWasm { args } => { log::info!("testing WASM"); let _proxy_handle = tokio::spawn(server::serve()); - let mut test_handle = tokio::process::Command::new("wasm-pack") - .args([ - "test", - "--headless", - "--firefox", - "crates/renderling", - "--features", - "wasm", - "--jobs", - "1", - ]) - .spawn() - .unwrap(); + let mut test_handle = tokio::process::Command::new("wasm-pack"); + test_handle.args([ + "test", + "--headless", + "--firefox", + "crates/renderling", + "--features", + "wasm", + ]); + if !args.is_empty() { + test_handle.arg("--").args(args); + } + let mut test_handle = test_handle.spawn().unwrap(); let status = test_handle.wait().await.unwrap(); if !status.success() { panic!("Testing WASM failed :("); } } - Command::WebdriverProxy => { + Command::WasmServer => { server::serve().await; } } From 3f486ee9ad1f9b3e7bc7d58787d0e5e61f54a9ae Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Sun, 31 Aug 2025 11:40:18 +1200 Subject: [PATCH 06/22] add tutorial shaders back for WASM tests, renderling-build: when generating WGSL don't flip y --- crates/renderling/tests/wasm.rs | 112 ++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 14 deletions(-) diff --git a/crates/renderling/tests/wasm.rs b/crates/renderling/tests/wasm.rs index 3f3cb3e1..ba65aa92 100644 --- a/crates/renderling/tests/wasm.rs +++ b/crates/renderling/tests/wasm.rs @@ -145,19 +145,11 @@ async fn can_clear_background_sanity() { /// Test rendering a triangle using no mesh geometry. #[wasm_bindgen_test] async fn implicit_isosceles_triangle() { - let instance = renderling::internal::new_instance(None); - let (_adapter, device, queue, target) = - renderling::internal::new_headless_device_queue_and_target(100, 100, &instance) - .await - .unwrap(); - let runtime = WgpuRuntime { - device: device.into(), - queue: queue.into(), - }; - let label = Some("implicit isosceles triangle"); + let ctx = Context::headless(100, 100).await; + let runtime = ctx.as_ref(); // The first time through render with handwritten WGSL to ensure the setup works - let mut hand_written_wgsl_pipeline = { + let hand_written_wgsl_pipeline = { let vertex = runtime.device.create_shader_module(wgpu::include_wgsl!( "../src/tutorial/implicit_isosceles_vertex.wgsl" )); @@ -167,7 +159,7 @@ async fn implicit_isosceles_triangle() { runtime .device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label, + label: None, layout: None, vertex: wgpu::VertexState { module: &vertex, @@ -212,7 +204,100 @@ async fn implicit_isosceles_triangle() { runtime .device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label, + label: None, + layout: None, + vertex: wgpu::VertexState { + module: &vertex.module, + entry_point: Some(vertex.entry_point), + compilation_options: wgpu::PipelineCompilationOptions::default(), + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + module: &fragment.module, + entry_point: Some(fragment.entry_point), + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + multiview: None, + cache: None, + }) + }; + + async fn render(runtime: &WgpuRuntime, frame: &Frame, pipeline: wgpu::RenderPipeline) { + let mut encoder = runtime + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame.view(), + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::GREEN), + store: wgpu::StoreOp::Store, + }, + })], + ..Default::default() + }); + render_pass.set_pipeline(&pipeline); + render_pass.draw(0..3, 0..1); + } + let _index = runtime.queue.submit(std::iter::once(encoder.finish())); + + let img = frame + .read_image() + .await + .expect_throw("could not read frame"); + assert_img_eq("tutorial/implicit_isosceles_triangle.png", img).await; + } + + let frame = ctx.get_next_frame().unwrap(); + render(runtime, &frame, hand_written_wgsl_pipeline).await; + frame.present(); + let frame = ctx.get_next_frame().unwrap(); + render(runtime, &frame, linkage_pipeline).await; +} + +/// Test rendering a triangle from vertices on a slab, without an instance_index. +#[wasm_bindgen_test] +async fn slabbed_vertices_no_instance() { + let instance = renderling::internal::new_instance(None); + let (_adapter, device, queue, target) = + renderling::internal::new_headless_device_queue_and_target(100, 100, &instance) + .await + .unwrap(); + let runtime = WgpuRuntime { + device: device.into(), + queue: queue.into(), + }; + + let linkage_pipeline = { + let vertex = renderling::linkage::slabbed_vertices_no_instance::linkage(&runtime.device); + let fragment = renderling::linkage::passthru_fragment::linkage(&runtime.device); + runtime + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, layout: None, vertex: wgpu::VertexState { module: &vertex.module, @@ -279,7 +364,6 @@ async fn implicit_isosceles_triangle() { assert_img_eq("tutorial/implicit_isosceles_triangle.png", img).await; } - render(&runtime, &target, hand_written_wgsl_pipeline).await; render(&runtime, &target, linkage_pipeline).await; } From d60915987fab9b2ba09aebfdefe4b715d4e686f7 Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Sun, 31 Aug 2025 17:32:04 +1200 Subject: [PATCH 07/22] fixed wasm --- Cargo.lock | 13 +- Cargo.toml | 1 + crates/example-wasm/src/lib.rs | 114 +-- crates/renderling/Cargo.toml | 1 + .../shaders/tutorial-slabbed_renderlet.spv | Bin 44200 -> 44200 bytes .../shaders/tutorial-slabbed_vertices.spv | Bin 5708 -> 2556 bytes crates/renderling/src/stage/cpu.rs | 134 +++- crates/renderling/src/tutorial.rs | 11 +- crates/renderling/tests/wasm.rs | 755 +++++++----------- 9 files changed, 483 insertions(+), 546 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e1d7140..6784572e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,6 +768,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "web-sys", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1201,7 +1211,7 @@ name = "example-wasm" version = "0.1.0" dependencies = [ "console_error_panic_hook", - "console_log", + "console_log 0.2.2", "example", "fern", "futures-lite 1.13.0", @@ -3727,6 +3737,7 @@ dependencies = [ "async-channel 1.9.0", "bytemuck", "cfg_aliases", + "console_log 1.0.0", "craballoc", "crabslab", "crunch", diff --git a/Cargo.toml b/Cargo.toml index 2618873d..7e6a821c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ axum = "0.8.4" bytemuck = { version = "1.19.0", features = ["derive"] } cfg_aliases = "0.2" clap = { version = "4.5.23", features = ["derive"] } +console_log = "1.0.0" craballoc = { version = "0.2.3" } crabslab = { version = "0.6.5", default-features = false } plotters = "0.3.7" diff --git a/crates/example-wasm/src/lib.rs b/crates/example-wasm/src/lib.rs index cbc9c087..30ba1cbf 100644 --- a/crates/example-wasm/src/lib.rs +++ b/crates/example-wasm/src/lib.rs @@ -7,8 +7,8 @@ use web_sys::HtmlCanvasElement; mod event; mod req_animation_frame; -// const HDR_IMAGE_BYTES: &[u8] = include_bytes!("../../../img/hdr/helipad.hdr"); -// const GLTF_FOX_BYTES: &[u8] = include_bytes!("../../../gltf/Fox.glb"); +const HDR_IMAGE_BYTES: &[u8] = include_bytes!("../../../img/hdr/helipad.hdr"); +const GLTF_FOX_BYTES: &[u8] = include_bytes!("../../../gltf/Fox.glb"); fn surface_from_canvas(_canvas: HtmlCanvasElement) -> Option> { #[cfg(target_arch = "wasm32")] @@ -25,43 +25,18 @@ pub struct App { ctx: Context, ui: Ui, path: UiPath, - // stage: Stage, - // doc: GltfDocument, - // camera: Hybrid, - // text: UiText, + stage: Stage, + doc: GltfDocument, + camera: Hybrid, + text: UiText, } impl App { fn tick(&self) { let frame = self.ctx.get_next_frame().unwrap(); self.ui.render(&frame.view()); - // // self.stage.render(&frame.view()); - // let mut encoder = self - // .ctx - // .get_device() - // .create_command_encoder(&Default::default()); - // { - // let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - // color_attachments: &[Some(wgpu::RenderPassColorAttachment { - // view: &frame.view(), - // depth_slice: None, - // resolve_target: None, - // ops: wgpu::Operations { - // load: wgpu::LoadOp::Clear(wgpu::Color::RED), - // store: wgpu::StoreOp::Store, - // }, - // })], - // depth_stencil_attachment: None, - // ..Default::default() - // }); - // render_pass. - // } - + self.stage.render(&frame.view()); frame.present(); - self.ctx - .get_device() - .poll(wgpu::PollType::Wait) - .expect_throw("Error polling"); } } @@ -102,56 +77,47 @@ pub async fn main() { .with_circle(Vec2::splat(100.0), 20.0) .with_fill_color(Vec4::new(1.0, 1.0, 0.0, 1.0)) .fill(); - // let _ = ui - // .load_font("Recursive Mn Lnr St Med Nerd Font Complete.ttf") - // .await - // .expect_throw("Could not load font"); - // let text = ui - // .new_text() - // .with_color( - // // white - // Vec4::ONE, - // ) - // .with_section(Section::default().add_text(Text::new("WASM example").with_scale(24.0))) - // .build(); - - // let stage = ctx - // .new_stage() - // .with_background_color( - // // black - // // Vec3::ZERO.extend(1.0), - // Vec4::new(1.0, 0.0, 0.0, 1.0), - // ) - // .with_lighting(false); - - // let skybox = stage.new_skybox_from_bytes(HDR_IMAGE_BYTES).unwrap(); - // stage.set_skybox(skybox); - - // let fox = stage.load_gltf_document_from_bytes(GLTF_FOX_BYTES).unwrap(); - // log::info!("fox aabb: {:?}", fox.bounding_volume()); - - // let camera = stage.new_camera(Camera::default_perspective(800.0, 600.0)); + let _ = ui + .load_font("Recursive Mn Lnr St Med Nerd Font Complete.ttf") + .await + .expect_throw("Could not load font"); + let text = ui + .new_text() + .with_color( + // white + Vec4::ONE, + ) + .with_section(Section::default().add_text(Text::new("WASM example").with_scale(24.0))) + .build(); + + let stage = ctx + .new_stage() + .with_background_color( + // black + // Vec3::ZERO.extend(1.0), + Vec4::new(1.0, 0.0, 0.0, 1.0), + ) + .with_lighting(false); + + let skybox = stage.new_skybox_from_bytes(HDR_IMAGE_BYTES).unwrap(); + stage.set_skybox(skybox); + + let fox = stage.load_gltf_document_from_bytes(GLTF_FOX_BYTES).unwrap(); + log::info!("fox aabb: {:?}", fox.bounding_volume()); + + let camera = stage.new_camera(Camera::default_perspective(800.0, 600.0)); let app = App { ctx, ui, path, - // stage, - // doc: fox, - // camera, - // text, + stage, + doc: fox, + camera, + text, }; app.tick(); - // let mut app = example::App::new(&ctx, example::camera::CameraControl::Turntable); - // app.load_hdr_skybox(HDR_IMAGE_BYTES.to_vec()); - // app.load_default_model(); - // app.tick(); - // app.render(&ctx); - - // let window_resize = event::event_stream("resize", &dom_window); - // let mut all_events = window_resize; - loop { let _ = req_animation_frame::next_animation_frame().await; app.tick(); diff --git a/crates/renderling/Cargo.toml b/crates/renderling/Cargo.toml index aebd9c97..176a0d83 100644 --- a/crates/renderling/Cargo.toml +++ b/crates/renderling/Cargo.toml @@ -80,6 +80,7 @@ wasm-bindgen.workspace = true [dev-dependencies] assert_approx_eq.workspace = true +console_log.workspace = true ctor = "0.2.2" env_logger.workspace = true example = { path = "../example" } diff --git a/crates/renderling/shaders/tutorial-slabbed_renderlet.spv b/crates/renderling/shaders/tutorial-slabbed_renderlet.spv index 332e61755ed0036ffb39f7234bfc53b0d83744f4..f54c68e3516baa5a6dc5ffc9deadc254df81ec2c 100644 GIT binary patch literal 44200 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 diff --git a/crates/renderling/shaders/tutorial-slabbed_vertices.spv b/crates/renderling/shaders/tutorial-slabbed_vertices.spv index 8b54483bf36e5fd0d7bfd5ce4a315172a5de2556..da5e78a9ecf8d338465895ae1aedb40a506f8c5f 100644 GIT binary patch literal 2556 zcmZ9NM^9E!5QY!%AyMpN*@zdhM8JqHuxm6L>|)|ZT__+biVd(gV*fQR{27|q{((^w zjc$#-JkR~^m>Uln=AHJ=oWiB9wzVPE*QAuDq{%AQv{ajf>eo#vO-^;W-``XCaTy=I zFnV@)uy<(9nvtR2zP^E@hc6Bcj}G<^j3g>kUR6QYBjb?nR7uSR*56IWJhZvn$P&9+ zt16G1HwUp+-10A0wdWVM_0tO+_2F|?ebt_oe6B|9Hbh=$({^b)uXDPzozHkXBIgXW ztK3Il^qC5tNnM6kTp4x?x+cdd&1DLEWJq&y4pqC7+|xwF^O!}k$1jm~;`k1$X-j)a%4#GECbh+ z>`t)f)l9zkkrmR$O!6#8qIL!N56c-_!0tmX_t}MZE`5=|vamZ+L%!ct*v?&+4sO%h z)&Kc}V;%km#Gam=cW;ldz5B48$-Ov}`|*zD_N+UK_corO`|v#VnKJ>O{hcrRHsbRR z!k&n2udpX!J4+W{?+r6w-s4xM*o)|*vs_@pXdGr(eM3y zA^RoT^_@&(Pmvdhy~MW*u4mw=dt7im$L9>jzL77Nb=H?{{hv8)`@cf0liN37u5)Zh zyPU(E9mq~ZKJg|_#do(0aZjGJy<_jY!N$gldG{1?-(}S91xKy%sO!Ef!dOsqc ze)r*Q&fz>|Tfh6V<^W=yv-%dy-B-+V5NzBPzC&Pr-f!H^8?bk1%rp6jc6lbo-XiZ1 zXAwJ#IokT~7X99Xecy7$d3*pnhp}><_3j}yLNy2W*{vd^hGg#}Ty4In3!r`VjfVargb; z3bF*TcijC^u(9hzThRlEzBspIVDroG=s0$884s=#U}M2G2-X)|C&6-cA~Dw~aBvw9 zt|73o;5rS~*He7g&VZfO9>$`_S+G6)Mn;cw;OJpIdYlIviyp&ZeZe&XmdkHeaE)RI zm+|1b05%p}7s2|1>k?QlzjwiP89TU)2iFy_vEaH2))!pYz;gMm46f_g!DT$SZh(yi z*G;g#9^U){^ex1FJFk6=`yKH6J%)JB=J+m;pnX@qud=P*cWTXTWDN1``TaN7zj^;( z+(C@H^x6L|qL0+v=Nr6-nCmi6-+e@1IqyN?t3}6t9)hhm-*`K+fxnY+Xk+4fw0(cU HwhH+RQ>@{y literal 5708 zcmZ9QSBzCv6oxO%018M^q!<|ou>=?!s8nGFQL1JzQ4@o)l4xRLqUe(iBvEV+zNpav zVy_gz-n#;Jtf<(#i3RM@@4K^qIC1M@{j2PMtzFJN2O2sL?N;m3saC7?u5D5=`qnzv zgz8WF)oQ(J4RybN_U1o_@pEx5=o_fN$bI+Q0`YChIt}(M# zpVf%&f^J8^0mKeKYgi&%0uKk6e$vX}6%w)o!YF zuRkC5W9?|?nCBTO);St&9sSzJ6x-aa|5)%YNc!+@+eg-R9N50AVoZ)Xi1()|#;kWd zlD&w|`FRK2WAD^J^jZwhSf2RS;nQ}_Ej(R=(ChKdLfe!3>b~29wY--~*7Z8Mt|j&x z*zR%cH?iHD*l%HPSFtx>Z(p(B#_nFR-@)!tvERk+S+U>4-l1Z@kKLOzVtGO?QA3pED^OT*BcE7y?7a*=x+dFV!u@Axap3O%* zlh_Nu-hnA_8()a{j5zMa=d-$ig8F@?eoM$$5qnYpfi_2bA$mdKz62be zRfYRfeDmQoR-Nbk&8)=wmx0Zbs}4{ z=jJ2Xi_5{rE-L+AQR4o#W$u;W%r&05SAmVSGuL~5H6ow>Y9IFFy3Dx-G0)z7F6?=B zseLWjxFf#n!1_qtdN$W%c!tK@lbZ}O+!JHVkQ)&9K)tZ=F99D*o^!L_rTDCC%%0_P zo*Y$MziaTm+zg(KHplta$h}z(UP#P3_I5wE!`_UoL{=cyP+P+sZDYANtHI&Py;+0L z+QzE$oS%F10N6aaTB&Od`&euf4Er!=33w@DUA27XXdBDf-w4jxTRUgJ48O7Si2B=o z6QVEsdNbJk>OHyz-&QVgxNZd-3)gL6ec`$tESJw>`nm%gF5}_46KpJ8cY*b_mv`LV zV0*QOvDCN+Y>jE?)VLR%8pcy&IoMcgtN`l^*GjNl)6wBt1rC?-aNP$s7OvG`ec@UI zmdoG!aNUnDT*kxo0N7Z#9t7(P*F#{rd{>0)VSM2-9lja+$HCU|9h5px;7c9jsq-Y*Sn50l))%g)!E*Wj3fD9E!eu;M&w`DG>p8GKf1mtj zcpi~ofB0Vj%kTRz{4e4Qzwz+D1U44_m%;kNwH7Rw@6vFs!xt{&;aU$i7Oq#o`qJyG zVEOfj|244uzMI4UI==855C0orW8r@jtS?+|f#veO9m9JZaJ>tb z%Wr~ky@xMc#>4eK*jTte0PD;1xe;uy)-aYDAA+sncSdS_gfBIWr^d%%W2vzTtS?-j zfaUTVC0w843zzY5eFiobuFt{x!u17MF28TW^(DS=84uT2U}NF>8muo|-+<-v+bCS$ z;tQAYaD4|h7OwBX`tp4K0N#Xrj#$Td>ih_{j^A7pc{f~$JqPg}@Dtj1fI9E&pTT~w zET|cCovY6K`!}%j9r67R)(0iOAsydae}K(3=KJbTw8Qt6vA>YN z5&6|e@q0cHy*(tO(NhtJYjCc4&dHj)k|+N}By+pr)1UtG9kU%i^USaE$(ucKADnLu z=i5_a-SNv|%>ImJ?LDxKJL2nEe7Zf)8R#7_%yln%8DRXyZ?FE|#qNU^_T>-PtIm3N z0(;JK=PvaFH{x@@jOQ-x3^vxrSxh0;AMBlQuJ=RR9QVRrtYz)0ZBJF(yv_U#V>~YUABuy-1WIbu$`;UIqeR1z9YV&#iu)Wc@GS8jd^~18f3UOV|yWc zBl4?r9}fio|1LY%Jm+N12Y}_T-sO?l`qN+T@~Dz$zIzfrb@s%4aK1I1Z%>IG1eU{? z{Ta*J4+a}|#5cP5bmuOQ!7$go7;AvxUDiLY*yGW{3H;%D)miUEu;(my?(!sXBl+%^ z@!aKBu(A9uwt@9|w`Y?(8SEXA%lo2jj(cNo*0z_bZI4ykyxi?W!QO3iTG7sJL)*V| zykByvJ%97PkIluew)Yn1Oaa?ld-?Z(!?5qh_FHQs;%~b5-#ou%{6?_GVzf2Xp4%Yw zRC1gzzyGeV6rX2wIC>hkafiPB(bKW@JK~#Be7fVCS$v)G~-!`v=b$::new(0)); pretty_assertions::assert_eq!(stage.geometry_descriptor().get(), pbr_desc); } + + #[test] + fn slabbed_vertices_native() { + let ctx = Context::headless(100, 100).block(); + let runtime = ctx.as_ref(); + + // Create our geometry on the slab. + let slab = SlabAllocator::new( + runtime, + "slabbed_isosceles_triangle", + wgpu::BufferUsages::empty(), + ); + + let geometry = vec![ + (Vec3::new(0.5, -0.5, 0.0), Vec4::new(1.0, 0.0, 0.0, 1.0)), + (Vec3::new(0.0, 0.5, 0.0), Vec4::new(0.0, 1.0, 0.0, 1.0)), + (Vec3::new(-0.5, -0.5, 0.0), Vec4::new(0.0, 0.0, 1.0, 1.0)), + (Vec3::new(-1.0, 1.0, 0.0), Vec4::new(1.0, 0.0, 0.0, 1.0)), + (Vec3::new(-1.0, 0.0, 0.0), Vec4::new(0.0, 1.0, 0.0, 1.0)), + (Vec3::new(0.0, 1.0, 0.0), Vec4::new(0.0, 0.0, 1.0, 1.0)), + ]; + let vertices = slab.new_array(geometry); + let array = slab.new_value(vertices.array()); + + // Create a bindgroup for the slab so our shader can read out the types. + let bindgroup_layout = + runtime + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + 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, + }], + }); + let pipeline_layout = + runtime + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&bindgroup_layout], + push_constant_ranges: &[], + }); + + let vertex = crate::linkage::slabbed_vertices::linkage(&runtime.device); + let fragment = crate::linkage::passthru_fragment::linkage(&runtime.device); + let pipeline = runtime + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + cache: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + compilation_options: wgpu::PipelineCompilationOptions::default(), + module: &vertex.module, + entry_point: Some(vertex.entry_point), + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + compilation_options: Default::default(), + module: &fragment.module, + entry_point: Some(fragment.entry_point), + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + let slab_buffer = slab.commit(); + + let bindgroup = runtime + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &bindgroup_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: slab_buffer.as_entire_binding(), + }], + }); + + let frame = ctx.get_next_frame().unwrap(); + let mut encoder = runtime.device.create_command_encoder(&Default::default()); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame.view(), + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + ..Default::default() + }); + render_pass.set_pipeline(&pipeline); + render_pass.set_bind_group(0, &bindgroup, &[]); + let id = array.id().inner(); + render_pass.draw(0..vertices.len() as u32, id..id + 1); + } + runtime.queue.submit(std::iter::once(encoder.finish())); + + let img = frame + .read_linear_image() + .block() + .expect("could not read frame"); + img_diff::assert_img_eq("tutorial/slabbed_isosceles_triangle.png", img); + } } diff --git a/crates/renderling/src/tutorial.rs b/crates/renderling/src/tutorial.rs index 8b2fb816..c8707b58 100644 --- a/crates/renderling/src/tutorial.rs +++ b/crates/renderling/src/tutorial.rs @@ -1,7 +1,7 @@ //! Shaders used in the intro tutorial and in WASM tests. use crabslab::{Array, Id, Slab, SlabItem}; -use glam::{Vec3Swizzles, Vec4}; +use glam::{Vec3, Vec3Swizzles, Vec4}; use spirv_std::spirv; use crate::{ @@ -64,7 +64,7 @@ pub fn slabbed_vertices_no_instance( #[spirv(vertex)] pub fn slabbed_vertices( // Id of the array of vertices we are rendering - #[spirv(instance_index)] instance_index: u32, + #[spirv(instance_index)] array_id: Id>, // Which vertex within the render unit are we rendering #[spirv(vertex_index)] vertex_index: u32, @@ -73,12 +73,11 @@ pub fn slabbed_vertices( out_color: &mut Vec4, #[spirv(position)] clip_pos: &mut Vec4, ) { - let array_id = Id::>::from(instance_index); let array = slab.read(array_id); let vertex_id = array.at(vertex_index as usize); - let vertex = slab.read(vertex_id); - *clip_pos = vertex.position.extend(1.0); - *out_color = vertex.color; + let (position, color) = slab.read(vertex_id); + *clip_pos = position.extend(1.0); + *out_color = color; } // TODO: fix all this documentation diff --git a/crates/renderling/tests/wasm.rs b/crates/renderling/tests/wasm.rs index ba65aa92..98cc7891 100644 --- a/crates/renderling/tests/wasm.rs +++ b/crates/renderling/tests/wasm.rs @@ -148,22 +148,21 @@ async fn implicit_isosceles_triangle() { let ctx = Context::headless(100, 100).await; let runtime = ctx.as_ref(); - // The first time through render with handwritten WGSL to ensure the setup works - let hand_written_wgsl_pipeline = { - let vertex = runtime.device.create_shader_module(wgpu::include_wgsl!( - "../src/tutorial/implicit_isosceles_vertex.wgsl" - )); - let fragment = runtime - .device - .create_shader_module(wgpu::include_wgsl!("../src/tutorial/passthru.wgsl")); + fn create_pipeline( + runtime: &WgpuRuntime, + vmodule: &wgpu::ShaderModule, + ventry_point: &str, + fmodule: &wgpu::ShaderModule, + fentry_point: &str, + ) -> wgpu::RenderPipeline { runtime .device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: None, layout: None, vertex: wgpu::VertexState { - module: &vertex, - entry_point: Some("main"), + module: vmodule, + entry_point: Some(ventry_point), compilation_options: wgpu::PipelineCompilationOptions::default(), buffers: &[], }, @@ -183,8 +182,8 @@ async fn implicit_isosceles_triangle() { count: 1, }, fragment: Some(wgpu::FragmentState { - module: &fragment, - entry_point: Some("main"), + module: fmodule, + entry_point: Some(fentry_point), targets: &[Some(wgpu::ColorTargetState { format: wgpu::TextureFormat::Rgba8UnormSrgb, blend: Some(wgpu::BlendState::ALPHA_BLENDING), @@ -195,51 +194,29 @@ async fn implicit_isosceles_triangle() { multiview: None, cache: None, }) + } + // The first time through render with handwritten WGSL to ensure the setup works + let hand_written_wgsl_pipeline = { + let vertex = runtime.device.create_shader_module(wgpu::include_wgsl!( + "../src/tutorial/implicit_isosceles_vertex.wgsl" + )); + let fragment = runtime + .device + .create_shader_module(wgpu::include_wgsl!("../src/tutorial/passthru.wgsl")); + create_pipeline(runtime, &vertex, "main", &fragment, "main") }; // The second time render with WGSL that is transpiled from Rust code and pulled in through // the renderling linkage machinery. let linkage_pipeline = { let vertex = renderling::linkage::implicit_isosceles_vertex::linkage(&runtime.device); let fragment = renderling::linkage::passthru_fragment::linkage(&runtime.device); - runtime - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: None, - layout: None, - vertex: wgpu::VertexState { - module: &vertex.module, - entry_point: Some(vertex.entry_point), - compilation_options: wgpu::PipelineCompilationOptions::default(), - buffers: &[], - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: wgpu::PolygonMode::Fill, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - mask: !0, - alpha_to_coverage_enabled: false, - count: 1, - }, - fragment: Some(wgpu::FragmentState { - module: &fragment.module, - entry_point: Some(fragment.entry_point), - targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba8UnormSrgb, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }), - multiview: None, - cache: None, - }) + create_pipeline( + runtime, + &vertex.module, + vertex.entry_point, + &fragment.module, + fragment.entry_point, + ) }; async fn render(runtime: &WgpuRuntime, frame: &Frame, pipeline: wgpu::RenderPipeline) { @@ -281,6 +258,8 @@ async fn implicit_isosceles_triangle() { /// Test rendering a triangle from vertices on a slab, without an instance_index. #[wasm_bindgen_test] async fn slabbed_vertices_no_instance() { + let _ = console_log::init_with_level(log::Level::Debug); + let instance = renderling::internal::new_instance(None); let (_adapter, device, queue, target) = renderling::internal::new_headless_device_queue_and_target(100, 100, &instance) @@ -291,14 +270,68 @@ async fn slabbed_vertices_no_instance() { queue: queue.into(), }; - let linkage_pipeline = { + // Create our geometry on the slab. + let slab = SlabAllocator::new( + &runtime, + "isosceles-triangle-no-instance", + wgpu::BufferUsages::empty(), + ); + + let initial_vertices = [ + Vertex { + position: Vec3::new(0.5, -0.5, 0.0), + color: Vec4::new(1.0, 0.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec3::new(0.0, 0.5, 0.0), + color: Vec4::new(0.0, 1.0, 0.0, 1.0), + ..Default::default() + }, + Vertex { + position: Vec3::new(-0.5, -0.5, 0.0), + color: Vec4::new(0.0, 0.0, 1.0, 1.0), + ..Default::default() + }, + ]; + + let vertices = slab.new_array(initial_vertices); + + assert_eq!(3, vertices.len()); + + // Create a bindgroup for the slab so our shader can read out the types. + + let bindgroup_layout = + runtime + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + 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, + }], + }); + let pipeline_layout = runtime + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&bindgroup_layout], + push_constant_ranges: &[], + }); + let pipeline = { let vertex = renderling::linkage::slabbed_vertices_no_instance::linkage(&runtime.device); let fragment = renderling::linkage::passthru_fragment::linkage(&runtime.device); runtime .device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: None, - layout: None, + layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &vertex.module, entry_point: Some(vertex.entry_point), @@ -335,398 +368,179 @@ async fn slabbed_vertices_no_instance() { }) }; - async fn render(runtime: &WgpuRuntime, target: &RenderTarget, pipeline: wgpu::RenderPipeline) { - let texture = target.as_texture().expect("unexpected RenderTarget"); - let view = texture.create_view(&Default::default()); - let mut encoder = runtime - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - depth_slice: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::GREEN), - store: wgpu::StoreOp::Store, - }, - })], - ..Default::default() - }); - render_pass.set_pipeline(&pipeline); - render_pass.draw(0..3, 0..1); - } - let _index = runtime.queue.submit(std::iter::once(encoder.finish())); - - let buffer = CopiedTextureBuffer::new(runtime, texture).unwrap(); - let img = buffer.convert_to_rgba().await.unwrap(); - assert_img_eq("tutorial/implicit_isosceles_triangle.png", img).await; - } - - render(&runtime, &target, linkage_pipeline).await; -} - -// #[test] -// fn slabbed_isosceles_triangle_no_instance() { -// let mut r = Renderling::headless(100, 100).unwrap(); -// let (device, queue) = r.get_device_and_queue_owned(); - -// // Create our geometry on the slab. -// // Don't worry too much about capacity, it can grow. -// let slab = crate::slab::SlabBuffer::new(&device, 16); -// let vertices = slab.append_slice( -// &device, -// &queue, -// &[ -// Vertex { -// position: Vec4::new(0.5, -0.5, 0.0, 1.0), -// color: Vec4::new(1.0, 0.0, 0.0, 1.0), -// ..Default::default() -// }, -// Vertex { -// position: Vec4::new(0.0, 0.5, 0.0, 1.0), -// color: Vec4::new(0.0, 1.0, 0.0, 1.0), -// ..Default::default() -// }, -// Vertex { -// position: Vec4::new(-0.5, -0.5, 0.0, 1.0), -// color: Vec4::new(0.0, 0.0, 1.0, 1.0), -// ..Default::default() -// }, -// ], -// ); -// assert_eq!(3, vertices.len()); - -// // Create a bindgroup for the slab so our shader can read out the types. -// let label = Some("slabbed isosceles triangle"); -// let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { -// label, -// entries: &[wgpu::BindGroupLayoutEntry { -// binding: 0, -// 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, -// }], -// }); -// let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { -// label, -// bind_group_layouts: &[&bindgroup_layout], -// push_constant_ranges: &[], -// }); - -// let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { -// label, -// layout: Some(&pipeline_layout), -// vertex: wgpu::VertexState { -// module: &device.create_shader_module(wgpu::include_spirv!( -// "linkage/tutorial-slabbed_vertices_no_instance.spv" -// )), -// entry_point: "tutorial::slabbed_vertices_no_instance", -// buffers: &[], -// }, -// primitive: wgpu::PrimitiveState { -// topology: wgpu::PrimitiveTopology::TriangleList, -// strip_index_format: None, -// front_face: wgpu::FrontFace::Ccw, -// cull_mode: None, -// unclipped_depth: false, -// polygon_mode: wgpu::PolygonMode::Fill, -// conservative: false, -// }, -// depth_stencil: Some(wgpu::DepthStencilState { -// format: wgpu::TextureFormat::Depth32Float, -// depth_write_enabled: true, -// depth_compare: wgpu::CompareFunction::Less, -// stencil: wgpu::StencilState::default(), -// bias: wgpu::DepthBiasState::default(), -// }), -// multisample: wgpu::MultisampleState { -// mask: !0, -// alpha_to_coverage_enabled: false, -// count: 1, -// }, -// fragment: Some(wgpu::FragmentState { -// module: &device.create_shader_module(wgpu::include_spirv!( -// "linkage/tutorial-passthru_fragment.spv" -// )), -// entry_point: "tutorial::passthru_fragment", -// targets: &[Some(wgpu::ColorTargetState { -// format: wgpu::TextureFormat::Rgba8UnormSrgb, -// blend: Some(wgpu::BlendState::ALPHA_BLENDING), -// write_mask: wgpu::ColorWrites::ALL, -// })], -// }), -// multiview: None, -// }); - -// let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { -// label, -// layout: &bindgroup_layout, -// entries: &[wgpu::BindGroupEntry { -// binding: 0, -// resource: slab.get_buffer().as_entire_binding(), -// }], -// }); - -// struct App { -// pipeline: wgpu::RenderPipeline, -// bindgroup: wgpu::BindGroup, -// vertices: Array, -// } - -// let app = App { -// pipeline, -// bindgroup, -// vertices, -// }; -// r.graph.add_resource(app); - -// fn render( -// (device, queue, app, frame, depth): ( -// View, -// View, -// View, -// View, -// View, -// ), -// ) -> Result<(), GraphError> { -// let label = Some("slabbed isosceles triangle"); -// let mut encoder = -// device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); -// { -// let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { -// label, -// color_attachments: &[Some(wgpu::RenderPassColorAttachment { -// view: &frame.view, -// resolve_target: None, -// ops: wgpu::Operations { -// load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), -// store: true, -// }, -// })], -// depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { -// view: &depth.view, -// depth_ops: Some(wgpu::Operations { -// load: wgpu::LoadOp::Load, -// store: true, -// }), -// stencil_ops: None, -// }), -// }); -// render_pass.set_pipeline(&app.pipeline); -// render_pass.set_bind_group(0, &app.bindgroup, &[]); -// render_pass.draw(0..app.vertices.len() as u32, 0..1); -// } -// queue.submit(std::iter::once(encoder.finish())); -// Ok(()) -// } - -// use crate::frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}; -// r.graph.add_subgraph(graph!( -// create_frame -// < clear_frame_and_depth -// < render -// < copy_frame_to_post -// < present -// )); + let slab_buffer: SlabBuffer = slab.commit(); -// let img = r.render_image().unwrap(); -// img_diff::assert_img_eq("tutorial/slabbed_isosceles_triangle_no_instance.png", img); -// } + let bindgroup = runtime + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &bindgroup_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, -// #[test] -// fn slabbed_isosceles_triangle() { -// let mut r = Renderling::headless(100, 100).unwrap(); -// let (device, queue) = r.get_device_and_queue_owned(); - -// // Create our geometry on the slab. -// // Don't worry too much about capacity, it can grow. -// let slab = crate::slab::SlabBuffer::new(&device, 16); -// let geometry = vec![ -// Vertex { -// position: Vec4::new(0.5, -0.5, 0.0, 1.0), -// color: Vec4::new(1.0, 0.0, 0.0, 1.0), -// ..Default::default() -// }, -// Vertex { -// position: Vec4::new(0.0, 0.5, 0.0, 1.0), -// color: Vec4::new(0.0, 1.0, 0.0, 1.0), -// ..Default::default() -// }, -// Vertex { -// position: Vec4::new(-0.5, -0.5, 0.0, 1.0), -// color: Vec4::new(0.0, 0.0, 1.0, 1.0), -// ..Default::default() -// }, -// Vertex { -// position: Vec4::new(-1.0, 1.0, 0.0, 1.0), -// color: Vec4::new(1.0, 0.0, 0.0, 1.0), -// ..Default::default() -// }, -// Vertex { -// position: Vec4::new(-1.0, 0.0, 0.0, 1.0), -// color: Vec4::new(0.0, 1.0, 0.0, 1.0), -// ..Default::default() -// }, -// Vertex { -// position: Vec4::new(0.0, 1.0, 0.0, 1.0), -// color: Vec4::new(0.0, 0.0, 1.0, 1.0), -// ..Default::default() -// }, -// ]; -// let vertices = slab.append_slice(&device, &queue, &geometry); -// let vertices_id = slab.append(&device, &queue, &vertices); - -// // Create a bindgroup for the slab so our shader can read out the types. -// let label = Some("slabbed isosceles triangle"); -// let bindgroup_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { -// label, -// entries: &[wgpu::BindGroupLayoutEntry { -// binding: 0, -// 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, -// }], -// }); -// let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { -// label, -// bind_group_layouts: &[&bindgroup_layout], -// push_constant_ranges: &[], -// }); + resource: slab_buffer.as_entire_binding(), + }], + }); -// let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { -// label, -// layout: Some(&pipeline_layout), -// vertex: wgpu::VertexState { -// module: &device.create_shader_module(wgpu::include_spirv!( -// "linkage/tutorial-slabbed_vertices.spv" -// )), -// entry_point: "tutorial::slabbed_vertices", -// buffers: &[], -// }, -// primitive: wgpu::PrimitiveState { -// topology: wgpu::PrimitiveTopology::TriangleList, -// strip_index_format: None, -// front_face: wgpu::FrontFace::Ccw, -// cull_mode: None, -// unclipped_depth: false, -// polygon_mode: wgpu::PolygonMode::Fill, -// conservative: false, -// }, -// depth_stencil: Some(wgpu::DepthStencilState { -// format: wgpu::TextureFormat::Depth32Float, -// depth_write_enabled: true, -// depth_compare: wgpu::CompareFunction::Less, -// stencil: wgpu::StencilState::default(), -// bias: wgpu::DepthBiasState::default(), -// }), -// multisample: wgpu::MultisampleState { -// mask: !0, -// alpha_to_coverage_enabled: false, -// count: 1, -// }, -// fragment: Some(wgpu::FragmentState { -// module: &device.create_shader_module(wgpu::include_spirv!( -// "linkage/tutorial-passthru_fragment.spv" -// )), -// entry_point: "tutorial::passthru_fragment", -// targets: &[Some(wgpu::ColorTargetState { -// format: wgpu::TextureFormat::Rgba8UnormSrgb, -// blend: Some(wgpu::BlendState::ALPHA_BLENDING), -// write_mask: wgpu::ColorWrites::ALL, -// })], -// }), -// multiview: None, -// }); + let texture = target.as_texture().expect("unexpected RenderTarget"); + let view = texture.create_view(&Default::default()); + let mut encoder = runtime + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), + store: wgpu::StoreOp::Store, + }, + })], + ..Default::default() + }); + render_pass.set_pipeline(&pipeline); + render_pass.set_bind_group(0, &bindgroup, &[]); + render_pass.draw(0..3, 0..1); + } + let _index = runtime.queue.submit(std::iter::once(encoder.finish())); -// let bindgroup = device.create_bind_group(&wgpu::BindGroupDescriptor { -// label, -// layout: &bindgroup_layout, -// entries: &[wgpu::BindGroupEntry { -// binding: 0, -// resource: slab.get_buffer().as_entire_binding(), -// }], -// }); + let buffer = CopiedTextureBuffer::new(runtime, texture).unwrap(); + let img = buffer.convert_to_rgba().await.unwrap(); + assert_img_eq("tutorial/slabbed_isosceles_triangle_no_instance.png", img).await; +} -// struct App { -// pipeline: wgpu::RenderPipeline, -// bindgroup: wgpu::BindGroup, -// vertices_id: Id>, -// vertices: Array, -// } +#[wasm_bindgen_test] +async fn slabbed_isosceles_triangle() { + let ctx = Context::headless(100, 100).await; + let runtime = ctx.as_ref(); -// let app = App { -// pipeline, -// bindgroup, -// vertices_id, -// vertices, -// }; -// r.graph.add_resource(app); + // Create our geometry on the slab. + let slab = SlabAllocator::new( + runtime, + "slabbed_isosceles_triangle", + wgpu::BufferUsages::empty(), + ); + + let geometry = vec![ + (Vec3::new(0.5, -0.5, 0.0), Vec4::new(1.0, 0.0, 0.0, 1.0)), + (Vec3::new(0.0, 0.5, 0.0), Vec4::new(0.0, 1.0, 0.0, 1.0)), + (Vec3::new(-0.5, -0.5, 0.0), Vec4::new(0.0, 0.0, 1.0, 1.0)), + (Vec3::new(-1.0, 1.0, 0.0), Vec4::new(1.0, 0.0, 0.0, 1.0)), + (Vec3::new(-1.0, 0.0, 0.0), Vec4::new(0.0, 1.0, 0.0, 1.0)), + (Vec3::new(0.0, 1.0, 0.0), Vec4::new(0.0, 0.0, 1.0, 1.0)), + ]; + let vertices = slab.new_array(geometry); + let array = slab.new_value(vertices.array()); + + // Create a bindgroup for the slab so our shader can read out the types. + let bindgroup_layout = + runtime + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + 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, + }], + }); + let pipeline_layout = runtime + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&bindgroup_layout], + push_constant_ranges: &[], + }); -// fn render( -// (device, queue, app, frame, depth): ( -// View, -// View, -// View, -// View, -// View, -// ), -// ) -> Result<(), GraphError> { -// let label = Some("slabbed isosceles triangle"); -// let mut encoder = -// device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); -// { -// let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { -// label, -// color_attachments: &[Some(wgpu::RenderPassColorAttachment { -// view: &frame.view, -// resolve_target: None, -// ops: wgpu::Operations { -// load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), -// store: true, -// }, -// })], -// depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { -// view: &depth.view, -// depth_ops: Some(wgpu::Operations { -// load: wgpu::LoadOp::Load, -// store: true, -// }), -// stencil_ops: None, -// }), -// }); -// render_pass.set_pipeline(&app.pipeline); -// render_pass.set_bind_group(0, &app.bindgroup, &[]); -// render_pass.draw( -// 0..app.vertices.len() as u32, -// app.vertices_id.inner()..app.vertices_id.inner() + 1, -// ); -// } -// queue.submit(std::iter::once(encoder.finish())); -// Ok(()) -// } + let vertex = renderling::linkage::slabbed_vertices::linkage(&runtime.device); + let fragment = renderling::linkage::passthru_fragment::linkage(&runtime.device); + let pipeline = runtime + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + cache: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + compilation_options: wgpu::PipelineCompilationOptions::default(), + module: &vertex.module, + entry_point: Some(vertex.entry_point), + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: wgpu::PolygonMode::Fill, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + mask: !0, + alpha_to_coverage_enabled: false, + count: 1, + }, + fragment: Some(wgpu::FragmentState { + compilation_options: Default::default(), + module: &fragment.module, + entry_point: Some(fragment.entry_point), + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8UnormSrgb, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + let slab_buffer = slab.commit(); + + let bindgroup = runtime + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &bindgroup_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: slab_buffer.as_entire_binding(), + }], + }); -// use crate::frame::{clear_frame_and_depth, copy_frame_to_post, create_frame, present}; -// r.graph.add_subgraph(graph!( -// create_frame -// < clear_frame_and_depth -// < render -// < copy_frame_to_post -// < present -// )); + let frame = ctx.get_next_frame().unwrap(); + let mut encoder = runtime.device.create_command_encoder(&Default::default()); + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &frame.view(), + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + ..Default::default() + }); + render_pass.set_pipeline(&pipeline); + render_pass.set_bind_group(0, &bindgroup, &[]); + let id = array.id().inner(); + render_pass.draw(0..vertices.len() as u32, id..id + 1); + } + runtime.queue.submit(std::iter::once(encoder.finish())); -// let img = r.render_image().unwrap(); -// img_diff::assert_img_eq("tutorial/slabbed_isosceles_triangle.png", img); -// } + let img = frame + .read_linear_image() + .await + .expect_throw("could not read frame"); + assert_img_eq("tutorial/slabbed_isosceles_triangle.png", img).await; +} // #[test] // fn slabbed_render_unit() { @@ -1128,17 +942,17 @@ async fn slabbed_vertices_no_instance() { // } // } -// #[wasm_bindgen_test] -// async fn can_clear_background() { -// let ctx = Context::try_new_headless(100, 100, None).await.unwrap(); -// let stage = ctx -// .new_stage() -// .with_background_color(Vec4::new(1.0, 0.0, 0.0, 1.0)); -// let frame = ctx.get_next_frame().unwrap(); -// stage.render(&frame.view()); -// let seen = frame.read_image().await.unwrap(); -// assert_img_eq("cmy_triangle/hdr.png", seen).await; -// } +#[wasm_bindgen_test] +async fn can_clear_background() { + let ctx = Context::try_new_headless(2, 2, None).await.unwrap(); + let stage = ctx + .new_stage() + .with_background_color(Vec4::new(1.0, 0.0, 0.0, 1.0)); + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let seen = frame.read_image().await.unwrap(); + assert_img_eq("clear.png", seen).await; +} // #[wasm_bindgen_test] // #[should_panic] @@ -1147,20 +961,33 @@ async fn slabbed_vertices_no_instance() { // assert_img_eq("cmy_triangle/hdr.png", img).await; // } -// #[wasm_bindgen_test] -// async fn can_render_hello_triangle() { -// // This is a wasm version of cmy_triangle_sanity -// let ctx = Context::try_new_headless(100, 100, None).await.unwrap(); -// let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); -// let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); -// let _rez = stage -// .builder() -// .with_vertices(renderling::::right_tri_vertices()) -// .build(); - -// let frame = ctx.get_next_frame().unwrap(); -// stage.render(&frame.view()); -// frame.present(); - -// let hdr_img = stage.hdr_texture().read_hdr_image(&ctx).unwrap(); -// } +fn right_tri_vertices() -> Vec { + vec![ + Vertex::default() + .with_position([0.0, 0.0, 0.0]) + .with_color([0.0, 1.0, 1.0, 1.0]), + Vertex::default() + .with_position([0.0, 100.0, 0.0]) + .with_color([1.0, 1.0, 0.0, 1.0]), + Vertex::default() + .with_position([100.0, 0.0, 0.0]) + .with_color([1.0, 0.0, 1.0, 1.0]), + ] +} + +#[wasm_bindgen_test] +async fn can_render_hello_triangle() { + // This is a wasm version of cmy_triangle_sanity + let ctx = Context::try_new_headless(100, 100, None).await.unwrap(); + let stage = ctx.new_stage().with_background_color(Vec4::splat(1.0)); + let _camera = stage.new_camera(Camera::default_ortho2d(100.0, 100.0)); + let _rez = stage.builder().with_vertices(right_tri_vertices()).build(); + + let frame = ctx.get_next_frame().unwrap(); + stage.render(&frame.view()); + let img = frame + .read_linear_image() + .await + .expect_throw("could not read frame"); + assert_img_eq("cmy_triangle/hdr.png", img).await; +} From 086aae5b432b96655558223a671b6eb1deedeca5 Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 09:25:27 +1200 Subject: [PATCH 08/22] support generating linkage for WASM when outside the workspace --- crates/renderling-build/src/lib.rs | 24 ++++++++++++++++++------ crates/renderling/src/build.rs | 3 ++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/crates/renderling-build/src/lib.rs b/crates/renderling-build/src/lib.rs index 6834a9c7..7ad7a0de 100644 --- a/crates/renderling-build/src/lib.rs +++ b/crates/renderling-build/src/lib.rs @@ -137,8 +137,10 @@ fn wgsl(spv_filepath: impl AsRef, destination: impl AsRef, pub renderling_crate: std::path::PathBuf, pub shader_dir: std::path::PathBuf, pub shader_manifest: std::path::PathBuf, @@ -148,13 +150,23 @@ pub struct RenderlingPaths { impl RenderlingPaths { /// Create a new `RenderlingPaths`. /// - /// If the `CARGO_WORKSPACE_DIR` is _not_ available, this most likely means we're building renderling - /// outside of its own source tree, which means we **don't want to compile shaders or generate linkage**. + /// If the `CARGO_WORKSPACE_DIR` and subsequently the `cargo_workspace` is + /// _not_ available, this most likely means we're building renderling + /// outside of its own source tree, which means we **don't want to compile shaders**. /// - /// For this reason we return `Option`. + /// But we may still need to transpile the packaged SPIR-V into WGSL for WASM, and + /// so `cargo_workspace` is `Option` and the entire function also returns `Option`. pub fn new() -> Option { - let cargo_workspace = std::path::PathBuf::from(std::env::var("CARGO_WORKSPACE_DIR").ok()?); - let renderling_crate = cargo_workspace.join("crates").join("renderling"); + let cargo_workspace = std::env::var("CARGO_WORKSPACE_DIR") + .map(std::path::PathBuf::from) + .ok(); + let renderling_crate = if let Some(workspace) = cargo_workspace.as_ref() { + workspace.join("crates").join("renderling") + } else { + std::env::var("CARGO_MANIFEST_DIR") + .map(std::path::PathBuf::from) + .ok()? + }; log::debug!("cargo_manifest_dir: {renderling_crate:#?}"); let shader_dir = renderling_crate.join("shaders"); diff --git a/crates/renderling/src/build.rs b/crates/renderling/src/build.rs index 10c9dd58..138e0627 100644 --- a/crates/renderling/src/build.rs +++ b/crates/renderling/src/build.rs @@ -2,7 +2,8 @@ fn main() { if std::env::var("CARGO_CFG_TARGET_ARCH").as_deref() != Ok("spirv") { - if let Some(paths) = renderling_build::RenderlingPaths::new() { + let paths = renderling_build::RenderlingPaths::new(); + if let Some(paths) = paths { paths.generate_linkage(true, true, None); } } From f2777c73bacb9b805e988cfcf8604e082a29e040 Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 09:57:05 +1200 Subject: [PATCH 09/22] wasm gh wf --- .github/workflows/push.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 553c217f..22d508c0 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -134,3 +134,26 @@ jobs: with: name: test-output-${{ runner.os }} path: test_output + + # WASM tests + renderling-wasm-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: moonrepo/setup-rust@v1 + - uses: actions/cache@v4 + with: + path: ~/.cargo + key: ${{ runner.os }}-test-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + - name: Install linux deps + if: runner.os == 'Linux' + run: | + sudo apt-get -y update + sudo apt-get -y install mesa-vulkan-drivers libvulkan1 vulkan-tools vulkan-validationlayers + - name: Install wasm-pack + run: cargo install --locked wasm-pack || true + - name: Test WASM + env: + - RUST_LOG: info + run: cargo test-wasm From de3377be127c966eff9b6cd014adf1fbd7317fec Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 10:00:00 +1200 Subject: [PATCH 10/22] wasm gh wf fix --- .github/workflows/push.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 22d508c0..5b039310 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -155,5 +155,5 @@ jobs: run: cargo install --locked wasm-pack || true - name: Test WASM env: - - RUST_LOG: info + RUST_LOG: info run: cargo test-wasm From ca30e1a604e038ef85026910b1da5c68648e35d8 Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 10:09:08 +1200 Subject: [PATCH 11/22] ci matrix --- .github/workflows/push.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 5b039310..2321471f 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -17,7 +17,7 @@ jobs: install-cargo-gpu: strategy: matrix: - os: [ubuntu-24.04, macos-latest] + os: [ubuntu-latest, ubuntu-24.04, macos-latest] runs-on: ${{ matrix.os }} defaults: run: @@ -55,7 +55,7 @@ jobs: matrix: # temporarily skip windows, revisit after a fix for this error is found: # https://github.com/rust-lang/cc-rs/issues/1331 - os: [ubuntu-latest, macos-latest] #, windows-latest] + os: [ubuntu-latest, ubuntu-24.04, macos-latest] #, windows-latest] runs-on: ${{ matrix.os }} defaults: run: From 8894c3576f2c77a986747d40593c766be42bc624 Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 13:58:17 +1200 Subject: [PATCH 12/22] can save arbitrary test artifacts on WASM --- Cargo.lock | 1 + Cargo.toml | 1 + crates/loading-bytes/src/lib.rs | 65 +++++++++++++++++++++++++++++++++ crates/renderling/Cargo.toml | 8 +++- crates/renderling/tests/wasm.rs | 25 +++++++++++++ crates/wire-types/src/lib.rs | 6 +++ crates/xtask/Cargo.toml | 1 + crates/xtask/src/server.rs | 40 ++++++++++++++++++-- 8 files changed, 143 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6784572e..7c6122ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5888,6 +5888,7 @@ dependencies = [ "axum", "clap 4.5.40", "env_logger", + "futures-util", "image 0.25.6", "img-diff", "log", diff --git a/Cargo.toml b/Cargo.toml index 7e6a821c..65e707e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ ctor = "0.2.2" dagga = "0.2.1" env_logger = "0.10.0" futures-lite = "1.13" +futures-util = "0.3.31" glam = { version = "0.30", default-features = false } gltf = { version = "1.4,1", features = ["KHR_lights_punctual", "KHR_materials_unlit", "KHR_materials_emissive_strength", "extras", "extensions"] } glyph_brush = "0.7.8" diff --git a/crates/loading-bytes/src/lib.rs b/crates/loading-bytes/src/lib.rs index 8aafe561..0d3d6bd2 100644 --- a/crates/loading-bytes/src/lib.rs +++ b/crates/loading-bytes/src/lib.rs @@ -175,6 +175,71 @@ pub async fn post_json_wasm( Ok(t) } +// TODO: deduplicate post_bin_wasm and post_json_wasm +pub async fn post_bin_wasm( + path: &str, + data: &[u8], +) -> Result { + use js_sys::JsString; + use wasm_bindgen::JsCast; + + let path = path.to_string(); + let opts = web_sys::RequestInit::new(); + opts.set_method("POST"); + let headers = js_sys::Object::new(); + js_sys::Reflect::set( + &headers, + &JsString::from("content-type"), + &JsString::from("application/octet-stream"), + ) + .unwrap(); + opts.set_headers(&headers); + let body = js_sys::Uint8Array::from(data); + opts.set_body(&body.into()); + let request = web_sys::Request::new_with_str_and_init(&path, &opts).map_err(|msg| { + CreateRequestSnafu { + path: path.clone(), + msg: send_wrapper::SendWrapper::new(msg), + } + .build() + })?; + let window = web_sys::window().unwrap(); + let resp_value = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request)) + .await + .map_err(|msg| { + FetchSnafu { + path: path.clone(), + msg: send_wrapper::SendWrapper::new(msg), + } + .build() + })?; + let resp: web_sys::Response = resp_value.dyn_into().map_err(|msg| { + NotAResponseSnafu { + path: path.clone(), + msg: send_wrapper::SendWrapper::new(msg), + } + .build() + })?; + + snafu::ensure!( + resp.ok(), + OtherSnafu { + other: wasm_bindgen_futures::JsFuture::from(resp.text().unwrap()) + .await + .unwrap() + .as_string() + .unwrap() + } + ); + + let value = wasm_bindgen_futures::JsFuture::from(resp.text().unwrap_throw()) + .await + .unwrap_throw(); + let s = value.as_string().expect_throw(&format!("{value:#?}")); + let t = serde_json::from_str::(&s).unwrap_throw(); + Ok(t) +} + /// Load the file at the given url fragment or path and return it as a vector of bytes, if /// possible. pub async fn load(path: &str) -> Result, LoadingBytesError> { diff --git a/crates/renderling/Cargo.toml b/crates/renderling/Cargo.toml index 176a0d83..40e726e3 100644 --- a/crates/renderling/Cargo.toml +++ b/crates/renderling/Cargo.toml @@ -92,7 +92,6 @@ img-diff = { path = "../img-diff" } naga.workspace = true ttf-parser = "0.20.0" wasm-bindgen-test.workspace = true -web-sys.workspace = true wgpu-core.workspace = true winit.workspace = true wire-types = { path = "../wire-types" } @@ -102,3 +101,10 @@ glam = { workspace = true, features = ["std", "debug-glam-assert"] } [target.'cfg(target_os = "macos")'.dev-dependencies] metal.workspace = true + +[dev-dependencies.web-sys] +workspace = true +features = [ + "Navigator", + "Window" +] diff --git a/crates/renderling/tests/wasm.rs b/crates/renderling/tests/wasm.rs index 98cc7891..6e9744af 100644 --- a/crates/renderling/tests/wasm.rs +++ b/crates/renderling/tests/wasm.rs @@ -10,6 +10,31 @@ use wire_types::{Error, PixelType}; wasm_bindgen_test_configure!(run_in_browser); +#[wasm_bindgen_test] +/// Writes a textfile containing some system info. +/// +/// If you need more info on CI etc, add it here. +async fn can_write_system_info_artifact() { + let user_agent = web_sys::window() + .expect_throw("no window") + .navigator() + .user_agent() + .expect_throw("no user agent"); + + let table = std::collections::HashMap::::from_iter(Some(( + "user_agent".to_owned(), + user_agent, + ))); + let file = format!("{table:#?}"); + loading_bytes::post_bin_wasm::>( + "http://127.0.0.1:4000/artifact/info.txt", + file.as_bytes(), + ) + .await + .unwrap_throw() + .unwrap_throw(); +} + #[wasm_bindgen_test] async fn can_create_headless_ctx() { let _ctx = renderling::Context::try_new_headless(256, 256, None) diff --git a/crates/wire-types/src/lib.rs b/crates/wire-types/src/lib.rs index 3fec866b..f8ff03d5 100644 --- a/crates/wire-types/src/lib.rs +++ b/crates/wire-types/src/lib.rs @@ -18,3 +18,9 @@ pub struct Image { pub struct Error { pub description: String, } + +impl From for Error { + fn from(description: String) -> Self { + Error { description } + } +} diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml index 9d265715..9088d57c 100644 --- a/crates/xtask/Cargo.toml +++ b/crates/xtask/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" axum.workspace = true clap.workspace = true env_logger.workspace = true +futures-util.workspace = true image.workspace = true img-diff = { path = "../img-diff" } log.workspace = true diff --git a/crates/xtask/src/server.rs b/crates/xtask/src/server.rs index e7e87856..551cb99a 100644 --- a/crates/xtask/src/server.rs +++ b/crates/xtask/src/server.rs @@ -13,6 +13,7 @@ use axum::{ }; use image::DynamicImage; use img_diff::DiffCfg; +use tokio::io::AsyncWriteExt; use wire_types::Error; pub async fn serve() { @@ -23,6 +24,8 @@ pub async fn serve() { .route("/assert_img_eq/{*filename}", post(assert_img_eq)) .route("/save/{*filename}", options(accept)) .route("/save/{*filename}", post(save)) + .route("/artifact/{*filename}", options(accept)) + .route("/artifact/{*filename}", post(artifact)) .route("/{*rest}", any(accept)); let listener = tokio::net::TcpListener::bind("127.0.0.1:4000") .await @@ -30,6 +33,19 @@ pub async fn serve() { axum::serve(listener, app).await.unwrap(); } +/// Responds with access control headers to allow anything from anywhere. +async fn accept(request: Request) -> Response { + log::info!("accept: {request:#?}"); + Response::builder() + .status(StatusCode::OK) + .header("accept", "*/*") + .header("access-control-allow-origin", "*") + .header("access-control-allow-methods", "*") + .header("access-control-allow-headers", "*") + .body(Body::default()) + .unwrap() +} + async fn static_file(Path(path): Path) -> Result { log::info!("requested static '{path}'"); let test_img = std::path::PathBuf::from(std::env!("CARGO_WORKSPACE_DIR")).join("test_img"); @@ -142,14 +158,32 @@ async fn save( .unwrap() } -async fn accept(request: Request) -> Response { - log::info!("accept: {request:#?}"); +async fn artifact_inner(filename: impl AsRef, body: Body) -> Result<(), Error> { + use futures_util::StreamExt; + + let mut byte_stream = body.into_data_stream(); + let mut file = tokio::fs::File::create(filename) + .await + .map_err(|e| Error::from(e.to_string()))?; + while let Some(result_bytes) = byte_stream.next().await { + let bytes = result_bytes.map_err(|e| Error::from(e.to_string()))?; + file.write_all(&bytes) + .await + .map_err(|e| Error::from(e.to_string()))?; + } + Ok(()) +} + +async fn artifact(Path(parts): Path>, body: Body) -> Response { + let filename = std::path::PathBuf::from(img_diff::WASM_TEST_OUTPUT_DIR).join(parts.join("/")); + log::info!("saving artifact to {filename:?}"); + let result = artifact_inner(filename, body).await; Response::builder() .status(StatusCode::OK) .header("accept", "*/*") .header("access-control-allow-origin", "*") .header("access-control-allow-methods", "*") .header("access-control-allow-headers", "*") - .body(Body::default()) + .body(Json(result).into_response().into_body()) .unwrap() } From d4417e90e5a2560e8c80773ff265d561bf94c916 Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 14:09:59 +1200 Subject: [PATCH 13/22] create artifact dir first --- crates/renderling/tests/wasm.rs | 3 +++ crates/xtask/src/server.rs | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/crates/renderling/tests/wasm.rs b/crates/renderling/tests/wasm.rs index 6e9744af..838f7e93 100644 --- a/crates/renderling/tests/wasm.rs +++ b/crates/renderling/tests/wasm.rs @@ -15,11 +15,14 @@ wasm_bindgen_test_configure!(run_in_browser); /// /// If you need more info on CI etc, add it here. async fn can_write_system_info_artifact() { + let _ = console_log::init(); + let user_agent = web_sys::window() .expect_throw("no window") .navigator() .user_agent() .expect_throw("no user agent"); + log::info!("user_agent: {user_agent}"); let table = std::collections::HashMap::::from_iter(Some(( "user_agent".to_owned(), diff --git a/crates/xtask/src/server.rs b/crates/xtask/src/server.rs index 551cb99a..1dfd8f61 100644 --- a/crates/xtask/src/server.rs +++ b/crates/xtask/src/server.rs @@ -162,6 +162,14 @@ async fn artifact_inner(filename: impl AsRef, body: Body) -> Re use futures_util::StreamExt; let mut byte_stream = body.into_data_stream(); + tokio::fs::create_dir_all( + filename + .as_ref() + .parent() + .ok_or_else(|| Error::from(format!("'{:?}' has no parent dir", filename.as_ref())))?, + ) + .await + .map_err(|e| Error::from(e.to_string()))?; let mut file = tokio::fs::File::create(filename) .await .map_err(|e| Error::from(e.to_string()))?; From 7dee82dfe436f6ef0eb73e923ede805061095074 Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 14:15:48 +1200 Subject: [PATCH 14/22] use chrome on CI for WASM testing --- .github/workflows/push.yaml | 2 +- crates/xtask/src/main.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 2321471f..fcb2d067 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -156,4 +156,4 @@ jobs: - name: Test WASM env: RUST_LOG: info - run: cargo test-wasm + run: cargo test-wasm --chrome diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index f69dc3ff..514c82bd 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -28,6 +28,9 @@ enum Command { /// Cargo args. #[clap(last = true)] args: Vec, + /// Set to use chrome, otherwise firefox will be used. + #[clap(long)] + chrome: bool, }, } @@ -72,14 +75,14 @@ async fn main() { let paths = renderling_build::RenderlingPaths::new().unwrap(); paths.generate_linkage(from_cargo, wgsl, only_fn_with_name); } - Command::TestWasm { args } => { + Command::TestWasm { args, chrome } => { log::info!("testing WASM"); let _proxy_handle = tokio::spawn(server::serve()); let mut test_handle = tokio::process::Command::new("wasm-pack"); test_handle.args([ "test", "--headless", - "--firefox", + if chrome { "--chrome" } else { "--firefox" }, "crates/renderling", "--features", "wasm", From 92399b1b0bc87ec017ab1f0234f718a38d368f4b Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 14:19:15 +1200 Subject: [PATCH 15/22] save wasm artifacts --- .github/workflows/push.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index fcb2d067..6f223f0d 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -157,3 +157,8 @@ jobs: env: RUST_LOG: info run: cargo test-wasm --chrome + - uses: actions/upload-artifact@v4 + if: always() + with: + name: test-output-${{ runner.os }}-wasm + path: test_output From f9fac302ae527754432a4849d5c350b219db9ce5 Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 14:37:08 +1200 Subject: [PATCH 16/22] CI: thrash on enabling webgpu on chrome --- .github/workflows/push.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 6f223f0d..f0433e58 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -156,6 +156,7 @@ jobs: - name: Test WASM env: RUST_LOG: info + WASM_BINDGEN_TEST_BROWSER: "chrome --headless --enable-unsafe-webgpu" run: cargo test-wasm --chrome - uses: actions/upload-artifact@v4 if: always() From 15d9631f021e8ec112f27aaeacbeb4d90bc1ce2b Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 15:20:23 +1200 Subject: [PATCH 17/22] ci: thrash to get a webgpu enabled browser --- .github/workflows/push.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index f0433e58..0a8eb711 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -137,6 +137,10 @@ jobs: # WASM tests renderling-wasm-test: + strategy: + matrix: + # empty string means ff, --chrome is chrome + browser: ["", "--chrome"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -156,8 +160,7 @@ jobs: - name: Test WASM env: RUST_LOG: info - WASM_BINDGEN_TEST_BROWSER: "chrome --headless --enable-unsafe-webgpu" - run: cargo test-wasm --chrome + run: cargo test-wasm ${{ matrix.browser }} - uses: actions/upload-artifact@v4 if: always() with: From ad75e8cd18fc956912d9a9797021ee64acf8f70f Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 15:43:43 +1200 Subject: [PATCH 18/22] webdriver --- crates/renderling/webdriver.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 crates/renderling/webdriver.json diff --git a/crates/renderling/webdriver.json b/crates/renderling/webdriver.json new file mode 100644 index 00000000..27d04249 --- /dev/null +++ b/crates/renderling/webdriver.json @@ -0,0 +1,13 @@ +{ + "moz:firefoxOptions": { + "prefs": { + "dom.webgpu.enabled": true + }, + "args": [] + }, + "goog:chromeOptions": { + "args": [ + "--enable-unsafe-webgpu" + ] + } +} From f58ad4dbf227787232857baf94f66448b89bd85b Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 16:16:43 +1200 Subject: [PATCH 19/22] don't fail fast --- .github/workflows/push.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 0a8eb711..62c37a0a 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -52,6 +52,7 @@ jobs: renderling-build-shaders: needs: install-cargo-gpu strategy: + fail-fast: false matrix: # temporarily skip windows, revisit after a fix for this error is found: # https://github.com/rust-lang/cc-rs/issues/1331 From f70e85f7188fee557e618691ed159040025d3a2e Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 16:51:25 +1200 Subject: [PATCH 20/22] thrash --- .github/workflows/push.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 62c37a0a..63cadae1 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -138,10 +138,10 @@ jobs: # WASM tests renderling-wasm-test: - strategy: - matrix: - # empty string means ff, --chrome is chrome - browser: ["", "--chrome"] + # strategy: + # matrix: + # # empty string means ff, --chrome is chrome + # browser: ["", "--chrome"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -161,7 +161,7 @@ jobs: - name: Test WASM env: RUST_LOG: info - run: cargo test-wasm ${{ matrix.browser }} + run: cargo test-wasm --chrome #${{ matrix.browser }} - uses: actions/upload-artifact@v4 if: always() with: From ea853f5116329f51bb8ed1b598f93e19009c60f0 Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 17:19:50 +1200 Subject: [PATCH 21/22] loading-bytes: add window to web-sys features --- crates/loading-bytes/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/loading-bytes/Cargo.toml b/crates/loading-bytes/Cargo.toml index 05646d5e..6416d73f 100644 --- a/crates/loading-bytes/Cargo.toml +++ b/crates/loading-bytes/Cargo.toml @@ -20,4 +20,4 @@ serde_json.workspace = true snafu = {workspace = true} wasm-bindgen = {workspace = true} wasm-bindgen-futures = {workspace = true} -web-sys = { workspace = true, features = ["Request", "RequestInit", "Response"] } +web-sys = { workspace = true, features = ["Request", "RequestInit", "Response", "Window"] } From 5a5a541c335d0418d7124e3ad3e7d440285c49be Mon Sep 17 00:00:00 2001 From: Schell Carl Scivally Date: Mon, 1 Sep 2025 17:20:36 +1200 Subject: [PATCH 22/22] commenting out wasm tests on CI as no browser can run them --- .github/workflows/push.yaml | 62 ++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 63cadae1..94801214 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -136,34 +136,34 @@ jobs: name: test-output-${{ runner.os }} path: test_output - # WASM tests - renderling-wasm-test: - # strategy: - # matrix: - # # empty string means ff, --chrome is chrome - # browser: ["", "--chrome"] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: moonrepo/setup-rust@v1 - - uses: actions/cache@v4 - with: - path: ~/.cargo - key: ${{ runner.os }}-test-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- - - name: Install linux deps - if: runner.os == 'Linux' - run: | - sudo apt-get -y update - sudo apt-get -y install mesa-vulkan-drivers libvulkan1 vulkan-tools vulkan-validationlayers - - name: Install wasm-pack - run: cargo install --locked wasm-pack || true - - name: Test WASM - env: - RUST_LOG: info - run: cargo test-wasm --chrome #${{ matrix.browser }} - - uses: actions/upload-artifact@v4 - if: always() - with: - name: test-output-${{ runner.os }}-wasm - path: test_output + ## WASM tests, commented out until we can get a proper headless browser on CI + # renderling-wasm-test: + # # strategy: + # # matrix: + # # # empty string means ff, --chrome is chrome + # # browser: ["", "--chrome"] + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v2 + # - uses: moonrepo/setup-rust@v1 + # - uses: actions/cache@v4 + # with: + # path: ~/.cargo + # key: ${{ runner.os }}-test-cargo-${{ hashFiles('**/Cargo.lock') }} + # restore-keys: ${{ runner.os }}-cargo- + # - name: Install linux deps + # if: runner.os == 'Linux' + # run: | + # sudo apt-get -y update + # sudo apt-get -y install mesa-vulkan-drivers libvulkan1 vulkan-tools vulkan-validationlayers + # - name: Install wasm-pack + # run: cargo install --locked wasm-pack || true + # - name: Test WASM + # env: + # RUST_LOG: info + # run: cargo test-wasm --chrome #${{ matrix.browser }} + # - uses: actions/upload-artifact@v4 + # if: always() + # with: + # name: test-output-${{ runner.os }}-wasm + # path: test_output