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/.github/workflows/push.yaml b/.github/workflows/push.yaml index 553c217f..94801214 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: @@ -52,10 +52,11 @@ 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 - os: [ubuntu-latest, macos-latest] #, windows-latest] + os: [ubuntu-latest, ubuntu-24.04, macos-latest] #, windows-latest] runs-on: ${{ matrix.os }} defaults: run: @@ -134,3 +135,35 @@ jobs: with: name: test-output-${{ runner.os }} 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 diff --git a/Cargo.lock b/Cargo.lock index 3ca1ef03..7c6122ad 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" @@ -684,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" @@ -719,7 +813,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 +847,7 @@ checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" dependencies = [ "core-foundation 0.9.4", "core-graphics", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -912,6 +1006,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 +1077,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" @@ -1097,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", @@ -1109,6 +1223,7 @@ dependencies = [ "wasm-bindgen-test", "web-sys", "wgpu", + "winit", ] [[package]] @@ -1187,6 +1302,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 +1339,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 +1355,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 +1369,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 +1407,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 +1453,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 +1546,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 +1590,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 +1721,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 +1795,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 +1853,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 +1972,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,12 +2179,39 @@ 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", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "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", ] @@ -1895,6 +2375,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 +2394,8 @@ dependencies = [ "async-fs", "js-sys", "send_wrapper", + "serde", + "serde_json", "snafu 0.8.6", "wasm-bindgen", "wasm-bindgen-futures", @@ -2000,6 +2488,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 +2528,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 +2566,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 +2604,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 +2657,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 +2977,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 +2998,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 +3178,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 +3290,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" @@ -3131,6 +3737,7 @@ dependencies = [ "async-channel 1.9.0", "bytemuck", "cfg_aliases", + "console_log 1.0.0", "craballoc", "crabslab", "crunch", @@ -3165,9 +3772,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 +3792,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 +3922,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 +3977,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 +4010,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 +4077,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 +4096,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 +4248,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 +4296,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 +4332,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 +4360,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 +4420,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 +4527,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 +4635,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 +4723,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 +4750,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 +4774,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 +4820,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 +4860,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 +4979,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 +5394,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 +5790,13 @@ dependencies = [ "winapi", ] +[[package]] +name = "wire-types" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -4767,6 +5806,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 +5885,18 @@ checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" name = "xtask" version = "0.1.0" dependencies = [ + "axum", "clap 4.5.40", "env_logger", + "futures-util", + "image 0.25.6", + "img-diff", "log", + "new_mime_guess", "renderling_build", + "reqwest", + "tokio", + "wire-types", ] [[package]] @@ -4863,6 +5916,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 +5960,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..65e707e9 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,9 +18,11 @@ 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"] } +console_log = "1.0.0" craballoc = { version = "0.2.3" } crabslab = { version = "0.6.5", default-features = false } plotters = "0.3.7" @@ -27,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" @@ -35,9 +39,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 +53,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..30ba1cbf 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,33 @@ 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()); + frame.present(); + } +} + #[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 +55,71 @@ 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(); + + 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..e3cd62c8 100644 --- a/crates/img-diff/src/lib.rs +++ b/crates/img-diff/src/lib.rs @@ -4,8 +4,10 @@ use image::{DynamicImage, Luma, Rgb, Rgb32FImage, Rgba32FImage}; use snafu::prelude::*; 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( @@ -138,7 +151,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(); @@ -146,10 +159,11 @@ 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, - Err(e) => panic!("Asserting {filename} failed: {e}"), + Err(e) => return Err(format!("Asserting {filename} failed: {e}")), }; if let Some(DiffResults { num_pixels: diffs, @@ -168,10 +182,10 @@ 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)); + 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"); @@ -192,28 +206,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..6416d73f 100644 --- a/crates/loading-bytes/Cargo.toml +++ b/crates/loading-bytes/Cargo.toml @@ -15,7 +15,9 @@ 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} -web-sys = { workspace = true, features = ["Request", "RequestInit", "Response"] } +web-sys = { workspace = true, features = ["Request", "RequestInit", "Response", "Window"] } diff --git a/crates/loading-bytes/src/lib.rs b/crates/loading-bytes/src/lib.rs index fdaef2eb..0d3d6bd2 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,200 @@ 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), + } + .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 buffer = wasm_bindgen_futures::JsFuture::from(array_promise) - .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() + })?; + + 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) +} + +// 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), - })?; - 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-build/src/lib.rs b/crates/renderling-build/src/lib.rs index dd548123..7ad7a0de 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 AsRef, destination: impl AsRef, pub renderling_crate: std::path::PathBuf, pub shader_dir: std::path::PathBuf, pub shader_manifest: std::path::PathBuf, @@ -145,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/Cargo.toml b/crates/renderling/Cargo.toml index c2fc3152..40e726e3 100644 --- a/crates/renderling/Cargo.toml +++ b/crates/renderling/Cargo.toml @@ -58,7 +58,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} @@ -75,22 +74,37 @@ 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 +console_log.workspace = true 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" } naga.workspace = true ttf-parser = "0.20.0" +wasm-bindgen-test.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"] } [target.'cfg(target_os = "macos")'.dev-dependencies] metal.workspace = true + +[dev-dependencies.web-sys] +workspace = true +features = [ + "Navigator", + "Window" +] diff --git a/crates/renderling/shaders/manifest.json b/crates/renderling/shaders/manifest.json index ca27d96e..b38af795 100644 --- a/crates/renderling/shaders/manifest.json +++ b/crates/renderling/shaders/manifest.json @@ -178,5 +178,30 @@ "source_path": "shaders/tonemapping-tonemapping_vertex.spv", "entry_point": "tonemapping::tonemapping_vertex", "wgsl_entry_point": "tonemappingtonemapping_vertex" + }, + { + "source_path": "shaders/tutorial-implicit_isosceles_vertex.spv", + "entry_point": "tutorial::implicit_isosceles_vertex", + "wgsl_entry_point": "tutorialimplicit_isosceles_vertex" + }, + { + "source_path": "shaders/tutorial-passthru_fragment.spv", + "entry_point": "tutorial::passthru_fragment", + "wgsl_entry_point": "tutorialpassthru_fragment" + }, + { + "source_path": "shaders/tutorial-slabbed_renderlet.spv", + "entry_point": "tutorial::slabbed_renderlet", + "wgsl_entry_point": "tutorialslabbed_renderlet" + }, + { + "source_path": "shaders/tutorial-slabbed_vertices.spv", + "entry_point": "tutorial::slabbed_vertices", + "wgsl_entry_point": "tutorialslabbed_vertices" + }, + { + "source_path": "shaders/tutorial-slabbed_vertices_no_instance.spv", + "entry_point": "tutorial::slabbed_vertices_no_instance", + "wgsl_entry_point": "tutorialslabbed_vertices_no_instance" } ] \ No newline at end of file diff --git a/crates/renderling/shaders/tutorial-implicit_isosceles_vertex.spv b/crates/renderling/shaders/tutorial-implicit_isosceles_vertex.spv new file mode 100644 index 00000000..45b7d00f Binary files /dev/null and b/crates/renderling/shaders/tutorial-implicit_isosceles_vertex.spv differ diff --git a/crates/renderling/shaders/tutorial-passthru_fragment.spv b/crates/renderling/shaders/tutorial-passthru_fragment.spv new file mode 100644 index 00000000..1169e630 Binary files /dev/null and b/crates/renderling/shaders/tutorial-passthru_fragment.spv differ diff --git a/crates/renderling/shaders/tutorial-slabbed_renderlet.spv b/crates/renderling/shaders/tutorial-slabbed_renderlet.spv new file mode 100644 index 00000000..f54c68e3 Binary files /dev/null and b/crates/renderling/shaders/tutorial-slabbed_renderlet.spv differ diff --git a/crates/renderling/shaders/tutorial-slabbed_vertices.spv b/crates/renderling/shaders/tutorial-slabbed_vertices.spv new file mode 100644 index 00000000..da5e78a9 Binary files /dev/null and b/crates/renderling/shaders/tutorial-slabbed_vertices.spv differ diff --git a/crates/renderling/shaders/tutorial-slabbed_vertices_no_instance.spv b/crates/renderling/shaders/tutorial-slabbed_vertices_no_instance.spv new file mode 100644 index 00000000..1e6d7ea4 Binary files /dev/null and b/crates/renderling/shaders/tutorial-slabbed_vertices_no_instance.spv differ diff --git a/crates/renderling/src/atlas/cpu.rs b/crates/renderling/src/atlas/cpu.rs index b0a92e36..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, @@ -436,16 +438,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 +940,7 @@ mod test { material::Materials, pbr::Material, stage::Vertex, + test::BlockOnFuture, transform::Transform, Context, }; @@ -947,8 +952,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 +1001,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 +1014,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 +1091,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 +1104,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 +1184,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 +1208,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 +1220,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/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); } } diff --git a/crates/renderling/src/bvol.rs b/crates/renderling/src/bvol.rs index a63236b9..e76345fc 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 { @@ -587,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::*; @@ -641,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) @@ -709,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); } @@ -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..2b4adfa2 100644 --- a/crates/renderling/src/context.rs +++ b/crates/renderling/src/context.rs @@ -12,9 +12,10 @@ use snafu::prelude::*; use crate::{ stage::Stage, texture::{BufferDimensions, CopiedTextureBuffer, Texture, TextureError}, + ui::Ui, }; -enum RenderTargetInner { +pub(crate) enum RenderTargetInner { Surface { surface: wgpu::Surface<'static>, surface_config: wgpu::SurfaceConfiguration, @@ -30,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 { @@ -87,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 { @@ -101,159 +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)?; - 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:#?}"); - } - 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::trace!( - "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 }, @@ -374,14 +232,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) } @@ -395,11 +253,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. /// @@ -410,10 +268,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. @@ -435,7 +293,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. @@ -443,7 +301,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, @@ -503,79 +361,58 @@ 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)) } - pub async fn try_from_raw_window( + pub async fn try_new_with_surface( width: u32, height: u32, 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)) } #[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 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 { @@ -754,4 +591,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/cubemap/cpu.rs b/crates/renderling/src/cubemap/cpu.rs index 09734d32..663314aa 100644 --- a/crates/renderling/src/cubemap/cpu.rs +++ b/crates/renderling/src/cubemap/cpu.rs @@ -272,6 +272,8 @@ mod test { use crate::{ math::{UNIT_INDICES, UNIT_POINTS}, stage::Vertex, + test::BlockOnFuture, + texture::CopiedTextureBuffer, }; use super::*; @@ -280,7 +282,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 +308,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 +489,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( @@ -511,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, @@ -522,6 +525,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 edb86349..5accb2bd 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() ); @@ -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/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/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 0f50d760..0c7a9b91 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(); //! ``` //! @@ -160,9 +160,11 @@ pub mod debug; pub mod draw; pub mod geometry; pub mod ibl; +#[cfg(cpu)] +pub mod internal; pub mod light; #[cfg(cpu)] -mod linkage; +pub mod linkage; pub mod material; pub mod math; pub mod pbr; @@ -175,6 +177,7 @@ pub mod texture; pub mod tonemapping; pub mod transform; pub mod tuple; +pub mod tutorial; #[cfg(feature = "ui")] pub mod ui; @@ -222,7 +225,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"); @@ -237,6 +240,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 +358,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 +368,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 +376,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 +395,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 +409,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 +425,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 +444,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 +490,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 +509,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 +536,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 +551,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 +578,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 +589,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 +599,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 +607,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 +626,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 +640,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 +694,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 +721,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 +731,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 +748,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 +804,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 +813,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 +873,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 +921,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 +997,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 +1014,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 +1035,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 +1046,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 +1055,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 +1078,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.rs b/crates/renderling/src/light.rs index b87b1158..694902e2 100644 --- a/crates/renderling/src/light.rs +++ b/crates/renderling/src/light.rs @@ -16,7 +16,7 @@ use crate::{ cubemap::{CubemapDescriptor, CubemapFaceDirection}, geometry::GeometryDescriptor, math::{Fetch, IsSampler, IsVector, Sample2dArray}, - stage::Renderlet, + stage::{Renderlet, VertexInfo}, transform::Transform, }; @@ -135,8 +135,12 @@ pub fn shadow_mapping_vertex( return; } - let (_vertex, _transform, _model_matrix, world_pos) = - renderlet.get_vertex_info(vertex_index, geometry_slab); + let VertexInfo { + world_pos, + vertex: _vertex, + transform: _transform, + model_matrix: _model_matrix, + } = renderlet.get_vertex_info(vertex_index, geometry_slab); let lighting_desc = light_slab.read_unchecked(Id::::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/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/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/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..26d6d038 100644 --- a/crates/renderling/src/pbr.rs +++ b/crates/renderling/src/pbr.rs @@ -717,9 +717,10 @@ mod test { use crate::{ atlas::AtlasImage, camera::Camera, - math::{Vec3, Vec4}, 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/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/skybox/cpu.rs b/crates/renderling/src/skybox/cpu.rs index 0281556e..f3d929e6 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, texture::CopiedTextureBuffer, 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); @@ -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, @@ -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()) @@ -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, @@ -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.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/stage/cpu.rs b/crates/renderling/src/stage/cpu.rs index 1b70cb9a..a0eadfa9 100644 --- a/crates/renderling/src/stage/cpu.rs +++ b/crates/renderling/src/stage/cpu.rs @@ -2,6 +2,7 @@ //! //! The `Stage` object contains a slab buffer and a render pipeline. //! It is used to stage [`Renderlet`]s for rendering. +use core::ops::Deref; use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; use craballoc::prelude::*; use crabslab::Id; @@ -768,6 +769,10 @@ impl Stage { &self.runtime().queue } + pub fn hdr_texture(&self) -> impl Deref + '_ { + self.hdr_texture.read().unwrap() + } + pub fn builder(&self) -> RenderletBuilder<'_, ()> { RenderletBuilder::new(self) } @@ -1430,6 +1435,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()) @@ -1699,12 +1709,13 @@ impl NestedTransform { mod test { use craballoc::runtime::CpuRuntime; use crabslab::{Array, Id, Slab}; - use glam::{Mat4, Vec2, Vec3}; + use glam::{Mat4, Vec2, Vec3, Vec4}; use crate::{ camera::Camera, geometry::{Geometry, GeometryDescriptor}, stage::{cpu::SlabAllocator, NestedTransform, Renderlet, Vertex}, + test::BlockOnFuture, transform::Transform, Context, }; @@ -1779,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]) @@ -1803,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, @@ -1819,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, @@ -1833,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()); @@ -1846,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(); @@ -1858,4 +1869,136 @@ mod test { let pbr_desc = slab.read_unchecked(Id::::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/stage/gltf_support.rs b/crates/renderling/src/stage/gltf_support.rs index 94083efc..c7a16f0a 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 { @@ -1192,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] @@ -1217,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); @@ -1233,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) @@ -1258,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); } @@ -1267,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) @@ -1309,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)) @@ -1335,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); } @@ -1343,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) @@ -1355,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) @@ -1389,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, @@ -1445,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 0ac9def6..72958a41 100644 --- a/crates/renderling/src/stage/gltf_support/anime.rs +++ b/crates/renderling/src/stage/gltf_support/anime.rs @@ -771,11 +771,12 @@ impl Animator { #[cfg(test)] mod test { - use crate::{camera::Camera, math::Vec3, 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) @@ -797,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(); @@ -806,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..dbc3e655 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; @@ -39,8 +39,11 @@ 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 }, #[snafu(display("Driver poll error: {source}"))] Poll { source: wgpu::PollError }, @@ -48,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), @@ -58,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())); @@ -595,7 +607,7 @@ impl Texture { channels: usize, subpixel_bytes: usize, ) -> CopiedTextureBuffer { - Self::read_from( + CopiedTextureBuffer::read_from( runtime, texture, width, @@ -607,68 +619,7 @@ 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 fn read_hdr_image( + pub async fn read_hdr_image( &self, runtime: impl AsRef, ) -> Result { @@ -684,7 +635,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 +713,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 +736,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, @@ -827,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 { @@ -844,18 +796,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 +832,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 +878,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 +940,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 +949,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,13 +961,16 @@ 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), - 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) @@ -992,7 +982,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 +990,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 +1026,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 +1055,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)" @@ -1074,17 +1076,96 @@ 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::Context; + use crate::{test::BlockOnFuture, texture::CopiedTextureBuffer, 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(); @@ -1103,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, @@ -1119,7 +1200,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/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/src/tutorial.rs b/crates/renderling/src/tutorial.rs new file mode 100644 index 00000000..c8707b58 --- /dev/null +++ b/crates/renderling/src/tutorial.rs @@ -0,0 +1,114 @@ +//! Shaders used in the intro tutorial and in WASM tests. + +use crabslab::{Array, Id, Slab, SlabItem}; +use glam::{Vec3, 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)] array_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 array = slab.read(array_id); + let vertex_id = array.at(vertex_index as usize); + let (position, color) = slab.read(vertex_id); + *clip_pos = position.extend(1.0); + *out_color = 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/src/ui.rs b/crates/renderling/src/ui.rs index 3b37eb21..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 @@ -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..1d610b05 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,15 @@ impl UiPathBuilder { #[cfg(test)] mod test { use crate::{ - math::{hex_to_vec4, Vec2}, + math::hex_to_vec4, + test::BlockOnFuture, ui::{ test::{cute_beach_palette, Colors}, Ui, }, Context, }; + use glam::Vec2; use super::*; @@ -555,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() @@ -569,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); } @@ -581,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, @@ -595,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() @@ -671,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); @@ -705,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 56f3b070..fa2325cd 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}, }; @@ -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/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..838f7e93 --- /dev/null +++ b/crates/renderling/tests/wasm.rs @@ -0,0 +1,1021 @@ +//! WASM tests. +#![allow(dead_code)] + +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::prelude::*; +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 _ = 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(), + 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) + .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) +} + +fn image_to_wire(seen: impl Into) -> wire_types::Image { + 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"), + }; + 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}"), + &data, + ) + .await + .unwrap(); + + if let Err(Error { description }) = result { + panic!("{description}"); + } +} + +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; +} + +#[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; +} + +/// 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_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; +} + +/// Test rendering a triangle using no mesh geometry. +#[wasm_bindgen_test] +async fn implicit_isosceles_triangle() { + let ctx = Context::headless(100, 100).await; + let runtime = ctx.as_ref(); + + 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: vmodule, + entry_point: Some(ventry_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: fmodule, + entry_point: Some(fentry_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, + }) + } + // 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); + create_pipeline( + runtime, + &vertex.module, + vertex.entry_point, + &fragment.module, + fragment.entry_point, + ) + }; + + 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 _ = 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) + .await + .unwrap(); + let runtime = WgpuRuntime { + device: device.into(), + queue: queue.into(), + }; + + // 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: Some(&pipeline_layout), + 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, + }) + }; + + let slab_buffer: SlabBuffer = 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 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 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; +} + +#[wasm_bindgen_test] +async fn slabbed_isosceles_triangle() { + let ctx = Context::headless(100, 100).await; + 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 = 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(), + }], + }); + + 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() + .await + .expect_throw("could not read frame"); + assert_img_eq("tutorial/slabbed_isosceles_triangle.png", img).await; +} + +// #[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(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] +// async fn can_save_wrong_diffs() { +// let img = load_test_img("jolt.png").await; +// assert_img_eq("cmy_triangle/hdr.png", img).await; +// } + +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; +} 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" + ] + } +} 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..7e53c2cb --- /dev/null +++ b/crates/wire-types/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "wire-types" +version = "0.1.0" +edition = "2021" + +[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..f8ff03d5 --- /dev/null +++ b/crates/wire-types/src/lib.rs @@ -0,0 +1,26 @@ +/// 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, +} + +impl From for Error { + fn from(description: String) -> Self { + Error { description } + } +} diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml index 45ea47ea..9088d57c 100644 --- a/crates/xtask/Cargo.toml +++ b/crates/xtask/Cargo.toml @@ -4,7 +4,15 @@ version = "0.1.0" edition = "2021" [dependencies] +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 +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..514c82bd 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,17 @@ enum Command { #[clap(long)] from_cargo: bool, }, + /// Run the WASM test server + WasmServer, + /// Compile for WASM and run headless browser tests + TestWasm { + /// Cargo args. + #[clap(last = true)] + args: Vec, + /// Set to use chrome, otherwise firefox will be used. + #[clap(long)] + chrome: bool, + }, } #[derive(Parser)] @@ -29,9 +42,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 +75,29 @@ fn main() { let paths = renderling_build::RenderlingPaths::new().unwrap(); paths.generate_linkage(from_cargo, wgsl, only_fn_with_name); } + 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", + if chrome { "--chrome" } else { "--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::WasmServer => { + server::serve().await; + } } } diff --git a/crates/xtask/src/server.rs b/crates/xtask/src/server.rs new file mode 100644 index 00000000..1dfd8f61 --- /dev/null +++ b/crates/xtask/src/server.rs @@ -0,0 +1,197 @@ +//! 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 img_diff::DiffCfg; +use tokio::io::AsyncWriteExt; +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("/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 + .unwrap(); + 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"); + 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) + } +} + +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) + } + 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 } + }) +} + +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 } + }) +} + +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 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 artifact_inner(filename: impl AsRef, body: Body) -> Result<(), Error> { + 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()))?; + 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(Json(result).into_response().into_body()) + .unwrap() +} diff --git a/test_img/clear.png b/test_img/clear.png new file mode 100644 index 00000000..17dadd45 Binary files /dev/null and b/test_img/clear.png differ