diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..539e381 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,76 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ${{ matrix.os }} + env: + RUSTFLAGS: ${{ matrix.RUSTFLAGS }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-latest + CHECK_COMMAND: cargo check --all-targets --verbose + TEST_COMMAND: cargo test --all-targets --no-run --verbose + - os: macos-latest + CHECK_COMMAND: cargo check --all-targets --verbose + TEST_COMMAND: cargo test --all-targets --no-run --verbose + - os: windows-latest + CHECK_COMMAND: rustup default stable-msvc && cargo check --all-targets --verbose + TEST_COMMAND: rustup default stable-msvc && cargo test --all-targets --no-run --verbose + steps: + - name: Checkout sources + uses: actions/checkout@v2 + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - name: cargo check + run: ${{ matrix.CHECK_COMMAND }} + - name: cargo test --no-run + run: ${{ matrix.TEST_COMMAND }} + + lint: + name: lint + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings + + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install latest nightly + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + continue-on-error: true + - name: cargo doc + run: cargo --version; cargo doc --lib --no-deps diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67d6deb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +*png +*cap \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..eee9325 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1206 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayvec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" + +[[package]] +name = "ash" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c69a8137596e84c22d57f3da1b5de1d4230b1742a710091c85f4d7ce50f00f38" +dependencies = [ + "libloading", +] + +[[package]] +name = "atom" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c86699c3f02778ec07158376991c8f783dd1f2f95c579ffaf0738dc984b2fe2" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "bytemuck" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41aa2ec95ca3b5c54cf73c91acf06d24f4495d5f1b1c12506ae3483d646177ac" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "cc" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cloudabi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" +dependencies = [ + "bitflags", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +dependencies = [ + "bitflags", + "block", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "copyless" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" + +[[package]] +name = "core-foundation" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5ed8e7e76c45974e15e41bfa8d5b0483cd90191639e01d8f5f1e606299d3fb" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a21fa21941700a3cd8fcb4091f361a6a712fac632f85d9f487cc892045d55c6" + +[[package]] +name = "core-graphics-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e92f5d519093a4178296707dbaa3880eae85a5ef5386675f361a1cf25376e93c" +dependencies = [ + "bitflags", + "core-foundation", + "foreign-types", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "d3d12" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a60cceb22c7c53035f8980524fdc7f17cf49681a3c154e6757d30afbec6ec4" +dependencies = [ + "bitflags", + "libloading", + "winapi", +] + +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "float-cmp" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "futures" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" + +[[package]] +name = "futures-executor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" + +[[package]] +name = "futures-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" + +[[package]] +name = "futures-task" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gfx-auxil" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec012a32036c6439180b688b15a24dc8a3fbdb3b1cd02eb55266201db4c1b0f" +dependencies = [ + "fxhash", + "gfx-hal", + "spirv_cross", +] + +[[package]] +name = "gfx-backend-dx11" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c7fd31dcf0f8496fc94fe44b285a0bfeeb33b5a8ec0f59ea5f8fa64428071b4" +dependencies = [ + "bitflags", + "gfx-auxil", + "gfx-hal", + "libloading", + "log", + "parking_lot", + "range-alloc", + "raw-window-handle", + "smallvec", + "spirv_cross", + "winapi", + "wio", +] + +[[package]] +name = "gfx-backend-dx12" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfc194d9a1540073f181bae94087ffc9d84a5586b71962cd1b46b86e5a6d697" +dependencies = [ + "bitflags", + "d3d12", + "gfx-auxil", + "gfx-hal", + "log", + "range-alloc", + "raw-window-handle", + "smallvec", + "spirv_cross", + "winapi", +] + +[[package]] +name = "gfx-backend-empty" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2085227c12b78f6657a900c829f2d0deb46a9be3eaf86844fde263cdc218f77c" +dependencies = [ + "gfx-hal", + "log", + "raw-window-handle", +] + +[[package]] +name = "gfx-backend-metal" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b2b1e2510c8a283beac1e680cd152edc05d138c00dfabc0e3f636e143ffd66" +dependencies = [ + "arrayvec", + "bitflags", + "block", + "cocoa-foundation", + "copyless", + "foreign-types", + "gfx-auxil", + "gfx-hal", + "lazy_static", + "log", + "metal", + "objc", + "parking_lot", + "range-alloc", + "raw-window-handle", + "smallvec", + "spirv_cross", + "storage-map", +] + +[[package]] +name = "gfx-backend-vulkan" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84bda4200a82e1912d575801e2bb76ae19c6256359afbc0adfbbaec02fcadc6" +dependencies = [ + "arrayvec", + "ash", + "byteorder", + "core-graphics-types", + "gfx-hal", + "inplace_it", + "lazy_static", + "log", + "objc", + "raw-window-handle", + "smallvec", + "winapi", + "x11", +] + +[[package]] +name = "gfx-descriptor" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8c7afcd000f279d541a490e27117e61037537279b9342279abf4938fe60c6b" +dependencies = [ + "arrayvec", + "fxhash", + "gfx-hal", + "log", +] + +[[package]] +name = "gfx-hal" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d0754f5b7a43915fd7466883b2d1bb0800d7cc4609178d0b27bf143b9e5123" +dependencies = [ + "bitflags", + "raw-window-handle", +] + +[[package]] +name = "gfx-memory" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe8d8855df07f438eb8a765e90356d5b821d644ea3b59b870091450b89576a9f" +dependencies = [ + "fxhash", + "gfx-hal", + "hibitset", + "log", + "slab", +] + +[[package]] +name = "hermit-abi" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" +dependencies = [ + "libc", +] + +[[package]] +name = "hibitset" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93a1bb8316a44459a7d14253c4d28dd7395cbd23cc04a68c46e851b8e46d64b1" +dependencies = [ + "atom", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "image" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974e194911d1f7efe3cd8a8f9db3b767e43536327e899e8bc9a12ef5711b74d2" +dependencies = [ + "bytemuck", + "byteorder", + "num-iter", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "inplace_it" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd01a2a73f2f399df96b22dc88ea687ef4d76226284e7531ae3c7ee1dc5cb534" + +[[package]] +name = "instant" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b141fdc7836c525d4d594027d318c84161ca17aaf8113ab1f81ab93ae897485" + +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" + +[[package]] +name = "libloading" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2443d8f0478b16759158b2f66d525991a05491138bc05814ef52a250148ef4f9" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "lock_api" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "metal" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4e8a431536529327e28c9ba6992f2cb0c15d4222f0602a16e6d7695ff3bccf" +dependencies = [ + "bitflags", + "block", + "cocoa-foundation", + "foreign-types", + "log", + "objc", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "naga" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0873deb76cf44b7454fba7b2ba6a89d3de70c08aceffd2c489379b3d9d08e661" +dependencies = [ + "bitflags", + "fxhash", + "log", + "num-traits", + "spirv_headers", + "thiserror", +] + +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e6b7c748f995c4c29c5f5ae0248536e04a5739927c74ec0fa564805094b9f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b4d7360f362cfb50dde8143501e6940b22f644be75a4cc90b2d81968908138" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" + +[[package]] +name = "parking_lot" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +dependencies = [ + "cfg-if", + "cloudabi", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "pin-project" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" + +[[package]] +name = "png" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfe7f9f1c730833200b134370e1d5098964231af8450bce9b78ee3ab5278b970" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" + +[[package]] +name = "proc-macro-nested" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" + +[[package]] +name = "proc-macro2" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e28516df94f3dd551a587da5357459d9b36d945a7c37c3557928c1c2ff2a2c" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "range-alloc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a871f1e45a3a3f0c73fb60343c811238bb5143a81642e27c2ac7aac27ff01a63" + +[[package]] +name = "raw-window-handle" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a441a7a6c80ad6473bd4b74ec1c9a4c951794285bf941c2126f607c72e48211" +dependencies = [ + "libc", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "regex" +version = "1.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" + +[[package]] +name = "renderdoc" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f084dd613b0ac0d206540f11f32e9627322479005733b17cc0c346afc2ec6e1a" +dependencies = [ + "bitflags", + "float-cmp", + "libloading", + "once_cell", + "renderdoc-sys", + "winapi", + "wio", +] + +[[package]] +name = "renderdoc-sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" + +[[package]] +name = "spirv_cross" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b631bd956108f3e34a4fb7e39621711ac15ce022bc567da2d953c6df13f00e00" +dependencies = [ + "cc", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "spirv_headers" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f5b132530b1ac069df335577e3581765995cba5a13995cdbbdbc8fb057c532c" +dependencies = [ + "bitflags", + "num-traits", +] + +[[package]] +name = "storage-map" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418bb14643aa55a7841d5303f72cf512cfb323b8cc221d51580500a1ca75206c" +dependencies = [ + "lock_api", +] + +[[package]] +name = "syn" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6690e3e9f692504b941dc6c3b188fd28df054f7fb8469ab40680df52fdcc842b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" +dependencies = [ + "cfg-if", + "log", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcf46c1f1f06aeea2d6b81f3c863d0930a596c86ad1920d4e5bad6dd1d7119a" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "typed-arena" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "wasm-bindgen" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7866cab0aa01de1edf8b5d7936938a7e397ee50ce24119aef3e1eaa3b6171da" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" + +[[package]] +name = "web-sys" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wgpu" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549160f188eef412ac978499ddf0ceadad4c9159bb1160f9e6b9d4cc8ee977dc" +dependencies = [ + "arrayvec", + "futures", + "gfx-backend-vulkan", + "js-sys", + "objc", + "parking_lot", + "raw-window-handle", + "smallvec", + "tracing", + "typed-arena", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317d19c2876fc26d5bc15fe986a0d2d28958337e639323fcaded23a7cf8865b9" +dependencies = [ + "arrayvec", + "bitflags", + "copyless", + "fxhash", + "gfx-backend-dx11", + "gfx-backend-dx12", + "gfx-backend-empty", + "gfx-backend-metal", + "gfx-backend-vulkan", + "gfx-descriptor", + "gfx-hal", + "gfx-memory", + "naga", + "parking_lot", + "raw-window-handle", + "smallvec", + "thiserror", + "tracing", + "wgpu-types", +] + +[[package]] +name = "wgpu-mipmap" +version = "0.1.0" +dependencies = [ + "bytemuck", + "env_logger", + "futures", + "image", + "log", + "renderdoc", + "thiserror", + "tracing", + "wgpu", +] + +[[package]] +name = "wgpu-types" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e3529528e608b54838ee618c3923b0f46e6db0334cfc6c42a16cf4ceb3bdb57" +dependencies = [ + "bitflags", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "x11" +version = "2.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ecd092546cb16f25783a5451538e73afc8d32e242648d54f4ae5459ba1e773" +dependencies = [ + "libc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..eaf73e2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "wgpu-mipmap" +version = "0.1.0" +authors = ["Justin Shrake "] +edition = "2018" + +[features] +debug = ["renderdoc"] + +[dependencies] +log = "0.4" +# renderdoc is only used in the examples, but +# cargo does not support optional dev dependencies +renderdoc = {version = "0.9.1", optional=true} +thiserror = "1.0" +wgpu = "0.6" + +[dev-dependencies] +bytemuck = "1.4.1" +env_logger = "0.7.1" +futures = "0.3" +image = {version = "0.23", default-features = false, features = ["png"]} +tracing = { version = "0.1", features = ["log"] } + +# TODO: Compiling for target=wasm32-unknown-unknown only works +# against the HEAD of the master branch of wgpu-rs. Users +# who wish to compile for this target will need to manually +# fiddle with this for now. +[patch.crates-io] +#wgpu = {git = "https://github.com/gfx-rs/wgpu-rs", branch = "master"} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ed98d18 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2020 Justin Shrake + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..da7d50f --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: test +test: + cargo test + +.PHONY: check +check: + cargo check + +.PHONY: lint +lint: + cargo clippy -- -D warnings + +.PHONY: doc +doc: + cargo doc --open + +.PHONY: cat +cat: + cargo run --example cat + +.PHONY: checkerboard +checkerboard: + cargo run --example checkerboard + +.PHONY: build-shaders +build-shaders: + ./src/backends/shaders/compile.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..31fd658 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# wgpu-mipmap + +![ci](https://github.com/jshrake/wgpu-mipmap/workflows/ci/badge.svg) + +Generate mipmaps for [wgpu](https://github.com/gfx-rs/wgpu-rs) textures. + +## Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +wgpu-mipmap = "0.1" +``` + +Example usage: + +```rust +use wgpu_mipmap::*; +fn example(device: &wgpu::Device, queue: &wgpu::Queue) -> Result<(), Error> { + // create a recommended generator + let generator = RecommendedMipmapGenerator::new(&device); + // create and upload data to a texture + let texture_descriptor = wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width: 512, + height: 512, + depth: 1, + }, + mip_level_count: 10, // 1 + log2(512) + sample_count: 1, + format: wgpu::TextureFormat::Rgba8Unorm, + dimension: wgpu::TextureDimension::D2, + usage: wgpu::TextureUsage::STORAGE, + label: None, + }; + let texture = device.create_texture(&texture_descriptor); + // upload_data_to_texture(&texture); + // create an encoder and generate mipmaps for the texture + let mut encoder = device.create_command_encoder(&Default::default()); + generator.generate(&device, &mut encoder, &texture, &texture_descriptor)?; + queue.submit(std::iter::once(encoder.finish())); + Ok(()) +} +``` + +## Features + +wgpu-mipmap is in the early stages of development and can only generate mipmaps for +2D textures with floating-point formats. The library implements several backends +in order to support various texture usage patterns: + +- `ComputeMipmapGenerator`: For power of two textures with with usage + `TextureUsage::STORAGE`. Uses a compute pipeline to generate mipmaps. +- `RenderMipmapGenerator`: For textures with usage + `TextureUsage::OUTPUT_ATTACHMENT`. Uses a render pipeline to generate mipmaps. +- `CopyMipmapGenerator`: For textures with usage `TextureUsage::SAMPLED`. + Allocates a new texture, uses a render pipeline to generate mipmaps in the new + texture, then copies the result back to the original texture. +- `RecommendedMipmapGenerator`: Uses one of the above implementations depending + on texture usage (prefers the compute backend, followed by the render backend, + and finally the copy backend). + +## Development + +### Run the examples + +The examples test various use cases and generate images for manual inspection and comparsion. + +```console +$ cargo run --example cat +$ cargo run --example checkerboard +``` + +### Run the tests + +```console +$ cargo test +``` + +### How to compile the shaders + +```console +$ make build-shaders +``` + +See [src/shaders/README.md](src/shaders/README.md) for dependencies and more information. + +## Benchmarks + +TODO + +## Resources + +- https://github.com/gpuweb/gpuweb/issues/386 +- https://github.com/gpuweb/gpuweb/issues/513 +- https://github.com/gfx-rs/wgpu-rs/blob/master/examples/mipmap/main.rs +- https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkCmdBlitImage.html#_description diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..da45f5b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,19 @@ +# examples + +The examples write out the generated mipmaps as pngs to the root directory for visual inspection. + +## cat + +Generates mipmaps for an sRGB png [cat.png](cat.png) ([https://commons.wikimedia.org/wiki/File:Avatar_cat.png](https://commons.wikimedia.org/wiki/File:Avatar_cat.png)) using the various backends. + +```console +$ cargo run --example cat +``` + +## checkerboard + +Generates mipmaps for both a linear and srgb checkerboard texture with the various backends. + +```console +$ cargo run --example checkerboard +``` diff --git a/examples/cat.png b/examples/cat.png new file mode 100644 index 0000000..5991f4f Binary files /dev/null and b/examples/cat.png differ diff --git a/examples/cat.rs b/examples/cat.rs new file mode 100644 index 0000000..ee41286 --- /dev/null +++ b/examples/cat.rs @@ -0,0 +1,105 @@ +#[cfg(feature = "debug")] +use renderdoc::{RenderDoc, V110}; +use wgpu_mipmap::RecommendedMipmapGenerator; + +fn main() { + env_logger::init(); + + #[cfg(feature = "debug")] + let mut rd: RenderDoc = RenderDoc::new().expect("Unable to connect"); + #[cfg(feature = "debug")] + rd.start_frame_capture(std::ptr::null(), std::ptr::null()); + + let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY); + futures::executor::block_on((|| { + async { + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: None, + }) + .await + .expect("Failed to find an appropiate adapter"); + // Create the logical device and command queue + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + features: wgpu::Features::empty(), + limits: wgpu::Limits::default(), + shader_validation: true, + }, + None, + ) + .await + .expect("Failed to create device"); + // Generate texture data on the CPU + let cat_png_bytes = include_bytes!("cat.png"); + let cat_image = image::load_from_memory(cat_png_bytes).expect("this should work"); + let cat_image = cat_image.into_rgba(); + let width = cat_image.width(); + let height = cat_image.height(); + let mip_level_count = 1 + (width.max(height) as f64).log2().floor() as u32; + // Create a texture + let format = wgpu::TextureFormat::Rgba8UnormSrgb; + let texture_extent = wgpu::Extent3d { + width, + height, + depth: 1, + }; + let supported_usage: std::collections::HashMap<_, _> = vec![ + ( + "compute", + wgpu_mipmap::ComputeMipmapGenerator::required_usage(), + ), + ( + "render", + wgpu_mipmap::RenderMipmapGenerator::required_usage(), + ), + ("copy", wgpu_mipmap::CopyMipmapGenerator::required_usage()), + ] + .into_iter() + .collect(); + let generator = RecommendedMipmapGenerator::new(&device); + for (usage_str, usage) in &supported_usage { + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: *usage | wgpu::TextureUsage::COPY_DST | wgpu::TextureUsage::COPY_SRC, + label: None, + }; + + let mipmap_buffers = wgpu_mipmap::util::generate_and_copy_to_cpu( + &device, + &queue, + &generator, + &cat_image.to_vec(), + &texture_descriptor, + ) + .await + .expect("shouldn't fail"); + + let has_file_system_available = cfg!(not(target_arch = "wasm32")); + if !has_file_system_available { + return; + } + + // Write the different mip levels as files + for (i, mip) in mipmap_buffers.iter().enumerate() { + image::save_buffer( + format!("cat-{}-{}.png", usage_str, i), + &mip.buffer, + mip.dimensions.width as u32, + mip.dimensions.height as u32, + image::ColorType::Rgba8, + ) + .unwrap(); + } + } + } + })()); + #[cfg(feature = "debug")] + rd.end_frame_capture(std::ptr::null(), std::ptr::null()); +} diff --git a/examples/checkerboard.rs b/examples/checkerboard.rs new file mode 100644 index 0000000..24de813 --- /dev/null +++ b/examples/checkerboard.rs @@ -0,0 +1,109 @@ +#[cfg(feature = "debug")] +use renderdoc::{RenderDoc, V110}; +use wgpu_mipmap::RecommendedMipmapGenerator; + +fn main() { + env_logger::init(); + #[cfg(feature = "debug")] + let mut rd: RenderDoc = RenderDoc::new().expect("Unable to connect"); + #[cfg(feature = "debug")] + rd.start_frame_capture(std::ptr::null(), std::ptr::null()); + let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY); + futures::executor::block_on((|| { + async { + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: None, + }) + .await + .expect("Failed to find an appropiate adapter"); + // Create the logical device and command queue + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + features: wgpu::Features::empty(), + limits: wgpu::Limits::default(), + shader_validation: true, + }, + None, + ) + .await + .expect("Failed to create device"); + // Generate texture data on the CPU + let width = 512; + let height = 512; + let mip_level_count = 1 + (width.max(height) as f32).log2().floor() as u32; + let data = wgpu_mipmap::util::checkerboard_rgba8(width, height, 16); + let texture_extent = wgpu::Extent3d { + width, + height, + depth: 1, + }; + // Generate different mipmaps for both a linear and srgb format + // with both the render and compute code paths + let formats: std::collections::HashMap<_, _> = vec![ + ("linear", wgpu::TextureFormat::Rgba8Unorm), + ("srgb", wgpu::TextureFormat::Rgba8UnormSrgb), + ] + .into_iter() + .collect(); + let supported_usage: std::collections::HashMap<_, _> = vec![ + ( + "compute", + wgpu_mipmap::ComputeMipmapGenerator::required_usage(), + ), + ( + "render", + wgpu_mipmap::RenderMipmapGenerator::required_usage(), + ), + ("copy", wgpu_mipmap::CopyMipmapGenerator::required_usage()), + ] + .into_iter() + .collect(); + let generator = RecommendedMipmapGenerator::new(&device); + for (format_str, format) in &formats { + for (usage_str, usage) in &supported_usage { + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format: *format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: *usage | wgpu::TextureUsage::COPY_DST | wgpu::TextureUsage::COPY_SRC, + label: None, + }; + + let mipmap_buffers = wgpu_mipmap::util::generate_and_copy_to_cpu( + &device, + &queue, + &generator, + &data, + &texture_descriptor, + ) + .await + .expect("shouldn't fail"); + + let has_file_system_available = cfg!(not(target_arch = "wasm32")); + if !has_file_system_available { + return; + } + + // Write the different mip levels as files + for (i, mip) in mipmap_buffers.iter().enumerate() { + image::save_buffer( + format!("checkerboard-{}-{}-{}.png", format_str, usage_str, i), + &mip.buffer, + mip.dimensions.width as u32, + mip.dimensions.height as u32, + image::ColorType::Rgba8, + ) + .unwrap(); + } + } + } + } + })()); + #[cfg(feature = "debug")] + rd.end_frame_capture(std::ptr::null(), std::ptr::null()); +} diff --git a/src/backends/compute.rs b/src/backends/compute.rs new file mode 100644 index 0000000..70fff6e --- /dev/null +++ b/src/backends/compute.rs @@ -0,0 +1,375 @@ +use crate::core::*; +use crate::util::get_mip_extent; +use log::warn; +use std::collections::HashMap; + +/// Generates mipmaps for textures with storage usage. +#[derive(Debug)] +pub struct ComputeMipmapGenerator { + layout_cache: HashMap, + pipeline_cache: HashMap, +} + +impl ComputeMipmapGenerator { + /// Returns the texture usage `ComputeMipmapGenerator` requires for mipmap generation. + pub fn required_usage() -> wgpu::TextureUsage { + wgpu::TextureUsage::STORAGE + } + + /// Creates a new `ComputeMipmapGenerator`. Once created, it can be used repeatedly to + /// generate mipmaps for any texture with format specified in `format_hints`. + pub fn new_with_format_hints( + device: &wgpu::Device, + format_hints: &[wgpu::TextureFormat], + ) -> Self { + let mut layout_cache = HashMap::new(); + let mut pipeline_cache = HashMap::new(); + for format in format_hints { + if let Some(module) = shader_for_format(device, format) { + let bind_group_layout = bind_group_layout_for_format(device, format); + let pipeline = + compute_pipeline_for_format(device, &module, &bind_group_layout, format); + layout_cache.insert(*format, bind_group_layout); + pipeline_cache.insert(*format, pipeline); + } else { + warn!( + "ComputeMipmapGenerator does not support requested format {:?}", + format + ); + continue; + } + } + Self { + layout_cache, + pipeline_cache, + } + } +} + +impl MipmapGenerator for ComputeMipmapGenerator { + fn generate( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + texture: &wgpu::Texture, + texture_descriptor: &wgpu::TextureDescriptor, + ) -> Result<(), Error> { + // Texture width and height must be a power of 2 + if !texture_descriptor.size.width.is_power_of_two() + || !texture_descriptor.size.height.is_power_of_two() + { + return Err(Error::NpotTexture); + } + // Texture dimension must be 2D + if texture_descriptor.dimension != wgpu::TextureDimension::D2 { + return Err(Error::UnsupportedDimension(texture_descriptor.dimension)); + } + if !texture_descriptor.usage.contains(Self::required_usage()) { + return Err(Error::UnsupportedUsage(texture_descriptor.usage)); + } + + let layout = self + .layout_cache + .get(&texture_descriptor.format) + .ok_or(Error::UnknownFormat(texture_descriptor.format))?; + let pipeline = self + .pipeline_cache + .get(&texture_descriptor.format) + .ok_or(Error::UnknownFormat(texture_descriptor.format))?; + + let mip_count = texture_descriptor.mip_level_count; + // TODO: Can we create the views every call? + let views = (0..mip_count) + .map(|base_mip_level| { + texture.create_view(&wgpu::TextureViewDescriptor { + label: None, + format: None, + dimension: None, + aspect: wgpu::TextureAspect::All, + base_mip_level, + level_count: std::num::NonZeroU32::new(1), + array_layer_count: None, + base_array_layer: 0, + }) + }) + .collect::>(); + // Now dispatch the compute pipeline for each mip level + // TODO: Likely need more flexibility here + // - The compute shaders must have matching local_size_x and local_size_y values + // - When the image size is less than 32x32, more work is performed than required + let x_work_group_count = 32; + let y_work_group_count = 32; + for mip in 1..mip_count as usize { + let src_view = &views[mip - 1]; + let dst_view = &views[mip]; + let mip_ext = get_mip_extent(&texture_descriptor.size, mip as u32); + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&src_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&dst_view), + }, + ], + }); + let mut pass = encoder.begin_compute_pass(); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, &bind_group, &[]); + pass.dispatch( + (mip_ext.width / x_work_group_count).max(1), + (mip_ext.height / y_work_group_count).max(1), + 1, + ); + } + Ok(()) + } +} + +fn shader_for_format( + device: &wgpu::Device, + format: &wgpu::TextureFormat, +) -> Option { + use wgpu::TextureFormat; + let s = |d| Some(device.create_shader_module(wgpu::util::make_spirv(d))); + match format { + TextureFormat::R8Unorm => s(include_bytes!("shaders/box_r8.comp.spv")), + TextureFormat::R8Snorm => s(include_bytes!("shaders/box_r8_snorm.comp.spv")), + TextureFormat::R16Float => s(include_bytes!("shaders/box_r16f.comp.spv")), + TextureFormat::Rg8Unorm => s(include_bytes!("shaders/box_rg8.comp.spv")), + TextureFormat::Rg8Snorm => s(include_bytes!("shaders/box_rg8_snorm.comp.spv")), + TextureFormat::R32Float => s(include_bytes!("shaders/box_r32f.comp.spv")), + TextureFormat::Rg16Float => s(include_bytes!("shaders/box_rg16f.comp.spv")), + TextureFormat::Rgba8Unorm => s(include_bytes!("shaders/box_rgba8.comp.spv")), + TextureFormat::Rgba8UnormSrgb | TextureFormat::Bgra8UnormSrgb => { + // On MacOS, my GPUFamily2 v1 capable GPU + // seems to perform the srgb -> linear before I load it + // in the shader, but expects me to perform the linear -> srgb + // conversion before storing. + #[cfg(target_os = "macos")] + { + s(include_bytes!("shaders/box_srgb_macos.comp.spv")) + } + // On Vulkan (and DX12?), the implementation does not perform + // any conversion, so this shader handles it all + #[cfg(not(target_os = "macos"))] + { + s(include_bytes!("shaders/box_srgb.comp.spv")) + } + } + TextureFormat::Rgba8Snorm => s(include_bytes!("shaders/box_rgba8_snorm.comp.spv")), + TextureFormat::Bgra8Unorm => s(include_bytes!("shaders/box_rgba8.comp.spv")), + TextureFormat::Rgb10a2Unorm => s(include_bytes!("shaders/box_rgb10_a2.comp.spv")), + TextureFormat::Rg11b10Float => s(include_bytes!("shaders/box_r11f_g11f_b10f.comp.spv")), + TextureFormat::Rg32Float => s(include_bytes!("shaders/box_rg32f.comp.spv")), + TextureFormat::Rgba16Float => s(include_bytes!("shaders/box_rgba16f.comp.spv")), + TextureFormat::Rgba32Float => s(include_bytes!("shaders/box_rgba32f.comp.spv")), + _ => None, + } +} + +fn bind_group_layout_for_format( + device: &wgpu::Device, + format: &wgpu::TextureFormat, +) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStage::COMPUTE, + ty: wgpu::BindingType::StorageTexture { + dimension: wgpu::TextureViewDimension::D2, + format: *format, + readonly: true, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStage::COMPUTE, + ty: wgpu::BindingType::StorageTexture { + dimension: wgpu::TextureViewDimension::D2, + format: *format, + readonly: false, + }, + count: None, + }, + ], + }) +} + +fn compute_pipeline_for_format( + device: &wgpu::Device, + module: &wgpu::ShaderModule, + bind_group_layout: &wgpu::BindGroupLayout, + format: &wgpu::TextureFormat, +) -> wgpu::ComputePipeline { + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: Some(&format!("wgpu-mipmap-compute-pipeline-{:?}", format)), + layout: Some(&pipeline_layout), + compute_stage: wgpu::ProgrammableStageDescriptor { + module: &module, + entry_point: "main", + }, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::*; + + fn init() { + let _ = env_logger::builder().is_test(true).try_init(); + } + + #[allow(dead_code)] + async fn generate_and_copy_to_cpu_compute( + buffer: &[u8], + texture_descriptor: &wgpu::TextureDescriptor<'_>, + ) -> Result, Error> { + let (_instance, _adaptor, device, queue) = wgpu_setup().await; + let generator = crate::backends::ComputeMipmapGenerator::new_with_format_hints( + &device, + &[texture_descriptor.format], + ); + Ok( + generate_and_copy_to_cpu(&device, &queue, &generator, buffer, texture_descriptor) + .await?, + ) + } + + async fn generate_test(texture_descriptor: &wgpu::TextureDescriptor<'_>) -> Result<(), Error> { + let (_instance, _adapter, device, _queue) = wgpu_setup().await; + let generator = + ComputeMipmapGenerator::new_with_format_hints(&device, &[texture_descriptor.format]); + let texture = device.create_texture(&texture_descriptor); + let mut encoder = device.create_command_encoder(&Default::default()); + generator.generate(&device, &mut encoder, &texture, &texture_descriptor) + } + + #[test] + fn sanity_check() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + // Create a texture + let format = wgpu::TextureFormat::R8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: ComputeMipmapGenerator::required_usage(), + label: None, + }; + futures::executor::block_on((|| async { + let res = generate_test(&texture_descriptor).await; + assert!(res.is_ok()); + })()); + } + + #[test] + fn unsupported_npot() { + init(); + // Generate texture data on the CPU + let size = 511; + let mip_level_count = 1 + (size as f32).log2() as u32; + // Create a texture + let format = wgpu::TextureFormat::R8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: ComputeMipmapGenerator::required_usage(), + label: None, + }; + futures::executor::block_on((|| async { + let res = generate_test(&texture_descriptor).await; + assert!(res.is_err()); + assert!(res.err() == Some(Error::NpotTexture)); + })()); + } + + #[test] + fn unsupported_usage() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + // Create a texture + let format = wgpu::TextureFormat::R8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: wgpu::TextureUsage::empty(), + label: None, + }; + futures::executor::block_on((|| async { + let res = generate_test(&texture_descriptor).await; + assert!(res.is_err()); + assert!(res.err() == Some(Error::UnsupportedUsage(wgpu::TextureUsage::empty()))); + })()); + } + + #[test] + fn unknown_format() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + // Create a texture + let format = wgpu::TextureFormat::Rg16Sint; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: ComputeMipmapGenerator::required_usage(), + label: None, + }; + futures::executor::block_on((|| async { + let res = generate_test(&texture_descriptor).await; + assert!(res.is_err()); + assert!(res.err() == Some(Error::UnknownFormat(wgpu::TextureFormat::Rg16Sint))); + })()); + } +} diff --git a/src/backends/copy.rs b/src/backends/copy.rs new file mode 100644 index 0000000..abe31c0 --- /dev/null +++ b/src/backends/copy.rs @@ -0,0 +1,171 @@ +use crate::backends::RenderMipmapGenerator; +use crate::core::*; +use crate::util::get_mip_extent; + +/// Generates mipmaps for textures with sampled usage. +pub struct CopyMipmapGenerator<'a> { + generator: &'a RenderMipmapGenerator, +} + +impl<'a> CopyMipmapGenerator<'a> { + // Creates a new `CopyMipmapGenerator` from an existing `RenderMipmapGenerator` + /// Once created, it can be used repeatedly to generate mipmaps for any + /// texture supported by the render generator. + pub fn new(generator: &'a RenderMipmapGenerator) -> Self { + Self { generator } + } + + /// Returns the texture usage `CopyMipmapGenerator` requires for mipmap + /// generation. + pub fn required_usage() -> wgpu::TextureUsage { + wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::COPY_DST + } +} + +impl<'a> MipmapGenerator for CopyMipmapGenerator<'a> { + fn generate( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + texture: &wgpu::Texture, + texture_descriptor: &wgpu::TextureDescriptor, + ) -> Result<(), Error> { + // Create a temporary texture with half the resolution + // of the original texture, and one less mip level + // We'll generate mipmaps into this texture, then + // copy the results back into the mip levels of the original texture + let tmp_descriptor = wgpu::TextureDescriptor { + label: None, + size: get_mip_extent(&texture_descriptor.size, 1), + mip_level_count: texture_descriptor.mip_level_count - 1, + sample_count: texture_descriptor.sample_count, + dimension: texture_descriptor.dimension, + format: texture_descriptor.format, + usage: RenderMipmapGenerator::required_usage() | wgpu::TextureUsage::COPY_SRC, + }; + let tmp_texture = device.create_texture(&tmp_descriptor); + self.generator.generate_src_dst( + device, + encoder, + &texture, + &tmp_texture, + texture_descriptor, + &tmp_descriptor, + 1, + )?; + let mip_count = tmp_descriptor.mip_level_count; + for i in 0..mip_count { + encoder.copy_texture_to_texture( + wgpu::TextureCopyView { + texture: &tmp_texture, + mip_level: i, + origin: wgpu::Origin3d::default(), + }, + wgpu::TextureCopyView { + texture: &texture, + mip_level: i + 1, + origin: wgpu::Origin3d::default(), + }, + get_mip_extent(&tmp_descriptor.size, i), + ); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::*; + + fn init() { + let _ = env_logger::builder().is_test(true).try_init(); + } + + #[allow(dead_code)] + async fn generate_and_copy_to_cpu_render_slow( + buffer: &[u8], + texture_descriptor: &wgpu::TextureDescriptor<'_>, + ) -> Result, Error> { + let (_instance, _adaptor, device, queue) = wgpu_setup().await; + + let generator = crate::backends::RenderMipmapGenerator::new_with_format_hints( + &device, + &[texture_descriptor.format], + ); + let fallback = CopyMipmapGenerator::new(&generator); + Ok( + generate_and_copy_to_cpu(&device, &queue, &fallback, buffer, texture_descriptor) + .await?, + ) + } + + async fn generate_test(texture_descriptor: &wgpu::TextureDescriptor<'_>) -> Result<(), Error> { + let (_instance, _adapter, device, _queue) = wgpu_setup().await; + let render = crate::backends::RenderMipmapGenerator::new_with_format_hints( + &device, + &[texture_descriptor.format], + ); + let generator = CopyMipmapGenerator::new(&render); + let texture = device.create_texture(&texture_descriptor); + let mut encoder = device.create_command_encoder(&Default::default()); + generator.generate(&device, &mut encoder, &texture, &texture_descriptor) + } + + #[test] + fn sanity_check() { + init(); + // Generate texture data on the CPU + let size = 511; + let mip_level_count = 1 + (size as f32).log2() as u32; + // Create a texture + let format = wgpu::TextureFormat::R8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: CopyMipmapGenerator::required_usage(), + label: None, + }; + futures::executor::block_on((|| async { + let res = generate_test(&texture_descriptor).await; + assert!(res.is_ok()); + })()); + } + + #[test] + fn unsupported_format() { + init(); + // Generate texture data on the CPU + let size = 511; + let mip_level_count = 1 + (size as f32).log2() as u32; + // Create a texture + let format = wgpu::TextureFormat::R8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: wgpu::TextureUsage::empty(), + label: None, + }; + futures::executor::block_on((|| async { + let res = generate_test(&texture_descriptor).await; + assert!(res.is_err()); + assert!(res.err() == Some(Error::UnsupportedUsage(texture_descriptor.usage))); + })()); + } +} diff --git a/src/backends/mod.rs b/src/backends/mod.rs new file mode 100644 index 0000000..25823fa --- /dev/null +++ b/src/backends/mod.rs @@ -0,0 +1,9 @@ +mod compute; +mod copy; +mod recommended; +mod render; + +pub use compute::*; +pub use copy::*; +pub use recommended::*; +pub use render::*; diff --git a/src/backends/recommended.rs b/src/backends/recommended.rs new file mode 100644 index 0000000..39b0409 --- /dev/null +++ b/src/backends/recommended.rs @@ -0,0 +1,658 @@ +use super::compute::*; +use super::copy::*; +use super::render::*; +use crate::core::*; +use log::{debug, warn}; + +/// Generates mipmaps for textures with any usage using the compute, render, or copy backends. +#[derive(Debug)] +pub struct RecommendedMipmapGenerator { + render: RenderMipmapGenerator, + compute: ComputeMipmapGenerator, +} + +/// A list of supported texture formats. +const SUPPORTED_FORMATS: [wgpu::TextureFormat; 17] = { + use wgpu::TextureFormat; + [ + TextureFormat::R8Unorm, + TextureFormat::R8Snorm, + TextureFormat::R16Float, + TextureFormat::Rg8Unorm, + TextureFormat::Rg8Snorm, + TextureFormat::R32Float, + TextureFormat::Rg16Float, + TextureFormat::Rgba8Unorm, + TextureFormat::Rgba8Snorm, + TextureFormat::Bgra8Unorm, + TextureFormat::Bgra8UnormSrgb, + TextureFormat::Rgba8UnormSrgb, + TextureFormat::Rgb10a2Unorm, + TextureFormat::Rg11b10Float, + TextureFormat::Rg32Float, + TextureFormat::Rgba16Float, + TextureFormat::Rgba32Float, + ] +}; + +impl RecommendedMipmapGenerator { + /// Creates a new `RecommendedMipmapGenerator`. Once created, it can be used repeatedly to + /// generate mipmaps for any texture with a supported format. + pub fn new(device: &wgpu::Device) -> Self { + Self::new_with_format_hints(device, &SUPPORTED_FORMATS) + } + + /// Creates a new `RecommendedMipmapGenerator`. Once created, it can be used repeatedly to + /// generate mipmaps for any texture with format specified in `format_hints`. + pub fn new_with_format_hints( + device: &wgpu::Device, + format_hints: &[wgpu::TextureFormat], + ) -> Self { + for format in format_hints { + if !SUPPORTED_FORMATS.contains(&format) { + warn!("[RecommendedMipmapGenerator::new] No support for requested texture format {:?}", *format); + warn!("[RecommendedMipmapGenerator::new] Attempting to continue, but calls to generate may fail or produce unexpected results."); + continue; + } + } + let render = RenderMipmapGenerator::new_with_format_hints(device, format_hints); + let compute = ComputeMipmapGenerator::new_with_format_hints(device, format_hints); + Self { render, compute } + } +} + +impl MipmapGenerator for RecommendedMipmapGenerator { + fn generate( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + texture: &wgpu::Texture, + texture_descriptor: &wgpu::TextureDescriptor, + ) -> Result<(), Error> { + // compute backend + match self + .compute + .generate(device, encoder, texture, texture_descriptor) + { + Err(e) => { + debug!("[RecommendedMipmapGenerator::generate] compute error {}.\n falling back to render backend.", e); + } + ok => return ok, + }; + // render backend + match self + .render + .generate(device, encoder, texture, texture_descriptor) + { + Err(e) => { + debug!("[RecommendedMipmapGenerator::generate] render error {}.\n falling back to copy backend.", e); + } + ok => return ok, + }; + // copy backend + match CopyMipmapGenerator::new(&self.render).generate( + device, + encoder, + texture, + texture_descriptor, + ) { + Err(e) => { + debug!("[RecommendedMipmapGenerator::generate] copy error {}.", e); + } + ok => return ok, + } + Err(Error::UnsupportedUsage(texture_descriptor.usage)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::*; + + fn init() { + let _ = env_logger::builder().is_test(true).try_init(); + } + + #[allow(dead_code)] + async fn generate_and_copy_to_cpu_recommended( + buffer: &[u8], + texture_descriptor: &wgpu::TextureDescriptor<'_>, + ) -> Result, Error> { + let (_instance, _adaptor, device, queue) = wgpu_setup().await; + let generator = crate::backends::RecommendedMipmapGenerator::new_with_format_hints( + &device, + &[texture_descriptor.format], + ); + Ok( + generate_and_copy_to_cpu(&device, &queue, &generator, buffer, texture_descriptor) + .await?, + ) + } + #[test] + fn checkerboard_r8_render() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + let checkboard_size = 16; + let data = checkerboard_r8(size, size, checkboard_size); + // Create a texture + let format = wgpu::TextureFormat::R8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: crate::RenderMipmapGenerator::required_usage() + | wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC, + label: None, + }; + dbg!(format); + futures::executor::block_on((|| async { + let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor) + .await + .unwrap(); + + assert!(mipmap_buffers.len() == mip_level_count as usize); + + for mip in &mipmap_buffers { + assert!( + mip.buffer.len() + == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height + ); + } + // The last mip map level should be 1x1 and the value is an average of 0 and 255 + mipmap_buffers.last().map(|mip| { + let width = mip.dimensions.width; + let height = mip.dimensions.height; + let bpp = mip.dimensions.bytes_per_channel; + let data = &mip.buffer; + dbg!(data); + assert!(width == 1); + assert!(height == 1); + assert!(data.len() == width * height * bpp); + // The final result is implementation dependent + // but we expect the pixel to be a perfect + // blend of white and black, i.e 255 / 2 = 127.5 + // Depending on the platform and underlying implementation, + // this might round up or down so check 127 and 128 + assert!(data[0] == 127 || data[0] == 128); + }); + })()); + } + + #[test] + fn checkerboard_rgba8_render() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + let checkboard_size = 16; + let data = checkerboard_rgba8(size, size, checkboard_size); + // Create a texture + let format = wgpu::TextureFormat::Rgba8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: crate::RenderMipmapGenerator::required_usage() + | wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC, + label: None, + }; + futures::executor::block_on((|| async { + let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor) + .await + .unwrap(); + assert!(mipmap_buffers.len() == mip_level_count as usize); + for mip in &mipmap_buffers { + assert!( + mip.buffer.len() + == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height + ); + } + // The last mip map level should be 1x1 and each of the 4 componenets per pixel + // should be the average of 0 and 255, but in sRGB color space + mipmap_buffers.last().map(|mip| { + let width = mip.dimensions.width; + let height = mip.dimensions.height; + let bpp = mip.dimensions.bytes_per_channel; + let data = &mip.buffer; + assert!(width == 1); + assert!(height == 1); + assert!(data.len() == width * height * bpp); + assert!(data[0] == 127 || data[0] == 128); + assert!(data[1] == 127 || data[1] == 128); + assert!(data[2] == 127 || data[2] == 128); + assert!(data[3] == 255); + }); + })()); + } + + #[test] + fn checkerboard_srgba8_render() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + let checkboard_size = 16; + let data = checkerboard_rgba8(size, size, checkboard_size); + // Create a texture + let format = wgpu::TextureFormat::Rgba8UnormSrgb; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: crate::RenderMipmapGenerator::required_usage() + | wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC, + label: None, + }; + futures::executor::block_on((|| async { + let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor) + .await + .unwrap(); + assert!(mipmap_buffers.len() == mip_level_count as usize); + for mip in &mipmap_buffers { + assert!( + mip.buffer.len() + == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height + ); + } + // The last mip map level should be 1x1 and each of the 4 componenets per pixel + // should be the average of 0 and 255, but in sRGB color space + mipmap_buffers.last().map(|mip| { + let width = mip.dimensions.width; + let height = mip.dimensions.height; + let bpp = mip.dimensions.bytes_per_channel; + let data = &mip.buffer; + assert!(width == 1); + assert!(height == 1); + assert!(data.len() == width * height * bpp); + // The final result is implementation dependent + // See https://entropymine.com/imageworsener/srgbformula/ + // for how to convert between linear and srgb + // Where does 187 and 188 come from? Solve for x in: + // ((((x / 255 + 0.055) / 1.055)^2.4) * 255) == (255 / 2) + // -> x = 187.516155 + assert!(data[0] == 187 || data[0] == 188); + assert!(data[1] == 187 || data[1] == 188); + assert!(data[2] == 187 || data[2] == 188); + assert!(data[3] == 255); + }); + })()); + } + + #[test] + fn checkerboard_r8_compute() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + let checkboard_size = 16; + let data = checkerboard_r8(size, size, checkboard_size); + // Create a texture + let format = wgpu::TextureFormat::R8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: crate::ComputeMipmapGenerator::required_usage() + | wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC, + label: None, + }; + futures::executor::block_on((|| async { + let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor) + .await + .unwrap(); + + assert!(mipmap_buffers.len() == mip_level_count as usize); + + for mip in &mipmap_buffers { + assert!( + mip.buffer.len() + == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height + ); + } + // The last mip map level should be 1x1 and the value is an average of 0 and 255 + mipmap_buffers.last().map(|mip| { + let width = mip.dimensions.width; + let height = mip.dimensions.height; + let bpp = mip.dimensions.bytes_per_channel; + let data = &mip.buffer; + assert!(width == 1); + assert!(height == 1); + assert!(data.len() == width * height * bpp); + // The final result is implementation dependent + // but we expect the pixel to be a perfect + // blend of white and black, i.e 255 / 2 = 127.5 + // Depending on the platform and underlying implementation, + // this might round up or down so check 127 and 128 + assert!(data[0] == 127 || data[0] == 128); + }); + })()); + } + + #[test] + fn checkerboard_rgba8_compute() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + let checkboard_size = 16; + let data = checkerboard_rgba8(size, size, checkboard_size); + // Create a texture + let format = wgpu::TextureFormat::Rgba8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: crate::ComputeMipmapGenerator::required_usage() + | wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC, + label: None, + }; + futures::executor::block_on((|| async { + let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor) + .await + .unwrap(); + assert!(mipmap_buffers.len() == mip_level_count as usize); + for mip in &mipmap_buffers { + assert!( + mip.buffer.len() + == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height + ); + } + // The last mip map level should be 1x1 and each of the 4 componenets per pixel + // should be the average of 0 and 255 + mipmap_buffers.last().map(|mip| { + let width = mip.dimensions.width; + let height = mip.dimensions.height; + let bpp = mip.dimensions.bytes_per_channel; + let data = &mip.buffer; + assert!(width == 1); + assert!(height == 1); + assert!(data.len() == width * height * bpp); + assert!(data[0] == 127 || data[0] == 128); + assert!(data[1] == 127 || data[1] == 128); + assert!(data[2] == 127 || data[2] == 128); + assert!(data[3] == 255); + }); + })()); + } + + #[test] + fn checkerboard_srgba8_compute() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + let checkboard_size = 16; + let data = checkerboard_rgba8(size, size, checkboard_size); + // Create a texture + let format = wgpu::TextureFormat::Rgba8UnormSrgb; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: crate::ComputeMipmapGenerator::required_usage() + | wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC, + label: None, + }; + futures::executor::block_on((|| async { + let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor) + .await + .unwrap(); + assert!(mipmap_buffers.len() == mip_level_count as usize); + for mip in &mipmap_buffers { + assert!( + mip.buffer.len() + == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height + ); + } + // The last mip map level should be 1x1 and each of the 4 componenets per pixel + // should be the average of 0 and 255, but in sRGB color space + mipmap_buffers.last().map(|mip| { + let width = mip.dimensions.width; + let height = mip.dimensions.height; + let bpp = mip.dimensions.bytes_per_channel; + let data = &mip.buffer; + assert!(width == 1); + assert!(height == 1); + assert!(data.len() == width * height * bpp); + // The final result is implementation dependent + // See https://entropymine.com/imageworsener/srgbformula/ + // for how to convert between linear and srgb + // Where does 187 and 188 come from? Solve for x in: + // ((((x / 255 + 0.055) / 1.055)^2.4) * 255) == (255 / 2) + // -> x = 187.516155 + dbg!(data); + assert!(data[0] == 187 || data[0] == 188); + assert!(data[1] == 187 || data[1] == 188); + assert!(data[2] == 187 || data[2] == 188); + assert!(data[3] == 255); + }); + })()); + } + + #[test] + fn checkerboard_rgba32f_render() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + let checkboard_size = 16; + let data = checkerboard_rgba32f(size, size, checkboard_size); + // Create a texture + let format = wgpu::TextureFormat::Rgba32Float; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: crate::RenderMipmapGenerator::required_usage() + | wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC, + label: None, + }; + futures::executor::block_on((|| async { + let mipmap_buffers = generate_and_copy_to_cpu_recommended( + bytemuck::cast_slice(&data), + &texture_descriptor, + ) + .await + .unwrap(); + assert!(mipmap_buffers.len() == mip_level_count as usize); + for mip in &mipmap_buffers { + assert!( + mip.buffer.len() + == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height + ); + } + // The last mip map level should be 1x1 and each of the 4 componenets per pixel + // should be the average of 0.0 and 1.0 + mipmap_buffers.last().map(|mip| { + let width = mip.dimensions.width; + let height = mip.dimensions.height; + let bpp = mip.dimensions.bytes_per_channel; + let data = &mip.buffer; + assert!(width == 1); + assert!(height == 1); + assert!(data.len() == width * height * bpp); + let data: &[f32] = bytemuck::try_cast_slice(data).unwrap(); + dbg!(data); + assert!(data[0] == 0.5); + assert!(data[1] == 0.5); + assert!(data[2] == 0.5); + assert!(data[3] == 1.0); + }); + })()); + } + + #[test] + fn checkerboard_rgba32f_compute() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + let checkboard_size = 16; + let data = checkerboard_rgba32f(size, size, checkboard_size); + // Create a texture + let format = wgpu::TextureFormat::Rgba32Float; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: crate::ComputeMipmapGenerator::required_usage() + | wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC, + label: None, + }; + futures::executor::block_on((|| async { + let mipmap_buffers = generate_and_copy_to_cpu_recommended( + bytemuck::cast_slice(&data), + &texture_descriptor, + ) + .await + .unwrap(); + assert!(mipmap_buffers.len() == mip_level_count as usize); + for mip in &mipmap_buffers { + assert!( + mip.buffer.len() + == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height + ); + } + // The last mip map level should be 1x1 and each of the 4 componenets per pixel + // should be the average of 0.0 and 1.0 + mipmap_buffers.last().map(|mip| { + let width = mip.dimensions.width; + let height = mip.dimensions.height; + let bpp = mip.dimensions.bytes_per_channel; + let data = &mip.buffer; + assert!(width == 1); + assert!(height == 1); + assert!(data.len() == width * height * bpp); + let data: &[f32] = bytemuck::try_cast_slice(data).unwrap(); + dbg!(data); + assert!(data[0] == 0.5); + assert!(data[1] == 0.5); + assert!(data[2] == 0.5); + assert!(data[3] == 1.0); + }); + })()); + } + + #[test] + fn checkerboard_rgba8_copy() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + let checkboard_size = 16; + let data = checkerboard_rgba8(size, size, checkboard_size); + // Create a texture + let format = wgpu::TextureFormat::Rgba8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: crate::CopyMipmapGenerator::required_usage() + | wgpu::TextureUsage::COPY_SRC + | wgpu::TextureUsage::COPY_DST, + label: None, + }; + futures::executor::block_on((|| async { + let mipmap_buffers = generate_and_copy_to_cpu_recommended(&data, &texture_descriptor) + .await + .unwrap(); + assert!(mipmap_buffers.len() == mip_level_count as usize); + for mip in &mipmap_buffers { + assert!( + mip.buffer.len() + == mip.dimensions.unpadded_bytes_per_row * mip.dimensions.height + ); + } + // The last mip map level should be 1x1 and each of the 4 componenets per pixel + // should be the average of 0 and 255 + mipmap_buffers.last().map(|mip| { + let width = mip.dimensions.width; + let height = mip.dimensions.height; + let bpp = mip.dimensions.bytes_per_channel; + let data = &mip.buffer; + dbg!(data); + assert!(width == 1); + assert!(height == 1); + assert!(data.len() == width * height * bpp); + assert!(data[0] == 127 || data[0] == 128); + assert!(data[1] == 127 || data[1] == 128); + assert!(data[2] == 127 || data[2] == 128); + assert!(data[3] == 255); + }); + })()); + } +} diff --git a/src/backends/render.rs b/src/backends/render.rs new file mode 100644 index 0000000..7826461 --- /dev/null +++ b/src/backends/render.rs @@ -0,0 +1,399 @@ +use crate::core::*; +use crate::util::get_mip_extent; +use log::warn; +use std::collections::HashMap; + +/// Generates mipmaps for textures with output attachment usage. +#[derive(Debug)] +pub struct RenderMipmapGenerator { + sampler: wgpu::Sampler, + layout_cache: HashMap, + pipeline_cache: HashMap, +} + +impl RenderMipmapGenerator { + /// Returns the texture usage `RenderMipmapGenerator` requires for mipmap generation. + pub fn required_usage() -> wgpu::TextureUsage { + wgpu::TextureUsage::OUTPUT_ATTACHMENT | wgpu::TextureUsage::SAMPLED + } + + /// Creates a new `RenderMipmapGenerator`. Once created, it can be used repeatedly to + /// generate mipmaps for any texture with format specified in `format_hints`. + pub fn new_with_format_hints( + device: &wgpu::Device, + format_hints: &[wgpu::TextureFormat], + ) -> Self { + // A sampler for box filter with clamp to edge behavior + // In practice, the final result may be implementation dependent + // - [Vulkan](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#textures-texel-linear-filtering) + // - [Metal](https://developer.apple.com/documentation/metal/mtlsamplerminmagfilter/linear) + // - [DX12](https://docs.microsoft.com/en-us/windows/win32/api/d3d12/ne-d3d12-d3d12_filter) + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some(&"wgpu-mipmap-sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let render_layout_cache = { + let mut layout_cache = HashMap::new(); + // For now, we only cache a bind group layout for floating-point textures + for component_type in &[wgpu::TextureComponentType::Float] { + let bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some(&format!("wgpu-mipmap-bg-layout-{:?}", component_type)), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::SampledTexture { + dimension: wgpu::TextureViewDimension::D2, + component_type: *component_type, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Sampler { comparison: false }, + count: None, + }, + ], + }); + layout_cache.insert(*component_type, bind_group_layout); + } + layout_cache + }; + + let render_pipeline_cache = { + let mut pipeline_cache = HashMap::new(); + let vertex_module = device.create_shader_module(wgpu::util::make_spirv( + include_bytes!("shaders/triangle.vert.spv"), + )); + let box_filter = device.create_shader_module(wgpu::util::make_spirv(include_bytes!( + "shaders/box.frag.spv" + ))); + for format in format_hints { + let fragment_module = &box_filter; + + let component_type = wgpu::TextureComponentType::from(*format); + if let Some(bind_group_layout) = render_layout_cache.get(&component_type) { + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[bind_group_layout], + push_constant_ranges: &[], + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(&format!("wgpu-mipmap-render-pipeline-{:?}", format)), + layout: Some(&layout), + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &vertex_module, + entry_point: "main", + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { + module: &fragment_module, + entry_point: "main", + }), + rasterization_state: Some(wgpu::RasterizationStateDescriptor { + front_face: wgpu::FrontFace::Ccw, + cull_mode: wgpu::CullMode::Back, + ..Default::default() + }), + primitive_topology: wgpu::PrimitiveTopology::TriangleList, + color_states: &[(*format).into()], + depth_stencil_state: None, + vertex_state: wgpu::VertexStateDescriptor { + index_format: wgpu::IndexFormat::Uint16, + vertex_buffers: &[], + }, + sample_count: 1, + sample_mask: !0, + alpha_to_coverage_enabled: false, + }); + pipeline_cache.insert(*format, pipeline); + } else { + warn!( + "RenderMipmapGenerator does not support requested format {:?}", + format + ); + continue; + } + } + pipeline_cache + }; + + Self { + sampler, + layout_cache: render_layout_cache, + pipeline_cache: render_pipeline_cache, + } + } + + /// Generate mipmaps from level 0 of `src_texture` to + /// levels `dst_mip_offset..dst_texture_descriptor.mip_level_count` + // of `dst_texture`. + #[allow(clippy::too_many_arguments)] + pub(crate) fn generate_src_dst( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + src_texture: &wgpu::Texture, + dst_texture: &wgpu::Texture, + src_texture_descriptor: &wgpu::TextureDescriptor, + dst_texture_descriptor: &wgpu::TextureDescriptor, + dst_mip_offset: u32, + ) -> Result<(), Error> { + let src_format = src_texture_descriptor.format; + let src_mip_count = src_texture_descriptor.mip_level_count; + let src_ext = src_texture_descriptor.size; + let src_dim = src_texture_descriptor.dimension; + let src_usage = src_texture_descriptor.usage; + let src_next_mip_ext = get_mip_extent(&src_ext, 1); + + let dst_format = dst_texture_descriptor.format; + let dst_mip_count = dst_texture_descriptor.mip_level_count; + let dst_ext = dst_texture_descriptor.size; + let dst_dim = dst_texture_descriptor.dimension; + let dst_usage = dst_texture_descriptor.usage; + // invariants that we expect callers to uphold + if src_format != dst_format { + dbg!(src_texture_descriptor); + dbg!(dst_texture_descriptor); + panic!("src and dst texture formats must be equal"); + } + if src_dim != dst_dim { + dbg!(src_texture_descriptor); + dbg!(dst_texture_descriptor); + panic!("src and dst texture dimensions must be eqaul"); + } + if !((src_mip_count == dst_mip_count && src_ext == dst_ext) + || (src_next_mip_ext == dst_ext)) + { + dbg!(src_texture_descriptor); + dbg!(dst_texture_descriptor); + panic!("src and dst texture extents must match or dst must be half the size of src"); + } + + if src_dim != wgpu::TextureDimension::D2 { + return Err(Error::UnsupportedDimension(src_dim)); + } + // src texture must be sampled + if !src_usage.contains(wgpu::TextureUsage::SAMPLED) { + return Err(Error::UnsupportedUsage(src_usage)); + } + // dst texture must be sampled and output attachment + if !dst_usage.contains(Self::required_usage()) { + return Err(Error::UnsupportedUsage(dst_usage)); + } + let format = src_format; + let pipeline = self + .pipeline_cache + .get(&format) + .ok_or(Error::UnknownFormat(format))?; + let component_type = wgpu::TextureComponentType::from(format); + let layout = self + .layout_cache + .get(&component_type) + .ok_or(Error::UnknownFormat(format))?; + let views = (0..src_mip_count) + .map(|mip_level| { + // The first view is mip level 0 of the src texture + // Subsequent views are for the dst_texture + let (texture, base_mip_level) = if mip_level == 0 { + (src_texture, 0) + } else { + (dst_texture, mip_level - dst_mip_offset) + }; + texture.create_view(&wgpu::TextureViewDescriptor { + label: None, + format: None, + dimension: None, + aspect: wgpu::TextureAspect::All, + base_mip_level, + level_count: std::num::NonZeroU32::new(1), + array_layer_count: None, + base_array_layer: 0, + }) + }) + .collect::>(); + for mip in 1..src_mip_count as usize { + let src_view = &views[mip - 1]; + let dst_view = &views[mip]; + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&src_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&self.sampler), + }, + ], + }); + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: &dst_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + }], + depth_stencil_attachment: None, + }); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, &bind_group, &[]); + pass.draw(0..3, 0..1); + } + Ok(()) + } +} + +impl MipmapGenerator for RenderMipmapGenerator { + fn generate( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + texture: &wgpu::Texture, + texture_descriptor: &wgpu::TextureDescriptor, + ) -> Result<(), Error> { + self.generate_src_dst( + device, + encoder, + &texture, + &texture, + &texture_descriptor, + &texture_descriptor, + 0, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::*; + + fn init() { + let _ = env_logger::builder().is_test(true).try_init(); + } + + #[allow(dead_code)] + async fn generate_and_copy_to_cpu_render( + buffer: &[u8], + texture_descriptor: &wgpu::TextureDescriptor<'_>, + ) -> Result, Error> { + let (_instance, _adaptor, device, queue) = wgpu_setup().await; + let generator = crate::backends::RenderMipmapGenerator::new_with_format_hints( + &device, + &[texture_descriptor.format], + ); + Ok( + generate_and_copy_to_cpu(&device, &queue, &generator, buffer, texture_descriptor) + .await?, + ) + } + + async fn generate_test(texture_descriptor: &wgpu::TextureDescriptor<'_>) -> Result<(), Error> { + let (_instance, _adapter, device, _queue) = wgpu_setup().await; + let generator = + RenderMipmapGenerator::new_with_format_hints(&device, &[texture_descriptor.format]); + let texture = device.create_texture(&texture_descriptor); + let mut encoder = device.create_command_encoder(&Default::default()); + generator.generate(&device, &mut encoder, &texture, &texture_descriptor) + } + + #[test] + fn sanity_check() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + // Create a texture + let format = wgpu::TextureFormat::R8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: RenderMipmapGenerator::required_usage(), + label: None, + }; + futures::executor::block_on((|| async { + let res = generate_test(&texture_descriptor).await; + assert!(res.is_ok()); + })()); + } + + #[test] + fn unsupported_usage() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + // Create a texture + let format = wgpu::TextureFormat::R8Unorm; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: wgpu::TextureUsage::empty(), + label: None, + }; + futures::executor::block_on((|| async { + let res = generate_test(&texture_descriptor).await; + assert!(res.is_err()); + assert!(res.err() == Some(Error::UnsupportedUsage(wgpu::TextureUsage::empty()))); + })()); + } + + #[test] + fn unknown_format() { + init(); + // Generate texture data on the CPU + let size = 512; + let mip_level_count = 1 + (size as f32).log2() as u32; + // Create a texture + let format = wgpu::TextureFormat::Rgba8Sint; + let texture_extent = wgpu::Extent3d { + width: size, + height: size, + depth: 1, + }; + let texture_descriptor = wgpu::TextureDescriptor { + size: texture_extent, + mip_level_count, + format, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::OUTPUT_ATTACHMENT, + label: None, + }; + futures::executor::block_on((|| async { + let res = generate_test(&texture_descriptor).await; + assert!(res.is_err()); + assert!(res.err() == Some(Error::UnknownFormat(wgpu::TextureFormat::Rgba8Sint))); + })()); + } +} diff --git a/src/backends/shaders/README.md b/src/backends/shaders/README.md new file mode 100644 index 0000000..6f91b37 --- /dev/null +++ b/src/backends/shaders/README.md @@ -0,0 +1,16 @@ +# Shaders + +## Dependencies + +In order to compile GLSL to SPIRV, the following binaries must be on your `$PATH`: + +- `glslc` from [Google/shaderc](https://github.com/google/shaderc) +- `spirv-opt` from [KhronosGroup/SPIRV-Tools][https://github.com/khronosgroup/spirv-tools] + +Then, you can run: + +```console +$ ./compile.sh +``` + +This script handles generating all the compute shader combinations required by the code. diff --git a/src/backends/shaders/box.comp b/src/backends/shaders/box.comp new file mode 100644 index 0000000..7ac706c --- /dev/null +++ b/src/backends/shaders/box.comp @@ -0,0 +1,26 @@ +// This is a template file! +// It is expectd that you wiill generate a real file from it using envsubst: +// +// FORMAT=rgba8 envsubst box.comp + +#version 450 +// The size values must match the values specified in +// backends/compute.rs +layout(local_size_x = 32, local_size_y = 32) in; + +layout(set = 0, binding = 0, ${FORMAT}) uniform readonly image2D u_src; +layout(set = 0, binding = 1, ${FORMAT}) uniform writeonly image2D u_dst; + +// Clamp to edge +#define L(u) imageLoad(u_src, clamp(u, ivec2(0), ivec2(imageSize(u_src) - 1))) + +void main() { + ivec2 dst_uv = ivec2(gl_GlobalInvocationID.xy); + ivec2 src_uv = 2 * dst_uv; + vec4 l = L(src_uv + ivec2(0, 0)); + vec4 r = L(src_uv + ivec2(1, 0)); + vec4 u = L(src_uv + ivec2(0, 1)); + vec4 d = L(src_uv + ivec2(1, 1)); + vec4 c = (l + r + u + d) / 4.0; + imageStore(u_dst, dst_uv, c); +} diff --git a/src/backends/shaders/box.frag b/src/backends/shaders/box.frag new file mode 100644 index 0000000..12a68fe --- /dev/null +++ b/src/backends/shaders/box.frag @@ -0,0 +1,11 @@ +#version 450 +// Expected that the sampler has mag_filter set to linear +layout(set = 0, binding = 0) uniform texture2D u_texture; +layout(set = 0, binding = 1) uniform sampler u_sampler; + +layout(location = 0) out vec4 out_color; +layout(location = 0) in vec2 v_uv; + +void main() { + out_color = textureLod(sampler2D(u_texture, u_sampler), v_uv, 0.0); +} diff --git a/src/backends/shaders/box.frag.spv b/src/backends/shaders/box.frag.spv new file mode 100644 index 0000000..6ff6ed4 Binary files /dev/null and b/src/backends/shaders/box.frag.spv differ diff --git a/src/backends/shaders/box_r11f_g11f_b10f.comp.spv b/src/backends/shaders/box_r11f_g11f_b10f.comp.spv new file mode 100644 index 0000000..58ccd8c Binary files /dev/null and b/src/backends/shaders/box_r11f_g11f_b10f.comp.spv differ diff --git a/src/backends/shaders/box_r16_snorm.comp.spv b/src/backends/shaders/box_r16_snorm.comp.spv new file mode 100644 index 0000000..422b315 Binary files /dev/null and b/src/backends/shaders/box_r16_snorm.comp.spv differ diff --git a/src/backends/shaders/box_r16f.comp.spv b/src/backends/shaders/box_r16f.comp.spv new file mode 100644 index 0000000..87ea3bd Binary files /dev/null and b/src/backends/shaders/box_r16f.comp.spv differ diff --git a/src/backends/shaders/box_r32f.comp.spv b/src/backends/shaders/box_r32f.comp.spv new file mode 100644 index 0000000..1feee16 Binary files /dev/null and b/src/backends/shaders/box_r32f.comp.spv differ diff --git a/src/backends/shaders/box_r8.comp.spv b/src/backends/shaders/box_r8.comp.spv new file mode 100644 index 0000000..3e9f581 Binary files /dev/null and b/src/backends/shaders/box_r8.comp.spv differ diff --git a/src/backends/shaders/box_r8_snorm.comp.spv b/src/backends/shaders/box_r8_snorm.comp.spv new file mode 100644 index 0000000..87a8f35 Binary files /dev/null and b/src/backends/shaders/box_r8_snorm.comp.spv differ diff --git a/src/backends/shaders/box_rg16_snorm.comp.spv b/src/backends/shaders/box_rg16_snorm.comp.spv new file mode 100644 index 0000000..b829b0c Binary files /dev/null and b/src/backends/shaders/box_rg16_snorm.comp.spv differ diff --git a/src/backends/shaders/box_rg16f.comp.spv b/src/backends/shaders/box_rg16f.comp.spv new file mode 100644 index 0000000..ab15053 Binary files /dev/null and b/src/backends/shaders/box_rg16f.comp.spv differ diff --git a/src/backends/shaders/box_rg32f.comp.spv b/src/backends/shaders/box_rg32f.comp.spv new file mode 100644 index 0000000..18db250 Binary files /dev/null and b/src/backends/shaders/box_rg32f.comp.spv differ diff --git a/src/backends/shaders/box_rg8.comp.spv b/src/backends/shaders/box_rg8.comp.spv new file mode 100644 index 0000000..96d4a20 Binary files /dev/null and b/src/backends/shaders/box_rg8.comp.spv differ diff --git a/src/backends/shaders/box_rg8_snorm.comp.spv b/src/backends/shaders/box_rg8_snorm.comp.spv new file mode 100644 index 0000000..ce20ad3 Binary files /dev/null and b/src/backends/shaders/box_rg8_snorm.comp.spv differ diff --git a/src/backends/shaders/box_rgb10_a2.comp.spv b/src/backends/shaders/box_rgb10_a2.comp.spv new file mode 100644 index 0000000..193bede Binary files /dev/null and b/src/backends/shaders/box_rgb10_a2.comp.spv differ diff --git a/src/backends/shaders/box_rgba16_snorm.comp.spv b/src/backends/shaders/box_rgba16_snorm.comp.spv new file mode 100644 index 0000000..df893e9 Binary files /dev/null and b/src/backends/shaders/box_rgba16_snorm.comp.spv differ diff --git a/src/backends/shaders/box_rgba16f.comp.spv b/src/backends/shaders/box_rgba16f.comp.spv new file mode 100644 index 0000000..e1e8aef Binary files /dev/null and b/src/backends/shaders/box_rgba16f.comp.spv differ diff --git a/src/backends/shaders/box_rgba32f.comp.spv b/src/backends/shaders/box_rgba32f.comp.spv new file mode 100644 index 0000000..d566a90 Binary files /dev/null and b/src/backends/shaders/box_rgba32f.comp.spv differ diff --git a/src/backends/shaders/box_rgba8.comp.spv b/src/backends/shaders/box_rgba8.comp.spv new file mode 100644 index 0000000..643b6e9 Binary files /dev/null and b/src/backends/shaders/box_rgba8.comp.spv differ diff --git a/src/backends/shaders/box_rgba8_snorm.comp.spv b/src/backends/shaders/box_rgba8_snorm.comp.spv new file mode 100644 index 0000000..514b691 Binary files /dev/null and b/src/backends/shaders/box_rgba8_snorm.comp.spv differ diff --git a/src/backends/shaders/box_srgb.comp b/src/backends/shaders/box_srgb.comp new file mode 100644 index 0000000..e73a89b --- /dev/null +++ b/src/backends/shaders/box_srgb.comp @@ -0,0 +1,38 @@ +#version 450 +layout(local_size_x = 32, local_size_y = 32) in; + +layout(set = 0, binding = 0, rgba8) uniform readonly image2D u_src; +layout(set = 0, binding = 1, rgba8) uniform writeonly image2D u_dst; + +// Clamp to edge +#define L(u) imageLoad(u_src, clamp(u, ivec2(0), ivec2(imageSize(u_src) - 1))) + +// From page 220, Chapter 7.7.7 Conversion Rules for sRGBA and sBGRA Textures +// https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf +vec3 srgb_to_linear(vec3 c) { + return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c)); +} + +vec3 linear_to_srgb(vec3 c) { + c = mix(c, vec3(0.0), isnan(c)); + c = clamp(c, vec3(0.0), vec3(1.0)); + return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, + step(0.0031308, c)); +} + +// In contrast to box_srgb_macos.comp, we need to perform the +// conversion between srgb and linear after load and before +// store. +void main() { + ivec2 dst_uv = ivec2(gl_GlobalInvocationID.xy); + ivec2 src_uv = 2 * dst_uv; + vec4 l = L(src_uv + ivec2(0, 0)); + vec4 r = L(src_uv + ivec2(1, 0)); + vec4 u = L(src_uv + ivec2(0, 1)); + vec4 d = L(src_uv + ivec2(1, 1)); + vec3 c = (srgb_to_linear(l.rgb) + srgb_to_linear(r.rgb) + + srgb_to_linear(u.rgb) + srgb_to_linear(d.rgb)) / + 4.0; + float a = (l.a + r.a + u.a + d.a) / 4.0; + imageStore(u_dst, dst_uv, vec4(linear_to_srgb(c), a)); +} diff --git a/src/backends/shaders/box_srgb.comp.spv b/src/backends/shaders/box_srgb.comp.spv new file mode 100644 index 0000000..1cb2b71 Binary files /dev/null and b/src/backends/shaders/box_srgb.comp.spv differ diff --git a/src/backends/shaders/box_srgb_macos.comp b/src/backends/shaders/box_srgb_macos.comp new file mode 100644 index 0000000..b8e3650 --- /dev/null +++ b/src/backends/shaders/box_srgb_macos.comp @@ -0,0 +1,35 @@ +#version 450 +layout(local_size_x = 32, local_size_y = 32) in; + +layout(set = 0, binding = 0, rgba8) uniform readonly image2D u_src; +layout(set = 0, binding = 1, rgba8) uniform writeonly image2D u_dst; + +// Clamp to edge +#define L(u) imageLoad(u_src, clamp(u, ivec2(0), ivec2(imageSize(u_src) - 1))) + +// From page 220, Chapter 7.7.7 Conversion Rules for sRGBA and sBGRA Textures +// https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf +vec3 srgb_to_linear(vec3 c) { + return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c)); +} + +vec3 linear_to_srgb(vec3 c) { + c = mix(c, vec3(0.0), isnan(c)); + c = clamp(c, vec3(0.0), vec3(1.0)); + return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, + step(0.0031308, c)); +} + +// The behavior I'm observing on MacOS with a GPUFamily2 v1 capable GPU +// is that the implementation performs the conversion from srgb to linear +// on load, but it's up to me to convert back to srgb before calling store +void main() { + ivec2 dst_uv = ivec2(gl_GlobalInvocationID.xy); + ivec2 src_uv = 2 * dst_uv; + vec4 l = L(src_uv + ivec2(0, 0)); + vec4 r = L(src_uv + ivec2(1, 0)); + vec4 u = L(src_uv + ivec2(0, 1)); + vec4 d = L(src_uv + ivec2(1, 1)); + vec4 c = (l + r + u + d) / 4.0; + imageStore(u_dst, dst_uv, vec4(linear_to_srgb(c.rgb), c.a)); +} diff --git a/src/backends/shaders/box_srgb_macos.comp.spv b/src/backends/shaders/box_srgb_macos.comp.spv new file mode 100644 index 0000000..a06bb61 Binary files /dev/null and b/src/backends/shaders/box_srgb_macos.comp.spv differ diff --git a/src/backends/shaders/compile.sh b/src/backends/shaders/compile.sh new file mode 100755 index 0000000..2d33c8c --- /dev/null +++ b/src/backends/shaders/compile.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -e + +function compile { + glslc -c $1 -o $2 + spirv-opt -Os $2 -o $2 +} + + +cd "$(dirname "$0")" +compile triangle.vert triangle.vert.spv +compile box.frag box.frag.spv +compile box_srgb.comp box_srgb.comp.spv +compile box_srgb_macos.comp box_srgb_macos.comp.spv + +# https://www.khronos.org/opengl/wiki/Image_Load_Store#Format_qualifiers +SUPPORTED_FORMATS=( + rgba32f + rgba16f + rg32f + rg16f + r32f + r16f + rgba8 + rg8 + r8 + r11f_g11f_b10f + rgb10_a2 + rgba16_snorm + rgba8_snorm + rg16_snorm + rg8_snorm + r16_snorm + r8_snorm +) +for FORMAT in ${SUPPORTED_FORMATS[@]}; do + (FORMAT=${FORMAT} envsubst < box.comp) > box_${FORMAT}.comp + compile box_${FORMAT}.comp box_${FORMAT}.comp.spv + rm box_${FORMAT}.comp +done diff --git a/src/backends/shaders/triangle.vert b/src/backends/shaders/triangle.vert new file mode 100644 index 0000000..c576c5d --- /dev/null +++ b/src/backends/shaders/triangle.vert @@ -0,0 +1,8 @@ +#version 450 +// https://www.saschawillems.de/blog/2016/08/13/vulkan-tutorial-on-rendering-a-fullscreen-quad-without-buffers/ +layout(location = 0) out vec2 v_uv; +void main() { + v_uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); + gl_Position = vec4(vec2(v_uv.x, v_uv.y) * 2.0 - 1.0, 0.0, 1.0); + v_uv.y = 1.0 - v_uv.y; +} diff --git a/src/backends/shaders/triangle.vert.spv b/src/backends/shaders/triangle.vert.spv new file mode 100644 index 0000000..6dcda65 Binary files /dev/null and b/src/backends/shaders/triangle.vert.spv differ diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..8072897 --- /dev/null +++ b/src/core.rs @@ -0,0 +1,33 @@ +use thiserror::Error; + +/// MipmapGenerator describes types that can generate mipmaps for a texture. +pub trait MipmapGenerator { + /// Encodes commands to generate mipmaps for a texture. + /// + /// Expectations: + /// - `texture_descriptor` should be the same descriptor used to create the `texture`. + fn generate( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + texture: &wgpu::Texture, + texture_descriptor: &wgpu::TextureDescriptor, + ) -> Result<(), Error>; +} + +/// An error that occurred during mipmap generation. +#[derive(Debug, Error, PartialEq, Eq)] +pub enum Error { + #[error("Unsupported texture usage `{0:?}`.\nYour texture usage must contain one of: 1. TextureUsage::STORAGE, 2. TextureUsage::OUTPUT_ATTACHMENT | TextureUsage::SAMPLED, 3. TextureUsage::COPY_SRC | TextureUsage::COPY_DST")] + UnsupportedUsage(wgpu::TextureUsage), + #[error( + "Unsupported texture dimension `{0:?}. You texture dimension must be TextureDimension::D2`" + )] + UnsupportedDimension(wgpu::TextureDimension), + #[error("Unsupported texture format `{0:?}`. Try using the render backend.")] + UnsupportedFormat(wgpu::TextureFormat), + #[error("Unsupported texture size. Texture size must be a power of 2.")] + NpotTexture, + #[error("Unknown texture format `{0:?}`.\nDid you mean to specify it in `MipmapGeneratorDescriptor::formats`?")] + UnknownFormat(wgpu::TextureFormat), +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0455e4a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,56 @@ +/*! +Generate mipmaps for [wgpu](https://github.com/gfx-rs/wgpu-rs) textures. + +## Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +wgpu-mipmap = "0.1" +``` + +Example usage: + +```rust +use wgpu_mipmap::*; +fn example(device: &wgpu::Device, queue: &wgpu::Queue) -> Result<(), Error> { + // create a recommended generator + let generator = RecommendedMipmapGenerator::new(&device); + // create and upload data to a texture + let texture_descriptor = wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width: 512, + height: 512, + depth: 1, + }, + mip_level_count: 10, // 1 + log2(512) + sample_count: 1, + format: wgpu::TextureFormat::Rgba8Unorm, + dimension: wgpu::TextureDimension::D2, + usage: wgpu::TextureUsage::STORAGE, + label: None, + }; + let texture = device.create_texture(&texture_descriptor); + // upload_data_to_texture(&texture); + // create an encoder and generate mipmaps for the texture + let mut encoder = device.create_command_encoder(&Default::default()); + generator.generate(&device, &mut encoder, &texture, &texture_descriptor)?; + queue.submit(std::iter::once(encoder.finish())); + Ok(()) +} +``` +*/ +mod backends; +mod core; + +#[doc(hidden)] +pub mod util; + +#[doc(inline)] +pub use crate::backends::{ + ComputeMipmapGenerator, CopyMipmapGenerator, RecommendedMipmapGenerator, RenderMipmapGenerator, +}; + +#[doc(inline)] +pub use crate::core::*; diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..68f4e61 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,284 @@ +/// utilities used throughout the project. Not part of the official API. +use crate::core::*; + +#[derive(Debug)] +pub struct MipBuffer { + pub buffer: Vec, + pub dimensions: MipBufferDimensions, + pub level: u32, +} + +#[derive(Debug, Copy, Clone)] +pub struct MipBufferDimensions { + pub width: usize, + pub height: usize, + pub bytes_per_channel: usize, + pub unpadded_bytes_per_row: usize, + pub padded_bytes_per_row: usize, +} + +impl MipBufferDimensions { + pub fn new(width: usize, height: usize, bytes_per_channel: usize) -> Self { + let width = width.max(1); + let height = height.max(1); + let unpadded_bytes_per_row = width * bytes_per_channel; + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; + let padded_bytes_per_row_padding = (align - unpadded_bytes_per_row % align) % align; + let padded_bytes_per_row = unpadded_bytes_per_row + padded_bytes_per_row_padding; + Self { + width, + height, + bytes_per_channel, + unpadded_bytes_per_row, + padded_bytes_per_row, + } + } +} + +pub async fn generate_and_copy_to_cpu( + device: &wgpu::Device, + queue: &wgpu::Queue, + generator: &dyn MipmapGenerator, + data: &[u8], + texture_descriptor: &wgpu::TextureDescriptor<'_>, +) -> Result, Error> { + // Create a texture + let buffer_dimensions = MipBufferDimensions::new( + texture_descriptor.size.width as usize, + texture_descriptor.size.height as usize, + format_bytes_per_channel(&texture_descriptor.format), + ); + let texture = device.create_texture(&texture_descriptor); + // Upload `data` to the texture + queue.write_texture( + wgpu::TextureCopyView { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + }, + &data, + wgpu::TextureDataLayout { + offset: 0, + bytes_per_row: buffer_dimensions.unpadded_bytes_per_row as u32, + rows_per_image: 0, + }, + wgpu::Extent3d { + width: buffer_dimensions.width as u32, + height: buffer_dimensions.height as u32, + depth: 1, + }, + ); + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + generator.generate(&device, &mut encoder, &texture, &texture_descriptor)?; + // Copy all mipmap levels, including the base, to GPU buffers + let buffers = { + let mut buffers = Vec::new(); + for i in 0..texture_descriptor.mip_level_count { + let mip_width = buffer_dimensions.width / 2usize.pow(i); + let mip_height = buffer_dimensions.height / 2usize.pow(i); + let mip_dimensions = MipBufferDimensions::new( + mip_width, + mip_height, + buffer_dimensions.bytes_per_channel, + ); + let size = (mip_dimensions.height * mip_dimensions.padded_bytes_per_row) as u64; + let mip_texture_extent = wgpu::Extent3d { + width: mip_width as u32, + height: mip_height as u32, + depth: 1, + }; + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size, + usage: wgpu::BufferUsage::COPY_DST | wgpu::BufferUsage::MAP_READ, + mapped_at_creation: false, + }); + encoder.copy_texture_to_buffer( + wgpu::TextureCopyView { + texture: &texture, + mip_level: i, + origin: wgpu::Origin3d::ZERO, + }, + wgpu::BufferCopyView { + buffer: &buffer, + layout: wgpu::TextureDataLayout { + offset: 0, + bytes_per_row: mip_dimensions.padded_bytes_per_row as u32, + rows_per_image: 0, + }, + }, + mip_texture_extent, + ); + buffers.push((buffer, mip_dimensions)); + } + buffers + }; + queue.submit(std::iter::once(encoder.finish())); + // Copy the GPU buffers to the CPU + let mut mip_buffers: Vec = Vec::new(); + for (level, (buffer, buffer_dimensions)) in buffers.iter().enumerate() { + // Note that we're not calling `.await` here. + let buffer_slice = buffer.slice(..); + let buffer_future = buffer_slice.map_async(wgpu::MapMode::Read); + // Poll the device in a blocking manner so that our future resolves. + // In an actual application, `device.poll(...)` should + // be called in an event loop or on another thread. + device.poll(wgpu::Maintain::Wait); + match buffer_future.await { + Err(e) => panic!("Unexpected failure: {}", e), + Ok(()) => { + let padded_buffer = buffer_slice.get_mapped_range(); + // The buffer we get back is padded, so only extract what we need + let mut exact_buffer = Vec::with_capacity( + buffer_dimensions.unpadded_bytes_per_row * buffer_dimensions.height, + ); + for y in 0..buffer_dimensions.height { + let row_beg = y * buffer_dimensions.padded_bytes_per_row; + let row_end = row_beg + buffer_dimensions.unpadded_bytes_per_row; + exact_buffer.extend_from_slice(&padded_buffer[row_beg..row_end]); + } + mip_buffers.push(MipBuffer { + buffer: exact_buffer, + dimensions: *buffer_dimensions, + level: level as u32, + }); + } + } + } + Ok(mip_buffers) +} + +pub fn checkerboard_r8(width: u32, height: u32, n: u32) -> Vec { + use std::iter; + + (0..width * height) + .flat_map(|id| { + let x = id % width; + let y = id / height; + let v = (((x / n + y / n) % 2) * 255) as u8; + iter::once(v) + }) + .collect() +} + +#[doc(hidden)] +pub fn checkerboard_rgba8(width: u32, height: u32, n: u32) -> Vec { + use std::iter; + + (0..width * height) + .flat_map(|id| { + let x = id % width; + let y = id / height; + let v = (((x / n + y / n) % 2) * 255) as u8; + iter::once(v) + .chain(iter::once(v)) + .chain(iter::once(v)) + .chain(iter::once(255)) + }) + .collect() +} + +#[doc(hidden)] +pub fn checkerboard_rgba32f(width: u32, height: u32, n: u32) -> Vec { + use std::iter; + + (0..width * height) + .flat_map(|id| { + let x = id % width; + let y = id / height; + let v = ((x / n + y / n) % 2) as f32; + iter::once(v) + .chain(iter::once(v)) + .chain(iter::once(v)) + .chain(iter::once(1.0)) + }) + .collect() +} + +fn format_bytes_per_channel(format: &wgpu::TextureFormat) -> usize { + use wgpu::TextureFormat; + match format { + // 8 bit per channel + TextureFormat::R8Unorm => 1, + TextureFormat::R8Snorm => 1, + TextureFormat::R8Uint => 1, + TextureFormat::R8Sint => 1, + // 16 bit per channel + TextureFormat::R16Uint => 2, + TextureFormat::R16Sint => 2, + TextureFormat::R16Float => 2, + TextureFormat::Rg8Unorm => 2, + TextureFormat::Rg8Snorm => 2, + TextureFormat::Rg8Uint => 2, + TextureFormat::Rg8Sint => 2, + // 32 bit per channel + TextureFormat::R32Uint => 4, + TextureFormat::R32Sint => 4, + TextureFormat::R32Float => 4, + TextureFormat::Rg16Uint => 4, + TextureFormat::Rg16Sint => 4, + TextureFormat::Rg16Float => 4, + TextureFormat::Rgba8Unorm => 4, + TextureFormat::Rgba8Snorm => 4, + TextureFormat::Rgba8Uint => 4, + TextureFormat::Rgba8Sint => 4, + TextureFormat::Bgra8Unorm => 4, + TextureFormat::Bgra8UnormSrgb => 4, + TextureFormat::Rgba8UnormSrgb => 4, + // packed 32 bit per channel + TextureFormat::Rgb10a2Unorm => 4, + TextureFormat::Rg11b10Float => 4, + // 64 bit per channel + TextureFormat::Rg32Uint => 8, + TextureFormat::Rg32Sint => 8, + TextureFormat::Rg32Float => 8, + TextureFormat::Rgba16Uint => 8, + TextureFormat::Rgba16Sint => 8, + TextureFormat::Rgba16Float => 8, + // 128 bit per channel + TextureFormat::Rgba32Uint => 16, + TextureFormat::Rgba32Sint => 16, + TextureFormat::Rgba32Float => 16, + _ => unimplemented!(), + } +} + +#[doc(hidden)] +#[allow(dead_code)] +pub(crate) async fn wgpu_setup() -> (wgpu::Instance, wgpu::Adapter, wgpu::Device, wgpu::Queue) { + let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY); + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: None, + }) + .await + .expect("Failed to find an appropiate adapter"); + // Create the logical device and command queue + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + features: wgpu::Features::empty(), + limits: wgpu::Limits::default(), + shader_validation: true, + }, + None, + ) + .await + .expect("Failed to create device"); + (instance, adapter, device, queue) +} + +#[doc(hidden)] +#[allow(dead_code)] +pub(crate) fn get_mip_extent(extent: &wgpu::Extent3d, level: u32) -> wgpu::Extent3d { + let mip_width = ((extent.width as f32) / (2u32.pow(level) as f32)).floor() as u32; + let mip_height = ((extent.height as f32) / (2u32.pow(level) as f32)).floor() as u32; + let mip_depth = ((extent.depth as f32) / (2u32.pow(level) as f32)).floor() as u32; + wgpu::Extent3d { + width: mip_width.max(1), + height: mip_height.max(1), + depth: mip_depth.max(1), + } +}