diff --git a/Cargo.lock b/Cargo.lock index 26fdcf1..f5fd8c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,7 +34,8 @@ version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "gimli", + "fallible-iterator", + "gimli 0.31.1", ] [[package]] @@ -67,10 +68,25 @@ dependencies = [ ] [[package]] -name = "alsa" -version = "0.9.1" +name = "alloc-no-stdlib" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "alsa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdc00893e7a970727e9304671b2c88577b4cfe53dc64019fdfdf9683573a09c4" dependencies = [ "alsa-sys", "bitflags 2.9.3", @@ -182,9 +198,9 @@ checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arboard" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ "clipboard-win", "image", @@ -196,7 +212,7 @@ dependencies = [ "objc2-foundation 0.3.1", "parking_lot", "percent-encoding", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "x11rb", ] @@ -274,10 +290,27 @@ dependencies = [ ] [[package]] -name = "async-executor" -version = "1.13.2" +name = "async-compression" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +checksum = "6448dfb3960f0b038e88c781ead1e7eb7929dfc3a71a1336ec9086c00f6d1e75" +dependencies = [ + "brotli", + "compression-codecs", + "compression-core", + "flate2", + "futures-core", + "futures-io", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", @@ -448,11 +481,23 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.36.7", "rustc-demangle", "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "binary-merge" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597bb81c80a54b6a4381b23faba8d7774b144c94cbd1d6fe3f1329bd776554ab" + [[package]] name = "bindgen" version = "0.72.0" @@ -538,6 +583,27 @@ dependencies = [ "piper", ] +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -564,6 +630,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -576,6 +648,18 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cab" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171228650e6721d5acc0868a462cd864f49ac5f64e4a42cde270406e64e404d2" +dependencies = [ + "byteorder", + "flate2", + "lzxd", + "time", +] + [[package]] name = "calloop" version = "0.13.0" @@ -653,9 +737,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.45" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" dependencies = [ "clap_builder", "clap_derive", @@ -663,9 +747,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" dependencies = [ "anstream", "anstyle", @@ -727,6 +811,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "compression-codecs" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46cc6539bf1c592cff488b9f253b30bc0ec50d15407c2cf45e27bd8f308d5905" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", +] + +[[package]] +name = "compression-core" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2957e823c15bde7ecf1e8b64e537aa03a6be5fda0e2334e99887669e75b12e01" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -828,6 +932,30 @@ dependencies = [ "windows 0.54.0", ] +[[package]] +name = "cpp_demangle" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -864,6 +992,24 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "directories" version = "6.0.0" @@ -873,6 +1019,15 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -1046,6 +1201,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elsa" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9abf33c656a7256451ebb7d0082c5a471820c31269e49d807c538c252352186e" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "emath" version = "0.32.1" @@ -1182,6 +1346,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fastrand" version = "2.3.0" @@ -1217,6 +1387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1268,6 +1439,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix 1.0.8", + "windows-sys 0.59.0", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1313,6 +1494,12 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -1328,6 +1515,7 @@ dependencies = [ "futures-core", "futures-io", "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -1335,6 +1523,20 @@ dependencies = [ "slab", ] +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags 2.9.3", + "debugid", + "rustc-hash 2.1.1", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -1352,8 +1554,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1363,9 +1567,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1409,6 +1615,21 @@ name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "stable_deref_trait", +] + +[[package]] +name = "gimli" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6298e594375a7fead9efd5568f0a46e6a154fb6a9bdcbe3c06946ffd81a5f6" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] [[package]] name = "gl_generator" @@ -1543,6 +1764,108 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -1693,6 +2016,15 @@ dependencies = [ "libc", ] +[[package]] +name = "inplace-vec-builder" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf64c2edc8226891a71f127587a2861b132d2b942310843814d5001d99a1d307" +dependencies = [ + "smallvec", +] + [[package]] name = "io-kit-sys" version = "0.4.1" @@ -1714,6 +2046,22 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1768,9 +2116,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -1834,12 +2182,16 @@ dependencies = [ "egui-winit", "egui_extras", "fixed", + "fxprof-processed-profile", "gilrs", + "gimli 0.32.2", "hex", "image", "itertools 0.14.0", + "normpath", "num-derive", "num-traits", + "object 0.37.3", "oneshot", "pollster", "rand 0.9.2", @@ -1853,6 +2205,7 @@ dependencies = [ "tracing", "tracing-subscriber", "wgpu", + "wholesym", "windows 0.61.3", "winit", "winresource", @@ -1901,6 +2254,48 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" + +[[package]] +name = "linux-perf-data" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f85f35725e15ad6c62b9db73f3d62439094e616a2f83500f7bcdc01ae5b84d8" +dependencies = [ + "byteorder", + "linear-map", + "linux-perf-event-reader", + "memchr", + "prost", + "prost-derive", + "thiserror 2.0.16", +] + +[[package]] +name = "linux-perf-event-reader" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8fc7e83909ea3b9e2784591655637d3401f2f16014f9d8d6e23ccd138e665f" +dependencies = [ + "bitflags 2.9.3", + "byteorder", + "memchr", + "thiserror 2.0.16", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1941,6 +2336,28 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzxd" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9" + [[package]] name = "mach2" version = "0.4.3" @@ -1950,6 +2367,17 @@ dependencies = [ "libc", ] +[[package]] +name = "macho-unwind-info" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" +dependencies = [ + "thiserror 2.0.16", + "zerocopy", + "zerocopy-derive", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1968,6 +2396,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.7.5" @@ -2052,6 +2486,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "msvc-demangler" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeff6bd154a309b2ada5639b2661ca6ae4599b34e8487dc276d2cd637da2d76" +dependencies = [ + "bitflags 2.9.3", + "itoa", +] + [[package]] name = "naga" version = "25.0.1" @@ -2171,6 +2615,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "normpath" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2190,6 +2643,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.2" @@ -2528,7 +2987,20 @@ version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ + "flate2", "memchr", + "ruzstd 0.7.3", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "flate2", + "memchr", + "ruzstd 0.8.1", ] [[package]] @@ -2656,6 +3128,31 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pdb-addr2line" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a2e496e34cd96a1bb26f681e5adb11c98f1e5378e294e60c06c0cf04c526ba" +dependencies = [ + "bitflags 2.9.3", + "elsa", + "maybe-owned", + "pdb2", + "range-collections", + "thiserror 2.0.16", +] + +[[package]] +name = "pdb2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b8733697ccc3aa405a9b2cf485a42746ad1cf73d3b0497b79b24f9e874a71" +dependencies = [ + "fallible-iterator", + "scroll", + "uuid", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2803,6 +3300,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2851,6 +3354,28 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -2860,6 +3385,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.16", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.16", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.40" @@ -2925,6 +3505,24 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" +[[package]] +name = "range-collections" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "861706ea9c4aded7584c5cd1d241cec2ea7f5f50999f236c22b65409a1f1a0d0" +dependencies = [ + "binary-merge", + "inplace-vec-builder", + "ref-cast", + "smallvec", +] + +[[package]] +name = "rangemap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2970,15 +3568,35 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.11.1" +name = "ref-cast" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -2992,13 +3610,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", ] [[package]] @@ -3009,9 +3627,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "renderdoc-sys" @@ -3019,6 +3637,48 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "async-compression", + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + [[package]] name = "rfd" version = "0.15.4" @@ -3043,6 +3703,20 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rtrb" version = "0.3.2" @@ -3119,12 +3793,65 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ruzstd" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f" +dependencies = [ + "twox-hash 1.6.3", +] + +[[package]] +name = "ruzstd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640bec8aad418d7d03c72ea2de10d5c646a598f9883c7babc160d91e3c1b26c" +dependencies = [ + "twox-hash 2.1.1", +] + [[package]] name = "ryu" version = "1.0.20" @@ -3140,6 +3867,46 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "samply-symbols" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97e73d38bb04a373dba1260af91d4b0010e84cecd92d20b8e9949a910d5b9cbb" +dependencies = [ + "addr2line", + "bitflags 2.9.3", + "cpp_demangle", + "crc32fast", + "debugid", + "elsa", + "flate2", + "gimli 0.31.1", + "linux-perf-data", + "lzma-rs", + "macho-unwind-info", + "memchr", + "msvc-demangler", + "nom", + "object 0.36.7", + "pdb-addr2line", + "rangemap", + "rustc-demangle", + "scala-native-demangle", + "srcsrv", + "thiserror 2.0.16", + "uuid", + "yoke", + "yoke-derive", + "zerocopy", + "zerocopy-derive", +] + +[[package]] +name = "scala-native-demangle" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4416eddc0eaf31e04aa4039bd3db4288ea1ba613955d86cf9c310049c5d1e2" + [[package]] name = "scoped-tls" version = "1.0.1" @@ -3152,6 +3919,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" + [[package]] name = "sctk-adwaita" version = "0.10.1" @@ -3217,6 +3990,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3338,6 +4123,16 @@ dependencies = [ "bitflags 2.9.3", ] +[[package]] +name = "srcsrv" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cd3e3828fb4dd5ba0e7091777edb6c3db3cd2d6fc10547b29b40f6949a29be" +dependencies = [ + "memchr", + "thiserror 2.0.16", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3390,6 +4185,30 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symsrv" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c73f1f7b9bc1423e6b5c6b4ed3840f266f8291b270520c0fb0501bad3a0aa7" +dependencies = [ + "async-compression", + "cab", + "dirs", + "fs4", + "futures-util", + "http", + "reqwest", + "scopeguard", + "thiserror 2.0.16", + "tokio", +] + [[package]] name = "syn" version = "2.0.106" @@ -3401,6 +4220,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -3508,6 +4336,25 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + [[package]] name = "tiny-skia" version = "0.11.4" @@ -3543,6 +4390,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.47.1" @@ -3572,6 +4434,29 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -3613,6 +4498,51 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.3", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -3684,12 +4614,34 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "twox-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56" + [[package]] name = "type-map" version = "0.5.1" @@ -3741,10 +4693,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] -name = "url" -version = "2.5.6" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -3808,6 +4766,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3894,6 +4861,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wayland-backend" version = "0.3.11" @@ -4039,6 +5019,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weezl" version = "0.1.10" @@ -4192,6 +5181,34 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wholesym" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea291707798a4b15f75d46418b6f7c5a044cce8d55e2f18584ccebcdb8b4354" +dependencies = [ + "async-compression", + "bytes", + "core-foundation 0.10.1", + "core-foundation-sys", + "debugid", + "flate2", + "fs4", + "futures-util", + "http", + "libc", + "memmap2", + "reqwest", + "samply-symbols", + "scopeguard", + "symsrv", + "thiserror 2.0.16", + "tokio", + "uuid", + "yoke", + "yoke-derive", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4906,9 +5923,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.9.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" +checksum = "67a073be99ace1adc48af593701c8015cd9817df372e14a1a6b0ee8f8bf043be" dependencies = [ "async-broadcast", "async-executor", @@ -4930,7 +5947,7 @@ dependencies = [ "serde_repr", "tracing", "uds_windows", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "winnow", "zbus_macros", "zbus_names", @@ -4939,9 +5956,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.9.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" +checksum = "0e80cd713a45a49859dcb648053f63265f4f2851b6420d47a958e5697c68b131" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -5005,6 +6022,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerotrie" version = "0.2.2" @@ -5039,10 +6062,16 @@ dependencies = [ ] [[package]] -name = "zvariant" -version = "5.6.0" +name = "zlib-rs" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zvariant" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" dependencies = [ "endi", "enumflags2", @@ -5055,9 +6084,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.6.0" +version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" +checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -5068,14 +6097,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" dependencies = [ "proc-macro2", "quote", "serde", - "static_assertions", "syn", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index a151041..b9bdac6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,13 +21,17 @@ egui_extras = { version = "0.32", features = ["image"] } egui-notify = "0.20" egui-winit = "0.32" egui-wgpu = { version = "0.32", features = ["winit"] } +fxprof-processed-profile = "0.8" fixed = { version = "1.28", features = ["num-traits"] } gilrs = { version = "0.11", features = ["serde-serialize"] } +gimli = "0.32" hex = "0.4" image = { version = "0.25", default-features = false, features = ["png"] } itertools = "0.14" +normpath = "1" num-derive = "0.4" num-traits = "0.2" +object = "0.37" oneshot = "0.1" pollster = "0.4" rand = "0.9" @@ -41,6 +45,7 @@ tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", " tracing = { version = "0.1", features = ["release_max_level_info"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } wgpu = "25" +wholesym = "0.8" winit = { version = "0.30", features = ["serde"] } [target.'cfg(windows)'.dependencies] diff --git a/build.rs b/build.rs index aeb1a79..57b8731 100644 --- a/build.rs +++ b/build.rs @@ -23,7 +23,9 @@ fn main() -> Result<(), Box> { .define("VB_LITTLE_ENDIAN", None) .define("VB_SIGNED_PROPAGATE", None) .define("VB_DIV_GENERIC", None) + .define("VB_DIRECT_EXCEPTION", "on_exception") .define("VB_DIRECT_EXECUTE", "on_execute") + .define("VB_DIRECT_FETCH", "on_fetch") .define("VB_DIRECT_FRAME", "on_frame") .define("VB_DIRECT_READ", "on_read") .define("VB_DIRECT_WRITE", "on_write") diff --git a/src/app.rs b/src/app.rs index 2c299d2..0ad3ee1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,8 +24,8 @@ use crate::{ persistence::Persistence, window::{ AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow, - GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, RegisterWindow, TerminalWindow, - WorldWindow, + GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, ProfileWindow, RegisterWindow, + TerminalWindow, WorldWindow, }, }; @@ -54,6 +54,7 @@ pub struct Application { viewports: HashMap, focused: Option, init_debug_port: Option, + init_profiling: bool, } impl Application { @@ -61,6 +62,7 @@ impl Application { client: EmulatorClient, proxy: EventLoopProxy, debug_port: Option, + profiling: bool, ) -> Self { let wgpu = WgpuState::new(); let icon = load_icon().ok().map(Arc::new); @@ -89,6 +91,7 @@ impl Application { viewports: HashMap::new(), focused: None, init_debug_port: debug_port, + init_profiling: profiling, } } @@ -121,6 +124,11 @@ impl ApplicationHandler for Application { SimId::Player1, ); self.open(event_loop, Box::new(app)); + if self.init_profiling { + let mut profiler = ProfileWindow::new(SimId::Player1, self.client.clone()); + profiler.launch(); + self.open(event_loop, Box::new(profiler)); + } } fn window_event( @@ -247,6 +255,10 @@ impl ApplicationHandler for Application { let terminal = TerminalWindow::new(sim_id, &self.client); self.open(event_loop, Box::new(terminal)); } + UserEvent::OpenProfiler(sim_id) => { + let profile = ProfileWindow::new(sim_id, self.client.clone()); + self.open(event_loop, Box::new(profile)); + } UserEvent::OpenDebugger(sim_id) => { let debugger = GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); @@ -521,6 +533,7 @@ pub enum UserEvent { OpenFrameBuffers(SimId), OpenRegisters(SimId), OpenTerminal(SimId), + OpenProfiler(SimId), OpenDebugger(SimId), OpenInput, OpenHotkeys, diff --git a/src/emulator.rs b/src/emulator.rs index 85ac5f1..5c2c91c 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -18,15 +18,20 @@ use tracing::{error, warn}; use crate::{ audio::Audio, - emulator::cart::Cart, graphics::TextureSink, memory::{MemoryRange, MemoryRegion}, }; +use cart::Cart; +pub use game_info::GameInfo; +pub use inline_stack_map::InlineStack; +use inline_stack_map::InlineStackMap; use shrooms_vb_core::{EXPECTED_FRAME_SIZE, Sim, StopReason}; -pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType}; +pub use shrooms_vb_core::{SimEvent, VBKey, VBRegister, VBWatchpointType}; mod address_set; mod cart; +mod game_info; +mod inline_stack_map; mod shrooms_vb_core; #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] @@ -137,6 +142,7 @@ pub struct Emulator { state: Arc>, audio_on: Arc<[AtomicBool; 2]>, linked: Arc, + profilers: [Option; 2], renderers: HashMap, messages: HashMap>, debuggers: HashMap, @@ -164,6 +170,7 @@ impl Emulator { state, audio_on, linked, + profilers: [None, None], renderers: HashMap::new(), messages: HashMap::new(), debuggers: HashMap::new(), @@ -218,6 +225,22 @@ impl Emulator { self.carts[index] = Some(cart); self.sim_state[index].store(SimState::Ready, Ordering::Release); } + let mut profiling = false; + if let Some(profiler) = self.profilers[sim_id.to_index()].as_ref() + && let Some(cart) = self.carts[index].as_ref() + && profiler + .send(ProfileEvent::Start { + info: cart.info.clone(), + }) + .is_ok() + { + sim.monitor_events(true, cart.info.inline_stack_map().clone()); + profiling = true; + } + if !profiling { + sim.monitor_events(false, InlineStackMap::empty()); + } + if self.sim_state[index].load(Ordering::Acquire) == SimState::Ready { self.resume_sims(); } @@ -315,6 +338,11 @@ impl Emulator { Ok(()) } + fn start_profiling(&mut self, sim_id: SimId, sender: ProfileSender) -> Result<()> { + self.profilers[sim_id.to_index()] = Some(sender); + self.reset_sim(sim_id, None) + } + fn start_debugging(&mut self, sim_id: SimId, sender: DebugSender) { if self.sim_state[sim_id.to_index()].load(Ordering::Acquire) != SimState::Ready { // Can't debug unless a game is connected @@ -438,12 +466,32 @@ impl Emulator { let p1_running = running && p1_state == SimState::Ready; let p2_running = running && p2_state == SimState::Ready; let mut idle = !p1_running && !p2_running; - if p1_running && p2_running { - Sim::emulate_many(&mut self.sims); - } else if p1_running { - self.sims[SimId::Player1.to_index()].emulate(); - } else if p2_running { - self.sims[SimId::Player2.to_index()].emulate(); + + let cycles = self.emulate(p1_running, p2_running); + + // if we're profiling, track events + for ((sim, profiler), running) in self + .sims + .iter_mut() + .zip(self.profilers.iter_mut()) + .zip([p1_running, p2_running]) + { + if !running { + continue; + } + if let Some(p) = profiler { + let (event, inline_stack) = sim.take_profiler_updates(); + if p.send(ProfileEvent::Update { + cycles, + event, + inline_stack, + }) + .is_err() + { + sim.monitor_events(false, InlineStackMap::empty()); + *profiler = None; + } + } } if state == EmulatorState::Stepping { @@ -470,7 +518,7 @@ impl Emulator { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { continue; }; - if let Some(reason) = sim.stop_reason() { + if let Some(reason) = sim.take_stop_reason() { let stop_reason = match reason { StopReason::Stepped => DebugStopReason::Trace, StopReason::Watchpoint(watch, address) => { @@ -529,6 +577,19 @@ impl Emulator { idle } + fn emulate(&mut self, p1_running: bool, p2_running: bool) -> u32 { + const MAX_CYCLES: u32 = 20_000_000; + let mut cycles = MAX_CYCLES; + if p1_running && p2_running { + Sim::emulate_many(&mut self.sims, &mut cycles); + } else if p1_running { + self.sims[SimId::Player1.to_index()].emulate(&mut cycles); + } else if p2_running { + self.sims[SimId::Player2.to_index()].emulate(&mut cycles); + } + MAX_CYCLES - cycles + } + fn handle_command(&mut self, command: EmulatorCommand) { match command { EmulatorCommand::ConnectToSim(sim_id, renderer, messages) => { @@ -572,6 +633,11 @@ impl Emulator { self.report_error(SimId::Player1, format!("Error setting speed: {error}")); } } + EmulatorCommand::StartProfiling(sim_id, profiler) => { + if let Err(error) = self.start_profiling(sim_id, profiler) { + self.report_error(SimId::Player1, format!("Error enaling profiler: {error}")); + } + } EmulatorCommand::StartDebugging(sim_id, debugger) => { self.start_debugging(sim_id, debugger); } @@ -709,6 +775,7 @@ pub enum EmulatorCommand { Resume, FrameAdvance, SetSpeed(f64), + StartProfiling(SimId, ProfileSender), StartDebugging(SimId, DebugSender), StopDebugging(SimId), DebugInterrupt(SimId), @@ -750,6 +817,7 @@ pub enum EmulatorState { Debugging, } +type ProfileSender = tokio::sync::mpsc::UnboundedSender; type DebugSender = tokio::sync::mpsc::UnboundedSender; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -773,6 +841,17 @@ pub enum DebugEvent { Stopped(DebugStopReason), } +pub enum ProfileEvent { + Start { + info: Arc, + }, + Update { + cycles: u32, + event: Option, + inline_stack: Option, + }, +} + #[derive(Clone)] pub struct EmulatorClient { queue: mpsc::Sender, diff --git a/src/emulator/cart.rs b/src/emulator/cart.rs index 79f53c3..efc7730 100644 --- a/src/emulator/cart.rs +++ b/src/emulator/cart.rs @@ -4,20 +4,24 @@ use std::{ fs::{self, File}, io::{Read, Seek as _, SeekFrom, Write as _}, path::{Path, PathBuf}, + sync::Arc, }; -use crate::emulator::SimId; +use crate::emulator::{SimId, game_info::GameInfo}; pub struct Cart { pub file_path: PathBuf, pub rom: Vec, sram_file: File, pub sram: Vec, + pub info: Arc, } impl Cart { pub fn load(file_path: &Path, sim_id: SimId) -> Result { let rom = fs::read(file_path)?; + let (rom, info) = + try_parse_elf(file_path, &rom).unwrap_or_else(|| (rom, GameInfo::empty(file_path))); let mut sram_file = File::options() .read(true) @@ -45,6 +49,7 @@ impl Cart { rom, sram_file, sram, + info: Arc::new(info), }) } @@ -55,6 +60,44 @@ impl Cart { } } +fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec, GameInfo)> { + use object::read::elf::FileHeader; + let program = match object::FileKind::parse(data).ok()? { + object::FileKind::Elf32 => { + let header = object::elf::FileHeader32::parse(data).ok()?; + parse_elf_program(header, data)? + } + object::FileKind::Elf64 => { + let header = object::elf::FileHeader64::parse(data).ok()?; + parse_elf_program(header, data)? + } + _ => return None, + }; + let info = GameInfo::new(file_path, data).unwrap_or_else(|_| GameInfo::empty(file_path)); + Some((program, info)) +} + +fn parse_elf_program>( + header: &Elf, + data: &[u8], +) -> Option> { + use object::read::elf::ProgramHeader; + let endian = header.endian().ok()?; + let mut bytes = vec![]; + let mut pstart = None; + for phdr in header.program_headers(endian, data).ok()? { + if phdr.p_filesz(endian).into() == 0 { + continue; + } + let start = pstart.unwrap_or(phdr.p_paddr(endian).into()); + pstart = Some(start); + bytes.resize((phdr.p_paddr(endian).into() - start) as usize, 0); + let data = phdr.data(endian, data).ok()?; + bytes.extend_from_slice(data); + } + Some(bytes) +} + fn sram_path(file_path: &Path, sim_id: SimId) -> PathBuf { match sim_id { SimId::Player1 => file_path.with_extension("p1.sram"), diff --git a/src/emulator/game_info.rs b/src/emulator/game_info.rs new file mode 100644 index 0000000..9f72e7c --- /dev/null +++ b/src/emulator/game_info.rs @@ -0,0 +1,201 @@ +use std::{borrow::Cow, path::Path, sync::Arc}; + +use anyhow::{Result, bail}; +use fxprof_processed_profile::{LibraryInfo, Symbol, SymbolTable, debugid::DebugId}; +use object::{Object, ObjectSection, ObjectSymbol}; +use wholesym::samply_symbols::{DebugIdExt, demangle_any}; + +use crate::emulator::inline_stack_map::{InlineStackMap, InlineStackMapBuilder}; + +#[derive(Debug)] +pub struct GameInfo { + library_info: LibraryInfo, + inline_stack_map: InlineStackMap, +} + +impl GameInfo { + pub fn new(file_path: &Path, input: &[u8]) -> Result { + let file = object::File::parse(input)?; + + let (name, path) = name_and_path(file_path); + let debug_id = file + .build_id()? + .map(|id| DebugId::from_identifier(id, true)) + .unwrap_or_default(); + let code_id = file.build_id()?.map(hex::encode); + let mut symbols = vec![]; + for sym in file.symbols() { + symbols.push(Symbol { + address: sym.address() as u32, + size: Some(sym.size() as u32), + name: demangle_any(sym.name()?), + }); + } + + let inline_stack_map = + build_inline_stack_map(file).unwrap_or_else(|_| InlineStackMap::empty()); + + let library_info = LibraryInfo { + name: name.clone(), + debug_name: name, + path: path.clone(), + debug_path: path, + debug_id, + code_id, + arch: None, + symbol_table: Some(Arc::new(SymbolTable::new(symbols))), + }; + + Ok(Self { + library_info, + inline_stack_map, + }) + } + + pub fn empty(file_path: &Path) -> Self { + let (name, path) = name_and_path(file_path); + let library_info = LibraryInfo { + name: name.clone(), + debug_name: name, + path: path.clone(), + debug_path: path, + debug_id: DebugId::default(), + code_id: None, + arch: None, + symbol_table: None, + }; + let inline_stack_map = InlineStackMap::empty(); + Self { + library_info, + inline_stack_map, + } + } + + pub fn name(&self) -> &str { + &self.library_info.name + } + + pub fn library_info(&self) -> &LibraryInfo { + &self.library_info + } + + pub fn inline_stack_map(&self) -> &InlineStackMap { + &self.inline_stack_map + } +} + +fn build_inline_stack_map(file: object::File) -> Result { + let endian = if file.is_little_endian() { + gimli::RunTimeEndian::Little + } else { + gimli::RunTimeEndian::Big + }; + fn load_section<'a>(file: &'a object::File, id: gimli::SectionId) -> Result> { + let input = match file.section_by_name(id.name()) { + Some(section) => section.uncompressed_data()?, + None => Cow::Owned(vec![]), + }; + Ok(input) + } + let dorf = gimli::DwarfSections::load(|id| load_section(&file, id))?; + let dorf = dorf.borrow(|sec| gimli::EndianSlice::new(sec, endian)); + let mut units = dorf.units(); + let mut frames = InlineStackMap::builder(); + while let Some(header) = units.next()? { + let unit = dorf.unit(header)?; + let mut entree = unit.entries_tree(None)?; + let root = entree.root()?; + let mut ctx = ParseContext { + dorf: &dorf, + unit: &unit, + frames: &mut frames, + }; + parse_inline(&mut ctx, root)?; + } + Ok(frames.build()) +} + +type Reader<'a> = gimli::EndianSlice<'a, gimli::RunTimeEndian>; + +struct ParseContext<'a> { + dorf: &'a gimli::Dwarf>, + unit: &'a gimli::Unit>, + frames: &'a mut InlineStackMapBuilder, +} +impl ParseContext<'_> { + fn name_attr(&self, attr: gimli::AttributeValue) -> Result> { + match attr { + gimli::AttributeValue::DebugInfoRef(offset) => { + let mut units = self.dorf.units(); + while let Some(header) = units.next()? { + if let Some(offset) = offset.to_unit_offset(&header) { + let unit = self.dorf.unit(header)?; + return self.name_entry(&unit, offset); + } + } + Ok(None) + } + gimli::AttributeValue::UnitRef(offset) => self.name_entry(self.unit, offset), + other => { + bail!("unrecognized attr {other:?}"); + } + } + } + + fn name_entry( + &self, + unit: &gimli::Unit, + offset: gimli::UnitOffset, + ) -> Result> { + let abbreviations = self.dorf.abbreviations(&unit.header)?; + let mut entries = unit.header.entries_raw(&abbreviations, Some(offset))?; + let Some(abbrev) = entries.read_abbreviation()? else { + return Ok(None); + }; + let mut name = None; + for spec in abbrev.attributes() { + let attr = entries.read_attribute(*spec)?; + if attr.name() == gimli::DW_AT_linkage_name + || (attr.name() == gimli::DW_AT_name && name.is_none()) + { + name = Some(self.dorf.attr_string(unit, attr.value())?) + } + } + Ok(name.map(|n| demangle_any(&String::from_utf8_lossy(&n)))) + } +} + +fn parse_inline(ctx: &mut ParseContext, node: gimli::EntriesTreeNode) -> Result<()> { + if node.entry().tag() == gimli::DW_TAG_inlined_subroutine + && let Some(attr) = node.entry().attr_value(gimli::DW_AT_abstract_origin)? + && let Some(name) = ctx.name_attr(attr)? + { + let name = Arc::new(name); + let mut ranges = ctx.dorf.die_ranges(ctx.unit, node.entry())?; + while let Some(range) = ranges.next()? { + let start = range.begin as u32; + let end = range.end as u32; + ctx.frames.add(start, end, name.clone()); + } + } + let mut children = node.children(); + while let Some(child) = children.next()? { + parse_inline(ctx, child)?; + } + Ok(()) +} + +fn name_and_path(file_path: &Path) -> (String, String) { + let normalized = normpath::PathExt::normalize(file_path); + let path = normalized + .as_ref() + .map(|n| n.as_path()) + .unwrap_or(file_path); + + let name = match path.file_stem() { + Some(s) => s.to_string_lossy().into_owned(), + None => "game".to_string(), + }; + let path = path.to_string_lossy().into_owned(); + (name, path) +} diff --git a/src/emulator/inline_stack_map.rs b/src/emulator/inline_stack_map.rs new file mode 100644 index 0000000..ca1f50c --- /dev/null +++ b/src/emulator/inline_stack_map.rs @@ -0,0 +1,87 @@ +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, +}; + +pub type InlineStack = Arc>>; + +#[derive(Debug, Clone)] +pub struct InlineStackMap { + entries: Vec<(u32, InlineStack)>, + empty: InlineStack, +} + +impl InlineStackMap { + pub fn empty() -> Self { + Self { + entries: vec![], + empty: Arc::new(vec![]), + } + } + pub fn builder() -> InlineStackMapBuilder { + InlineStackMapBuilder { + events: BTreeMap::new(), + } + } + pub fn get(&self, address: u32) -> &InlineStack { + match self.entries.binary_search_by_key(&address, |(a, _)| *a) { + Ok(index) => self.entries.get(index), + Err(after) => after.checked_sub(1).and_then(|i| self.entries.get(i)), + } + .map(|(_, s)| s) + .unwrap_or(&self.empty) + } +} + +#[derive(Default)] +struct Event { + end: usize, + start: Vec>, +} + +pub struct InlineStackMapBuilder { + events: BTreeMap, +} + +impl InlineStackMapBuilder { + pub fn add(&mut self, start: u32, end: u32, name: Arc) { + self.events.entry(start).or_default().start.push(name); + self.events.entry(end).or_default().end += 1; + } + + pub fn build(self) -> InlineStackMap { + let empty = Arc::new(vec![]); + let mut entries = vec![]; + let mut stack_indexes = vec![]; + let mut stack = vec![]; + let mut string_dedup = HashMap::new(); + let mut stack_dedup = BTreeMap::new(); + stack_dedup.insert(vec![], empty.clone()); + + for (address, event) in self.events { + for _ in 0..event.end { + stack.pop(); + stack_indexes.pop(); + } + for call in event.start { + if let Some(index) = string_dedup.get(&call) { + stack.push(call); + stack_indexes.push(*index); + } else { + let index = string_dedup.len(); + string_dedup.insert(call.clone(), index); + stack.push(call); + stack_indexes.push(index); + } + } + if let Some(stack) = stack_dedup.get(&stack_indexes) { + entries.push((address, stack.clone())); + } else { + let stack = Arc::new(stack.clone()); + stack_dedup.insert(stack_indexes.clone(), stack.clone()); + entries.push((address, stack)); + } + } + InlineStackMap { entries, empty } + } +} diff --git a/src/emulator/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs index 772bdf8..ffe98e7 100644 --- a/src/emulator/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -1,10 +1,12 @@ -use std::{ffi::c_void, ptr, slice}; +use std::{borrow::Cow, ffi::c_void, ptr, slice, sync::Arc}; use anyhow::{Result, anyhow}; use bitflags::bitflags; use num_derive::{FromPrimitive, ToPrimitive}; use serde::{Deserialize, Serialize}; +use crate::emulator::inline_stack_map::{InlineStack, InlineStackMap}; + use super::address_set::AddressSet; #[repr(C)] @@ -71,8 +73,16 @@ pub enum VBWatchpointType { Access, } +type OnException = extern "C" fn(sim: *mut VB, cause: *mut u16) -> c_int; type OnExecute = extern "C" fn(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int; +type OnFetch = extern "C" fn( + sim: *mut VB, + fetch: c_int, + address: u32, + value: *mut i32, + cycles: *mut u32, +) -> c_int; type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; type OnRead = extern "C" fn( sim: *mut VB, @@ -135,8 +145,15 @@ unsafe extern "C" { fn vb_set_cart_ram(sim: *mut VB, sram: *mut c_void, size: u32) -> c_int; #[link_name = "vbSetCartROM"] fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int; + #[link_name = "vbSetExceptionCallback"] + fn vb_set_exception_callback( + sim: *mut VB, + callback: Option, + ) -> Option; #[link_name = "vbSetExecuteCallback"] fn vb_set_execute_callback(sim: *mut VB, callback: Option) -> Option; + #[link_name = "vbSetFetchCallback"] + fn vb_set_fetch_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSetFrameCallback"] fn vb_set_frame_callback(sim: *mut VB, callback: Option) -> Option; #[link_name = "vbSetKeys"] @@ -176,16 +193,27 @@ extern "C" fn on_frame(sim: *mut VB) -> c_int { // There is no way for the userdata to be null or otherwise invalid. let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; data.frame_seen = true; + if data.monitor.enabled { + data.monitor.event = Some(SimEvent::Marker(Cow::Borrowed("Frame Drawn"))); + } 1 } #[unsafe(no_mangle)] -extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int { +extern "C" fn on_execute(sim: *mut VB, address: u32, code: *const u16, length: c_int) -> c_int { // SAFETY: the *mut VB owns its userdata. // There is no way for the userdata to be null or otherwise invalid. let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; - let mut stopped = data.stop_reason.is_some(); + if data.monitor.enabled { + // SAFETY: length is the length of code, in elements + let code = unsafe { slice::from_raw_parts(code, length as usize) }; + data.monitor.detect_event(sim, address, code); + // Something interesting will happen after this instruction is run. + // We'll react in the on_fetch callback it does. + } + + let mut stopped = data.stop_reason.is_some() || data.monitor.event.is_some(); if data.step_from.is_some_and(|s| s != address) { data.step_from = None; data.stop_reason = Some(StopReason::Stepped); @@ -199,6 +227,49 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: if stopped { 1 } else { 0 } } +#[unsafe(no_mangle)] +extern "C" fn on_fetch( + sim: *mut VB, + _fetch: c_int, + address: u32, + _value: *mut i32, + _cycles: *mut u32, +) -> c_int { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; + data.monitor.event = data.monitor.queued_event.take(); + data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(address); + unsafe { vb_set_exception_callback(sim, Some(on_exception)) }; + if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() { + 1 + } else { + 0 + } +} + +#[unsafe(no_mangle)] +extern "C" fn on_exception(sim: *mut VB, cause: *mut u16) -> c_int { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; + let cause = unsafe { *cause }; + let pc = if cause == 0xff70 { + 0xffffff60 + } else { + (cause & 0xfff0) as u32 | 0xffff0000 + }; + data.monitor.event = data.monitor.queued_event.take(); + data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(pc); + data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc)); + unsafe { vb_set_exception_callback(sim, None) }; + if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() { + 1 + } else { + 0 + } +} + #[unsafe(no_mangle)] extern "C" fn on_read( sim: *mut VB, @@ -229,7 +300,7 @@ extern "C" fn on_read( extern "C" fn on_write( sim: *mut VB, address: u32, - _type: VBDataType, + typ_: VBDataType, value: *mut i32, _cycles: *mut u32, _cancel: *mut c_int, @@ -246,6 +317,29 @@ extern "C" fn on_write( } } + // If we have profiling enabled, track custom markers + if data.monitor.enabled { + let normalized_hw_address = address & 0x0700003f; + if normalized_hw_address == 0x02000038 && matches!(typ_, VBDataType::S32) { + assert!(data.monitor.queued_event.is_none()); + // The game has written the address of a null-terminated string + // (whose length is at most 64 bytes). Read that string. + let str_address = unsafe { *value } as u32; + let mut bytes = [0u8; 64]; + let mut len = 0; + for (dst, src_address) in bytes.iter_mut().zip(str_address..str_address + 64) { + let char = unsafe { vb_read(sim, src_address, VBDataType::U8) } as u8; + if char == 0 { + break; + } + *dst = char; + len += 1; + } + let name = String::from_utf8_lossy(&bytes[..len]).into_owned(); + data.monitor.queued_event = Some(SimEvent::Marker(Cow::Owned(name))); + } + } + if let Some(start) = data.write_watchpoints.start_of_range_containing(address) { let watch = if data.read_watchpoints.contains(address) { VBWatchpointType::Access @@ -260,6 +354,117 @@ extern "C" fn on_write( 0 } +#[allow(dead_code)] +#[derive(Debug)] +pub enum SimEvent { + Call(u32), + Return, + Halt, + Interrupt(u16, u32), + Reti, + Marker(Cow<'static, str>), +} + +struct EventMonitor { + enabled: bool, + event: Option, + queued_event: Option, + just_halted: bool, + inline_stack_map: InlineStackMap, + new_inline_stack: Option, + last_inline_stack: InlineStack, +} + +impl EventMonitor { + fn new() -> Self { + let inline_stack_map = InlineStackMap::empty(); + let last_inline_stack = inline_stack_map.get(0).clone(); + Self { + enabled: false, + event: None, + queued_event: None, + just_halted: false, + inline_stack_map, + new_inline_stack: None, + last_inline_stack, + } + } + + fn detect_new_inline_stack(&mut self, address: u32) -> Option { + let stack = self.inline_stack_map.get(address); + if Arc::ptr_eq(stack, &self.last_inline_stack) { + return None; + } + self.last_inline_stack = stack.clone(); + Some(stack.clone()) + } + + fn detect_event(&mut self, sim: *mut VB, address: u32, code: &[u16]) -> bool { + self.queued_event = self.do_detect_event(sim, address, code); + self.queued_event.is_some() + } + + fn do_detect_event(&mut self, sim: *mut VB, address: u32, code: &[u16]) -> Option { + const HALT_OPCODE: u16 = 0b011010; + const JAL_OPCODE: u16 = 0b101011; + const JMP_OPCODE: u16 = 0b000110; + const RETI_OPCODE: u16 = 0b011001; + + const fn format_i_reg_1(code: &[u16]) -> u8 { + (code[0] & 0x1f) as u8 + } + + const fn format_iv_disp(code: &[u16]) -> i32 { + let value = ((code[0] & 0x3ff) as i32) << 16 | (code[1] as i32); + value << 6 >> 6 + } + + let opcode = code[0] >> 10; + + if opcode == HALT_OPCODE { + if !self.just_halted { + self.just_halted = true; + self.event = Some(SimEvent::Halt); + } else { + self.just_halted = false; + } + // Don't _return_ an event, we want to emit this right away. + // If the CPU is halting, no other callbacks will run for a long time. + return None; + } + + if opcode == JAL_OPCODE { + let disp = format_iv_disp(code); + if disp != 4 { + // JAL .+4 is how programs get r31 to a known value for indirect calls + // (which we detect later.) + // Any other JAL is a function call. + return Some(SimEvent::Call(address.wrapping_add_signed(disp))); + } + } + + if opcode == JMP_OPCODE { + let jmp_reg = format_i_reg_1(code); + if jmp_reg == 31 { + // JMP[r31] is a return + return Some(SimEvent::Return); + } + let r31 = unsafe { vb_get_program_register(sim, 31) }; + if r31 as u32 == address.wrapping_add(2) { + // JMP anywhere else, if r31 points to after the JMP, is an indirect call + let target = unsafe { vb_get_program_register(sim, jmp_reg as u32) }; + return Some(SimEvent::Call(target as u32)); + } + } + + if opcode == RETI_OPCODE { + return Some(SimEvent::Reti); + } + + None + } +} + const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4; const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2; pub const EXPECTED_FRAME_SIZE: usize = 834 * 2; @@ -267,6 +472,7 @@ pub const EXPECTED_FRAME_SIZE: usize = 834 * 2; struct VBState { frame_seen: bool, stop_reason: Option, + monitor: EventMonitor, step_from: Option, breakpoints: Vec, read_watchpoints: AddressSet, @@ -277,10 +483,15 @@ struct VBState { impl VBState { fn needs_execute_callback(&self) -> bool { self.step_from.is_some() + || self.monitor.enabled || !self.breakpoints.is_empty() || !self.read_watchpoints.is_empty() || !self.write_watchpoints.is_empty() } + + fn needs_write_callback(&self) -> bool { + self.stdout.is_some() || self.monitor.enabled || !self.write_watchpoints.is_empty() + } } pub enum StopReason { @@ -311,6 +522,7 @@ impl Sim { let state = VBState { frame_seen: false, stop_reason: None, + monitor: EventMonitor::new(), step_from: None, breakpoints: vec![], read_watchpoints: AddressSet::new(), @@ -332,6 +544,33 @@ impl Sim { unsafe { vb_reset(self.sim) }; } + pub fn monitor_events(&mut self, enabled: bool, inline_stack_map: InlineStackMap) { + let state = self.get_state(); + state.monitor.enabled = enabled; + state.monitor.event = None; + state.monitor.queued_event = None; + state.monitor.new_inline_stack = None; + state.monitor.last_inline_stack = inline_stack_map.get(0).clone(); + state.monitor.inline_stack_map = inline_stack_map; + if enabled { + unsafe { vb_set_execute_callback(self.sim, Some(on_execute)) }; + unsafe { vb_set_exception_callback(self.sim, Some(on_exception)) }; + unsafe { vb_set_fetch_callback(self.sim, Some(on_fetch)) }; + unsafe { vb_set_write_callback(self.sim, Some(on_write)) }; + } else { + let needs_execute = state.needs_execute_callback(); + let needs_write = state.needs_write_callback(); + if !needs_execute { + unsafe { vb_set_execute_callback(self.sim, None) }; + } + unsafe { vb_set_exception_callback(self.sim, None) }; + unsafe { vb_set_fetch_callback(self.sim, None) }; + if !needs_write { + unsafe { vb_set_write_callback(self.sim, None) }; + } + } + } + pub fn load_cart(&mut self, mut rom: Vec, mut sram: Vec) -> Result<()> { self.unload_cart(); @@ -394,16 +633,14 @@ impl Sim { unsafe { vb_set_peer(self.sim, ptr::null_mut()) }; } - pub fn emulate(&mut self) { - let mut cycles = 20_000_000; - unsafe { vb_emulate(self.sim, &mut cycles) }; + pub fn emulate(&mut self, cycles: &mut u32) { + unsafe { vb_emulate(self.sim, cycles) }; } - pub fn emulate_many(sims: &mut [Sim]) { - let mut cycles = 20_000_000; + pub fn emulate_many(sims: &mut [Sim], cycles: &mut u32) { let count = sims.len() as c_uint; let sims = sims.as_mut_ptr().cast(); - unsafe { vb_emulate_ex(sims, count, &mut cycles) }; + unsafe { vb_emulate_ex(sims, count, cycles) }; } pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool { @@ -572,9 +809,10 @@ impl Sim { fn remove_write_watchpoint(&mut self, address: u32, length: usize) { let state = self.get_state(); state.write_watchpoints.remove(address, length); + let needs_write = state.needs_write_callback(); let needs_execute = state.needs_execute_callback(); if state.write_watchpoints.is_empty() { - if state.stdout.is_none() { + if !needs_write { unsafe { vb_set_write_callback(self.sim, None) }; } if !needs_execute { @@ -598,12 +836,15 @@ impl Sim { data.breakpoints.clear(); data.read_watchpoints.clear(); data.write_watchpoints.clear(); - let needs_write = data.stdout.is_some(); + let needs_write = data.needs_write_callback(); + let needs_execute = data.needs_execute_callback(); unsafe { vb_set_read_callback(self.sim, None) }; if !needs_write { unsafe { vb_set_write_callback(self.sim, None) }; } - unsafe { vb_set_execute_callback(self.sim, None) }; + if !needs_execute { + unsafe { vb_set_execute_callback(self.sim, None) }; + } } pub fn watch_stdout(&mut self, watch: bool) { @@ -615,7 +856,7 @@ impl Sim { } } else { data.stdout.take(); - if data.write_watchpoints.is_empty() { + if !data.needs_write_callback() { unsafe { vb_set_write_callback(self.sim, None) }; } } @@ -632,7 +873,7 @@ impl Sim { Some(string) } - pub fn stop_reason(&mut self) -> Option { + pub fn take_stop_reason(&mut self) -> Option { let data = self.get_state(); let reason = data.stop_reason.take(); if !data.needs_execute_callback() { @@ -641,6 +882,13 @@ impl Sim { reason } + pub fn take_profiler_updates(&mut self) -> (Option, Option) { + let data = self.get_state(); + let event = data.monitor.event.take(); + let inline_stack = data.monitor.new_inline_stack.take(); + (event, inline_stack) + } + fn get_state(&mut self) -> &mut VBState { // SAFETY: the *mut VB owns its userdata. // There is no way for the userdata to be null or otherwise invalid. diff --git a/src/main.rs b/src/main.rs index a9b0b46..04f823c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ mod images; mod input; mod memory; mod persistence; +mod profiler; mod window; #[derive(Parser)] @@ -31,6 +32,9 @@ struct Args { /// Start a GDB/LLDB debug server on this port. #[arg(short, long)] debug_port: Option, + /// Enable profiling a game + #[arg(short, long)] + profile: bool, } fn init_logger() { @@ -106,6 +110,9 @@ fn main() -> Result<()> { } builder = builder.start_paused(true); } + if args.profile { + builder = builder.start_paused(true) + } ThreadBuilder::default() .name("Emulator".to_owned()) @@ -124,6 +131,11 @@ fn main() -> Result<()> { let event_loop = EventLoop::with_user_event().build().unwrap(); event_loop.set_control_flow(ControlFlow::Poll); let proxy = event_loop.create_proxy(); - event_loop.run_app(&mut Application::new(client, proxy, args.debug_port))?; + event_loop.run_app(&mut Application::new( + client, + proxy, + args.debug_port, + args.profile, + ))?; Ok(()) } diff --git a/src/profiler.rs b/src/profiler.rs new file mode 100644 index 0000000..8148aad --- /dev/null +++ b/src/profiler.rs @@ -0,0 +1,264 @@ +use std::{ + sync::{Arc, Mutex}, + thread, +}; + +use anyhow::Result; +use tokio::{select, sync::mpsc}; + +use crate::emulator::{ + EmulatorClient, EmulatorCommand, GameInfo, InlineStack, ProfileEvent, SimEvent, SimId, +}; +use recording::Recording; +use state::ProgramState; + +mod recording; +mod state; + +pub struct Profiler { + sim_id: SimId, + client: EmulatorClient, + status: Arc>, + action: Option>, + killer: Option>, +} + +impl Profiler { + pub fn new(sim_id: SimId, client: EmulatorClient) -> Self { + Self { + sim_id, + client, + status: Arc::new(Mutex::new(ProfilerStatus::Disabled)), + action: None, + killer: None, + } + } + + pub fn status(&self) -> ProfilerStatus { + self.status.lock().unwrap().clone() + } + + pub fn enable(&mut self) { + let sim_id = self.sim_id; + let client = self.client.clone(); + let status = self.status.clone(); + let (action_tx, action_rx) = mpsc::unbounded_channel(); + self.action = Some(action_tx); + let (killer_tx, killer_rx) = oneshot::channel(); + self.killer = Some(killer_tx); + thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + select! { + _ = run_profile(sim_id, client, status.clone(), action_rx) => {} + _ = killer_rx => { + *status.lock().unwrap() = ProfilerStatus::Disabled; + } + } + }) + }); + } + + pub fn disable(&mut self) { + if let Some(killer) = self.killer.take() { + let _ = killer.send(()); + } + } + + pub fn start_recording(&mut self) { + if let Some(action) = &self.action { + let _ = action.send(RecordingAction::Start); + } + } + + pub fn finish_recording(&mut self) -> oneshot::Receiver> { + let (tx, rx) = oneshot::channel(); + if let Some(action) = &self.action { + let _ = action.send(RecordingAction::Finish(tx)); + } + rx + } + + pub fn cancel_recording(&mut self) { + if let Some(action) = &self.action { + let _ = action.send(RecordingAction::Cancel); + } + } +} + +impl Drop for Profiler { + fn drop(&mut self) { + self.disable(); + } +} + +async fn run_profile( + sim_id: SimId, + client: EmulatorClient, + status: Arc>, + mut action_source: mpsc::UnboundedReceiver, +) { + let (profile_sync, mut profile_source) = mpsc::unbounded_channel(); + client.send_command(EmulatorCommand::StartProfiling(sim_id, profile_sync)); + + *status.lock().unwrap() = ProfilerStatus::Enabled; + + let mut session = ProfilerSession::new(); + loop { + select! { + maybe_event = profile_source.recv() => { + let Some(event) = maybe_event else { + break; // emulator thread disconnected + }; + if let Err(error) = handle_event(event, &mut session).await { + *status.lock().unwrap() = ProfilerStatus::Error(error.to_string()); + return; + } + } + maybe_action = action_source.recv() => { + let Some(action) = maybe_action else { + break; // ui thread disconnected + }; + handle_action(action, &mut session, &status); + } + } + } + + *status.lock().unwrap() = ProfilerStatus::Disabled; +} + +async fn handle_event(event: ProfileEvent, session: &mut ProfilerSession) -> Result<()> { + match event { + ProfileEvent::Start { info } => session.start_profiling(info).await, + ProfileEvent::Update { + cycles, + event, + inline_stack, + } => { + session.track_elapsed_cycles(cycles); + if let Some(event) = event { + session.track_event(event)?; + } + if let Some(stack) = inline_stack { + session.track_inline_stack(stack); + } + } + } + Ok(()) +} + +fn handle_action( + action: RecordingAction, + session: &mut ProfilerSession, + status: &Mutex, +) { + match action { + RecordingAction::Start => { + session.start_recording(); + *status.lock().unwrap() = ProfilerStatus::Recording; + } + RecordingAction::Finish(rx) => { + if let Some(bytes) = session.finish_recording() { + let _ = rx.send(bytes); + } + *status.lock().unwrap() = ProfilerStatus::Enabled; + } + RecordingAction::Cancel => { + session.cancel_recording(); + *status.lock().unwrap() = ProfilerStatus::Enabled; + } + } +} + +#[derive(Clone)] +pub enum ProfilerStatus { + Disabled, + Enabled, + Recording, + Error(String), +} + +impl ProfilerStatus { + pub fn enabled(&self) -> bool { + matches!(self, Self::Enabled | Self::Recording) + } +} + +enum RecordingAction { + Start, + Finish(oneshot::Sender>), + Cancel, +} + +struct ProfilerSession { + program: Option, + recording: Option, +} + +impl ProfilerSession { + fn new() -> Self { + Self { + program: None, + recording: None, + } + } + + async fn start_profiling(&mut self, info: Arc) { + let program = ProgramState::new(info).await; + let recording = if self.recording.is_some() { + Some(Recording::new(&program)) + } else { + None + }; + self.program = Some(program); + self.recording = recording; + } + + fn track_elapsed_cycles(&mut self, cycles: u32) { + if let (Some(state), Some(recording)) = (&self.program, &mut self.recording) { + recording.track_elapsed_cycles(state, cycles); + } + } + + fn track_event(&mut self, event: SimEvent) -> Result<()> { + let Some(program) = &mut self.program else { + return Ok(()); + }; + match event { + SimEvent::Call(address) => program.track_call(address), + SimEvent::Return => program.track_return(), + SimEvent::Halt => program.track_halt(), + SimEvent::Interrupt(code, address) => program.track_interrupt(code, address), + SimEvent::Reti => program.track_reti(), + SimEvent::Marker(name) => { + if let Some(recording) = &mut self.recording { + recording.track_marker(name); + }; + Ok(()) + } + } + } + + fn track_inline_stack(&mut self, inline_stack: InlineStack) { + if let Some(program) = &mut self.program { + program.track_inline_stack(inline_stack); + } + } + + fn start_recording(&mut self) { + if let Some(program) = &self.program { + self.recording = Some(Recording::new(program)); + } + } + + fn finish_recording(&mut self) -> Option> { + self.recording.take().map(|r| r.finish()) + } + + fn cancel_recording(&mut self) { + self.recording.take(); + } +} diff --git a/src/profiler/recording.rs b/src/profiler/recording.rs new file mode 100644 index 0000000..69ee13e --- /dev/null +++ b/src/profiler/recording.rs @@ -0,0 +1,152 @@ +use std::{borrow::Cow, collections::HashMap}; + +use fxprof_processed_profile::{ + CategoryHandle, CpuDelta, Frame, FrameFlags, FrameInfo, MarkerTiming, ProcessHandle, Profile, + ReferenceTimestamp, SamplingInterval, StackHandle, StaticSchemaMarker, StringHandle, + ThreadHandle, Timestamp, +}; + +use crate::profiler::state::{ProgramState, RESET_CODE, StackFrame}; + +pub struct Recording { + profile: Profile, + process: ProcessHandle, + threads: HashMap, + now: u64, +} + +impl Recording { + pub fn new(state: &ProgramState) -> Self { + let reference_timestamp = ReferenceTimestamp::from_millis_since_unix_epoch(0.0); + let interval = SamplingInterval::from_hz(20_000_000.0); + let mut profile = Profile::new(state.name(), reference_timestamp, interval); + + let process = + profile.add_process(state.name(), 1, Timestamp::from_nanos_since_reference(0)); + + let lib = profile.add_lib(state.library_info().clone()); + profile.add_lib_mapping(process, lib, 0x00000000, 0xffffffff, 0); + + let mut me = Self { + profile, + process, + threads: HashMap::new(), + now: 0, + }; + me.track_elapsed_cycles(state, 0); + me + } + + pub fn track_elapsed_cycles(&mut self, state: &ProgramState, cycles: u32) { + self.now += cycles as u64; + let timestamp = Timestamp::from_nanos_since_reference(self.now * 50); + let weight = 1; + + let active_code = if let Some((code, frames)) = state.current_stack() { + let thread = *self.threads.entry(code).or_insert_with(|| { + let process = self.process; + let tid = code as u32; + let start_time = Timestamp::from_nanos_since_reference(self.now * 50); + let is_main = code == RESET_CODE; + let thread = self.profile.add_thread(process, tid, start_time, is_main); + self.profile + .set_thread_name(thread, &thread_name_for_code(code)); + thread + }); + + let stack = self.handle_for_stack(thread, frames); + let cpu_delta = CpuDelta::from_nanos((self.now - cycles as u64) * 50); + self.profile + .add_sample(thread, timestamp, stack, cpu_delta, weight); + Some(code) + } else { + None + }; + for (code, thread) in &self.threads { + if active_code == Some(*code) { + continue; + } + self.profile + .add_sample_same_stack_zero_cpu(*thread, timestamp, weight); + } + } + + pub fn track_marker(&mut self, name: Cow<'static, str>) { + let Some(thread) = self.threads.get(&RESET_CODE) else { + return; + }; + let timing = MarkerTiming::Instant(Timestamp::from_nanos_since_reference(self.now * 50)); + let marker = SimpleMarker(name); + self.profile.add_marker(*thread, timing, marker); + } + + pub fn finish(self) -> Vec { + serde_json::to_vec(&self.profile).expect("could not serialize profile") + } + + fn handle_for_stack( + &mut self, + thread: ThreadHandle, + frames: &[StackFrame], + ) -> Option { + let frames = frames + .iter() + .map(|f| { + let frame = match f { + StackFrame::Address(address) => Frame::InstructionPointer(*address as u64), + StackFrame::Label(label) => Frame::Label(self.profile.intern_string(label)), + }; + FrameInfo { + frame, + category_pair: CategoryHandle::OTHER.into(), + flags: FrameFlags::empty(), + } + }) + .collect::>(); + self.profile.intern_stack_frames(thread, frames.into_iter()) + } +} + +struct SimpleMarker(Cow<'static, str>); + +impl StaticSchemaMarker for SimpleMarker { + const UNIQUE_MARKER_TYPE_NAME: &'static str = "Simple"; + const FIELDS: &'static [fxprof_processed_profile::StaticSchemaMarkerField] = &[]; + + fn name(&self, profile: &mut Profile) -> StringHandle { + profile.intern_string(&self.0) + } + + fn category(&self, _profile: &mut Profile) -> CategoryHandle { + CategoryHandle::OTHER + } + + fn string_field_value(&self, _field_index: u32) -> StringHandle { + unreachable!() + } + + fn number_field_value(&self, _field_index: u32) -> f64 { + unreachable!() + } +} + +fn thread_name_for_code(code: u16) -> std::borrow::Cow<'static, str> { + match code { + RESET_CODE => "Main".into(), + 0xffd0 => "Duplexed exception".into(), + 0xfe40 => "VIP interrupt".into(), + 0xfe30 => "Communication interrupt".into(), + 0xfe20 => "Game pak interrupt".into(), + 0xfe10 => "Timer interrupt".into(), + 0xfe00 => "Game pad interrupt".into(), + 0xffc0 => "Address trap".into(), + 0xffa0..0xffc0 => format!("Trap (vector {})", code - 0xffa0).into(), + 0xff90 => "Illegal opcode exception".into(), + 0xff80 => "Zero division exception".into(), + 0xff60 => "Floating-point reserved operand exception".into(), + 0xff70 => "Floating-point invalid operation exception".into(), + 0xff68 => "Floating-point zero division exception".into(), + 0xff64 => "Floating-point overflow exception".into(), + other => format!("Unrecognized handler (0x{other:04x})").into(), + } +} diff --git a/src/profiler/state.rs b/src/profiler/state.rs new file mode 100644 index 0000000..f305bbc --- /dev/null +++ b/src/profiler/state.rs @@ -0,0 +1,124 @@ +use std::{collections::HashMap, sync::Arc}; + +use anyhow::{Result, bail}; +use fxprof_processed_profile::LibraryInfo; + +use crate::emulator::{GameInfo, InlineStack}; + +pub struct ProgramState { + info: Arc, + call_stacks: HashMap>, + context_stack: Vec, +} + +pub enum StackFrame { + Address(u32), + Label(Arc), +} + +pub const RESET_CODE: u16 = 0xfff0; +impl ProgramState { + pub async fn new(info: Arc) -> Self { + let mut call_stacks = HashMap::new(); + call_stacks.insert(RESET_CODE, vec![StackFrame::Address(0xfffffff0)]); + Self { + info, + call_stacks, + context_stack: vec![RESET_CODE], + } + } + + pub fn name(&self) -> &str { + self.info.name() + } + + pub fn library_info(&self) -> &LibraryInfo { + self.info.library_info() + } + + pub fn current_stack(&self) -> Option<(u16, &[StackFrame])> { + let code = self.context_stack.last()?; + let call_stack = self.call_stacks.get(code)?; + Some((*code, call_stack)) + } + + pub fn track_call(&mut self, address: u32) -> Result<()> { + let Some(code) = self.context_stack.last() else { + bail!("How did we call anything when we're halted?"); + }; + let Some(stack) = self.call_stacks.get_mut(code) else { + bail!("missing stack {code:04x}"); + }; + stack.push(StackFrame::Address(address)); + Ok(()) + } + + pub fn track_return(&mut self) -> Result<()> { + let Some(code) = self.context_stack.last() else { + bail!("how did we return when we're halted?"); + }; + let Some(stack) = self.call_stacks.get_mut(code) else { + bail!("missing stack {code:04x}"); + }; + if stack.pop().is_none() { + bail!("returned from {code:04x} but stack was empty"); + } + if stack.is_empty() { + bail!("returned to oblivion"); + } + Ok(()) + } + + pub fn track_halt(&mut self) -> Result<()> { + let Some(RESET_CODE) = self.context_stack.pop() else { + bail!("halted when not in an interrupt"); + }; + Ok(()) + } + + pub fn track_interrupt(&mut self, code: u16, address: u32) -> Result<()> { + // if the CPU was halted before, wake it up now + if self.context_stack.is_empty() { + self.context_stack.push(RESET_CODE); + } + + self.context_stack.push(code); + if self + .call_stacks + .insert(code, vec![StackFrame::Address(address)]) + .is_some() + { + bail!("{code:04x} fired twice"); + } + Ok(()) + } + + pub fn track_reti(&mut self) -> Result<()> { + let Some(code) = self.context_stack.pop() else { + bail!("RETI when halted"); + }; + if code == RESET_CODE { + bail!("RETI when not in interrupt"); + } + if self.call_stacks.remove(&code).is_none() { + bail!("{code:04x} popped but never called"); + } + Ok(()) + } + + pub fn track_inline_stack(&mut self, inline_stack: InlineStack) { + let Some(code) = self.context_stack.last() else { + return; + }; + let Some(call_stack) = self.call_stacks.get_mut(code) else { + return; + }; + while call_stack + .pop_if(|f| matches!(f, StackFrame::Label(_))) + .is_some() + {} + for label in inline_stack.iter() { + call_stack.push(StackFrame::Label(label.clone())); + } + } +} diff --git a/src/window.rs b/src/window.rs index 502fae4..237f262 100644 --- a/src/window.rs +++ b/src/window.rs @@ -4,6 +4,7 @@ pub use game::GameWindow; pub use gdb::GdbServerWindow; pub use hotkeys::HotkeysWindow; pub use input::InputWindow; +pub use profile::ProfileWindow; pub use terminal::TerminalWindow; pub use vip::{ BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow, @@ -18,6 +19,7 @@ mod game_screen; mod gdb; mod hotkeys; mod input; +mod profile; mod terminal; mod utils; mod vip; diff --git a/src/window/game.rs b/src/window/game.rs index c71abd0..3dc9dda 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -228,6 +228,11 @@ impl GameWindow { .send_event(UserEvent::OpenTerminal(self.sim_id)) .unwrap(); } + if ui.button("Profiler").clicked() { + self.proxy + .send_event(UserEvent::OpenProfiler(self.sim_id)) + .unwrap(); + } if ui.button("GDB Server").clicked() { self.proxy .send_event(UserEvent::OpenDebugger(self.sim_id)) diff --git a/src/window/profile.rs b/src/window/profile.rs new file mode 100644 index 0000000..0499bfa --- /dev/null +++ b/src/window/profile.rs @@ -0,0 +1,162 @@ +use std::{fs, time::Duration}; + +use anyhow::Result; +use egui::{Button, CentralPanel, Checkbox, Label, ViewportBuilder, ViewportId}; +use egui_notify::{Anchor, Toast, Toasts}; + +use crate::{ + emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId}, + profiler::{Profiler, ProfilerStatus}, + window::AppWindow, +}; + +pub struct ProfileWindow { + sim_id: SimId, + client: EmulatorClient, + profiler: Profiler, + toasts: Toasts, +} + +impl ProfileWindow { + pub fn new(sim_id: SimId, client: EmulatorClient) -> Self { + Self { + sim_id, + client: client.clone(), + profiler: Profiler::new(sim_id, client), + toasts: Toasts::new() + .with_anchor(Anchor::BottomLeft) + .with_margin((10.0, 10.0).into()) + .reverse(true), + } + } + + pub fn launch(&mut self) { + self.profiler.enable(); + } + + fn finish_recording(&mut self) { + let pause = matches!(self.client.emulator_state(), EmulatorState::Running); + if pause { + self.client.send_command(EmulatorCommand::Pause); + } + match self.try_finish_recording() { + Ok(Some(path)) => { + let mut toast = Toast::info(format!("Saved to {path}")); + toast.duration(Some(Duration::from_secs(5))); + self.toasts.add(toast); + } + Ok(None) => {} + Err(error) => { + let mut toast = Toast::error(format!("{error:#}")); + toast.duration(Some(Duration::from_secs(5))); + self.toasts.add(toast); + } + } + if pause { + self.client.send_command(EmulatorCommand::Resume); + } + } + + fn try_finish_recording(&mut self) -> Result> { + let bytes_receiver = self.profiler.finish_recording(); + let file = rfd::FileDialog::new() + .add_filter("Profiler files", &["json"]) + .set_file_name("profile.json") + .save_file(); + if let Some(path) = file { + let bytes = pollster::block_on(bytes_receiver)?; + fs::write(&path, bytes)?; + Ok(Some(path.display().to_string())) + } else { + self.profiler.cancel_recording(); + Ok(None) + } + } +} + +impl AppWindow for ProfileWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of(format!("Profile-{}", self.sim_id)) + } + + fn sim_id(&self) -> SimId { + self.sim_id + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title(format!("Profiler ({})", self.sim_id)) + .with_inner_size((300.0, 200.0)) + } + + fn show(&mut self, ctx: &egui::Context) { + let status = self.profiler.status(); + let recording = matches!(status, ProfilerStatus::Recording); + CentralPanel::default().show(ctx, |ui| { + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.add( + Label::new( + "Use this tool to record performance profiles of your game, for use in ", + ) + .wrap_mode(egui::TextWrapMode::Wrap), + ); + ui.hyperlink("https://profiler.firefox.com"); + ui.label("."); + }); + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.add( + Label::new("For more instructions, see ").wrap_mode(egui::TextWrapMode::Wrap), + ); + ui.hyperlink_to( + "the Lemur wiki", + "https://git.virtual-boy.com/PVB/lemur/wiki/Profiling-with-Lemur", + ); + ui.label("."); + }); + ui.separator(); + + let mut enabled = status.enabled(); + let enabled_checkbox = Checkbox::new(&mut enabled, "Enable profiling"); + if ui.add_enabled(!recording, enabled_checkbox).changed() { + if enabled { + self.profiler.enable(); + } else { + self.profiler.disable(); + } + } + if !enabled { + ui.label("Enabling profiling will restart your current game."); + } else { + ui.horizontal(|ui| { + if !recording { + let record_button = Button::new("Record"); + let can_record = matches!(status, ProfilerStatus::Enabled); + if ui.add_enabled(can_record, record_button).clicked() { + self.profiler.start_recording(); + } + } else { + if ui.button("Finish recording").clicked() { + self.finish_recording(); + } + if ui.button("Cancel recording").clicked() { + self.profiler.cancel_recording(); + } + } + }); + } + + match &status { + ProfilerStatus::Recording => { + ui.label("Recording..."); + } + ProfilerStatus::Error(message) => { + ui.label(message); + } + _ => {} + } + }); + self.toasts.show(ctx); + } +}