├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── compute_shader.wgsl ├── demo.rs └── shader.wgsl ├── src ├── chrometrace.rs ├── errors.rs ├── lib.rs ├── profiler.rs ├── profiler_command_recorder.rs ├── profiler_query.rs ├── profiler_settings.rs ├── puffin.rs ├── scope.rs └── tracy.rs └── tests ├── src ├── dropped_frame_handling.rs ├── errors.rs ├── interleaved_command_buffer.rs ├── mod.rs ├── multiple_resolves_per_frame.rs └── nested_scopes.rs └── tests.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | check: 14 | name: Rust Format and Clippy 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: dtolnay/rust-toolchain@stable 19 | with: 20 | target: wasm32-unknown-unknown 21 | - run: cargo fmt -- --check 22 | 23 | - run: cargo clippy --locked --all-targets -- -D warnings 24 | # profiling & tracy features can't be both enabled at the same time in the demo. This is a limitation of the `profiling` crate. 25 | - run: cargo clippy --locked --all-features -- -D warnings 26 | - run: cargo clippy --locked --all-targets --features puffin -- -D warnings 27 | - run: cargo clippy --locked --all-targets --features tracy -- -D warnings 28 | 29 | - run: cargo check --locked --target wasm32-unknown-unknown 30 | 31 | - run: cargo doc --no-deps 32 | env: 33 | RUSTDOCFLAGS: '-D warnings' 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | trace.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.23.0 4 | * Update to wgpu 24.0.0, by @waywardmonkeys in [#95](https://github.com/Wumpf/wgpu-profiler/pull/95) 5 | 6 | ## 0.22.0 7 | * Device no longer needs to be passed for scope/start-query/end-query, in [#93](https://github.com/Wumpf/wgpu-profiler/pull/93) 8 | 9 | ## 0.21.1 10 | * Add accessor for settings. 11 | 12 | ## 0.21.0 13 | * Integration with puffin, by @gonkalbell in [#81](https://github.com/Wumpf/wgpu-profiler/pull/81) 14 | 15 | ## 0.20.0 16 | * Update to tracy-client 0.18.0, update to thiserror 2, in [#91](https://github.com/Wumpf/wgpu-profiler/pull/91) 17 | * Update to wgpu 24.0.0, by @songhuaixu in [#90](https://github.com/Wumpf/wgpu-profiler/pull/90) 18 | 19 | ## 0.19.0 20 | * Update to wgpu 23.0.0, by @waywardmonkeys in [#88](https://github.com/Wumpf/wgpu-profiler/pull/88) 21 | 22 | ## 0.18.2 23 | * Fix `resolve_queries` calls per frame causing invalid buffer copy operations, in [#83](https://github.com/Wumpf/wgpu-profiler/pull/83) 24 | 25 | ## 0.18.1 26 | * Fix issues with multiple calls to `GpuProfiler::resolve_queries` per frame, in [#79](https://github.com/Wumpf/wgpu-profiler/pull/79) 27 | 28 | ## 0.18.0 29 | * Update to wgpu 22.1.0, by @waywardmonkeys in [#75](https://github.com/Wumpf/wgpu-profiler/pull/75) 30 | 31 | ## 0.17.0 32 | * Update to wgpu 0.20 33 | * `GpuTimerQueryResult` are now also produced when timing is disabled for that scope 34 | * `GpuTimerQueryResult::time` is an `Option` now 35 | * Update tracy client to 0.17.0 36 | 37 | ## 0.16.2 38 | 39 | * Updating to wgpu 0.19.3 thus removing the need for pinned web-sys, by @xStrom in [#65](https://github.com/Wumpf/wgpu-profiler/pull/65) 40 | 41 | ## 0.16.1 42 | 43 | * Fix building for wasm, by @davidster in [#62](https://github.com/Wumpf/wgpu-profiler/pull/62) 44 | 45 | ## 0.16 46 | 47 | * update to wgpu 0.19 48 | * ⚠️ Includes many major breaking changes! ⚠️ 49 | * `GpuProfiler` can now be used with several command buffers interleaved or in parallel! 50 | * `Scope`/`OwningScope`/`ManualScope`/ are now all top-level in the `gpu_profiler` module. `GpuProfiler` has utilities to create them directly. 51 | * `GpuProfiler::begin_query` returns a query and `GpuProfiler::end_query` consumes it again 52 | * nesting of profiling scopes is no longer done automatically: To manually associate a `GpuProfilerQuery` with a parent, use `GpuProfilerQuery::with_parent` 53 | * removed profiling macro (doesn't work well with the new nesting model) 54 | * `GpuProfiler` can now directly create scope structs using `GpuProfiler::scope`/`owning_scope` 55 | 56 | ## 0.15 57 | 58 | * update to wgpu 0.18, by @Zoxc in [#50](https://github.com/Wumpf/wgpu-profiler/pull/50) 59 | * sample & doc fixes, by @waywardmonkeys in [#41](https://github.com/Wumpf/wgpu-profiler/pull/41), [#44](https://github.com/Wumpf/wgpu-profiler/pull/44) 60 | * various methods return `thiserror` errors instead of internal unwrap/except on user errors, by @Wumpf in [#45](https://github.com/Wumpf/wgpu-profiler/pull/45) and following PRs 61 | * overhauled `GpuProfiler` creation & configuration: 62 | * takes settings object that can be changed after the fact (allows disabling on the fly!) 63 | * adapter/queue/device no longer needed on creation unless tracy client is required. 64 | * separate creation method for tracy support 65 | 66 | ## 0.14.2 67 | 68 | * Fix pointing to wrong tracy version, by @waywardmonkeys in [#36](https://github.com/Wumpf/wgpu-profiler/pull/35) 69 | * Doc fixes, by @waywardmonkeys in [#38](https://github.com/Wumpf/wgpu-profiler/pull/35) 70 | 71 | ## 0.14.1 72 | 73 | * Tracy integration, by @cwfitzgerald in [#35](https://github.com/Wumpf/wgpu-profiler/pull/35) 74 | 75 | ## 0.13.0 76 | 77 | * Upgrade to wgpu 0.17, by @waywardmonkeys in [#31](https://github.com/Wumpf/wgpu-profiler/pull/31) 78 | 79 | ## 0.12.1 80 | 81 | * Fix wgpu validation error due to mapping of query resolve buffer, by @Davidster [#28](https://github.com/Wumpf/wgpu-profiler/pull/28) 82 | 83 | ## 0.12.0 84 | 85 | * Upgrade to wgpu 0.16, by @davidster in [#26](https://github.com/Wumpf/wgpu-profiler/pull/26) 86 | 87 | ## 0.11.0 88 | 89 | * Upgrade to wgpu 0.15 90 | 91 | ## 0.10.0 92 | 93 | * Upgrade to wgpu 0.14 and switch to rust 2021 edition, by @Imberflur in [#23](https://github.com/Wumpf/wgpu-profiler/pull/23) 94 | 95 | ## 0.9.1 96 | 97 | * Better docs [#21](https://github.com/Wumpf/wgpu-profiler/pull/21) 98 | * Fix crash on dropped frame [#20](https://github.com/Wumpf/wgpu-profiler/pull/20), reported by @JCapucho in [#19](https://github.com/Wumpf/wgpu-profiler/pull/19) 99 | * Fix enable_pass_timer/enable_encoder_timer checking wrong features 100 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ab_glyph" 7 | version = "0.2.29" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" 10 | dependencies = [ 11 | "ab_glyph_rasterizer", 12 | "owned_ttf_parser", 13 | ] 14 | 15 | [[package]] 16 | name = "ab_glyph_rasterizer" 17 | version = "0.1.8" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" 20 | 21 | [[package]] 22 | name = "ahash" 23 | version = "0.8.11" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 26 | dependencies = [ 27 | "cfg-if", 28 | "getrandom", 29 | "once_cell", 30 | "version_check", 31 | "zerocopy", 32 | ] 33 | 34 | [[package]] 35 | name = "aho-corasick" 36 | version = "1.1.3" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 39 | dependencies = [ 40 | "memchr", 41 | ] 42 | 43 | [[package]] 44 | name = "android-activity" 45 | version = "0.6.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" 48 | dependencies = [ 49 | "android-properties", 50 | "bitflags 2.9.0", 51 | "cc", 52 | "cesu8", 53 | "jni", 54 | "jni-sys", 55 | "libc", 56 | "log", 57 | "ndk", 58 | "ndk-context", 59 | "ndk-sys 0.6.0+11769913", 60 | "num_enum", 61 | "thiserror 1.0.69", 62 | ] 63 | 64 | [[package]] 65 | name = "android-properties" 66 | version = "0.2.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" 69 | 70 | [[package]] 71 | name = "android_system_properties" 72 | version = "0.1.5" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 75 | dependencies = [ 76 | "libc", 77 | ] 78 | 79 | [[package]] 80 | name = "anyhow" 81 | version = "1.0.87" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" 84 | 85 | [[package]] 86 | name = "arrayref" 87 | version = "0.3.9" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" 90 | 91 | [[package]] 92 | name = "arrayvec" 93 | version = "0.7.6" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 96 | 97 | [[package]] 98 | name = "as-raw-xcb-connection" 99 | version = "1.0.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" 102 | 103 | [[package]] 104 | name = "ash" 105 | version = "0.38.0+1.3.281" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" 108 | dependencies = [ 109 | "libloading", 110 | ] 111 | 112 | [[package]] 113 | name = "atomic-waker" 114 | version = "1.1.2" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 117 | 118 | [[package]] 119 | name = "autocfg" 120 | version = "1.4.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 123 | 124 | [[package]] 125 | name = "bincode" 126 | version = "1.3.3" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 129 | dependencies = [ 130 | "serde", 131 | ] 132 | 133 | [[package]] 134 | name = "bit-set" 135 | version = "0.8.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" 138 | dependencies = [ 139 | "bit-vec", 140 | ] 141 | 142 | [[package]] 143 | name = "bit-vec" 144 | version = "0.8.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 147 | 148 | [[package]] 149 | name = "bitflags" 150 | version = "1.3.2" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 153 | 154 | [[package]] 155 | name = "bitflags" 156 | version = "2.9.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 159 | dependencies = [ 160 | "serde", 161 | ] 162 | 163 | [[package]] 164 | name = "block" 165 | version = "0.1.6" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 168 | 169 | [[package]] 170 | name = "block2" 171 | version = "0.5.1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" 174 | dependencies = [ 175 | "objc2", 176 | ] 177 | 178 | [[package]] 179 | name = "bumpalo" 180 | version = "3.16.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 183 | 184 | [[package]] 185 | name = "bytemuck" 186 | version = "1.22.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" 189 | dependencies = [ 190 | "bytemuck_derive", 191 | ] 192 | 193 | [[package]] 194 | name = "bytemuck_derive" 195 | version = "1.8.1" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" 198 | dependencies = [ 199 | "proc-macro2", 200 | "quote", 201 | "syn", 202 | ] 203 | 204 | [[package]] 205 | name = "byteorder" 206 | version = "1.5.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 209 | 210 | [[package]] 211 | name = "bytes" 212 | version = "1.9.0" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 215 | 216 | [[package]] 217 | name = "calloop" 218 | version = "0.13.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" 221 | dependencies = [ 222 | "bitflags 2.9.0", 223 | "log", 224 | "polling", 225 | "rustix", 226 | "slab", 227 | "thiserror 1.0.69", 228 | ] 229 | 230 | [[package]] 231 | name = "calloop-wayland-source" 232 | version = "0.3.0" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" 235 | dependencies = [ 236 | "calloop", 237 | "rustix", 238 | "wayland-backend", 239 | "wayland-client", 240 | ] 241 | 242 | [[package]] 243 | name = "cc" 244 | version = "1.2.10" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" 247 | dependencies = [ 248 | "jobserver", 249 | "libc", 250 | "shlex", 251 | ] 252 | 253 | [[package]] 254 | name = "cesu8" 255 | version = "1.1.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 258 | 259 | [[package]] 260 | name = "cfg-if" 261 | version = "1.0.0" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 264 | 265 | [[package]] 266 | name = "cfg_aliases" 267 | version = "0.2.1" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 270 | 271 | [[package]] 272 | name = "codespan-reporting" 273 | version = "0.12.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" 276 | dependencies = [ 277 | "serde", 278 | "termcolor", 279 | "unicode-width", 280 | ] 281 | 282 | [[package]] 283 | name = "combine" 284 | version = "4.6.7" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 287 | dependencies = [ 288 | "bytes", 289 | "memchr", 290 | ] 291 | 292 | [[package]] 293 | name = "concurrent-queue" 294 | version = "2.5.0" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 297 | dependencies = [ 298 | "crossbeam-utils", 299 | ] 300 | 301 | [[package]] 302 | name = "core-foundation" 303 | version = "0.9.4" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 306 | dependencies = [ 307 | "core-foundation-sys", 308 | "libc", 309 | ] 310 | 311 | [[package]] 312 | name = "core-foundation-sys" 313 | version = "0.8.7" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 316 | 317 | [[package]] 318 | name = "core-graphics" 319 | version = "0.23.2" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" 322 | dependencies = [ 323 | "bitflags 1.3.2", 324 | "core-foundation", 325 | "core-graphics-types", 326 | "foreign-types", 327 | "libc", 328 | ] 329 | 330 | [[package]] 331 | name = "core-graphics-types" 332 | version = "0.1.3" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" 335 | dependencies = [ 336 | "bitflags 1.3.2", 337 | "core-foundation", 338 | "libc", 339 | ] 340 | 341 | [[package]] 342 | name = "crossbeam-channel" 343 | version = "0.5.13" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" 346 | dependencies = [ 347 | "crossbeam-utils", 348 | ] 349 | 350 | [[package]] 351 | name = "crossbeam-utils" 352 | version = "0.8.21" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 355 | 356 | [[package]] 357 | name = "crunchy" 358 | version = "0.2.3" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" 361 | 362 | [[package]] 363 | name = "cursor-icon" 364 | version = "1.1.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" 367 | 368 | [[package]] 369 | name = "dispatch" 370 | version = "0.2.0" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" 373 | 374 | [[package]] 375 | name = "dlib" 376 | version = "0.5.2" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" 379 | dependencies = [ 380 | "libloading", 381 | ] 382 | 383 | [[package]] 384 | name = "document-features" 385 | version = "0.2.11" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 388 | dependencies = [ 389 | "litrs", 390 | ] 391 | 392 | [[package]] 393 | name = "downcast-rs" 394 | version = "1.2.1" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 397 | 398 | [[package]] 399 | name = "dpi" 400 | version = "0.1.1" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" 403 | 404 | [[package]] 405 | name = "either" 406 | version = "1.13.0" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 409 | 410 | [[package]] 411 | name = "equivalent" 412 | version = "1.0.1" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 415 | 416 | [[package]] 417 | name = "errno" 418 | version = "0.3.10" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 421 | dependencies = [ 422 | "libc", 423 | "windows-sys 0.59.0", 424 | ] 425 | 426 | [[package]] 427 | name = "fastrand" 428 | version = "2.3.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 431 | 432 | [[package]] 433 | name = "foldhash" 434 | version = "0.1.4" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" 437 | 438 | [[package]] 439 | name = "foreign-types" 440 | version = "0.5.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" 443 | dependencies = [ 444 | "foreign-types-macros", 445 | "foreign-types-shared", 446 | ] 447 | 448 | [[package]] 449 | name = "foreign-types-macros" 450 | version = "0.2.3" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" 453 | dependencies = [ 454 | "proc-macro2", 455 | "quote", 456 | "syn", 457 | ] 458 | 459 | [[package]] 460 | name = "foreign-types-shared" 461 | version = "0.3.1" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" 464 | 465 | [[package]] 466 | name = "futures-core" 467 | version = "0.3.31" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 470 | 471 | [[package]] 472 | name = "futures-io" 473 | version = "0.3.31" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 476 | 477 | [[package]] 478 | name = "futures-lite" 479 | version = "2.6.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" 482 | dependencies = [ 483 | "fastrand", 484 | "futures-core", 485 | "futures-io", 486 | "parking", 487 | "pin-project-lite", 488 | ] 489 | 490 | [[package]] 491 | name = "generator" 492 | version = "0.8.4" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" 495 | dependencies = [ 496 | "cfg-if", 497 | "libc", 498 | "log", 499 | "rustversion", 500 | "windows", 501 | ] 502 | 503 | [[package]] 504 | name = "gethostname" 505 | version = "0.4.3" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" 508 | dependencies = [ 509 | "libc", 510 | "windows-targets 0.48.5", 511 | ] 512 | 513 | [[package]] 514 | name = "getrandom" 515 | version = "0.2.15" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 518 | dependencies = [ 519 | "cfg-if", 520 | "libc", 521 | "wasi", 522 | ] 523 | 524 | [[package]] 525 | name = "gl_generator" 526 | version = "0.14.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" 529 | dependencies = [ 530 | "khronos_api", 531 | "log", 532 | "xml-rs", 533 | ] 534 | 535 | [[package]] 536 | name = "glow" 537 | version = "0.16.0" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" 540 | dependencies = [ 541 | "js-sys", 542 | "slotmap", 543 | "wasm-bindgen", 544 | "web-sys", 545 | ] 546 | 547 | [[package]] 548 | name = "glutin_wgl_sys" 549 | version = "0.6.1" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" 552 | dependencies = [ 553 | "gl_generator", 554 | ] 555 | 556 | [[package]] 557 | name = "gpu-alloc" 558 | version = "0.6.0" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" 561 | dependencies = [ 562 | "bitflags 2.9.0", 563 | "gpu-alloc-types", 564 | ] 565 | 566 | [[package]] 567 | name = "gpu-alloc-types" 568 | version = "0.3.0" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" 571 | dependencies = [ 572 | "bitflags 2.9.0", 573 | ] 574 | 575 | [[package]] 576 | name = "gpu-allocator" 577 | version = "0.27.0" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" 580 | dependencies = [ 581 | "log", 582 | "presser", 583 | "thiserror 1.0.69", 584 | "windows", 585 | ] 586 | 587 | [[package]] 588 | name = "gpu-descriptor" 589 | version = "0.3.1" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" 592 | dependencies = [ 593 | "bitflags 2.9.0", 594 | "gpu-descriptor-types", 595 | "hashbrown", 596 | ] 597 | 598 | [[package]] 599 | name = "gpu-descriptor-types" 600 | version = "0.2.0" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" 603 | dependencies = [ 604 | "bitflags 2.9.0", 605 | ] 606 | 607 | [[package]] 608 | name = "half" 609 | version = "2.6.0" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 612 | dependencies = [ 613 | "cfg-if", 614 | "crunchy", 615 | "num-traits", 616 | ] 617 | 618 | [[package]] 619 | name = "hashbrown" 620 | version = "0.15.2" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 623 | dependencies = [ 624 | "foldhash", 625 | ] 626 | 627 | [[package]] 628 | name = "heck" 629 | version = "0.5.0" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 632 | 633 | [[package]] 634 | name = "hermit-abi" 635 | version = "0.4.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 638 | 639 | [[package]] 640 | name = "hexf-parse" 641 | version = "0.2.1" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" 644 | 645 | [[package]] 646 | name = "indexmap" 647 | version = "2.9.0" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 650 | dependencies = [ 651 | "equivalent", 652 | "hashbrown", 653 | ] 654 | 655 | [[package]] 656 | name = "itertools" 657 | version = "0.10.5" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 660 | dependencies = [ 661 | "either", 662 | ] 663 | 664 | [[package]] 665 | name = "jni" 666 | version = "0.21.1" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 669 | dependencies = [ 670 | "cesu8", 671 | "cfg-if", 672 | "combine", 673 | "jni-sys", 674 | "log", 675 | "thiserror 1.0.69", 676 | "walkdir", 677 | "windows-sys 0.45.0", 678 | ] 679 | 680 | [[package]] 681 | name = "jni-sys" 682 | version = "0.3.0" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 685 | 686 | [[package]] 687 | name = "jobserver" 688 | version = "0.1.32" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" 691 | dependencies = [ 692 | "libc", 693 | ] 694 | 695 | [[package]] 696 | name = "js-sys" 697 | version = "0.3.77" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 700 | dependencies = [ 701 | "once_cell", 702 | "wasm-bindgen", 703 | ] 704 | 705 | [[package]] 706 | name = "khronos-egl" 707 | version = "6.0.0" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" 710 | dependencies = [ 711 | "libc", 712 | "libloading", 713 | "pkg-config", 714 | ] 715 | 716 | [[package]] 717 | name = "khronos_api" 718 | version = "3.1.0" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" 721 | 722 | [[package]] 723 | name = "lazy_static" 724 | version = "1.5.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 727 | 728 | [[package]] 729 | name = "libc" 730 | version = "0.2.169" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 733 | 734 | [[package]] 735 | name = "libloading" 736 | version = "0.8.6" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" 739 | dependencies = [ 740 | "cfg-if", 741 | "windows-targets 0.52.6", 742 | ] 743 | 744 | [[package]] 745 | name = "libm" 746 | version = "0.2.11" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" 749 | 750 | [[package]] 751 | name = "libredox" 752 | version = "0.1.3" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 755 | dependencies = [ 756 | "bitflags 2.9.0", 757 | "libc", 758 | "redox_syscall 0.5.8", 759 | ] 760 | 761 | [[package]] 762 | name = "linux-raw-sys" 763 | version = "0.4.15" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 766 | 767 | [[package]] 768 | name = "litrs" 769 | version = "0.4.1" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 772 | 773 | [[package]] 774 | name = "lock_api" 775 | version = "0.4.12" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 778 | dependencies = [ 779 | "autocfg", 780 | "scopeguard", 781 | ] 782 | 783 | [[package]] 784 | name = "log" 785 | version = "0.4.25" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 788 | 789 | [[package]] 790 | name = "loom" 791 | version = "0.7.2" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" 794 | dependencies = [ 795 | "cfg-if", 796 | "generator", 797 | "scoped-tls", 798 | "tracing", 799 | "tracing-subscriber", 800 | ] 801 | 802 | [[package]] 803 | name = "lz4_flex" 804 | version = "0.11.3" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" 807 | 808 | [[package]] 809 | name = "malloc_buf" 810 | version = "0.0.6" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 813 | dependencies = [ 814 | "libc", 815 | ] 816 | 817 | [[package]] 818 | name = "matchers" 819 | version = "0.1.0" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 822 | dependencies = [ 823 | "regex-automata 0.1.10", 824 | ] 825 | 826 | [[package]] 827 | name = "memchr" 828 | version = "2.7.4" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 831 | 832 | [[package]] 833 | name = "memmap2" 834 | version = "0.9.5" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" 837 | dependencies = [ 838 | "libc", 839 | ] 840 | 841 | [[package]] 842 | name = "metal" 843 | version = "0.31.0" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" 846 | dependencies = [ 847 | "bitflags 2.9.0", 848 | "block", 849 | "core-graphics-types", 850 | "foreign-types", 851 | "log", 852 | "objc", 853 | "paste", 854 | ] 855 | 856 | [[package]] 857 | name = "naga" 858 | version = "25.0.1" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" 861 | dependencies = [ 862 | "arrayvec", 863 | "bit-set", 864 | "bitflags 2.9.0", 865 | "cfg_aliases", 866 | "codespan-reporting", 867 | "half", 868 | "hashbrown", 869 | "hexf-parse", 870 | "indexmap", 871 | "log", 872 | "num-traits", 873 | "once_cell", 874 | "rustc-hash", 875 | "spirv", 876 | "strum", 877 | "thiserror 2.0.11", 878 | "unicode-ident", 879 | ] 880 | 881 | [[package]] 882 | name = "ndk" 883 | version = "0.9.0" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" 886 | dependencies = [ 887 | "bitflags 2.9.0", 888 | "jni-sys", 889 | "log", 890 | "ndk-sys 0.6.0+11769913", 891 | "num_enum", 892 | "raw-window-handle", 893 | "thiserror 1.0.69", 894 | ] 895 | 896 | [[package]] 897 | name = "ndk-context" 898 | version = "0.1.1" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 901 | 902 | [[package]] 903 | name = "ndk-sys" 904 | version = "0.5.0+25.2.9519653" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" 907 | dependencies = [ 908 | "jni-sys", 909 | ] 910 | 911 | [[package]] 912 | name = "ndk-sys" 913 | version = "0.6.0+11769913" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" 916 | dependencies = [ 917 | "jni-sys", 918 | ] 919 | 920 | [[package]] 921 | name = "nu-ansi-term" 922 | version = "0.46.0" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 925 | dependencies = [ 926 | "overload", 927 | "winapi", 928 | ] 929 | 930 | [[package]] 931 | name = "num-traits" 932 | version = "0.2.19" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 935 | dependencies = [ 936 | "autocfg", 937 | "libm", 938 | ] 939 | 940 | [[package]] 941 | name = "num_enum" 942 | version = "0.7.3" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" 945 | dependencies = [ 946 | "num_enum_derive", 947 | ] 948 | 949 | [[package]] 950 | name = "num_enum_derive" 951 | version = "0.7.3" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" 954 | dependencies = [ 955 | "proc-macro-crate", 956 | "proc-macro2", 957 | "quote", 958 | "syn", 959 | ] 960 | 961 | [[package]] 962 | name = "objc" 963 | version = "0.2.7" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 966 | dependencies = [ 967 | "malloc_buf", 968 | ] 969 | 970 | [[package]] 971 | name = "objc-sys" 972 | version = "0.3.5" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" 975 | 976 | [[package]] 977 | name = "objc2" 978 | version = "0.5.2" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" 981 | dependencies = [ 982 | "objc-sys", 983 | "objc2-encode", 984 | ] 985 | 986 | [[package]] 987 | name = "objc2-app-kit" 988 | version = "0.2.2" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" 991 | dependencies = [ 992 | "bitflags 2.9.0", 993 | "block2", 994 | "libc", 995 | "objc2", 996 | "objc2-core-data", 997 | "objc2-core-image", 998 | "objc2-foundation", 999 | "objc2-quartz-core", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "objc2-cloud-kit" 1004 | version = "0.2.2" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" 1007 | dependencies = [ 1008 | "bitflags 2.9.0", 1009 | "block2", 1010 | "objc2", 1011 | "objc2-core-location", 1012 | "objc2-foundation", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "objc2-contacts" 1017 | version = "0.2.2" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" 1020 | dependencies = [ 1021 | "block2", 1022 | "objc2", 1023 | "objc2-foundation", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "objc2-core-data" 1028 | version = "0.2.2" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" 1031 | dependencies = [ 1032 | "bitflags 2.9.0", 1033 | "block2", 1034 | "objc2", 1035 | "objc2-foundation", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "objc2-core-image" 1040 | version = "0.2.2" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" 1043 | dependencies = [ 1044 | "block2", 1045 | "objc2", 1046 | "objc2-foundation", 1047 | "objc2-metal", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "objc2-core-location" 1052 | version = "0.2.2" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" 1055 | dependencies = [ 1056 | "block2", 1057 | "objc2", 1058 | "objc2-contacts", 1059 | "objc2-foundation", 1060 | ] 1061 | 1062 | [[package]] 1063 | name = "objc2-encode" 1064 | version = "4.0.3" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" 1067 | 1068 | [[package]] 1069 | name = "objc2-foundation" 1070 | version = "0.2.2" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" 1073 | dependencies = [ 1074 | "bitflags 2.9.0", 1075 | "block2", 1076 | "dispatch", 1077 | "libc", 1078 | "objc2", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "objc2-link-presentation" 1083 | version = "0.2.2" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" 1086 | dependencies = [ 1087 | "block2", 1088 | "objc2", 1089 | "objc2-app-kit", 1090 | "objc2-foundation", 1091 | ] 1092 | 1093 | [[package]] 1094 | name = "objc2-metal" 1095 | version = "0.2.2" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" 1098 | dependencies = [ 1099 | "bitflags 2.9.0", 1100 | "block2", 1101 | "objc2", 1102 | "objc2-foundation", 1103 | ] 1104 | 1105 | [[package]] 1106 | name = "objc2-quartz-core" 1107 | version = "0.2.2" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" 1110 | dependencies = [ 1111 | "bitflags 2.9.0", 1112 | "block2", 1113 | "objc2", 1114 | "objc2-foundation", 1115 | "objc2-metal", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "objc2-symbols" 1120 | version = "0.2.2" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" 1123 | dependencies = [ 1124 | "objc2", 1125 | "objc2-foundation", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "objc2-ui-kit" 1130 | version = "0.2.2" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" 1133 | dependencies = [ 1134 | "bitflags 2.9.0", 1135 | "block2", 1136 | "objc2", 1137 | "objc2-cloud-kit", 1138 | "objc2-core-data", 1139 | "objc2-core-image", 1140 | "objc2-core-location", 1141 | "objc2-foundation", 1142 | "objc2-link-presentation", 1143 | "objc2-quartz-core", 1144 | "objc2-symbols", 1145 | "objc2-uniform-type-identifiers", 1146 | "objc2-user-notifications", 1147 | ] 1148 | 1149 | [[package]] 1150 | name = "objc2-uniform-type-identifiers" 1151 | version = "0.2.2" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" 1154 | dependencies = [ 1155 | "block2", 1156 | "objc2", 1157 | "objc2-foundation", 1158 | ] 1159 | 1160 | [[package]] 1161 | name = "objc2-user-notifications" 1162 | version = "0.2.2" 1163 | source = "registry+https://github.com/rust-lang/crates.io-index" 1164 | checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" 1165 | dependencies = [ 1166 | "bitflags 2.9.0", 1167 | "block2", 1168 | "objc2", 1169 | "objc2-core-location", 1170 | "objc2-foundation", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "once_cell" 1175 | version = "1.21.3" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1178 | 1179 | [[package]] 1180 | name = "orbclient" 1181 | version = "0.3.48" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" 1184 | dependencies = [ 1185 | "libredox", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "ordered-float" 1190 | version = "4.6.0" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" 1193 | dependencies = [ 1194 | "num-traits", 1195 | ] 1196 | 1197 | [[package]] 1198 | name = "overload" 1199 | version = "0.1.1" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1202 | 1203 | [[package]] 1204 | name = "owned_ttf_parser" 1205 | version = "0.25.0" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" 1208 | dependencies = [ 1209 | "ttf-parser", 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "parking" 1214 | version = "2.2.1" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1217 | 1218 | [[package]] 1219 | name = "parking_lot" 1220 | version = "0.12.3" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1223 | dependencies = [ 1224 | "lock_api", 1225 | "parking_lot_core", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "parking_lot_core" 1230 | version = "0.9.10" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1233 | dependencies = [ 1234 | "cfg-if", 1235 | "libc", 1236 | "redox_syscall 0.5.8", 1237 | "smallvec", 1238 | "windows-targets 0.52.6", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "paste" 1243 | version = "1.0.15" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1246 | 1247 | [[package]] 1248 | name = "percent-encoding" 1249 | version = "2.3.1" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1252 | 1253 | [[package]] 1254 | name = "pin-project" 1255 | version = "1.1.8" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" 1258 | dependencies = [ 1259 | "pin-project-internal", 1260 | ] 1261 | 1262 | [[package]] 1263 | name = "pin-project-internal" 1264 | version = "1.1.8" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" 1267 | dependencies = [ 1268 | "proc-macro2", 1269 | "quote", 1270 | "syn", 1271 | ] 1272 | 1273 | [[package]] 1274 | name = "pin-project-lite" 1275 | version = "0.2.16" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1278 | 1279 | [[package]] 1280 | name = "pkg-config" 1281 | version = "0.3.31" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 1284 | 1285 | [[package]] 1286 | name = "polling" 1287 | version = "3.7.4" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" 1290 | dependencies = [ 1291 | "cfg-if", 1292 | "concurrent-queue", 1293 | "hermit-abi", 1294 | "pin-project-lite", 1295 | "rustix", 1296 | "tracing", 1297 | "windows-sys 0.59.0", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "portable-atomic" 1302 | version = "1.11.0" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 1305 | 1306 | [[package]] 1307 | name = "presser" 1308 | version = "0.3.1" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" 1311 | 1312 | [[package]] 1313 | name = "proc-macro-crate" 1314 | version = "3.2.0" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" 1317 | dependencies = [ 1318 | "toml_edit", 1319 | ] 1320 | 1321 | [[package]] 1322 | name = "proc-macro2" 1323 | version = "1.0.93" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 1326 | dependencies = [ 1327 | "unicode-ident", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "profiling" 1332 | version = "1.0.16" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" 1335 | dependencies = [ 1336 | "profiling-procmacros", 1337 | "puffin", 1338 | "tracy-client 0.17.6", 1339 | ] 1340 | 1341 | [[package]] 1342 | name = "profiling-procmacros" 1343 | version = "1.0.16" 1344 | source = "registry+https://github.com/rust-lang/crates.io-index" 1345 | checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" 1346 | dependencies = [ 1347 | "quote", 1348 | "syn", 1349 | ] 1350 | 1351 | [[package]] 1352 | name = "puffin" 1353 | version = "0.19.1" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "fa9dae7b05c02ec1a6bc9bcf20d8bc64a7dcbf57934107902a872014899b741f" 1356 | dependencies = [ 1357 | "anyhow", 1358 | "bincode", 1359 | "byteorder", 1360 | "cfg-if", 1361 | "itertools", 1362 | "lz4_flex", 1363 | "once_cell", 1364 | "parking_lot", 1365 | "serde", 1366 | ] 1367 | 1368 | [[package]] 1369 | name = "puffin_http" 1370 | version = "0.16.1" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "739a3c7f56604713b553d7addd7718c226e88d598979ae3450320800bd0e9810" 1373 | dependencies = [ 1374 | "anyhow", 1375 | "crossbeam-channel", 1376 | "log", 1377 | "parking_lot", 1378 | "puffin", 1379 | ] 1380 | 1381 | [[package]] 1382 | name = "quick-xml" 1383 | version = "0.36.2" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" 1386 | dependencies = [ 1387 | "memchr", 1388 | ] 1389 | 1390 | [[package]] 1391 | name = "quote" 1392 | version = "1.0.38" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 1395 | dependencies = [ 1396 | "proc-macro2", 1397 | ] 1398 | 1399 | [[package]] 1400 | name = "range-alloc" 1401 | version = "0.1.4" 1402 | source = "registry+https://github.com/rust-lang/crates.io-index" 1403 | checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" 1404 | 1405 | [[package]] 1406 | name = "raw-window-handle" 1407 | version = "0.6.2" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" 1410 | 1411 | [[package]] 1412 | name = "redox_syscall" 1413 | version = "0.4.1" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 1416 | dependencies = [ 1417 | "bitflags 1.3.2", 1418 | ] 1419 | 1420 | [[package]] 1421 | name = "redox_syscall" 1422 | version = "0.5.8" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 1425 | dependencies = [ 1426 | "bitflags 2.9.0", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "regex" 1431 | version = "1.11.1" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1434 | dependencies = [ 1435 | "aho-corasick", 1436 | "memchr", 1437 | "regex-automata 0.4.9", 1438 | "regex-syntax 0.8.5", 1439 | ] 1440 | 1441 | [[package]] 1442 | name = "regex-automata" 1443 | version = "0.1.10" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1446 | dependencies = [ 1447 | "regex-syntax 0.6.29", 1448 | ] 1449 | 1450 | [[package]] 1451 | name = "regex-automata" 1452 | version = "0.4.9" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1455 | dependencies = [ 1456 | "aho-corasick", 1457 | "memchr", 1458 | "regex-syntax 0.8.5", 1459 | ] 1460 | 1461 | [[package]] 1462 | name = "regex-syntax" 1463 | version = "0.6.29" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1466 | 1467 | [[package]] 1468 | name = "regex-syntax" 1469 | version = "0.8.5" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1472 | 1473 | [[package]] 1474 | name = "renderdoc-sys" 1475 | version = "1.1.0" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" 1478 | 1479 | [[package]] 1480 | name = "rustc-hash" 1481 | version = "1.1.0" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1484 | 1485 | [[package]] 1486 | name = "rustix" 1487 | version = "0.38.43" 1488 | source = "registry+https://github.com/rust-lang/crates.io-index" 1489 | checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" 1490 | dependencies = [ 1491 | "bitflags 2.9.0", 1492 | "errno", 1493 | "libc", 1494 | "linux-raw-sys", 1495 | "windows-sys 0.59.0", 1496 | ] 1497 | 1498 | [[package]] 1499 | name = "rustversion" 1500 | version = "1.0.19" 1501 | source = "registry+https://github.com/rust-lang/crates.io-index" 1502 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 1503 | 1504 | [[package]] 1505 | name = "same-file" 1506 | version = "1.0.6" 1507 | source = "registry+https://github.com/rust-lang/crates.io-index" 1508 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1509 | dependencies = [ 1510 | "winapi-util", 1511 | ] 1512 | 1513 | [[package]] 1514 | name = "scoped-tls" 1515 | version = "1.0.1" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 1518 | 1519 | [[package]] 1520 | name = "scopeguard" 1521 | version = "1.2.0" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1524 | 1525 | [[package]] 1526 | name = "sctk-adwaita" 1527 | version = "0.10.1" 1528 | source = "registry+https://github.com/rust-lang/crates.io-index" 1529 | checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" 1530 | dependencies = [ 1531 | "ab_glyph", 1532 | "log", 1533 | "memmap2", 1534 | "smithay-client-toolkit", 1535 | "tiny-skia", 1536 | ] 1537 | 1538 | [[package]] 1539 | name = "serde" 1540 | version = "1.0.217" 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" 1542 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 1543 | dependencies = [ 1544 | "serde_derive", 1545 | ] 1546 | 1547 | [[package]] 1548 | name = "serde_derive" 1549 | version = "1.0.217" 1550 | source = "registry+https://github.com/rust-lang/crates.io-index" 1551 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 1552 | dependencies = [ 1553 | "proc-macro2", 1554 | "quote", 1555 | "syn", 1556 | ] 1557 | 1558 | [[package]] 1559 | name = "sharded-slab" 1560 | version = "0.1.7" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1563 | dependencies = [ 1564 | "lazy_static", 1565 | ] 1566 | 1567 | [[package]] 1568 | name = "shlex" 1569 | version = "1.3.0" 1570 | source = "registry+https://github.com/rust-lang/crates.io-index" 1571 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1572 | 1573 | [[package]] 1574 | name = "slab" 1575 | version = "0.4.9" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1578 | dependencies = [ 1579 | "autocfg", 1580 | ] 1581 | 1582 | [[package]] 1583 | name = "slotmap" 1584 | version = "1.0.7" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" 1587 | dependencies = [ 1588 | "version_check", 1589 | ] 1590 | 1591 | [[package]] 1592 | name = "smallvec" 1593 | version = "1.13.2" 1594 | source = "registry+https://github.com/rust-lang/crates.io-index" 1595 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1596 | 1597 | [[package]] 1598 | name = "smithay-client-toolkit" 1599 | version = "0.19.2" 1600 | source = "registry+https://github.com/rust-lang/crates.io-index" 1601 | checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" 1602 | dependencies = [ 1603 | "bitflags 2.9.0", 1604 | "calloop", 1605 | "calloop-wayland-source", 1606 | "cursor-icon", 1607 | "libc", 1608 | "log", 1609 | "memmap2", 1610 | "rustix", 1611 | "thiserror 1.0.69", 1612 | "wayland-backend", 1613 | "wayland-client", 1614 | "wayland-csd-frame", 1615 | "wayland-cursor", 1616 | "wayland-protocols", 1617 | "wayland-protocols-wlr", 1618 | "wayland-scanner", 1619 | "xkeysym", 1620 | ] 1621 | 1622 | [[package]] 1623 | name = "smol_str" 1624 | version = "0.2.2" 1625 | source = "registry+https://github.com/rust-lang/crates.io-index" 1626 | checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" 1627 | dependencies = [ 1628 | "serde", 1629 | ] 1630 | 1631 | [[package]] 1632 | name = "spirv" 1633 | version = "0.3.0+sdk-1.3.268.0" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" 1636 | dependencies = [ 1637 | "bitflags 2.9.0", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "static_assertions" 1642 | version = "1.1.0" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1645 | 1646 | [[package]] 1647 | name = "strict-num" 1648 | version = "0.1.1" 1649 | source = "registry+https://github.com/rust-lang/crates.io-index" 1650 | checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" 1651 | 1652 | [[package]] 1653 | name = "strum" 1654 | version = "0.26.3" 1655 | source = "registry+https://github.com/rust-lang/crates.io-index" 1656 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 1657 | dependencies = [ 1658 | "strum_macros", 1659 | ] 1660 | 1661 | [[package]] 1662 | name = "strum_macros" 1663 | version = "0.26.4" 1664 | source = "registry+https://github.com/rust-lang/crates.io-index" 1665 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1666 | dependencies = [ 1667 | "heck", 1668 | "proc-macro2", 1669 | "quote", 1670 | "rustversion", 1671 | "syn", 1672 | ] 1673 | 1674 | [[package]] 1675 | name = "syn" 1676 | version = "2.0.96" 1677 | source = "registry+https://github.com/rust-lang/crates.io-index" 1678 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 1679 | dependencies = [ 1680 | "proc-macro2", 1681 | "quote", 1682 | "unicode-ident", 1683 | ] 1684 | 1685 | [[package]] 1686 | name = "termcolor" 1687 | version = "1.4.1" 1688 | source = "registry+https://github.com/rust-lang/crates.io-index" 1689 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1690 | dependencies = [ 1691 | "winapi-util", 1692 | ] 1693 | 1694 | [[package]] 1695 | name = "thiserror" 1696 | version = "1.0.69" 1697 | source = "registry+https://github.com/rust-lang/crates.io-index" 1698 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1699 | dependencies = [ 1700 | "thiserror-impl 1.0.69", 1701 | ] 1702 | 1703 | [[package]] 1704 | name = "thiserror" 1705 | version = "2.0.11" 1706 | source = "registry+https://github.com/rust-lang/crates.io-index" 1707 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 1708 | dependencies = [ 1709 | "thiserror-impl 2.0.11", 1710 | ] 1711 | 1712 | [[package]] 1713 | name = "thiserror-impl" 1714 | version = "1.0.69" 1715 | source = "registry+https://github.com/rust-lang/crates.io-index" 1716 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1717 | dependencies = [ 1718 | "proc-macro2", 1719 | "quote", 1720 | "syn", 1721 | ] 1722 | 1723 | [[package]] 1724 | name = "thiserror-impl" 1725 | version = "2.0.11" 1726 | source = "registry+https://github.com/rust-lang/crates.io-index" 1727 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 1728 | dependencies = [ 1729 | "proc-macro2", 1730 | "quote", 1731 | "syn", 1732 | ] 1733 | 1734 | [[package]] 1735 | name = "thread_local" 1736 | version = "1.1.8" 1737 | source = "registry+https://github.com/rust-lang/crates.io-index" 1738 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1739 | dependencies = [ 1740 | "cfg-if", 1741 | "once_cell", 1742 | ] 1743 | 1744 | [[package]] 1745 | name = "tiny-skia" 1746 | version = "0.11.4" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" 1749 | dependencies = [ 1750 | "arrayref", 1751 | "arrayvec", 1752 | "bytemuck", 1753 | "cfg-if", 1754 | "log", 1755 | "tiny-skia-path", 1756 | ] 1757 | 1758 | [[package]] 1759 | name = "tiny-skia-path" 1760 | version = "0.11.4" 1761 | source = "registry+https://github.com/rust-lang/crates.io-index" 1762 | checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" 1763 | dependencies = [ 1764 | "arrayref", 1765 | "bytemuck", 1766 | "strict-num", 1767 | ] 1768 | 1769 | [[package]] 1770 | name = "toml_datetime" 1771 | version = "0.6.8" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1774 | 1775 | [[package]] 1776 | name = "toml_edit" 1777 | version = "0.22.22" 1778 | source = "registry+https://github.com/rust-lang/crates.io-index" 1779 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 1780 | dependencies = [ 1781 | "indexmap", 1782 | "toml_datetime", 1783 | "winnow", 1784 | ] 1785 | 1786 | [[package]] 1787 | name = "tracing" 1788 | version = "0.1.41" 1789 | source = "registry+https://github.com/rust-lang/crates.io-index" 1790 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1791 | dependencies = [ 1792 | "pin-project-lite", 1793 | "tracing-core", 1794 | ] 1795 | 1796 | [[package]] 1797 | name = "tracing-core" 1798 | version = "0.1.33" 1799 | source = "registry+https://github.com/rust-lang/crates.io-index" 1800 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1801 | dependencies = [ 1802 | "once_cell", 1803 | "valuable", 1804 | ] 1805 | 1806 | [[package]] 1807 | name = "tracing-log" 1808 | version = "0.2.0" 1809 | source = "registry+https://github.com/rust-lang/crates.io-index" 1810 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1811 | dependencies = [ 1812 | "log", 1813 | "once_cell", 1814 | "tracing-core", 1815 | ] 1816 | 1817 | [[package]] 1818 | name = "tracing-subscriber" 1819 | version = "0.3.19" 1820 | source = "registry+https://github.com/rust-lang/crates.io-index" 1821 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 1822 | dependencies = [ 1823 | "matchers", 1824 | "nu-ansi-term", 1825 | "once_cell", 1826 | "regex", 1827 | "sharded-slab", 1828 | "smallvec", 1829 | "thread_local", 1830 | "tracing", 1831 | "tracing-core", 1832 | "tracing-log", 1833 | ] 1834 | 1835 | [[package]] 1836 | name = "tracy-client" 1837 | version = "0.17.6" 1838 | source = "registry+https://github.com/rust-lang/crates.io-index" 1839 | checksum = "73202d787346a5418f8222eddb5a00f29ea47caf3c7d38a8f2f69f8455fa7c7e" 1840 | dependencies = [ 1841 | "loom", 1842 | "once_cell", 1843 | "tracy-client-sys", 1844 | ] 1845 | 1846 | [[package]] 1847 | name = "tracy-client" 1848 | version = "0.18.0" 1849 | source = "registry+https://github.com/rust-lang/crates.io-index" 1850 | checksum = "d90a2c01305b02b76fdd89ac8608bae27e173c829a35f7d76a345ab5d33836db" 1851 | dependencies = [ 1852 | "loom", 1853 | "once_cell", 1854 | "tracy-client-sys", 1855 | ] 1856 | 1857 | [[package]] 1858 | name = "tracy-client-sys" 1859 | version = "0.24.3" 1860 | source = "registry+https://github.com/rust-lang/crates.io-index" 1861 | checksum = "69fff37da548239c3bf9e64a12193d261e8b22b660991c6fd2df057c168f435f" 1862 | dependencies = [ 1863 | "cc", 1864 | "windows-targets 0.52.6", 1865 | ] 1866 | 1867 | [[package]] 1868 | name = "ttf-parser" 1869 | version = "0.25.1" 1870 | source = "registry+https://github.com/rust-lang/crates.io-index" 1871 | checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" 1872 | 1873 | [[package]] 1874 | name = "unicode-ident" 1875 | version = "1.0.14" 1876 | source = "registry+https://github.com/rust-lang/crates.io-index" 1877 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 1878 | 1879 | [[package]] 1880 | name = "unicode-segmentation" 1881 | version = "1.12.0" 1882 | source = "registry+https://github.com/rust-lang/crates.io-index" 1883 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1884 | 1885 | [[package]] 1886 | name = "unicode-width" 1887 | version = "0.1.14" 1888 | source = "registry+https://github.com/rust-lang/crates.io-index" 1889 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1890 | 1891 | [[package]] 1892 | name = "valuable" 1893 | version = "0.1.1" 1894 | source = "registry+https://github.com/rust-lang/crates.io-index" 1895 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1896 | 1897 | [[package]] 1898 | name = "version_check" 1899 | version = "0.9.5" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1902 | 1903 | [[package]] 1904 | name = "walkdir" 1905 | version = "2.5.0" 1906 | source = "registry+https://github.com/rust-lang/crates.io-index" 1907 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1908 | dependencies = [ 1909 | "same-file", 1910 | "winapi-util", 1911 | ] 1912 | 1913 | [[package]] 1914 | name = "wasi" 1915 | version = "0.11.0+wasi-snapshot-preview1" 1916 | source = "registry+https://github.com/rust-lang/crates.io-index" 1917 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1918 | 1919 | [[package]] 1920 | name = "wasm-bindgen" 1921 | version = "0.2.100" 1922 | source = "registry+https://github.com/rust-lang/crates.io-index" 1923 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1924 | dependencies = [ 1925 | "cfg-if", 1926 | "once_cell", 1927 | "rustversion", 1928 | "wasm-bindgen-macro", 1929 | ] 1930 | 1931 | [[package]] 1932 | name = "wasm-bindgen-backend" 1933 | version = "0.2.100" 1934 | source = "registry+https://github.com/rust-lang/crates.io-index" 1935 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1936 | dependencies = [ 1937 | "bumpalo", 1938 | "log", 1939 | "proc-macro2", 1940 | "quote", 1941 | "syn", 1942 | "wasm-bindgen-shared", 1943 | ] 1944 | 1945 | [[package]] 1946 | name = "wasm-bindgen-futures" 1947 | version = "0.4.50" 1948 | source = "registry+https://github.com/rust-lang/crates.io-index" 1949 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1950 | dependencies = [ 1951 | "cfg-if", 1952 | "js-sys", 1953 | "once_cell", 1954 | "wasm-bindgen", 1955 | "web-sys", 1956 | ] 1957 | 1958 | [[package]] 1959 | name = "wasm-bindgen-macro" 1960 | version = "0.2.100" 1961 | source = "registry+https://github.com/rust-lang/crates.io-index" 1962 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1963 | dependencies = [ 1964 | "quote", 1965 | "wasm-bindgen-macro-support", 1966 | ] 1967 | 1968 | [[package]] 1969 | name = "wasm-bindgen-macro-support" 1970 | version = "0.2.100" 1971 | source = "registry+https://github.com/rust-lang/crates.io-index" 1972 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1973 | dependencies = [ 1974 | "proc-macro2", 1975 | "quote", 1976 | "syn", 1977 | "wasm-bindgen-backend", 1978 | "wasm-bindgen-shared", 1979 | ] 1980 | 1981 | [[package]] 1982 | name = "wasm-bindgen-shared" 1983 | version = "0.2.100" 1984 | source = "registry+https://github.com/rust-lang/crates.io-index" 1985 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1986 | dependencies = [ 1987 | "unicode-ident", 1988 | ] 1989 | 1990 | [[package]] 1991 | name = "wayland-backend" 1992 | version = "0.3.7" 1993 | source = "registry+https://github.com/rust-lang/crates.io-index" 1994 | checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" 1995 | dependencies = [ 1996 | "cc", 1997 | "downcast-rs", 1998 | "rustix", 1999 | "scoped-tls", 2000 | "smallvec", 2001 | "wayland-sys", 2002 | ] 2003 | 2004 | [[package]] 2005 | name = "wayland-client" 2006 | version = "0.31.7" 2007 | source = "registry+https://github.com/rust-lang/crates.io-index" 2008 | checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" 2009 | dependencies = [ 2010 | "bitflags 2.9.0", 2011 | "rustix", 2012 | "wayland-backend", 2013 | "wayland-scanner", 2014 | ] 2015 | 2016 | [[package]] 2017 | name = "wayland-csd-frame" 2018 | version = "0.3.0" 2019 | source = "registry+https://github.com/rust-lang/crates.io-index" 2020 | checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" 2021 | dependencies = [ 2022 | "bitflags 2.9.0", 2023 | "cursor-icon", 2024 | "wayland-backend", 2025 | ] 2026 | 2027 | [[package]] 2028 | name = "wayland-cursor" 2029 | version = "0.31.7" 2030 | source = "registry+https://github.com/rust-lang/crates.io-index" 2031 | checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" 2032 | dependencies = [ 2033 | "rustix", 2034 | "wayland-client", 2035 | "xcursor", 2036 | ] 2037 | 2038 | [[package]] 2039 | name = "wayland-protocols" 2040 | version = "0.32.5" 2041 | source = "registry+https://github.com/rust-lang/crates.io-index" 2042 | checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" 2043 | dependencies = [ 2044 | "bitflags 2.9.0", 2045 | "wayland-backend", 2046 | "wayland-client", 2047 | "wayland-scanner", 2048 | ] 2049 | 2050 | [[package]] 2051 | name = "wayland-protocols-plasma" 2052 | version = "0.3.5" 2053 | source = "registry+https://github.com/rust-lang/crates.io-index" 2054 | checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" 2055 | dependencies = [ 2056 | "bitflags 2.9.0", 2057 | "wayland-backend", 2058 | "wayland-client", 2059 | "wayland-protocols", 2060 | "wayland-scanner", 2061 | ] 2062 | 2063 | [[package]] 2064 | name = "wayland-protocols-wlr" 2065 | version = "0.3.5" 2066 | source = "registry+https://github.com/rust-lang/crates.io-index" 2067 | checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" 2068 | dependencies = [ 2069 | "bitflags 2.9.0", 2070 | "wayland-backend", 2071 | "wayland-client", 2072 | "wayland-protocols", 2073 | "wayland-scanner", 2074 | ] 2075 | 2076 | [[package]] 2077 | name = "wayland-scanner" 2078 | version = "0.31.5" 2079 | source = "registry+https://github.com/rust-lang/crates.io-index" 2080 | checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" 2081 | dependencies = [ 2082 | "proc-macro2", 2083 | "quick-xml", 2084 | "quote", 2085 | ] 2086 | 2087 | [[package]] 2088 | name = "wayland-sys" 2089 | version = "0.31.5" 2090 | source = "registry+https://github.com/rust-lang/crates.io-index" 2091 | checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" 2092 | dependencies = [ 2093 | "dlib", 2094 | "log", 2095 | "once_cell", 2096 | "pkg-config", 2097 | ] 2098 | 2099 | [[package]] 2100 | name = "web-sys" 2101 | version = "0.3.77" 2102 | source = "registry+https://github.com/rust-lang/crates.io-index" 2103 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 2104 | dependencies = [ 2105 | "js-sys", 2106 | "wasm-bindgen", 2107 | ] 2108 | 2109 | [[package]] 2110 | name = "web-time" 2111 | version = "1.1.0" 2112 | source = "registry+https://github.com/rust-lang/crates.io-index" 2113 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 2114 | dependencies = [ 2115 | "js-sys", 2116 | "wasm-bindgen", 2117 | ] 2118 | 2119 | [[package]] 2120 | name = "wgpu" 2121 | version = "25.0.0" 2122 | source = "registry+https://github.com/rust-lang/crates.io-index" 2123 | checksum = "ca6049eb2014a0e0d8689f9b787605dd71d5bbfdc74095ead499f3cff705c229" 2124 | dependencies = [ 2125 | "arrayvec", 2126 | "bitflags 2.9.0", 2127 | "cfg_aliases", 2128 | "document-features", 2129 | "hashbrown", 2130 | "js-sys", 2131 | "log", 2132 | "naga", 2133 | "parking_lot", 2134 | "portable-atomic", 2135 | "profiling", 2136 | "raw-window-handle", 2137 | "smallvec", 2138 | "static_assertions", 2139 | "wasm-bindgen", 2140 | "wasm-bindgen-futures", 2141 | "web-sys", 2142 | "wgpu-core", 2143 | "wgpu-hal", 2144 | "wgpu-types", 2145 | ] 2146 | 2147 | [[package]] 2148 | name = "wgpu-core" 2149 | version = "25.0.1" 2150 | source = "registry+https://github.com/rust-lang/crates.io-index" 2151 | checksum = "a19813e647da7aa3cdaa84f5846e2c64114970ea7c86b1e6aae8be08091f4bdc" 2152 | dependencies = [ 2153 | "arrayvec", 2154 | "bit-set", 2155 | "bit-vec", 2156 | "bitflags 2.9.0", 2157 | "cfg_aliases", 2158 | "document-features", 2159 | "hashbrown", 2160 | "indexmap", 2161 | "log", 2162 | "naga", 2163 | "once_cell", 2164 | "parking_lot", 2165 | "portable-atomic", 2166 | "profiling", 2167 | "raw-window-handle", 2168 | "rustc-hash", 2169 | "smallvec", 2170 | "thiserror 2.0.11", 2171 | "wgpu-core-deps-apple", 2172 | "wgpu-core-deps-emscripten", 2173 | "wgpu-core-deps-windows-linux-android", 2174 | "wgpu-hal", 2175 | "wgpu-types", 2176 | ] 2177 | 2178 | [[package]] 2179 | name = "wgpu-core-deps-apple" 2180 | version = "25.0.0" 2181 | source = "registry+https://github.com/rust-lang/crates.io-index" 2182 | checksum = "cfd488b3239b6b7b185c3b045c39ca6bf8af34467a4c5de4e0b1a564135d093d" 2183 | dependencies = [ 2184 | "wgpu-hal", 2185 | ] 2186 | 2187 | [[package]] 2188 | name = "wgpu-core-deps-emscripten" 2189 | version = "25.0.0" 2190 | source = "registry+https://github.com/rust-lang/crates.io-index" 2191 | checksum = "f09ad7aceb3818e52539acc679f049d3475775586f3f4e311c30165cf2c00445" 2192 | dependencies = [ 2193 | "wgpu-hal", 2194 | ] 2195 | 2196 | [[package]] 2197 | name = "wgpu-core-deps-windows-linux-android" 2198 | version = "25.0.0" 2199 | source = "registry+https://github.com/rust-lang/crates.io-index" 2200 | checksum = "cba5fb5f7f9c98baa7c889d444f63ace25574833df56f5b817985f641af58e46" 2201 | dependencies = [ 2202 | "wgpu-hal", 2203 | ] 2204 | 2205 | [[package]] 2206 | name = "wgpu-hal" 2207 | version = "25.0.1" 2208 | source = "registry+https://github.com/rust-lang/crates.io-index" 2209 | checksum = "fb7c4a1dc42ff14c23c9b11ebf1ee85cde661a9b1cf0392f79c1faca5bc559fb" 2210 | dependencies = [ 2211 | "android_system_properties", 2212 | "arrayvec", 2213 | "ash", 2214 | "bit-set", 2215 | "bitflags 2.9.0", 2216 | "block", 2217 | "bytemuck", 2218 | "cfg-if", 2219 | "cfg_aliases", 2220 | "core-graphics-types", 2221 | "glow", 2222 | "glutin_wgl_sys", 2223 | "gpu-alloc", 2224 | "gpu-allocator", 2225 | "gpu-descriptor", 2226 | "hashbrown", 2227 | "js-sys", 2228 | "khronos-egl", 2229 | "libc", 2230 | "libloading", 2231 | "log", 2232 | "metal", 2233 | "naga", 2234 | "ndk-sys 0.5.0+25.2.9519653", 2235 | "objc", 2236 | "ordered-float", 2237 | "parking_lot", 2238 | "portable-atomic", 2239 | "profiling", 2240 | "range-alloc", 2241 | "raw-window-handle", 2242 | "renderdoc-sys", 2243 | "smallvec", 2244 | "thiserror 2.0.11", 2245 | "wasm-bindgen", 2246 | "web-sys", 2247 | "wgpu-types", 2248 | "windows", 2249 | "windows-core", 2250 | ] 2251 | 2252 | [[package]] 2253 | name = "wgpu-profiler" 2254 | version = "0.23.0" 2255 | dependencies = [ 2256 | "futures-lite", 2257 | "parking_lot", 2258 | "profiling", 2259 | "puffin", 2260 | "puffin_http", 2261 | "thiserror 2.0.11", 2262 | "tracy-client 0.18.0", 2263 | "wgpu", 2264 | "winit", 2265 | ] 2266 | 2267 | [[package]] 2268 | name = "wgpu-types" 2269 | version = "25.0.0" 2270 | source = "registry+https://github.com/rust-lang/crates.io-index" 2271 | checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" 2272 | dependencies = [ 2273 | "bitflags 2.9.0", 2274 | "bytemuck", 2275 | "js-sys", 2276 | "log", 2277 | "thiserror 2.0.11", 2278 | "web-sys", 2279 | ] 2280 | 2281 | [[package]] 2282 | name = "winapi" 2283 | version = "0.3.9" 2284 | source = "registry+https://github.com/rust-lang/crates.io-index" 2285 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2286 | dependencies = [ 2287 | "winapi-i686-pc-windows-gnu", 2288 | "winapi-x86_64-pc-windows-gnu", 2289 | ] 2290 | 2291 | [[package]] 2292 | name = "winapi-i686-pc-windows-gnu" 2293 | version = "0.4.0" 2294 | source = "registry+https://github.com/rust-lang/crates.io-index" 2295 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2296 | 2297 | [[package]] 2298 | name = "winapi-util" 2299 | version = "0.1.9" 2300 | source = "registry+https://github.com/rust-lang/crates.io-index" 2301 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 2302 | dependencies = [ 2303 | "windows-sys 0.59.0", 2304 | ] 2305 | 2306 | [[package]] 2307 | name = "winapi-x86_64-pc-windows-gnu" 2308 | version = "0.4.0" 2309 | source = "registry+https://github.com/rust-lang/crates.io-index" 2310 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2311 | 2312 | [[package]] 2313 | name = "windows" 2314 | version = "0.58.0" 2315 | source = "registry+https://github.com/rust-lang/crates.io-index" 2316 | checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" 2317 | dependencies = [ 2318 | "windows-core", 2319 | "windows-targets 0.52.6", 2320 | ] 2321 | 2322 | [[package]] 2323 | name = "windows-core" 2324 | version = "0.58.0" 2325 | source = "registry+https://github.com/rust-lang/crates.io-index" 2326 | checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" 2327 | dependencies = [ 2328 | "windows-implement", 2329 | "windows-interface", 2330 | "windows-result", 2331 | "windows-strings", 2332 | "windows-targets 0.52.6", 2333 | ] 2334 | 2335 | [[package]] 2336 | name = "windows-implement" 2337 | version = "0.58.0" 2338 | source = "registry+https://github.com/rust-lang/crates.io-index" 2339 | checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" 2340 | dependencies = [ 2341 | "proc-macro2", 2342 | "quote", 2343 | "syn", 2344 | ] 2345 | 2346 | [[package]] 2347 | name = "windows-interface" 2348 | version = "0.58.0" 2349 | source = "registry+https://github.com/rust-lang/crates.io-index" 2350 | checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" 2351 | dependencies = [ 2352 | "proc-macro2", 2353 | "quote", 2354 | "syn", 2355 | ] 2356 | 2357 | [[package]] 2358 | name = "windows-result" 2359 | version = "0.2.0" 2360 | source = "registry+https://github.com/rust-lang/crates.io-index" 2361 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 2362 | dependencies = [ 2363 | "windows-targets 0.52.6", 2364 | ] 2365 | 2366 | [[package]] 2367 | name = "windows-strings" 2368 | version = "0.1.0" 2369 | source = "registry+https://github.com/rust-lang/crates.io-index" 2370 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 2371 | dependencies = [ 2372 | "windows-result", 2373 | "windows-targets 0.52.6", 2374 | ] 2375 | 2376 | [[package]] 2377 | name = "windows-sys" 2378 | version = "0.45.0" 2379 | source = "registry+https://github.com/rust-lang/crates.io-index" 2380 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 2381 | dependencies = [ 2382 | "windows-targets 0.42.2", 2383 | ] 2384 | 2385 | [[package]] 2386 | name = "windows-sys" 2387 | version = "0.52.0" 2388 | source = "registry+https://github.com/rust-lang/crates.io-index" 2389 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2390 | dependencies = [ 2391 | "windows-targets 0.52.6", 2392 | ] 2393 | 2394 | [[package]] 2395 | name = "windows-sys" 2396 | version = "0.59.0" 2397 | source = "registry+https://github.com/rust-lang/crates.io-index" 2398 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2399 | dependencies = [ 2400 | "windows-targets 0.52.6", 2401 | ] 2402 | 2403 | [[package]] 2404 | name = "windows-targets" 2405 | version = "0.42.2" 2406 | source = "registry+https://github.com/rust-lang/crates.io-index" 2407 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 2408 | dependencies = [ 2409 | "windows_aarch64_gnullvm 0.42.2", 2410 | "windows_aarch64_msvc 0.42.2", 2411 | "windows_i686_gnu 0.42.2", 2412 | "windows_i686_msvc 0.42.2", 2413 | "windows_x86_64_gnu 0.42.2", 2414 | "windows_x86_64_gnullvm 0.42.2", 2415 | "windows_x86_64_msvc 0.42.2", 2416 | ] 2417 | 2418 | [[package]] 2419 | name = "windows-targets" 2420 | version = "0.48.5" 2421 | source = "registry+https://github.com/rust-lang/crates.io-index" 2422 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2423 | dependencies = [ 2424 | "windows_aarch64_gnullvm 0.48.5", 2425 | "windows_aarch64_msvc 0.48.5", 2426 | "windows_i686_gnu 0.48.5", 2427 | "windows_i686_msvc 0.48.5", 2428 | "windows_x86_64_gnu 0.48.5", 2429 | "windows_x86_64_gnullvm 0.48.5", 2430 | "windows_x86_64_msvc 0.48.5", 2431 | ] 2432 | 2433 | [[package]] 2434 | name = "windows-targets" 2435 | version = "0.52.6" 2436 | source = "registry+https://github.com/rust-lang/crates.io-index" 2437 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2438 | dependencies = [ 2439 | "windows_aarch64_gnullvm 0.52.6", 2440 | "windows_aarch64_msvc 0.52.6", 2441 | "windows_i686_gnu 0.52.6", 2442 | "windows_i686_gnullvm", 2443 | "windows_i686_msvc 0.52.6", 2444 | "windows_x86_64_gnu 0.52.6", 2445 | "windows_x86_64_gnullvm 0.52.6", 2446 | "windows_x86_64_msvc 0.52.6", 2447 | ] 2448 | 2449 | [[package]] 2450 | name = "windows_aarch64_gnullvm" 2451 | version = "0.42.2" 2452 | source = "registry+https://github.com/rust-lang/crates.io-index" 2453 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 2454 | 2455 | [[package]] 2456 | name = "windows_aarch64_gnullvm" 2457 | version = "0.48.5" 2458 | source = "registry+https://github.com/rust-lang/crates.io-index" 2459 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2460 | 2461 | [[package]] 2462 | name = "windows_aarch64_gnullvm" 2463 | version = "0.52.6" 2464 | source = "registry+https://github.com/rust-lang/crates.io-index" 2465 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2466 | 2467 | [[package]] 2468 | name = "windows_aarch64_msvc" 2469 | version = "0.42.2" 2470 | source = "registry+https://github.com/rust-lang/crates.io-index" 2471 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 2472 | 2473 | [[package]] 2474 | name = "windows_aarch64_msvc" 2475 | version = "0.48.5" 2476 | source = "registry+https://github.com/rust-lang/crates.io-index" 2477 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2478 | 2479 | [[package]] 2480 | name = "windows_aarch64_msvc" 2481 | version = "0.52.6" 2482 | source = "registry+https://github.com/rust-lang/crates.io-index" 2483 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2484 | 2485 | [[package]] 2486 | name = "windows_i686_gnu" 2487 | version = "0.42.2" 2488 | source = "registry+https://github.com/rust-lang/crates.io-index" 2489 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 2490 | 2491 | [[package]] 2492 | name = "windows_i686_gnu" 2493 | version = "0.48.5" 2494 | source = "registry+https://github.com/rust-lang/crates.io-index" 2495 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2496 | 2497 | [[package]] 2498 | name = "windows_i686_gnu" 2499 | version = "0.52.6" 2500 | source = "registry+https://github.com/rust-lang/crates.io-index" 2501 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2502 | 2503 | [[package]] 2504 | name = "windows_i686_gnullvm" 2505 | version = "0.52.6" 2506 | source = "registry+https://github.com/rust-lang/crates.io-index" 2507 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2508 | 2509 | [[package]] 2510 | name = "windows_i686_msvc" 2511 | version = "0.42.2" 2512 | source = "registry+https://github.com/rust-lang/crates.io-index" 2513 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 2514 | 2515 | [[package]] 2516 | name = "windows_i686_msvc" 2517 | version = "0.48.5" 2518 | source = "registry+https://github.com/rust-lang/crates.io-index" 2519 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2520 | 2521 | [[package]] 2522 | name = "windows_i686_msvc" 2523 | version = "0.52.6" 2524 | source = "registry+https://github.com/rust-lang/crates.io-index" 2525 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2526 | 2527 | [[package]] 2528 | name = "windows_x86_64_gnu" 2529 | version = "0.42.2" 2530 | source = "registry+https://github.com/rust-lang/crates.io-index" 2531 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 2532 | 2533 | [[package]] 2534 | name = "windows_x86_64_gnu" 2535 | version = "0.48.5" 2536 | source = "registry+https://github.com/rust-lang/crates.io-index" 2537 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2538 | 2539 | [[package]] 2540 | name = "windows_x86_64_gnu" 2541 | version = "0.52.6" 2542 | source = "registry+https://github.com/rust-lang/crates.io-index" 2543 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2544 | 2545 | [[package]] 2546 | name = "windows_x86_64_gnullvm" 2547 | version = "0.42.2" 2548 | source = "registry+https://github.com/rust-lang/crates.io-index" 2549 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 2550 | 2551 | [[package]] 2552 | name = "windows_x86_64_gnullvm" 2553 | version = "0.48.5" 2554 | source = "registry+https://github.com/rust-lang/crates.io-index" 2555 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2556 | 2557 | [[package]] 2558 | name = "windows_x86_64_gnullvm" 2559 | version = "0.52.6" 2560 | source = "registry+https://github.com/rust-lang/crates.io-index" 2561 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2562 | 2563 | [[package]] 2564 | name = "windows_x86_64_msvc" 2565 | version = "0.42.2" 2566 | source = "registry+https://github.com/rust-lang/crates.io-index" 2567 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 2568 | 2569 | [[package]] 2570 | name = "windows_x86_64_msvc" 2571 | version = "0.48.5" 2572 | source = "registry+https://github.com/rust-lang/crates.io-index" 2573 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2574 | 2575 | [[package]] 2576 | name = "windows_x86_64_msvc" 2577 | version = "0.52.6" 2578 | source = "registry+https://github.com/rust-lang/crates.io-index" 2579 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2580 | 2581 | [[package]] 2582 | name = "winit" 2583 | version = "0.30.8" 2584 | source = "registry+https://github.com/rust-lang/crates.io-index" 2585 | checksum = "f5d74280aabb958072864bff6cfbcf9025cf8bfacdde5e32b5e12920ef703b0f" 2586 | dependencies = [ 2587 | "ahash", 2588 | "android-activity", 2589 | "atomic-waker", 2590 | "bitflags 2.9.0", 2591 | "block2", 2592 | "bytemuck", 2593 | "calloop", 2594 | "cfg_aliases", 2595 | "concurrent-queue", 2596 | "core-foundation", 2597 | "core-graphics", 2598 | "cursor-icon", 2599 | "dpi", 2600 | "js-sys", 2601 | "libc", 2602 | "memmap2", 2603 | "ndk", 2604 | "objc2", 2605 | "objc2-app-kit", 2606 | "objc2-foundation", 2607 | "objc2-ui-kit", 2608 | "orbclient", 2609 | "percent-encoding", 2610 | "pin-project", 2611 | "raw-window-handle", 2612 | "redox_syscall 0.4.1", 2613 | "rustix", 2614 | "sctk-adwaita", 2615 | "smithay-client-toolkit", 2616 | "smol_str", 2617 | "tracing", 2618 | "unicode-segmentation", 2619 | "wasm-bindgen", 2620 | "wasm-bindgen-futures", 2621 | "wayland-backend", 2622 | "wayland-client", 2623 | "wayland-protocols", 2624 | "wayland-protocols-plasma", 2625 | "web-sys", 2626 | "web-time", 2627 | "windows-sys 0.52.0", 2628 | "x11-dl", 2629 | "x11rb", 2630 | "xkbcommon-dl", 2631 | ] 2632 | 2633 | [[package]] 2634 | name = "winnow" 2635 | version = "0.6.24" 2636 | source = "registry+https://github.com/rust-lang/crates.io-index" 2637 | checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" 2638 | dependencies = [ 2639 | "memchr", 2640 | ] 2641 | 2642 | [[package]] 2643 | name = "x11-dl" 2644 | version = "2.21.0" 2645 | source = "registry+https://github.com/rust-lang/crates.io-index" 2646 | checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" 2647 | dependencies = [ 2648 | "libc", 2649 | "once_cell", 2650 | "pkg-config", 2651 | ] 2652 | 2653 | [[package]] 2654 | name = "x11rb" 2655 | version = "0.13.1" 2656 | source = "registry+https://github.com/rust-lang/crates.io-index" 2657 | checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" 2658 | dependencies = [ 2659 | "as-raw-xcb-connection", 2660 | "gethostname", 2661 | "libc", 2662 | "libloading", 2663 | "once_cell", 2664 | "rustix", 2665 | "x11rb-protocol", 2666 | ] 2667 | 2668 | [[package]] 2669 | name = "x11rb-protocol" 2670 | version = "0.13.1" 2671 | source = "registry+https://github.com/rust-lang/crates.io-index" 2672 | checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" 2673 | 2674 | [[package]] 2675 | name = "xcursor" 2676 | version = "0.3.8" 2677 | source = "registry+https://github.com/rust-lang/crates.io-index" 2678 | checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" 2679 | 2680 | [[package]] 2681 | name = "xkbcommon-dl" 2682 | version = "0.4.2" 2683 | source = "registry+https://github.com/rust-lang/crates.io-index" 2684 | checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" 2685 | dependencies = [ 2686 | "bitflags 2.9.0", 2687 | "dlib", 2688 | "log", 2689 | "once_cell", 2690 | "xkeysym", 2691 | ] 2692 | 2693 | [[package]] 2694 | name = "xkeysym" 2695 | version = "0.2.1" 2696 | source = "registry+https://github.com/rust-lang/crates.io-index" 2697 | checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" 2698 | 2699 | [[package]] 2700 | name = "xml-rs" 2701 | version = "0.8.25" 2702 | source = "registry+https://github.com/rust-lang/crates.io-index" 2703 | checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" 2704 | 2705 | [[package]] 2706 | name = "zerocopy" 2707 | version = "0.7.35" 2708 | source = "registry+https://github.com/rust-lang/crates.io-index" 2709 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 2710 | dependencies = [ 2711 | "zerocopy-derive", 2712 | ] 2713 | 2714 | [[package]] 2715 | name = "zerocopy-derive" 2716 | version = "0.7.35" 2717 | source = "registry+https://github.com/rust-lang/crates.io-index" 2718 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 2719 | dependencies = [ 2720 | "proc-macro2", 2721 | "quote", 2722 | "syn", 2723 | ] 2724 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wgpu-profiler" 3 | version = "0.23.0" 4 | authors = ["Andreas Reich "] 5 | edition = "2021" 6 | description = "Simple profiler scopes for wgpu using timer queries" 7 | homepage = "https://github.com/Wumpf/wgpu-profiler" 8 | repository = "https://github.com/Wumpf/wgpu-profiler" 9 | keywords = ["graphics"] 10 | license = "MIT OR Apache-2.0" 11 | 12 | [lints] 13 | clippy.doc_markdown = "warn" 14 | 15 | [features] 16 | tracy = ["dep:tracy-client", "profiling/profile-with-tracy"] 17 | puffin = ["dep:puffin", "profiling/profile-with-puffin"] 18 | 19 | [lib] 20 | 21 | [dependencies] 22 | parking_lot = "0.12" # Used for Mutex & RwLock. Note that wgpu already depends on parking_lot as well. 23 | thiserror = "2" 24 | wgpu = { version = "25.0.0", default-features = false } 25 | 26 | tracy-client = { version = "0.18", optional = true } 27 | puffin = { version = "0.19.1", optional = true } 28 | 29 | [dev-dependencies] 30 | futures-lite = "2" 31 | profiling = "1" 32 | puffin_http = "0.16.1" 33 | tracy-client = "0.18" 34 | wgpu = { version = "25.0.0", default-features = true } 35 | winit = "0.30" 36 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Andreas Reich 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andreas Reich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wgpu-profiler 2 | [![Crates.io](https://img.shields.io/crates/v/wgpu-profiler.svg)](https://crates.io/crates/wgpu-profiler) 3 | 4 | Simple profiler scopes for wgpu using timer queries 5 | 6 | ## Features 7 | 8 | * Easy to use profiler scopes 9 | * Allows nesting! 10 | * Can be disabled by runtime flag 11 | * Additionally generates debug markers 12 | * Thread-safe - can profile several command encoder/buffers in parallel 13 | * Internally creates pools of timer queries automatically 14 | * Does not need to know in advance how many queries/profiling scopes are needed 15 | * Caches up profiler-frames until results are available 16 | * No stalling of the device at any time! 17 | * Many profiler instances can live side by side 18 | * chrome trace flamegraph json export 19 | * Tracy integration (behind `tracy` feature flag) 20 | * Puffin integration (behind `puffin` feature flag) 21 | 22 | ## How to use 23 | 24 | Create a new profiler object: 25 | ```rust 26 | use wgpu_profiler::{wgpu_profiler, GpuProfiler, GpuProfilerSettings}; 27 | // ... 28 | let mut profiler = GpuProfiler::new(&device, GpuProfilerSettings::default()); 29 | ``` 30 | 31 | Now you can start creating profiler scopes: 32 | ```rust 33 | // You can now open profiling scopes on any encoder or pass: 34 | let mut scope = profiler.scope("name of your scope", &mut encoder); 35 | 36 | // Scopes can be nested arbitrarily! 37 | let mut nested_scope = scope.scope("nested!"); 38 | 39 | // Scopes on encoders can be used to easily create profiled passes! 40 | let mut compute_pass = nested_scope.scoped_compute_pass("profiled compute"); 41 | 42 | // Scopes expose the underlying encoder or pass they wrap: 43 | compute_pass.set_pipeline(&pipeline); 44 | // ... 45 | 46 | // Scopes created this way are automatically closed when dropped. 47 | ``` 48 | 49 | `GpuProfiler` reads the device features on first use: 50 | 51 | * `wgpu::Features::TIMESTAMP_QUERY` is required to emit any timer queries. 52 | * Alone, this allows you to use timestamp writes on pass definition as done by `Scope::scoped_compute_pass`/`Scope::scoped_render_pass` 53 | * `wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS` is required to issue queries at any point within encoders. 54 | * `wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES` is required to issue queries at any point within passes. 55 | 56 | Wgpu-profiler needs to insert buffer copy commands, so when you're done with an encoder and won't do any more profiling scopes on it, you need to resolve the queries: 57 | ```rust 58 | profiler.resolve_queries(&mut encoder); 59 | ``` 60 | 61 | And finally, to end a profiling frame, call `end_frame`. This does a few checks and will let you know if something is off! 62 | ```rust 63 | profiler.end_frame().unwrap(); 64 | ``` 65 | 66 | Retrieving the oldest available frame and writing it out to a chrome trace file. 67 | ```rust 68 | if let Some(profiling_data) = profiler.process_finished_frame(queue.get_timestamp_period()) { 69 | wgpu_profiler::chrometrace::write_chrometrace(std::path::Path::new("mytrace.json"), &profiling_data); 70 | } 71 | ``` 72 | 73 | 74 | To get a look of it in action, check out the [example](./examples/demo.rs) project! 75 | 76 | ## License 77 | 78 | Licensed under either of 79 | 80 | * Apache License, Version 2.0 81 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 82 | * MIT license 83 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 84 | 85 | at your option. 86 | 87 | ## Contribution 88 | 89 | Unless you explicitly state otherwise, any contribution intentionally submitted 90 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 91 | dual licensed as above, without any additional terms or conditions. 92 | -------------------------------------------------------------------------------- /examples/compute_shader.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) 2 | @binding(0) 3 | var v_indices: array; // this is used as both input and output for convenience 4 | 5 | // The Collatz Conjecture states that for any integer n: 6 | // If n is even, n = n/2 7 | // If n is odd, n = 3n+1 8 | // And repeat this process for each new n, you will always eventually reach 1. 9 | // Though the conjecture has not been proven, no counterexample has ever been found. 10 | // This function returns how many times this recurrence needs to be applied to reach 1. 11 | fn collatz_iterations(n_base: u32) -> u32{ 12 | var n: u32 = n_base; 13 | var i: u32 = 0u; 14 | loop { 15 | if (n <= 1u) { 16 | break; 17 | } 18 | if (n % 2u == 0u) { 19 | n = n / 2u; 20 | } 21 | else { 22 | // Overflow? (i.e. 3*n + 1 > 0xffffffffu?) 23 | if (n >= 1431655765u) { // 0x55555555u 24 | return 4294967295u; // 0xffffffffu 25 | } 26 | 27 | n = 3u * n + 1u; 28 | } 29 | i = i + 1u; 30 | } 31 | return i; 32 | } 33 | 34 | @compute 35 | @workgroup_size(1) 36 | fn main(@builtin(global_invocation_id) global_id: vec3) { 37 | v_indices[global_id.x] = collatz_iterations(v_indices[global_id.x]); 38 | } 39 | -------------------------------------------------------------------------------- /examples/demo.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, sync::Arc}; 2 | use wgpu_profiler::{GpuProfiler, GpuProfilerSettings, GpuTimerQueryResult}; 3 | use winit::{ 4 | application::ApplicationHandler, 5 | event::WindowEvent, 6 | event_loop::EventLoop, 7 | keyboard::{KeyCode, PhysicalKey}, 8 | }; 9 | 10 | #[cfg(feature = "puffin")] 11 | // Since the timing information we get from WGPU may be several frames behind the CPU, we can't report these frames to 12 | // the singleton returned by `puffin::GlobalProfiler::lock`. Instead, we need our own `puffin::GlobalProfiler` that we 13 | // can be several frames behind puffin's main global profiler singleton. 14 | static PUFFIN_GPU_PROFILER: std::sync::LazyLock> = 15 | std::sync::LazyLock::new(|| std::sync::Mutex::new(puffin::GlobalProfiler::default())); 16 | 17 | fn scopes_to_console_recursive(results: &[GpuTimerQueryResult], indentation: u32) { 18 | for scope in results { 19 | if indentation > 0 { 20 | print!("{:>, enabled_features: wgpu::Features) { 40 | profiling::scope!("console_output"); 41 | print!("\x1B[2J\x1B[1;1H"); // Clear terminal and put cursor to first row first column 42 | println!("Welcome to wgpu_profiler demo!"); 43 | println!(); 44 | println!("Enabled device features: {:?}", enabled_features); 45 | println!(); 46 | println!( 47 | "Press space to write out a trace file that can be viewed in chrome's chrome://tracing" 48 | ); 49 | println!(); 50 | match results { 51 | Some(results) => { 52 | scopes_to_console_recursive(results, 0); 53 | } 54 | None => println!("No profiling results available yet!"), 55 | } 56 | } 57 | 58 | #[derive(Default)] 59 | struct State { 60 | window: Option>, 61 | gfx_state: Option, 62 | latest_profiler_results: Option>, 63 | } 64 | 65 | struct GfxState { 66 | surface: wgpu::Surface<'static>, 67 | surface_desc: wgpu::SurfaceConfiguration, 68 | device: wgpu::Device, 69 | queue: wgpu::Queue, 70 | render_pipeline: wgpu::RenderPipeline, 71 | profiler: GpuProfiler, 72 | } 73 | 74 | impl GfxState { 75 | async fn new(window: &Arc) -> Self { 76 | let size = window.inner_size(); 77 | let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); 78 | let surface = instance 79 | .create_surface(window.clone()) 80 | .expect("Failed to create surface."); 81 | let adapter = instance 82 | .request_adapter(&wgpu::RequestAdapterOptions { 83 | power_preference: wgpu::PowerPreference::default(), 84 | compatible_surface: Some(&surface), 85 | force_fallback_adapter: false, 86 | }) 87 | .await 88 | .expect("Failed to find an appropriate adapter"); 89 | 90 | dbg!(adapter.features()); 91 | 92 | let (device, queue) = adapter 93 | .request_device(&wgpu::DeviceDescriptor { 94 | required_features: adapter.features() & GpuProfiler::ALL_WGPU_TIMER_FEATURES, 95 | ..Default::default() 96 | }) 97 | .await 98 | .expect("Failed to create device"); 99 | 100 | let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 101 | label: None, 102 | source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))), 103 | }); 104 | 105 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 106 | label: None, 107 | bind_group_layouts: &[], 108 | push_constant_ranges: &[], 109 | }); 110 | 111 | let surface_desc = wgpu::SurfaceConfiguration { 112 | present_mode: wgpu::PresentMode::AutoVsync, 113 | ..surface 114 | .get_default_config(&adapter, size.width, size.height) 115 | .unwrap() 116 | }; 117 | surface.configure(&device, &surface_desc); 118 | 119 | let swapchain_format = surface_desc.format; 120 | 121 | let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 122 | label: None, 123 | layout: Some(&pipeline_layout), 124 | vertex: wgpu::VertexState { 125 | module: &shader, 126 | entry_point: Some("vs_main"), 127 | compilation_options: wgpu::PipelineCompilationOptions::default(), 128 | buffers: &[], 129 | }, 130 | fragment: Some(wgpu::FragmentState { 131 | module: &shader, 132 | entry_point: Some("fs_main"), 133 | compilation_options: wgpu::PipelineCompilationOptions::default(), 134 | targets: &[Some(swapchain_format.into())], 135 | }), 136 | primitive: wgpu::PrimitiveState::default(), 137 | depth_stencil: None, 138 | multisample: wgpu::MultisampleState::default(), 139 | multiview: None, 140 | cache: None, 141 | }); 142 | 143 | // Create a new profiler instance. 144 | #[cfg(feature = "tracy")] 145 | let profiler = GpuProfiler::new_with_tracy_client( 146 | GpuProfilerSettings::default(), 147 | adapter.get_info().backend, 148 | &device, 149 | &queue, 150 | ) 151 | .unwrap_or_else(|err| match err { 152 | wgpu_profiler::CreationError::TracyClientNotRunning 153 | | wgpu_profiler::CreationError::TracyGpuContextCreationError(_) => { 154 | println!("Failed to connect to Tracy. Continuing without Tracy integration."); 155 | GpuProfiler::new(&device, GpuProfilerSettings::default()) 156 | .expect("Failed to create profiler") 157 | } 158 | _ => { 159 | panic!("Failed to create profiler: {}", err); 160 | } 161 | }); 162 | 163 | #[cfg(not(feature = "tracy"))] 164 | let profiler = GpuProfiler::new(&device, GpuProfilerSettings::default()) 165 | .expect("Failed to create profiler"); 166 | 167 | Self { 168 | surface, 169 | surface_desc, 170 | device, 171 | queue, 172 | render_pipeline, 173 | profiler, 174 | } 175 | } 176 | } 177 | 178 | impl ApplicationHandler<()> for State { 179 | fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { 180 | if self.window.is_none() { 181 | let window = Arc::new( 182 | event_loop 183 | .create_window( 184 | winit::window::WindowAttributes::default().with_title("wgpu_profiler demo"), 185 | ) 186 | .expect("Failed to create window"), 187 | ); 188 | 189 | // Future versions of winit are supposed to be able to return a Future here for web support: 190 | // https://github.com/rust-windowing/winit/issues/3626#issuecomment-2097916252 191 | let gfx_state = futures_lite::future::block_on(GfxState::new(&window)); 192 | 193 | self.window = Some(window); 194 | self.gfx_state = Some(gfx_state); 195 | } 196 | } 197 | 198 | fn window_event( 199 | &mut self, 200 | event_loop: &winit::event_loop::ActiveEventLoop, 201 | _window_id: winit::window::WindowId, 202 | event: WindowEvent, 203 | ) { 204 | let Some(GfxState { 205 | surface, 206 | surface_desc, 207 | device, 208 | queue, 209 | render_pipeline, 210 | profiler, 211 | }) = self.gfx_state.as_mut() 212 | else { 213 | return; 214 | }; 215 | 216 | match event { 217 | WindowEvent::Resized(size) => { 218 | if size.width > 0 && size.height > 0 { 219 | surface_desc.width = size.width; 220 | surface_desc.height = size.height; 221 | surface.configure(device, surface_desc); 222 | } 223 | } 224 | WindowEvent::CloseRequested => { 225 | event_loop.exit(); 226 | } 227 | 228 | WindowEvent::RedrawRequested => { 229 | profiling::scope!("Redraw Requested"); 230 | 231 | let frame = surface 232 | .get_current_texture() 233 | .expect("Failed to acquire next surface texture"); 234 | let frame_view = frame 235 | .texture 236 | .create_view(&wgpu::TextureViewDescriptor::default()); 237 | let mut encoder = 238 | device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); 239 | 240 | draw(profiler, &mut encoder, &frame_view, render_pipeline); 241 | 242 | // Resolves any queries that might be in flight. 243 | profiler.resolve_queries(&mut encoder); 244 | 245 | { 246 | profiling::scope!("Submit"); 247 | queue.submit(Some(encoder.finish())); 248 | } 249 | { 250 | profiling::scope!("Present"); 251 | frame.present(); 252 | } 253 | 254 | profiling::finish_frame!(); 255 | 256 | // Signal to the profiler that the frame is finished. 257 | profiler.end_frame().unwrap(); 258 | // Query for oldest finished frame (this is almost certainly not the one we just submitted!) and display results in the command line. 259 | self.latest_profiler_results = 260 | profiler.process_finished_frame(queue.get_timestamp_period()); 261 | console_output(&self.latest_profiler_results, device.features()); 262 | #[cfg(feature = "puffin")] 263 | { 264 | let mut gpu_profiler = PUFFIN_GPU_PROFILER.lock().unwrap(); 265 | wgpu_profiler::puffin::output_frame_to_puffin( 266 | &mut gpu_profiler, 267 | self.latest_profiler_results.as_deref().unwrap_or_default(), 268 | ); 269 | gpu_profiler.new_frame(); 270 | } 271 | } 272 | 273 | WindowEvent::KeyboardInput { 274 | event: 275 | winit::event::KeyEvent { 276 | physical_key: PhysicalKey::Code(keycode), 277 | .. 278 | }, 279 | .. 280 | } => match keycode { 281 | KeyCode::Escape => { 282 | event_loop.exit(); 283 | } 284 | KeyCode::Space => { 285 | if let Some(profile_data) = &self.latest_profiler_results { 286 | wgpu_profiler::chrometrace::write_chrometrace( 287 | std::path::Path::new("trace.json"), 288 | profile_data, 289 | ) 290 | .expect("Failed to write trace.json"); 291 | } 292 | } 293 | _ => {} 294 | }, 295 | _ => (), 296 | }; 297 | } 298 | 299 | fn about_to_wait(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) { 300 | if let Some(window) = &self.window { 301 | // Continuous rendering! 302 | window.request_redraw(); 303 | } 304 | } 305 | } 306 | 307 | fn draw( 308 | profiler: &GpuProfiler, 309 | encoder: &mut wgpu::CommandEncoder, 310 | view: &wgpu::TextureView, 311 | render_pipeline: &wgpu::RenderPipeline, 312 | ) { 313 | // Create a new profiling scope that we nest the other scopes in. 314 | let mut scope = profiler.scope("rendering", encoder); 315 | // For demonstration purposes we divide our scene into two render passes. 316 | { 317 | // Once we created a scope, we can use it to create nested scopes within. 318 | // Note that the resulting scope fully owns the render pass. 319 | // But just as before, it behaves like a transparent wrapper, so you can use it just like a normal render pass. 320 | let mut rpass = scope.scoped_render_pass( 321 | "render pass top", 322 | wgpu::RenderPassDescriptor { 323 | label: None, 324 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { 325 | view, 326 | resolve_target: None, 327 | ops: wgpu::Operations { 328 | load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), 329 | store: wgpu::StoreOp::Store, 330 | }, 331 | })], 332 | ..Default::default() 333 | }, 334 | ); 335 | 336 | rpass.set_pipeline(render_pipeline); 337 | 338 | // Sub-scopes within the pass only work if wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES is enabled. 339 | // If this feature is lacking, no timings will be taken. 340 | { 341 | let mut rpass = rpass.scope("fractal 0"); 342 | rpass.draw(0..6, 0..1); 343 | }; 344 | { 345 | let mut rpass = rpass.scope("fractal 1"); 346 | rpass.draw(0..6, 1..2); 347 | } 348 | } 349 | { 350 | // It's also possible to take timings by hand, manually calling `begin_query` and `end_query`. 351 | // This is generally not recommended as it's very easy to mess up by accident :) 352 | let pass_scope = profiler 353 | .begin_pass_query("render pass bottom", scope.recorder) 354 | .with_parent(scope.scope.as_ref()); 355 | let mut rpass = scope 356 | .recorder 357 | .begin_render_pass(&wgpu::RenderPassDescriptor { 358 | label: None, 359 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { 360 | view, 361 | resolve_target: None, 362 | ops: wgpu::Operations { 363 | load: wgpu::LoadOp::Load, 364 | store: wgpu::StoreOp::Store, 365 | }, 366 | })], 367 | depth_stencil_attachment: None, 368 | occlusion_query_set: None, 369 | timestamp_writes: pass_scope.render_pass_timestamp_writes(), 370 | }); 371 | 372 | rpass.set_pipeline(render_pipeline); 373 | 374 | // Similarly, you can manually manage nested scopes within a render pass. 375 | // Again, to do any actual timing, you need to enable wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES. 376 | { 377 | let query = profiler 378 | .begin_query("fractal 2", &mut rpass) 379 | .with_parent(Some(&pass_scope)); 380 | rpass.draw(0..6, 2..3); 381 | 382 | // Don't forget to end the query! 383 | profiler.end_query(&mut rpass, query); 384 | } 385 | // Another variant is to use `ManualOwningScope`, forming a middle ground between no scope helpers and fully automatic scope closing. 386 | let mut rpass = { 387 | let mut rpass = profiler.manual_owning_scope("fractal 3", rpass); 388 | rpass.draw(0..6, 3..4); 389 | 390 | // Don't forget to end the scope. 391 | // Ending a `ManualOwningScope` will return the pass or encoder it owned. 392 | rpass.end_query() 393 | }; 394 | 395 | // Don't forget to end the scope. 396 | profiler.end_query(&mut rpass, pass_scope); 397 | } 398 | } 399 | 400 | fn main() { 401 | #[cfg(feature = "tracy")] 402 | tracy_client::Client::start(); 403 | 404 | //env_logger::init_from_env(env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "warn")); 405 | let event_loop = EventLoop::new().unwrap(); 406 | event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); 407 | 408 | #[cfg(feature = "puffin")] 409 | let (_cpu_server, _gpu_server) = { 410 | puffin::set_scopes_on(true); 411 | let cpu_server = 412 | puffin_http::Server::new(&format!("0.0.0.0:{}", puffin_http::DEFAULT_PORT)).unwrap(); 413 | let gpu_server = puffin_http::Server::new_custom( 414 | &format!("0.0.0.0:{}", puffin_http::DEFAULT_PORT + 1), 415 | |sink| PUFFIN_GPU_PROFILER.lock().unwrap().add_sink(sink), 416 | |id| _ = PUFFIN_GPU_PROFILER.lock().unwrap().remove_sink(id), 417 | ); 418 | (cpu_server, gpu_server) 419 | }; 420 | let _ = event_loop.run_app(&mut State::default()); 421 | } 422 | -------------------------------------------------------------------------------- /examples/shader.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @location(0) coord: vec2, 3 | @location(1) instance: f32, 4 | @builtin(position) pos: vec4, 5 | }; 6 | 7 | @vertex 8 | fn vs_main( 9 | @builtin(vertex_index) vertex_index: u32, 10 | @builtin(instance_index) instance_index: u32, 11 | ) -> VertexOutput { 12 | var out: VertexOutput; 13 | 14 | var x: f32 = f32(((vertex_index + 2u) / 3u) % 2u); 15 | var y: f32 = f32(((vertex_index + 1u) / 3u) % 2u); 16 | out.coord = vec2(x, y); 17 | 18 | x = x - f32(instance_index % 2u); 19 | y = y - f32(instance_index / 2u); 20 | out.pos = vec4(x, y, 0.0, 1.0); 21 | 22 | out.instance = f32(instance_index); 23 | 24 | return out; 25 | } 26 | 27 | @fragment 28 | fn fs_main(in: VertexOutput) -> @location(0) vec4 { 29 | var c: vec2 = vec2(-0.79, 0.15); 30 | if (in.instance == 0.0) { 31 | c = vec2(-1.476, 0.0); 32 | } 33 | if (in.instance == 1.0) { 34 | c = vec2(0.28, 0.008); 35 | } 36 | if (in.instance == 2.0) { 37 | c = vec2(-0.12, -0.77); 38 | } 39 | 40 | var max_iter: u32 = 200u; 41 | var z: vec2 = (in.coord.xy - vec2(0.5, 0.5)) * 3.0; 42 | 43 | var i: u32 = 0u; 44 | loop { 45 | if (i >= max_iter) { 46 | break; 47 | } 48 | z = vec2(z.x * z.x - z.y * z.y, z.x * z.y + z.y * z.x) + c; 49 | if (dot(z, z) > 4.0) { 50 | break; 51 | } 52 | continuing { 53 | i = i + 1u; 54 | } 55 | } 56 | 57 | var t: f32 = f32(i) / f32(max_iter); 58 | return vec4(t * 3.0, t * 3.0 - 1.0, t * 3.0 - 2.0, 1.0); 59 | //return vec4(in.coord.x, in.coord.y, 0.0, 1.0); 60 | } 61 | -------------------------------------------------------------------------------- /src/chrometrace.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Write, path::Path}; 2 | 3 | use crate::GpuTimerQueryResult; 4 | 5 | /// Writes a .json trace file that can be viewed as a flame graph in Chrome or Edge via 6 | pub fn write_chrometrace( 7 | target: &Path, 8 | profile_data: &[GpuTimerQueryResult], 9 | ) -> std::io::Result<()> { 10 | let mut file = File::create(target)?; 11 | 12 | writeln!(file, "{{")?; 13 | writeln!(file, "\"traceEvents\": [")?; 14 | 15 | if !profile_data.is_empty() { 16 | for child in profile_data.iter().take(profile_data.len() - 1) { 17 | write_results_recursive(&mut file, child, false)?; 18 | } 19 | write_results_recursive(&mut file, profile_data.last().unwrap(), true)?; 20 | } 21 | 22 | writeln!(file, "]")?; 23 | writeln!(file, "}}")?; 24 | 25 | Ok(()) 26 | } 27 | 28 | fn write_results_recursive( 29 | file: &mut File, 30 | result: &GpuTimerQueryResult, 31 | last: bool, 32 | ) -> std::io::Result<()> { 33 | let GpuTimerQueryResult { 34 | label, 35 | pid, 36 | tid, 37 | time, 38 | nested_queries, 39 | } = result; 40 | 41 | if let Some(time) = time { 42 | // note: ThreadIds are under the control of Rust’s standard library 43 | // and there may not be any relationship between ThreadId and the underlying platform’s notion of a thread identifier 44 | // 45 | // There's a proposal for stabilization of ThreadId::as_u64, which 46 | // would eliminate the need for this hack: https://github.com/rust-lang/rust/pull/110738 47 | // 48 | // for now, we use this hack to convert to integer 49 | let tid_to_int = |tid| { 50 | format!("{:?}", tid) 51 | .replace("ThreadId(", "") 52 | .replace(')', "") 53 | .parse::() 54 | .unwrap_or(u64::MAX) 55 | }; 56 | write!( 57 | file, 58 | r#"{{ "pid":{}, "tid":{}, "ts":{}, "dur":{}, "ph":"X", "name":"{}" }}{}"#, 59 | pid, 60 | tid_to_int(tid), 61 | time.start * 1000.0 * 1000.0, 62 | (time.end - time.start) * 1000.0 * 1000.0, 63 | label, 64 | if last && nested_queries.is_empty() { 65 | "\n" 66 | } else { 67 | ",\n" 68 | } 69 | )?; 70 | } 71 | if nested_queries.is_empty() { 72 | return Ok(()); 73 | } 74 | 75 | for child in nested_queries.iter().take(nested_queries.len() - 1) { 76 | write_results_recursive(file, child, false)?; 77 | } 78 | write_results_recursive(file, nested_queries.last().unwrap(), last)?; 79 | 80 | Ok(()) 81 | // { "pid":1, "tid":1, "ts":546867, "dur":121564, "ph":"X", "name":"DoThings" 82 | } 83 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | /// Errors that can occur during profiler creation. 2 | #[cfg_attr(not(feature = "tracy"), derive(PartialEq))] 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum CreationError { 5 | #[error(transparent)] 6 | InvalidSettings(#[from] SettingsError), 7 | 8 | #[cfg(feature = "tracy")] 9 | #[error("Tracy client doesn't run yet.")] 10 | TracyClientNotRunning, 11 | 12 | #[cfg(feature = "tracy")] 13 | #[error("Failed to create Tracy GPU context: {0}")] 14 | TracyGpuContextCreationError(#[from] tracy_client::GpuContextCreationError), 15 | } 16 | 17 | #[cfg(feature = "tracy")] 18 | impl PartialEq for CreationError { 19 | fn eq(&self, other: &Self) -> bool { 20 | match self { 21 | CreationError::InvalidSettings(left) => match other { 22 | CreationError::InvalidSettings(right) => left == right, 23 | _ => false, 24 | }, 25 | CreationError::TracyClientNotRunning => { 26 | matches!(other, CreationError::TracyClientNotRunning) 27 | } 28 | CreationError::TracyGpuContextCreationError(left) => match left { 29 | tracy_client::GpuContextCreationError::TooManyContextsCreated => matches!( 30 | other, 31 | CreationError::TracyGpuContextCreationError( 32 | tracy_client::GpuContextCreationError::TooManyContextsCreated 33 | ) 34 | ), 35 | }, 36 | } 37 | } 38 | } 39 | 40 | impl Eq for CreationError {} 41 | 42 | /// Errors that can occur during settings validation and change. 43 | #[derive(thiserror::Error, Debug, PartialEq, Eq)] 44 | pub enum SettingsError { 45 | #[error("GpuProfilerSettings::max_num_pending_frames must be at least 1.")] 46 | InvalidMaxNumPendingFrames, 47 | } 48 | 49 | /// Errors that can occur during [`crate::GpuProfiler::end_frame`]. 50 | #[derive(thiserror::Error, Debug, PartialEq, Eq)] 51 | pub enum EndFrameError { 52 | #[error("All profiling queries need to be closed before ending a frame. There were still {0} open queries.")] 53 | UnclosedQueries(u32), 54 | 55 | #[error( 56 | "Not all queries were resolved before ending a frame.\n 57 | Call `GpuProfiler::resolve_queries` after all profiling queries have been closed and before ending the frame.\n 58 | There were still {0} queries unresolved." 59 | )] 60 | UnresolvedQueries(u32), 61 | } 62 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | Easy to use profiler scopes for [wgpu](https://github.com/gfx-rs/wgpu) using timer queries. 4 | 5 | `wgpu_profiler` manages all the necessary [`wgpu::QuerySet`] and [`wgpu::Buffer`] behind the scenes 6 | and allows you to create to create timer scopes with minimal overhead! 7 | 8 | # How to use 9 | 10 | ``` 11 | use wgpu_profiler::*; 12 | 13 | # async fn wgpu_init() -> (wgpu::Instance, wgpu::Adapter, wgpu::Device, wgpu::Queue) { 14 | # let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); 15 | # let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions::default()).await.unwrap(); 16 | # let (device, queue) = adapter 17 | # .request_device(&wgpu::DeviceDescriptor { 18 | # required_features: wgpu::Features::TIMESTAMP_QUERY, 19 | # ..Default::default() 20 | # }) 21 | # .await 22 | # .unwrap(); 23 | # (instance, adapter, device, queue) 24 | # } 25 | # let (instance, adapter, device, queue) = futures_lite::future::block_on(wgpu_init()); 26 | # let cs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { 27 | # label: None, 28 | # source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!("../examples/compute_shader.wgsl"))), 29 | # }); 30 | # let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { 31 | # label: None, 32 | # layout: None, 33 | # module: &cs_module, 34 | # entry_point: Some("main"), 35 | # compilation_options: wgpu::PipelineCompilationOptions::default(), 36 | # cache: None, 37 | # }); 38 | // ... 39 | 40 | let mut profiler = GpuProfiler::new(&device, GpuProfilerSettings::default()).unwrap(); 41 | 42 | // ... 43 | 44 | # let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 45 | { 46 | // You can now open profiling scopes on any encoder or pass: 47 | let mut scope = profiler.scope("name of your scope", &mut encoder); 48 | 49 | // Scopes can be nested arbitrarily! 50 | let mut nested_scope = scope.scope("nested!"); 51 | 52 | // Scopes on encoders can be used to easily create profiled passes! 53 | let mut compute_pass = nested_scope.scoped_compute_pass("profiled compute"); 54 | 55 | 56 | // Scopes expose the underlying encoder or pass they wrap: 57 | compute_pass.set_pipeline(&pipeline); 58 | // ... 59 | 60 | // Scopes created this way are automatically closed when dropped. 61 | } 62 | 63 | // Wgpu-profiler needs to insert buffer copy commands. 64 | profiler.resolve_queries(&mut encoder); 65 | # drop(encoder); 66 | 67 | // ... 68 | 69 | // And finally, to end a profiling frame, call `end_frame`. 70 | // This does a few checks and will let you know if something is off! 71 | profiler.end_frame().unwrap(); 72 | 73 | // Retrieving the oldest available frame and writing it out to a chrome trace file. 74 | if let Some(profiling_data) = profiler.process_finished_frame(queue.get_timestamp_period()) { 75 | # let button_pressed = false; 76 | // You usually want to write to disk only under some condition, e.g. press of a key. 77 | if button_pressed { 78 | wgpu_profiler::chrometrace::write_chrometrace( 79 | std::path::Path::new("mytrace.json"), &profiling_data); 80 | } 81 | } 82 | ``` 83 | Check also the [Example](https://github.com/Wumpf/wgpu-profiler/blob/main/examples/demo.rs) where everything can be seen in action. 84 | 85 | ## Tracy integration 86 | 87 | If you want to use [tracy](https://github.com/wolfpld/tracy) for profiling, you can enable the `tracy` feature. 88 | 89 | This adds `wgpu_profiler::new_with_tracy_client` which will automatically report profiling data to tracy. 90 | 91 | For details check the example code. 92 | 93 | ## Puffin integration 94 | 95 | If you want to use [puffin](https://github.com/EmbarkStudios/puffin) for profiling, you can enable the `puffin` feature. 96 | 97 | This adds `wgpu_profiler::puffin::output_frame_to_puffin` which makes it easy to report profiling data to a `puffin::GlobalProfiler`. 98 | 99 | You can run the demo example with puffin by running `cargo run --example demo --features puffin`. 100 | All CPU profiling goes to port `8585`, all GPU profiling goes to port `8586`. You can open puffin viewers for both. 101 | ```sh 102 | puffin_viewer --url 127.0.0.1:8585 103 | puffin_viewer --url 127.0.0.1:8586 104 | ``` 105 | 106 | For details check the example code. 107 | 108 | # Internals 109 | 110 | For every frame that hasn't completely finished processing yet 111 | (i.e. hasn't returned results via [`GpuProfiler::process_finished_frame`]) 112 | we keep a `PendingFrame` around. 113 | 114 | Whenever a profiling scope is opened, we allocate two queries. 115 | This is done by either using the most recent `QueryPool` or creating a new one if there's no non-exhausted one ready. 116 | Ideally, we only ever need a single `QueryPool` per frame! In order to converge to this, 117 | we allocate new query pools with the size of all previous query pools in a given frame, effectively doubling the size. 118 | On [`GpuProfiler::end_frame`], we memorize the total size of all `QueryPool`s in the current frame and make this the new minimum pool size. 119 | 120 | `QueryPool` from finished frames are re-used, unless they are deemed too small. 121 | */ 122 | 123 | pub mod chrometrace; 124 | mod errors; 125 | mod profiler; 126 | mod profiler_command_recorder; 127 | mod profiler_query; 128 | mod profiler_settings; 129 | #[cfg(feature = "puffin")] 130 | pub mod puffin; 131 | mod scope; 132 | #[cfg(feature = "tracy")] 133 | mod tracy; 134 | 135 | pub use errors::{CreationError, EndFrameError, SettingsError}; 136 | pub use profiler::GpuProfiler; 137 | pub use profiler_command_recorder::ProfilerCommandRecorder; 138 | pub use profiler_query::{GpuProfilerQuery, GpuTimerQueryResult}; 139 | pub use profiler_settings::GpuProfilerSettings; 140 | pub use scope::{ManualOwningScope, OwningScope, Scope}; 141 | -------------------------------------------------------------------------------- /src/profiler.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{ 4 | atomic::{AtomicU32, Ordering}, 5 | Arc, 6 | }, 7 | }; 8 | 9 | use parking_lot::{Mutex, RwLock}; 10 | 11 | use crate::{ 12 | CreationError, EndFrameError, GpuProfilerQuery, GpuProfilerSettings, GpuTimerQueryResult, 13 | ManualOwningScope, OwningScope, ProfilerCommandRecorder, Scope, SettingsError, 14 | }; 15 | 16 | /// Profiler instance. 17 | /// 18 | /// You can have an arbitrary number of independent profiler instances per application/adapter. 19 | /// Manages all the necessary [`wgpu::QuerySet`] and [`wgpu::Buffer`] behind the scenes. 20 | /// 21 | /// Any query creation method may allocate a new [`wgpu::QuerySet`] and [`wgpu::Buffer`] internally if necessary. 22 | /// 23 | /// [`GpuProfiler`] is associated with a single [`wgpu::Device`] upon creation. 24 | /// All references wgpu objects passed in subsequent calls must originate from that device. 25 | pub struct GpuProfiler { 26 | device: wgpu::Device, 27 | 28 | unused_pools: Vec, 29 | 30 | active_frame: ActiveFrame, 31 | pending_frames: Vec, 32 | 33 | num_open_queries: AtomicU32, 34 | next_query_handle: AtomicU32, 35 | 36 | size_for_new_query_pools: u32, 37 | 38 | settings: GpuProfilerSettings, 39 | 40 | #[cfg(feature = "tracy")] 41 | tracy_context: Option, 42 | } 43 | 44 | // Public interface 45 | impl GpuProfiler { 46 | /// Combination of all timer query features [`GpuProfiler`] can leverage. 47 | pub const ALL_WGPU_TIMER_FEATURES: wgpu::Features = wgpu::Features::TIMESTAMP_QUERY 48 | .union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS) 49 | .union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES); 50 | 51 | /// Combination of all timer query features [`GpuProfiler`] can leverage. 52 | #[deprecated(since = "0.9.0", note = "Use ALL_WGPU_TIMER_FEATURES instead")] 53 | pub const REQUIRED_WGPU_FEATURES: wgpu::Features = GpuProfiler::ALL_WGPU_TIMER_FEATURES; 54 | 55 | /// Creates a new Profiler object. 56 | /// 57 | /// There is nothing preventing the use of several independent profiler objects. 58 | pub fn new( 59 | device: &wgpu::Device, 60 | settings: GpuProfilerSettings, 61 | ) -> Result { 62 | settings.validate()?; 63 | 64 | let (closed_scope_sender, closed_scope_receiver) = std::sync::mpsc::channel(); 65 | 66 | Ok(GpuProfiler { 67 | device: device.clone(), 68 | 69 | unused_pools: Vec::new(), 70 | 71 | pending_frames: Vec::with_capacity(settings.max_num_pending_frames), 72 | active_frame: ActiveFrame { 73 | query_pools: RwLock::new(PendingFramePools::default()), 74 | closed_query_sender: closed_scope_sender, 75 | closed_query_receiver: Mutex::new(closed_scope_receiver), 76 | }, 77 | 78 | num_open_queries: AtomicU32::new(0), 79 | next_query_handle: AtomicU32::new(0), 80 | 81 | size_for_new_query_pools: QueryPool::MIN_CAPACITY, 82 | 83 | settings, 84 | 85 | #[cfg(feature = "tracy")] 86 | tracy_context: None, 87 | }) 88 | } 89 | 90 | /// Creates a new profiler and connects to a running Tracy client. 91 | #[cfg(feature = "tracy")] 92 | pub fn new_with_tracy_client( 93 | settings: GpuProfilerSettings, 94 | backend: wgpu::Backend, 95 | device: &wgpu::Device, 96 | queue: &wgpu::Queue, 97 | ) -> Result { 98 | let mut profiler = Self::new(device, settings)?; 99 | profiler.tracy_context = Some(crate::tracy::create_tracy_gpu_client( 100 | backend, device, queue, 101 | )?); 102 | Ok(profiler) 103 | } 104 | 105 | /// Returns currently active settings. 106 | pub fn settings(&self) -> &GpuProfilerSettings { 107 | &self.settings 108 | } 109 | 110 | /// Changes the settings of an existing profiler. 111 | /// 112 | /// If timer scopes are disabled by setting [`GpuProfilerSettings::enable_timer_queries`] to false, 113 | /// any timer queries that are in flight will still be processed, 114 | /// but unused query sets and buffers will be deallocated during [`Self::process_finished_frame`]. 115 | /// Similarly, any opened debugging scope will still be closed if debug groups are disabled by setting 116 | /// [`GpuProfilerSettings::enable_debug_groups`] to false. 117 | pub fn change_settings(&mut self, settings: GpuProfilerSettings) -> Result<(), SettingsError> { 118 | settings.validate()?; 119 | if !settings.enable_timer_queries { 120 | self.unused_pools.clear(); 121 | } 122 | self.settings = settings; 123 | 124 | Ok(()) 125 | } 126 | 127 | /// Starts a new auto-closing profiler scope. 128 | /// 129 | /// To nest scopes inside this scope, call [`Scope::scope`] on the returned scope. 130 | /// 131 | /// If an [`wgpu::CommandEncoder`] is passed but the [`wgpu::Device`] 132 | /// does not support [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS`], no gpu timer will 133 | /// be queried and the scope will not show up in the final results. 134 | /// If an [`wgpu::ComputePass`] or [`wgpu::RenderPass`] is passed but the [`wgpu::Device`] 135 | /// does not support [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES`], no scope will be opened. 136 | /// 137 | /// If [`GpuProfilerSettings::enable_debug_groups`] is true, a debug group will be pushed on the encoder or pass. 138 | /// 139 | /// Scope is automatically closed on drop. 140 | #[must_use] 141 | #[track_caller] 142 | #[inline] 143 | pub fn scope<'a, Recorder: ProfilerCommandRecorder>( 144 | &'a self, 145 | label: impl Into, 146 | encoder_or_pass: &'a mut Recorder, 147 | ) -> Scope<'a, Recorder> { 148 | let scope = self.begin_query(label, encoder_or_pass); 149 | Scope { 150 | profiler: self, 151 | recorder: encoder_or_pass, 152 | scope: Some(scope), 153 | } 154 | } 155 | 156 | /// Starts a new auto-closing profiler scope that takes ownership of the passed encoder or rendering/compute pass. 157 | /// 158 | /// To nest scopes inside this scope, call [`OwningScope::scope`] on the returned scope. 159 | /// 160 | /// If an [`wgpu::CommandEncoder`] is passed but the [`wgpu::Device`] 161 | /// does not support [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS`], no gpu timer will be queried 162 | /// and the scope will not show up in the final results. 163 | /// If an [`wgpu::ComputePass`] or [`wgpu::RenderPass`] is passed but the [`wgpu::Device`] 164 | /// does not support [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES`], no scope will be opened. 165 | /// 166 | /// If [`GpuProfilerSettings::enable_debug_groups`] is true, a debug group will be pushed on the encoder or pass. 167 | /// 168 | /// Scope is automatically closed on drop. 169 | #[must_use] 170 | #[track_caller] 171 | #[inline] 172 | pub fn owning_scope( 173 | &'_ self, 174 | label: impl Into, 175 | mut encoder_or_pass: Recorder, 176 | ) -> OwningScope<'_, Recorder> { 177 | let scope = self.begin_query(label, &mut encoder_or_pass); 178 | OwningScope { 179 | profiler: self, 180 | recorder: encoder_or_pass, 181 | scope: Some(scope), 182 | } 183 | } 184 | 185 | /// Starts a new **manually closed** profiler scope that takes ownership of the passed encoder or rendering/compute pass. 186 | /// 187 | /// Does NOT call [`GpuProfiler::end_query()`] on drop. 188 | /// This construct is just for completeness in cases where working with scopes is preferred but one can't rely on the Drop call in the right place. 189 | /// This is useful when the owned value needs to be recovered after the end of the scope. 190 | /// In particular, to submit a [`wgpu::CommandEncoder`] to a queue, ownership of the encoder is necessary. 191 | /// 192 | /// To nest scopes inside this scope, call [`ManualOwningScope::scope`] on the returned scope. 193 | /// 194 | /// If an [`wgpu::CommandEncoder`] is passed but the [`wgpu::Device`] 195 | /// does not support [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS`], no gpu timer will be queried and the scope will 196 | /// not show up in the final results. 197 | /// If an [`wgpu::ComputePass`] or [`wgpu::RenderPass`] is passed but the [`wgpu::Device`] 198 | /// does not support [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES`], no scope will be opened. 199 | /// 200 | /// If [`GpuProfilerSettings::enable_debug_groups`] is true, a debug group will be pushed on the encoder or pass. 201 | #[must_use] 202 | #[track_caller] 203 | #[inline] 204 | pub fn manual_owning_scope( 205 | &self, 206 | label: impl Into, 207 | mut encoder_or_pass: Recorder, 208 | ) -> ManualOwningScope<'_, Recorder> { 209 | let scope = self.begin_query(label, &mut encoder_or_pass); 210 | ManualOwningScope { 211 | profiler: self, 212 | recorder: encoder_or_pass, 213 | scope: Some(scope), 214 | } 215 | } 216 | 217 | /// Starts a new profiler query on the given encoder or rendering/compute pass (if enabled). 218 | /// 219 | /// The returned query *must* be closed by calling [`GpuProfiler::end_query`] with the same encoder/pass, 220 | /// even if timer queries are disabled. 221 | /// To do this automatically, use [`GpuProfiler::scope`]/[`GpuProfiler::owning_scope`] instead. 222 | /// 223 | /// If an [`wgpu::CommandEncoder`] is passed but the [`wgpu::Device`] 224 | /// does not support [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS`], no gpu timer will be queried and the scope will 225 | /// not show up in the final results. 226 | /// If an [`wgpu::ComputePass`] or [`wgpu::RenderPass`] is passed but the [`wgpu::Device`] 227 | /// does not support [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES`], no timer queries will be allocated. 228 | /// 229 | /// If [`GpuProfilerSettings::enable_debug_groups`] is true, a debug group will be pushed on the encoder or pass. 230 | #[track_caller] 231 | #[must_use] 232 | pub fn begin_query( 233 | &self, 234 | label: impl Into, 235 | encoder_or_pass: &mut Recorder, 236 | ) -> GpuProfilerQuery { 237 | let is_for_pass_timestamp_writes = false; 238 | let mut query = 239 | self.begin_query_internal(label.into(), is_for_pass_timestamp_writes, encoder_or_pass); 240 | if let Some(timer_query) = &mut query.timer_query_pair { 241 | encoder_or_pass 242 | .write_timestamp(&timer_query.pool.query_set, timer_query.start_query_idx); 243 | timer_query.usage_state = QueryPairUsageState::OnlyStartWritten; 244 | }; 245 | 246 | if self.settings.enable_debug_groups { 247 | encoder_or_pass.push_debug_group(&query.label); 248 | query.has_debug_group = true; 249 | } 250 | query 251 | } 252 | 253 | /// Starts a new profiler query to be used for render/compute pass timestamp writes. 254 | /// 255 | /// The returned query *must* be closed by calling [`GpuProfiler::end_query`], even if timer queries are disabled. 256 | /// To do this automatically, use [`Scope::scoped_render_pass`]/[`Scope::scoped_compute_pass`] instead. 257 | /// 258 | /// Call [`GpuProfilerQuery::render_pass_timestamp_writes`] or [`GpuProfilerQuery::compute_pass_timestamp_writes`] 259 | /// to acquire the corresponding [`wgpu::RenderPassTimestampWrites`]/[`wgpu::ComputePassTimestampWrites`] object. 260 | /// 261 | /// If the [`wgpu::Device`] does not support [`wgpu::Features::TIMESTAMP_QUERY`], no gpu timer will be reserved. 262 | /// 263 | /// Unlike [`GpuProfiler::begin_query`] this will not create a debug scope, 264 | /// in order to not force passing of the same encoder/pass to [`GpuProfiler::end_query`]. 265 | /// (this is needed to relax resource tracking requirements a bit, making it easier to implement the automatic scopes) 266 | pub fn begin_pass_query( 267 | &self, 268 | label: impl Into, 269 | encoder: &mut wgpu::CommandEncoder, 270 | ) -> GpuProfilerQuery { 271 | let is_for_pass_timestamp_writes = true; 272 | let mut query = 273 | self.begin_query_internal(label.into(), is_for_pass_timestamp_writes, encoder); 274 | if let Some(timer_query) = &mut query.timer_query_pair { 275 | timer_query.usage_state = QueryPairUsageState::ReservedForPassTimestampWrites; 276 | } 277 | query 278 | } 279 | 280 | /// Ends passed query. 281 | /// 282 | /// If the passed query was opened with [`GpuProfiler::begin_query`], the passed encoder or pass must be the same 283 | /// as when the query was opened. 284 | pub fn end_query( 285 | &self, 286 | encoder_or_pass: &mut Recorder, 287 | mut query: GpuProfilerQuery, 288 | ) { 289 | if let Some(timer_query) = &mut query.timer_query_pair { 290 | match timer_query.usage_state { 291 | QueryPairUsageState::Reserved => { 292 | unreachable!("Query pair has been reserved but isn't used for anything!") 293 | } 294 | QueryPairUsageState::ReservedForPassTimestampWrites => { 295 | // No need to do a timestamp write, this is handled by wgpu. 296 | } 297 | QueryPairUsageState::OnlyStartWritten => { 298 | encoder_or_pass.write_timestamp( 299 | &timer_query.pool.query_set, 300 | timer_query.start_query_idx + 1, 301 | ); 302 | timer_query.usage_state = QueryPairUsageState::BothStartAndEndWritten; 303 | } 304 | QueryPairUsageState::BothStartAndEndWritten => { 305 | unreachable!("Query pair has already been used!") 306 | } 307 | } 308 | } 309 | 310 | #[cfg(feature = "tracy")] 311 | if let Some(ref mut tracy_scope) = query.tracy_scope { 312 | tracy_scope.end_zone(); 313 | } 314 | 315 | if query.has_debug_group { 316 | encoder_or_pass.pop_debug_group(); 317 | } 318 | 319 | let send_result = self.active_frame.closed_query_sender.send(query); 320 | 321 | // The only way we can fail sending the query is if the receiver has been dropped. 322 | // Since it sits on `active_frame` as well, there's no way for this to happen! 323 | debug_assert!(send_result.is_ok()); 324 | 325 | // Count queries even if we haven't processed this one, makes experiences more consistent 326 | // if there's a lack of support for some queries. 327 | self.num_open_queries.fetch_sub(1, Ordering::Release); 328 | } 329 | 330 | /// Puts query resolve commands in the encoder for all unresolved, pending queries of the active profiler frame. 331 | /// 332 | /// Note that you do *not* need to do this for every encoder, it is sufficient do do this once per frame as long 333 | /// as you submit the corresponding command buffer after all others that may have opened queries in the same frame. 334 | /// (It does not matter if the passed encoder itself has previously opened queries or not.) 335 | /// If you were to make this part of a command buffer that is enqueued before any other that has 336 | /// opened queries in the same profiling frame, no failure will occur but some timing results may be invalid. 337 | /// 338 | /// It is advised to call this only once at the end of a profiling frame, but it is safe to do so several times. 339 | /// 340 | /// 341 | /// Implementation note: 342 | /// This method could be made `&self`, taking the internal lock on the query pools. 343 | /// However, the intended use is to call this once at the end of a frame, so we instead 344 | /// encourage this explicit sync point and avoid the lock. 345 | pub fn resolve_queries(&mut self, encoder: &mut wgpu::CommandEncoder) { 346 | let query_pools = self.active_frame.query_pools.get_mut(); 347 | 348 | for query_pool in query_pools.used_pools.iter_mut() { 349 | // We sync with the last update of num_used_query (which has Release semantics) 350 | // mostly to be on the safe side - it happened inside a lock which gives it release semantics anyways 351 | // but the concern is that if we don't acquire here, we may miss on other side prior effects of the query begin. 352 | let num_used_queries = query_pool.num_used_queries.load(Ordering::Acquire); 353 | let num_resolved_queries = query_pool.num_resolved_queries.load(Ordering::Acquire); 354 | 355 | if num_resolved_queries == num_used_queries { 356 | continue; 357 | } 358 | 359 | debug_assert!(query_pool.capacity >= num_used_queries); 360 | debug_assert!(num_resolved_queries < num_used_queries); 361 | 362 | // Resolve into offset 0 of the resolve buffer - this way we don't have to worry about 363 | // the offset restrictions on resolve buffers (`wgpu::QUERY_RESOLVE_BUFFER_ALIGNMENT`) 364 | // and we copy it anyways. 365 | encoder.resolve_query_set( 366 | &query_pool.query_set, 367 | num_resolved_queries..num_used_queries, 368 | &query_pool.resolve_buffer, 369 | 0, 370 | ); 371 | // Copy the newly resolved queries into the read buffer, making sure 372 | // that we don't override any of the results that are already there. 373 | let destination_offset = (num_resolved_queries * wgpu::QUERY_SIZE) as u64; 374 | let copy_size = ((num_used_queries - num_resolved_queries) * wgpu::QUERY_SIZE) as u64; 375 | encoder.copy_buffer_to_buffer( 376 | &query_pool.resolve_buffer, 377 | 0, 378 | &query_pool.read_buffer, 379 | destination_offset, 380 | copy_size, 381 | ); 382 | 383 | query_pool 384 | .num_resolved_queries 385 | .store(num_used_queries, Ordering::Release); 386 | } 387 | } 388 | 389 | /// Marks the end of a frame. 390 | /// 391 | /// Needs to be called **after** submitting any encoder used in the current profiler frame. 392 | /// 393 | /// Fails if there are still open queries or unresolved queries. 394 | pub fn end_frame(&mut self) -> Result<(), EndFrameError> { 395 | let num_open_queries = self.num_open_queries.load(Ordering::Acquire); 396 | if num_open_queries != 0 { 397 | return Err(EndFrameError::UnclosedQueries(num_open_queries)); 398 | } 399 | 400 | let query_pools = self.active_frame.query_pools.get_mut(); 401 | 402 | let mut new_pending_frame = PendingFrame { 403 | query_pools: std::mem::take(&mut query_pools.used_pools), 404 | closed_query_by_parent_handle: HashMap::new(), 405 | mapped_buffers: Arc::new(AtomicU32::new(0)), 406 | }; 407 | 408 | for query in self.active_frame.closed_query_receiver.get_mut().try_iter() { 409 | new_pending_frame 410 | .closed_query_by_parent_handle 411 | .entry(query.parent_handle) 412 | .or_default() 413 | .push(query); 414 | } 415 | 416 | // All loads of pool.num_used_queries are Relaxed since we assume, 417 | // that we already acquired the state during `resolve_queries` and no further otherwise unobserved 418 | // modifications happened since then. 419 | 420 | let num_unresolved_queries = new_pending_frame 421 | .query_pools 422 | .iter() 423 | .map(|pool| { 424 | pool.num_used_queries.load(Ordering::Relaxed) 425 | - pool.num_resolved_queries.load(Ordering::Relaxed) 426 | }) 427 | .sum(); 428 | if num_unresolved_queries != 0 { 429 | return Err(EndFrameError::UnresolvedQueries(num_unresolved_queries)); 430 | } 431 | 432 | // Next time we create a new query pool, we want it to be at least as big to hold all queries of this frame. 433 | self.size_for_new_query_pools = self 434 | .size_for_new_query_pools 435 | .max( 436 | new_pending_frame 437 | .query_pools 438 | .iter() 439 | .map(|pool| pool.num_used_queries.load(Ordering::Relaxed)) 440 | .sum(), 441 | ) 442 | .min(QUERY_SET_MAX_QUERIES); 443 | 444 | // Make sure we don't overflow. 445 | if self.pending_frames.len() == self.settings.max_num_pending_frames { 446 | // Drop previous (!) frame. 447 | // Dropping the oldest frame could get us into an endless cycle where we're never able to complete 448 | // any pending frames as the ones closest to completion would be evicted. 449 | if let Some(dropped_frame) = self.pending_frames.pop() { 450 | // Drop queries first since they still have references to the query pools that we want to reuse. 451 | drop(dropped_frame.closed_query_by_parent_handle); 452 | 453 | // Mark the frame as dropped. We'll give back the query pools once the mapping is done. 454 | // Any previously issued map_async call that haven't finished yet, will invoke their callback with mapping abort. 455 | self.reset_and_cache_unused_query_pools(dropped_frame.query_pools); 456 | } 457 | } 458 | 459 | // Map all buffers. 460 | for pool in new_pending_frame.query_pools.iter_mut() { 461 | let mapped_buffers = new_pending_frame.mapped_buffers.clone(); 462 | pool.read_buffer 463 | .slice(0..(pool.num_used_queries.load(Ordering::Relaxed) * wgpu::QUERY_SIZE) as u64) 464 | .map_async(wgpu::MapMode::Read, move |mapping_result| { 465 | // Mapping should not fail unless it was cancelled due to the frame being dropped. 466 | match mapping_result { 467 | Err(_) => { 468 | // We only want to ignore the error iff the mapping has been aborted by us (due to a dropped frame, see above). 469 | // In any other case, we need should panic as this would imply something went seriously sideways. 470 | // 471 | // As of writing, this is not yet possible in wgpu, see https://github.com/gfx-rs/wgpu/pull/2939 472 | } 473 | Ok(()) => { 474 | mapped_buffers.fetch_add(1, std::sync::atomic::Ordering::Release); 475 | } 476 | } 477 | }); 478 | } 479 | 480 | // Enqueue 481 | self.pending_frames.push(new_pending_frame); 482 | assert!(self.pending_frames.len() <= self.settings.max_num_pending_frames); 483 | 484 | Ok(()) 485 | } 486 | 487 | /// Checks if all timer queries for the oldest pending finished frame are done and returns that snapshot if any. 488 | /// 489 | /// `timestamp_period`: 490 | /// The timestamp period of the device. Pass the result of [`wgpu::Queue::get_timestamp_period()`]. 491 | /// Note that some implementations (Chrome as of writing) may converge to a timestamp period while the application is running, 492 | /// so caching this value is usually not recommended. 493 | pub fn process_finished_frame( 494 | &mut self, 495 | timestamp_period: f32, 496 | ) -> Option> { 497 | let frame = self.pending_frames.first_mut()?; 498 | 499 | // We only process if all mappings succeed. 500 | if frame 501 | .mapped_buffers 502 | .load(std::sync::atomic::Ordering::Acquire) 503 | != frame.query_pools.len() as u32 504 | { 505 | return None; 506 | } 507 | 508 | let PendingFrame { 509 | query_pools, 510 | mut closed_query_by_parent_handle, 511 | mapped_buffers: _, 512 | } = self.pending_frames.remove(0); 513 | 514 | let results = { 515 | let timestamp_to_sec = timestamp_period as f64 / 1000.0 / 1000.0 / 1000.0; 516 | 517 | Self::process_timings_recursive( 518 | timestamp_to_sec, 519 | &mut closed_query_by_parent_handle, 520 | ROOT_QUERY_HANDLE, 521 | ) 522 | }; 523 | 524 | // Ensure that closed queries no longer hold references to the query pools. 525 | // `process_timings_recursive` should have handled this already. 526 | debug_assert!(closed_query_by_parent_handle.is_empty()); 527 | drop(closed_query_by_parent_handle); // But just in case, we make sure to drop it here even if above debug assertion fails. 528 | 529 | self.reset_and_cache_unused_query_pools(query_pools); 530 | 531 | Some(results) 532 | } 533 | } 534 | 535 | // -------------------------------------------------------------------------------- 536 | // Internals 537 | // -------------------------------------------------------------------------------- 538 | 539 | const QUERY_SET_MAX_QUERIES: u32 = wgpu::QUERY_SET_MAX_QUERIES; 540 | 541 | /// Returns true if a timestamp query is supported. 542 | fn timestamp_query_support( 543 | is_for_pass_timestamp_writes: bool, 544 | encoder_or_pass: &mut Recorder, 545 | features: wgpu::Features, 546 | ) -> bool { 547 | let required_feature = if is_for_pass_timestamp_writes { 548 | wgpu::Features::TIMESTAMP_QUERY 549 | } else if encoder_or_pass.is_pass() { 550 | wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES 551 | } else { 552 | wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS 553 | }; 554 | features.contains(required_feature) 555 | } 556 | 557 | impl GpuProfiler { 558 | fn next_scope_tree_handle(&self) -> GpuTimerQueryTreeHandle { 559 | // Relaxed is fine, we just want a number that nobody uses this frame already. 560 | let mut handle = self.next_query_handle.fetch_add(1, Ordering::Relaxed); 561 | 562 | // We don't ever expect to run out of handles during a single frame, but who knows how long the app runs. 563 | while handle == ROOT_QUERY_HANDLE { 564 | handle = self.next_query_handle.fetch_add(1, Ordering::Relaxed); 565 | } 566 | 567 | handle 568 | } 569 | 570 | fn reset_and_cache_unused_query_pools(&mut self, mut discarded_pools: Vec>) { 571 | let capacity_threshold = self.size_for_new_query_pools / 2; 572 | for pool in discarded_pools.drain(..) { 573 | // If the pool is truly unused now, it's ref count should be 1! 574 | // If we use it anywhere else we have an implementation bug. 575 | let mut pool = Arc::into_inner(pool).expect("Pool still in use"); 576 | pool.reset(); 577 | 578 | // If a pool was less than half of the size of the max frame, then we don't keep it. 579 | // This way we're going to need less pools in upcoming frames and thus have less overhead in the long run. 580 | // If timer queries were disabled, we also don't keep any pools. 581 | if self.settings.enable_timer_queries && pool.capacity >= capacity_threshold { 582 | self.active_frame 583 | .query_pools 584 | .get_mut() 585 | .unused_pools 586 | .push(pool); 587 | } 588 | } 589 | } 590 | 591 | fn try_reserve_query_pair(pool: &Arc) -> Option { 592 | let mut num_used_queries = pool.num_used_queries.load(Ordering::Relaxed); 593 | 594 | loop { 595 | if pool.capacity < num_used_queries + 2 { 596 | // This pool is out of capacity, we failed the operation. 597 | return None; 598 | } 599 | 600 | match pool.num_used_queries.compare_exchange_weak( 601 | num_used_queries, 602 | num_used_queries + 2, 603 | // Write to num_used_queries with release semantics to be on the safe side. 604 | // (It doesn't look like there's other side effects that we need to publish.) 605 | Ordering::Release, 606 | // No barrier for the failure case. 607 | // The only thing we have to acquire is the pool's capacity which is constant and 608 | // was definitely acquired by the RWLock prior to this call. 609 | Ordering::Relaxed, 610 | ) { 611 | Ok(_) => { 612 | // We successfully acquired two queries! 613 | return Some(ReservedTimerQueryPair { 614 | pool: pool.clone(), 615 | start_query_idx: num_used_queries, 616 | usage_state: QueryPairUsageState::Reserved, 617 | }); 618 | } 619 | Err(updated) => { 620 | // Someone else acquired queries in the meantime, try again. 621 | num_used_queries = updated; 622 | } 623 | } 624 | } 625 | } 626 | 627 | // Reserves two query objects. 628 | // Our query pools always have an even number of queries, so we know the next query is the next in the same pool. 629 | fn reserve_query_pair(&self) -> ReservedTimerQueryPair { 630 | // First, try to allocate from current top pool. 631 | // Requires taking a read lock on the current query pool. 632 | { 633 | let query_pools = self.active_frame.query_pools.read(); 634 | if let Some(pair) = query_pools 635 | .used_pools 636 | .last() 637 | .and_then(Self::try_reserve_query_pair) 638 | { 639 | return pair; 640 | } 641 | } 642 | // If this didn't work, we may need to add a new pool. 643 | // Requires taking a write lock on the current query pool. 644 | { 645 | let mut query_pools = self.active_frame.query_pools.write(); 646 | 647 | // It could be that by now, another thread has already added a new pool! 648 | // This is a bit unfortunate because it means we unnecessarily took a write lock, but it seems hard to get around this. 649 | if let Some(pair) = query_pools 650 | .used_pools 651 | .last() 652 | .and_then(Self::try_reserve_query_pair) 653 | { 654 | return pair; 655 | } 656 | 657 | // Now we know for certain that the last pool is exhausted, so add a new one! 658 | let new_pool = if let Some(reused_pool) = query_pools.unused_pools.pop() { 659 | // First check if there's an unused pool we can take. 660 | Arc::new(reused_pool) 661 | } else { 662 | // If we can't, create a new pool that is as big as all previous pools combined. 663 | Arc::new(QueryPool::new( 664 | query_pools 665 | .used_pools 666 | .iter() 667 | .map(|pool| pool.capacity) 668 | .sum::() 669 | .max(self.size_for_new_query_pools) 670 | .min(QUERY_SET_MAX_QUERIES), 671 | &self.device, 672 | )) 673 | }; 674 | 675 | let pair = Self::try_reserve_query_pair(&new_pool) 676 | .expect("Freshly reserved pool doesn't have enough capacity"); 677 | query_pools.used_pools.push(new_pool); 678 | 679 | pair 680 | } 681 | } 682 | 683 | #[track_caller] 684 | #[must_use] 685 | fn begin_query_internal( 686 | &self, 687 | label: String, 688 | is_for_pass_timestamp_writes: bool, 689 | encoder_or_pass: &mut Recorder, 690 | ) -> GpuProfilerQuery { 691 | // Give opening/closing queries acquire/release semantics: 692 | // This way, we won't get any nasty surprises when observing zero open queries. 693 | self.num_open_queries.fetch_add(1, Ordering::Acquire); 694 | 695 | let query = if self.settings.enable_timer_queries 696 | && timestamp_query_support( 697 | is_for_pass_timestamp_writes, 698 | encoder_or_pass, 699 | self.device.features(), 700 | ) { 701 | Some(self.reserve_query_pair()) 702 | } else { 703 | None 704 | }; 705 | 706 | let _tracy_scope = if self.settings.enable_timer_queries { 707 | #[cfg(feature = "tracy")] 708 | { 709 | let location = std::panic::Location::caller(); 710 | self.tracy_context.as_ref().and_then(|c| { 711 | c.span_alloc(&label, "", location.file(), location.line()) 712 | .ok() 713 | }) 714 | } 715 | #[cfg(not(feature = "tracy"))] 716 | Option::<()>::None 717 | } else { 718 | None 719 | }; 720 | 721 | let pid = if cfg!(target_arch = "wasm32") { 722 | 0 723 | } else { 724 | std::process::id() 725 | }; 726 | 727 | GpuProfilerQuery { 728 | label, 729 | pid, 730 | tid: std::thread::current().id(), 731 | timer_query_pair: query, 732 | handle: self.next_scope_tree_handle(), 733 | parent_handle: ROOT_QUERY_HANDLE, 734 | has_debug_group: false, 735 | #[cfg(feature = "tracy")] 736 | tracy_scope: _tracy_scope, 737 | } 738 | } 739 | 740 | fn process_timings_recursive( 741 | timestamp_to_sec: f64, 742 | closed_scope_by_parent_handle: &mut HashMap>, 743 | parent_handle: GpuTimerQueryTreeHandle, 744 | ) -> Vec { 745 | let Some(queries_with_same_parent) = closed_scope_by_parent_handle.remove(&parent_handle) 746 | else { 747 | return Vec::new(); 748 | }; 749 | 750 | queries_with_same_parent 751 | .into_iter() 752 | .map(|mut scope| { 753 | // Note that inactive queries may still have nested queries, it's therefore important we process all of them. 754 | // In particular, this happens if only `wgpu::Features::TIMESTAMP_QUERY`` is enabled and `timestamp_writes` 755 | // on passes are nested inside inactive encoder timer queries. 756 | let time_raw = scope.timer_query_pair.take().map(|query| { 757 | // Read timestamp from buffer. 758 | // By design timestamps for start/end are consecutive. 759 | let offset = (query.start_query_idx * wgpu::QUERY_SIZE) as u64; 760 | let buffer_slice = &query 761 | .pool 762 | .read_buffer 763 | .slice(offset..(offset + (wgpu::QUERY_SIZE * 2) as u64)) 764 | .get_mapped_range(); 765 | let start_raw = u64::from_le_bytes( 766 | buffer_slice[0..wgpu::QUERY_SIZE as usize] 767 | .try_into() 768 | .unwrap(), 769 | ); 770 | let end_raw = u64::from_le_bytes( 771 | buffer_slice[wgpu::QUERY_SIZE as usize..(wgpu::QUERY_SIZE as usize) * 2] 772 | .try_into() 773 | .unwrap(), 774 | ); 775 | 776 | start_raw..end_raw 777 | }); 778 | 779 | let time = time_raw.as_ref().map(|time_raw| { 780 | (time_raw.start as f64 * timestamp_to_sec) 781 | ..(time_raw.end as f64 * timestamp_to_sec) 782 | }); 783 | 784 | #[cfg(feature = "tracy")] 785 | if let (Some(tracy_scope), Some(time_raw)) = (&scope.tracy_scope, &time_raw) { 786 | tracy_scope.upload_timestamp_start(time_raw.start as i64); 787 | } 788 | 789 | let nested_queries = Self::process_timings_recursive( 790 | timestamp_to_sec, 791 | closed_scope_by_parent_handle, 792 | scope.handle, 793 | ); 794 | 795 | #[cfg(feature = "tracy")] 796 | if let (Some(tracy_scope), Some(time_raw)) = (&scope.tracy_scope, time_raw) { 797 | tracy_scope.upload_timestamp_end(time_raw.end as i64); 798 | } 799 | 800 | GpuTimerQueryResult { 801 | label: std::mem::take(&mut scope.label), 802 | time, 803 | nested_queries, 804 | pid: scope.pid, 805 | tid: scope.tid, 806 | } 807 | }) 808 | .collect::>() 809 | } 810 | } 811 | 812 | #[derive(PartialEq, Eq)] 813 | pub enum QueryPairUsageState { 814 | /// Transitional state used upon creation. 815 | Reserved, 816 | 817 | /// Don't do manual timestamp writes, wgpu is expected to do them for us. 818 | ReservedForPassTimestampWrites, 819 | 820 | /// Start query has been used, end query is still available. 821 | OnlyStartWritten, 822 | 823 | /// Both start & end query have been used. 824 | BothStartAndEndWritten, 825 | } 826 | 827 | pub struct ReservedTimerQueryPair { 828 | /// [`QueryPool`] on which both start & end queries of the scope are done. 829 | /// 830 | /// By putting an arc here instead of an index into a vec, we don't need 831 | /// need to take any locks upon closing a profiling scope. 832 | pub pool: Arc, 833 | 834 | /// Query index at which the scope begins. 835 | /// The query after this is reserved for the end of the scope. 836 | pub start_query_idx: u32, 837 | 838 | /// Current use of the query pair. 839 | pub usage_state: QueryPairUsageState, 840 | } 841 | 842 | /// A pool of queries, consisting of a single queryset & buffer for query results. 843 | #[derive(Debug)] 844 | pub struct QueryPool { 845 | pub query_set: wgpu::QuerySet, 846 | 847 | resolve_buffer: wgpu::Buffer, 848 | read_buffer: wgpu::Buffer, 849 | 850 | capacity: u32, 851 | num_used_queries: AtomicU32, 852 | num_resolved_queries: AtomicU32, 853 | } 854 | 855 | impl QueryPool { 856 | const MIN_CAPACITY: u32 = 32; 857 | 858 | fn new(capacity: u32, device: &wgpu::Device) -> Self { 859 | QueryPool { 860 | query_set: device.create_query_set(&wgpu::QuerySetDescriptor { 861 | label: Some("GpuProfiler - Query Set"), 862 | ty: wgpu::QueryType::Timestamp, 863 | count: capacity, 864 | }), 865 | 866 | resolve_buffer: device.create_buffer(&wgpu::BufferDescriptor { 867 | label: Some("GpuProfiler - Query Resolve Buffer"), 868 | size: (wgpu::QUERY_SIZE * capacity) as u64, 869 | usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC, 870 | mapped_at_creation: false, 871 | }), 872 | 873 | read_buffer: device.create_buffer(&wgpu::BufferDescriptor { 874 | label: Some("GpuProfiler - Query Read Buffer"), 875 | size: (wgpu::QUERY_SIZE * capacity) as u64, 876 | usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, 877 | mapped_at_creation: false, 878 | }), 879 | 880 | capacity, 881 | num_used_queries: AtomicU32::new(0), 882 | num_resolved_queries: AtomicU32::new(0), 883 | } 884 | } 885 | 886 | fn reset(&mut self) { 887 | self.num_used_queries = AtomicU32::new(0); 888 | self.num_resolved_queries = AtomicU32::new(0); 889 | self.read_buffer.unmap(); 890 | } 891 | } 892 | 893 | #[derive(Default)] 894 | struct PendingFramePools { 895 | /// List of all pools used in this frame. 896 | /// The last pool is the one new profiling queries will try to make timer queries into. 897 | used_pools: Vec>, 898 | 899 | /// List of unused pools recycled from previous frames. 900 | unused_pools: Vec, 901 | } 902 | 903 | /// Internal handle to building a tree of profiling queries. 904 | pub type GpuTimerQueryTreeHandle = u32; 905 | 906 | /// Handle for the root scope. 907 | pub const ROOT_QUERY_HANDLE: GpuTimerQueryTreeHandle = u32::MAX; 908 | 909 | struct ActiveFrame { 910 | query_pools: RwLock, 911 | 912 | /// Closed queries get send to this channel. 913 | /// 914 | /// Note that channel is still overkill for what we want here: 915 | /// We're in a multi producer situation, *but* the single consumer is known to be only 916 | /// active in a mut context, i.e. while we're consuming we know that we're not producing. 917 | /// We have to wrap it in a Mutex because the channel is not Sync, but we actually never lock it 918 | /// since we only ever access it in a `mut` context. 919 | closed_query_sender: std::sync::mpsc::Sender, 920 | closed_query_receiver: Mutex>, 921 | } 922 | 923 | struct PendingFrame { 924 | query_pools: Vec>, 925 | closed_query_by_parent_handle: HashMap>, 926 | 927 | /// Keeps track of the number of buffers in the query pool that have been mapped successfully. 928 | mapped_buffers: std::sync::Arc, 929 | } 930 | -------------------------------------------------------------------------------- /src/profiler_command_recorder.rs: -------------------------------------------------------------------------------- 1 | /// Trait for exposing the methods of `wgpu::CommandEncoder`, `wgpu::RenderPass` and `wgpu::ComputePass` that are used by the profiler. 2 | pub trait ProfilerCommandRecorder { 3 | /// Returns `true` if it's a pass or `false` if it's an encoder 4 | fn is_pass(&self) -> bool; 5 | fn write_timestamp(&mut self, query_set: &wgpu::QuerySet, query_index: u32); 6 | fn push_debug_group(&mut self, label: &str); 7 | fn pop_debug_group(&mut self); 8 | } 9 | 10 | macro_rules! ImplProfilerCommandRecorder { 11 | ($($name:ident $(< $lt:lifetime >)? : $pass:literal,)*) => { 12 | $( 13 | impl $(< $lt >)? ProfilerCommandRecorder for wgpu::$name $(< $lt >)? { 14 | fn is_pass(&self) -> bool { $pass } 15 | 16 | fn write_timestamp(&mut self, query_set: &wgpu::QuerySet, query_index: u32) { 17 | self.write_timestamp(query_set, query_index) 18 | } 19 | 20 | fn push_debug_group(&mut self, label: &str) { 21 | self.push_debug_group(label) 22 | } 23 | 24 | fn pop_debug_group(&mut self) { 25 | self.pop_debug_group() 26 | } 27 | } 28 | )* 29 | }; 30 | } 31 | 32 | ImplProfilerCommandRecorder!(CommandEncoder:false, RenderPass<'a>:true, ComputePass<'a>:true,); 33 | -------------------------------------------------------------------------------- /src/profiler_query.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::Range, thread::ThreadId}; 2 | 3 | use crate::profiler::{ 4 | GpuTimerQueryTreeHandle, QueryPairUsageState, ReservedTimerQueryPair, ROOT_QUERY_HANDLE, 5 | }; 6 | 7 | /// The result of a gpu timer scope. 8 | #[derive(Debug, Clone)] 9 | pub struct GpuTimerQueryResult { 10 | /// Label that was specified when opening the scope. 11 | pub label: String, 12 | 13 | /// The process id of the process that opened this scope. 14 | pub pid: u32, 15 | 16 | /// The thread id of the thread that opened this scope. 17 | pub tid: ThreadId, 18 | 19 | /// Time range of this scope in seconds. 20 | /// 21 | /// Meaning of absolute value is not defined. 22 | /// If timestamp writing was disabled for this scope, this is None. 23 | pub time: Option>, 24 | 25 | /// Scopes that were opened while this scope was open. 26 | pub nested_queries: Vec, 27 | } 28 | 29 | /// An inflight query for the profiler. 30 | /// 31 | /// If timer queries are enabled, this represents a reserved timer query pair on 32 | /// one of the profiler's query sets. 33 | /// *Must* be closed by calling [`GpuProfiler::end_query`]. 34 | /// 35 | /// Emitted by [`GpuProfiler::begin_query`]/[`GpuProfiler::begin_pass_query`] and consumed by [`GpuProfiler::end_query`]. 36 | /// 37 | /// [`GpuProfiler::begin_pass_query`]: crate::GpuProfiler::begin_pass_query 38 | /// [`GpuProfiler::begin_query`]: crate::GpuProfiler::begin_query 39 | /// [`GpuProfiler::end_query`]: crate::GpuProfiler::end_query 40 | pub struct GpuProfilerQuery { 41 | /// The label assigned to this query. 42 | /// Will be moved into [`GpuProfilerQuery::label`] once the query is fully processed. 43 | pub label: String, 44 | 45 | /// The process id of the process that opened this query. 46 | pub pid: u32, 47 | 48 | /// The thread id of the thread that opened this query. 49 | pub tid: ThreadId, 50 | 51 | /// The actual query on a query pool if any (none if disabled for this type of query). 52 | pub(crate) timer_query_pair: Option, 53 | 54 | /// Handle which identifies this query, used for building the tree of queries. 55 | pub(crate) handle: GpuTimerQueryTreeHandle, 56 | 57 | /// Which query this query is a child of. 58 | pub(crate) parent_handle: GpuTimerQueryTreeHandle, 59 | 60 | /// Whether a debug group was opened for this scope. 61 | pub(crate) has_debug_group: bool, 62 | 63 | #[cfg(feature = "tracy")] 64 | pub(crate) tracy_scope: Option, 65 | } 66 | 67 | impl GpuProfilerQuery { 68 | /// Use the reserved query for render pass timestamp writes if any. 69 | /// 70 | /// Use this only for a single render/compute pass, otherwise results will be overwritten. 71 | /// Only ever returns `Some` for queries that were created using [`GpuProfiler::begin_pass_query`]. 72 | /// 73 | /// [`GpuProfiler::begin_pass_query`]: crate::GpuProfiler::begin_pass_query 74 | pub fn render_pass_timestamp_writes(&self) -> Option { 75 | self.timer_query_pair.as_ref().and_then(|query| { 76 | (query.usage_state == QueryPairUsageState::ReservedForPassTimestampWrites).then(|| { 77 | wgpu::RenderPassTimestampWrites { 78 | query_set: &query.pool.query_set, 79 | beginning_of_pass_write_index: Some(query.start_query_idx), 80 | end_of_pass_write_index: Some(query.start_query_idx + 1), 81 | } 82 | }) 83 | }) 84 | } 85 | 86 | /// Use the reserved query for compute pass timestamp writes if any. 87 | /// 88 | /// Use this only for a single render/compute pass, otherwise results will be overwritten. 89 | /// Only ever returns `Some` for queries that were created using [`GpuProfiler::begin_pass_query`]. 90 | /// 91 | /// [`GpuProfiler::begin_pass_query`]: crate::GpuProfiler::begin_pass_query 92 | pub fn compute_pass_timestamp_writes(&self) -> Option { 93 | self.timer_query_pair.as_ref().and_then(|query| { 94 | (query.usage_state == QueryPairUsageState::ReservedForPassTimestampWrites).then(|| { 95 | wgpu::ComputePassTimestampWrites { 96 | query_set: &query.pool.query_set, 97 | beginning_of_pass_write_index: Some(query.start_query_idx), 98 | end_of_pass_write_index: Some(query.start_query_idx + 1), 99 | } 100 | }) 101 | }) 102 | } 103 | 104 | /// Makes this scope a child of the passed scope. 105 | #[inline] 106 | pub fn with_parent(self, parent: Option<&GpuProfilerQuery>) -> Self { 107 | Self { 108 | parent_handle: parent.map_or(ROOT_QUERY_HANDLE, |p| p.handle), 109 | ..self 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/profiler_settings.rs: -------------------------------------------------------------------------------- 1 | use crate::SettingsError; 2 | 3 | /// Settings passed on initialization of [`GpuProfiler`](crate::GpuProfiler). 4 | #[derive(Debug, Clone)] 5 | pub struct GpuProfilerSettings { 6 | /// Enables/disables gpu timer queries. 7 | /// 8 | /// If false, the profiler will not emit any timer queries, making most operations on [`GpuProfiler`](crate::GpuProfiler) no-ops. 9 | /// 10 | /// Since all resource creation is done lazily, this provides an effective way of disabling the profiler at runtime 11 | /// without the need of special build configurations or code to handle enabled/disabled profiling. 12 | pub enable_timer_queries: bool, 13 | 14 | /// Enables/disables debug markers for all scopes on the respective encoder or pass. 15 | /// 16 | /// This is useful for debugging with tools like [RenderDoc](https://renderdoc.org/). 17 | /// Debug markers will be emitted even if the device does not support timer queries or disables them via 18 | /// [`GpuProfilerSettings::enable_timer_queries`]. 19 | pub enable_debug_groups: bool, 20 | 21 | /// The profiler queues up to `max_num_pending_frames` "profiler-frames" at a time. 22 | /// 23 | /// A profiler-frame is regarded as in-flight until its queries have been successfully 24 | /// resolved using [`GpuProfiler::process_finished_frame`](crate::GpuProfiler::process_finished_frame). 25 | /// How long this takes to happen, depends on how fast buffer mappings return successfully 26 | /// which in turn primarily depends on how fast the device is able to finish work queued to the [`wgpu::Queue`]. 27 | /// 28 | /// If this threshold is exceeded, [`GpuProfiler::end_frame`](crate::GpuProfiler::end_frame) will silently drop frames. 29 | /// *Newer* frames will be dropped first in order to get results back eventually. 30 | /// (If the profiler were to drop the oldest frame, one may end up in a situation where there is never 31 | /// frame that is fully processed and thus never any results to be retrieved). 32 | /// 33 | /// Good values for `max_num_pending_frames` are 2-4 but may depend on your application workload 34 | /// and GPU-CPU syncing strategy. 35 | /// Must be greater than 0. 36 | pub max_num_pending_frames: usize, 37 | } 38 | 39 | impl Default for GpuProfilerSettings { 40 | fn default() -> Self { 41 | Self { 42 | enable_timer_queries: true, 43 | enable_debug_groups: true, 44 | max_num_pending_frames: 3, 45 | } 46 | } 47 | } 48 | 49 | impl GpuProfilerSettings { 50 | pub fn validate(&self) -> Result<(), SettingsError> { 51 | if self.max_num_pending_frames == 0 { 52 | Err(SettingsError::InvalidMaxNumPendingFrames) 53 | } else { 54 | Ok(()) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/puffin.rs: -------------------------------------------------------------------------------- 1 | use puffin::{GlobalProfiler, NanoSecond, ScopeDetails, StreamInfo, ThreadInfo}; 2 | 3 | use crate::GpuTimerQueryResult; 4 | 5 | /// Visualize the query results in a `puffin::GlobalProfiler`. 6 | pub fn output_frame_to_puffin(profiler: &mut GlobalProfiler, query_result: &[GpuTimerQueryResult]) { 7 | let mut stream_info = StreamInfo::default(); 8 | collect_stream_info_recursive(profiler, &mut stream_info, query_result, 0); 9 | 10 | profiler.report_user_scopes( 11 | ThreadInfo { 12 | start_time_ns: None, 13 | name: "GPU".to_string(), 14 | }, 15 | &stream_info.as_stream_into_ref(), 16 | ); 17 | } 18 | 19 | fn collect_stream_info_recursive( 20 | profiler: &mut GlobalProfiler, 21 | stream_info: &mut StreamInfo, 22 | query_result: &[GpuTimerQueryResult], 23 | depth: usize, 24 | ) { 25 | let details: Vec<_> = query_result 26 | .iter() 27 | .map(|query| ScopeDetails::from_scope_name(query.label.clone())) 28 | .collect(); 29 | let ids = profiler.register_user_scopes(&details); 30 | for (query, id) in query_result.iter().zip(ids) { 31 | if let Some(time) = &query.time { 32 | let start = (time.start * 1e9) as NanoSecond; 33 | let end = (time.end * 1e9) as NanoSecond; 34 | 35 | stream_info.depth = stream_info.depth.max(depth); 36 | stream_info.num_scopes += 1; 37 | stream_info.range_ns.0 = stream_info.range_ns.0.min(start); 38 | stream_info.range_ns.1 = stream_info.range_ns.0.max(end); 39 | 40 | let (offset, _) = stream_info.stream.begin_scope(|| start, id, ""); 41 | collect_stream_info_recursive(profiler, stream_info, &query.nested_queries, depth + 1); 42 | stream_info.stream.end_scope(offset, end as NanoSecond); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/scope.rs: -------------------------------------------------------------------------------- 1 | //! Scope types that wrap a `wgpu` encoder/pass and start a scope on creation. In most cases, they 2 | //! then allow automatically ending the scope on drop. 3 | 4 | use crate::{GpuProfiler, GpuProfilerQuery, ProfilerCommandRecorder}; 5 | 6 | /// Scope that takes a (mutable) reference to the encoder/pass. 7 | /// 8 | /// Calls [`GpuProfiler::end_query()`] on drop. 9 | pub struct Scope<'a, Recorder: ProfilerCommandRecorder> { 10 | pub profiler: &'a GpuProfiler, 11 | pub recorder: &'a mut Recorder, 12 | pub scope: Option, 13 | } 14 | 15 | impl Drop for Scope<'_, R> { 16 | #[inline] 17 | fn drop(&mut self) { 18 | if let Some(scope) = self.scope.take() { 19 | self.profiler.end_query(self.recorder, scope); 20 | } 21 | } 22 | } 23 | 24 | /// Scope that takes ownership of the encoder/pass. 25 | /// 26 | /// Calls [`GpuProfiler::end_query()`] on drop. 27 | pub struct OwningScope<'a, Recorder: ProfilerCommandRecorder> { 28 | pub profiler: &'a GpuProfiler, 29 | pub recorder: Recorder, 30 | pub scope: Option, 31 | } 32 | 33 | impl Drop for OwningScope<'_, R> { 34 | #[inline] 35 | fn drop(&mut self) { 36 | if let Some(scope) = self.scope.take() { 37 | self.profiler.end_query(&mut self.recorder, scope); 38 | } 39 | } 40 | } 41 | 42 | /// Scope that takes ownership of the encoder/pass. 43 | /// 44 | /// Does NOT call [`GpuProfiler::end_query()`] on drop. 45 | /// This construct is just for completeness in cases where working with scopes is preferred but one can't rely on the Drop call in the right place. 46 | /// This is useful when the owned value needs to be recovered after the end of the scope. 47 | /// In particular, to submit a [`wgpu::CommandEncoder`] to a queue, ownership of the encoder is necessary. 48 | pub struct ManualOwningScope<'a, Recorder: ProfilerCommandRecorder> { 49 | pub profiler: &'a GpuProfiler, 50 | pub recorder: Recorder, 51 | pub scope: Option, 52 | } 53 | 54 | impl ManualOwningScope<'_, R> { 55 | /// Ends the scope allowing the extraction of the owned [`ProfilerCommandRecorder`]. 56 | #[track_caller] 57 | #[inline] 58 | pub fn end_query(mut self) -> R { 59 | // Can't fail since creation implies begin_query. 60 | self.profiler 61 | .end_query(&mut self.recorder, self.scope.take().unwrap()); 62 | self.recorder 63 | } 64 | } 65 | 66 | /// Most implementation code of the different scope types is exactly the same. 67 | /// 68 | /// This macro allows to avoid code duplication. 69 | /// Another way of achieving this are extension traits, but this would mean that a user has to 70 | /// import the extension trait to use all methods of the scope types which I found a bit annoying. 71 | macro_rules! impl_scope_ext { 72 | ($scope:ident, $recorder_type:ty) => { 73 | impl<'a, R: ProfilerCommandRecorder> $scope<'a, R> { 74 | /// Starts a new profiler scope nested within this one. 75 | #[must_use] 76 | #[track_caller] 77 | #[inline] 78 | pub fn scope(&mut self, label: impl Into) -> Scope<'_, R> { 79 | let recorder: &mut R = &mut self.recorder; 80 | let scope = self 81 | .profiler 82 | .begin_query(label, recorder) 83 | .with_parent(self.scope.as_ref()); 84 | Scope { 85 | profiler: self.profiler, 86 | recorder, 87 | scope: Some(scope), 88 | } 89 | } 90 | } 91 | 92 | impl<'a> $scope<'a, wgpu::CommandEncoder> { 93 | /// Start a render pass wrapped in a [`OwningScope`]. 94 | /// 95 | /// Ignores passed `wgpu::RenderPassDescriptor::timestamp_writes` and replaces it with 96 | /// `timestamp_writes` managed by `GpuProfiler` if profiling is enabled. 97 | /// 98 | /// This also sets the `wgpu::RenderPassDescriptor::label` if it's `None` (default). 99 | /// 100 | /// Note that in order to take measurements, this requires the [`wgpu::Features::TIMESTAMP_QUERY`] feature. 101 | /// [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS`] & [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES`] are not required. 102 | #[track_caller] 103 | pub fn scoped_render_pass( 104 | &mut self, 105 | label: impl Into, 106 | pass_descriptor: wgpu::RenderPassDescriptor<'_>, 107 | ) -> OwningScope<'_, wgpu::RenderPass<'_>> { 108 | let child_scope = self 109 | .profiler 110 | .begin_pass_query(label, &mut self.recorder) 111 | .with_parent(self.scope.as_ref()); 112 | let render_pass = self 113 | .recorder 114 | .begin_render_pass(&wgpu::RenderPassDescriptor { 115 | timestamp_writes: child_scope.render_pass_timestamp_writes(), 116 | label: pass_descriptor.label.or(Some(&child_scope.label)), 117 | ..pass_descriptor 118 | }); 119 | 120 | OwningScope { 121 | profiler: self.profiler, 122 | recorder: render_pass, 123 | scope: Some(child_scope), 124 | } 125 | } 126 | 127 | /// Start a compute pass wrapped in a [`OwningScope`]. 128 | /// 129 | /// Uses passed label both for profiler scope and compute pass label. 130 | /// `timestamp_writes` managed by `GpuProfiler` if profiling is enabled. 131 | /// 132 | /// Note that in order to take measurements, this requires the [`wgpu::Features::TIMESTAMP_QUERY`] feature. 133 | /// [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS`] & [`wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES`] are not required. 134 | #[track_caller] 135 | pub fn scoped_compute_pass( 136 | &mut self, 137 | label: impl Into, 138 | ) -> OwningScope<'_, wgpu::ComputePass<'_>> { 139 | let child_scope = self 140 | .profiler 141 | .begin_pass_query(label, &mut self.recorder) 142 | .with_parent(self.scope.as_ref()); 143 | 144 | let render_pass = self 145 | .recorder 146 | .begin_compute_pass(&wgpu::ComputePassDescriptor { 147 | label: Some(&child_scope.label), 148 | timestamp_writes: child_scope.compute_pass_timestamp_writes(), 149 | }); 150 | 151 | OwningScope { 152 | profiler: self.profiler, 153 | recorder: render_pass, 154 | scope: Some(child_scope), 155 | } 156 | } 157 | } 158 | 159 | impl<'a, R: ProfilerCommandRecorder> std::ops::Deref for $scope<'a, R> { 160 | type Target = R; 161 | 162 | #[inline] 163 | fn deref(&self) -> &Self::Target { 164 | &self.recorder 165 | } 166 | } 167 | 168 | impl<'a, R: ProfilerCommandRecorder> std::ops::DerefMut for $scope<'a, R> { 169 | #[inline] 170 | fn deref_mut(&mut self) -> &mut Self::Target { 171 | &mut self.recorder 172 | } 173 | } 174 | }; 175 | } 176 | 177 | impl_scope_ext!(Scope, &'a mut R); 178 | impl_scope_ext!(OwningScope, R); 179 | impl_scope_ext!(ManualOwningScope, R); 180 | -------------------------------------------------------------------------------- /src/tracy.rs: -------------------------------------------------------------------------------- 1 | use crate::CreationError; 2 | 3 | pub fn create_tracy_gpu_client( 4 | backend: wgpu::Backend, 5 | device: &wgpu::Device, 6 | queue: &wgpu::Queue, 7 | ) -> Result { 8 | let query_set = device.create_query_set(&wgpu::QuerySetDescriptor { 9 | label: Some("wgpu-profiler gpu -> cpu sync query_set"), 10 | ty: wgpu::QueryType::Timestamp, 11 | count: 1, 12 | }); 13 | 14 | let resolve_buffer = device.create_buffer(&wgpu::BufferDescriptor { 15 | label: Some("wgpu-profiler gpu -> cpu resolve buffer"), 16 | size: wgpu::QUERY_SIZE as _, 17 | usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC, 18 | mapped_at_creation: false, 19 | }); 20 | 21 | let map_buffer = device.create_buffer(&wgpu::BufferDescriptor { 22 | label: Some("wgpu-profiler gpu -> cpu map buffer"), 23 | size: wgpu::QUERY_SIZE as _, 24 | usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, 25 | mapped_at_creation: false, 26 | }); 27 | 28 | let mut timestamp_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { 29 | label: Some("wgpu-profiler gpu -> cpu query timestamp"), 30 | }); 31 | timestamp_encoder.write_timestamp(&query_set, 0); 32 | timestamp_encoder.resolve_query_set(&query_set, 0..1, &resolve_buffer, 0); 33 | // Workaround for https://github.com/gfx-rs/wgpu/issues/6406 34 | // TODO when that bug is fixed, merge these encoders together again 35 | let mut copy_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { 36 | label: Some("wgpu-profiler gpu -> cpu copy timestamp"), 37 | }); 38 | copy_encoder.copy_buffer_to_buffer(&resolve_buffer, 0, &map_buffer, 0, wgpu::QUERY_SIZE as _); 39 | queue.submit([timestamp_encoder.finish(), copy_encoder.finish()]); 40 | 41 | map_buffer.slice(..).map_async(wgpu::MapMode::Read, |_| ()); 42 | device.poll(wgpu::PollType::Wait).unwrap(); 43 | 44 | let view = map_buffer.slice(..).get_mapped_range(); 45 | let timestamp: i64 = i64::from_le_bytes((*view).try_into().unwrap()); 46 | 47 | let tracy_backend = match backend { 48 | wgpu::Backend::Noop | wgpu::Backend::Metal | wgpu::Backend::BrowserWebGpu => { 49 | tracy_client::GpuContextType::Invalid 50 | } 51 | wgpu::Backend::Vulkan => tracy_client::GpuContextType::Vulkan, 52 | wgpu::Backend::Dx12 => tracy_client::GpuContextType::Direct3D12, 53 | wgpu::Backend::Gl => tracy_client::GpuContextType::OpenGL, 54 | }; 55 | 56 | tracy_client::Client::running() 57 | .ok_or(CreationError::TracyClientNotRunning)? 58 | .new_gpu_context( 59 | Some("wgpu"), 60 | tracy_backend, 61 | timestamp, 62 | queue.get_timestamp_period(), 63 | ) 64 | .map_err(CreationError::from) 65 | } 66 | -------------------------------------------------------------------------------- /tests/src/dropped_frame_handling.rs: -------------------------------------------------------------------------------- 1 | use wgpu_profiler::GpuProfilerSettings; 2 | 3 | use super::create_device; 4 | 5 | // regression test for bug described in https://github.com/Wumpf/wgpu-profiler/pull/18 6 | #[test] 7 | fn handle_dropped_frames_gracefully() { 8 | let (_, device, queue) = create_device( 9 | wgpu::Features::TIMESTAMP_QUERY.union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS), 10 | ) 11 | .unwrap(); 12 | 13 | // max_num_pending_frames is one! 14 | let mut profiler = wgpu_profiler::GpuProfiler::new( 15 | &device, 16 | GpuProfilerSettings { 17 | max_num_pending_frames: 1, 18 | ..Default::default() 19 | }, 20 | ) 21 | .unwrap(); 22 | 23 | // Two frames without device poll, causing the profiler to drop a frame on the second round. 24 | for _ in 0..2 { 25 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 26 | { 27 | let _ = profiler.scope("testscope", &mut encoder); 28 | } 29 | profiler.resolve_queries(&mut encoder); 30 | profiler.end_frame().unwrap(); 31 | 32 | // We haven't done a device poll, so there can't be a result! 33 | assert!(profiler 34 | .process_finished_frame(queue.get_timestamp_period()) 35 | .is_none()); 36 | } 37 | 38 | // Poll to explicitly trigger mapping callbacks. 39 | device.poll(wgpu::PollType::Wait).unwrap(); 40 | 41 | // A single (!) frame should now be available. 42 | assert!(profiler 43 | .process_finished_frame(queue.get_timestamp_period()) 44 | .is_some()); 45 | assert!(profiler 46 | .process_finished_frame(queue.get_timestamp_period()) 47 | .is_none()); 48 | } 49 | -------------------------------------------------------------------------------- /tests/src/errors.rs: -------------------------------------------------------------------------------- 1 | use wgpu_profiler::GpuProfilerSettings; 2 | 3 | use super::create_device; 4 | 5 | #[test] 6 | fn invalid_pending_frame_count() { 7 | let (_, device, _queue) = create_device(wgpu::Features::TIMESTAMP_QUERY).unwrap(); 8 | 9 | let profiler = wgpu_profiler::GpuProfiler::new( 10 | &device, 11 | wgpu_profiler::GpuProfilerSettings { 12 | max_num_pending_frames: 0, 13 | ..Default::default() 14 | }, 15 | ); 16 | assert!(matches!( 17 | profiler, 18 | Err(wgpu_profiler::CreationError::InvalidSettings( 19 | wgpu_profiler::SettingsError::InvalidMaxNumPendingFrames 20 | )) 21 | )); 22 | } 23 | 24 | #[test] 25 | fn end_frame_unclosed_query() { 26 | let (_, device, _queue) = create_device( 27 | wgpu::Features::TIMESTAMP_QUERY.union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS), 28 | ) 29 | .unwrap(); 30 | 31 | let mut profiler = 32 | wgpu_profiler::GpuProfiler::new(&device, GpuProfilerSettings::default()).unwrap(); 33 | let unclosed_query = { 34 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 35 | let query = profiler.begin_query("open query", &mut encoder); 36 | profiler.resolve_queries(&mut encoder); 37 | query 38 | }; 39 | 40 | assert_eq!( 41 | profiler.end_frame(), 42 | Err(wgpu_profiler::EndFrameError::UnclosedQueries(1)) 43 | ); 44 | 45 | // Make sure we can recover from this. 46 | { 47 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 48 | profiler.end_query(&mut encoder, unclosed_query); 49 | profiler.resolve_queries(&mut encoder); 50 | } 51 | assert_eq!(profiler.end_frame(), Ok(())); 52 | } 53 | 54 | #[test] 55 | fn end_frame_unresolved_query() { 56 | let (_, device, _queue) = create_device( 57 | wgpu::Features::TIMESTAMP_QUERY.union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS), 58 | ) 59 | .unwrap(); 60 | 61 | let mut profiler = 62 | wgpu_profiler::GpuProfiler::new(&device, GpuProfilerSettings::default()).unwrap(); 63 | { 64 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 65 | let query = profiler.begin_query("open query", &mut encoder); 66 | profiler.end_query(&mut encoder, query); 67 | } 68 | 69 | assert_eq!( 70 | profiler.end_frame(), 71 | Err(wgpu_profiler::EndFrameError::UnresolvedQueries(2)) 72 | ); 73 | 74 | // Make sure we can recover from this! 75 | { 76 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 77 | profiler.resolve_queries(&mut encoder); 78 | } 79 | assert_eq!(profiler.end_frame(), Ok(())); 80 | 81 | device.poll(wgpu::PollType::Wait).unwrap(); 82 | } 83 | 84 | #[test] 85 | fn change_settings_while_query_open() { 86 | let (_, device, _queue) = create_device( 87 | wgpu::Features::TIMESTAMP_QUERY.union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS), 88 | ) 89 | .unwrap(); 90 | 91 | let mut profiler = 92 | wgpu_profiler::GpuProfiler::new(&device, GpuProfilerSettings::default()).unwrap(); 93 | 94 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 95 | let query = profiler.begin_query("open query", &mut encoder); 96 | 97 | assert_eq!( 98 | profiler.change_settings(GpuProfilerSettings::default()), 99 | Ok(()) 100 | ); 101 | 102 | profiler.end_query(&mut encoder, query); 103 | } 104 | -------------------------------------------------------------------------------- /tests/src/interleaved_command_buffer.rs: -------------------------------------------------------------------------------- 1 | use wgpu_profiler::{GpuProfiler, GpuProfilerSettings}; 2 | 3 | use crate::src::{expected_scope, validate_results, validate_results_unordered, Requires}; 4 | 5 | use super::create_device; 6 | 7 | #[test] 8 | fn interleaved_scopes() { 9 | let (_, device, queue) = create_device(wgpu::Features::TIMESTAMP_QUERY).unwrap(); 10 | 11 | let mut profiler = GpuProfiler::new(&device, GpuProfilerSettings::default()).unwrap(); 12 | 13 | let mut encoder0 = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 14 | let mut encoder1 = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 15 | 16 | { 17 | let mut e0_s0 = profiler.scope("e0_s0", &mut encoder0); 18 | let mut e1_s0 = profiler.scope("e1_s0", &mut encoder1); 19 | 20 | drop(e0_s0.scope("e0_s0_s0")); 21 | drop(e0_s0.scope("e0_s0_s1")); 22 | drop(e1_s0.scope("e1_s0_s0")); 23 | } 24 | 25 | profiler.resolve_queries(&mut encoder0); 26 | queue.submit([encoder1.finish(), encoder0.finish()]); 27 | profiler.end_frame().unwrap(); 28 | 29 | device.poll(wgpu::PollType::Wait).unwrap(); 30 | 31 | // Single frame should now be available. 32 | let frame = profiler 33 | .process_finished_frame(queue.get_timestamp_period()) 34 | .unwrap(); 35 | 36 | // Print entire tree. Useful for debugging the test if it fails! 37 | println!("{:#?}", frame); 38 | 39 | // Check if the frame gives us the expected nesting of timer scopes. 40 | validate_results( 41 | device.features(), 42 | &frame, 43 | &[ 44 | expected_scope( 45 | "e1_s0", 46 | Requires::TimestampsInEncoders, 47 | [expected_scope( 48 | "e1_s0_s0", 49 | Requires::TimestampsInEncoders, 50 | [], 51 | )], 52 | ), 53 | expected_scope( 54 | "e0_s0", 55 | Requires::TimestampsInEncoders, 56 | [ 57 | expected_scope("e0_s0_s0", Requires::TimestampsInEncoders, []), 58 | expected_scope("e0_s0_s1", Requires::TimestampsInEncoders, []), 59 | ], 60 | ), 61 | ], 62 | ); 63 | } 64 | 65 | #[test] 66 | fn multithreaded_scopes() { 67 | let (_, device, queue) = create_device(wgpu::Features::TIMESTAMP_QUERY).unwrap(); 68 | 69 | let mut profiler = GpuProfiler::new(&device, GpuProfilerSettings::default()).unwrap(); 70 | 71 | const NUM_SCOPES_PER_THREAD: usize = 1000; 72 | 73 | let barrier = std::sync::Barrier::new(2); 74 | let (command_buffer0, command_buffer1) = std::thread::scope(|thread_scope| { 75 | let join_handle0 = thread_scope.spawn(|| { 76 | let mut encoder = 77 | device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 78 | barrier.wait(); 79 | 80 | for i in 0..NUM_SCOPES_PER_THREAD { 81 | let _ = profiler.scope(format!("e0_s{i}"), &mut encoder); 82 | } 83 | encoder.finish() 84 | }); 85 | let join_handle1 = thread_scope.spawn(|| { 86 | let mut encoder = 87 | device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 88 | barrier.wait(); 89 | 90 | for i in 0..NUM_SCOPES_PER_THREAD { 91 | let _ = profiler.scope(format!("e1_s{i}"), &mut encoder); 92 | } 93 | encoder.finish() 94 | }); 95 | 96 | (join_handle0.join().unwrap(), join_handle1.join().unwrap()) 97 | }); 98 | 99 | let mut resolve_encoder = 100 | device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 101 | profiler.resolve_queries(&mut resolve_encoder); 102 | queue.submit([command_buffer0, command_buffer1, resolve_encoder.finish()]); 103 | profiler.end_frame().unwrap(); 104 | 105 | device.poll(wgpu::PollType::Wait).unwrap(); 106 | 107 | // Single frame should now be available. 108 | let frame = profiler 109 | .process_finished_frame(queue.get_timestamp_period()) 110 | .unwrap(); 111 | 112 | // Print entire tree. Useful for debugging the test if it fails! 113 | println!("{:#?}", frame); 114 | 115 | // Both encoders should have produces the scopes, albeit in arbitrary order. 116 | validate_results_unordered( 117 | device.features(), 118 | &frame, 119 | &(0..NUM_SCOPES_PER_THREAD) 120 | .map(|i| expected_scope(format!("e0_s{i}"), Requires::TimestampsInEncoders, [])) 121 | .chain( 122 | (0..NUM_SCOPES_PER_THREAD).map(|i| { 123 | expected_scope(format!("e1_s{i}"), Requires::TimestampsInEncoders, []) 124 | }), 125 | ) 126 | .collect::>(), 127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /tests/src/mod.rs: -------------------------------------------------------------------------------- 1 | mod dropped_frame_handling; 2 | mod errors; 3 | mod interleaved_command_buffer; 4 | mod multiple_resolves_per_frame; 5 | mod nested_scopes; 6 | 7 | pub fn create_device( 8 | features: wgpu::Features, 9 | ) -> Result<(wgpu::Backend, wgpu::Device, wgpu::Queue), wgpu::RequestDeviceError> { 10 | async fn create_default_device_async( 11 | features: wgpu::Features, 12 | ) -> Result<(wgpu::Backend, wgpu::Device, wgpu::Queue), wgpu::RequestDeviceError> { 13 | let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { 14 | backends: wgpu::Backends::PRIMARY, // Workaround for wgl having issues with parallel device destruction. 15 | ..Default::default() 16 | }); 17 | let adapter = instance 18 | .request_adapter(&wgpu::RequestAdapterOptions::default()) 19 | .await 20 | .unwrap(); 21 | let (device, queue) = adapter 22 | .request_device(&wgpu::DeviceDescriptor { 23 | required_features: features, 24 | ..Default::default() 25 | }) 26 | .await?; 27 | Ok((adapter.get_info().backend, device, queue)) 28 | } 29 | 30 | futures_lite::future::block_on(create_default_device_async(features)) 31 | } 32 | 33 | #[derive(Debug, Clone, Copy)] 34 | enum Requires { 35 | Disabled, 36 | Timestamps, 37 | TimestampsInEncoders, 38 | TimestampsInPasses, 39 | } 40 | 41 | impl Requires { 42 | fn expect_time_result(self, features: wgpu::Features) -> bool { 43 | match self { 44 | Requires::Timestamps => features.contains(wgpu::Features::TIMESTAMP_QUERY), 45 | Requires::TimestampsInEncoders => { 46 | features.contains(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS) 47 | } 48 | Requires::TimestampsInPasses => { 49 | features.contains(wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES) 50 | } 51 | Requires::Disabled => false, 52 | } 53 | } 54 | } 55 | 56 | #[derive(Debug)] 57 | struct ExpectedScope(String, Requires, Vec); 58 | 59 | fn expected_scope( 60 | label: impl Into, 61 | requires: Requires, 62 | children: impl Into>, 63 | ) -> ExpectedScope { 64 | ExpectedScope(label.into(), requires, children.into()) 65 | } 66 | 67 | fn validate_results( 68 | features: wgpu::Features, 69 | results: &[wgpu_profiler::GpuTimerQueryResult], 70 | expected: &[ExpectedScope], 71 | ) { 72 | assert_eq!( 73 | results.len(), 74 | expected.len(), 75 | "results: {results:?}\nexpected: {expected:?}" 76 | ); 77 | for (result, expected) in results.iter().zip(expected.iter()) { 78 | assert_eq!(result.label, expected.0); 79 | assert_eq!( 80 | result.time.is_some(), 81 | expected.1.expect_time_result(features), 82 | "label: {}", 83 | result.label 84 | ); 85 | 86 | validate_results(features, &result.nested_queries, &expected.2); 87 | } 88 | } 89 | 90 | fn validate_results_unordered( 91 | features: wgpu::Features, 92 | results: &[wgpu_profiler::GpuTimerQueryResult], 93 | expected: &[ExpectedScope], 94 | ) { 95 | assert_eq!( 96 | results.len(), 97 | expected.len(), 98 | "result: {results:?}\nexpected: {expected:?}" 99 | ); 100 | 101 | let mut expected_by_label = 102 | std::collections::HashMap::::from_iter( 103 | expected 104 | .iter() 105 | .map(|expected| (expected.0.clone(), (expected.1, expected.2.as_ref()))), 106 | ); 107 | 108 | for result in results { 109 | let Some((requires, nested_expectations)) = expected_by_label.remove(&result.label) else { 110 | panic!("missing result for label: {}", result.label); 111 | }; 112 | assert_eq!( 113 | result.time.is_some(), 114 | requires.expect_time_result(features), 115 | "label: {}", 116 | result.label 117 | ); 118 | 119 | validate_results(features, &result.nested_queries, nested_expectations); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/src/multiple_resolves_per_frame.rs: -------------------------------------------------------------------------------- 1 | // Regression test for bug described in 2 | // * https://github.com/Wumpf/wgpu-profiler/issues/79 3 | // * https://github.com/Wumpf/wgpu-profiler/issues/82 4 | #[test] 5 | fn multiple_resolves_per_frame() { 6 | const NUM_SCOPES: usize = 1000; 7 | 8 | let (_, device, queue) = super::create_device( 9 | wgpu::Features::TIMESTAMP_QUERY.union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS), 10 | ) 11 | .unwrap(); 12 | 13 | let mut profiler = 14 | wgpu_profiler::GpuProfiler::new(&device, wgpu_profiler::GpuProfilerSettings::default()) 15 | .unwrap(); 16 | 17 | { 18 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 19 | 20 | // Resolve call per scope. 21 | // Do this many times to check for potential buffer overflows as found in 22 | // https://github.com/Wumpf/wgpu-profiler/issues/82 23 | for i in 0..NUM_SCOPES { 24 | { 25 | let _ = profiler.scope(format!("{i}"), &mut encoder); 26 | } 27 | profiler.resolve_queries(&mut encoder); 28 | } 29 | 30 | // And an extra resolve for good measure (this should be a no-op). 31 | profiler.resolve_queries(&mut encoder); 32 | 33 | profiler.end_frame().unwrap(); 34 | } 35 | 36 | // Poll to explicitly trigger mapping callbacks. 37 | device.poll(wgpu::PollType::Wait).unwrap(); 38 | 39 | // Frame should now be available and contain all the scopes. 40 | let scopes = profiler 41 | .process_finished_frame(queue.get_timestamp_period()) 42 | .unwrap(); 43 | assert_eq!(scopes.len(), NUM_SCOPES); 44 | for (i, scope) in scopes.iter().enumerate() { 45 | assert_eq!(scope.label, format!("{i}")); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/src/nested_scopes.rs: -------------------------------------------------------------------------------- 1 | use wgpu_profiler::{GpuProfiler, GpuProfilerSettings}; 2 | 3 | use crate::src::{expected_scope, validate_results, Requires}; 4 | 5 | use super::create_device; 6 | 7 | fn nested_scopes(device: &wgpu::Device, queue: &wgpu::Queue) { 8 | let mut profiler = GpuProfiler::new(device, GpuProfilerSettings::default()).unwrap(); 9 | 10 | let mut encoder0 = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 11 | let mut encoder1 = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 12 | let mut encoder2 = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 13 | 14 | { 15 | let mut outer_scope = profiler.scope("e0_s0", &mut encoder0); 16 | { 17 | drop(outer_scope.scoped_compute_pass("e0_s0_c0")); 18 | { 19 | let mut inner_scope = outer_scope.scoped_compute_pass("e0_s0_c1"); 20 | { 21 | drop(inner_scope.scope("e0_s0_c1_s0")); 22 | let mut innermost_scope = inner_scope.scope("e0_s0_c1_s1"); 23 | { 24 | let mut scope = innermost_scope.scope("e0_s0_c1_s1_s0"); 25 | drop(scope.scope("e0_s0_c1_s1_s0_s0")); 26 | } 27 | } 28 | } 29 | } 30 | } 31 | // Bunch of interleaved scopes on an encoder. 32 | { 33 | let mut scope = profiler.scope("e1_s0", &mut encoder1); 34 | { 35 | drop(scope.scope("e1_s0_s0")); 36 | drop(scope.scope("e1_s0_s1")); 37 | { 38 | let mut scope = scope.scope("e1_s0_s2"); 39 | drop(scope.scope("e1_s0_s2_s0")); 40 | } 41 | } 42 | } 43 | drop(profiler.scope("e2_s0", &mut encoder2)); 44 | { 45 | // Another scope, but with the profiler disabled which should be possible on the fly. 46 | profiler 47 | .change_settings(GpuProfilerSettings { 48 | enable_timer_queries: false, 49 | ..Default::default() 50 | }) 51 | .unwrap(); 52 | let mut scope = profiler.scope("e2_s1", &mut encoder0); 53 | { 54 | let mut scope = scope.scoped_compute_pass("e2_s1_c1"); 55 | drop(scope.scope("e2_s1_c1_s0")); 56 | } 57 | } 58 | 59 | profiler.resolve_queries(&mut encoder2); 60 | queue.submit([encoder0.finish(), encoder1.finish(), encoder2.finish()]); 61 | profiler.end_frame().unwrap(); 62 | 63 | device.poll(wgpu::PollType::Wait).unwrap(); 64 | 65 | // Single frame should now be available. 66 | let frame = profiler 67 | .process_finished_frame(queue.get_timestamp_period()) 68 | .unwrap(); 69 | 70 | // Print entire tree. Useful for debugging the test if it fails! 71 | println!("{:#?}", frame); 72 | 73 | // Check if the frame gives us the expected nesting of timer scopes. 74 | validate_results( 75 | device.features(), 76 | &frame, 77 | &[ 78 | expected_scope( 79 | "e0_s0", 80 | Requires::TimestampsInEncoders, 81 | [ 82 | expected_scope("e0_s0_c0", Requires::Timestamps, []), 83 | expected_scope( 84 | "e0_s0_c1", 85 | Requires::Timestamps, 86 | [ 87 | expected_scope("e0_s0_c1_s0", Requires::TimestampsInPasses, []), 88 | expected_scope( 89 | "e0_s0_c1_s1", 90 | Requires::TimestampsInPasses, 91 | [expected_scope( 92 | "e0_s0_c1_s1_s0", 93 | Requires::TimestampsInPasses, 94 | [ 95 | expected_scope( 96 | "e0_s0_c1_s1_s0_s0", 97 | Requires::TimestampsInPasses, 98 | [], 99 | ), // 100 | ], 101 | )], 102 | ), 103 | ], 104 | ), 105 | ], 106 | ), 107 | expected_scope( 108 | "e1_s0", 109 | Requires::TimestampsInEncoders, 110 | [ 111 | expected_scope("e1_s0_s0", Requires::TimestampsInEncoders, []), 112 | expected_scope("e1_s0_s1", Requires::TimestampsInEncoders, []), 113 | expected_scope( 114 | "e1_s0_s2", 115 | Requires::TimestampsInEncoders, 116 | [ 117 | expected_scope("e1_s0_s2_s0", Requires::TimestampsInEncoders, []), // 118 | ], 119 | ), 120 | ], 121 | ), 122 | expected_scope("e2_s0", Requires::TimestampsInEncoders, []), 123 | expected_scope( 124 | "e2_s1", 125 | Requires::Disabled, 126 | [expected_scope( 127 | "e2_s1_c1", 128 | Requires::Disabled, 129 | [expected_scope("e2_s1_c1_s0", Requires::Disabled, [])], 130 | )], 131 | ), 132 | ], 133 | ); 134 | } 135 | 136 | // Note that `TIMESTAMP_QUERY_INSIDE_PASSES` implies support for `TIMESTAMP_QUERY_INSIDE_ENCODERS`. 137 | // But as of writing wgpu allows enabling pass timestamps without encoder timestamps and we should handle this fine as well! 138 | 139 | #[test] 140 | fn nested_scopes_timestamp_in_passes_and_encoder_enabled() { 141 | let Ok((_, device, queue)) = create_device( 142 | wgpu::Features::TIMESTAMP_QUERY 143 | | wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES 144 | | wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS, 145 | ) else { 146 | println!("Skipping test because device doesn't support TIMESTAMP_QUERY_INSIDE_PASSES"); 147 | return; 148 | }; 149 | nested_scopes(&device, &queue); 150 | } 151 | 152 | #[test] 153 | fn nested_scopes_timestamp_in_passes_enabled() { 154 | let Ok((_, device, queue)) = create_device( 155 | wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES, 156 | ) else { 157 | println!("Skipping test because device doesn't support TIMESTAMP_QUERY_INSIDE_PASSES"); 158 | return; 159 | }; 160 | nested_scopes(&device, &queue); 161 | } 162 | 163 | #[test] 164 | fn nested_scopes_timestamp_in_encoders_enabled() { 165 | let Ok((_, device, queue)) = create_device( 166 | wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS, 167 | ) else { 168 | println!("Skipping test because device doesn't support TIMESTAMP_QUERY_INSIDE_ENCODERS"); 169 | return; 170 | }; 171 | nested_scopes(&device, &queue); 172 | } 173 | 174 | #[test] 175 | fn nested_scopes_timestamp_enabled() { 176 | let Ok((_, device, queue)) = create_device(wgpu::Features::TIMESTAMP_QUERY) else { 177 | println!("Skipping test because device doesn't support TIMESTAMP_QUERY"); 178 | return; 179 | }; 180 | nested_scopes(&device, &queue); 181 | } 182 | 183 | #[test] 184 | fn nested_scopes_no_features() { 185 | let (_, device, queue) = create_device(wgpu::Features::empty()).unwrap(); 186 | nested_scopes(&device, &queue); 187 | } 188 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | // By default each module in the tests folder is its own binary. 2 | // This is a bit annoying in that far that it adds a lot of link time for no good reason 3 | // since we practically always want to run all the tests. 4 | // 5 | // There's an easy workaround though: 6 | // By having only a single top level module, everything becomes a single binary again! 7 | 8 | mod src; 9 | --------------------------------------------------------------------------------