AVM Analyzer initial commit.
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..ca8f068
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+**/target
+**/.vscode
+**/.git
+**/dist
+**/Dockerfile*
+**/*.sh
+!tools/avm_analyzer/build_avm.sh
diff --git a/tools/avm_analyzer/Cargo.lock b/tools/avm_analyzer/Cargo.lock
new file mode 100644
index 0000000..2147d78
--- /dev/null
+++ b/tools/avm_analyzer/Cargo.lock
@@ -0,0 +1,4625 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ab_glyph"
+version = "0.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80179d7dd5d7e8c285d67c4a1e652972a92de7475beddfb92028c76463b13225"
+dependencies = [
+ "ab_glyph_rasterizer",
+ "owned_ttf_parser",
+]
+
+[[package]]
+name = "ab_glyph_rasterizer"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
+
+[[package]]
+name = "accesskit"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb10ed32c63247e4e39a8f42e8e30fb9442fbf7878c8e4a9849e7e381619bea"
+dependencies = [
+ "enumn",
+ "serde",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "ahash"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
+dependencies = [
+ "cfg-if",
+ "getrandom",
+ "once_cell",
+ "serde",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-activity"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39b801912a977c3fd52d80511fe1c0c8480c6f957f21ae2ce1b92ffe970cf4b9"
+dependencies = [
+ "android-properties",
+ "bitflags 2.4.2",
+ "cc",
+ "cesu8",
+ "jni",
+ "jni-sys",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "num_enum",
+ "thiserror",
+]
+
+[[package]]
+name = "android-properties"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
+
+[[package]]
+name = "anstream"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
+
+[[package]]
+name = "arboard"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aafb29b107435aa276664c1db8954ac27a6e105cdad3c88287a199eb0e313c08"
+dependencies = [
+ "clipboard-win",
+ "log",
+ "objc",
+ "objc-foundation",
+ "objc_id",
+ "parking_lot",
+ "thiserror",
+ "winapi",
+ "x11rb 0.12.0",
+]
+
+[[package]]
+name = "as-raw-xcb-connection"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 2.5.3",
+ "futures-core",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 4.0.3",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-fs"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd1f344136bad34df1f83a47f3fd7f2ab85d75cb8a940af4ccf6d482a84ea01b"
+dependencies = [
+ "async-lock 3.3.0",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb41eb19024a91746eba0773aa5e16036045bbf45733766661099e182ea6a744"
+dependencies = [
+ "async-lock 3.3.0",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
+dependencies = [
+ "event-listener 2.5.3",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b"
+dependencies = [
+ "event-listener 4.0.3",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15c1cd5d253ecac3d3cf15e390fd96bd92a13b1d14497d81abf077304794fb04"
+dependencies = [
+ "async-channel 2.1.1",
+ "async-io",
+ "async-lock 3.3.0",
+ "async-signal",
+ "blocking",
+ "cfg-if",
+ "event-listener 4.0.3",
+ "futures-lite",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"
+dependencies = [
+ "async-io",
+ "async-lock 2.8.0",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
+
+[[package]]
+name = "async-trait"
+version = "0.1.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "atomic_enum"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6227a8d6fdb862bcb100c4314d0d9579e5cd73fa6df31a2e6f6e1acd3c5f1207"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "avm-analyzer-app"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "avm-analyzer-common",
+ "avm-stats",
+ "base64",
+ "bincode",
+ "console_error_panic_hook",
+ "convert_case",
+ "eframe",
+ "egui",
+ "egui_dock",
+ "egui_extras",
+ "egui_plot",
+ "ehttp",
+ "ezsockets",
+ "futures",
+ "getrandom",
+ "image",
+ "itertools 0.10.5",
+ "js-sys",
+ "log",
+ "mime",
+ "mime_guess",
+ "once_cell",
+ "ordered-float",
+ "poll-promise",
+ "prost",
+ "rand",
+ "re_memory",
+ "rfd",
+ "ron",
+ "serde",
+ "serde_json",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "web-time 1.0.0",
+ "weezl",
+ "y4m",
+ "zip",
+]
+
+[[package]]
+name = "avm-analyzer-common"
+version = "0.1.0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "avm-analyzer-server"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-fs",
+ "async-process",
+ "avm-analyzer-common",
+ "avm-stats",
+ "axum",
+ "clap",
+ "futures-lite",
+ "image",
+ "prost",
+ "prost-types",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tower",
+ "tower-http",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "avm-stats"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "emath 0.25.0",
+ "itertools 0.10.5",
+ "log",
+ "num",
+ "num-derive",
+ "num-traits",
+ "once_cell",
+ "ordered-float",
+ "prost",
+ "prost-build",
+ "prost-types",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "axum"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "http 1.0.0",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "multer",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http 1.0.0",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit_field"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7"
+dependencies = [
+ "objc-sys",
+]
+
+[[package]]
+name = "block2"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68"
+dependencies = [
+ "block-sys",
+ "objc2",
+]
+
+[[package]]
+name = "blocking"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
+dependencies = [
+ "async-channel 2.1.1",
+ "async-lock 3.3.0",
+ "async-task",
+ "fastrand",
+ "futures-io",
+ "futures-lite",
+ "piper",
+ "tracing",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
+
+[[package]]
+name = "bytemuck"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
+dependencies = [
+ "bytemuck_derive",
+]
+
+[[package]]
+name = "bytemuck_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "calloop"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298"
+dependencies = [
+ "bitflags 2.4.2",
+ "log",
+ "polling",
+ "rustix",
+ "slab",
+ "thiserror",
+]
+
+[[package]]
+name = "calloop-wayland-source"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02"
+dependencies = [
+ "calloop",
+ "rustix",
+ "wayland-backend",
+ "wayland-client",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "jobserver",
+ "libc",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6100bc57b6209840798d95cb2775684849d332f7bd788db2a8c8caf7ef82a41a"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
+[[package]]
+name = "cgl"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "clap"
+version = "4.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
+
+[[package]]
+name = "clipboard-win"
+version = "4.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362"
+dependencies = [
+ "error-code",
+ "str-buf",
+ "winapi",
+]
+
+[[package]]
+name = "cocoa"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
+dependencies = [
+ "bitflags 1.3.2",
+ "block",
+ "cocoa-foundation",
+ "core-foundation",
+ "core-graphics",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "cocoa-foundation"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
+dependencies = [
+ "bitflags 1.3.2",
+ "block",
+ "core-foundation",
+ "core-graphics-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "combine"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "core-graphics"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cursor-icon"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
+
+[[package]]
+name = "data-encoding"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "directories-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dlib"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
+
+[[package]]
+name = "duplicate"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de78e66ac9061e030587b2a2e75cc88f22304913c907b11307bca737141230cb"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+]
+
+[[package]]
+name = "ecolor"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57539aabcdbb733b6806ef421b66dec158dc1582107ad6d51913db3600303354"
+dependencies = [
+ "bytemuck",
+ "serde",
+]
+
+[[package]]
+name = "eframe"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79c00143a1d564cf27570234c9a199cbe75dc3d43a135510fb2b93406a87ee8e"
+dependencies = [
+ "bytemuck",
+ "cocoa",
+ "directories-next",
+ "egui",
+ "egui-winit",
+ "egui_glow",
+ "glow",
+ "glutin",
+ "glutin-winit",
+ "image",
+ "js-sys",
+ "log",
+ "objc",
+ "parking_lot",
+ "percent-encoding",
+ "raw-window-handle",
+ "ron",
+ "serde",
+ "static_assertions",
+ "thiserror",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winapi",
+ "winit",
+]
+
+[[package]]
+name = "egui"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0bf640ed7f3bf3d14ebf00d73bacc09c886443ee84ca6494bde37953012c9e3"
+dependencies = [
+ "accesskit",
+ "ahash",
+ "epaint",
+ "log",
+ "nohash-hasher",
+ "ron",
+ "serde",
+]
+
+[[package]]
+name = "egui-winit"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d95d9762056c541bd2724de02910d8bccf3af8e37689dc114b21730e64f80a0"
+dependencies = [
+ "arboard",
+ "egui",
+ "log",
+ "raw-window-handle",
+ "serde",
+ "smithay-clipboard",
+ "web-time 0.2.4",
+ "webbrowser",
+ "winit",
+]
+
+[[package]]
+name = "egui_dock"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "727ea33d9940fb3eea4ba59c78e47db5d0c25bdcb3f3169be68d0ee7088f5e61"
+dependencies = [
+ "duplicate",
+ "egui",
+ "paste",
+]
+
+[[package]]
+name = "egui_extras"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "753c36d3e2f7a32425af5290af2e52efb3471ea3a263b87f003b5433351b0fd7"
+dependencies = [
+ "egui",
+ "enum-map",
+ "image",
+ "log",
+ "mime_guess2",
+ "serde",
+]
+
+[[package]]
+name = "egui_glow"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb2ef815e80d117339c7d6b813f7678d23522d699ccd3243e267ef06166009b9"
+dependencies = [
+ "bytemuck",
+ "egui",
+ "glow",
+ "log",
+ "memoffset",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "egui_plot"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a159fffebf052f79d1fd26d48e68906a21fec2fce808f7c0a982ec14ed506be"
+dependencies = [
+ "egui",
+]
+
+[[package]]
+name = "ehttp"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80b69a6f9168b96c0ae04763bec27a8b06b34343c334dd2703a4ec21f0f5e110"
+dependencies = [
+ "js-sys",
+ "ureq",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "either"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+
+[[package]]
+name = "emath"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a045c6c0b44b35e98513fc1e9d183ab42881ac27caccb9fa345465601f56cce4"
+
+[[package]]
+name = "emath"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ee58355767587db7ba3738930d93cad3052cd834c2b48b9ef6ef26fe4823b7e"
+dependencies = [
+ "bytemuck",
+ "serde",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "enfync"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ce8c1fbec15a38aced3838c4d7ea90a1403bd50364b5cfe8008e679df0c8dde"
+dependencies = [
+ "async-trait",
+ "futures",
+ "tokio",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasmtimer",
+]
+
+[[package]]
+name = "enum-map"
+version = "2.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9"
+dependencies = [
+ "enum-map-derive",
+ "serde",
+]
+
+[[package]]
+name = "enum-map-derive"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "enumn"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fd000fd6988e73bbe993ea3db9b1aa64906ab88766d654973924340c8cddb42"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "termcolor",
+]
+
+[[package]]
+name = "epaint"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e638cb066bff0903bbb6143116cfd134a42279c7d68f19c0352a94f15a402de7"
+dependencies = [
+ "ab_glyph",
+ "ahash",
+ "bytemuck",
+ "ecolor",
+ "emath 0.25.0",
+ "log",
+ "nohash-hasher",
+ "parking_lot",
+ "serde",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "error-code"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21"
+dependencies = [
+ "libc",
+ "str-buf",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "event-listener"
+version = "4.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3"
+dependencies = [
+ "event-listener 4.0.3",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "exr"
+version = "1.71.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8"
+dependencies = [
+ "bit_field",
+ "flume",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
+[[package]]
+name = "ezsockets"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ab2935707532c19357939ce9649a0ca45eb02c2295bf37990f4601cb3a8afd4"
+dependencies = [
+ "async-channel 1.9.0",
+ "async-trait",
+ "atomic_enum",
+ "base64",
+ "cfg-if",
+ "enfync",
+ "fragile",
+ "futures",
+ "futures-util",
+ "getrandom",
+ "http 0.2.11",
+ "tokio",
+ "tokio-tungstenite-wasm",
+ "tracing",
+ "tungstenite",
+ "url",
+ "wasm-bindgen-futures",
+ "wasmtimer",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "fixedbitset"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+
+[[package]]
+name = "flate2"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "flume"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fragile"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
+
+[[package]]
+name = "futures"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+ "num_cpus",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-lite"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "gethostname"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "gethostname"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
+dependencies = [
+ "libc",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gif"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "gl_generator"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
+dependencies = [
+ "khronos_api",
+ "log",
+ "xml-rs",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glow"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1"
+dependencies = [
+ "js-sys",
+ "slotmap",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "glutin"
+version = "0.31.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "005459a22af86adc706522d78d360101118e2638ec21df3852fcc626e0dbb212"
+dependencies = [
+ "bitflags 2.4.2",
+ "cfg_aliases",
+ "cgl",
+ "core-foundation",
+ "dispatch",
+ "glutin_egl_sys",
+ "glutin_glx_sys",
+ "glutin_wgl_sys",
+ "icrate",
+ "libloading",
+ "objc2",
+ "once_cell",
+ "raw-window-handle",
+ "wayland-sys",
+ "windows-sys 0.48.0",
+ "x11-dl",
+]
+
+[[package]]
+name = "glutin-winit"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebcdfba24f73b8412c5181e56f092b5eff16671c514ce896b258a0a64bd7735"
+dependencies = [
+ "cfg_aliases",
+ "glutin",
+ "raw-window-handle",
+ "winit",
+]
+
+[[package]]
+name = "glutin_egl_sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77cc5623f5309ef433c3dd4ca1223195347fe62c413da8e2fdd0eb76db2d9bcd"
+dependencies = [
+ "gl_generator",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "glutin_glx_sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a165fd686c10dcc2d45380b35796e577eacfd43d4660ee741ec8ebe2201b3b4f"
+dependencies = [
+ "gl_generator",
+ "x11-dl",
+]
+
+[[package]]
+name = "glutin_wgl_sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead"
+dependencies = [
+ "gl_generator",
+]
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 1.0.0",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f"
+
+[[package]]
+name = "home"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "http"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
+dependencies = [
+ "bytes",
+ "http 1.0.0",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http 1.0.0",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-range-header"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe"
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "hyper"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http 1.0.0",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.0.0",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "icrate"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319"
+dependencies = [
+ "block2",
+ "dispatch",
+ "objc2",
+]
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "image"
+version = "0.24.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "exr",
+ "gif",
+ "jpeg-decoder",
+ "num-traits",
+ "png",
+ "qoi",
+ "tiff",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455"
+dependencies = [
+ "hermit-abi",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "jobserver"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "jpeg-decoder"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
+dependencies = [
+ "rayon",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "khronos_api"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lebe"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
+
+[[package]]
+name = "libc"
+version = "0.2.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
+
+[[package]]
+name = "libloading"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "libredox"
+version = "0.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
+dependencies = [
+ "bitflags 2.4.2",
+ "libc",
+ "redox_syscall 0.4.1",
+]
+
+[[package]]
+name = "libredox"
+version = "0.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607"
+dependencies = [
+ "bitflags 2.4.2",
+ "libc",
+ "redox_syscall 0.4.1",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+
+[[package]]
+name = "litrs"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
+
+[[package]]
+name = "lock_api"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+
+[[package]]
+name = "log-once"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d8a05e3879b317b1b6dbf353e5bba7062bedcc59815267bb23eaa0c576cebf0"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "memchr"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+
+[[package]]
+name = "memmap2"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45fd3a57831bf88bc63f8cebc0cf956116276e97fef3966103e96416209f7c92"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "memory-stats"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34f79cf9964c5c9545493acda1263f1912f8d2c56c8a2ffee2606cb960acaacc"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "mime_guess2"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a3333bb1609500601edc766a39b4c1772874a4ce26022f4d866854dc020c41"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "multer"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a15d522be0a9c3e46fd2632e272d178f56387bdb5c9fbb3a36c649062e9b5219"
+dependencies = [
+ "bytes",
+ "encoding_rs",
+ "futures-util",
+ "http 1.0.0",
+ "httparse",
+ "log",
+ "memchr",
+ "mime",
+ "spin",
+ "version_check",
+]
+
+[[package]]
+name = "multimap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
+
+[[package]]
+name = "ndk"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
+dependencies = [
+ "bitflags 2.4.2",
+ "jni-sys",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.5.0+25.2.9519653"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
+dependencies = [
+ "jni-sys",
+]
+
+[[package]]
+name = "nix"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
+dependencies = [
+ "bitflags 1.3.2",
+ "cfg-if",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "nohash-hasher"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
+
+[[package]]
+name = "ntapi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-derive"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
+dependencies = [
+ "autocfg",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+]
+
+[[package]]
+name = "objc-foundation"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
+dependencies = [
+ "block",
+ "objc",
+ "objc_id",
+]
+
+[[package]]
+name = "objc-sys"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459"
+
+[[package]]
+name = "objc2"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666"
+
+[[package]]
+name = "objc_id"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
+dependencies = [
+ "objc",
+]
+
+[[package]]
+name = "object"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "orbclient"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166"
+dependencies = [
+ "libredox 0.0.2",
+]
+
+[[package]]
+name = "ordered-float"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "owned_ttf_parser"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7"
+dependencies = [
+ "ttf-parser",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.4.1",
+ "smallvec",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "petgraph"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
+dependencies = [
+ "fixedbitset",
+ "indexmap",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb"
+
+[[package]]
+name = "png"
+version = "0.17.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f6c3c3e617595665b8ea2ff95a86066be38fb121ff920a9c0eb282abcd1da5a"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "poll-promise"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6a58fecbf9da8965bcdb20ce4fd29788d1acee68ddbb64f0ba1b81bccdb7df"
+dependencies = [
+ "document-features",
+ "static_assertions",
+]
+
+[[package]]
+name = "polling"
+version = "3.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "545c980a3880efd47b2e262f6a4bb6daad6555cf3367aa9c4e52895f69537a41"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "pin-project-lite",
+ "rustix",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "prettyplease"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
+dependencies = [
+ "proc-macro2",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prost"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270"
+dependencies = [
+ "bytes",
+ "heck",
+ "itertools 0.10.5",
+ "lazy_static",
+ "log",
+ "multimap",
+ "petgraph",
+ "prettyplease",
+ "prost",
+ "prost-types",
+ "regex",
+ "syn 1.0.109",
+ "tempfile",
+ "which",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
+dependencies = [
+ "anyhow",
+ "itertools 0.10.5",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "puffin"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02330f795caafc2007510f742624c10aa813b8c3097c77ff344b1b86eb6be846"
+dependencies = [
+ "anyhow",
+ "byteorder",
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
+
+[[package]]
+name = "rayon"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "re_format"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17f7253a068deca4b2f9f75ec8f2f1a31de7330dbf42f109423b5a258a939623"
+
+[[package]]
+name = "re_log"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45ff89b2c17420a6395510f2c2821a86d3164ed8d86d95334b44b707e0e36b36"
+dependencies = [
+ "env_logger",
+ "js-sys",
+ "log",
+ "log-once",
+ "parking_lot",
+ "tracing",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "re_memory"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ca1c9e3b9ca11d8249670984f11b3e206654bea437f68645c201ffcc37cf3e2"
+dependencies = [
+ "ahash",
+ "backtrace",
+ "emath 0.24.1",
+ "itertools 0.12.0",
+ "memory-stats",
+ "nohash-hasher",
+ "once_cell",
+ "parking_lot",
+ "re_format",
+ "re_log",
+ "re_tracing",
+ "smallvec",
+ "sysinfo",
+ "wasm-bindgen",
+ "web-time 0.2.4",
+]
+
+[[package]]
+name = "re_tracing"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ad438a971b668f3f554df33a843576636ea1ef97e7039f18dd89973c7f97850"
+dependencies = [
+ "puffin",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
+dependencies = [
+ "getrandom",
+ "libredox 0.0.1",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata 0.4.4",
+ "regex-syntax 0.8.2",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.8.2",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+
+[[package]]
+name = "rfd"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c9e7b57df6e8472152674607f6cc68aa14a748a3157a857a94f516e11aeacc2"
+dependencies = [
+ "block",
+ "dispatch",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "js-sys",
+ "log",
+ "objc",
+ "objc-foundation",
+ "objc_id",
+ "raw-window-handle",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74"
+dependencies = [
+ "cc",
+ "getrandom",
+ "libc",
+ "spin",
+ "untrusted",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "ron"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+dependencies = [
+ "base64",
+ "bitflags 2.4.2",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustix"
+version = "0.38.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"
+dependencies = [
+ "bitflags 2.4.2",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.21.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
+dependencies = [
+ "log",
+ "ring",
+ "rustls-webpki",
+ "sct",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.101.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
+[[package]]
+name = "ryu"
+version = "1.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sct"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.195"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.195"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c"
+dependencies = [
+ "itoa",
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
+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 = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "slotmap"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
+
+[[package]]
+name = "smithay-client-toolkit"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60e3d9941fa3bacf7c2bf4b065304faa14164151254cd16ce1b1bc8fc381600f"
+dependencies = [
+ "bitflags 2.4.2",
+ "calloop",
+ "calloop-wayland-source",
+ "cursor-icon",
+ "libc",
+ "log",
+ "memmap2",
+ "rustix",
+ "thiserror",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-csd-frame",
+ "wayland-cursor",
+ "wayland-protocols",
+ "wayland-protocols-wlr",
+ "wayland-scanner",
+ "xkeysym",
+]
+
+[[package]]
+name = "smithay-clipboard"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb62b280ce5a5cba847669933a0948d00904cf83845c944eae96a4738cea1a6"
+dependencies = [
+ "libc",
+ "smithay-client-toolkit",
+ "wayland-backend",
+]
+
+[[package]]
+name = "smol_str"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
+dependencies = [
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "str-buf"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "sysinfo"
+version = "0.30.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb4f3438c8f6389c864e61221cbc97e9bca98b4daf39a5beb7bea660f528bb2"
+dependencies = [
+ "cfg-if",
+ "core-foundation-sys",
+ "libc",
+ "ntapi",
+ "once_cell",
+ "windows",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331"
+dependencies = [
+ "cfg-expr",
+ "heck",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae"
+
+[[package]]
+name = "tempfile"
+version = "3.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "redox_syscall 0.4.1",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tiff"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
+dependencies = [
+ "flate2",
+ "jpeg-decoder",
+ "weezl",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+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.35.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-tungstenite-wasm"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ec8c7cf09b20184f946f114e3d8c0deca34368912c90100812861c14bb63b66"
+dependencies = [
+ "futures-channel",
+ "futures-util",
+ "http 0.2.11",
+ "httparse",
+ "js-sys",
+ "thiserror",
+ "tokio",
+ "tokio-tungstenite",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit 0.21.0",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e"
+dependencies = [
+ "bitflags 2.4.2",
+ "bytes",
+ "futures-util",
+ "http 1.0.0",
+ "http-body",
+ "http-body-util",
+ "http-range-header",
+ "httpdate",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "ttf-parser"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4"
+
+[[package]]
+name = "tungstenite"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http 0.2.11",
+ "httparse",
+ "log",
+ "rand",
+ "sha1",
+ "thiserror",
+ "url",
+ "utf-8",
+]
+
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "unicase"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "ureq"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97"
+dependencies = [
+ "base64",
+ "flate2",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-webpki",
+ "url",
+ "webpki-roots",
+]
+
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
+[[package]]
+name = "version-compare"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b"
+
+[[package]]
+name = "wasmtimer"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f656cd8858a5164932d8a90f936700860976ec21eb00e0fe2aa8cab13f6b4cf"
+dependencies = [
+ "futures",
+ "js-sys",
+ "parking_lot",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wayland-backend"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19152ddd73f45f024ed4534d9ca2594e0ef252c1847695255dae47f34df9fbe4"
+dependencies = [
+ "cc",
+ "downcast-rs",
+ "nix",
+ "scoped-tls",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-client"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3"
+dependencies = [
+ "bitflags 2.4.2",
+ "nix",
+ "wayland-backend",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-csd-frame"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
+dependencies = [
+ "bitflags 2.4.2",
+ "cursor-icon",
+ "wayland-backend",
+]
+
+[[package]]
+name = "wayland-cursor"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44aa20ae986659d6c77d64d808a046996a932aa763913864dc40c359ef7ad5b"
+dependencies = [
+ "nix",
+ "wayland-client",
+ "xcursor",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c"
+dependencies = [
+ "bitflags 2.4.2",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-plasma"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
+dependencies = [
+ "bitflags 2.4.2",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-wlr"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6"
+dependencies = [
+ "bitflags 2.4.2",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c"
+dependencies = [
+ "proc-macro2",
+ "quick-xml",
+ "quote",
+]
+
+[[package]]
+name = "wayland-sys"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af"
+dependencies = [
+ "dlib",
+ "log",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ee269d72cc29bf77a2c4bc689cc750fb39f5cbd493d2205bbb3f5c7779cf7b0"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webbrowser"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b2391658b02c27719fc5a0a73d6e696285138e8b12fba9d4baa70451023c71"
+dependencies = [
+ "core-foundation",
+ "home",
+ "jni",
+ "log",
+ "ndk-context",
+ "objc",
+ "raw-window-handle",
+ "url",
+ "web-sys",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.25.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10"
+
+[[package]]
+name = "weezl"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
+
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-wsapoll"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
+dependencies = [
+ "windows-core",
+ "windows-targets 0.52.0",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.0",
+ "windows_aarch64_msvc 0.52.0",
+ "windows_i686_gnu 0.52.0",
+ "windows_i686_msvc 0.52.0",
+ "windows_x86_64_gnu 0.52.0",
+ "windows_x86_64_gnullvm 0.52.0",
+ "windows_x86_64_msvc 0.52.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+
+[[package]]
+name = "winit"
+version = "0.29.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c824f11941eeae66ec71111cc2674373c772f482b58939bb4066b642aa2ffcf"
+dependencies = [
+ "ahash",
+ "android-activity",
+ "atomic-waker",
+ "bitflags 2.4.2",
+ "bytemuck",
+ "calloop",
+ "cfg_aliases",
+ "core-foundation",
+ "core-graphics",
+ "cursor-icon",
+ "icrate",
+ "js-sys",
+ "libc",
+ "log",
+ "memmap2",
+ "ndk",
+ "ndk-sys",
+ "objc2",
+ "once_cell",
+ "orbclient",
+ "percent-encoding",
+ "raw-window-handle",
+ "redox_syscall 0.3.5",
+ "rustix",
+ "smithay-client-toolkit",
+ "smol_str",
+ "unicode-segmentation",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-protocols-plasma",
+ "web-sys",
+ "web-time 0.2.4",
+ "windows-sys 0.48.0",
+ "x11-dl",
+ "x11rb 0.13.0",
+ "xkbcommon-dl",
+]
+
+[[package]]
+name = "winnow"
+version = "0.5.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11rb"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a"
+dependencies = [
+ "gethostname 0.3.0",
+ "nix",
+ "winapi",
+ "winapi-wsapoll",
+ "x11rb-protocol 0.12.0",
+]
+
+[[package]]
+name = "x11rb"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a"
+dependencies = [
+ "as-raw-xcb-connection",
+ "gethostname 0.4.3",
+ "libc",
+ "libloading",
+ "once_cell",
+ "rustix",
+ "x11rb-protocol 0.13.0",
+]
+
+[[package]]
+name = "x11rb-protocol"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc"
+dependencies = [
+ "nix",
+]
+
+[[package]]
+name = "x11rb-protocol"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34"
+
+[[package]]
+name = "xcursor"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911"
+
+[[package]]
+name = "xkbcommon-dl"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6924668544c48c0133152e7eec86d644a056ca3d09275eb8d5cdb9855f9d8699"
+dependencies = [
+ "bitflags 2.4.2",
+ "dlib",
+ "log",
+ "once_cell",
+ "xkeysym",
+]
+
+[[package]]
+name = "xkeysym"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621"
+
+[[package]]
+name = "xml-rs"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a"
+
+[[package]]
+name = "y4m"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
+name = "zip"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
+dependencies = [
+ "byteorder",
+ "crc32fast",
+ "crossbeam-utils",
+ "flate2",
+]
+
+[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]
diff --git a/tools/avm_analyzer/Cargo.toml b/tools/avm_analyzer/Cargo.toml
new file mode 100644
index 0000000..86bac2e
--- /dev/null
+++ b/tools/avm_analyzer/Cargo.toml
@@ -0,0 +1,13 @@
+[workspace]
+members = ["avm_analyzer_app", "avm_analyzer_common", "avm_analyzer_server", "avm_stats"]
+resolver = "2"
+
+[profile.dev]
+opt-level = 1
+
+[profile.release]
+opt-level = 2 # fast and small wasm
+
+# Optimize all dependencies even in debug builds:
+[profile.dev.package."*"]
+opt-level = 2
diff --git a/tools/avm_analyzer/Dockerfile.builder b/tools/avm_analyzer/Dockerfile.builder
new file mode 100644
index 0000000..2418172
--- /dev/null
+++ b/tools/avm_analyzer/Dockerfile.builder
@@ -0,0 +1,47 @@
+# TODO(comc): cargo chef caching doesn't seem to be working properly.
+FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
+RUN apt-get update
+RUN apt-get install -y make pkg-config git unzip cmake protobuf-compiler libssl-dev --no-install-recommends
+RUN cargo install trunk
+RUN rustup target add wasm32-unknown-unknown
+RUN mkdir /extract_proto
+COPY tools/extract_proto/avm_frame.proto /extract_proto/avm_frame.proto
+WORKDIR /app
+
+FROM chef as planner_frontend
+COPY tools/avm_analyzer .
+RUN cargo chef prepare --recipe-path recipe.json --bin avm_analyzer_app
+
+FROM chef AS cacher_frontend
+COPY --from=planner_frontend /app/recipe.json recipe.json
+RUN cargo chef cook --release --target wasm32-unknown-unknown --recipe-path recipe.json --bin avm-analyzer-app
+
+FROM chef as planner_backend
+COPY tools/avm_analyzer /app
+RUN cargo chef prepare --recipe-path recipe.json --bin avm_analyzer_server
+
+FROM chef AS cacher_backend
+COPY --from=planner_backend /app/recipe.json recipe.json
+RUN cargo chef cook --release --recipe-path recipe.json --bin avm-analyzer-server
+
+FROM chef AS builder_frontend
+COPY --from=cacher_frontend /app .
+COPY tools/avm_analyzer .
+RUN trunk build --release avm_analyzer_app/index.html
+
+FROM chef AS builder_backend
+COPY --from=cacher_backend /app .
+COPY tools/avm_analyzer .
+RUN cargo build --release --bin avm-analyzer-server
+
+FROM avm_analyzer_runtime as avm_builder
+COPY . /avm
+RUN mkdir /avm_build
+RUN /scripts/build_avm.sh --avm_build_dir /avm_build --avm_source_dir /avm
+RUN rm -r /avm
+
+FROM avm_analyzer_runtime as runtime
+WORKDIR /app
+COPY --from=builder_frontend /app/avm_analyzer_app/dist dist
+COPY --from=builder_backend /app/target/release/avm-analyzer-server avm-analyzer-server
+COPY --from=avm_builder /avm_build /avm_build
diff --git a/tools/avm_analyzer/Dockerfile.runtime b/tools/avm_analyzer/Dockerfile.runtime
new file mode 100644
index 0000000..af2011e
--- /dev/null
+++ b/tools/avm_analyzer/Dockerfile.runtime
@@ -0,0 +1,7 @@
+# Basic runtime image with required dependencies to build libavm.
+FROM ubuntu:22.04
+RUN apt-get update
+RUN apt-get install -y cmake git perl g++ yasm make protobuf-compiler libprotobuf-dev python3 --no-install-recommends
+WORKDIR /scripts
+COPY tools/avm_analyzer/build_avm.sh /scripts/build_avm.sh
+RUN chmod +x /scripts/build_avm.sh
diff --git a/tools/avm_analyzer/README.md b/tools/avm_analyzer/README.md
new file mode 100644
index 0000000..f53300e
--- /dev/null
+++ b/tools/avm_analyzer/README.md
@@ -0,0 +1,39 @@
+# AVM Analyzer
+
+## Building
+
+### With Docker
+
+```
+./launch_server_docker.sh --streams_dir <STREAMS_PATH> [--port <PORT>]
+```
+
+### Local Build
+1. Install dependencies
+```
+# libavm (if not already installed)
+apt install cmake yasm perl
+# Protobuf compiler
+apt install protobuf-compiler libprotobuf-dev
+# Rust toolchain (see rustup.rs)
+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+# Trunk
+cargo install --locked trunk
+# WebAssembly build target
+rustup target add wasm32-unknown-unknown
+```
+
+2. Build AVM
+```
+export AOM_ROOT=/path/to/git/root
+export AOM_BUILD_DIR=/path/to/avm/build
+./build_avm.sh --avm_source_dir ${AOM_ROOT} --avm_build_dir ${AOM_BUILD_DIR}
+```
+
+3. Build and launch AVM Analyzer
+```
+./launch_server_local.sh --streams_dir <STREAMS_PATH> --avm_build_dir ${AOM_BUILD_DIR} [--port <PORT>]
+```
+
+## Troubleshooting
+Please contact comc@google.com.
\ No newline at end of file
diff --git a/tools/avm_analyzer/avm_analyzer_app/.cargo/config.toml b/tools/avm_analyzer/avm_analyzer_app/.cargo/config.toml
new file mode 100644
index 0000000..e7becff
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/.cargo/config.toml
@@ -0,0 +1,2 @@
+[target.wasm32-unknown-unknown]
+rustflags = ["--cfg=web_sys_unstable_apis"]
\ No newline at end of file
diff --git a/tools/avm_analyzer/avm_analyzer_app/Cargo.toml b/tools/avm_analyzer/avm_analyzer_app/Cargo.toml
new file mode 100644
index 0000000..95d6354
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/Cargo.toml
@@ -0,0 +1,73 @@
+[package]
+name = "avm-analyzer-app"
+version = "0.1.0"
+authors = ["Conor McCullough <comc@google.com>"]
+edition = "2021"
+rust-version = "1.75"
+
+
+[dependencies]
+avm-analyzer-common = { path = "../avm_analyzer_common" }
+avm-stats = { path = "../avm_stats" }
+anyhow = "1.0"
+egui = "0.25"
+egui_extras = { version = "0.25", features = ["image"] }
+eframe = { version = "0.25", default-features = false, features = [
+ "default_fonts",
+ "glow",
+ "persistence",
+ "__screenshot",
+] }
+ehttp = "0.2.0"
+egui_plot = "0.25"
+egui_dock = "0.10"
+image = { version = "0.24", features = ["jpeg", "png"] }
+itertools = "0.10"
+prost = "0.11"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1.0"
+ron = "0.8"
+poll-promise = "0.3.0"
+y4m = "0.8.0"
+web-sys = { version = "0.3.64", features = [
+ "Blob",
+ "FileReader",
+ "Document",
+ "Element",
+ "History",
+ "Performance",
+ "HtmlElement",
+ "UrlSearchParams",
+ "Location",
+ "Window",
+ "Url",
+ "UrlSearchParams",
+] }
+js-sys = { version = "0.3" }
+futures = { version = "0.3.10", features = ["thread-pool"] }
+rfd = { version = "0.12", features = ["file-handle-inner"] }
+log = "0.4"
+once_cell = "1.19.0"
+zip = { version = "0.6.6", default-features = false, features = ["deflate"] }
+web-time = "1.0.0"
+async-trait = "0.1.52"
+ezsockets = { version = "0.6", default-features = false, features = [
+ "wasm_client",
+] }
+url = "2.2.2"
+wasm-bindgen = "0.2"
+mime = { version = "0.3" }
+mime_guess = { version = "2.0" }
+rand = { version = "0.8.5" }
+getrandom = { version = "0.2.10", features = ["js"] }
+ordered-float = "4.2.0"
+re_memory = "0.12.1"
+bincode = "1.3"
+base64 = "0.21.7"
+weezl = "0.1.8"
+convert_case = "0.6.0"
+
+# web:
+[target.'cfg(target_family = "wasm")'.dependencies]
+console_error_panic_hook = "0.1.6"
+wasm-bindgen-futures = "0.4"
diff --git a/tools/avm_analyzer/avm_analyzer_app/Trunk.toml b/tools/avm_analyzer/avm_analyzer_app/Trunk.toml
new file mode 100644
index 0000000..bd6c484
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/Trunk.toml
@@ -0,0 +1,2 @@
+[build]
+filehash = false
diff --git a/tools/avm_analyzer/avm_analyzer_app/assets/favicon.ico b/tools/avm_analyzer/avm_analyzer_app/assets/favicon.ico
new file mode 100644
index 0000000..ccf2ba4
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/assets/favicon.ico
Binary files differ
diff --git a/tools/avm_analyzer/avm_analyzer_app/assets/icon-192x192.png b/tools/avm_analyzer/avm_analyzer_app/assets/icon-192x192.png
new file mode 100644
index 0000000..a6fef93
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/assets/icon-192x192.png
Binary files differ
diff --git a/tools/avm_analyzer/avm_analyzer_app/assets/icon-256x256.png b/tools/avm_analyzer/avm_analyzer_app/assets/icon-256x256.png
new file mode 100644
index 0000000..cb66e93
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/assets/icon-256x256.png
Binary files differ
diff --git a/tools/avm_analyzer/avm_analyzer_app/assets/icon-384x384.png b/tools/avm_analyzer/avm_analyzer_app/assets/icon-384x384.png
new file mode 100644
index 0000000..4dd0a66
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/assets/icon-384x384.png
Binary files differ
diff --git a/tools/avm_analyzer/avm_analyzer_app/assets/icon-512x512.png b/tools/avm_analyzer/avm_analyzer_app/assets/icon-512x512.png
new file mode 100644
index 0000000..a9dbcd9
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/assets/icon-512x512.png
Binary files differ
diff --git a/tools/avm_analyzer/avm_analyzer_app/assets/leo_qcif.ivf b/tools/avm_analyzer/avm_analyzer_app/assets/leo_qcif.ivf
new file mode 100644
index 0000000..23a58d8
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/assets/leo_qcif.ivf
Binary files differ
diff --git a/tools/avm_analyzer/avm_analyzer_app/assets/leo_qcif.yuv b/tools/avm_analyzer/avm_analyzer_app/assets/leo_qcif.yuv
new file mode 100644
index 0000000..657a7e9
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/assets/leo_qcif.yuv
@@ -0,0 +1 @@
+}ysorz
s`VSV_l|
p[KA>DKXo
uZIA??AEIVs
~
{aJ<:=BGLPWdz
~~}~
dK><?HS\cjnt{}{||}~
[@8=CKYfmrtuy
}{{{|}~
uS?;>JT\fmrtuvy
}|zzzzz{~
jH;;<IW^binpqsuw~
~zyyxxxxx|~
dD9<=ES]`binopqtw|
}zyyxwxvvy|}
~~~~}~~
~]=7<AIS\cdhmrrrsvz|}zyxvvvuux{}
~~~}|{{{z{{|~
{]<5:BMWZailptvuttuuz~yxvuutuvy|~
~~}|||zzyyxyyyz{|~
wV;49?JW_chmnprrqpppqv
~wutsssuvx}{`]t~}{{{zyyxvuuuvvwyz{|~
tU607>ER]aglnnooqrrrrrvurrrrstvx|
kSVg
}zzyyxwvusssttttuxzz{}
}qT8/5=HR^ddhlnnnnopqqrrttqrqqrtux|
t^QM^
~|yxxxwutsrrrrrpqrtwwwyz|
vaK604=GRZ`abgkmmmmllllllnyspqrrrtvx|
ykULGSu~{yxxxutsqppqpoonorttuwxx{|}||~~~}u^E1-3;EQWY[`cfjllklkkjjjjmx
~tnqsssuvx|{saTLDOp}zyxxwtsqnnnnmmmmmoqsstttwxxxxz{{{{|}~~~~~~~~}zrY>2148>HRUW]ehlopqrsuwyzzxx}}wuuttuwy|
|zuldSHFN_x}yyxvutrollllklkklmnooppqstutuwxxxxyzz{{|||||||{{|{|~wjYD4.28>BIRUY`giloqsuwwwwvvvw|zwuuuuwy|
}{wpk[OJFENm}|yxvtsrqolkkkkkjkklmmlmnnqqrrrsuuuvvxwwwwxxxwvwwvvuwzykQ:4437@DHNVY\befghknppnonmmlnu{utttuwxy}{xpk`UOJC?Ja
~{ywuttsrqnkjjjkiijkkkkkklmnnnooqsssstttttsssssrsstsqokbQ<.256;DHLPV[^`cdcfiklmlmmmnnnrvqqstuwyz~
{xqkbZUPHA@D[p~
|zwtsssssrnkjjihhhjkkkkklmlllllmnonqrqqqpprpoooopqrojaSC4.149?CILNSY\^`abcegjllmnnoqrorupoqrsux{~|yrld^ZTLE?<=GXhponv}{xuqqstuutolkhfeefhijjjjjkkkkklklkkpqpppommkjjlkjjicWI82026:AGKPRVZ]_abcdfikmnnnnprttpsuqopqrsx{~
|xuoia]XQJC>:8=HOONWu|{zzwsokjlqtvuolmjihhilnpppqrrssssrrokknpqqnhcbaaab`^\ZP?6/-/7>EKOSUX[]`cefikmprrqqqqsvyxtwwsrrrruz|~
~|yuqlda]VOJC=98:=<=CWkkegiiiheb`biruspnllmpuz~~~~zrpprtsmb\WUUUUTQOK?30/.3?IMPTXZ]^acfilmoqtvuttttwz}{wyxtsstuwz}~
|ytqnhea[UPJB<978668=JNLPRTUVWVTWamvvtqqsw||vvxyxqd[TNKONMIB=5//02?MQRVY[`bcefgkmnqsuwuuuuuvz}{x{
vrrstvxz}~
}
~zusqlhd_[VOG@;864357:=@ADGJLLJIKWmxyz{~
}|}~|wlc[OHLKG@:82-.2<HQV\_ccddefghlmprtttqoooptx{zx{{rnopstux|
|
}zxurlifc]YSJD>:75424589:<CFFFFGJWoz|
~vshYOJID<98546>GNU[bedcbceghjnopqppnmlklnsxzzx|wnklnqqsx{~~
~|wtqmjfc`[YRKC>;75223467;ADDDDHN[nz
{ztcWOLD;9879>GMTY`dfb`adhiilpqponlkkkllosxz{y}~rkikmoprvz~
|wurmjfc`\YWRJC>952111468<@HKMR\eq|
|xoc]YM?=::@GNSZ`fgfb`ckomkkllkjiihijkloty{{z}oihjknqruy|
|xusnlhd`]YXUPGA;840/1025;BPY`hnsy
|upgWIFEEIOSU_lojfddippmigeccfghijklmouz}|z}pkkklnqrtx{
}zvsplhd^ZXXUTNHA:62////4AS`kt{~
zn^VTSSTWY[fttnhfgptlhffddgijjkmmmnquyzyx~soollmoqswz~
}{yvrnjfa^[ZYVSOH@:73322<Rblv
~
uib_^^`acdpwtrmjmuqfacgimopponnlkmrtvutu~vqpmlmopruy}}{zyvspnljfa]YWUPHB?=><>Pbnw
}{}{qllllkjknvvssooutjfcglptwtqnkjiikostssu
~xsolllnprux{|~~{{yxutsrqmha\YXWSJGILNVamv|~|}~
wtuxxtorvzurtorwtpkipw{|vmhgdcgmnrstuv|ysolllnortw{~
~z{|~~|{zyvtsrppmg`[YZ\TNOZgmpv{
~}z~|y{|}ysoryvpoms{kdcccgntuxxyx{xqlkllnopsvz
~zzz}~~}}|{zwsqonmmiea]Z]`ZWdry}
~}
}rnx~wsquriijkox~~~~~vnklopqpqrtx{~{yyz{||||{zxurnkijiigaZ[dhfkw
|{|}}
ysy}}yxzvmorux}}rlnsutspqqsvy{|yyyz{{||||zzwrnklmnlg`^gpw{
~~
|yz{|~
{~zz{tvxxy~
~|yz
zsuz~yusppqsuxz}
|zxyyyz{|}~zvrppponlnt~~}{{}zyzyz{
~
|xxzz{yz{|xsrpopstvxz}
}{yxxyz|~
}xuvxyzy}
~~~}zwvxz|}~ }zyywx|~
zwwx{~zx{~zwsonnpruvy{}}zyyz~
~|{zyvtssuwxz|~£¤¤ ~zxwuwz~
{zxw{uv
|rnlmnosuxy{~
}z{{|}{yyxvsppprtsvxy¡¥¦¤¡zwvvwz|
~}}~~yqtrllllmnptvwz{~}zwuttsronnoqorvu~¢£¥¤¡
~zwwuvz|~
wpuvllljklmnqstwy{~
|wtsrrqonmnnnnpsr £¤¥¥¢ywusvz|}~
wsx{nlikkjklmoqrsuxz}
|yusqoonnmkklllnqn¡£¤¥¥¢ {xtruyz{}~~}~~~
|stz
uqnlmmlkklnoprsvwy}
~{xurrpnllkhhhhjnol¢¤¤¥¦£¡|wtqrwxz{||||{{}~
~vsu{ztrpnmljlmnpstuwz~
~}|zyvsqonmlligfedlpl¤¥¥¦¦¤£ |wtonsvxyz{{{zyxxxy
yvvy~
~wsqpmkkmnprttux{}z|
~}{yxusqnmnmlieca`ipp ¦¦§§¦¥¥¢ytqkipstvxyyyvvtssty}zxwx{~~~
~xuspllmnortttwz}|{y}
~|{xusqnmmmmlid`]`ksx£¥§§¦¦¥¥¤ xplebjnoqtvvvtsrqrrv{
~|zwuw|xmpx~}zwuronooqrstvy{}~
~z}
~}|zxvtqnkjijkjhea^dq ¥¦§¨§§¦¦¥¡zpja[dgglrtsrroonoorwz|
|xvx}rcgpy}zxwurqpopstuwxy
~~~
}}{yxvutrojggghghihdbn ¤§¨§§©¨§¦£tl^W```eosrpomllmmprvx|
{xx{~zjdlt{}|{zwvuttsssuuw~}{z{~
{zxwtrqpmhfddfggghhdg| £¤§¨¨¨©©©§£
vkVT\\[`hmnnlkjijlnoruy~
|xx{zrggmuz~~}|{{{yyxwttuv{|xwxy{|~
~{yvsrqmkifa^^]`ceffghiw¤¦§¨ªªªª©©¨§¥¡|gOTZXZ_fjjjhhhhijlmoqv{
}zyywpiiov{}|zywvvyzywvwyvtsvxyxy
|wttpnnljid]YWWY_bbdeinw£¨ªªª««««©©¨§¥£iMPUUZaghgggfeghkkmnpsx}
~|zxusruy|
}zxtstxz{zyxsgehnuvtv{
}xwsppmjijihbZTRPSZ^aeek} ¥©«ªª«««ªª¨¨¨¦¤ pNMRU\dfeddcbbeghklmnquz
~}~}~~}|{ywvvvwwxyeTUW\hptvy
|wrpomkifefghaXRNMOUZaggt ¤¦©«ª«¬¬«ªª©©¨§¥¢pNNTY^cdca`_^_aeghijlmsw|
}ywvuvwyiZOONQ[fotw{
}xtnjigdb``aceaYQMMOT[cko ¤¦©«««¬¬¬¬«ªªª©¨¦¤¡rPTZ__`aa_\ZY\_bcdfghkquy~
~~~}{z
|lYPKLLNU^clu{
{uqjfcb^^^^``a_ZSOOQU[eo{¡¤¦¨««¬«¬®¬¬«ª«©§¥£ddhid_a`]XTTWZ]_abdgjotx{
}~~}|o^MCEJMS[aku~|uphca_\\]\]]^]ZVSSTV[ft
¢¦©ª¬¬¬®®¬«ªª©¦¥¡}yxrf_`]XROPRUX[^aehlpswz}
zxwx{|yk\OBAFGLTcox
}uqjca_\[[[ZZZZZXUUVW\hz£¥¨ª¬¬®®¬¬ª«ª¨¦¢ug^ZTNJJKNQTWZ^fjmpsvy|
|ywtuw{~~wm]PHBC@BCNdu~wrkfca^[YXWWWWYYYYZ[_l£¥¨ªª®¯®¬¬®¬¬««ª©¦£
wgYOICBCEFGIOU\fkmpsux{}||
|ywttz}}{sg[PHB@ADOV^l|zuoieb^[YVTSTTUXZ[\^an
£¦©«¬¬®¬¬«¬«ª©§¤
}paNEBDKOLJHFJPXdkmpstuxyyy|
~{yx{}{sj^VRJDDHOVbnsy
{wsngb]YWSOMOOQTVY\^bo¤¨ª«««¬¬««««©§¢|rjbRCDMXaffed[QQU_hlpststuvwz~
~~}vl]VTRLIP^jpu|{wsoha[UQMIFGJLPVY\^ew¥¨ª«««¬®®®®¯®¯¬«¬««ª¥ph_[YPMTYZ[_dhmmbVRYeknpqrrstuy}
wnf[WTTU\fw~
}zuroi`WOKJFEGIJLNXafo¡¦¨©ª«¬¬®®®®¯¯¯®¬«««ª¨lc_[VUVVVRPLNU[bjkaWU`hklllortwy|~
{tof`ZXX[er|
~}zxuroh_UMGMY`_^ZSOUbw¢¥¦¨ªª«¬®®®®®¯¯®¬ªªªª¦v`][XWVURKGF@EKQV]gj`W]fghgfinrvxz{|}||
~vpkd^[]clu~
~~~}|zywtrpi]RQ[gpngb^[XW]s£¤¥§¨ª««®®®®®¯¯®¬«ªªª¦q\WVVXXSMFDB?CGJOS]ll[Xbfebadhnsvvwxyyz~|uqjebbity~
}|zxwutsrpogXVepnfYPLLNSXZf££¥¦¦¨ª«¬®¯¯¯°°¯¬««ª¨£m[RRWYVPIDDBDEBHOSZip_P\db^\`fjnstsvxzz{}||~vpmklosx}
}{yutrpoononcYgum[J@<=?CKT\dv¢£¥¥¦¨ªª«¯°°±°¯¬ª©§¦¡qaSSYYWSJ?CBBCFMRX\ep`IS]ZYY]bdgkortwyz{~
zxwuuuvqnnpsx~}{zzxurpjhhijjja`moaQD979<>FOYai~¢¤¥¦¦¦¨ª¬°±±±°¯««©¨¥¥¡wgVT_]YTF:IOGCKTRV]fq`DKUUVX]`acglnqtwz|~
zusrrrrztqpruw|{yxwtqnjd`bdedc^enfUH<568=>AKSZcp £¤¥¥¥§©¬®°±°±¯®««¨§¥¤ qXUbbYTJMiBBHKMViq[BISTUY\_`aejmoswz}
|vsqqqr{wutv|~zwurqnke_]\`ba[[kk]MA88:=?<@JPYbh££¤£¤¥§ª¬¯¯¯¯®¬«ª¦¥¥£xTTgh[UNNr\ABFJN[mjTCHQUY[\^_`diloswz}}{xuuuu|zxx|}xtpookga]ZZ[_[MUlfVJA96<B<>MXUZehp¢¤¤££¤§ª¬®¬ª¨¥¤¤£¡uMOfk]TOIIGBDEINUdl^J@GOTXZZ\^^chlosuy}
~zyxx
}|{}}wrnmligb]ZXXZWISidUKB<Iia?;GOS]hig ¢¢¢£§ª«¬¬«©§¥£¤££pGI[k`VRLIGFGJMQZfeTDBHMRVVXZ\]agknrvy}
}zx}wrnljhgb]ZWWWTGQfcUKB<`t?8@FP^kja{¡¢£¥ª««««ª©©¨¥¤£¤¢£ kC>HbdYURONOOOPV^cZICGLPTTSVY[\`glnrwz}
}zwt
|vrnkhfe_[XUTUREMbcWMD@FLB8<ELS`kfYn¡ ¡¡¢¦¨¦¥¦¥¥¥¤£¢¢¡ iC<>N]\WUTTUTSSZb\NBDLPSUUVY[\_dhmosx|
~zxtqzupljgc_\YVVUUQEDYf^PEA@<8:AJOXdg]Sh¡ jG=>FNVVSRRRQPQUVLDGT^fiigefedgiknrvy}}yurq~ztoihe_\YWVVUUSJCNfhYOIC@>>FNS_hbVRg gQFGZ[SPQRSTTVYZXYeu~|xurnhefilotx}zvts
~yrmieb]ZVWVVWWVRI@Qge[RJGJIMSW`f]TTerdekz{zz{z{{}{vqigjnprwz
|yu
~ysnifb]YUUXYYYWVQC<Oa`ZSTVVWZ]ada^\g{|vqpsttty}
~zx~
}zwqkhd`\ZZ\[[ZXVRLEFOSRSSTVZ`hs{xppu¡¢¢
~}ywy{~
~{xsmiebaaceecb`][\WTSSSSSUboz{{¡¥©ªª¨¦§¨¨©¨¦¤£
~yusvy|~
|yvqjgeedflpqrsx}£§«¯±¯«¬¬¬¬«¨¦¤£ ~ynqsvxz|
~|ytmhfefhlpu|¢¦ª¯°¯®«ª§¦¥¤¢¡hlnpsuxz}zupkffhklp{¡¡¡¡ £¦«®¯¯®®®®¬««ª¨§¥¥¤¢¡ ¡
dfhkmprtw{}}xsnlkmpqu¡¥¥¥¥£¡ ££¡¢¤¦ª¯°°°°°°¯®¬«ªª©§§¦¥£¡¡¡ ¡¢¢¢¢£££¢¢¡ ¡¢¡¡ `acegilnptvy}{upnoruz¢¦©ªª©©¨¨¨¨§¤¢¤¤£¢¢¢££¥§«®±±°°°°°°°¯ªª©©¨§¦¥¤££¤££¤££¤¤¤¤££¤¤¤££¢¢ |]_acdfhkmoruy}~xtrtvz£¦©ª«®®®¬«§£ ££¤¤¤££¤¤¥¨«®°±°°°°°°¯¯¬«ªª©¨§§¦¥£¤¥¥¥¥¦§§¦§§¦§§¦¥¥¤£¢|\^`abceghiloty|~~~ ¦ªª«¬¯¯¯®¬«©§¤ ¡¡
¢¤¤¤¤¥¥¤¤¥¦¨©¬¯°°¯¯¯°±°¯®¬««ª©©§¦¥¥¥¥¦¦§¨¨©©¨©©©©©¨§¦¦¥£
Z[^__``bcdgjotwy{~
£§ª¬¬®®®¯«©¦¢ ¡¢£¡~¡££¥¤¥¥¤¥¦¦¨¨©¬®¯¯°°°°°¯¬««©¨¨¦¦¥¦¦§§§¨©©ªª©©©©©©©¨§¦¥£¡XY[\\\]^_abfjortuwz} ¡¢ ¡£¤§©ª¬¬®®®®®«©¥¢¡ ¢¢£¢}~ ¤¥¥¥¥¦¦§©©©ª«¯°°°°°¯¬«ª©©¨§§¦§¨©©©ª««ªªª©©ª¨¨¨¨§¦¥£¡WYYYYZZ[\]_afkmnortw{}¡¢¢¢¢¡ ¡¢¤§ªªªª¬¬®®®®®«¨¥£¢¡ ¢£¢¢£~
¡£¤¥¥¦¦¦¨©©ª©©ª¬®®®®¬ªª©¨¨¨¨¨¨©ª«««¬¬¬«ªªªªª©ª©¨§¦¥£¡YZYYYYZZ[\]^bgjkmoqtwz ¢£¢£¤¤¤¡¢£££¢£¥¦¨ªªª«¬¬®®®®®¬«©¥¤£¢££££¡ wx~~~}z
¢¤¥¥¥¦¦§¨©©©¨§¨ªª«««««¨§¨¨§¨©©ªª«¬¬¬¬¬««««««ªª©¨§¦¥£¢ XZYYZYZZZ[[[]behjlorvy~ ¢¤¤£¤¥¥¦¤¤¤¤¤¤¥¦¨ª©ª«««¬®®®¬ª§¥¤£££¢£¢¡xrsw{z}gju| ¢¤¤¤¥¥¦¦¦§¨¨¨¨§§¨©©ªª©¨¦¥¦¥¦§¨©««¬¬¬¬¬¬¬«ªªª©¨§¦¤££¡XYYYXXXYZZZZ[]_beikmqv{
¡¢¤¤£¥¦¥¦¥¦¦¦¦¥¦§ª«ªª«ªª®¬¬«©¦¥¤££££££ uurfao
pgv¢£¤¦¤¥¥¥¥¥¥¦§¨¨¨¨¨¨¨©©©§¦¥¢¢£¤§©ª««¬¬¬¬¬¬¬¬«ªª©©§¦¥£¤£ XYYXUUVWYZZZ[\]`cefimty¡¤¥¤¥¦¥¥¦¦¦¦¦§§¨ªª©ª«««®¬««©©¦¥££¢££££¡wnttx¢¤¥¦§¦¥¥¥¥¥¤¦§¨¨¨¨©©©©¨¨¦¦¥£££¥§©ª««¬¬¬¬¬¬¬¬¬¬¬¬¬«ªª©©§¦¥¤££ XYYWTRTUVWWYYZ\]_begmt{¢¤¥¦¥¦¦¦¦§¦§©©¨©¨¨©ª««¬®¬«ªª¨§¦¥£¢¢££¤¤£¢¡ ¢¢¤§¨¨¦¦¥¦§§§©©¨©©©©¨¨¨¨§¦¥¥¥¤£¥§©ª«¬¬¬¬¬¬¬¬¬¬««ªª©©§¦¥¤¤¢ YYYYURSTSRSUWY[\^bgls|¢£¥¦¦¦§¨¨¨¨©©©©©¨¨ª«««¬¬«ªª©§¦¦¦¥£¢££¤¥¥¥¦¥¤¤¢£¤¥¥¨©©¨¨§¦§©©©ªªªª©©©©©§¦¦¥¥¥¥¤¦¨ª««¬¬®®®®®¯®¬¬«ª«ª©©¨§¤¢¢¡Z[\[XUUUUTTUWY[]`els{¡¢¤¥§¨¨©¨§¨©¨©ª©¨¨©©ª««««ª©¨¦¥¦¦¦¥¤££¤¤¥¦§§§§§§¦¤£¤§¨¨©ª©ª©¨¨¨¨©©ªªªª©©©¨¨¦¦¥¥¥¥¤¤¦¨ª«¬®¯°¯¯¯¯°¯¯®¬««ª©¨¨¨¤¡\]]]YVXXVWWWWZZ^bgow~¢¤¥§¨¨©¨§¨©©©ªªª©©©ªªªªª©¨§¥¥¦¦¦¦¥¤£¤¥¥¦§§¨¨©©§¥¢¢¦¨ªªªªª©©©¨¨¨©©©©©©©©©©¨¦¦¥¥¥¥¥¦¦¨©««®®¯¯¯¯¯°¯¯°¯®¬ª©¨¤¢¡¡]^][VSTTTUTTSVY]ciry¤¥¦¨©ª©©©ªªª««««©©©©©©©¨¨§§§¨¨¦¦¥¤¤¥¥¥¤¥§©¨¨¨§¥¤ ¤¦©ª«ªªª©©©©©©©¨©©©©¨¨¨¦¦¦¥¤¤¥¥¥¦§¨ª¬¬®®¯¯¯¯¯¯¯¯ª¨¦¤¢^_][VSSSSSSRRTZ_hpv|¢¤§©¨ªª©ª«««««¬«©©©¨©¨¨¨¨¨¨¨¨¨§§¦¥¤¤¥¥¥¦¨©¨¨¨§¦¤ ¢¤¦¨ªª««ªª©©©©©©©¨¨¨§§§¦¥¦¥¥¥¥¦¦¦¥¨©«¬®¯¯¯°°°°®¬¬ª¦¤¡
`a`]ZXWVTTTTTW\`fov¢¤¦¨¨ª«ªª«««««««©©¨¨¨¨¨¨¨¨¨¨¨¨¨¨¦¥¤¥¥¦¦¦¨¨¨§¦¦¦¥¡¤¦¨©ªª©©©©¨¨¨§§§§¦¥¥¥¥¦¥¦¦¦§§¦¥¦¨««®¯¯¯°°®¬«ª§£ ~cbba^\ZYXVVUVX\`enx¢¤¦¨©ª«««««««««ª©©©¨©¨¨§¦¦§§§©¨¨§¦¥¦§¦§¦¦§§¥¥¥¥¤~ ¢¤¥§¦§§¨©©©©§¦¦¦¦¥¥¦¦§§§§§§¨©§§¦§ª«®¯¯¯¯¯¬«©§¥£
hhhfdb`^\[ZXX\aflu~¡£¥¨©ª««««¬¬¬««ª©¨©©©¨¨§¦¦¦¦¦¨©©¨§¦¦§§¦¦¦¦¥¤¤¤¤¢y ££¥¥¦¦§¨§§§§¦¥¥¥¦¦¨§¨©©©©©©¨¨©ª««¬®¯®®®¬«©§¦¥¤¡
mmnljhfdca^]_ekr{¡¤¥¨©ª««««¬¬¬¬««©¨¨©©©¨¨§§§§§¨©©¨§§§¦¦¦¦¥¤££¤£¢~v{ ¢£¤¥¦¦§§¦¦¥¥¥¥¥¥¥¦§§©©©©©©©¨¨©¬¬¬®®¬¬¬¬ª©¦¤ qqrqpnmljgedipw}¡¤¥¨©©««««¬¬¬¬¬¬«ª©©©©¨¨¨¨¨¨¨©©©©¨¨¨¦¦¥¥¤£££ {} ¤¥¥¤¥¦¦¦¥¥¥¥¥¥¥¦§©©©ªªª©ªª«¬®®®®¬«ª©§¤¢vvuutttsqnmpw{¡£¥¨©©ª«««¬¬¬¬¬¬ª©¨¨§¦¦¦§§§¨©©§§§§¦¥¤£¢¢
¡¡¢£¤¥¥¥¥¥¥¥¥¦¨©©©©©©©ª««®®¯®¯¯¬«©§¤£¡ zzyyyxxxwvx{
£¥¨¨©«¬¬¬®®¯°¯¬ª©¨¨§¦¦¦¦¦§¨§§¦¦¥££¢ ||¢¤¥¥¤¤¥¦¦¦§©©ªª©©ªª«¬¬¯®®®¬ª©¦¤£¡¡ }}{z{{z{} £¥¨¨ª¬¬®¯¯¯°±²²±¯«ª©¨§§¨¨¨¨¨§¦¥¦¤£¢ }ysru~££££¤¥¦¦¦¨©©©©ªª¬¬¬®®®¯®¬«¨¦£¡ ~||}} £¥§©ª¬¯°°°±±²²²²°®¬«ªª©©©©©§¦¦¥¥¥£¡~{ ¡¢£¥¥¦¦¨©©©©«¬¯®®®¯®®®®¯°¯¯«©¦¤¢ }|
£¥¨©«¬®°°±±±²³³²²±°¯®¯®®¬ª©©©¨§¦¦¥¥£ ¡¢¤¥¦¦§©©ª©©«®¯°¯¯¬®®¬ª¨¥£
~
£¥¨ª¬®¯°±±±°²³³´³²±±°°°¯«©©©¨§¦¦¥¤£ ¡¡¡ ¡¢¡ £¥¥¦§¨©©©©ª«¬®°¯¬«¬««««ªª©¥£¡~||{} £¥§ª¬®°°°±±±²³³³²²²²²±°°¯¬ªª©©§¦¦¦¥£¢£¢¢££¤¤¤££¢¡ ¡¢££££ ¢¤¦§§§©©ªªªª«®®«««©ª©©¨§¥¤¡
{zzz}
£¥§ª¬¯°°±±°±²²±²²±±³²±°°«ª©©¨¨¨¨§¥¥¥¦¥¥¥¥¥¤£¤¤£¢¡ ¢¤¥¤¤¤¥¤£¡ ¡¡£¤¥¥§©©ªª©ª«««¬¬««ªªª©©©¨§¦¤ |yyyy|
£¥§ª«¯°°±±±±²²²±±²²²±±°¯¬ª©©¨¨¨©©©¨¨¨§¦¦¦¦¥¤¤¥¤¤£¤¤¥¦¥¥¥¥¦¦¦£¢¡ ££¤¥¦¥§©ªªª©ªª««««©ª©©¨¨¨¦¦¥£¢ {zxy{~ £¥¦ª¬®¯°±²²²²²²²²²²²²²±°¯¬«ª©¨©©ªª©©©©¨¨¨§¦¥¥¥¦¦¦¥¥¦§§¦§¦¦§¨§¥¤££¢¤¤¥¦§¨©ªªªªªªªª««ªªª¨§§¦¦¤¢¡ |z|~
¢¤¦©¬¯°°±²²²²²²²²²²²±±°¯®®«ªªªªªª©©©©¨¨¨¨¨§§¦¨©©©©¨©¨§§§§¨©©§¤£££¤¦§©¨©«««ªªª©©ªªª¨¨§¦¥¤¡ ~
¢¤¥¨«¬¯°°°±²²²²²²²²±±±±°°¯¯¬¬«ªª©ªª©©¨¨§¨¨¨¨¨§©ªª©ª©ª©§¦¦¦¦¦¦¦£££¤¥§©ª¨©«««««ª§§¤¢
¢¤¥¨ª«°°°°±±±±±±±±±±±±°°¯¯®¬«««ªª©©©©¨¨¨¨¨¨¨§¦¨©©©©©ª©©¥¤¥¥¤¤¥££¤¤¥§©¨¨©ª«««¬¬©¦¡£¥§©ª¯°°°°°°°°°°°°°°°°¯®¬«ªªªªª©©ªª©©©¨¨¨¨§¨©©¨¨©©ª©¨¥££¤£££¢££¥§¨¨¨¨©ªªª«¬¬«§
£¥¦©«®¯°°°¯¯¯¯¯¯¯¯¯¯¯®®®¬««ª©©ª©©©ªª©ª©©©©¨©©©¨¨©©©©©¦¤£¤¤¤£¢£££¦¨©¨¨¨©ªªª«¬¬ª¥¢¥¦©«®®®¬®®®®®®®«««ª©©©©©©©©©©ªªªªªªªª¨¨¨¨¨¨¨¨¨§¥¥¥§§¨¥¤¤¥§¨©©¨¨¨©ªªª«¬¬ª¥¢¤§©«¬¬«««¬««««««¬¬«ªª©©§¥¥¦¦§¨©©©ªªªªªªªªª©¦¦¦¦§¦¦¦¦¥¦§¨©©¨§§¨©©ª©§¨¨ªªªª«¬¬¨¢ ¤¦©ª«ªªª©©ªª©ªª©ªª©¨§§§¦¤¢¢¢£££¤¥¦¦§¨©©ªªªª©§§§¦§§¨¨§§¨ª««ªª©©ªªª©¨§§¨ªªª«ªª«§¡
£¦§©©©©§§§¨¨¨§§§§§¦¥¤¤££¢ ¢¤§©©ªªª©¦¦¦¦§§©ª«¬¬««¬¬¬«©¨¨¨©©ªª©©§¦¡
£¥§¦§§§¦¦¦¦¦¦¤¤¤¤¥¤£¤¢¢¢¢¡ £¥¨©¨¨¨§¤¤¤¦§¨ª¬¬®®®®«¬¬¯¬ª©©¨¨§©©©¨¦¤
¡¤¤¦¥¥¥¥¦¦¥¥¥¤££££¤£££¢¢£££¢ ¡¢£¥¦¨§¦¤¡¡¢¥¨ª«¬®®¬¬®°ªªª©¦¦¨§§¥£¡
¡¡¡ ¡¢££¡¡££¤¥¥¥¥¥¥¥¤¥¤£££££££¢¢£¤£££££¡ ¡¡¡¢£¤¥¦¨¦¥£¢¢¦ª¬®¬¬¬«««««ª¯¯«ª«¬ª¥¤¤£¢¡
¢£££¤¤¤¤¥¦¦¦¥£¡ ¡¡¢¤¤¤¥¥¥¥¥¥¥¥¥¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤£££¢¢¢¢¢¢¢¢£¤¥¥§§§¥¤¤¥ª¬®¬¬ªª©¨¨¨¨§¨ª®¬¨§ª¬©¥£¡
£¦¦¦¦¦¦¥¥¥¥¦¦¥¦¥£¢¡ ¡¢¢¢¤¤¤¤¥¥¥¥¥¥¥¥¤¤£¤¤¤¤¤¤¤¤¤¤¤¤¤¤£¢¢¢¢¢¢¢¢¢£¥¥¦¨§¦¥¤¤¦«¬ª§¦¥¥¥¤¤£¦©¬«¨£¤¨§¥¢
¡£¥§¦¦¦¦¥¤¥¤¥¥¥¥¥¥¥¤¤££¢¡ ¡¡¢¢¢££¤¥¥¤¤¤¤££¢¢££¤¤¤¤¤¤¤¤¤¤¤¤¤£££££££¢¡¡¢¤¥¥§¦¦¤£££§ª«©¥¤£¢£¢ ¡£¥§§¡¢
¢¤¥¦¦¦¦¦¥¥¥¥¥¦¦¦¥¥¥¥¥¥¥¤£¢¢¡¡¡¡¡¢£££¤¤¥¤¤¤££¢¡£££¤¤¤¤¤¤¤¤¤¤¤¥¤¤¤¤¤¤¤£¢£¥¥¥¥¥£¡ ¤¦¦¤¡ ¡¡¡ ¢¤¥¥¥¥¥¥¥¥¦¦¦¦¦¦¦¦¦¦¦¦¦¥¥¤¢¢¢¢¢££¤¤¤¤¥¥¥¥¥¤£¢£¤¤¤¤¤¤¥¥¥¥¥¥¤¤¤¤¤¤¤¤£¢ ¢¥¦¦¥£¡ £¡¡£¡¢¤¤¤¤¤¥¥¦§§§¦¦¦¦§§¨¨©¨§§§¦¥¥¤¤¥¥¦¥¦¦¦¦¥¥¤¤¤£¤¤¤¤¤¤¥¥¥¥¥¥¥¥¤¤¥¤¤¥¤£¢ £¥¦¦¥£¢ £ ¢¢£¤¤¥¥¦§§§§§§¨¨¨©©©©©©©©©©¨¨¨¨©¨¨¦§¦¥¥££££££¤¤¤¤¤¥¥¦¦¦¦¦¦¥¥¥¥¥¥¤¢¡¡£¤¥§§¦¤£¡¡¡££¤¥¥§§§§¦¦§©¨©ª©©©©©©©©©©©©¨¨¨¨¦¦¦¥¤£¢¢£££¤¤¤¥¦¦§§§§§§¦¦¦¦¥¥¥¤¢¡ ¢¤¥¥§¨¦¥¤¢¡££¥¥¥§§§§¦¦¨©©©©©©©©©©©©©©©©©¨©¨¦¦¥¤£¢£££££¤¤¥¥¦§¨¨¨¨¨¨¦¦¦¦¥¥¥¤£££¤¥¥¥¦§¦¦¥¤¡~~~~~||||}}~~zzz{{|~~~}}~~~~~~~zxyyz|}~~~}||}}}}~~}|zyyyz|~~}|{{{{||}~|zyzz{}}||{z{{||}~~~~}~}~}}zyyz|~|{|zz{{{{|}}}}|}}}~~~~~~}|zyzz{}~}||{{{zz{||||{|{{|||}}}}}}}}}~}|zzzz{||~~}|{zxyxz||{{||{{|{{|||||}}|}}~}|zzzz{}}~}|{zzwwyz{{zzz{zzz{{||||||||}}|zzzyz|}}|{{zyxxyzzzzzzzzz{{z{{{{||||~~~~~~}zzzyz|~~}}|{yyzzzzzzzz{{{zz{|{zz{|~~~~~
|zzzzz|~~|{zyz{zzzzzz{{{{{{||{{{|~~}}~
|zzzzy|~~}{zzzzzzzzzz{{{{{||{{{{|}~~
}{zzzz{~~}~~|zyzzzzzzzz{{{{{|{{{|{}~~|zzzzyz~~~~~~~}zyzzzzz{zz{{{{{||||}}~|yxyyxy~~}{yzzzzz|{zz{|||||}}}}~~{xxyyxy}~~}}|zyzzzz|{zz{|||{|~}}}}~~~zxxyyxz|}~}}|{zzzzz|{{{{{||z{}||||}~~}zyyyyx{{}~~~~}||{zzzzz{{|}}||{yyz{{{||}}~~~~~~}{zyxxx{{|~~~~~||{zzzzzz{|~~}{yzz{||||}}}~~~~~}}{zxxxyz||~~~~~}|{{{{yzzz{~}|zzzz||||}}}}~~~~~~}~~|{yxxxyyz|~~~}||{z{z{zz|}|{zzzzzz{|}}}}}~~}}}}}~|{zyxxxxyz|}}~}|{{zz{zz|~~~~}{zzzzyyz{|}|}}}}|}||}~~~}|{zyxwxxxx{|{}}|{{zz{{{}~|zzzyxxzz{||||}}||||}}}}~~}||zzzyxyyxxz{{~~~~}|||{{{|}}~|zz{yxy{{{{||{|||||||}}}|}|{{{z{yyyyxxzz{~}}||||||||}~~}{|||{||||||||||||}||}||{{{z{{||zzzzyyyzz~}|{{||}}|}~~|||||||||||||||||}{z|{zzzzz{{{{{{zzyyyzz}|{{{}~~~}~~|||||||}}|{{{||{{|{z{{{yyz{||||{{{zyzzzz~}{z{{}~~~~~}|}}|}~}|}|||||{z|zz{z{zy{{|||||{{{zzz{z}}{|||~~~~|||}}}}~~}}|||{zz{yyyyyz{{z|||}}||||{{z{~~~||}~~~~}~}}~~~~~~~|{{zzzzyyyyyz{{z{{}}~}||||{{{~~~~~}}}~~}|}~~~~~}||||{zzyyyzzz|{||}~~~}}|||||~~~~~|{||~~}}}~~}}|||zzyxxzzz{{|||}~~}}|||{|~~~~~|{{|}}}~~}~}}}||{xwwxzzz{{{{||}~~}|{z{{~~~~~~~~~|{||}}}~~}~~~}||{xwwyzzz{|{|||}}}}|{zz{~~}}~~~~~~~|{{{|||~~}}}~~}||{zxwwyzzz{{z{||}||||{zzz~~~}}~~~~~~}{zzz{{
~}}|~~}}|{zxwwyzzz{{z{||||{|z{zzz~~~~~~~~~~|{zz{{|~~~~~}|zyyxyyzzz{|{|zz{{z{zzzzy~~~~~~~|{zz{{|~~~~~{zyyyyyzzz{|{|{{{{{{zzzzy~~~~~~~|{z{{|}~}|}}}~}|{yyyyyzzz|{{|{{{{zzzzzzy}}~~~~}~|{z{|}~~~~}||}~~}|{zyyzyzz{{{{{{{{{{{{{{z{}~~~~~~~~~}|{zzz|}~~}||||~~~}}}|||{zz{{{z{{{{{{z{{{{z{{|}}|}~~~}|{{{||~~||||~~~~~~~~~~|||{{|{{|||{{{{z{{{{{zz||||}~~~~}}}}}}}~~}||{{~~~~~~~~~}|||||||}|||||{{||||{zz{{{||}~~~~~~}|zzz|}~~~~~~~~~}}}}||{{{{|||||||zzzzz{|}||}~|{zzz{|}~~~~~}}|{{{|}|}}}}zzzzz{||{{|~~}|{{{|}}~~~~~~~~~~}}}|||||}~~~~zzzzz{|{z{|~}||{|}~~~~}}}~~~~~~}|||||||}}~~~zzzzzzz{||}~~}|||}~~~~~}|}~~~~~~}}|||||||||||zzzzzzz{}}~~~|}~~~~~|||}~~~}}}}}~}}}|{{{{{{{|zzzzzyy{|}~~~~~}}|}~~~}|}}}}}||{{{{{{{|zzzzzzz{|~~~~}|~~~}|{|}}}||{{{{{{{{zzzzzzz|}~~~~~~~~}}{{{||{{{|{{{z{{{zzzzzzz|~~~~~~~~}{{{{{zzz{{{zzzz{{zzz{{{{|~~~~~~~}}|{{{{{{z{{{zz{{zz{||||{{}~~~}}}}|{|}||{|{zzzzzz}}|||{{|}~~~~~}}~}|||}|{{{{z{{{z{}}}}|}|{|}}}}~}~~~~}}~~}||}}|{{{|||{z{}}}}}||||}}~~~~~~~~}}}}|||}|||||||{{z}~~}||||}|}~~~~~~~~~~~}|{{{{|{{{{|{zzzz~~|||}}}}~~~~~~~}|}}}|}~}zz{zzzzz{{{yyyyz~~~}|}~~}~}~~~~~}}}|||}||}||z{z{zzzzzzzzyyz~}~~~~~~~~~~~~}||}|}|{|}}|z{zz{{zzzzzzzzz~~~~~~~~~~~}}}}}}~~~~~~}|}~}}}|||}|z{zzzzzzzzzz{{z~~~~~}}}}}}}~~~}}}}}}}}}}}}}}}}}~~~~~~~}}~}}}|||}|z{zzzzzzzzzzzzz~~~~}}|||~~~}|~~~~}}}|||}||||||||||}~~~}}}}~}}|||}}||||{zzzzz{{{{{z{zz{~~~~~~}}~~~}|}~~~}}}}}}|||||||||||}|}~~~~~}}||}~}}}}}}|||||{{{z{{{{{{{{zz{~~}~~~~}}}}}}||||}}}}}}}}}|||}||}}}}||}}|~~}}}}}}}||||||||||{{{|||{{|~~~~~}}~~~}}}}}}||||}}}}}}}}}||||{{{{{{||}}}~~}}}}}}||{{{{|||||{{{||{{{{{~~~~}~~~~~~~~~~~~~~~~}~~~~~~~~~~~}}~~~~~~~~~~~~~~~~~~~~~~~~~}~~~~~~~~~~~~~~~}~~~~~~~~~~~~~}~~~~~}~~~~~~~~~~~}}}}|}}}}}}}}}}}}}}}|~~~~~~~~~~~}}}}}}~~~~~~~~~~~~~~~}}}}}}}}}}}}}}}}}}}}}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}}}}}~}}~~~~~~~~~~~~~~~~~~~~~~~~
~~~~}}}~~~~~~~
~~~~}}}~~~~~
~~~~~}}~~~~
~~~~~
~
~
~
~~~~~
~~~~~
~~~~~~
~~~~~~
~
~~
~~
~~~
~~
~~~~
~
~~~~
}}~~~~
|~~~~~
}~}~~~
~~}~~~
~~~
~~~
~~
~~~~~
~~~~
~~
~~~
~
~
~
~~
~~~~}~~~~
~~~~}}}~~~~
~}~}}~~~~}}}}~~~~
}}}}}}~~~~~~~~~~~
~}~~}}}~~~~~}~~~
~~}}}}~~~~~~~
~}}}}}~~~}}}}
}}}}}~~~~|}}}~~
}}}}}}}}~~~}|}|}~~
~}}}}}}}}~~~}|}}}~~
~}~~~~~~}}~~}}{}~
~~~~~~~~~}||||}~~
~~~~~~~~~}|}}}}~~~
~~~~~}}~}}~~~}}~
~~~~~~~~~~~~
~~~~~
~~~
~~
{wy~vjb`ajx
z`IBDKWi}
zdMB?@CGPj
gJ=<?BFKNXr
oT@;@HS^dinw
~
]A:=CO^ipuvy
~~~~{V?:@KU`kquwwx
~}|}}|}
mM=:?NZ_fmpqssw
|zzzzzyz}
hD8<>HX`bhmnnnrv|{xxyxxwx{}
^@8<AIV_bdioqrquz~
zxxxwvvvz|
~~~}||||}}}~
|]<5;DOV^eilquxwvwy|
zxxwuuvwz}
~~}||{{zzzyzzz{}
xZ=5:AMZ^cjnrtuussrswyvuutttvy}
xo
~}|||zzzyxwwwxwwy{|}~
sR828>GU_ejmorsssrrrrvvssrrstvx|
vZWo
~{zzzyyxvutsttttuxzz{}~
lO806?GQ_dglnooqrrsstsvuqqqqstvx|
}fSTf|yyyxxwtsrrrsrqqrtwwwyz|
}whJ425<GS^ddfklmnoonnnnmpzuprqrrtvx|
}p[NK[}
~|yyxxwusrrrqqqonpqtttwwx{|}}}
~pW?1/4=HSW\]adhklmllkjjiimw
uoprsstvx| ~vgUKFRu}{yyxwusqpppponmlmorsssstwxwwxz}}}~~~~{nU=0049BMUVY^ekoqqqrtuyzywv|
}vuvuvuvx|
}zo_RHDQm~{yywvtrommmmllkklmnooooqstssuwyyyz{||}~~~~}}}~~~~~~}xmT9/058<DLTVZcjmprtuwxxwxxwx}|zyxxwwx}
{ysm^NIGJWu~|zyxwvtqnlkkkkkjklllllmmoqqqrrsuvvwwxxyyyyyzyyz{{{{{~taM>304;@EKRXZ^cefgjmoqpponnlnv~xwwxxxxy}
{ysmdVOJDBNk
}{yxwwvuqnlkkjjijkkklllllnnnoooqsssttuvvvvvvvutuvvvttvsbC2246;DIOTY\]`cccehjmnmmmmonmrvqrtuvwxz}
}ztme[TOH@@Hb~
~|yvuvvvusolkjihhijkkklklllmllmmnooqrrssrrsrqpqqrssrokbUA2047:@GNTX\]]_abbdgilllmnoqqnruopqrsuxy}
}ztoh`YUME@?CSds~|
|zxtstvxwtpmkigfefhjjjjjjklklllklkkoqqqpooonlklmnookdZI:1148>CHMRW\^^^_acfgjlmnnnoqttpsupoqrruxy}}zuqkb]YQIC>:;BQ\][bz}{zwtroptvxvqnmkiihjloqpppqrrsrrrrolknpqrokgfeddedbbb\L@3.06;@HKPSW[^aaacfjnprrqqqrsvxxtwwsrsstuxy}
~zvsme`\VNHC<98<ACCHdutpqrsqolhefkrvvqmlllosz~~~zsoqrstqg]XVVWVURPQI91-,0:CJNQUXZ^acefilnqtvutttuwz}{vyxttttvvwz~
~zwsoiea[UOIB;98999:DUVTVY[\]_]Y[cltwtppru{|vvwxyvi[TOMPONJHE<2.,-6EMORUY\_bdfhjmoqsvvvvuuvvz~{xzvrrtuvvwy}
{wsqlhe`[VOH@;877668=BEFHKLNQQNNVguyz{|
}|}}~{pb[PGLMIB><80-/3BPSVZ]_adeghjlnpstutsqopruy|zw{{qmnqsuvwy}}
~{wtrnjfb^ZULD>:8654469;=?DHHHHGINew{
ypk]QJIF>:;8227AJSX^cccbdfghjnoqrqqonlklnsxyzx|xojknqsuwz~}
|xtrokgca]YRJC>;85212369<@EEDDDHPdv}
}zyl\OJG=::669CKQW]cdcaadhiiloqqomlkklllotxz{y}slikmoqtw{~~
{wupmidb^[XSJC?:62001347;ACEFIOYfv
~{ug[UQA<;9>DLQW^dgfc`aiomklnnlkhhhiklmouz{{{~oihjlnqsvz}
|xtolieb^[YVQIB<841////16:CMSYajq{
~|vnh]KDEDGLST[iokgecgoqnjggeeghiikmmnpv{}}z~pljklnqsvx|
|zusokhf`\YWVTOGA;73/--.05>O[emtwz
|teWRRRRUXXaquoiffmslhfeedfikmmooopruz{zysoommorstx{~
~}{zvtqmif_ZXWWUSOH?940./.0<O`jt|
~
znc_]]^`aajvwsnjjrsgabfjloqqrqqpnpstvvuuurpmmorrtwy}
~~{zxvsolifc`\XUUPH?:6456;M`ku~
}z~umkjkihjktxstpmsxmfcfmrwxvronkjjmqsttru
xspmlnrrsvy|~}|{zwsssqnia\YWURICAACCM^ju~~{}
ztvwyupqsyxrtpovvpkiow}ypigfehmoqsstv{ysollnrrruy{
|zz~~|zywusrrpnhb\XW^WLKNW_fkt{~~
~z|~{{||vsquysqnq|
pgeedflsuwyyx{yplklnqpqtw{
{yz{}~|{zywtqononlg_ZX^c\TWgrwz}
}{|~}ypq}{srt}ujhkmmt|~vnkmprqopruy|{zzz{|{{{zyxuqmkjkkjfa\Yagcakw
|z|
utz~{xy~znlquw}}}rmouxvsporuwz|
|zzzzz{{{{{{yvrnjijlmiaZ^horw
}{||~}|~{z{ttvxy||y{{tw{~yurpoqtwy{}¡¡}zzyyyz{|}|vnlnonjffjt~
~}|~~zy{{}}|vwyz{~}z{}xtronpruwy|
¡¡
}zyxwxz{}
ztqrsuutv|
}zxx{~~
|yzyz{~xvxy{~yz~yvsnmnptuxy}¡¡|zxwwy{
{y|~~
~}}|ywuvwyzz} ¤¥¡{yxxy|
wtwy{xtz
{smlmoqtvxz
¡~zyyz}
}{zyyvrqqrtuwy{£¦§¤¡
~zxvvx{~
}z{{|trz
tnlklmorsvy|
~~}yvvuuspoooqqswx~¤§¨¥¢~ywwvw{~
{qr}wmkjjklmortvx{~
}zvsqqqponmnonquuz¥§¨§£ }ywvux{}
{tu
}oljiijjjmprstxz|
{wtrqpoonmlmnmorp|¡¦§¨§£
~ywtsvy{}~
usxvpmlllllkjloqruvy}
|zwtrqonmkihjllopk|¡¦§¨¨¤¡
zvsptxz|}}~~}}~
ztsy
wrqonnlklmnorsvy|
~~|{zwuspnmmlhgghinqj}¢¦§©¨¥¢
|wroqvxz{|||{zz{{{|vvx~
}vqnmlkkllnqrtvx}}{}
~|{zxvurpnnmljgdbclrk¤§§©¨¦¥¡zvqklsvwxzzyywvvvvy
~zxxw{~
}uonnmmmmnqsstv{~|zz
~{zyvtspnmmmmkga^`kqp ¥¨©©¨¦¦£wqmefnqstxxwutttttux}
}zwuw{spv~yutssrpopqstux{~
{{
~|zyxtrpnlkkllkhb]_my£¦§¨©¨§¦¥¡yqkb^fknqtwuttqqqqruy}
}ytw|xgfnx{xywurooprtvxy{}~~
}|}{zxxwsqmihiiiiihdagw ¤¦§©¨©¨§¦¡tlaY`eflrtsrqomnnpruy|
~yvx}}kbjsz~~}zwtrrrstvwz|zz~
~~|zxvtsspmjgfgghhhhfdq¢£§©ª©ª©©§£
wm[U]__fnppomkkllnpruy}
zxyzrfenuz~~{yxvuttvx||yxyxy{
~|xvusqonkgdbbccefggggl ¤¦§©«««ª©©§¤¡|jRRZ[\bjllljiiiiknpruz
}yxvoffovz
}|{xwxzzyxxx}zwwvvuux
}yvsqomkjhc^[Y[`ccdegkp}¢¦©ªª««¬¬«©©§¥£ hMOUX\ciiiihgghijlnorw}
}zwsqqsx|
~}zyvtvz||{yy~soqvzvqqy
|zwsqnlkkiie\WTTV]`adehu ¥¨ª«ª«¬¬¬¬ª©¨¦¤£nLJQV]dgfffedegiiklnqty~
}|}}}}}~}|yvtuvxy
vaZ\cntrru{
}zvusomjgghihdZSQPQW[_egm¢§ª«««¬¬«ª©¨§¥¢qMIPW`fecbbaaadghiklmrw}
~ytstvx|hPHHJVeptvy}
yupomjgebacefbYRNNNSZahkw¢¥©«¬«¬¬¬¬«ªª©¦£sNNW\abba`][]_bdfghijouy}
~}|{{m^OJKKR\fmty
{wrmjhdb`__``baZRNNNRZblt
¢¥§ª¬¬¬¬««©§¤\Zbfd`a`^[VVY]_accfimrw{~
~~~ubTKJKLPY^frz{vpjec`^]]^^___[TQQQUYbp~ ¤§©¬®¯¯¯°°®¬¬¬«¨¥¡yrtqi_`_ZUQQTX[]`cfimrvy|
~yxvvx}vgUGBGJNW`ju~|vpic`_]\[[\\\]\XUSTVZct¢£§©«¯°¯¯¯¯¬¬¬¬«©¦£xj`\WQLILPSVZ]cikmqtwz~
|yvttw|
~xq_TH@CDEJZmx
}vqjda_][ZYXXXZZZWVXY[fy¡¤¦©«¯°°¯¯¯¯¬¬¬«©§£zk]RKDABDGJMSZahlnrtwy|}~
|zxuu{~}vhYOJCA@ADHZn|xsnhda^[ZXVUUVXYYY]^^i£¦¨«¬®¯¯¯¯¯¯¯°¯¬¬«©¨£xgSFA?DIIGFGMWajlnqsvxz{{~
}zxy|}xqaVLED@BDLY`hu
{vrlgc^[XURPRUVY\^_aaj¢¦©««¬®®¯¯°°°¯¯¯«¬«ª§¤ztiXFDJU`hfb[OMS]ilmpsuvxxxz~
~}~~wl^URLFFMW]anvz~|wsoha\WSOKIKOQTX\\^do
¤¨ª««¬®¯¯°°°¯¯¯¯«¬«ª§ynf`\RKRZ^`chlnh]QTailnpstuvvy}
wndYVSOOR`pwx}
|wsqjbYRNJDACBDHMW\`ft¤§©««¬®®¯¯¯°°¯¯¯®¬¬«ª¨¤yga[XWWXXUSNOV^hqn]SXeikmmnrtvy}}vne_ZXVV_iu
}{wsqkbYPHDEILLLJIQ]ly¡¥§¨ªª¬®®®®®¯¯¯¯®««ªª¨
g]\YXYYXPJHCGMSXbmo^Ubgghhhlquyz}~~}}
|tnha\ZZ^ep|~~{xurpkbWOJN]ghgd`YW\p£¥¦¦©ª«¬®®®®®¯¯¯®ª«ªª©y]WWWYZYSKEB@DGLPUbpjY\egebbfkqvvxyyyz}
~wqlhc`^dmu|
~}}}{zywtsqk_TS_ouoe]XUVXYd£¤¥¦¦©«¬®®¯¯¯¯¯¯¬««ªª¨t[RQV[[WQGDCDDCIOT]ko[Sada^^chnsuuuxyyz~~{zyrnkklnry~|{yxvvttsqpj[UestiWKDBCGLS]t¢£¥¦¦¨ª«®°±±°°¯®¬««©©¥saUPZ`\WPDEBCDGMRX]gpZKZ]YX[`ehlqstwyyz{
|xwvut
|tqooquy
}zxusqoonnnoh[btrdRB;;<?BDKUf¢£¥¥¦§¨ª¬®°±±°°¯¬ª©§¦¤yiZOak_XN=ADBCLTRX^hqYDRVVVZ^adhloqtwxyz}
zusrqrsxsppsuw}
~zyzxuqnkgghhijd^jpfUJ@989=ABGN[p¡¤¥¦¥¦¨ª¬¯°±±°®®¬«©¨¦¤£r_OgqaWNHvaCDJMNYkpVBNUUWZ^_aeilnruxz|~{vsqpqrzvtsx||xwwtqmjfaacdec^bnj[L@;;;<>>@EKUg£¥¥¥¦§©«°°°¯®¬««¨¦¦¤¢_KiveYSQeBCGJO]niPCMSVZ\^_`cgkosvy{}
|{xvvvv}zyxz~~zvtqqnje`]]`bc_[isgVH>9;<=<COJIUdv¢¤£¤¤¥§ª¬¯¯¯®¬«ª§¥¥¤¢VIdukZTPNLHCFKOXfj[HBKRWZ[[\]_dimqux{}{zyx
|z|}ytpnnlhc^[Y[^_VPhrcSGA;GLC;I]NJYgi£¤££¤¦ª¬®®®¬«¨¥¥¤¤¤¡}PGXpqaZVNHHFIMR]gaOBCKQUWXYZ[]cilptw{~
}{x
}xsomligc]ZXXZ[PJcn`PGAMW:=AGN]jdx¡¢¢¤¦©««¬¬¬««ª¦¥¤¤£¤£yJBFcrobZSNMMNPW_cWGCGMRTUUXZ\_ekmptx|~zxv
|ytqmlihd]YWVWXNH^k`QJDHfkG:<BJQbk`m ¢££¥©«©ªª©¨¨¦¤££¢¡¡¡vH>=Lbqj\XUUUQR[`ZKAFLPUVVVY\_ehlmqvz~
~{wtr|xsomkhfb\XVUUUKAThdWNHFC;7;BJOVegXa¡£¢¢¡¢¤¥¤£££££¢¡¡ tK==CMV^[ZXVUQRXVKBER^cfecdddehimptx||xuro}wrmjhea^ZXVUVUM?IbkcUMJF>:?INR^g^S^¤¢pQD?MQPOMNOPQUWVTUcs{~~{wsqkfdfinquyywvu
~{vplhe_[YXWXWWWRGCSkpdZQJGDENRWdfXQ] t_]`nwwonoqrsw|
~xtmhhmorty}~||z
{vojgc]YVUVWYZYXQF?OjlcYTSRRVX\`_XU^x{}
yroqtttw{
~{y}|wqlhd_\YWYZ[\ZXTMCANZ^dc[Z[]`gmnkjk ¡¢}}{{~
}zxwz||ytokhecb``aa_^YVTRMLMOUWTU^iu|wz¢¦©ªª§§¨¨©§¦¤¡yuquwz|~|yvpkifeeeinmllmpsssssrqrt{£¦«°²±¯¬¬¬ª§¥£¡~ylortwy{}~~zwtohecdejpsw} £§¬°²±°¯®¬ªª§¦¥££¡ hknpqsvx{~{wqkfefhkow¡¡¡ ¢¤¨¯±±±°¯®¯¬¬ª©§¦¥¥£¡ ¡
begiknqtx{}|xsnkkllns{£¥¥¥¤¢ ¡£¢¡ £¥©¬¯±²²²±°°¯®¬«ª©¨§§¦¤¢ ¡¢¢¢¢¢£££¢¢¢¡¡¡
^`bdfgkoswx{~zvsppqrv{¥©ª««ª©§¦¦¤¡¡¤£¤¢¡¡¢¤¦ª®°±²³²²±°°¯¬«ª©©¨§¥¤¤¤£££¤¤¤¤¥¥¥¤¤¤¤¤£££¢¡|[]_`cehkmorvz~
yusrsv|¥©«®®®¬«ª¨¦£ ¡¤¥¤¤££££¥¦©°²²±°°°°°¯®¬««ª©©¨§¦¦¥¤¥¥¥¥¥¦§¨§¦¦§§§¦¥¥¤£ |Z\^_acdeghkosx{~
|xusu|£©«¯¯¯°°®¬«©¦¤¢ ¡¡
£¦¦¤¤¤¤¤¤¥¦¨ª°°°°°°°°¯¯««ª©ª¨§¦¦¦¦¦¦¦¦§¨©©©©¨©©©©§§¦¤¡
XZ\^___abcehlrvy|~
¦ª«¯¯°±±±¯¬ª§¤¡¡¤¤£¡
~¢¦¤¤¥¥¥¥¥¥¦¨©ª¬¯°°°°±±°¯®««ªª©¨¦¥¥¦§§§¨¨©©ª««««ª©©¨§§¦¤¢XZ[[\\]]^_adinqsuxz~ ¡¡ ¤¨«¬¯°°±±±°®«¨¤¡¡¤¥¤£ ¢¤¥¥¥¥¥¦¨©ª©ª¯¯°°°°°¯««ª©¨©§§¨¨©©ªªªª««««««ª©¨©§§¦¤¢YYYYYYZ[\]^_dimopqtx{} ¡£££¡ ¢¥§©ª«¬¯°°°±°°®«¨¥£¢¢£¤¥¥¥¤
¤¥¥¥¥¦§¨©©©©ª«¬®¯¯¯¯¬ªª©¨©©©©©ª«««¬¬««««««««ªªª©§¦¤¢ ZYXXXXYY[\\]_dikmnqtvy}
¢£¢¢££¢£¢¡¡¡¢¢£¤¥§ªªª«¬¯°°°°¯¯¬ª¨¥¤¤¤¤¥¦¥¤¢
{
¡¤¥¥¥¥¦§¨©©©©¨©ªª««««ª©©©¨©©ªªª«¬®®¬«««««¬«ª«©¨§¦¤£¡YYXXZZZZZ[[\\_dgjlnqux{ ¡¢¤£££¤¥¥¤£¤¤£££¥§¨©ªª««¯°°°°¯«©§¦¦¤¤¥¥¥¤£ts|{ruy{¢¤¥¥¤¤¤¥¦§©©¨§¨¨©©ª©©¨¦¦¦¦§¨ªªª«®¯¯¬¬¬¬¬¬¬¬«ªª©¨§¥¤£¢ YYWXYXYZYZZYZ\`cdgilpux¡¢¤¤¤¥¥¥¦¥¥¥¥¥¦§¨ª«««¬¬¬¯°°¯°¯«©§¦¦¤¤¥¥¥¤¢nlpngob`s¡£¤¥¥¥¤£££¥§¨¨§¨¨¨¨©©©¨¦¥£¢¤¦©©ª«¬¬¬¬¬¬¬¬¬«ªª©¨§¥¤££¡ YYXWVUUXYZ[ZZ[]_`cdhlqw ¡£¤¤¥¥¥¥¦¦¦¥¦¨©©ª««««¬¬¬®¯¯¯®¬¬ª¨¦¥¥¥¥¥¥¤¢ztk`br|iu£¤¦§¦¦¥¥¤¤¤¦§¨¨¨¨©©©©©©§¦¤£¢£¦©©«¬¬¬¬¬¬¬¬¬¬¬¬¬¬«ªª©¨§¥¥££¢ XZZWTRSUWXYZ\\\\]`cglry £¥¥¦¦¥¥¦¦§§§©ª««¬¬¬¬¬®¬¬««ª¨§¥¥¤¤¤¥¦¥¤¢vuy £¤¦©©©©¨¦¦§¨©©©©©©¨¨¨¨¨¨¦¦¥¤£¤§©ª«¬¬¬¬¬¬¬¬¬¬¬«ªªª©¨§¦¥¤£¢VY[WTQRSRQRTW[]]\afkr{¡£¥¥¥§§¦§¨¨©©¨©ª«¬¬¬¬®¯®¬¬¬««ª©©§¦¥¥¤¤¤¤¥¦§¦¥¤££¡¡£¤¦¨ªªªª©¨¨¨©©©©ªª©©©©¨§¦¥¥¥¥¥¥§©ª«¬¬®®®®¬««ªªª©¨¨¥¤¢¡VZ[ZWTTSSSSSUXX[_cksz¡£¤¥¦¦¨¨¨©©©©¨¨©ª««¬¬¬¬¬¬¬¬ªª¨§¦¦¦¦¥¥¤¤¤¥§¨©©©¨¨¦¤ ¤¥§©ªªªª©©¨©©©ªªªªª©©©©¨¦¥¥¥¥¥¥¥¨©ª¬¬®¯¯¯¯¯¯°°¯¬«ªª©¨¨¦£\^^]ZVWXXXXYZZZ\cir{¢¤¤¦§¨¨¨©©©¨¨§¨©©©ª«¬¬«««ª©¨¦¦¦¦¦¦¦¦¥¥¥¦§©ªªªªªª©§¡£¦§©ª««ªª©¨§©©©©©©©©©©¨¨§¦¥¤¥¥¥¥¦¨ªª¬®¯¯¯¯¯¯°¯°®¬¬ª©§¥¢¡ aba^ZVVWVWWXYY[^gov}
£¤¥§¨¨©©©©ªªªªªªªª«««ªª©©¨¨§¨¨¨¨¨§¦¥¥¥¦§¨©«ªª«««¨¥¡¥¨ª«««««ª©©¨©©©©©©©©¨§§¦¦¥¥¥¦¦¦§§©«¬®®¯¯¯¯°¯¯¯¯¬ª©¦¤¡^_^[WRRRSSRQRTX_emt{¢¤¥¦¨©©ª««¬¬¬¬¬¬¬¬«««ª©¨¨¨¨¨¨¨¨¨¨§¦¦¥¦¦§¨©ªªª«««ª§¢£¥§ª«««¬«««ªª©©©©¨¨©¨§¦¦¦¥¥¥¥¦¦¦¦¦¨«¬¯®¯¯¯¯°¯¬©¦¤¡
]^^[XUUUTSSSUX]admv¡£¥§¨©ª«««¬¬¬¬¬¬¬¬ªªª©©¨¨¨¨©©©©©©¨§¦¦¦¦¨©©©©©ªª©¨§£¡£¥©ªª¬¬«««ª©©¨¨§§§¦¥¥¥¥¦¥¥¦¦§¦¦¦©ª«¯¯¯¯¯¯¯«ª¨¥£
}}~bcb_\[[ZXVUUVY^afoy £¤¦©ª«¬¬¬¬¬¬¬¬¬¬¬ªª©©¨¨©©©ªªªª©©©©¨¨¨§§¨¨¨¨§§§¦¦¥¤¢¥¦§©©©ª©©©¨§§¦¦¦¦¥¦¦§§¨§¨¨§¦¦¦¨ª«®®¯°°¯¬«ª©¨¥¡
~iihfddca^[YWX]cfnw ¢¤¦©ª«¬¬¬¬¬¬¬¬¬¬¬«ª©©¨¨¨©©ªªªªªªªª©©©§¦§¦§§¥¥¥¥¥¤¢~ ¡£¤¥¥¦§¨©¨¨¨§§¦¥¥¥¦§¨©©©©©ªª¨§§¨ª¬®¯¯¯®¯«ª¨§¥¥£oonmkkjhda^]`gns} £¤§©ª«¬¬¬¬¬¬¬ª©©¨¨¨©ªªªªªªªªª©©©§¦¦¦¥¥¤¤¤¤£¡}y
¢£¤¥¥¦¦§§§¦¦¥¥¥¥¥¦§©ªªªªª««ª©¨©«¬¯¯®«ª©¥£ sssrrrqnkgffkrx~ £¤¨¨©ª«¬¬¬¬¬¬«ª©©©¨©ªªªªªªªªª©©©§§§¦¤£££¢¢ {x¢¤¥¤¥¥¦¦¦¥¤¤¤¥¥¥§§©ªªªªª¬¬¬««¬®®®««ªª¨¦¤¡wwvvuuutronry~ ¤¥¨¨©ª««¬¬¬®®®®®¬ªª©ª©ªªªªªªªªªªª©¨§¦¦¥¥¤¤¢
¢£¢£¥¥¥¥¤¤¥¥¥¥¦§©©©©©ª«¬¬¬¬®¯¬«©§¦¤£¢ {{zyyyyyxxy} ¤¥¨¨©ª««¬®¯¯°°°°¯®¬«ªªªªªªªªªªªªªª©§¦¥¤¤££¡
£¤¤¤¥¤¥¥¦¦§©ªªªªª¬¬¯¯¯®®®¬ª©¦¥¤¢¡ }}}{{{{|
¤¥¨©ª«¬¬¯°°±²²²²±°®¬««««ªªªªªªªª©©§¦¥£¢ }{xttz¡£££££¤¥¦§§©ªªªª¬®®¯¯°¯¯®®¬«§¦¥£¡ |{}
¤¥§©««¬®¯°±±²²²³²²±°®¬««ª©©©©©©¨§§¦¥¤¢ }xw ¡¢¢£¤¥¦¨¨©ªª©«¬®¯¯°¯¯°¯¯¯¯¯®«¨¦¥£¢~{~
¢¥§©«¬®¯°±±²²³³³³³³±±°¯¬«©ª©©©¨©¨§¦¦¥£ ¡£¤¥¥§¨©ªª©ª¬®¯¯¯¯¯¯¯®®¯¯®«¨¦¤£¡
¢¤§©«®¯¯±±°²²³³³³µ´´´³±¯®¬«ª©©©¨§¦¦¥¥£ ¡ ¡¢¢¢£¥¦¦¨©©ªªªª¬®¯¯¯®¬««¬¬«ª¦¤¢
~|zy|
¢¥§©¬®¯°±°°²³³´´´µµµµ³²²±°¬«ª©¨§§¦§¥£¢¢¢¢££££££¢¢ ¡¡££¤£¢ ¢£¤¦§¨©©ªªªª«¬¬®¬¬««©ªª¨§¥¢¡~zyyy} ¢¤§©¬¯¯±±±²³µµµµµµµµ³³³²°¯®¬«©¨¨¨¨¨§¥¥¦¥¥¥¥¥¤££¤££¡¡¡£¤¤¤¤¥¥¦¥¢ ¡¢¤¤¥¦¨©©©ªª«««¬¬¬«««ªªªª©¨§¤¡ {yyxxz~
¢¤§ª¬¯°²²²²³µµµµµµµµ³³³²±¯®¬«ª©¨¨©ª©¨¨¨§§§¦¦¥¤£¤¥¥¤£¤¥¦¥¥¥¦§§¦¤¢ ¡¢£¥¦¦§©©©ªªªª««¬«ªªªª©ªª¨§¦¤¢~{ywwy| ¢¤§ª¬¯°±²²²³´³³³³³³³³³³²±¯®¬«ªª©ªªª©©©©¨§§§¦¦¥¥¥¦§¦¦¦§§¦¦¥¦¨©©§¤£¡¡£¥¦¦§©ªªªªªªª««««ª©¨©¨§§¥¢¡¡ ~zyz|~
£¤§ª¬¯¯±²²²²²²²²±±±±³³²²±°®«ªªªª©©©ª©¨¨¨¨¨¨¨§§¨¨©©©©¨©¨§¦¦¨©ª¨¤£¢£¥¦§¨ªª««««ªªªªªª©§¦¦¥¥£¢ {{
¢¤§ª«¬¯¯°±²²²²²²²±±±±²²±°°¯¯«ªªªª©©©©©©©©¨¨¨¨¨¨¨¨¨¨©ª©ª©¨¦¥¦¨©§£££¥¦§©©ª«¬«¬««ª¨¤¡
¢¤¦©««¯°°±±±²²²²²²²±°°°°¯¯¯«««ªªªªª©©©©©©¨¨¨¨¨¨¨¨¨©©ªª©§¥¤¤¥¥¤££¤¥§¨©©ª«««¬¬¬¬¨¡¡£¤§ª«®¯°±±±±±²²±±±±°°°°¯¯¬¬«ªª©©ªªªªªªªª©¨¨¨¨¨¨¨¨¨©ª©§¦¤£££¤£££¤¥§¨¨©ªªª«¬¬¨¡ £¤¦©«¯°±±±±±±°±°°±±°¯¯¯¯®¬«ªªªª©©ªªª«««ªªª©©©©©¨©©©©©©§¦¤££££¢££¤§©¨¨©ªªª««¬«§ ~ £¤¦©«¯¯®®®®¯¯®¯¯¯¯¯¯®¬ªª©©©©©©ª««¬¬¬¬««ªªªªª©©©©©©¨¦¥¤¥¦¦¥¤¤¥§¨©©¨©ªªª«¬¬«¦¡¢¤¦©«««¬¬¬¬¬¬¬¬¬¬««««©¨§¨©©©©ª«¬¬«««««ª©©¨¨§§¥¤¥§¨¨¨§§§¨©©¨¨ªªªª««¬¬ª¤ £¦©«¬¬«ªªª««©ª©ª««¬ª©¨¨¨§¦¤¥¥¦§©©©ª«¬¬¬¬¬«««ªª©©©©¨¦¦¦§©©ªª©©©©ª¨§¨ªªªªªªª«©¢
£¥¨ªªªª©©©©ª©©¨¨§§§¦¥¤¤¤£¢¡¡ ¡¡¡¡¢¥§¨©ª««ªªª¨©©¨©©ªª©ª«¬¬¬¬«««¬¬ª¨¨©©ªªªªª¨§£
£¥§¨©¨©§§§§¨§§¥¤££££¤££££¢¡ ¢¥§ªªª©¨¥¥¥§¨¨ª«¬¬®®¯¬¬¬®¬«©©©©¨©ª©¨¦¥¢
£¥¦§¦¦¦¦¦¦¦¤££¢££££££££££¢¢¢¡¢£¦¨¨©¨¦£¢£¤§ª¬¯®®®««°«ª«ª§§¨¨¨§¥£
¡¢££¢ ¢¤¤¥¥¦¦¦¥¥¥¤¤££££¤¤¤¤¤¤£££¢¢¢¢¡¡¡¡¡¢¡££¤¥¦§¨¦¤££¥ª«¬¬¬¬¬¬¬«ªª«®°ª¬¬ª¦¤¥¥¤¡
¡¡¡¢£££¤¥¥¦¦¥¤¢ ¡¢£¤¤¤¥¥¥¥¥¥¥¥¥¥¤¤¤¤¤¤¤¤¤¤¤¤££££¢¢¢¢£££££¤¥¥§¨§¦¤¤¥©¬¬ªªªª©¨¨§¨©®¨¨ª««¦£¡
¢¥¥¥¥¥¦¦¥¥¥¦¦¦¦¥¤£¢¢¡¡ ¡¢¢£¤¤¤¤¤¤¥¥¥¥¥¥¥¥¤¤¤¤¤¤¤¤¤¤¥¤¤£££¢¢¢¢¢£££¤¥¥§©§¦¦¥¤¥ª¬¬«§¦§§¦¥¥¤¤¨«¬¨¤¤©¨¦£¡
¢¥§¦¦¦¦¦¥¥¥¥¥¥¥¥¥¦¥¤¤¤££¢¢¢¡¡¡¢£££¤¤¤¤¥¥¥¥£¢£££¤¤¤¤¥¤¥¥¥¥¤¤¤¤££££££££¢£¤¥¥¦¦¦¥¤££¥¨ª©¥¤¤¤££¡ ¡¤¥¨¨¡¢£¡
¢£¥¦¦¦¦¦¦¥¥¥¥¥¥¥¥¥¥¥¥¥¥¤¤¤£¢¢¡¡¡¢¢£¤¤¤¤¤¤¤£¢¢¢¢££¤¤¥¥¤¥¥¥¤¥¤¤¤¤¤¤¤¤¤££¡ ¢¥¥¥¥¥¤¢ £¦¦£¢¡ ¡ ¢
¡¢£¥¦¥¥¦¦¥¦¦¦¦¦¦¦¦¦§¦¦¦¦¦¥¥£¢¢¢¢£££¤¤¤¤¤¥¤¤¤£¢¢££¤¤¤¦¥¦¦¦¦¦¥¥¤¤¤¤¤¤¤¤¢¡¥¦¦¦¥£ £¢ ¤¢ ¢¤¥¤¤¥¥¦§¦¦§§§§§¨©©ª©ª©©§§¥¤£¤¤¥¥¥¦¦¦¦¥¥¥¤¤££¤¤¤¤¤¦¦¦¦¦¦¦¦¦¥¥¤¤¤¤¤£¡¢¥¦¦¦¤¢ ¡¢¡¡¢¤¤¤¥¥¦¨§¨¨¨¨©©©ªªªª«¬¬ª©©¨§§§©¨¨§¦¦¦¥¤¤¤¤£¤¤¤¤¤¤¦¦¦¦¦¦¦¦¦¦¥¦¥¥¥¤£¢¤¦§§¦¥¤¢ ¡£¤¤¥¥¦§§¨¨¨©©©©©©©©©¨©©©©©©©©©©©§¦¦¥¤££££££¤¤¥¥¥¦¦¦§§¨¨¨§¦¦¦¦¥¥¤£¡¢¤¤¥§©§¦¥£ ££¤¤¥¦§§§§©©©ªªªª©¨©¨§¨©©©©©©©¨©§¥¥¥¤¢¢¢£££¤¥¥¦¦§§§¨©©©©§¦¦¦¥¥¥¤¤££¤¥¥¥§¨¦¦¥¢~~~~~~~}~}~~~~~~~}}}|~}|{{|~~~~~~|}}~~~|zyy|}~~}}}}}~}~~}|ywyy{}~}}{{{||}}}}~}zxwyz|}~~}}|{zz{|||||}~~~~~}zxwyz|~}||{zzz{||{|}}}}}}~~~~~}zyyzz{~~}}||zzyy{|||||{{{{||}}~}~~~}|zz{z{||~}}|{zxxxz{{{||||{||||}}}}}}}}~~~~|{z{z{}}~~||{{yxxyzzzz{{{{{{{{||||||||}~~~~|zzzz{}~~|{{{zyyyzzzzz{{zz{{{zzz{||||~~~~|zzzz{}~}|||{yzzzzzzzzzzzzz|{zzz{|}}~~}}
~|{zzz|}~~~|{zzzzzzzzzz{{zz{||{{{{||}~}}~|z{{{||~~~~{zzzzzzzzzzzzzz{||{{{{|}}~}}~
|zz||{{}~~~}{zzzzzz{zzzz{{|||||||}~~~~
{yyzzy|~~~~~~|zyzzy{{z{{{{{|||||||~{xwxyx{~~~~|{yyzzz|{zz{{{{||||}}}~~zxxxyx{}~~}}|{zyzzz{|{{{|{|||}}~~~~~~zxwxyxz|}~~||zzzyzz{{{{{{||{{}}}}}~~~~~}{yxyxy{||~~~~~|{yzzzzz{{|}~}||zz{{|}|}}}~~~~|{zyxwx{||~~~~~~~}{{zzzzz{|~~||zzz{||}}}}}~~~~~~~~}|zxxxx{|~~~~~}||{zzzzz{}~||zyzz||}}|}}}~~~~}}}}{zyyxxz{}~~~~|||{{zzzzzz|~}|zyzyyz{{z{|}}~~}}}|}~|{zzyxwxy|}}~}|}}|{zzyzzz|~~|zzzyxyzzzz{|}}~}|||}~~~~~~|zzzyxwwxz{||~}||}||{zz{{|}}zzzywxyyxyz{||}|||||}}}}~}|{zzyyyxwxxz{|~}||||||{z{|}~~{zzywxzyyyz{||||||||}}}||||{zzzyyxxxxyz{~}}||||||||}~~~|||{z{{{{||||||||}||}}||||{{{{{zzyyxxyyz}}|{||||||}~~||||||||||||||||||{{||{{z{{|{{{{{zyyxyyz~}|{z|}~~~~~}|||||{||||||||{{|{z{{{zz{|||||||{zzzzzz~||{{|~~|}}}|}}}||||}}{{|{zzz{zz{{||||||{{z{z{z~}||||~~~~~}}}}}}}}}}}||{zzzyzzzzy{zz{|||||||{{{zz~}}~~~~~~~~}}}~}}}~}}{{zzzyyyzzzzzz{{{|}}}||||{{{~}~~~~}}}~~~~}|||{yyyyzzzz{{|}}}~}}}||||~~~}}|{||}~}}}}~~|}|{zyxxyzz{{z{|}~~~}}||{{|~}~~|{{z{{|}}}~~~}~~}}|{yxwwyzz{z||}~~~}}{zzz|~~~~~~~||{{{||}}}~}~~}~||{xwwwyzz{{||||}}}}{zzz{~}}}}}~~}||{{|||||~
~}}}~~}}|{{xwwwyz{{{{{|||}}|{zzzz~}}~~~~~}||{{{{{{}~
}}}}~}|{zxwwxyzzzzzz{|||{|{{{zz~~~~~~~}|||{zzz{|~
~~}}}{zxxyyyzzz{|{zzzzzz{z{z{~~~~~}|||z{z{z|}
~~~}}{yyyyyyzz{{{|{z{z{{zz{zz~~~~~}|||zzzz{{|~~~~~~~}|zyyyyzzz{{{{{z|{z{{{{{z|}~~~~}}|zyz{|}}~}|}~~~~~~~~}{zzzzzzz||{{z{{{{z{{{{z{|}~~~~~~}|{{z{||}~~}||}~~~~~~~~~}}|||{{{{{{{{{zz{{{z{{{zzz{}}}}~~}|{{z{|}~~}}}}}}~~~~~~}}}||{{{{{|{{{{z{{{{zzz{||}}~~~}|||||}~|}}|}}~~~~~~~~~}}}}||||}||||{{{||{{zzzz||}~}~~}~}}~~}|{{|}~~~~~~~~~~~~}}}}||{||{{|||||zzyz{{|}||~}}{zy{}}~~~~}}|||||{||}}}zzzyz{||z{|~}{zzz}}~~~~~~~~~}}|||||}~~~~zzzzzz|{{{|~~}{{{{|}~}}~~~~~~~~}}}||||}~~~}zzzzzzzz{|}~}}|||}~~~~}~}|~~~~~~~~~}|||||{{||}}|zzzyzzzz||}}|}}}~}}}|~}~~}}}|}}}}||{{{{{{||zzyyzzz{||~~~~~~~}||~~~}}|}}}}}|{{{{{{{{zzzzzzz{|}~~~~~~~~~~}|{|}}}|{{{{{z{{{zyyzzzz{}~~~~}|{{{{|{{zzzzzzzz{yzyyzzz|~~~~~~~~~}}|{{{{{zz{{{{zzzz{{{{{zzz|~~~~~~~~~~}}|{{{{{{zz{{{zzzz{|||||{z|~~}}}}}|{|}||{{{zzzzzz||}}|{{|}~}~}}~}}}}}|{}|||{{{zzzzz{|}}}}||||}}~~~~~~~~}}}}}|{||}|{{|{||{{{}}~}}||||}}~~~~~~~~~}}}}||||}||||||||{|}~}||{|||}~~~~~}~~|{|{{z{{{{{{zzy{~~}|||||}~~~~~}}}}}}}}}}}{{zzzzz{{zzzyyy{~~|}~}~~~~}~~~~}}}}}}|||}|{{{z{{{zzzzyzz{~~~~~~~~~~~~~~~~~~}}}}}|{|}}|{z{z{{{{{{yyyzz~~~~~~~~}||}}}}}}}~~~~~~~~~~~}}}}}}||}||{zzz{{{{{{zz{zz~~~~~~}~~~~}}}}}}||}}}}}}}}||}}}}}~~~~~~~~}}}}}}}|}}|zzz{{{{{{zzzzzz~~~}|}}}~~~}|}}|||||||||||||||||||}}~~}}}~~}|||}||||||{zz{z{zz{zzzz{zz~~~}}}}}}~~}}|||}}}}}}}||||||||}|||}~~~~~}||}~~}}}}}}}||||{{{{{{{{{{z{{{zz~}}~~}}||}}}}||||}}}}}}}}|||||}}}}}}}||}~~~}}}}}|||||}}|||||{||||{{{|~~}}~~}}}}~}||||{|}}}}}}}|||||{{{|{{||}}}~~}}}}}}|{{{{{{|||||{{{{{z{{{~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}}}}}}}}}}}}}}}}}}}}~~~~~~~~~}}}}}}}}}}}}}}}}~~~~~~~~}}}}}}}}}}}}}}}}}}}}}}~~~~~~}}~~~~~~~~~~}}}}}}}~~~~~~~~~~~~~~~~~~~}}}}}}~}}~~~~~~~~}~~~~~~~~~~~~~~~~~~~~}}}~}}~~~~~~~
~~~}~}}~~~~
~~}}}}~~~~~
~~~~~~~
~~
~
~~
~
~
~
~
~~
~}~
~}}~
~~~~
~~~}~~
~~~~~~
~~~~~
~
~
~
~~
~
~
~~~
~
~~~
~
~
~
~
~~~~~~}}~~
~~~~~~}|||}
~~~~~~~}|||}
~}}}~~~~~~~~}|||~
~|{|}}}}~~~~~~~~~~~~
~}||}}}}~~~~~~~~~~
~}}~}}}}~~~~~~~
}}}}}}}~~~~~~~~~~
}}}}}}}~~~~~~~~~~~~
~~}}}}}}~~~~~~~~~~~~
~}}}}}~~~~~~~~~~}}~
}}}}}~~~}}}}}||~~
~~}}}}}}~~|||||||~~
~~}}}~~~~||||}}}~~~
~~}}~~~~~}}}}}~}~~}}}}}
~~~~~~~~~~~~~~
~~~~~~
~~
~~~~
}z|¡ ~ukgju
¡ mZPORYiz¡¡lVGCBFKUl¡¡rS@<>AEHKVq
¡¡ y]E<=ELRX^cm{~ ¡¡kI;<AJYemrty
~
¡¡¡
^E;>HR\gpuwxy~~~~~~
¡¡ ¡¢
sQ?;<HW^dkpsuux
~}|}}|}¡¢¡ ¡¡¡¡¡ ¡¢¢¡¡¢¢
mG9<=CS^beknoprv}
|zzzzzz{~~¡¡¡¡¡¡¡¡¡¢¢¡ ¢¢¢
cC9<@FR]acgnpopsy}
{xxyxxwy|~}¡¡¡¡¡¡¡¡ ¡¢¢¡ ~}}||||}||~b@5:BMS[chlotvuuvy}
zxxxwvwx{~~¡¡¡ ¡¡¡ ~||{zzyyzzz{}wY>6;BNZ]binruvxvuuw{zxxxwwwx{~
¡¡¡¡¡¡¡ ¡¡ |ow
~~|{zyxxwwwwxyz|~
rN74:@JW`ejnqttuuutssyxuvuuwwxz}¡¡¡¡¡¡¡ ¡¡ |dZh
~||{yxvutsttstuvwz|}~
oO517AGS_dhloqsttttrppwwqsssuvxz}¡¡¡¡¡¡¡ ¢¡p^W^z~|{zzwttsrsrrrsstuwyz{{}~
xkN526=IT_ecgkmoqrsrqqpot}
wqsrrtuwz}¡¡¡¡¡¡¡ ¢¢{mYOSn
~zyyyvsrrrrqpqqrrsuwxxyz{|}
s[B2/6>ITX\_afjlnprqqqrqqs|
vqrrsuvwz}¡¡¡¡¡¡¡ ¢¡
}teSILd~}zyyxtqpppponoppqrsuuuuwxyz|~~~~|sY@103;DOVWZ`fkoppqsttuuuux
{uttuuvwz|¡¡¡¡¡¡¡ ¢¡
|ym\NFIb|zyxwspnmmmmlmnnnoqrsrstuvwxz{{{||}}~~~~{pU;0059>FOTV\fmqtvuvwwxwvvvw~}yxwwxxz}¡¡¡¡¡¡¡¡¡{ysiYMIKTh}{yyxurnkkkkkllllmmnopppqrrstuwxwxyzyz{{{{{{{{{{{{|||ufU@303:AEJSXZ_ehkmoqsssrrrrrszzxxxxxx{~ ¡¡¡ ¢¡
|ztmdWOIDJa}zyxxwuspmkkjjjklllmmmnnnooopprsttuvvwwwwwwwwwwxwwuuvraA3356>EINSY\]`cdfhjmnoopopqqry
wrsuuwxy}
¡¡¡ ¡¡~zung\UNF@CZw
~zwvvvwvtqnljiiiklllmnnoonnnnnnnppqrsttsstsrrrrstsrokbUA0158:BIOTX]]]`bbcfhkmmmmnorrrwuopqrtwy}
¡¡¡ ¡¡
~{upjaZTJC?AM^n{}y~|yussvxxvrnlkgfgjlkklmmnnmmmmmmmmlnqqqrqpppnmmnooojcYI:1158?CINSV\^^^_acfhjlnnnooqtuswupoprsvy}
¡¡¡ ¡¡~zvqlc^WOHC=;?KX\[_w|xurnnrvxwsomljhikllmmnnonmnnnnnnlkmpqqpmjhghfggecb\M@2-06;@HLPTW[^`aacfjmorrqqqrtxzzw{
wsrrsuwy}¡¡¡¡¢~{wsnf`[TNJD>:8=DEG`uuppssrolhdchpvwtomllnqstvxxxxxxwwwwwwwsnoqrsqkb][ZYXWURRI92-,0:BHLRVXZ^acegiloruvvttuvy}}y}
xtsstvwy}¡¡¡¡¢
{wrpid_WSOJC<98::;CUYTVY[[\]]XX_iswvrqrty
{tuuvxvn`XTPPROLIE<2/,-7DKNQVZ]`cdfikmortvwwvuvw{z~
wrstuvxy}
¡¡¡ ¡¡~
|xtqmhb\XUOHA;87778=BDFHKLMOPMLRarxy{|~
{z{{|ztg_ZMIMLEA=81./4COQUY\_bdeghjlnqstvutqqrty}~z|rnortvwy}
¡¡¡ |}yusokea^ZUME>;865468:=?DGGGGFGL_u{}sohYNJHB=;9426?JRW]bccbceggjmprsssqomlnrw{}}{woklpruvy}¡¡¡¡¡¡¡¡¡¡¡¡ ¡ }
}ywtqlieb^ZSMD?<8522368:?DEDDDHP_s}
|zvfXMJD:;878@HOV\beeb_chkjkorrqqonnllnrx|~~|tmlloqtuy|¡¡¡¡¡¡¡¡¡¡¡ ¡¢¡
{ywrnkfc`\YTLE@;73002349>AGLOU`kx~zqcXSL>=:=BIPT\eihfb`enpmlnonmljjlklosy}~~}olmmnptux{
¡¡¡¡¡¡¡¡¡¡¡ ¡¢¡ }zvqmjgd_[YWRJC>840../159ES\djqx~
~{rkdWHFGGJQUVbpqjfefmqnkhhffhjjjklmosz}~}|}nnonmpstvz}¡¡¡¡¡¡¡¡¡¡¡ ¡¢¡
}yuqmkhd^ZWVUPIC;62/../6BVdnwz}
~yp`VTUSUXX]lwtnjgiqrjfeddehllmmnoqu{~}{{{rrponpssvy|¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡
|yvromhd^ZXXVTQKA:62313?Ugq{
wkc_^^acchvyurmlqwqfadhkmoppppoosuy{zwy|ttpmnqssux{~¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡ ¡
~|yxtqmjfda]ZWVSJA=;<;@Qcq{}~~smmmliklr}ytsopxwmhfjpuwvtqpnllptvvusy~wtpmmqrrtwz}
¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡ }~{zwsqpookc]ZXVSKHIKMUbnx}}
xwyzztrty~wtsotyurllsz~}uljhfimortutt}wsommprrsvz}
¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡ ~~~{ywtrqopojc^[YZTOR\gkov~}~{ups|ytqnuykeedemstvwxy}xqnmmoqqsuy|
¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡
}}}{yvsqonnnmid_[`aXXgsx}
}}~
}ts}xrqw
|niihksy|~vnloqqqprux{~
¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡~|{~~|zyxuqmjijkkje]_ggdmx
~||~zv
xuztopsv}
~sorvwurpquyz|
¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡}zz|}~}}|}}}{wtokjlnomechqw|
}|~~{{vwzzz~{}
yux}~xtrprtyz{~
¢¢¢¡¡¡¡¢¢¢¡¡¡¡¡ |z{||||}|vomoqrqqqy~}~
~|}|~
ywz||||~wsqpqswzz|
¢¢¢¡¡¡¡¢¢¢¡¡¡¡¡}z{|||~
}xvxxz|}~{yy{~~¡¡
}{zz|~
{xyz}
{}xuqooqsvy{|~¢¢¢¡¡¡¡¢¢¢¡¡¡¡¡~{{{||
}||}{xvvxyzz|~
¤¦¤¡{zxy{~
}yz|}}wy
zqnnoqrvy{|¢¢¢¡¡¡¡¢¢¢¡¡¡¡¡}||}~
}{yyyxtrqqttuyy¢¥§¦£ {yxyz}
~~vt|smmlnnprvyz~¡¡¡¡¡¡¡¡¢¢¡¡¡¡¡|ywuvurpoorqquwz¡¤¦§§¥¡}zyyyz|
{qs}slkklmnortwy|
¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡
}zwtsrrqonnooprqx¢¦§§¨§£ |yyyy{|
}tvwmkjjjjkmpsuvxz}
¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡
~{yvrqpoonmmonoqmv£¦§¦¨¦£ }{ywwz{~~
ysw
rollkkjkmortsuwy|
¡¡¡¡¡¡¡¡¡¡¡¡ }zwtrqoomkjjllnqkv£¥§§©§¤¡{wuswy{|}~}}}}~~
wtx
~ztrpnnnnnoqqrsvy|
¡¡¡¡¡¡¡¡¢¢¡¡
{xvsqooonlhggfjsly¤¦¦¨©§¥£
zuqnsxyz{|}{zyzzz|
zwy}ztpnmmmmnoprsux|¡¡¡¡¡¡¡¡ ¡
|ywurpnoonmifcbhsn¥¦§©©§¦¥¡xsniotvxy{{ywvvvvw{
~yxx{xpnmmmmmmorstuy|¡¡¡¡¡¡¡
~|zxuspnmnoomie``hst¡¥§¨©©§¦¦£toiciorswxwvutssstvz
|ywx|uryytrrsrqoprssux{}¡¡¡¡¡¡
}{zywtqnmlmmmlje`an}¤¥¨ª©ª©¨§¥¡wnf]`gjnsuutsrqqqrtwz~
|ww|xhgoy}yxywtrqqstvxyz}¡¡¡¡¡¡
|zywwuspmkkjjiiifbj|¡¥§©«««ªª¨¦¢|pbW[abgpssrqonmnortwz
|wx}zkdkt{
~}yvtttwxxxyz
~|yxwtqqokhgghhhhiigev¡£¥©«¬¬¬«ª©¨¤s[QY]\`inonmljjjlnqtw{
}xyyqgfmu{
~zxxxxxxxxy}{xxxzz~
{yusqpnljea_]acefggijp¤¦¨¨«¬«©©¦¢tVMVZ\`gkkjjihhijmoqtx}
|ywqkjpw{
}{ywwwxxyyyyxvssuvw{~~yvtqnmljie^YWWYacdegnu¥ª¬¬¬¬«ª§¤¡zWHQV[aghhhhgfgijlmnqtz
|zwvvvy}
~|zxwwwxxyzyzqhhkpstwyz
}zyuromjiiihdZTRRT[`egiy¤ª¬®®¯¯®¬«ª©¦£YHNS\dgfeddddfhiklmosx|
~|{{zyxyzyz
wcTU]ejmptx}|wssqmjgedeghbXRONPV_gho¡¨¬¯¯®¯°°¯®¬««¨¤YKRW^ddca``_`bdghiklou{
}{zzxy
|iUILPV^dinu|
~ysnlkgdb``acdaYQNMPV`iny¦«®°°°°°°°¯®¬¬©¥¢dT[`baaa`^[Y\`bcefhimrw|
}|{|s`RKJLOU^env
{wqlgeb_^^_``b`YROORX^jv
£©¬¯°±±°°°¯¯¯®«ª§£{jnplb_`]XTSUZ^`acfgkpty}
~raLCFJMT^gqz}wqjda^\\\]]_`^YTRSVY^m|¡¦ª°±±±°°¯°°¯¬«¨¤ ~}|qd^\VQMKLSY[\aehkpsx{~
}|{{|
~zk\K>@EEISht}
}vrlea^\[ZYZ[[][WUVYZ`p£¤¨«¯°°°°°¯¯¯¯®®®¬¬ª¨¥ qcYTNIHHIOTVZ`fimqswy|~zxwtv}}seXNE@@@CEQgx~xsngca^[YYXWWYYWX[]]cv¤§ª¬¯°°°°°¯¯¯¯¯¯¯¬ª¨¥ o\NHCEILMORSW`eimqtvx{}~
}{ywy|~zn`RJFB@CGU^do}
{vqjfca][XUSSTUW\_aafy¢¦©«¬°±°°°¯¯°°°°°¯¬«©§}sbODFMX[ZYZYWUYcknostvy{{{
~|z|~
}xjZSOGDFLY]hsy}}xtnjf`[WRNMNRUX\^_ck|£§ª«¬¯°°°°¯¯°°°¯¯¯¬«©¥{rjaWJLV]`abccc`[V^kmortvwxxy}
~wk]VSPLMTdsx|
}xtoke]VOJGJNPQTY\_fp£§ª«¬¯°°°°¯®°°°¯¯¯¬«¨rh`\YXUWYVSSW_cgig]Zdjmoqsuvvx|¡¡
~vndZXVSX_jy
}zvsojaYOGDGMQQONR\dly¥¨ª««¬¯°°°°¯®¯¯¯¯¯®¬ª©¥vc`][Z\ZVOKIILT\cjmfX[eikkmpsuxz}
}tnf`[ZZ^ht~
}zwtqmg]TLHN\b`^\VTZh| ¥¨ª«¬¬®¯¯®®®®¯¯¯¯®ª©©¥g][\\\\ZQJEEFHMRW`ln`W^fgffhlquxz|}}}~
~vpkfa]^fow~~~|{xutqmg[RSbptme^\YXZc}¢¥¦¨ª««®®¯¯¯¯¯¯¯¯®¬ª¨¨£
`USR\^\WMEBCEFGLRWepjUVcdbacflrvvwyyz|xrlihilu{
}|zyxwutsqnfVWhtsfVMIILQU]q¢¤¥§ª««®®¯°±±°°°¯¬«§¦bXPL_b[UKDCCDCEKRW`mnRL[^]\_diorttvxyz|}{{{{tonorv{~{yvtrpppoomcYjwqaL@<>BDFKVdy¢£¥¦¨©ª«¯°°±±°°°®¬¬«¨¦
l_OOgdZSIBQTCEMQU[ajmNESWWX]aehlprtxyzz}|xvusrrxsopruz~{{{xurokiijjkkaasscRF<:<@CCGNYj¤¥¦§¨ª«®¯°°±°°°°®¬«ª¦¥¡xfQTni]TJQ|sLDNRSW`llJBPUVX\_bchlnruwyz|ztrppprzutsx|{xwwtqnkfbceefbZesjVI?;:<?AADKSay¢¤¥§§©ª¯°°°°¯¯®««¨¦¤ nMTsoaVNpnHAEIKO_pgFBOTWZ]_`bejlptwz|~{wurrsv}zyxyzwtrqojf`]_abc^WmsdQC<<@A>?EFIR`m¤¤¥¦¦¨«¯°¯¯¯®¬«©¦¥£¡jGTpsfYQegLFFHIMVgn[ABMTY[]^]^bgloswz~~~}zwxyx
}{z|~ytpnnkga\Z[]_]PToo^NFAB[W>ANNJSchr¡¤¥¦¥¦©«®®®®¬¬«©¦¤¤¤£eERgle]WNEGJIKKPanhPAEMTXZ[\[]`fkosw{
}|zx
~yspmkiga[YXY[XIQkkYMGCcj==CILWghg¢¤¥¤¥¨«¬¬¬«ªª¨¦¤£££¥^DGXhda^WPMMLMNWggP>DKPTWXYZ[^bglnrx{
|yw
}ytqnljga[XVWXUGMeiZOJJhvP;=CKMZje_¢£¤£¤¨««ªª©¨§¦¤¢¢¢¡ ¢\BBKbgc_[VSTUUW^cW@=HNSWYY[]_cgjnoty}
|xvs}xsnmkhf_ZWUUURCF^i`UMHKH=;BHMR`i^Ux ¦¤¢¡¡¤§¦¤£¢¡ ¡ ]CBEUdb][YYYYYYZVIFPZbhjjihgffikorvz~
}zurp
}wqlkhd`\YXVUVRFDWih^RLHE@?FKNXggVSt¥¡^L@GV`_YY[[YXY\bb^iv~~{wtqkeehlpsx||wtrp}vplhd_[YXWXXXVMIN`nj^VNLKJNQTaldSSp i__jtwrlmprux|~xtmilpqsw{
zyxw{tnjfb\XUVWYZZYUOHM[ga]WSRRUX]cifZYnz~ytrtttuy~~|z~
|vqmhe`\Z[]]]\ZWRKHO[_``\XZ]bkt|{pmv¡¢¡¡¢£¤£¢~~
}zxy{~~zvqligeddggfea^\ZXW[_abacfkv
{}¢¦©ª«¨¦§¨¨¨¦¤£¡
}yvrtxz{}~zxsnjgfglssrsw{}
£§¬±²²¯¬¬¬«©¦¤£¢
zvlnstvxz}|yvrkfehmruz ¡£§±²±°°¯®¬ª¨§¥¤£ ¡¡¡
{gjnqsuwz|~
|ytmhgjnqw¢¤£¢¡¡¢¤©¯±±±°¯¯®¬¬ª©¨¥£¡ ¡¢¡¡¡ cfikmptvxy}
zuplmnqt{¢§¨§§¦¥£¢¢¢¡££¢¡ ¡£¦©°²³³²±±°¯¯¯¯¯¬«ª©¦¤¢¡¡¡¢££££¤¤¤¤¢¡¡¡¡ {^adfhjmpruz}
|xtrrtw¢§ª««ª©¨©§¤ ¡£¤¤£¢£££¤¦ª®±²³³²²±±±±±°°®¬ª©§¥¤¤¤¤¤¤¤¥¥§§§§¦¤¤¤£¢¢¡ ~wZ[_bdehjmqtwz}
{vssw}¡¦ª¬®¯¯°°¯®®¬«¦ ¡£¦¥¤¤£¤££¥§©¬¯²²±°°°°°±±°°®¬ª©¨§§¦¥¥¥¥¦¦§¨©ª©¨§§§§¦¥¤£¢|XZ]_`bdfhkmpsw{~~||¤©«¬¯°°±±±°¯®¬«¦¢ £¢¡
¡¥¦¥¤¥¥¤¤¤¥¦©ª±°°°°°°°±±°¯«ª©¨§¦¦¦¦¦§§¨¨©©ª«ªªªªª©¨¦¤¢
VY[\^_`acfghlqvz}
¡¡¡ ¥ª¬°±±²²²±°®«©¥¢ £¤££
£¤¤¥¥¥¥¤¥¦§©«¬®°°°°±±±±±°¯¬ªª©¨¦¦¦¦¨¨¨¨©ªªª«¬¬«ªªª©¨¦¥£ WYZ[\]]^`acdglquwy|} ¡££££¢¡¡£¦©«¬¯±±²²²±°®¬©¦¢ £¥¥¥¤
£¥¥¥¤¤¥¦§©«¬¬®¯°°±±°±°°®«««©¨§¨¨¨©ªª«ªªª«¬¬¬««ª©§§¥£¡WYYYYZZ[]]_`bhmpstvx{} ¡¢¤¤££¤¤¤¢ ¤¦©©ªª¬°±±²²²±°®¬«§£¢¢£¤¦¦¦¤
£¥¥¥¤¥¥¦§©ª«ª«®¯°°°¯¯¬«ªª©©©©ª««¬¬¬¬¬¬¬¬¬¬«ª¨§¦£ YYYXXYYY[\]]^cilmnqtwz¡£¤¤¤£¥¥¥¥¤££¤¤¤¥¥¦¨ª©ª«¬¯±±²²²±¯¬«§¤¢£¤¥¥¥¤}}
|¤¥¥¥¥¥¤¦©©ªª©©ª««¬¬¬¬«ªª©©©ªªªª¬®®¬¬¬«ª¨¦¥¤¡ZZXXYZYZZ[\\\_chjmorvy|¡¢¤¤¤¥¥¦¦¥¥¥¥¦§¨¨©©ªªª««¬¯°±²²±°¯«ª§¦¥¤¤¥¥¤¢qt{}mnx£¤¥¥¦¦¥¤¥©©ª©¨¨¨©©ªªª©¨§§¦§©ªªª«¬®¯®¬¬¬«©¨¦¥£¢ YZYYYZYYYZZ[[\^cgijlpuz¢£¤¥¥¥¥¥¦¦¦¥§¨ª«««««««¬¬¯°°²²±°¯ª©¨¦¦¤¤¥¥¤ urrpgk{dq
££¦§¥¥¦¦¥£¥©ª©¨¨¨¨¨©©©©¨¦¤¤¤¦¨©ªª¬®¯¯¯®®®¬¬«ª¨¦¥£¢¢YYXXXWWXYYYZYZ]`acehmsz¡¢¤¥¦¦¦¦¦¦§§©©«¬¬¬«««¬®¯±±²²±¯®¬«©¨¦¥¤¥¦¦¤¢ {qn{{s£¥§©©¨¨¨§¦¤¦©«ª©©©©©©©©©§¦¤££¦¨©ª«¬®®¯¯¯®¬«««©¨¦¥¤££ XYZWWUUWWWXXXZ\]_adhnu{¢£¥¥¦§§§¨©©ªª«««««¬¬¬®¯°±±°®¬«©©§¦¥¥¥¦¦¦¥¤¢¡¤¤§ª«ªª©©©¨¨¨ª««ªªªªªªª©©§¦¥¥¥¦©ª«¬¬®¯¯¯¯®¬¬««ªª©¨¦¥¤££ WX[YUSSSSSTVXZ\\^bhmt~¡£¤¥¦§¨©©ª«¬¬¬«««¬®¯°°¯««ª©¨§§¦¥¥¦¦¨©©¨§¦¦¥¤ ¤¦¨«¬¬«ªª©ª©©ª««««ªªªªªªªª¨¦¦¥¥¨ª««¬®¯°°°°®¯¯¯¯¬««ªªª©¨§¥£¡ VX[ZWSSSTTTVXZ\]`emu} ¢¤¤¦§¨©ª«¬¬¬¬«««¬®¯°°®«ª¨§¦¦¦¦¦¦¦¦¨©©©ª¨§¨¨¨§¦£¦©«¬¬««ªªª©©ªªªªªªªªªªªª¨¦¦¥¦¨ª«¬®°°±±¯°°°°¯®¬«ªªª¨§¦¤¡Y\]^[WWXYYXYZ[]`djs|
£¤¥§¨©«¬¬¬¬¬«¬«¬®®®ª¨§¦§§¦¦¦¦¦¦¨©©©ªªªªªªª©¤ ¦¨ª«¬¬¬¬«ªªª©¨©©©©©©©©ª©©¨§¦¥¦¨ª«¯¯°±±±°¯¯¯°¯®¬«ª©¦£¡ _bba^ZZZ[ZYXXZ]aipx~£¤¥¦¨ª¬¬¬®®®¬¬¬¬ª©§§¨§§§§§§§¦§§©©««¬««««ª¦¤§ªª«¬¬«ªªªªªªªª©©©©©©©©¨¨§¦§©ª«®¯°°°°°°°°¯¯®«ª§¥£_a`_\VUVWUTSSW]`gov}¢¤¥§¨ª«¬¬®®®®®¯®®¬¬««ª©¨¨©©¨¨¨¨§§§¦¦§©ª¬¬¬¬«ªª§¡ ¤¥¨©ª«¬¬¬¬««ªªªªªª©©¨¨¨¨¨¨¨¨¨©¨§©ª«®¯¯¯°°°¯®ª§¥¢ ]^_^[WVWXVTTVZ`bgox¡£¥¦©ª«¬¬®®®®®®¯®®¬¬«ª©¨¨©ª©¨©©©¨¨¨§§¨ª«¬¬«ª©©¨§¢
£¦¨ª«¬¬¬¬««ª«ªªªªª¨§§¨¨¨¨¨¨¨©©§¨ª«®¯¯¯¯°¯®««©§¤
~~`aba^\[[[ZXWY[^ahq|¡¢¤¦¨ª«¬¬®¯®®®®®¯®®¬¬ªª©©©ªªªªª©©©©©¨¨©©ª«««ª¨§¦¥¥¡¡£¥¨©ªª«««ªª©©¨¨¨¨§§§©©©©©¨¨¨¨¨§©¬¯¯¯¯¯¯®¬¬«©¨¦£
ghhfedb_\ZYXX\`gpy ¢¤¦©ª«¬¬®®®®®®®¯¯®¬«ªªªªª««««««ª©©©©©¨¨¨¨©§¥¥¥¤£ z ¢¤¥¦§¨ªª©©¨§§§§§§§¨©©ªªª©©©©ª©©«®¯¯¯°°¯¬«©§¦¥£onnnlkkfa_\[]cjt ¢¥¦¨ª«¬¬®®¯¯¯®®®¯¯®¬¬««««««««««««ª©©©©¨§§¦¥¥¤¤££¡
y~ ¢¤¥¥¦¨©©§¦¥¥¥¥¥¦§©©©ª©©©©ª«¬¬¬®¯¯°°®¬ª¨¥¢ trsrrqqmieccgnv ¢¥¦©ª«¬®®¯¯¯®®®¯¯®¬««««««««««««ª©©©©§¦¦¦¤£¤¢ ~ ¤¥¥¦§¨¦¤¤¤¤¤¤¦¨©©ªªªª©ªª«¬®¯¯¯¯®¬«ª¨¦£¡wwvuttrqnmotz £¥¦¨ª¬®°¯¯¯¯¯¯¯°±°¯¬¬«««««««««««ª©©©©§§¦¥¥¥¤
¢¤¥¥¤¤¤¤¥¦¨©ªªªª©©««®°¯¯¯¯¬ª§§¥£¢ {{zxwvuuuv| £¥§¨ª¬¯°±°±±±±±²²±°¬«¬¬¬¬¬¬¬«««ª©©©©§¥¤¢¡
~~¢¤¤¤¤¤¤¦©ª©©©©©ª¬®®¯¯¯¯®¯¯®«ª¨¦¥£¡¡ |}|zyyz|
£¥§©«¬¯°±±²²²²²²²²±¯®¬¬¬¬¬¬«««ª©©©¨¥¤¡~zx{¢¢¢£££¦©ªª©©ªª¬¯¯¯¯¯¯¯®¯¯®¬«©§¦£¡ ~}{{} ¤¥§©«®°±²²³´³²²³´³²²±°¯¬¬¬¬¬««ª©©©¨¦¥¢
¡¡¡¢£¦©ª©ªª©ª¯¯¯¯¯¯¯¯®¯¯®¬ª¨¦£¢|{~ ¢¥§ª¬¯°±²²²³´´´´´´´³²²°¯®¬«ªª©©©¨§¦¥¡ ¡ ¡£¤¦§©ªªªªªª°°¯®®¯®¬«©¦¥¢
~}
¢¤§ª¬¯°±²²³²³´µµµ´´µ´³³±°¯¯¬¬ªªª©©¨§§§¤¢¡ ¡£££££¢ ¢££¤£ ¢¤¥§¨©ªªª««ª¯¯®¬«««««ª©¥¤¢
~}¢¤§ª¬¯±±²³³³³µµ´´´´´´´³³²±°¯¯«ª©¨¨§¦§¥£¤¤¤¥¥¥¤¤¢£££¡ ¡£¤¤¤¤¥¤¤¢ ¡¢¤¥¦§©¨ªªª«««¬¬««ª©ª©§¥¢ ~zyy{¢¥§ª¬¯°±²³³³³µ´´´´´´´´³³³³±°¯®¬«ª¨¨¨§©¨§§§¦¦¦¦¥¤£¤¤¤¤£¤¥¦¥¥¥¦¦¦¤£ ¡¢£¤¥¥§¨©©ªªª«««¬¬«««ª©©©¨§¦£ ~zyyz|~ ¢¤§ª¬¯±±²³³µµµ³³³´µµ´³³³²±°¯¯¬¬«©©¨©ªª©©©¨§¨§¦¥¥¥¦§§¦¦§¨¦¦¦§¨©¨¦¤¢¢¢£¤¥¦§¨©ªªªªªª«««ªª©ª©©©§¦¥£¡ }{yyy{} ¢¤§ª¬¬®°±²³³µµµ³³³´µµ´³³³²²°¯®¬««ªªª©©ª©¨¨¨¨¨¨¨¨¨¨¨©©©¨¨¨¨§§¨©ªª§¤£¢£¥¦§¨ª«««««ªªª«««ªª©¨§¦¦¢ |yz}
¢¥§ª¬¬®°±²²³´µ´³³²´µ´´³²²²²±°®¬ªªªªª©©©©©©¨¨¨¨¨¨¨¨¨¨©©©©©§¦§©ª©¦££¤¦§©©ª««¬¬«««ªªªª§¤¤¤¤£¡ ~|~
£¥§ª¬¬®°±²²²³µµ´´³´´´³²±±±°¯¯¯««ª«ªªªªªªª©©©¨©©©©©©©ª©ª©©¨¦¥¥§§¤¤¤¤§¨¨©ª«««¬¬¬««¨¥¡
¢¤¦©¬¬¯¯°±²²²³´³³²³³²²±°°°¯¯¯¯¬¬¬¬«««¬¬¬¬ªªªªª©©ªªªªªªªª©©§¥¤£¥¥££¤¦§§¨¨©ª«««¬«¦¢¤¥¨ª«¯°°±±²²²²²²²²²²±°¯°¯¯®®¬««««««¬¬¬«ªªªª©©ªªªªªª©ªª©§¥££££¢££¦§¦¨¨©ªª««¬ª£
¡£¤§©ª®°°±±±±±²²±±±±±°¯®¯®¬¬«ª«ª«««¬¬«ªªªª©ªªªªªª«ª©©¨¦¥¤¤¤£££¤¨©§©¨©ª«««¬¬¬©¡
£¤¦©«¯°±±±±±±°°±±°±¯¯®¬¬«ªªªªªªª«¬¬«««ª«ªªªªªªªª©©§¥¥¦§§¦¦¦¨©©©©©ªª«««¬¬§¡
¢¤¦©«¯¯®®®®¯¯¯¯¯¯¯¯¬¬«ªªªª¨¨¨©©ª©ª¬¬¬«ª©©©ª©ª©©¨§¥¦©©©©©©©©©§¨©ªª«««««¦¡£¦©«««¬¬¬¬¬¬¬¬¬¬¬«©¨¨§§§¤¤¥¥§¨©©ªª«¬¬¬¬¬ª©©ªªªª©ªªª©ª«¬¬«ªªª«©¦§©ªª««ªª«©¤£¥¨ª¬¬«ªªª««ªªªªª«ª¨§¦¥£¤££¡ ¡¡¢¦¨©«¬¬¬««©©ªª©ªªª«¬¬¬¬¬ª§¨©ªª«ªª©¨¦¤
£¥§©ªªª©©©©ª©©¨©©©§¥¥¥¤¤¤££¢ ¡¥¨ª««ª©¨§§§¨©ª«¯®®®®®¬¬®¯®¬ªªª©©ªª©§¥£¡¡¤¥¦§§§¨¨©©¨¨§¦¥¦§¦¥¤¤¤¤¤¤¤£¢¡ ¢¤¦©ª«ª¨¥¤£¤¦©«¬®¯¬«¬¯°««««¨¦§§¦¤¢
¡¡¢¢££££¢£¢¢ ¢£¤¥¥¦¦¦¦¦¦¦¦¥¤¤¤¥§¦¦¤¤¤¤¤¤¤££¢¡¡£¤¤¦§¨©§¥¤££¨«¬¬««««ªªª©«®°¬ª«¬¬¨¤¤£
¡£¤¤¤¥¥¥¥¤¤¤¤¤¤¤£¡ ¡¢¢£¤¤¤¥¦¦¥¤¤¤¥§¦¦¦¦§§¦¤¤¤¤¤¤¤¤££¢ ¢££¤¥¥§¨§¦¥¤¤§«¬¬ª¨©©©§§§¨©¨§§ªª¨¤¢¢¤¤¦¦¦¦¦¥¥¥¥¥¥¥¥¥¥¤¤££¢¡¡¢¡¢¢¢£¤¤¤¤¤¥¥¥¥¥¥§¨¨§¦¦¦¦¥¥¥¥¥¥¥¤¤£¢¢ ¡¢££¤¦¥§©¨¦¦¥¤¤§«««¨¥¥¥¥¤£¢¤¨ª¬¨¢¤¥¥£¡
¢¥¦¦¦¦¦¦¥¥¥¥¥¥¤£££¤¥¥¥¤¤££¢¡¡¡¢£££¤¥¥¥¤¥¥¥¥¥¥¥¥¥¥¥¥¦¥¥¥¥¥¥¥¤¤¤£¢¢¢¢¢££¤¥¦¦§¦¦¥¥£££¦§©¦¤££¢¢¡¡£¤§§¡¡
£¤¦§¦¥¦¦¦¥¥¦¥¥¥¥¥¥¥¥¦¦¦¦¦¥¥£¢¡¡¢£££¤¥¥¥¤¥¥¤£££££££¤¤¥¦¦¦¦¦¦¦¦¤¤¤££££££¡¡£¥¥¥¦¥¤£¡ ¡¢¤¤¡ ¢¡¡ ¢£¤¦¥¥¥¥¥¦¦¦¦¦¦¦¦¦§§¨¨¨©¨§§¥¤££££££¤¤¤¥¤¤¤£¢¢¢¢¢¢£¤¥¥¦¦¦¦¦¦¦¦¥¥¤¤¤¤£££ ¡¥§¦¦¥¤¡¢ ¢¡¡£¤¥¤¥¥¦¦¦¦§¦¦§§§¨©©©©ª©©©¨§¦¥¦¦¥¥¥¥¥¥¥¥¤¤¤£££££¤¤¤¦¦¦¦¦¦¦¦¦¦¥¥¤¤¤¤££ £¦¦¥¥£¡ ¡¡£¤¤¥¥¦§§§¨§§¨¨¨©ªªªªªª©©ª©¨¨©©ª©¨¦¦¦¦¥¥¥¥££¤¤¤¥¥¥¦¦¦¦¦¦¦¦¦¥¥¥¥¤¤¤¤£ £¤¦¦¦¥¤¢ ¡£¤¥¥¦§§¨©¨¨©©©©ª©ª©©©©©©©©©©ªªª©¨¦¦¦¥¤¤¤££¤¤¤¥¥¦¦¦¦¦¦§§§§¦¥¥¥¤¤¤¤¢ £¤¥¦©¨¦¥¤¡¡£¤¥¥¦§§§§¨©ªªªªªªª©©©©©©©©©©©©©§§§¦¥¤£¢£¤£¤¥¥¥¦¦¦¦¦§§¨©¨¨¨§¦¥¤¤¤¤£££¤¥¦¨©¦¥¥¢ ¢¤¥¦§§¦¦©ªªªªªªªª©©©©©©©©©©¨¨¨§¦¥¥¥¤¢¢£¤¤¥¥¥¦¨¨¨¨¨¨©©©©¨©¨§¥¥¥¥¤¤¤¥¥¥¥¦¨§¦¦¥¢~~~~~~~~~~}}~~~~~}}}~~~~~~~{z{|}~~~~~}}}~~~~~~~{yyz|}~~~}{||}~}~}}yxyz{|~~}|z{{|}}}~~~~~}{xwyz{}~}}|zxyz{|||}}}}~}~~}zxwzz{}~}|||zyyyz{{|||||}}}}~~~|zxxzz{|~~|||zyyyzz{|{{||||||}}}}}~~~~}zyyzz|~~~~}}|zxxxzz{{|{{||||{|||}}|||}~~~~zyyzz|~~}}}|zxxyzzz{zzzz{{{z{|{|||||}~~~~zyyzz|~~|||{zyyzzzzz{z{{z{{zzzz{||||~~{yzzz|}}|||{yzzzzzzyzzzzz{{{zzzz|}}~~}}~
|{zzz{}~~}|{zzzzzzzzzzzzzzz{||{{{|}}~}|~|{zzz|}~~~~~|zzzzzzzzzzyz{zz{||{{{|}}~~}~
|zzzz{}~~~~}{zzzzzzz{{{{z{{{||||||~~~~{yyzyz}~~~~~~~~|zzzzz{{z{{zz{{||||}}}~ywxyxy|~~~~~~~~}}{yzzzz{|z{z{|{{||}|}~~}zxxxxy{}~~~~}}~~~}}{zzzzy{|z{z{||||}~~~}~}zxxxyy{}~~~}|zzyzzz{|{{{|||{{|}}~}}~~~|{xxxyy{|}~~~~}|zzzzzy{|}}~~||{{{{|}}}}|}~~~}|{||zyyyy{||~~~~}{|zzzzz{}~}|{zzz{||}}|}~~~~~}|||{zxyyy{|}~~~~}||{zzzzzz|~}|{yyz{|}|{|}~~~~~~}}~~}}||{zxxyy{|}~~}~~~}||{{{zzzzz|~~}{yyzz{|{zz|}}~}}|}|}}|{zyyxxyz|~~~}||{{{zyzz{}}{zyzz{|{yz{||}}}|}|}~~~~|zzzxxwwyz||}~}|||{{|zzz|}~~{zyzzz{zxyz{|||||||}}}}}~}{zzzyywwxxz||~}|||{|||{{{}~|zzzyz{zyyz{{{||||||}}}|}||{zzzzyxxwxz{{}~}|||}||||}~~}{||{{{|{{||||||}|}|}}}{||{{zzzzzyywwxzz~}||{|}~}}}}{{||{|||||||||||}|{{|{{z{{{{{{{{yyxwxyy}|{{{}~~|||||||||||||||{{|{{{{{z{|||||{|zzyxyyy}|{{|}~~~}}}}}~~~~}}||||z{{zzz{zz{{|||||{zzyyxyz~}|||}}~~}~}~}}|{{{zzyzzzzz{z{|{||{{zzzzyyz}}|~~~~~~~~}~|{{zzzyyzzzzz{{{|}|||{{{zzyz~~}}}~~}}|~~}|||{zyyyzzz{{z|}}}}||{{|{z|~~~}|||||}~}}}}~}}}|{yxxxyz{{z||}~~}}|||{{{~~~~}||{{{|}}|~}}}}|{yxwwyyzzz||}~~~~}|{{{{~~~~~~~~~||{{||||}~~~~~}}||ywwxyyzzz{|||}}}}{{{zz}~~~}~~}}|||{|||||
~}}~}~~}}|{ywwxyyzzzzz|||}||{{{{z~~~~~~~}}||{zz{{{}
~~~~~~~}|{zywxxyyzzzzz{{{|{{{{{{z~~~~~~}||{{zz{|}~}}|zyyyyyzz{||{{{{{{{{{{z{~~~~}||{zzz{{|~}~~}|zyyyyyzz{{{{{{{{{{{z{{z~~~~}}|zyz{|}~}~~~~~}|{zyyyyzz{{{{{{{{{{{z{{z|}}~~~}|{zy{|}}~}}}}~}}}}~~~~~}{{{zz{{{|{{{z{{{{{{{{zz{|}~~~~|{zzz{|~~~~}}}}||||~~~~~~~~}||||{{{{{{{zz{{{z{{{zzz{||}}~~||{{||}~~||||||||~~~~~~~~}}}|||{{{{||{{{{{{{{zzzz{||}}~~~~}}}}}~}||||}|}}~~~~~~~~~~~}}|}|||||||{|||{{zzzz{||}~~~~~~}|{{z|}~~}}~~~~~~~}}}~~}}||}}|||||||zzyz{{|||||~}|zyy|}~~}}}~~~~}}}~~~}}|}}||}|}}~zzzyzz||z{|~}|{zz|}~~~}}}~~~}~~}}}}}~~~~~~}~~}}}~~~~zzzzzz{{{{|~~}|{{|}}}}~~~}}~~~~}}}~~~~~}~}}}|||}}}|zzyyzzzz{|}~}||}~~}~~~~}}~~~~~~~}}}}}||{{|{{{{zzzzzzzz{||}}}~~~~~~~~~~~}}}}}}|}|{{|{{{zyzzzzzzz{|}~~~~~~}}||}}}}|{z{zz{ywzyzz{zz{}}~~~~~~~}{{{||{||zz{zzzyyzyyz{{{|~~~}~~~~}|{{{{{{{{{zzzzzyzzzzzzz{}~~~~~~~~~~~~~}||{{{{{z{z{{zzzzzz{{||{{|}~~~~~~~~~~~~~}}}|{{{|||{z{|{zzzzz|}|||{{}~~~~~}}}}||{{|{zz{{zzzzzz|}}}||{|}~}~~~~~~~}}}}|{z{{{z{{{{|{{{{|}}}}|||}}}}}}}~~}}}}||zz{{y{||}||{{{}~~}}}{|}}}}}}}~}}}}|{{z{{{|{||||{|{}}||||}}|{||}~~~~~~}{|||{{{{|{{{{zz||{{z~~}||}}}}{|||~~~~}}}|z{{|{{zzzzzzzyy||{zz~~}}~~~}||{|~~}~~~}}}|z{|||{zzz{{zzyz{zzzz~~||}~~}}}~~~~~~~}}~}}}}|{{|}|{{{{{{zzyzzz{{{~~~~~~}~~}}}}}}}}}}}}}}}}}}~~~~~|}}}}}|||}|{z{{{{{zzzz{{{{~~~~~}}}}}~~~}}}}~}}}}}}}}}}}}}}}}}}}~~~~~~~}}}}}}}|||||{{{{{z{|{zzz{z{~~~~}||||}}}|}|||}}}}}}}||||||||||||}~}}}}~~}|||}||{{||{{|{{{{{{zz{{{{{~~~~~}|}~~}|}|}}}||}|||||||||||||||}}}}}}||}~~~}|}}}}||{|{{{{||{|{{{z{{{{z~}}~~}}|}}}}}}}}}}}}}}}}}||||||||||||}}~~~~}}}}}}}}}|zzz||||||||{{{||~~~~}}~}}}}}}}||||}}}}}}}}}|||||{{{{}|||}}~~~}}}}}}}}}}{zzz{{{|{||{{{{{{~~~~~~~~~~~~~~~~}|||~~~}~~}|}}||}}}}}}}}}}}}}}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}|||}~~~~~~~}~~}}~~~~~~~}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}}}||||}~~~~~}}}}}}}}}}}}}}}}~~~~~~~~~~~~~~~~~~~~~~~~~~}}}||||}~~~}}}}}}}}}}}}}}}~~~~~~~~~~~~~~~~~~~~~~~~~}}}||||}~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~}}}}}}}~}}}~~
~~~~~~~~~~~~~~~~~~~~}}}}}~~~~~
~~~~~~~~~~~~~~~~~~~~}}}}}}~~~~
~~~~~~~~~~~~~~~~~~}~~~~~~
~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~
~~~~~
~~~~~~~~~~~~
~~~
~~~~~~~~~
~~~~~~~~~
~~~~~~~~~~
~~~~~~~~~~
~~~~~~~~~
~~~~~~~~~~
~~~~~~~~~~~~
~~~~~~~~~~
~~~~~~~~~~
~~~~~~~~~~
~~~~~~~~~~
~~~~~~~~~
~~~~~~~~~
~~~~~~~~~~
~~~~~~~~~~
~~~~~~~~~~
~~~~~~~~~~
~~~~~~~~~
~~~~~~~~~
~~~~~~~~~~
~~~~~~
~~~~~
~~~~~
~~~~~
~~
~~~~
~~~~
~~~~~
~~
~~~~
~
~~~
~~
~
~~~~~~
~}}}}}}}~}
}|}|}}}~~}}~
~}|}}|||||}~~~}}~
~|{|}}}|||{|}|~~~~}}}~
~}|}}}}|{{{}~~~}}}}}~
~}~}}}}|||}~~~}}}}}~
~}}}}}}||{}~}}}}~~
}}~~~}}}|}~}}~~~~~
~~~~}~~~}}}}~~}}~~~~~
~~~~}}}}}}}}}}}}}~~~~
~~}}}}}}}}}}}}}~~
~~}~}}}}}}}}}}||}~
~~~}}~~~~}|||||}}~~~
~~}}~}}~}|||}}}~~~}}}~
~~~~}}}~~}~~}}~~~~~~
~~~~~~~~~~~~~
~~~~~~~~~~~~~
~~~~~
~
\ No newline at end of file
diff --git a/tools/avm_analyzer/avm_analyzer_app/assets/leo_qcif.zip b/tools/avm_analyzer/avm_analyzer_app/assets/leo_qcif.zip
new file mode 100644
index 0000000..4ac288b
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/assets/leo_qcif.zip
Binary files differ
diff --git a/tools/avm_analyzer/avm_analyzer_app/assets/manifest.json b/tools/avm_analyzer/avm_analyzer_app/assets/manifest.json
new file mode 100644
index 0000000..926727b
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/assets/manifest.json
@@ -0,0 +1,32 @@
+{
+ "name": "AVM Anaylzer",
+ "short_name": "avm-analyzer",
+ "icons": [
+ {
+ "src": "/icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon-256x256.png",
+ "sizes": "256x256",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon-384x384.png",
+ "sizes": "384x384",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "lang": "en-US",
+ "id": "/index.html",
+ "start_url": "./index.html",
+ "display": "standalone",
+ "background_color": "white",
+ "theme_color": "white"
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/assets/sw.js b/tools/avm_analyzer/avm_analyzer_app/assets/sw.js
new file mode 100644
index 0000000..ee7bf78
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/assets/sw.js
@@ -0,0 +1,25 @@
+var cacheName = 'avm-analyzer';
+var filesToCache = [
+ './',
+ './index.html',
+ './avm_analyzer.js',
+ './avm_analyzer.wasm',
+];
+
+/* Start the service worker and cache all of the app's content */
+self.addEventListener('install', function (e) {
+ e.waitUntil(
+ caches.open(cacheName).then(function (cache) {
+ return cache.addAll(filesToCache);
+ })
+ );
+});
+
+/* Serve cached content when offline */
+self.addEventListener('fetch', function (e) {
+ e.respondWith(
+ caches.match(e.request).then(function (response) {
+ return response || fetch(e.request);
+ })
+ );
+});
diff --git a/tools/avm_analyzer/avm_analyzer_app/index.html b/tools/avm_analyzer/avm_analyzer_app/index.html
new file mode 100644
index 0000000..d7d7dd7
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/index.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<!-- Forked from: https://github.com/emilk/eframe_template -->
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+
+<!-- Disable zooming: -->
+<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+
+<head>
+ <title>AVM Analyzer</title>
+
+ <link data-trunk rel="rust" data-wasm-opt="2" />
+ <base data-trunk-public-url />
+
+ <link data-trunk rel="icon" href="assets/favicon.ico">
+ <link data-trunk rel="copy-file" href="assets/sw.js" />
+ <link data-trunk rel="copy-file" href="assets/manifest.json" />
+ <link data-trunk rel="copy-file" href="assets/icon-192x192.png" />
+ <link data-trunk rel="copy-file" href="assets/icon-256x256.png" />
+ <link data-trunk rel="copy-file" href="assets/icon-384x384.png" />
+ <link data-trunk rel="copy-file" href="assets/icon-512x512.png" />
+ <link rel="manifest" href="manifest.json">
+ <link rel="apple-touch-icon" href="icon_ios_touch_192.png">
+ <meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
+ <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
+
+ <style>
+ html {
+ /* Remove touch delay: */
+ touch-action: manipulation;
+ }
+
+ body {
+ background: #909090;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ body {
+ background: #404040;
+ }
+ }
+
+ /* Allow canvas to fill entire web page: */
+ html,
+ body {
+ overflow: hidden;
+ margin: 0 !important;
+ padding: 0 !important;
+ height: 100%;
+ width: 100%;
+ }
+
+ /* Position canvas in center-top: */
+ canvas {
+ margin-right: auto;
+ margin-left: auto;
+ display: block;
+ position: absolute;
+ top: 0%;
+ left: 50%;
+ transform: translate(-50%, 0%);
+ }
+
+ .centered {
+ margin-right: auto;
+ margin-left: auto;
+ display: block;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: #f0f0f0;
+ font-size: 24px;
+ font-family: Ubuntu-Light, Helvetica, sans-serif;
+ text-align: center;
+ }
+
+ /* ---------------------------------------------- */
+ /* Loading animation from https://loading.io/css/ */
+ .lds-dual-ring {
+ display: inline-block;
+ width: 24px;
+ height: 24px;
+ }
+
+ .lds-dual-ring:after {
+ content: " ";
+ display: block;
+ width: 24px;
+ height: 24px;
+ margin: 0px;
+ border-radius: 50%;
+ border: 3px solid #fff;
+ border-color: #fff transparent #fff transparent;
+ animation: lds-dual-ring 1.2s linear infinite;
+ }
+
+ @keyframes lds-dual-ring {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+ </style>
+</head>
+
+<body>
+ <canvas id="avm_analyzer_canvas_id"></canvas>
+ <input type="file" id="local-stream-input" style="display: none;" />
+</body>
+
+</html>
\ No newline at end of file
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/app.rs b/tools/avm_analyzer/avm_analyzer_app/src/app.rs
new file mode 100644
index 0000000..1040b33
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/app.rs
@@ -0,0 +1,301 @@
+use eframe::Storage;
+use egui::{CentralPanel, TopBottomPanel};
+use egui_dock::NodeIndex;
+use egui_dock::{DockArea, DockState};
+
+use log::{info, warn};
+use wasm_bindgen::JsValue;
+use web_time::{Duration, Instant};
+
+use crate::app_state::{AppState, STATE_URL_QUERY_PARAM_NAME};
+use crate::stream::{ChangeFrame, CurrentFrame, HTTP_POLL_PERIOD_SECONDS};
+use crate::views::{
+ handle_drag_and_drop, BlockInfoViewer, CoeffsViewer, ControlsViewer, DecodeProgressViewer, DetailedPixelViewer,
+ FrameInfoViewer, FrameSelectViewer, FrameViewer, MenuBar, PerformanceViewer, RenderView, SelectedObjectKind,
+ SettingsViewer, StatsViewer, StreamSelectViewer, SymbolInfoViewer,
+};
+
+type TabType = Box<dyn RenderView>;
+
+pub struct AvmAnalyzerApp {
+ state: AppState,
+ dock_state: DockState<TabType>,
+}
+
+const SAVED_SETTINGS_KEY: &str = "settings";
+impl AvmAnalyzerApp {
+ pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
+ let mut saved_settings = None;
+ if let Some(storage) = cc.storage {
+ if let Some(settings) = storage.get_string(SAVED_SETTINGS_KEY) {
+ saved_settings = Some(settings);
+ }
+ }
+ let mut dock_state: DockState<TabType> = DockState::new(vec![Box::new(FrameViewer)]);
+ let surface = dock_state.main_surface_mut();
+
+ let [others_view, _frames_view] =
+ surface.split_above(NodeIndex::root(), 0.15, vec![Box::new(FrameSelectViewer)]);
+ let [yuv_view, _info_view] = surface.split_left(
+ others_view,
+ 0.2,
+ vec![
+ Box::new(FrameInfoViewer),
+ Box::new(BlockInfoViewer),
+ Box::new(SymbolInfoViewer),
+ Box::new(StatsViewer),
+ ],
+ );
+
+ let [_, _] = surface.split_below(yuv_view, 0.8, vec![Box::new(ControlsViewer)]);
+ let state = AppState::new(saved_settings);
+
+ Self { state, dock_state }
+ }
+
+ pub fn check_keyboard_input(&mut self, ctx: &egui::Context) {
+ if let Some(stream) = self.state.stream.as_mut() {
+ if ctx.input(|i| i.key_pressed(egui::Key::ArrowLeft)) {
+ stream.change_frame(ChangeFrame::prev().order(self.state.settings.persistent.frame_sort_order));
+ } else if ctx.input(|i| i.key_pressed(egui::Key::ArrowRight)) {
+ stream.change_frame(ChangeFrame::next().order(self.state.settings.persistent.frame_sort_order));
+ }
+ }
+ }
+
+ pub fn check_playback(&mut self) {
+ let playback = &mut self.state.settings.playback;
+ if playback.playback_running {
+ if let Some(stream) = self.state.stream.as_mut() {
+ let current_time = Instant::now();
+ let elapsed = current_time - playback.current_frame_display_instant;
+ let time_between_frames = Duration::from_secs_f32(1.0 / playback.playback_fps);
+ if elapsed > time_between_frames {
+ playback.current_frame_display_instant = current_time;
+ let changed = stream.change_frame(
+ ChangeFrame::next()
+ .order(self.state.settings.persistent.frame_sort_order)
+ .allow_loop(playback.playback_loop)
+ .loaded_only(playback.show_loaded_frames_only),
+ );
+ if !changed {
+ playback.playback_running = false;
+ }
+ }
+ }
+ }
+ }
+}
+
+impl eframe::App for AvmAnalyzerApp {
+ fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
+ self.state.check_stream_events();
+ self.state
+ .server_decode_manager
+ .check_progress(&mut self.state.stream, &mut self.state.http_stream_manager, &self.state.settings.sharable.streams_url);
+ self.check_keyboard_input(ctx);
+ self.check_playback();
+ handle_drag_and_drop(ctx, &mut self.state);
+
+ if let Some(stream) = self.state.stream.as_mut() {
+ stream.check_promises();
+ }
+
+ ctx.input(|input| {
+ self.state
+ .performance_history
+ .on_new_frame(input.time, frame.info().cpu_usage)
+ });
+
+ ctx.request_repaint_after(Duration::from_secs_f32(HTTP_POLL_PERIOD_SECONDS));
+ if let Err(err) = self
+ .state
+ .local_stream_manager
+ .check_local_stream_ready(&mut self.state.stream)
+ {
+ warn!("{}", err);
+ };
+
+ let menu_bar = MenuBar {};
+ TopBottomPanel::top(menu_bar.title()).show(ctx, |ui| {
+ if let Err(err) = menu_bar.render(ui, &mut self.state) {
+ warn!("{}", err);
+ }
+ });
+ CentralPanel::default()
+ .frame(egui::Frame::central_panel(&ctx.style()).inner_margin(0.))
+ .show(ctx, |ui| {
+ DockArea::new(&mut self.dock_state)
+ .show_close_buttons(false)
+ .show_add_buttons(false)
+ .draggable_tabs(false)
+ .show_tab_name_on_hover(false)
+ .show_inside(ui, &mut self.state);
+ });
+
+ let close_window = false;
+ let mut show_stream_select = self.state.settings.show_stream_select;
+ egui::Window::new("Load Stream")
+ .default_width(800.0)
+ .open(&mut show_stream_select)
+ .collapsible(false)
+ .show(ctx, |ui| {
+ if let Err(err) = StreamSelectViewer.render(ui, &mut self.state) {
+ warn!("{}", err);
+ }
+ });
+ if !show_stream_select || close_window {
+ self.state.settings.show_stream_select = false;
+ }
+
+ let mut show_decode_progress: bool = self.state.settings.show_decode_progress;
+ egui::Window::new("Decode Progress")
+ .open(&mut show_decode_progress)
+ .default_width(800.0)
+ .default_height(600.0)
+ .collapsible(false)
+ .show(ctx, |ui| {
+ if let Err(err) = DecodeProgressViewer.render(ui, &mut self.state) {
+ warn!("{}", err);
+ }
+ });
+ self.state.settings.show_decode_progress = show_decode_progress;
+
+ let mut show_performance_window: bool = self.state.settings.show_performance_window;
+ egui::Window::new("Performance")
+ .open(&mut show_performance_window)
+ .collapsible(false)
+ .show(ctx, |ui| {
+ if let Err(err) = PerformanceViewer.render(ui, &mut self.state) {
+ warn!("{}", err);
+ }
+ });
+ self.state.settings.show_performance_window = show_performance_window;
+
+ let mut show_settings_window: bool = self.state.settings.show_settings_window;
+ egui::Window::new("Settings")
+ .open(&mut show_settings_window)
+ .default_width(800.0)
+ .default_height(600.0)
+ .resizable(true)
+ .collapsible(false)
+ .show(ctx, |ui| {
+ if let Err(err) = SettingsViewer.render(ui, &mut self.state) {
+ warn!("{}", err);
+ }
+ });
+ self.state.settings.show_settings_window = show_settings_window;
+
+ let mut show_pixel_viewer: bool = self.state.settings.sharable.show_pixel_viewer;
+ let pixel_viewer_title = match &self.state.settings.selected_object {
+ None => {
+ show_pixel_viewer = false;
+ "".to_string()
+ }
+ Some(selected_object) => {
+ if let Some(frame) = self.state.stream.current_frame() {
+ selected_object
+ .rect(frame)
+ .map(|rect| {
+ format!(
+ "Pixels: {}x{} block at (x={}, y={})",
+ rect.width() as i32,
+ rect.height() as i32,
+ rect.left_top().x as i32,
+ rect.left_top().y as i32
+ )
+ })
+ .unwrap_or_default()
+ } else {
+ "".to_string()
+ }
+ }
+ };
+ egui::Window::new(pixel_viewer_title)
+ .id("Pixels".into())
+ .open(&mut show_pixel_viewer)
+ .default_width(400.0)
+ .default_height(400.0)
+ .resizable(true)
+ .collapsible(false)
+ .show(ctx, |ui| {
+ if let Err(err) = DetailedPixelViewer.render(ui, &mut self.state) {
+ warn!("{}", err);
+ }
+ });
+ self.state.settings.sharable.show_pixel_viewer = show_pixel_viewer;
+
+ let mut show_coeffs_viewer: bool = self.state.settings.sharable.show_coeffs_viewer;
+ let mut coeffs_viewer_title = "".to_string();
+ match &self.state.settings.selected_object {
+ None => {
+ show_coeffs_viewer = false;
+ }
+ Some(selected_object) => {
+ if let Some(frame) = self.state.stream.current_frame() {
+ if matches!(selected_object.kind, SelectedObjectKind::TransformUnit(_)) {
+ coeffs_viewer_title = selected_object
+ .rect(frame)
+ .map(|rect| {
+ format!(
+ "Coeffs: {}x{} block at (x={}, y={})",
+ rect.width() as i32,
+ rect.height() as i32,
+ rect.left_top().x as i32,
+ rect.left_top().y as i32
+ )
+ })
+ .unwrap_or_default();
+ }
+ }
+ }
+ };
+ egui::Window::new(coeffs_viewer_title)
+ .id("Coeffs".into())
+ .open(&mut show_coeffs_viewer)
+ .default_width(400.0)
+ .default_height(400.0)
+ .resizable(true)
+ .collapsible(false)
+ .show(ctx, |ui| {
+ if let Err(err) = CoeffsViewer.render(ui, &mut self.state) {
+ warn!("{}", err);
+ }
+ });
+ self.state.settings.sharable.show_coeffs_viewer = show_coeffs_viewer;
+
+ if self.state.settings.persistent.update_sharable_url {
+ let shared_state = self.state.settings.create_shared_settings(&self.state.stream);
+ if shared_state != self.state.previous_shared_state {
+ self.state.previous_shared_state = shared_state.clone();
+ let window = web_sys::window().unwrap();
+ let shared_settings_str = shared_state.encode();
+ if let Ok(history) = window.history() {
+ if let Err(err) = history.replace_state_with_url(
+ &JsValue::NULL,
+ "AVM Analyzer",
+ Some(&format!("?{STATE_URL_QUERY_PARAM_NAME}={shared_settings_str}")),
+ ) {
+ warn!("Unable to set URL state: {err:?}");
+ }
+ }
+ }
+ }
+ // TODO(comc): Add selected tab to shared settings.
+ // let find_me: Box<dyn RenderView> = Box::new(StatsViewer);
+ // let idx = self.dock_state.find_tab(&find_me);
+ }
+
+ fn save(&mut self, storage: &mut dyn Storage) {
+ info!("Saved settings to local storage.");
+ if let Ok(saved_settings) = serde_json::to_string(&self.state.settings.persistent) {
+ storage.set_string(SAVED_SETTINGS_KEY, saved_settings);
+ } else {
+ warn!("Error serialize saved settings.")
+ }
+ }
+
+ fn auto_save_interval(&self) -> std::time::Duration {
+ Duration::from_secs_f32(5.0)
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/app_state.rs b/tools/avm_analyzer/avm_analyzer_app/src/app_state.rs
new file mode 100644
index 0000000..f56023c
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/app_state.rs
@@ -0,0 +1,156 @@
+use crate::views::{PerformanceHistory, RenderView, SelectedObject, SelectedObjectKind};
+
+use avm_analyzer_common::{AvmStreamInfo, DEFAULT_PROTO_PATH_FRAME_SUFFIX_TEMPLATE};
+use avm_stats::Spatial;
+use egui::{Ui, WidgetText};
+use egui_dock::TabViewer;
+
+use log::{info, warn};
+
+use crate::settings::{Settings, SharableSettings};
+use crate::stream::{
+ CacheStrategy, FrameStatus, HttpStreamManager, LocalStreamManager, ServerDecodeManager, Stream, StreamEventType,
+};
+
+pub struct AppState {
+ pub stream: Option<Stream>,
+ pub settings: Settings,
+ pub local_stream_manager: LocalStreamManager,
+ pub server_decode_manager: ServerDecodeManager,
+ pub http_stream_manager: HttpStreamManager,
+ pub performance_history: PerformanceHistory,
+ pub previous_shared_state: SharableSettings,
+}
+
+pub const STATE_URL_QUERY_PARAM_NAME: &str = "state";
+pub const LOAD_STREAM_URL_PARAM_NAME: &str = "load_stream";
+pub const NUM_FRAMES_URL_PARAM_NAME: &str = "num_frames";
+
+impl AppState {
+ pub fn new(saved_settings: Option<String>) -> Self {
+ let http_stream_manager = HttpStreamManager::new();
+ let local_stream_manager = LocalStreamManager::new();
+ let mut stream = None;
+ let mut shared_settings = None;
+ let mut load_stream_info = None;
+ let window = web_sys::window().unwrap();
+ if let Ok(search) = window.location().search() {
+ if let Ok(params) = web_sys::UrlSearchParams::new_with_str(&search) {
+ if let Some(state) = params.get(STATE_URL_QUERY_PARAM_NAME) {
+ shared_settings = Some(state);
+ }
+ if let Some(load_stream) = params.get(LOAD_STREAM_URL_PARAM_NAME) {
+ let num_frames = params.get(NUM_FRAMES_URL_PARAM_NAME).unwrap_or("1".into());
+ let num_frames = num_frames.parse::<usize>().unwrap_or(1);
+ if let Some(stream_name_end) = load_stream.find(DEFAULT_PROTO_PATH_FRAME_SUFFIX_TEMPLATE) {
+ let stream_name = load_stream[..stream_name_end].to_string();
+ load_stream_info = Some(AvmStreamInfo {
+ stream_name,
+ num_frames,
+ proto_path_template: load_stream,
+ thumbnail_png: None
+ });
+ }
+ }
+ }
+ }
+
+ let mut settings = Settings::from_shared_settings_string(shared_settings.as_deref());
+ settings.apply_saved_settings_string(saved_settings);
+ if let Some(stream_info) = load_stream_info {
+ settings.sharable.selected_stream = Some(stream_info);
+ settings.sharable.show_remote_streams = false;
+ settings.sharable.streams_url = "".into();
+ }
+
+ if settings.sharable.show_remote_streams {
+ http_stream_manager.load_stream_list();
+ }
+
+ if let Some(stream_info) = &settings.sharable.selected_stream {
+ // Unwrap is okay here, since nothing can fail immediately when creating an HTTP stream.
+ stream = Some(Stream::from_http(stream_info.clone(), false, settings.sharable.selected_frame, &settings.sharable.streams_url).unwrap());
+ } else {
+ local_stream_manager.load_demo_stream();
+ local_stream_manager
+ .check_local_stream_ready(&mut stream)
+ .expect("Failed to load demo stream");
+ }
+ Self {
+ stream,
+ settings,
+ local_stream_manager,
+ server_decode_manager: ServerDecodeManager::new(),
+ http_stream_manager,
+ performance_history: PerformanceHistory::default(),
+ previous_shared_state: SharableSettings::default(),
+ }
+ }
+ /// Called whenever the current frame changed. Should reset anything specific to the current frame, e.g. selected objects.
+ pub fn check_stream_events(&mut self) {
+ if let Some(stream) = self.stream.as_mut() {
+ let events = stream.check_events();
+ for event in events {
+ let event = event.event;
+ info!("Event: {event:?}");
+ match event {
+ StreamEventType::NewStream => {
+ self.settings.selected_object = None;
+ self.settings.selected_object_leaf = None;
+ self.settings.cached_stat_data = None;
+ self.settings.scroll_to_index = Some(0);
+ }
+ StreamEventType::FrameChanged(index) => {
+ self.settings.selected_object = None;
+ self.settings.selected_object_leaf = None;
+ self.settings.cached_stat_data = None;
+ self.settings.scroll_to_index = Some(index);
+ let limit = if self.settings.persistent.apply_cache_strategy {
+ Some(self.settings.persistent.cache_strategy_limit)
+ } else {
+ None
+ };
+ stream.apply_cache_strategy(CacheStrategy::from_limit(limit));
+ }
+ StreamEventType::FirstFrameLoaded(index) => {
+ info!("First frame loaded: {index}");
+ if let FrameStatus::Loaded(frame) = stream.get_frame(index) {
+ if !self.settings.have_world_bounds_from_shared_settings {
+ self.settings.sharable.world_bounds = frame.rect();
+ }
+ if let Some(shared_selected_object) = self.settings.sharable.selected_object_kind.take() {
+ if let FrameStatus::Loaded(frame) = stream.get_frame(index) {
+ let is_valid = match &shared_selected_object {
+ SelectedObjectKind::CodingUnit(obj) => obj.try_resolve(frame).is_some(),
+ SelectedObjectKind::TransformUnit(obj) => obj.try_resolve(frame).is_some(),
+ SelectedObjectKind::Superblock(obj) => obj.try_resolve(frame).is_some(),
+ SelectedObjectKind::Partition(obj) => obj.try_resolve(frame).is_some(),
+ };
+ if is_valid {
+ info!("Restored selected object: {shared_selected_object:?}");
+ self.settings.selected_object =
+ Some(SelectedObject::new(shared_selected_object));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+impl TabViewer for AppState {
+ type Tab = Box<dyn RenderView>;
+
+ fn ui(&mut self, ui: &mut Ui, tab: &mut Self::Tab) {
+ if let Err(err) = tab.render(ui, self) {
+ warn!("{}", err);
+ }
+ }
+
+ fn title(&mut self, tab: &mut Self::Tab) -> WidgetText {
+ tab.title().into()
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/image_manager/color_maps.rs b/tools/avm_analyzer/avm_analyzer_app/src/image_manager/color_maps.rs
new file mode 100644
index 0000000..1e83e64
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/image_manager/color_maps.rs
@@ -0,0 +1,259 @@
+// TODO(comc): Add other colormap options.
+pub const JET_COLORMAP: [[u8; 3]; 256] = [
+ [0, 0, 127],
+ [0, 0, 132],
+ [0, 0, 136],
+ [0, 0, 141],
+ [0, 0, 145],
+ [0, 0, 150],
+ [0, 0, 154],
+ [0, 0, 159],
+ [0, 0, 163],
+ [0, 0, 168],
+ [0, 0, 172],
+ [0, 0, 177],
+ [0, 0, 182],
+ [0, 0, 186],
+ [0, 0, 191],
+ [0, 0, 195],
+ [0, 0, 200],
+ [0, 0, 204],
+ [0, 0, 209],
+ [0, 0, 213],
+ [0, 0, 218],
+ [0, 0, 222],
+ [0, 0, 227],
+ [0, 0, 232],
+ [0, 0, 236],
+ [0, 0, 241],
+ [0, 0, 245],
+ [0, 0, 250],
+ [0, 0, 254],
+ [0, 0, 255],
+ [0, 0, 255],
+ [0, 0, 255],
+ [0, 0, 255],
+ [0, 4, 255],
+ [0, 8, 255],
+ [0, 12, 255],
+ [0, 16, 255],
+ [0, 20, 255],
+ [0, 24, 255],
+ [0, 28, 255],
+ [0, 32, 255],
+ [0, 36, 255],
+ [0, 40, 255],
+ [0, 44, 255],
+ [0, 48, 255],
+ [0, 52, 255],
+ [0, 56, 255],
+ [0, 60, 255],
+ [0, 64, 255],
+ [0, 68, 255],
+ [0, 72, 255],
+ [0, 76, 255],
+ [0, 80, 255],
+ [0, 84, 255],
+ [0, 88, 255],
+ [0, 92, 255],
+ [0, 96, 255],
+ [0, 100, 255],
+ [0, 104, 255],
+ [0, 108, 255],
+ [0, 112, 255],
+ [0, 116, 255],
+ [0, 120, 255],
+ [0, 124, 255],
+ [0, 128, 255],
+ [0, 132, 255],
+ [0, 136, 255],
+ [0, 140, 255],
+ [0, 144, 255],
+ [0, 148, 255],
+ [0, 152, 255],
+ [0, 156, 255],
+ [0, 160, 255],
+ [0, 164, 255],
+ [0, 168, 255],
+ [0, 172, 255],
+ [0, 176, 255],
+ [0, 180, 255],
+ [0, 184, 255],
+ [0, 188, 255],
+ [0, 192, 255],
+ [0, 196, 255],
+ [0, 200, 255],
+ [0, 204, 255],
+ [0, 208, 255],
+ [0, 212, 255],
+ [0, 216, 255],
+ [0, 220, 254],
+ [0, 224, 250],
+ [0, 228, 247],
+ [2, 232, 244],
+ [5, 236, 241],
+ [8, 240, 237],
+ [12, 244, 234],
+ [15, 248, 231],
+ [18, 252, 228],
+ [21, 255, 225],
+ [24, 255, 221],
+ [28, 255, 218],
+ [31, 255, 215],
+ [34, 255, 212],
+ [37, 255, 208],
+ [41, 255, 205],
+ [44, 255, 202],
+ [47, 255, 199],
+ [50, 255, 195],
+ [54, 255, 192],
+ [57, 255, 189],
+ [60, 255, 186],
+ [63, 255, 183],
+ [66, 255, 179],
+ [70, 255, 176],
+ [73, 255, 173],
+ [76, 255, 170],
+ [79, 255, 166],
+ [83, 255, 163],
+ [86, 255, 160],
+ [89, 255, 157],
+ [92, 255, 154],
+ [95, 255, 150],
+ [99, 255, 147],
+ [102, 255, 144],
+ [105, 255, 141],
+ [108, 255, 137],
+ [112, 255, 134],
+ [115, 255, 131],
+ [118, 255, 128],
+ [121, 255, 125],
+ [124, 255, 121],
+ [128, 255, 118],
+ [131, 255, 115],
+ [134, 255, 112],
+ [137, 255, 108],
+ [141, 255, 105],
+ [144, 255, 102],
+ [147, 255, 99],
+ [150, 255, 95],
+ [154, 255, 92],
+ [157, 255, 89],
+ [160, 255, 86],
+ [163, 255, 83],
+ [166, 255, 79],
+ [170, 255, 76],
+ [173, 255, 73],
+ [176, 255, 70],
+ [179, 255, 66],
+ [183, 255, 63],
+ [186, 255, 60],
+ [189, 255, 57],
+ [192, 255, 54],
+ [195, 255, 50],
+ [199, 255, 47],
+ [202, 255, 44],
+ [205, 255, 41],
+ [208, 255, 37],
+ [212, 255, 34],
+ [215, 255, 31],
+ [218, 255, 28],
+ [221, 255, 24],
+ [224, 255, 21],
+ [228, 255, 18],
+ [231, 255, 15],
+ [234, 255, 12],
+ [237, 255, 8],
+ [241, 252, 5],
+ [244, 248, 2],
+ [247, 244, 0],
+ [250, 240, 0],
+ [254, 237, 0],
+ [255, 233, 0],
+ [255, 229, 0],
+ [255, 226, 0],
+ [255, 222, 0],
+ [255, 218, 0],
+ [255, 215, 0],
+ [255, 211, 0],
+ [255, 207, 0],
+ [255, 203, 0],
+ [255, 200, 0],
+ [255, 196, 0],
+ [255, 192, 0],
+ [255, 189, 0],
+ [255, 185, 0],
+ [255, 181, 0],
+ [255, 177, 0],
+ [255, 174, 0],
+ [255, 170, 0],
+ [255, 166, 0],
+ [255, 163, 0],
+ [255, 159, 0],
+ [255, 155, 0],
+ [255, 152, 0],
+ [255, 148, 0],
+ [255, 144, 0],
+ [255, 140, 0],
+ [255, 137, 0],
+ [255, 133, 0],
+ [255, 129, 0],
+ [255, 126, 0],
+ [255, 122, 0],
+ [255, 118, 0],
+ [255, 115, 0],
+ [255, 111, 0],
+ [255, 107, 0],
+ [255, 103, 0],
+ [255, 100, 0],
+ [255, 96, 0],
+ [255, 92, 0],
+ [255, 89, 0],
+ [255, 85, 0],
+ [255, 81, 0],
+ [255, 77, 0],
+ [255, 74, 0],
+ [255, 70, 0],
+ [255, 66, 0],
+ [255, 63, 0],
+ [255, 59, 0],
+ [255, 55, 0],
+ [255, 52, 0],
+ [255, 48, 0],
+ [255, 44, 0],
+ [255, 40, 0],
+ [255, 37, 0],
+ [255, 33, 0],
+ [255, 29, 0],
+ [255, 26, 0],
+ [255, 22, 0],
+ [254, 18, 0],
+ [250, 15, 0],
+ [245, 11, 0],
+ [241, 7, 0],
+ [236, 3, 0],
+ [232, 0, 0],
+ [227, 0, 0],
+ [222, 0, 0],
+ [218, 0, 0],
+ [213, 0, 0],
+ [209, 0, 0],
+ [204, 0, 0],
+ [200, 0, 0],
+ [195, 0, 0],
+ [191, 0, 0],
+ [186, 0, 0],
+ [182, 0, 0],
+ [177, 0, 0],
+ [172, 0, 0],
+ [168, 0, 0],
+ [163, 0, 0],
+ [159, 0, 0],
+ [154, 0, 0],
+ [150, 0, 0],
+ [145, 0, 0],
+ [141, 0, 0],
+ [136, 0, 0],
+ [132, 0, 0],
+ [127, 0, 0],
+];
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/image_manager/mod.rs b/tools/avm_analyzer/avm_analyzer_app/src/image_manager/mod.rs
new file mode 100644
index 0000000..3a6c013
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/image_manager/mod.rs
@@ -0,0 +1,292 @@
+mod color_maps;
+
+use std::collections::HashMap;
+// TODO(comc): Consider using egui mutex instead of std::sync.
+use std::sync::{Arc, Mutex};
+
+use avm_stats::{
+ calculate_heatmap, Frame, FrameError, Heatmap, HeatmapSettings, PixelPlane, PixelType, Plane, PlaneType,
+};
+pub use color_maps::JET_COLORMAP;
+use egui::{ColorImage, TextureHandle, TextureOptions};
+use itertools::{Itertools, MinMaxResult};
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+struct PixelPlaneKey {
+ frame_decode_index: usize,
+ plane: Plane,
+ pixel_type: PixelType,
+}
+
+impl PixelPlaneKey {
+ pub fn new(frame_decode_index: usize, plane: Plane, pixel_type: PixelType) -> Self {
+ Self {
+ frame_decode_index,
+ plane,
+ pixel_type,
+ }
+ }
+}
+
+/// Manages a collection of of different `PixelPlane` buffers.
+#[derive(Default)]
+pub struct PixelDataManager {
+ // Lazily populated with actual pixel data.
+ pixel_data: Mutex<HashMap<PixelPlaneKey, Result<Arc<PixelPlane>, FrameError>>>,
+}
+
+impl PixelDataManager {
+ pub fn get_or_create_pixels(
+ &self,
+ frame: &Frame,
+ plane: Plane,
+ pixel_type: PixelType,
+ ) -> Result<Arc<PixelPlane>, FrameError> {
+ let key = PixelPlaneKey::new(frame.decode_index(), plane, pixel_type);
+ self.pixel_data
+ .lock()
+ .unwrap()
+ .entry(key)
+ .or_insert_with(|| PixelPlane::create_from_frame(frame, key.plane, key.pixel_type).map(Arc::new))
+ .clone()
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct ImageType {
+ pub plane_type: PlaneType,
+ pub pixel_type: PixelType,
+ pub show_relative_delta: bool,
+ pub is_heatmap: bool,
+}
+
+impl ImageType {
+ pub fn new(plane_type: PlaneType, pixel_type: PixelType, show_relative_delta: bool, is_heatmap: bool) -> Self {
+ Self {
+ plane_type,
+ pixel_type,
+ show_relative_delta,
+ is_heatmap,
+ }
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+struct ImageKey {
+ frame_decode_index: usize,
+ image_type: ImageType,
+}
+
+impl ImageKey {
+ fn new(frame_decode_index: usize, image_type: ImageType) -> Self {
+ Self {
+ frame_decode_index,
+ image_type,
+ }
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+struct HeatmapKey {
+ frame_decode_index: usize,
+}
+
+impl HeatmapKey {
+ fn new(frame_decode_index: usize) -> Self {
+ Self { frame_decode_index }
+ }
+}
+
+#[derive(Clone)]
+struct StoredHeatmap {
+ heatmap: Heatmap,
+ texture_handle: TextureHandle,
+}
+
+// TODO(comc): Wipe existing images when a frame is unloaded.
+#[derive(Default)]
+pub struct ImageManager {
+ // Lazily populated with actual images.
+ images: Mutex<HashMap<ImageKey, Result<TextureHandle, FrameError>>>,
+ heatmaps: Mutex<HashMap<HeatmapKey, Result<StoredHeatmap, FrameError>>>,
+}
+
+#[allow(clippy::identity_op)]
+impl ImageManager {
+ fn image_from_heatmap(ctx: &egui::Context, heatmap: &mut Heatmap) -> TextureHandle {
+ let width = heatmap.width;
+ let height = heatmap.height;
+ let raw_rgb = heatmap
+ .data
+ .drain(..)
+ .flat_map(|x| JET_COLORMAP[x as usize])
+ .collect_vec();
+ let color_image = ColorImage::from_rgb([width, height], raw_rgb.as_slice());
+ ctx.load_texture("heatmap", color_image, TextureOptions::NEAREST)
+ }
+
+ fn image_from_yuv_planes(ctx: &egui::Context, planes: &[&PixelPlane]) -> TextureHandle {
+ let width = planes[0].width as usize;
+ let height = planes[0].height as usize;
+
+ let mut raw_rgb = vec![0; width * height * 3];
+ let raw_y = planes[0].pixels.as_slice();
+ let raw_u = planes[1].pixels.as_slice();
+ let raw_v = planes[2].pixels.as_slice();
+
+ for i in 0..height {
+ for j in 0..width {
+ let y = raw_y[i * width + j] as f32;
+ let u = raw_u[(i / 2) * (width / 2) + (j / 2)] as f32;
+ let v = raw_v[(i / 2) * (width / 2) + (j / 2)] as f32;
+
+ let is_8_bit = planes[0].bit_depth == 8;
+ let y = if is_8_bit { y } else { y / 4.0 };
+ let u = if is_8_bit { u - 128.0 } else { u / 4.0 - 128.0 };
+ let v = if is_8_bit { v - 128.0 } else { v / 4.0 - 128.0 };
+ let r = (y + 1.13983 * v) as u8;
+ let g = (y - 0.39465 * u - 0.58060 * v) as u8;
+ let b = (y + 2.03211 * u) as u8;
+
+ raw_rgb[i * width * 3 + j * 3 + 0] = r;
+ raw_rgb[i * width * 3 + j * 3 + 1] = g;
+ raw_rgb[i * width * 3 + j * 3 + 2] = b;
+ }
+ }
+ let color_image = ColorImage::from_rgb([width, height], raw_rgb.as_slice());
+ ctx.load_texture("yuv", color_image, TextureOptions::NEAREST)
+ }
+
+ fn image_from_single_plane(ctx: &egui::Context, plane: &PixelPlane, show_relative_delta: bool) -> TextureHandle {
+ let width = plane.width as usize;
+ let height = plane.height as usize;
+ let mut raw_rgb = vec![0; width * height * 3];
+ let mut min = -255;
+ let mut max = 255;
+ if show_relative_delta {
+ match plane.pixels.iter().minmax() {
+ MinMaxResult::NoElements | MinMaxResult::OneElement(_) => {}
+ MinMaxResult::MinMax(&min_v, &max_v) => {
+ min = min_v;
+ max = max_v;
+ }
+ }
+ }
+
+ let pixel_max = 1 << plane.bit_depth;
+ for i in 0..height {
+ for j in 0..width {
+ let mut sample = plane.pixels[i * width + j];
+ if plane.pixel_type.is_delta() {
+ if show_relative_delta {
+ let rel = (sample - min) as f32 / (max - min) as f32;
+ sample = (rel * 255.0) as i16;
+ } else {
+ sample = (sample + pixel_max - 1) / 2;
+ }
+ }
+ let sample = if plane.bit_depth == 8 || show_relative_delta {
+ sample
+ } else {
+ sample / 4
+ } as u8;
+ raw_rgb[i * width * 3 + j * 3 + 0] = sample;
+ raw_rgb[i * width * 3 + j * 3 + 1] = sample;
+ raw_rgb[i * width * 3 + j * 3 + 2] = sample;
+ }
+ }
+ let color_image = ColorImage::from_rgb([width, height], raw_rgb.as_slice());
+ ctx.load_texture("yuv", color_image, TextureOptions::NEAREST)
+ }
+
+ fn create_image(
+ ctx: &egui::Context,
+ pixel_manager: &PixelDataManager,
+ frame: &Frame,
+ image_type: ImageType,
+ ) -> Result<TextureHandle, FrameError> {
+ match image_type.plane_type {
+ PlaneType::Rgb => {
+ let planes: Vec<_> = (0..3)
+ .map(|i| pixel_manager.get_or_create_pixels(frame, Plane::from_i32(i), image_type.pixel_type))
+ .collect::<Result<_, _>>()?;
+
+ Ok(Self::image_from_yuv_planes(
+ ctx,
+ planes.iter().map(|p| p.as_ref()).collect::<Vec<_>>().as_slice(),
+ ))
+ }
+ PlaneType::Planar(plane) => {
+ let pixels = pixel_manager.get_or_create_pixels(frame, plane, image_type.pixel_type)?;
+ Ok(Self::image_from_single_plane(
+ ctx,
+ pixels.as_ref(),
+ image_type.show_relative_delta,
+ ))
+ }
+ }
+ }
+
+ fn create_heatmap(
+ ctx: &egui::Context,
+ frame: &Frame,
+ _image_type: ImageType,
+ heatmap_settings: &HeatmapSettings,
+ ) -> Result<StoredHeatmap, FrameError> {
+ let mut heatmap = calculate_heatmap(frame, heatmap_settings)?;
+ let texture_handle = Self::image_from_heatmap(ctx, &mut heatmap);
+ Ok(StoredHeatmap {
+ heatmap,
+ texture_handle,
+ })
+ }
+
+ pub fn get_or_create_image(
+ &self,
+ ctx: &egui::Context,
+ pixel_manager: &PixelDataManager,
+ frame: &Frame,
+ image_type: ImageType,
+ heatmap_settings: &HeatmapSettings,
+ ) -> Result<TextureHandle, FrameError> {
+ if image_type.is_heatmap {
+ let key = HeatmapKey::new(frame.decode_index());
+ let mut heatmaps = self.heatmaps.lock().unwrap();
+ heatmaps
+ .entry(key)
+ .or_insert_with(move || Self::create_heatmap(ctx, frame, image_type, heatmap_settings))
+ .as_ref()
+ .map(|h| h.texture_handle.clone())
+ .map_err(|err| err.clone())
+ } else {
+ let key = ImageKey::new(frame.decode_index(), image_type);
+ let mut images = self.images.lock().unwrap();
+ images
+ .entry(key)
+ .or_insert_with(move || Self::create_image(ctx, pixel_manager, frame, image_type))
+ .clone()
+ }
+ }
+
+ // TODO(comc): Refactor to not need clone.
+ pub fn get_or_create_heatmap(
+ &self,
+ ctx: &egui::Context,
+ frame: &Frame,
+ image_type: ImageType,
+ heatmap_settings: &HeatmapSettings,
+ ) -> Result<Heatmap, FrameError> {
+ let mut heatmaps = self.heatmaps.lock().unwrap();
+ let key = HeatmapKey::new(frame.decode_index());
+ heatmaps
+ .entry(key)
+ .or_insert_with(move || Self::create_heatmap(ctx, frame, image_type, heatmap_settings))
+ .as_ref()
+ .map(|h| h.heatmap.clone())
+ .map_err(|err| err.clone())
+ }
+
+ pub fn clear_heatmaps(&self) {
+ self.heatmaps.lock().unwrap().clear();
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/lib.rs b/tools/avm_analyzer/avm_analyzer_app/src/lib.rs
new file mode 100644
index 0000000..f6483f8
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/lib.rs
@@ -0,0 +1,8 @@
+mod app;
+mod app_state;
+mod image_manager;
+mod settings;
+mod stream;
+mod views;
+
+pub use app::AvmAnalyzerApp;
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/main.rs b/tools/avm_analyzer/avm_analyzer_app/src/main.rs
new file mode 100644
index 0000000..fb773cb
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/main.rs
@@ -0,0 +1,25 @@
+// TODO(comc): Remove non-wasm code paths.
+use re_memory::AccountingAllocator;
+
+#[global_allocator]
+static GLOBAL: AccountingAllocator<std::alloc::System> = AccountingAllocator::new(std::alloc::System);
+
+#[cfg(target_family = "wasm")]
+fn main() {
+ eframe::WebLogger::init(log::LevelFilter::Debug).ok();
+ let web_options = eframe::WebOptions::default();
+
+ wasm_bindgen_futures::spawn_local(async {
+ eframe::WebRunner::new()
+ .start(
+ "avm_analyzer_canvas_id",
+ web_options,
+ Box::new(|cc| {
+ egui_extras::install_image_loaders(&cc.egui_ctx);
+ Box::new(avm_analyzer_app::AvmAnalyzerApp::new(cc))
+ }),
+ )
+ .await
+ .expect("failed to start eframe");
+ });
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/settings.rs b/tools/avm_analyzer/avm_analyzer_app/src/settings.rs
new file mode 100644
index 0000000..f8e0884
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/settings.rs
@@ -0,0 +1,350 @@
+use avm_analyzer_common::AvmStreamInfo;
+use avm_stats::{FrameStatistic, HeatmapSettings, PlaneType, Sample, StatsSettings};
+use base64::{engine::general_purpose::URL_SAFE, Engine as _};
+use egui::{pos2, vec2, Color32, Pos2, Rect, Stroke};
+use log::info;
+use serde::{Deserialize, Serialize};
+use web_time::Instant;
+
+use crate::{
+ stream::{Stream, StreamSource},
+ views::{SelectedObject, SelectedObjectKind, ViewMode},
+};
+
+const GITLAB_ROOT: &str = "https://gitlab.com/AOMediaCodec/avm/-/blob/research-v6.0.0";
+const DEFAULT_STREAMS_URL: &str = "/streams";
+const DEFAULT_WORLD_BOUNDS_WIDTH: f32 = 1280.0;
+const DEFAULT_WORLD_BOUNDS_HEIGHT: f32 = 720.0;
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct OverlayStyle {
+ pub transform_unit_stroke: Stroke,
+ pub coding_unit_stroke: Stroke,
+ pub superblock_stroke: Stroke,
+ pub highlighted_object_stroke: Stroke,
+ pub selected_object_stroke: Stroke,
+ pub mode_name_color: Color32,
+ pub pixel_viewer_text_color: Color32,
+ pub enable_text_shadows: bool,
+}
+
+impl Default for OverlayStyle {
+ fn default() -> Self {
+ Self {
+ transform_unit_stroke: Stroke::new(1.0, Color32::DARK_RED),
+ coding_unit_stroke: Stroke::new(1.0, Color32::RED),
+ superblock_stroke: Stroke::new(1.0, Color32::BLUE),
+ highlighted_object_stroke: Stroke::new(4.0, Color32::LIGHT_YELLOW),
+ selected_object_stroke: Stroke::new(3.0, Color32::YELLOW),
+ mode_name_color: Color32::LIGHT_GREEN,
+ pixel_viewer_text_color: Color32::LIGHT_GREEN,
+ enable_text_shadows: true,
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct StyleSettings {
+ pub overlay: OverlayStyle,
+}
+#[allow(clippy::derivable_impls)]
+impl Default for StyleSettings {
+ fn default() -> Self {
+ Self {
+ overlay: OverlayStyle::default(),
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
+pub enum FrameSortOrder {
+ Decode,
+ Display,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct PersistentSettings {
+ pub avm_source_url: String,
+ pub style: StyleSettings,
+ pub apply_cache_strategy: bool,
+ pub cache_strategy_limit: usize,
+ pub update_sharable_url: bool,
+ pub frame_sort_order: FrameSortOrder,
+}
+
+impl Default for PersistentSettings {
+ fn default() -> Self {
+ Self {
+ avm_source_url: GITLAB_ROOT.into(),
+ style: StyleSettings::default(),
+ apply_cache_strategy: false,
+ cache_strategy_limit: 10,
+ update_sharable_url: false,
+ frame_sort_order: FrameSortOrder::Decode,
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
+pub enum CoeffViewSelect {
+ Quantized,
+ Dequantized,
+ DequantValue,
+}
+
+impl CoeffViewSelect {
+ pub fn name(&self) -> &str {
+ match self {
+ Self::Quantized => "Quantized Coefficients",
+ Self::Dequantized => "Dequantized Coefficients",
+ Self::DequantValue => "Inverse Quantization Matrix",
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
+pub enum MotionFieldColoring {
+ RefFrames,
+ PastFuture,
+ Monochrome,
+}
+
+impl MotionFieldColoring {
+ pub fn name(&self) -> &str {
+ match self {
+ Self::RefFrames => "Reference frames",
+ Self::PastFuture => "Past vs Future",
+ Self::Monochrome => "Monochrome",
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
+pub struct MotionFieldSettings {
+ pub show: bool,
+ pub show_origin: bool,
+ pub normalize: bool,
+ /// In 4x4 units.
+ pub granularity: usize,
+ pub auto_granularity: bool,
+ pub coloring: MotionFieldColoring,
+ pub scale: f32,
+}
+
+impl Default for MotionFieldSettings {
+ fn default() -> Self {
+ Self {
+ show: false,
+ show_origin: true,
+ normalize: true,
+ granularity: 1,
+ auto_granularity: true,
+ coloring: MotionFieldColoring::RefFrames,
+ scale: 1.0,
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
+pub enum DistortionView {
+ Original,
+ Reconstruction,
+ Distortion,
+}
+
+impl DistortionView {
+ pub fn name(&self) -> &str {
+ match self {
+ Self::Original => "Source (pre-encode)",
+ Self::Reconstruction => "Reconstruction",
+ Self::Distortion => "Distortion",
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
+pub struct SharableSettings {
+ pub view_mode: ViewMode,
+ pub current_plane: PlaneType,
+ pub show_relative_delta: bool,
+ pub show_overlay: bool,
+ pub show_yuv: bool,
+ pub motion_field: MotionFieldSettings,
+ pub selected_stat: FrameStatistic,
+ pub stats_settings: StatsSettings,
+ pub symbol_info_filter: String,
+ pub symbol_info_show_tags: bool,
+ pub world_bounds: Rect,
+ pub show_pixel_viewer: bool,
+ pub show_coeffs_viewer: bool,
+ pub pixel_viewer_bounds: Rect,
+ pub coeffs_viewer_bounds: Rect,
+ pub coeff_view_select: CoeffViewSelect,
+ pub show_heatmap_legend: bool,
+ pub heatmap_settings: HeatmapSettings,
+ pub heatmap_histogram_log_scale: bool,
+ pub show_remote_streams: bool,
+ pub streams_url: String,
+ // The following fields are used only during serialization. Otherwise their settings or state equivalent is used.
+ pub selected_object_kind: Option<SelectedObjectKind>,
+ pub selected_stream: Option<AvmStreamInfo>,
+ pub selected_frame: usize,
+}
+
+impl Default for SharableSettings {
+ fn default() -> Self {
+ Self {
+ current_plane: PlaneType::Rgb,
+ show_relative_delta: false,
+ show_overlay: true,
+ show_yuv: true,
+ motion_field: MotionFieldSettings::default(),
+ view_mode: ViewMode::CodingFlow,
+ stats_settings: StatsSettings::default(),
+ selected_stat: FrameStatistic::BlockSizes,
+ symbol_info_filter: "".into(),
+ symbol_info_show_tags: true,
+ world_bounds: Rect::from_min_size(
+ Pos2::ZERO,
+ vec2(DEFAULT_WORLD_BOUNDS_WIDTH, DEFAULT_WORLD_BOUNDS_HEIGHT),
+ ),
+ show_pixel_viewer: false,
+ show_coeffs_viewer: false,
+ pixel_viewer_bounds: Rect::from_min_max(pos2(0.0, 0.0), pos2(4.0, 4.0)),
+ coeffs_viewer_bounds: Rect::from_min_max(pos2(0.0, 0.0), pos2(4.0, 4.0)),
+ coeff_view_select: CoeffViewSelect::Dequantized,
+ show_heatmap_legend: true,
+ heatmap_settings: HeatmapSettings::default(),
+ heatmap_histogram_log_scale: false,
+ show_remote_streams: true,
+ streams_url: DEFAULT_STREAMS_URL.into(),
+ selected_object_kind: None,
+ selected_stream: None,
+ selected_frame: 0,
+ }
+ }
+}
+
+impl SharableSettings {
+ pub fn encode(&self) -> String {
+ let bytes = bincode::serialize(self).unwrap();
+ let compressed = weezl::encode::Encoder::new(weezl::BitOrder::Lsb, 8)
+ .encode(&bytes)
+ .unwrap();
+ URL_SAFE.encode(compressed)
+ }
+
+ pub fn decode(settings_str: Option<&str>) -> Option<Self> {
+ if let Some(settings_str) = settings_str {
+ if let Ok(bytes) = URL_SAFE.decode(settings_str) {
+ if let Ok(uncompressed) = weezl::decode::Decoder::new(weezl::BitOrder::Lsb, 8).decode(&bytes) {
+ if let Ok(sharable) = bincode::deserialize::<SharableSettings>(&uncompressed) {
+ return Some(sharable);
+ }
+ }
+ }
+ }
+ None
+ }
+}
+
+pub struct PlaybackSettings {
+ pub playback_fps: f32,
+ pub show_loaded_frames_only: bool,
+ pub playback_loop: bool,
+ pub playback_running: bool,
+ pub current_frame_display_instant: Instant,
+}
+
+impl Default for PlaybackSettings {
+ fn default() -> Self {
+ Self {
+ playback_fps: 30.0,
+ show_loaded_frames_only: true,
+ playback_loop: true,
+ playback_running: false,
+ current_frame_display_instant: Instant::now(),
+ }
+ }
+}
+
+pub struct Settings {
+ pub show_stream_select: bool,
+ pub show_decode_progress: bool,
+ pub scroll_to_index: Option<usize>,
+ pub cached_stat_data: Option<Vec<Sample>>,
+ pub show_performance_window: bool,
+ pub show_settings_window: bool,
+ pub persistent: PersistentSettings,
+ pub sharable: SharableSettings,
+ pub selected_object: Option<SelectedObject>,
+ /// The first object that was selected when double clicked to navigate up the hierarchy.
+ pub selected_object_leaf: Option<SelectedObjectKind>,
+ pub have_world_bounds_from_shared_settings: bool,
+ pub playback: PlaybackSettings,
+}
+
+#[allow(clippy::derivable_impls)]
+impl Default for Settings {
+ fn default() -> Self {
+ Self {
+ show_stream_select: false,
+ show_decode_progress: false,
+ scroll_to_index: None,
+ cached_stat_data: None,
+ show_performance_window: false,
+ show_settings_window: false,
+ persistent: PersistentSettings::default(),
+ sharable: SharableSettings::default(),
+ selected_object: None,
+ selected_object_leaf: None,
+ have_world_bounds_from_shared_settings: false,
+ playback: PlaybackSettings::default(),
+ }
+ }
+}
+
+impl Settings {
+ pub fn apply_saved_settings_string(&mut self, settings: Option<String>) {
+ if let Some(settings) = settings {
+ if let Ok(persistent) = serde_json::from_str::<PersistentSettings>(&settings) {
+ info!("Restoring saved settings: {persistent:?}");
+ self.persistent = persistent;
+ }
+ }
+ }
+
+ pub fn from_shared_settings_string(settings_str: Option<&str>) -> Self {
+ if let Some(sharable) = SharableSettings::decode(settings_str) {
+ info!("Loaded shared settings: {sharable:?}");
+ return Self {
+ sharable,
+ have_world_bounds_from_shared_settings: true,
+ ..Default::default()
+ };
+ }
+ Self::default()
+ }
+
+ pub fn create_shared_settings(&self, stream: &Option<Stream>) -> SharableSettings {
+ let mut shared = self.sharable.clone();
+ shared.selected_frame = 0;
+ shared.selected_stream = None;
+ shared.selected_object_kind = None;
+ if let Some(stream) = stream {
+ // Only save state for HTTP streams.
+ if matches!(stream.source, StreamSource::Http(_)) {
+ shared.selected_frame = stream.current_frame_index;
+ // TODO(comc): Can be simplified with serde skip.
+ shared.selected_stream = Some(AvmStreamInfo {
+ thumbnail_png: None,
+ ..stream.stream_info.clone()
+ });
+ if let Some(selected_object) = &self.selected_object {
+ shared.selected_object_kind = Some(selected_object.kind.clone());
+ }
+ }
+ }
+ shared
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/stream/http_stream_manager.rs b/tools/avm_analyzer/avm_analyzer_app/src/stream/http_stream_manager.rs
new file mode 100644
index 0000000..5aadb93
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/stream/http_stream_manager.rs
@@ -0,0 +1,51 @@
+use std::{
+ collections::HashMap,
+ sync::{Arc, Mutex},
+};
+
+use avm_analyzer_common::{AvmStreamInfo, AvmStreamList, DecodeState};
+use log::info;
+
+use super::server_decode::PendingServerDecode;
+
+pub struct HttpStreamManager {
+ pub streams: Arc<Mutex<Vec<AvmStreamInfo>>>,
+}
+
+impl HttpStreamManager {
+ pub fn new() -> Self {
+ Self {
+ streams: Arc::new(Mutex::new(Vec::new())),
+ }
+ }
+
+ pub fn load_stream_list(&self) {
+ let request = ehttp::Request::get("/stream_list");
+ let streams = self.streams.clone();
+ ehttp::fetch(request, move |response| {
+ if let Ok(response) = response {
+ if let Some(json) = response.text() {
+ if let Ok(response) = serde_json::from_str::<AvmStreamList>(json) {
+ info!("Found: {} existing streams on server.", response.streams.len());
+ let mut streams = streams.lock().unwrap();
+ *streams.as_mut() = response.streams;
+ }
+ }
+ }
+ });
+ }
+ pub fn update_pending_decodes(&mut self, pending_decodes: &HashMap<String, PendingServerDecode>) {
+ for pending_decode in pending_decodes.values() {
+ if let DecodeState::Complete(_) = pending_decode.state {
+ let streams = self.streams.lock().unwrap();
+ // TODO(comc): Make streams a hashmap instead.
+ let already_exists = streams
+ .iter()
+ .any(|stream| stream.stream_name == pending_decode.stream_info.stream_name);
+ if !already_exists {
+ self.load_stream_list();
+ }
+ }
+ }
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/stream/local_stream_manager.rs b/tools/avm_analyzer/avm_analyzer_app/src/stream/local_stream_manager.rs
new file mode 100644
index 0000000..83624f4
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/stream/local_stream_manager.rs
@@ -0,0 +1,119 @@
+use std::{
+ io::Cursor,
+ sync::{Arc, Mutex},
+};
+
+use anyhow::anyhow;
+use avm_analyzer_common::{
+ AvmStreamInfo, DEFAULT_PROTO_PATH_FRAME_SUFFIX_FIRST, DEFAULT_PROTO_PATH_FRAME_SUFFIX_TEMPLATE,
+};
+use egui::DroppedFile;
+use itertools::Itertools;
+use log::info;
+use rfd::AsyncFileDialog;
+use wasm_bindgen_futures::spawn_local;
+use zip::ZipArchive;
+
+use crate::stream::Stream;
+
+use super::stream_name_from_file_name;
+
+pub struct LocalStreamInfo {
+ pub file_name: String,
+ pub zip_bytes: Arc<[u8]>,
+}
+
+impl LocalStreamInfo {
+ pub fn get_stream_info(&self) -> anyhow::Result<AvmStreamInfo> {
+ let archive = ZipArchive::new(Cursor::new(self.zip_bytes.clone()))?;
+ let proto_file_names: Vec<_> = archive.file_names().filter(|n| n.ends_with(".pb")).sorted().collect();
+ let first_proto = proto_file_names
+ .first()
+ .ok_or(anyhow!("No protobufs (.pb) found in .zip archive."))?;
+ if !first_proto.ends_with(DEFAULT_PROTO_PATH_FRAME_SUFFIX_FIRST) {
+ return Err(anyhow!("Unexpected protobuf naming scheme: expected suffix: {DEFAULT_PROTO_PATH_FRAME_SUFFIX_FIRST}, actual name: {first_proto}"));
+ }
+ let proto_path_template = first_proto.replace(
+ DEFAULT_PROTO_PATH_FRAME_SUFFIX_FIRST,
+ DEFAULT_PROTO_PATH_FRAME_SUFFIX_TEMPLATE,
+ );
+
+ info!("Found {} frames from .zip stream.", proto_file_names.len());
+ let stream_info = AvmStreamInfo {
+ stream_name: self.file_name.clone(),
+ num_frames: proto_file_names.len(),
+ proto_path_template,
+ thumbnail_png: None,
+ };
+ for (i, proto_file_name) in proto_file_names.iter().enumerate() {
+ let expected = stream_info.get_proto_path(i);
+ if expected != **proto_file_name {
+ return Err(anyhow!(
+ "Unexpected protobuf in stream: Expected name: {expected}, actual name: {proto_file_name}"
+ ));
+ }
+ }
+
+ Ok(stream_info)
+ }
+}
+
+const DEMO_STREAM_BYTES: &[u8] = include_bytes!("../../assets/leo_qcif.zip");
+
+pub struct LocalStreamManager {
+ local_stream: Arc<Mutex<Option<LocalStreamInfo>>>,
+}
+
+impl LocalStreamManager {
+ pub fn new() -> Self {
+ Self {
+ local_stream: Arc::new(Mutex::new(None)),
+ }
+ }
+
+ pub fn load_demo_stream(&self) {
+ let demo_stream = Some(LocalStreamInfo {
+ file_name: "leo_qcif.ivf".into(),
+ zip_bytes: DEMO_STREAM_BYTES.into(),
+ });
+ *self.local_stream.lock().unwrap() = demo_stream;
+ }
+ /// Currently only .zip files are supported.
+ pub fn prompt_local_stream(&self) {
+ let task = AsyncFileDialog::new().add_filter("ZIP", &["zip"]).pick_file();
+ let local_stream = self.local_stream.clone();
+ spawn_local(async move {
+ let file = task.await;
+ if let Some(file) = file {
+ let bytes = file.read().await;
+ let mut local_stream = local_stream.lock().unwrap();
+ *local_stream = Some(LocalStreamInfo {
+ file_name: stream_name_from_file_name(&file.file_name()),
+ zip_bytes: bytes.into(),
+ });
+ }
+ });
+ }
+
+ pub fn handle_dropped_file(&self, file: DroppedFile) {
+ if let Some(bytes) = file.bytes {
+ let mut local_stream = self.local_stream.lock().unwrap();
+ *local_stream = Some(LocalStreamInfo {
+ file_name: file.name,
+ zip_bytes: bytes,
+ });
+ }
+ }
+
+ pub fn check_local_stream_ready(&self, stream: &mut Option<Stream>) -> anyhow::Result<()> {
+ if let Some(local_stream) = self.local_stream.lock().unwrap().take() {
+ info!(
+ "Loaded local stream {}: {} bytes",
+ local_stream.file_name,
+ local_stream.zip_bytes.len()
+ );
+ *stream = Some(Stream::from_local_file(local_stream)?);
+ }
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/stream/mod.rs b/tools/avm_analyzer/avm_analyzer_app/src/stream/mod.rs
new file mode 100644
index 0000000..fa4c643
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/stream/mod.rs
@@ -0,0 +1,625 @@
+mod http_stream_manager;
+mod local_stream_manager;
+mod server_decode;
+
+pub use http_stream_manager::*;
+use itertools::Itertools;
+pub use local_stream_manager::*;
+pub use server_decode::*;
+
+use std::collections::{HashMap, HashSet};
+use std::io::Cursor;
+use std::path::Path;
+use std::sync::Arc;
+use std::{io::Read, sync::Mutex};
+
+use anyhow::anyhow;
+use avm_analyzer_common::{AvmStreamInfo, DecodeState};
+use avm_stats::{Frame, Spatial};
+
+use log::{info, warn};
+use poll_promise::Promise;
+use prost::Message;
+
+use web_time::Instant;
+use zip::ZipArchive;
+
+use crate::image_manager::{ImageManager, PixelDataManager};
+use crate::settings::FrameSortOrder;
+use local_stream_manager::LocalStreamInfo;
+use server_decode::PendingServerDecode;
+
+fn frame_from_bytes(bytes: &[u8]) -> anyhow::Result<Box<Frame>> {
+ let start = Instant::now();
+ let frame = Frame::decode(bytes)?;
+ let duration = Instant::now() - start;
+ info!(
+ "Decoded frame {} in {:.2}ms: {}x{}, {} superblocks.",
+ frame.decode_index(),
+ duration.as_secs_f32() * 1000.0,
+ frame.width(),
+ frame.height(),
+ frame.superblocks.len(),
+ );
+ Ok(Box::new(frame))
+}
+
+pub struct LocalFileZipSource {
+ zip_bytes: Arc<[u8]>,
+}
+
+impl LocalFileZipSource {
+ fn new(zip_bytes: Arc<[u8]>) -> Self {
+ Self { zip_bytes }
+ }
+
+ fn load_frame(&self, index: usize, stream_info: &AvmStreamInfo) -> anyhow::Result<FrameStatus> {
+ let proto_path = stream_info.get_proto_path(index);
+ let mut archive = ZipArchive::new(Cursor::new(self.zip_bytes.clone()))?;
+ let proto_file = archive.by_name(&proto_path)?;
+ info!(
+ "Uncompressing {} bytes from {}.",
+ proto_file.compressed_size(),
+ proto_path
+ );
+ let start = Instant::now();
+ let proto_bytes = proto_file.bytes().collect::<Result<Vec<_>, _>>()?;
+ let duration = Instant::now() - start;
+ info!(
+ "Decompressed proto frame in {:.2}ms: {} bytes",
+ duration.as_secs_f32() * 1000.0,
+ proto_bytes.len()
+ );
+
+ let frame = frame_from_bytes(proto_bytes.as_slice())?;
+ Ok(FrameStatus::Loaded(frame))
+ }
+}
+
+pub struct HttpSource {
+ url: String,
+ decode_in_progress: bool,
+}
+
+impl HttpSource {
+ fn new(url: impl AsRef<str>, decode_in_progress: bool) -> Self {
+ Self {
+ url: url.as_ref().into(),
+ decode_in_progress,
+ }
+ }
+
+ fn load_frame(
+ &self,
+ index: usize,
+ stream_info: &AvmStreamInfo,
+ promises: &mut Vec<Promise<FramePromiseResult>>,
+ ) -> anyhow::Result<FrameStatus> {
+ let url = format!("{}/{}", self.url, stream_info.get_proto_path(index));
+ info!("Loading frame proto over HTTP: {url}");
+ let (sender, promise) = Promise::new();
+ let request = ehttp::Request::get(url);
+ ehttp::fetch(request, move |response| {
+ let frame = parse_proto_response(response);
+ sender.send(FramePromiseResult { frame, index });
+ });
+ promises.push(promise);
+ Ok(FrameStatus::Pending)
+ }
+}
+
+pub enum StreamSource {
+ LocalZipFile(LocalFileZipSource),
+ Http(HttpSource),
+}
+
+#[derive(Clone)]
+pub enum FrameStatus {
+ Unloaded,
+ Decoding,
+ Pending,
+ Invalid,
+ OutOfBounds,
+ Loaded(Box<Frame>),
+}
+
+impl FrameStatus {
+ fn is_selectable(&self) -> bool {
+ match self {
+ FrameStatus::Decoding | FrameStatus::Invalid | FrameStatus::OutOfBounds => false,
+ FrameStatus::Unloaded | FrameStatus::Pending | FrameStatus::Loaded(_) => true,
+ }
+ }
+ fn is_loaded(&self) -> bool {
+ matches!(self, FrameStatus::Loaded(_))
+ }
+}
+
+struct FramePromiseResult {
+ frame: Result<Option<Box<Frame>>, anyhow::Error>,
+ index: usize,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum CacheStrategy {
+ Unlimited,
+ Limited(usize),
+}
+
+impl CacheStrategy {
+ pub fn from_limit(limit: Option<usize>) -> Self {
+ match limit {
+ Some(limit) => Self::Limited(limit),
+ None => Self::Unlimited,
+ }
+ }
+ fn keep_frame(&self, frame_index: usize, frame_visit_history: &[usize]) -> bool {
+ match self {
+ Self::Unlimited => true,
+ Self::Limited(limit) => {
+ let mut prev_frames = HashSet::new();
+ for prev_frame in frame_visit_history.iter().rev() {
+ prev_frames.insert(prev_frame);
+ if prev_frames.len() == *limit {
+ break;
+ }
+ }
+ prev_frames.contains(&frame_index)
+ }
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum StreamEventType {
+ NewStream,
+ // Note: The first frame loaded is not necessarily the first frame of the stream.
+ FirstFrameLoaded(usize),
+ FrameChanged(usize),
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct StreamEvent {
+ pub event: StreamEventType,
+ lifetime: i32,
+}
+
+impl StreamEvent {
+ pub fn new(event: StreamEventType) -> Self {
+ Self { event, lifetime: 2 }
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+enum ChangeFrameKind {
+ Next,
+ Prev,
+ First,
+ Last,
+ Index(usize),
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct ChangeFrame {
+ order: FrameSortOrder,
+ kind: ChangeFrameKind,
+ loaded_only: bool,
+ allow_loop: bool,
+}
+
+impl Default for ChangeFrame {
+ fn default() -> Self {
+ Self {
+ order: FrameSortOrder::Decode,
+ kind: ChangeFrameKind::Index(0),
+ loaded_only: false,
+ allow_loop: false,
+ }
+ }
+}
+
+impl ChangeFrame {
+ pub fn next() -> Self {
+ Self {
+ kind: ChangeFrameKind::Next,
+ ..Default::default()
+ }
+ }
+
+ pub fn prev() -> Self {
+ Self {
+ kind: ChangeFrameKind::Prev,
+ ..Default::default()
+ }
+ }
+
+ pub fn first() -> Self {
+ Self {
+ kind: ChangeFrameKind::First,
+ ..Default::default()
+ }
+ }
+
+ pub fn last() -> Self {
+ Self {
+ kind: ChangeFrameKind::Last,
+ ..Default::default()
+ }
+ }
+ pub fn index(index: usize) -> Self {
+ Self {
+ kind: ChangeFrameKind::Index(index),
+ ..Default::default()
+ }
+ }
+ pub fn loaded_only(mut self, loaded_only: bool) -> Self {
+ self.loaded_only = loaded_only;
+ self
+ }
+
+ pub fn allow_loop(mut self, allow_loop: bool) -> Self {
+ self.allow_loop = allow_loop;
+ self
+ }
+
+ pub fn order(mut self, order: FrameSortOrder) -> Self {
+ self.order = order;
+ self
+ }
+}
+
+pub struct Stream {
+ pub stream_info: AvmStreamInfo,
+ pub source: StreamSource,
+ pub frames: Vec<FrameStatus>,
+ pub images: ImageManager,
+ pub pixel_data: PixelDataManager,
+ promises: Mutex<Vec<Promise<FramePromiseResult>>>,
+ // TODO(comc): Handle decode_index to proto frame index mapping.
+ pub current_frame_index: usize,
+ events: Vec<StreamEvent>,
+ have_first_frame: bool,
+ pub frame_visit_history: Vec<usize>,
+}
+
+impl Stream {
+ fn new(stream_info: AvmStreamInfo, source: StreamSource) -> Self {
+ let mut default_frame_status = FrameStatus::Unloaded;
+ if let StreamSource::Http(ref http_source) = source {
+ if http_source.decode_in_progress {
+ default_frame_status = FrameStatus::Decoding;
+ }
+ }
+ let num_frames = stream_info.num_frames;
+ Self {
+ stream_info,
+ source,
+ frames: vec![default_frame_status; num_frames],
+ promises: Mutex::new(Vec::new()),
+ images: ImageManager::default(),
+ pixel_data: PixelDataManager::default(),
+ current_frame_index: usize::MAX,
+ events: vec![StreamEvent::new(StreamEventType::NewStream)],
+ have_first_frame: false,
+ frame_visit_history: vec![0],
+ }
+ }
+
+ pub fn from_http(
+ stream_info: AvmStreamInfo,
+ decode_in_progress: bool,
+ first_first_to_load: usize,
+ streams_url: &str
+ ) -> anyhow::Result<Self> {
+ let source = StreamSource::Http(HttpSource::new(streams_url, decode_in_progress));
+ let mut stream = Stream::new(stream_info, source);
+ stream.set_current_frame(first_first_to_load, false);
+ Ok(stream)
+ }
+
+ pub fn from_local_file(local_stream: LocalStreamInfo) -> anyhow::Result<Self> {
+ let stream_info = local_stream.get_stream_info()?;
+ let source = StreamSource::LocalZipFile(LocalFileZipSource::new(local_stream.zip_bytes));
+ let mut stream = Stream::new(stream_info, source);
+ stream.set_current_frame(0, false);
+ Ok(stream)
+ }
+
+ // Add a method on Frame for this.
+ pub fn have_orig_yuv(&self) -> bool {
+ if let FrameStatus::Loaded(frame) = self.get_frame(0) {
+ if let Some(superblock) = &frame.superblocks.first() {
+ if let Some(pixel_data) = &superblock.pixel_data.first() {
+ if pixel_data.original.is_some() {
+ return true;
+ }
+ }
+ }
+ }
+ false
+ }
+
+ /// Looks up an order hint from a motion vector and converts it into a decode index.
+ pub fn lookup_order_hint(&self, order_hint: i32) -> Option<usize> {
+ for index in (0..self.current_frame_index).rev() {
+ if let FrameStatus::Loaded(frame) = self.get_frame(index) {
+ if frame.frame_params.as_ref().unwrap().raw_display_index == order_hint {
+ return Some(index);
+ }
+ }
+ }
+ None
+ }
+
+ pub fn update_pending_decodes(&mut self, pending_decodes: &mut HashMap<String, PendingServerDecode>) {
+ for pending_decode in pending_decodes.values_mut() {
+ let pending_decode_matches = {
+ if let StreamSource::Http(ref http_source) = self.source {
+ http_source.decode_in_progress
+ && pending_decode.stream_info.stream_name == self.stream_info.stream_name
+ } else {
+ false
+ }
+ };
+ let mut decode_finished = false;
+ if pending_decode_matches {
+ match pending_decode.state {
+ DecodeState::Complete(num_frames) => {
+ for i in 0..num_frames {
+ let frame = &mut self.frames[i];
+ if let FrameStatus::Decoding = frame {
+ *frame = FrameStatus::Unloaded
+ }
+ }
+ if num_frames > 0 && !pending_decode.started_loading_first_frame {
+ self.set_current_frame(0, false);
+ pending_decode.started_loading_first_frame = true;
+ }
+ decode_finished = true;
+ }
+ DecodeState::Pending(ref pending) => {
+ for i in 0..pending.decoded_frames {
+ let frame = &mut self.frames[i];
+ if let FrameStatus::Decoding = frame {
+ *frame = FrameStatus::Unloaded
+ }
+ }
+ if pending.decoded_frames > 0 && !pending_decode.started_loading_first_frame {
+ self.set_current_frame(0, false);
+ pending_decode.started_loading_first_frame = true;
+ }
+ }
+ DecodeState::Failed => {
+ for frame in self.frames.iter_mut() {
+ if let FrameStatus::Decoding = frame {
+ *frame = FrameStatus::Invalid
+ }
+ }
+ }
+ DecodeState::Uploading => {}
+ DecodeState::UploadComplete => {}
+ }
+ }
+ if decode_finished {
+ if let StreamSource::Http(http_source) = &mut self.source {
+ http_source.decode_in_progress = false;
+ }
+ }
+ }
+ }
+
+ pub fn current_frame(&self) -> Option<&Frame> {
+ if let FrameStatus::Loaded(ref frame) = self.get_frame(self.current_frame_index) {
+ Some(frame)
+ } else {
+ None
+ }
+ }
+
+ /// Sets the current frame to `index`.
+ /// If `loaded_only` is true, only changed the frame if it is already loaded.
+ /// Returns true if the frame was changed.
+ fn set_current_frame(&mut self, index: usize, loaded_only: bool) -> bool {
+ if index < self.num_frames()
+ && index != self.current_frame_index
+ && self.frames[index].is_selectable()
+ && (self.frames[index].is_loaded() || !loaded_only)
+ {
+ self.current_frame_index = index;
+ if let Err(err) = self.load_frame(self.current_frame_index) {
+ warn!("Loading frame {} failed: {err:?}.", self.current_frame_index);
+ }
+ self.events.push(StreamEvent::new(StreamEventType::FrameChanged(index)));
+ self.frame_visit_history.push(index);
+ true
+ } else {
+ false
+ }
+ }
+
+ pub fn apply_cache_strategy(&mut self, cache_strategy: CacheStrategy) {
+ for frame_index in 0..self.num_frames() {
+ if !cache_strategy.keep_frame(frame_index, &self.frame_visit_history) {
+ self.unload_frame(frame_index);
+ }
+ }
+ }
+
+ pub fn unload_frame(&mut self, index: usize) {
+ if matches!(self.frames[index], FrameStatus::Loaded(_)) {
+ self.frames[index] = FrameStatus::Unloaded;
+ }
+ }
+
+ pub fn change_frame(&mut self, change: ChangeFrame) -> bool {
+ let sorted_frames = self.get_sorted_frames(change.order);
+ let Some(current_index) = sorted_frames.iter().position(|&i| i == self.current_frame_index) else {
+ return false;
+ };
+
+ match change.kind {
+ ChangeFrameKind::First => self.set_current_frame(sorted_frames[0], change.loaded_only),
+ ChangeFrameKind::Last => self.set_current_frame(sorted_frames[sorted_frames.len() - 1], change.loaded_only),
+ ChangeFrameKind::Index(index) => {
+ if let Some(sorted_index) = sorted_frames.get(index) {
+ self.set_current_frame(*sorted_index, change.loaded_only)
+ } else {
+ false
+ }
+ }
+ ChangeFrameKind::Prev => {
+ if current_index > 0 {
+ self.set_current_frame(sorted_frames[current_index - 1], change.loaded_only)
+ } else {
+ false
+ }
+ }
+ ChangeFrameKind::Next => {
+ let next_index = if change.allow_loop {
+ (current_index + 1) % sorted_frames.len()
+ } else {
+ current_index + 1
+ };
+ if next_index < sorted_frames.len() {
+ // Try to change to the next frame. If it fails (e.g. because that frame is not yet loaded) and looping is on, try loading the first frame instead.
+ let frame_changed = self.set_current_frame(sorted_frames[next_index], change.loaded_only);
+ if !frame_changed && change.allow_loop {
+ self.set_current_frame(sorted_frames[0], change.loaded_only)
+ } else {
+ frame_changed
+ }
+ } else {
+ false
+ }
+ }
+ }
+ }
+
+ pub fn get_sorted_frames(&self, frame_sort_order: FrameSortOrder) -> Vec<usize> {
+ let sorted_indices = match frame_sort_order {
+ FrameSortOrder::Decode => (0..self.num_frames()).collect_vec(),
+ // TODO(comc): There's not really a good way to sort frames by display order
+ // ahead-of-time unless they're already loaded. For now, the decode index is used
+ // as the sort key if we don't have the display index yet.
+ FrameSortOrder::Display => self
+ .frames
+ .iter()
+ .enumerate()
+ .sorted_by_key(|(index, frame)| match frame {
+ FrameStatus::Loaded(frame) => (frame.display_index(), *index, true),
+ _ => (*index, *index, false),
+ })
+ .map(|(index, _)| index)
+ .collect_vec(),
+ };
+ sorted_indices
+ }
+
+ pub fn get_frame(&self, index: usize) -> &FrameStatus {
+ self.frames.get(index).unwrap_or(&FrameStatus::OutOfBounds)
+ }
+
+ pub fn load_frame(&mut self, index: usize) -> anyhow::Result<()> {
+ let do_load = matches!(self.frames.get(index), Some(FrameStatus::Unloaded));
+ if do_load {
+ self.frames[index] = match &self.source {
+ StreamSource::LocalZipFile(local) => {
+ let frame = local.load_frame(index, &self.stream_info)?;
+ if !self.have_first_frame {
+ self.have_first_frame = true;
+ self.events
+ .push(StreamEvent::new(StreamEventType::FirstFrameLoaded(index)));
+ }
+ frame
+ }
+ StreamSource::Http(http) => {
+ http.load_frame(index, &self.stream_info, &mut self.promises.lock().unwrap())?
+ }
+ };
+ }
+ Ok(())
+ }
+
+ pub fn num_frames(&self) -> usize {
+ self.stream_info.num_frames
+ }
+
+ pub fn check_promises(&mut self) {
+ let mut promises = self.promises.lock().unwrap();
+ promises.retain_mut(|promise| {
+ if let Some(result) = promise.ready_mut() {
+ let frame = result.frame.as_mut();
+ match frame {
+ Ok(frame) => {
+ self.frames[result.index] = FrameStatus::Loaded(frame.take().unwrap());
+ if !self.have_first_frame {
+ self.have_first_frame = true;
+ self.events
+ .push(StreamEvent::new(StreamEventType::FirstFrameLoaded(result.index)));
+ }
+ }
+ Err(_err) => {
+ self.frames[result.index] = FrameStatus::Invalid;
+ }
+ }
+ false
+ } else {
+ true
+ }
+ });
+ }
+
+ pub fn check_events(&mut self) -> Vec<StreamEvent> {
+ let events = self.events.clone();
+ for event in self.events.iter_mut() {
+ event.lifetime -= 1;
+ }
+ self.events.retain(|ev| ev.lifetime > 0);
+ events
+ }
+}
+
+/// Helper to check if we have a current frame on an Option<Stream>.
+pub trait CurrentFrame {
+ fn current_frame(&self) -> Option<&Frame>;
+ fn current_frame_is_loaded(&self) -> bool;
+}
+
+impl CurrentFrame for Option<Stream> {
+ fn current_frame(&self) -> Option<&Frame> {
+ let stream = self.as_ref()?;
+ stream.current_frame()
+ }
+ fn current_frame_is_loaded(&self) -> bool {
+ self.current_frame().is_some()
+ }
+}
+
+fn parse_proto_response(response: Result<ehttp::Response, String>) -> anyhow::Result<Option<Box<Frame>>> {
+ let response = response.map_err(|err| anyhow!("HTTP error: {err}"))?;
+ let content_type = response.content_type().unwrap_or_default();
+ let frame = match content_type {
+ "application/octet-stream" => frame_from_bytes(response.bytes.as_slice())?,
+ "application/zip" => {
+ let mut archive = ZipArchive::new(Cursor::new(response.bytes.as_slice()))?;
+ let proto_file_name = archive
+ .file_names()
+ .filter(|n| n.ends_with(".pb"))
+ .sorted()
+ .next()
+ .ok_or(anyhow!("No protobufs (.pb) found in .zip archive."))?
+ .to_string();
+ let proto_file = archive.by_name(&proto_file_name)?;
+ let proto_bytes = proto_file.bytes().collect::<Result<Vec<_>, _>>()?;
+ frame_from_bytes(proto_bytes.as_slice())?
+ }
+ _ => {
+ return Err(anyhow!("Unexpected content type: {content_type}"));
+ }
+ };
+ Ok(Some(frame))
+}
+
+pub fn stream_name_from_file_name(file_name: &str) -> String {
+ Path::new(file_name).file_stem().unwrap().to_string_lossy().to_string()
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/stream/server_decode.rs b/tools/avm_analyzer/avm_analyzer_app/src/stream/server_decode.rs
new file mode 100644
index 0000000..f539388
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/stream/server_decode.rs
@@ -0,0 +1,248 @@
+use avm_analyzer_common::{AvmStreamInfo, DecodeProgress, DecodeState, ProgressResponse, StartDecodeResponse};
+use egui::DroppedFile;
+
+use log::{info, warn};
+
+use rand::distributions::Alphanumeric;
+use rand::Rng;
+use rfd::AsyncFileDialog;
+use std::collections::HashMap;
+use std::io::{self, Cursor, Write};
+use std::sync::{Arc, Mutex};
+use wasm_bindgen_futures::spawn_local;
+use web_time::Instant;
+
+use crate::stream::{stream_name_from_file_name, Stream};
+
+use super::HttpStreamManager;
+pub const HTTP_POLL_PERIOD_SECONDS: f32 = 1.0;
+pub const MAX_RETRIES: i32 = 5;
+
+#[derive(Clone)]
+pub struct PendingServerDecode {
+ pub stream_info: AvmStreamInfo,
+ pub state: DecodeState,
+ pub start_time: Instant,
+ pub started_loading_first_frame: bool,
+ // For retries:
+ pub bytes: Arc<[u8]>,
+ pub retry_count: i32,
+}
+
+impl PendingServerDecode {
+ fn new(name: &str, state: DecodeState, bytes: Arc<[u8]>, retries_left: i32) -> Self {
+ let stream_info = AvmStreamInfo {
+ stream_name: name.into(),
+ proto_path_template: "".into(),
+ num_frames: 0,
+ thumbnail_png: None,
+ };
+ Self {
+ stream_info,
+ state,
+ start_time: Instant::now(),
+ started_loading_first_frame: false,
+ bytes,
+ retry_count: retries_left,
+ }
+ }
+}
+
+pub struct ServerDecodeManager {
+ pub pending_decodes: Arc<Mutex<HashMap<String, PendingServerDecode>>>,
+ last_check_request: Instant,
+}
+
+impl ServerDecodeManager {
+ pub fn new() -> Self {
+ Self {
+ pending_decodes: Arc::new(Mutex::new(HashMap::new())),
+ last_check_request: Instant::now(),
+ }
+ }
+ pub fn prompt_stream(&self) {
+ let task = AsyncFileDialog::new()
+ .add_filter("AVM Stream", &["bin", "ivf", "obu"])
+ .pick_file();
+ let pending_decodes = self.pending_decodes.clone();
+ spawn_local(async move {
+ let file = task.await;
+ if let Some(file) = file {
+ let bytes = file.read().await;
+ upload(file.file_name().as_str(), bytes.into(), pending_decodes, MAX_RETRIES);
+ }
+ });
+ }
+
+ pub fn handle_dropped_file(&self, file: DroppedFile) {
+ let pending_decodes = self.pending_decodes.clone();
+ if let Some(bytes) = file.bytes {
+ spawn_local(async move {
+ upload(file.name.as_str(), bytes, pending_decodes, MAX_RETRIES);
+ });
+ }
+ }
+
+ pub fn check_newly_confirmed_uploads(&mut self, stream: &mut Option<Stream>, streams_url: &str) {
+ let mut pending_decodes = self.pending_decodes.lock().unwrap();
+ for (_name, pending_decode) in pending_decodes.iter_mut() {
+ if let DecodeState::UploadComplete = pending_decode.state {
+ *stream = Some(Stream::from_http(pending_decode.stream_info.clone(), true, 0, streams_url).unwrap());
+ pending_decode.state = DecodeState::Pending(DecodeProgress {
+ decoded_frames: 0,
+ total_frames: pending_decode.stream_info.num_frames,
+ });
+ }
+ }
+ }
+ fn check_for_retries(&mut self) {
+ let mut retries = Vec::new();
+ {
+ let mut pending_decodes = self.pending_decodes.lock().unwrap();
+ for (_name, pending_decode) in pending_decodes.iter_mut() {
+ if matches!(pending_decode.state, DecodeState::Failed) {
+ retries.push(pending_decode.clone());
+ }
+ }
+ }
+ for retry in retries {
+ if retry.retry_count > 0 {
+ warn!("Retrying stream: {}. Retries remaining: {}", retry.stream_info.stream_name, retry.retry_count - 1);
+ upload(
+ &retry.stream_info.stream_name,
+ retry.bytes,
+ self.pending_decodes.clone(),
+ retry.retry_count - 1
+ );
+ }
+ else {
+ warn!("Stream: {} failed after {} retries.", retry.stream_info.stream_name, MAX_RETRIES);
+ }
+ }
+ }
+
+ pub fn check_progress(&mut self, stream: &mut Option<Stream>, http_manager: &mut HttpStreamManager, streams_url: &str) {
+ self.check_newly_confirmed_uploads(stream, streams_url);
+ self.check_for_retries();
+ let now = Instant::now();
+ let elapsed = now - self.last_check_request;
+ if elapsed.as_secs_f32() < HTTP_POLL_PERIOD_SECONDS {
+ return;
+ }
+ self.last_check_request = now;
+ let mut pending_decodes = self.pending_decodes.lock().unwrap();
+ for (_name, pending_decode) in pending_decodes.iter() {
+ if let DecodeState::Pending(_) = &pending_decode.state {
+ let request = ehttp::Request::get(format!(
+ "/progress?stream_name={}",
+ pending_decode.stream_info.stream_name
+ ));
+ let inner_pending_decodes = self.pending_decodes.clone();
+ ehttp::fetch(request, move |response| {
+ if let Ok(response) = response {
+ if let Some(json) = response.text() {
+ if let Ok(response) = serde_json::from_str::<ProgressResponse>(json) {
+ info!("Progress: {response:?}");
+ let mut pending_decodes = inner_pending_decodes.lock().unwrap();
+ if let Some(pending_decode) = pending_decodes.get_mut(&response.stream_name) {
+ pending_decode.state = response.state;
+ } else {
+ warn!(
+ "Received decode progress update for unknown stream: {}.",
+ response.stream_name
+ );
+ }
+ }
+ }
+ }
+ });
+ }
+ }
+ if let Some(stream) = stream {
+ // TODO(comc): Move this logic elsewhere.
+ http_manager.update_pending_decodes(&pending_decodes);
+ stream.update_pending_decodes(&mut pending_decodes);
+ }
+ }
+}
+
+const MULTIPART_FORM_BOUNDARY_PREFIX: &str = "--------";
+const MULTIPART_FORM_BOUNDARY_LEN: usize = 16;
+
+fn random_boundary() -> String {
+ let alphanum: String = rand::thread_rng()
+ .sample_iter(Alphanumeric)
+ .take(MULTIPART_FORM_BOUNDARY_LEN)
+ .map(char::from)
+ .collect();
+ format!("{MULTIPART_FORM_BOUNDARY_PREFIX}{alphanum}")
+}
+
+fn create_upload_request(file_name: &str, bytes: Arc<[u8]>) -> Result<ehttp::Request, io::Error> {
+ let stream_name = stream_name_from_file_name(file_name);
+ let mut request_body = Vec::new();
+ let mut cursor = Cursor::new(&mut request_body);
+ let boundary = random_boundary();
+ write!(cursor, "--{}\r\n", boundary)?;
+ write!(
+ cursor,
+ "Content-Disposition: form-data; name=\"{stream_name}\"; filename=\"{file_name}\""
+ )?;
+ write!(cursor, "\r\nContent-Type: {}", mime::APPLICATION_OCTET_STREAM)?;
+ write!(cursor, "\r\n\r\n")?;
+ cursor.write_all(&bytes)?;
+ write!(cursor, "\r\n")?;
+ write!(cursor, "--{}--\r\n", boundary)?;
+ let content_type = format!("multipart/form-data; boundary={}", boundary);
+
+ Ok(ehttp::Request {
+ method: "POST".into(),
+ url: "/upload".into(),
+ body: request_body,
+ headers: ehttp::headers(&[("Accept", "*/*"), ("Content-Type", &content_type)]),
+ })
+}
+
+// TODO(comc): Handle duplicate stream names.
+pub fn upload(file_name: &str, bytes: Arc<[u8]>, pending_decodes: Arc<Mutex<HashMap<String, PendingServerDecode>>>, retries_left: i32) {
+ let stream_name = stream_name_from_file_name(file_name);
+ {
+ let mut pending_decodes = pending_decodes.lock().unwrap();
+ if let Some(existing_decode) = pending_decodes.get(&stream_name) {
+ match existing_decode.state {
+ DecodeState::Pending(_) | DecodeState::UploadComplete | DecodeState::Uploading => {
+ warn!("Decode request for stream {stream_name} already pending.");
+ return;
+ }
+ _ => {}
+ }
+ }
+ let new_decode = PendingServerDecode::new(&stream_name, DecodeState::Uploading, bytes.clone(), retries_left);
+ pending_decodes.insert(stream_name.clone(), new_decode);
+ }
+ let req = create_upload_request(file_name, bytes).unwrap();
+ // TODO(comc): Automatic retry on timeout.
+ ehttp::fetch(req, move |response| {
+ let mut decode_state = DecodeState::Failed;
+ let mut updated_stream_info = None;
+
+ if let Ok(response) = response {
+ if let Some(json) = response.text() {
+ info!("Got response: {}", json);
+ if let Ok(response) = serde_json::from_str::<StartDecodeResponse>(json) {
+ updated_stream_info = Some(response.stream_info);
+ decode_state = DecodeState::UploadComplete;
+ }
+ }
+ }
+ let mut pending_decodes = pending_decodes.lock().unwrap();
+ if let Some(pending_decode) = pending_decodes.get_mut(&stream_name) {
+ pending_decode.state = decode_state;
+ if let Some(updated_stream_info) = updated_stream_info {
+ pending_decode.stream_info = updated_stream_info;
+ }
+ } else {
+ warn!("Received status response for {stream_name}, but it no longer exists.");
+ }
+ });
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/block_info_viewer.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/block_info_viewer.rs
new file mode 100644
index 0000000..979be7c
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/block_info_viewer.rs
@@ -0,0 +1,118 @@
+use anyhow::anyhow;
+use egui::{Button, RichText, Ui};
+use egui_extras::{Column, TableBuilder};
+use log::warn;
+
+use crate::views::stats_viewer::create_file_download;
+use crate::{app_state::AppState, stream::CurrentFrame};
+
+use crate::views::{RenderView, SelectedObject};
+
+use super::SelectedObjectKind;
+
+pub struct BlockInfoViewer;
+
+// TODO(comc): Click on a MV to load the corresponding frame and block.
+impl RenderView for BlockInfoViewer {
+ fn title(&self) -> String {
+ "Block Info".into()
+ }
+
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ let Some(frame) = state.stream.current_frame() else {
+ // No frame loaded yet.
+ return Ok(());
+ };
+ let Some(SelectedObject {
+ kind: selected_object_kind,
+ ..
+ }) = &state.settings.selected_object
+ else {
+ // Nothing is selected.
+ return Ok(());
+ };
+ let selected_object_kind = selected_object_kind.clone();
+ let is_transform_unit = matches!(selected_object_kind, SelectedObjectKind::TransformUnit(_));
+ let is_superblock = matches!(selected_object_kind, SelectedObjectKind::Superblock(_));
+ if ui.add_enabled(!is_superblock, Button::new("Go to parent")).clicked() {
+ if let Some(parent) = selected_object_kind.get_parent(frame) {
+ state.settings.selected_object = Some(SelectedObject::new(parent));
+ }
+ }
+ ui.end_row();
+
+ if ui.button("Show pixels for current block").clicked() {
+ state.settings.sharable.show_pixel_viewer = true;
+ }
+ ui.end_row();
+
+ if let SelectedObjectKind::CodingUnit(cu) = selected_object_kind {
+ let ctx = cu.try_resolve(frame).ok_or(anyhow!("Invalid coding unit index"))?;
+ if ui.button("Dump current block as JSON").clicked() {
+ if let Some(stream) = &state.stream {
+ let Some(rect) = selected_object_kind.rect(frame) else {
+ return Err(anyhow!("Invalid coding unit index"));
+ };
+ let file_name = format!(
+ "{}_frame_{:04}_block_{}x{}_x{}_y{}.json",
+ stream.stream_info.stream_name,
+ stream.current_frame_index,
+ rect.width() as i32,
+ rect.height() as i32,
+ rect.left_top().x as i32,
+ rect.left_top().y as i32,
+ );
+
+ let data = serde_json::to_string_pretty(ctx.coding_unit).unwrap();
+
+ if let Err(err) = create_file_download(data.as_bytes(), &file_name) {
+ warn!("Failed to create file download: {err:?}");
+ }
+ }
+ }
+ }
+
+ // TODO(comc): Grey out instead of removing?
+ if is_transform_unit && ui.button("Show transform coeffs").clicked() {
+ state.settings.sharable.show_coeffs_viewer = true;
+ }
+
+ ui.separator();
+
+ let Ok(info) = state
+ .settings
+ .selected_object
+ .as_mut()
+ .unwrap()
+ .get_or_calculate_info(frame)
+ else {
+ return Ok(());
+ };
+
+ TableBuilder::new(ui)
+ .column(Column::initial(300.0).resizable(true))
+ .column(Column::remainder())
+ .striped(true)
+ .header(20.0, |mut header| {
+ header.col(|ui| {
+ ui.label(RichText::new("Property").strong());
+ });
+ header.col(|ui| {
+ ui.label(RichText::new("Value").strong());
+ });
+ })
+ .body(|mut body| {
+ for field in info.fields.iter() {
+ body.row(20.0, |mut row| {
+ row.col(|col| {
+ col.label(&field.name);
+ });
+ row.col(|col| {
+ col.label(&field.value);
+ });
+ });
+ }
+ });
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/coeffs_viewer.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/coeffs_viewer.rs
new file mode 100644
index 0000000..db1df64
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/coeffs_viewer.rs
@@ -0,0 +1,189 @@
+use avm_stats::MAX_COEFFS_SIZE;
+
+use anyhow::anyhow;
+use egui::{pos2, vec2, Align2, Color32, PointerButton, Rect, RichText, Rounding, Shape, Stroke, TextStyle, Ui, Vec2};
+use itertools::{Itertools, MinMaxResult};
+
+use crate::app_state::AppState;
+use crate::settings::CoeffViewSelect;
+use crate::views::RenderView;
+use crate::views::{ScreenBounds, SelectedObjectKind};
+use crate::views::{MIN_BLOCK_HEIGHT_FOR_TEXT, MIN_BLOCK_WIDTH_FOR_TEXT};
+
+pub struct CoeffsViewer;
+
+impl RenderView for CoeffsViewer {
+ fn title(&self) -> String {
+ "Coeffs View".into()
+ }
+
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ let Some(stream) = &state.stream else {
+ return Ok(());
+ };
+
+ let Some(frame) = stream.current_frame() else {
+ return Ok(());
+ };
+
+ let Some(selected_object) = &state.settings.selected_object else {
+ return Ok(());
+ };
+
+ let SelectedObjectKind::TransformUnit(transform_unit_index) = selected_object.kind.clone() else {
+ return Ok(());
+ };
+
+ let transform_unit_ctx = transform_unit_index
+ .try_resolve(frame)
+ .ok_or(anyhow!("Invalid transform unit index"))?;
+ let transform_unit = transform_unit_ctx.transform_unit;
+
+ let coeff_view_options = [
+ CoeffViewSelect::Dequantized,
+ CoeffViewSelect::Quantized,
+ CoeffViewSelect::DequantValue,
+ ];
+
+ ui.label(RichText::new("WARNING: Coeff data extraction is currently buggy.").color(Color32::RED));
+ ui.end_row();
+
+ egui::ComboBox::from_label("Coeff view")
+ .selected_text(state.settings.sharable.coeff_view_select.name())
+ .show_ui(ui, |ui| {
+ for coeff_option in coeff_view_options.iter() {
+ ui.selectable_value(
+ &mut state.settings.sharable.coeff_view_select,
+ *coeff_option,
+ coeff_option.name(),
+ );
+ }
+ });
+ ui.end_row();
+
+ let Some(mut object_rect) = selected_object.rect(frame) else {
+ return Err(anyhow!("Invalid selected object"));
+ };
+ if state.settings.sharable.current_plane.use_chroma() {
+ object_rect = object_rect * 0.5;
+ }
+ let size = ui.available_size_before_wrap();
+ let (screen_bounds, response) = ui.allocate_exact_size(size, egui::Sense::drag());
+ let mut world_bounds = state.settings.sharable.coeffs_viewer_bounds;
+ let scale = world_bounds.calc_scale(screen_bounds);
+ let mut hover_pos_world = None;
+
+ // TODO(comc): Factor out this common code with frame_viewer and detailed_pixel_viewer.
+ if response.dragged_by(PointerButton::Primary) {
+ let delta = response.drag_delta() / -scale;
+ world_bounds = world_bounds.translate(delta);
+ } else if response.hover_pos().is_some() && ui.input(|i| i.scroll_delta) != Vec2::ZERO {
+ let delta = ui.input(|i| i.scroll_delta);
+
+ if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) {
+ if screen_bounds.contains(mouse_pos) {
+ let zoom = if delta.y < 0.0 {
+ f32::powf(1.001, -delta.y)
+ } else {
+ 1.0 / (f32::powf(1.001, delta.y))
+ };
+ let zoom_center = world_bounds.screen_pos_to_world(mouse_pos, screen_bounds);
+ world_bounds.zoom_point(zoom_center, zoom);
+ }
+ }
+ } else if response.hover_pos().is_some() {
+ if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) {
+ hover_pos_world = Some(world_bounds.screen_pos_to_world(mouse_pos, screen_bounds));
+ }
+ }
+ state.settings.sharable.coeffs_viewer_bounds = world_bounds;
+
+ let painter = ui.painter().with_clip_rect(response.rect);
+ let clip_rect = painter.clip_rect();
+ let mut shapes = Vec::new();
+ let ui_style = ui.ctx().style();
+ let mut hover_text = None;
+ let mut min_coeff = 0;
+ let mut max_coeff = 255;
+ let coeff_view = match state.settings.sharable.coeff_view_select {
+ CoeffViewSelect::DequantValue => &transform_unit.dequantizer_values,
+ CoeffViewSelect::Dequantized => &transform_unit.dequantized_coeffs,
+ CoeffViewSelect::Quantized => &transform_unit.quantized_coeffs,
+ };
+ match coeff_view.iter().minmax() {
+ MinMaxResult::NoElements | MinMaxResult::OneElement(_) => {}
+ MinMaxResult::MinMax(&min_v, &max_v) => {
+ min_coeff = min_v;
+ max_coeff = max_v;
+ }
+ };
+
+ // TODO(comc): Manually doing an inverse DCT does not yield the expected pixels for some blocks. Verify that the Residual = (PreFiltered - Prediction) logic makes sense.
+ let coeffs_width = MAX_COEFFS_SIZE.min(object_rect.width() as usize);
+ let coeffs_height = MAX_COEFFS_SIZE.min(object_rect.height() as usize);
+
+ for y in 0..coeffs_height {
+ for x in 0..coeffs_width {
+ let stride = coeffs_width;
+ let index = y * stride + x;
+
+ let coeff = coeff_view.get(index).copied();
+ let have_coeff = coeff.is_some();
+ let coeff = coeff.unwrap_or_default();
+ let color = (coeff - min_coeff) as f32 / (max_coeff - min_coeff) as f32;
+ let world_rect = Rect::from_min_size(pos2(x as f32, y as f32), vec2(1.0, 1.0));
+ let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
+ if let Some(hover_pos_world) = hover_pos_world {
+ if world_rect.contains(hover_pos_world) && have_coeff {
+ hover_text = Some(format!("Coeff={} (x={}, y={})", coeff, x, y));
+ }
+ }
+
+ shapes.push(Shape::rect_filled(
+ screen_rect,
+ Rounding::ZERO,
+ Color32::from_gray((color * 255.0) as u8),
+ ));
+ shapes.push(Shape::rect_stroke(
+ screen_rect,
+ Rounding::ZERO,
+ Stroke::new(1.0, Color32::from_gray(20)),
+ ));
+
+ if have_coeff
+ && screen_rect.height() >= MIN_BLOCK_HEIGHT_FOR_TEXT
+ && screen_rect.width() >= MIN_BLOCK_WIDTH_FOR_TEXT
+ {
+ let overlay_style = &state.settings.persistent.style.overlay;
+ shapes.extend(painter.fonts(|f| {
+ let colors = if overlay_style.enable_text_shadows {
+ vec![Color32::BLACK, overlay_style.pixel_viewer_text_color]
+ } else {
+ vec![overlay_style.pixel_viewer_text_color]
+ };
+ colors
+ .into_iter()
+ .enumerate()
+ .map(|(i, color)| {
+ let pos_offset = vec2(i as f32, i as f32);
+ Shape::text(
+ f,
+ screen_rect.center() + pos_offset,
+ Align2::CENTER_CENTER,
+ format!("{coeff}"),
+ TextStyle::Body.resolve(&ui_style),
+ color,
+ )
+ })
+ .collect::<Vec<_>>()
+ }));
+ }
+ }
+ }
+ painter.extend(shapes);
+ if let Some(hover_text) = hover_text {
+ response.on_hover_text_at_pointer(hover_text);
+ }
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/controls_viewer.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/controls_viewer.rs
new file mode 100644
index 0000000..844cb4c
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/controls_viewer.rs
@@ -0,0 +1,253 @@
+use avm_stats::{Plane, PlaneType, Spatial};
+use egui::{vec2, Button, Checkbox, RichText, Ui};
+
+use crate::app_state::AppState;
+use crate::settings::{DistortionView, FrameSortOrder, MotionFieldColoring};
+use crate::stream::{ChangeFrame, CurrentFrame};
+use crate::views::RenderView;
+
+use super::ViewMode;
+
+const PLAYBACK_BUTTON_HEIGHT: f32 = 30.0;
+const PLAYBACK_BUTTON_WIDTH: f32 = 30.0;
+const PLAYBACK_BUTTON_FONT_SIZE: f32 = 18.0;
+
+fn create_playback_button(icon: &str) -> Button {
+ Button::new(RichText::new(format!(" {icon} ")).size(PLAYBACK_BUTTON_FONT_SIZE))
+ .min_size(vec2(PLAYBACK_BUTTON_WIDTH, PLAYBACK_BUTTON_HEIGHT))
+}
+
+pub struct ControlsViewer;
+
+impl RenderView for ControlsViewer {
+ fn title(&self) -> String {
+ "Controls".into()
+ }
+
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ ui.horizontal(|ui| {
+ ui.vertical(|ui| {
+ ui.label("View");
+ let frame_sort_options = [FrameSortOrder::Decode, FrameSortOrder::Display];
+ egui::ComboBox::from_label("Frame sort order")
+ .selected_text(format!("{:?}", state.settings.persistent.frame_sort_order))
+ .show_ui(ui, |ui| {
+ for &sort_option in frame_sort_options.iter() {
+ ui.selectable_value(
+ &mut state.settings.persistent.frame_sort_order,
+ sort_option,
+ format!("{sort_option:?}"),
+ );
+ }
+ });
+ ui.end_row();
+
+ let plane_options = [
+ PlaneType::Planar(Plane::Y),
+ PlaneType::Planar(Plane::U),
+ PlaneType::Planar(Plane::V),
+ PlaneType::Rgb,
+ ];
+
+ egui::ComboBox::from_label("Plane view")
+ .selected_text(format!("{}", state.settings.sharable.current_plane))
+ .show_ui(ui, |ui| {
+ for &plane_option in plane_options.iter() {
+ ui.selectable_value(
+ &mut state.settings.sharable.current_plane,
+ plane_option,
+ format!("{plane_option}"),
+ );
+ }
+ });
+ ui.end_row();
+
+ ui.add_enabled(
+ state.settings.sharable.view_mode.view_settings().pixel_type.is_delta(),
+ egui::Checkbox::new(&mut state.settings.sharable.show_relative_delta, "Show relative delta"),
+ );
+
+ ui.end_row();
+
+ ui.checkbox(&mut state.settings.sharable.show_overlay, "Show overlay");
+
+ ui.end_row();
+
+ ui.checkbox(&mut state.settings.sharable.show_yuv, "Show YUV");
+
+ ui.end_row();
+
+ if ui.button("Reset Zoom").clicked() {
+ if let Some(frame) = state.stream.current_frame() {
+ state.settings.sharable.world_bounds = frame.rect();
+ }
+ }
+ });
+ if let Some(stream) = &mut state.stream {
+ ui.separator();
+ ui.vertical(|ui| {
+ ui.label("Playback");
+ ui.horizontal(|ui| {
+ let frame_sort_order: FrameSortOrder = state.settings.persistent.frame_sort_order;
+ if ui
+ .add(create_playback_button("⏮"))
+ .on_hover_text("Go to first frame")
+ .clicked()
+ {
+ stream.change_frame(ChangeFrame::first().order(frame_sort_order));
+ }
+ if ui
+ .add(create_playback_button("⏪"))
+ .on_hover_text("Go to previous frame")
+ .clicked()
+ {
+ stream.change_frame(ChangeFrame::prev().order(frame_sort_order));
+ }
+ let play_pause = if state.settings.playback.playback_running {
+ "⏸"
+ } else {
+ "▶"
+ };
+ if ui
+ .add(create_playback_button(play_pause))
+ .on_hover_text("Start/stop playback")
+ .clicked()
+ {
+ state.settings.playback.playback_running = !state.settings.playback.playback_running;
+ }
+
+ if ui
+ .add(create_playback_button("⏩"))
+ .on_hover_text("Go to next frame")
+ .clicked()
+ {
+ stream.change_frame(ChangeFrame::next().order(frame_sort_order));
+ }
+ if ui
+ .add(create_playback_button("⏭"))
+ .on_hover_text("Go to last frame")
+ .clicked()
+ {
+ stream.change_frame(ChangeFrame::last().order(frame_sort_order));
+ }
+ });
+ // TODO(comc): Re-enable after adding pause logic for pending loads.
+ // ui.checkbox(
+ // &mut state.settings.playback.show_loaded_frames_only,
+ // "Playback loaded frames only",
+ // );
+ ui.checkbox(&mut state.settings.playback.playback_loop, "Loop playback");
+ ui.label("Playback FPS:");
+ ui.add(egui::Slider::new(&mut state.settings.playback.playback_fps, 1.0..=60.0).step_by(1.0));
+ });
+ }
+
+ if state.settings.sharable.view_mode == ViewMode::Heatmap {
+ ui.separator();
+ ui.vertical(|ui| {
+ ui.label("Heatmap");
+ ui.checkbox(&mut state.settings.sharable.show_heatmap_legend, "Show Heatmap Legend");
+ ui.end_row();
+ ui.label("Histogram buckets");
+ ui.add(egui::Slider::new(
+ &mut state.settings.sharable.heatmap_settings.histogram_buckets,
+ 4..=100,
+ ));
+ ui.end_row();
+ ui.checkbox(
+ &mut state.settings.sharable.heatmap_histogram_log_scale,
+ "Histogram log scale",
+ );
+ ui.end_row();
+ ui.label("Symbol filter");
+ ui.text_edit_singleline(&mut state.settings.sharable.heatmap_settings.symbol_filter);
+ if ui.button("Recalculate heatmap").clicked() {
+ if let Some(stream) = &state.stream {
+ stream.images.clear_heatmaps();
+ }
+ }
+ });
+ }
+
+ if let ViewMode::Distortion(mut distortion_view) = state.settings.sharable.view_mode {
+ ui.separator();
+ ui.vertical(|ui| {
+ let distortion_view_options = [
+ DistortionView::Distortion,
+ DistortionView::Original,
+ DistortionView::Reconstruction,
+ ];
+ egui::ComboBox::from_label("Displayed pixels")
+ .selected_text(distortion_view.name())
+ .show_ui(ui, |ui| {
+ for &distortion_view_option in distortion_view_options.iter() {
+ ui.selectable_value(
+ &mut distortion_view,
+ distortion_view_option,
+ distortion_view_option.name(),
+ );
+ }
+ });
+ ui.end_row();
+ });
+ state.settings.sharable.view_mode = ViewMode::Distortion(distortion_view);
+ }
+
+ if state.settings.sharable.view_mode == ViewMode::Motion {
+ ui.separator();
+ ui.vertical(|ui| {
+ ui.label("Motion");
+ ui.checkbox(&mut state.settings.sharable.motion_field.show, "Show motion field");
+ ui.end_row();
+ ui.checkbox(
+ &mut state.settings.sharable.motion_field.show_origin,
+ "Show motion vector origin",
+ );
+ ui.end_row();
+ ui.add_enabled(
+ state.settings.sharable.motion_field.show,
+ Checkbox::new(
+ &mut state.settings.sharable.motion_field.normalize,
+ "Normalize vector lengths",
+ ),
+ );
+ ui.end_row();
+ ui.label("Motion vector scale:");
+ ui.add(egui::Slider::new(&mut state.settings.sharable.motion_field.scale, 0.1..=10.0).step_by(0.1));
+
+ let coloring_options = [
+ MotionFieldColoring::RefFrames,
+ MotionFieldColoring::PastFuture,
+ MotionFieldColoring::Monochrome,
+ ];
+ egui::ComboBox::from_label("Coloring scheme")
+ .selected_text(state.settings.sharable.motion_field.coloring.name())
+ .show_ui(ui, |ui| {
+ for &coloring_option in coloring_options.iter() {
+ ui.selectable_value(
+ &mut state.settings.sharable.motion_field.coloring,
+ coloring_option,
+ coloring_option.name(),
+ );
+ }
+ });
+ ui.end_row();
+
+ ui.label("Granularity (in 4x4 units):");
+ ui.checkbox(&mut state.settings.sharable.motion_field.auto_granularity, "Automatic");
+ ui.add_enabled(
+ !state.settings.sharable.motion_field.auto_granularity,
+ egui::Slider::new(&mut state.settings.sharable.motion_field.granularity, 1..=32),
+ );
+ if let Some(frame) = state.stream.current_frame() {
+ if state.settings.sharable.motion_field.auto_granularity {
+ state.settings.sharable.motion_field.granularity = frame.height() as usize / 64;
+ }
+ }
+ ui.end_row();
+ });
+ }
+ });
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/decode_progress_viewer.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/decode_progress_viewer.rs
new file mode 100644
index 0000000..ddbe354
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/decode_progress_viewer.rs
@@ -0,0 +1,77 @@
+use avm_analyzer_common::DecodeState;
+use egui::{Color32, ProgressBar, Ui};
+use egui_extras::{Column, TableBuilder};
+use itertools::Itertools;
+
+use crate::app_state::AppState;
+use crate::stream::Stream;
+use crate::views::render_view::RenderView;
+
+pub struct DecodeProgressViewer;
+
+impl RenderView for DecodeProgressViewer {
+ fn title(&self) -> String {
+ "Decode Progress".into()
+ }
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ let mut pending_decodes = state.server_decode_manager.pending_decodes.lock().unwrap();
+ if ui.button("Clear").clicked() {
+ pending_decodes.retain(|_k, v| !matches!(v.state, DecodeState::Complete(_) | DecodeState::Failed));
+ }
+ TableBuilder::new(ui)
+ .column(Column::initial(200.0).resizable(true))
+ .column(Column::initial(200.0).resizable(true))
+ .striped(true)
+ .header(20.0, |mut header| {
+ header.col(|ui| {
+ ui.heading("Stream");
+ });
+ header.col(|ui| {
+ ui.heading("Status");
+ });
+ })
+ .body(|body| {
+ let sorted_pending_decodes: Vec<_> = pending_decodes.values().sorted_by_key(|p| p.start_time).collect();
+ let num_rows: usize = sorted_pending_decodes.len();
+ body.rows(20.0, num_rows, |mut row| {
+ let stream_info = &sorted_pending_decodes[row.index()].stream_info;
+ let stream_name = &stream_info.stream_name;
+ let decode_state = &sorted_pending_decodes[row.index()].state;
+ let progress_bar = match decode_state {
+ DecodeState::Complete(_) => ProgressBar::new(1.0).fill(Color32::LIGHT_GREEN).text("Finished"),
+ DecodeState::Failed => ProgressBar::new(1.0).fill(Color32::RED).text("FAILED"),
+ DecodeState::Pending(progress) => {
+ let percent = progress.decoded_frames as f32 / progress.total_frames as f32;
+ let text = format!(
+ "{}/{} ({:.0}%)",
+ progress.decoded_frames,
+ progress.total_frames,
+ percent * 100.0
+ );
+ ProgressBar::new(percent).text(text).animate(true)
+ }
+ DecodeState::UploadComplete => {
+ let text = format!("0/{} (0%)", stream_info.num_frames);
+ ProgressBar::new(1.0).text(text).animate(true)
+ }
+ DecodeState::Uploading => ProgressBar::new(1.0).fill(Color32::GRAY).text("Uploading"),
+ };
+ row.col(|ui| {
+ ui.horizontal(|ui| {
+ // TODO(comc): We could load pending streams too, as stream_select_viewer allows.
+ let ready_to_load = matches!(decode_state, DecodeState::Complete(_));
+ if ui.add_enabled(ready_to_load, egui::Button::new("Load")).clicked() {
+ state.stream = Some(Stream::from_http(stream_info.clone(), false, 0, &state.settings.sharable.streams_url).unwrap());
+ }
+
+ ui.label(stream_name);
+ });
+ });
+ row.col(|ui| {
+ ui.add(progress_bar);
+ });
+ });
+ });
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/detailed_pixel_viewer.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/detailed_pixel_viewer.rs
new file mode 100644
index 0000000..ba3c273
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/detailed_pixel_viewer.rs
@@ -0,0 +1,169 @@
+use anyhow::anyhow;
+use egui::{pos2, vec2, Align2, Color32, PointerButton, Rect, Rounding, Shape, Stroke, TextStyle, Ui, Vec2};
+
+use crate::app_state::AppState;
+use crate::views::RenderView;
+use crate::views::ScreenBounds;
+use crate::views::{MIN_BLOCK_HEIGHT_FOR_TEXT, MIN_BLOCK_WIDTH_FOR_TEXT};
+
+pub const MIN_BLOCK_SIZE_FOR_GRID: f32 = 4.0;
+pub struct DetailedPixelViewer;
+
+impl RenderView for DetailedPixelViewer {
+ fn title(&self) -> String {
+ "Pixel View".into()
+ }
+
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ let Some(stream) = &state.stream else {
+ return Ok(());
+ };
+
+ let Some(frame) = stream.current_frame() else {
+ return Ok(());
+ };
+
+ let Some(selected_object) = &state.settings.selected_object else {
+ return Ok(());
+ };
+
+ let plane = state.settings.sharable.current_plane.to_plane();
+ let Ok(pixel_data) = stream.pixel_data.get_or_create_pixels(
+ frame,
+ plane,
+ state.settings.sharable.view_mode.view_settings().pixel_type,
+ ) else {
+ return Ok(());
+ };
+ let mut object_rect = selected_object.rect(frame).ok_or(anyhow!("Invalid selected object"))?;
+ if plane.is_chroma() {
+ object_rect = object_rect / 2.0;
+ }
+
+ let size = ui.available_size_before_wrap();
+ let (screen_bounds, response) = ui.allocate_exact_size(size, egui::Sense::drag());
+ let mut world_bounds = state.settings.sharable.pixel_viewer_bounds;
+ let scale = world_bounds.calc_scale(screen_bounds);
+ let mut hover_pos_world = None;
+
+ if response.dragged_by(PointerButton::Primary) {
+ let delta = response.drag_delta() / -scale;
+ world_bounds = world_bounds.translate(delta);
+ } else if response.hover_pos().is_some() && ui.input(|i| i.scroll_delta) != Vec2::ZERO {
+ let delta = ui.input(|i| i.scroll_delta);
+
+ if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) {
+ if screen_bounds.contains(mouse_pos) {
+ let zoom = if delta.y < 0.0 {
+ f32::powf(1.001, -delta.y)
+ } else {
+ 1.0 / (f32::powf(1.001, delta.y))
+ };
+ let zoom_center = world_bounds.screen_pos_to_world(mouse_pos, screen_bounds);
+ world_bounds.zoom_point(zoom_center, zoom);
+ }
+ }
+ } else if response.hover_pos().is_some() {
+ if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) {
+ hover_pos_world = Some(world_bounds.screen_pos_to_world(mouse_pos, screen_bounds));
+ }
+ }
+ state.settings.sharable.pixel_viewer_bounds = world_bounds;
+
+ let painter = ui.painter().with_clip_rect(response.rect);
+ let clip_rect = painter.clip_rect();
+ let mut shapes = Vec::new();
+ let ui_style = ui.ctx().style();
+ let mut hover_text = None;
+ for y in 0..object_rect.height() as usize {
+ for x in 0..object_rect.width() as usize {
+ let stride = pixel_data.width as usize;
+ let offset_x = object_rect.left_top().x as usize;
+ let offset_y = object_rect.left_top().y as usize;
+ let abs_x = x + offset_x;
+ let abs_y = y + offset_y;
+ let in_bounds = abs_x < pixel_data.width as usize && abs_y < pixel_data.height as usize;
+ let index = (y + offset_y) * stride + x + offset_x;
+ let pixel = if in_bounds {
+ pixel_data.pixels.get(index).copied()
+ } else {
+ None
+ };
+
+ let mut color = pixel.unwrap_or(0);
+ let pixel_max = 1 << pixel_data.bit_depth;
+ if pixel_data.pixel_type.is_delta() {
+ color = (color + pixel_max - 1) / 2;
+ }
+ if pixel_data.bit_depth > 8 {
+ color >>= pixel_data.bit_depth - 8;
+ }
+ let world_rect = Rect::from_min_size(pos2(x as f32, y as f32), vec2(1.0, 1.0));
+ let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
+ if let Some(hover_pos_world) = hover_pos_world {
+ if world_rect.contains(hover_pos_world) {
+ if let Some(pixel) = pixel {
+ hover_text = Some(format!(
+ "{}, Relative: (x={}, y={}), Absolute: (x={}, y={})",
+ pixel,
+ x,
+ y,
+ x + offset_x,
+ y + offset_y
+ ));
+ }
+ }
+ }
+
+ shapes.push(Shape::rect_filled(
+ screen_rect,
+ Rounding::ZERO,
+ Color32::from_gray(color as u8),
+ ));
+ if screen_rect.width() >= MIN_BLOCK_SIZE_FOR_GRID {
+ shapes.push(Shape::rect_stroke(
+ screen_rect,
+ Rounding::ZERO,
+ Stroke::new(1.0, Color32::from_gray(20)),
+ ));
+ }
+
+ if let Some(pixel) = pixel {
+ if screen_rect.height() >= MIN_BLOCK_HEIGHT_FOR_TEXT
+ && screen_rect.width() >= MIN_BLOCK_WIDTH_FOR_TEXT
+ && clip_rect.intersects(screen_rect)
+ {
+ let overlay_style = &state.settings.persistent.style.overlay;
+ shapes.extend(painter.fonts(|f| {
+ let colors = if overlay_style.enable_text_shadows {
+ vec![Color32::BLACK, overlay_style.pixel_viewer_text_color]
+ } else {
+ vec![overlay_style.pixel_viewer_text_color]
+ };
+ colors
+ .into_iter()
+ .enumerate()
+ .map(|(i, color)| {
+ let pos_offset = vec2(i as f32, i as f32);
+ Shape::text(
+ f,
+ screen_rect.center() + pos_offset,
+ Align2::CENTER_CENTER,
+ format!("{pixel}"),
+ TextStyle::Body.resolve(&ui_style),
+ color,
+ )
+ })
+ .collect::<Vec<_>>()
+ }));
+ }
+ }
+ }
+ }
+ painter.extend(shapes);
+ if let Some(hover_text) = hover_text {
+ response.on_hover_text_at_pointer(hover_text);
+ }
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/drag_and_drop.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/drag_and_drop.rs
new file mode 100644
index 0000000..b8bd297
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/drag_and_drop.rs
@@ -0,0 +1,65 @@
+use egui::{Align2, Color32, Id, LayerId, Order, TextStyle};
+use itertools::Itertools;
+use log::warn;
+
+use crate::app_state::AppState;
+
+const KNOWN_AVM_STREAM_EXTENSIONS: &[&str] = &[".obu", ".bin", ".ivf"];
+const KNOWN_ZIP_EXTENSIONS: &[&str] = &[".zip"];
+
+const ZIP_MIME_TYPES: &[&str] = &["application/zip"];
+const AVM_STREAM_MIME_TYPES: &[&str] = &["application/macbinary", "application/octet-stream"];
+
+pub fn handle_drag_and_drop(ctx: &egui::Context, state: &mut AppState) {
+ preview_files_being_dropped(ctx);
+ ctx.input_mut(|i| {
+ let sorted_dropped_files = i.raw.dropped_files.drain(..).sorted_by_key(|file| file.name.clone());
+ for dropped_file in sorted_dropped_files {
+ if KNOWN_AVM_STREAM_EXTENSIONS
+ .iter()
+ .any(|ext| dropped_file.name.ends_with(ext))
+ {
+ state.server_decode_manager.handle_dropped_file(dropped_file);
+ state.settings.show_decode_progress = true;
+ } else if KNOWN_ZIP_EXTENSIONS.iter().any(|ext| dropped_file.name.ends_with(ext)) {
+ state.local_stream_manager.handle_dropped_file(dropped_file);
+ } else {
+ warn!("Unknown file type: {}", dropped_file.name);
+ }
+ }
+ });
+}
+
+fn preview_files_being_dropped(ctx: &egui::Context) {
+ if !ctx.input(|i| i.raw.hovered_files.is_empty()) {
+ let text = ctx.input(|i| {
+ if i.raw.hovered_files.len() > 1 {
+ format!("Decode multiple streams: {}", i.raw.hovered_files.len())
+ } else {
+ let mime_type = i.raw.hovered_files[0].mime.as_str();
+ if ZIP_MIME_TYPES.contains(&mime_type) {
+ "Load local stream (.zip)".to_string()
+ } else if AVM_STREAM_MIME_TYPES.contains(&mime_type) {
+ "Decode stream on server".to_string()
+ } else {
+ format!(
+ "Unsupported file type: {}",
+ if mime_type.is_empty() { "Unknown" } else { mime_type }
+ )
+ }
+ }
+ });
+
+ let painter = ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
+
+ let screen_rect = ctx.screen_rect();
+ painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
+ painter.text(
+ screen_rect.center(),
+ Align2::CENTER_CENTER,
+ text,
+ TextStyle::Heading.resolve(&ctx.style()),
+ Color32::WHITE,
+ );
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/frame_info_viewer.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_info_viewer.rs
new file mode 100644
index 0000000..37d09d8
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_info_viewer.rs
@@ -0,0 +1,75 @@
+use avm_stats::Frame;
+use egui::{RichText, Ui};
+use egui_extras::{Column, TableBuilder};
+
+use crate::{app_state::AppState, stream::CurrentFrame};
+
+use crate::views::render_view::RenderView;
+
+// TODO(comc): PSNR, SSIM stats if available.
+pub struct FrameInfoViewer;
+impl FrameInfoViewer {
+ fn frame_info(frame: &Frame) -> Option<Vec<(String, String)>> {
+ let mut info = Vec::new();
+
+ let params = frame.frame_params.as_ref()?;
+ info.push(("Stream".into(), frame.stream_params.as_ref()?.stream_name.to_string()));
+ info.push(("Width".into(), format!("{}", params.width)));
+ info.push(("Height".into(), format!("{}", params.height)));
+ info.push(("Frame Type".into(), frame.frame_type_name()));
+ info.push(("TIP mode".into(), frame.tip_mode_name()));
+ info.push(("Decode Index".into(), params.decode_index.to_string()));
+ info.push(("Display Index".into(), params.display_index.to_string()));
+ if let Some(superblock_size) = params.superblock_size.as_ref() {
+ info.push((
+ "Superblock size".into(),
+ format!("{}x{}", superblock_size.width, superblock_size.height),
+ ));
+ }
+ // TODO(comc): Check why this is false for inter frames.
+ // info.push(("Show frame".into(), params.show_frame.to_string()));
+ info.push(("Base QIndex".into(), params.base_qindex.to_string()));
+ info.push(("Bit depth".into(), params.bit_depth.to_string()));
+ Some(info)
+ }
+}
+impl RenderView for FrameInfoViewer {
+ fn title(&self) -> String {
+ "Frame Info".into()
+ }
+
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ let Some(frame) = state.stream.current_frame() else {
+ return Ok(());
+ };
+
+ let Some(frame_info) = Self::frame_info(frame) else {
+ return Ok(());
+ };
+ TableBuilder::new(ui)
+ .column(Column::initial(300.0).resizable(true))
+ .column(Column::remainder())
+ .striped(true)
+ .header(20.0, |mut header| {
+ header.col(|ui| {
+ ui.label(RichText::new("Property").strong());
+ });
+ header.col(|ui| {
+ ui.label(RichText::new("Value").strong());
+ });
+ })
+ .body(|mut body| {
+ for (info_name, info_value) in frame_info {
+ body.row(20.0, |mut row| {
+ row.col(|col| {
+ col.label(info_name);
+ });
+ row.col(|col| {
+ col.label(info_value);
+ });
+ });
+ }
+ });
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/frame_select_viewer.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_select_viewer.rs
new file mode 100644
index 0000000..9f98b98
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_select_viewer.rs
@@ -0,0 +1,252 @@
+use avm_stats::{PixelType, PlaneType};
+use convert_case::{Case, Casing};
+use egui::{pos2, vec2, Align2, Color32, Mesh, Rect, Rounding, Shape, Stroke, TextStyle, Ui};
+
+use crate::app_state::AppState;
+use crate::image_manager::ImageType;
+use crate::stream::{ChangeFrame, CurrentFrame, FrameStatus};
+
+use crate::views::render_view::RenderView;
+
+use super::frame_viewer::REF_FRAME_COLORS;
+use super::{SelectedObjectKind, ViewMode};
+
+// TODO(comc): There must be a built-in way to do horizontal scrolling with the mouse wheel in egui.
+const SCROLL_FACTOR: f32 = 0.2;
+pub struct FrameSelectViewer;
+impl RenderView for FrameSelectViewer {
+ fn title(&self) -> String {
+ "Frame Select".into()
+ }
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ let mut set_current_frame_index = None;
+ let Some(stream) = state.stream.as_ref() else {
+ return Ok(());
+ };
+ let scroll_to = state.settings.scroll_to_index.take();
+ let mut motion_vector_arrows: Vec<(usize, i32)> = Vec::new();
+ if state.settings.sharable.view_mode == ViewMode::Motion {
+ if let Some(selected_object) = state.settings.selected_object.as_ref() {
+ if let SelectedObjectKind::CodingUnit(cu) = selected_object.kind {
+ if let Some(frame) = stream.current_frame() {
+ if let Some(cu) = cu.try_resolve(frame) {
+ for mv in cu.coding_unit.prediction_mode.as_ref().unwrap().motion_vectors.iter() {
+ if mv.ref_frame_is_inter && !mv.ref_frame_is_tip {
+ let order_hint = mv.ref_frame_order_hint;
+ if let Some(frame_index) = stream.lookup_order_hint(order_hint) {
+ if !motion_vector_arrows
+ .iter()
+ .any(|(other_frame_index, _)| *other_frame_index == frame_index)
+ {
+ motion_vector_arrows.push((frame_index, mv.ref_frame))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ let sorted_frame_indices = stream.get_sorted_frames(state.settings.persistent.frame_sort_order);
+ let mut frame_rects = vec![Rect::NOTHING; sorted_frame_indices.len()];
+ egui::ScrollArea::horizontal()
+ .max_height(200.0)
+ .drag_to_scroll(true)
+ .vscroll(false)
+ .show(ui, |ui| {
+ egui::Grid::new("img_grid").show(ui, |ui| {
+ for &frame_id in sorted_frame_indices.iter() {
+ let frame = stream.get_frame(frame_id);
+ let (id, rect) = ui.allocate_space(egui::vec2(100.0, 100.0));
+ frame_rects[frame_id] = rect;
+ if let Some(scroll_to) = scroll_to {
+ if scroll_to == frame_id {
+ ui.scroll_to_rect(rect, None);
+ }
+ }
+ let response = ui.interact(rect, id, egui::Sense::click());
+ let scroll_delta = ui.input(|i| i.scroll_delta);
+ if scroll_delta.length() > 0.0 {
+ let scroll_delta = SCROLL_FACTOR * vec2(scroll_delta.y, scroll_delta.x);
+ ui.scroll_with_delta(scroll_delta);
+ }
+ ui.painter()
+ .add(Shape::rect_filled(rect, Rounding::ZERO, Color32::GRAY));
+
+ let frame_id_text = if let FrameStatus::Loaded(frame) = frame {
+ let display_index = frame.frame_params.as_ref().unwrap().display_index;
+ format!("Frame {frame_id} ({display_index})")
+ } else {
+ format!("Frame {frame_id}")
+ };
+ let text = ui.fonts(|f| {
+ // TODO(comc): Use display or decode index instead of "frame_id":
+ Shape::text(
+ f,
+ rect.center_top(),
+ Align2::CENTER_TOP,
+ frame_id_text,
+ TextStyle::Body.resolve(ui.style()),
+ Color32::BLACK,
+ )
+ });
+ ui.painter().add(text);
+
+ match frame {
+ FrameStatus::OutOfBounds => {}
+ FrameStatus::Decoding => {
+ let text = ui.fonts(|f| {
+ Shape::text(
+ f,
+ rect.center(),
+ Align2::CENTER_CENTER,
+ "Decoding...",
+ TextStyle::Body.resolve(ui.style()),
+ Color32::BLACK,
+ )
+ });
+ ui.painter().add(text);
+ }
+ FrameStatus::Invalid => {
+ let text = ui.fonts(|f| {
+ Shape::text(
+ f,
+ rect.center(),
+ Align2::CENTER_CENTER,
+ "ERROR",
+ TextStyle::Body.resolve(ui.style()),
+ Color32::BLACK,
+ )
+ });
+ ui.painter().add(text);
+ }
+ FrameStatus::Pending => {
+ let text = ui.fonts(|f| {
+ Shape::text(
+ f,
+ rect.center(),
+ Align2::CENTER_CENTER,
+ "Loading...",
+ TextStyle::Body.resolve(ui.style()),
+ Color32::BLACK,
+ )
+ });
+ ui.painter().add(text);
+ }
+ FrameStatus::Loaded(frame) => {
+ // TODO(comc): Don't unwrap!
+ let image_type =
+ ImageType::new(PlaneType::Rgb, PixelType::Reconstruction, false, false);
+ let Ok(texture_handle) = stream.images.get_or_create_image(
+ ui.ctx(),
+ &stream.pixel_data,
+ frame,
+ image_type,
+ &state.settings.sharable.heatmap_settings,
+ ) else {
+ return;
+ };
+ let mut image_mesh = Mesh::with_texture(texture_handle.id());
+ // TODO(comc): preserve aspect ratio.
+ let image_uv = Rect::from_min_size(pos2(0.0, 0.0), vec2(1.0, 1.0));
+ let mut image_rect = rect;
+ image_rect.set_top(rect.top() + 20.0);
+ image_rect.set_bottom(rect.bottom() - 20.0);
+ image_mesh.add_rect_with_uv(image_rect, image_uv, Color32::WHITE);
+ ui.painter().add(image_mesh);
+
+ if response.clicked() {
+ set_current_frame_index = Some(frame_id);
+ }
+ let mut frame_type_name =
+ frame.frame_type_name().trim_end_matches("_FRAME").to_case(Case::Pascal);
+ let tip_mode = frame.tip_mode_name();
+ if tip_mode == "TIP_FRAME_AS_OUTPUT" {
+ frame_type_name = format!("{frame_type_name} (TIP)");
+ }
+
+ let text = ui.fonts(|f| {
+ Shape::text(
+ f,
+ rect.center_bottom(),
+ Align2::CENTER_BOTTOM,
+ frame_type_name,
+ TextStyle::Body.resolve(ui.style()),
+ Color32::BLACK,
+ )
+ });
+ ui.painter().add(text);
+ }
+ FrameStatus::Unloaded => {
+ let text = ui.fonts(|f| {
+ Shape::text(
+ f,
+ rect.center(),
+ Align2::CENTER_CENTER,
+ "Click to load",
+ TextStyle::Body.resolve(ui.style()),
+ Color32::BLACK,
+ )
+ });
+ ui.painter().add(text);
+
+ if response.clicked() {
+ set_current_frame_index = Some(frame_id);
+ }
+ }
+ };
+
+ if let Some(current_frame) = state.stream.current_frame() {
+ if current_frame.decode_index() == frame_id {
+ ui.painter().add(Shape::rect_stroke(
+ rect,
+ Rounding::ZERO,
+ Stroke::new(3.0, Color32::YELLOW),
+ ));
+ }
+ }
+ }
+ });
+ let painter = ui.painter();
+ for (mv_index, &(frame_index, ref_frame)) in motion_vector_arrows.iter().enumerate() {
+ let start_rect = frame_rects[stream.current_frame_index];
+ let end_rect = frame_rects[frame_index];
+ let color_index = ref_frame as usize % 8;
+ let color = REF_FRAME_COLORS[color_index];
+ let pos_offset = vec2(0.0, mv_index as f32 * 5.0);
+ painter.add(Shape::line_segment(
+ [start_rect.center() + pos_offset, end_rect.center() + pos_offset],
+ Stroke::new(2.0, color),
+ ));
+ painter.add(Shape::circle_filled(
+ start_rect.center() + pos_offset,
+ 2.0,
+ REF_FRAME_COLORS[color_index],
+ ));
+
+ let dir_sign = (start_rect.left() - end_rect.left()).signum();
+ let triangle_tip = end_rect.center() + pos_offset;
+ let triangle_vertices = vec![
+ triangle_tip,
+ triangle_tip + vec2(dir_sign * 3.0, 3.0),
+ triangle_tip + vec2(dir_sign * 3.0, -3.0),
+ ];
+ painter.add(Shape::convex_polygon(
+ triangle_vertices,
+ REF_FRAME_COLORS[color_index],
+ Stroke::NONE,
+ ));
+
+ painter.add(Shape::rect_stroke(end_rect, Rounding::ZERO, Stroke::new(3.0, color)));
+ }
+ });
+ if let Some(current_frame_index) = set_current_frame_index {
+ // Unwrapping is okay, since we already know stream exists at this point.
+ let stream = state.stream.as_mut().unwrap();
+ stream.change_frame(ChangeFrame::index(current_frame_index));
+ state.settings.selected_object = None;
+ }
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/frame_overlay.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/frame_overlay.rs
new file mode 100644
index 0000000..1b0cbcc
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/frame_overlay.rs
@@ -0,0 +1,422 @@
+use super::ScreenBounds;
+use crate::settings::{MotionFieldColoring, Settings};
+use crate::views::{MIN_BLOCK_HEIGHT_FOR_TEXT, MIN_BLOCK_SIZE_TO_RENDER, MIN_BLOCK_WIDTH_FOR_TEXT};
+
+use avm_stats::{CodingUnitKind, Frame, ProtoEnumMapping, Spatial, MOTION_VECTOR_PRECISION};
+use egui::{pos2, vec2, Align2, Color32, Painter, Rect, Rounding, Shape, Stroke, TextStyle};
+use itertools::Itertools;
+
+fn is_rect_too_small(rect: Rect) -> bool {
+ rect.width() < MIN_BLOCK_SIZE_TO_RENDER || rect.height() < MIN_BLOCK_SIZE_TO_RENDER
+}
+
+fn is_text_too_small(rect: Rect) -> bool {
+ rect.width() < MIN_BLOCK_WIDTH_FOR_TEXT || rect.height() < MIN_BLOCK_HEIGHT_FOR_TEXT
+}
+
+// TODO(comc): Make configurable.
+pub const REF_FRAME_COLORS: &[Color32] = &[
+ Color32::LIGHT_RED,
+ Color32::LIGHT_GREEN,
+ Color32::LIGHT_BLUE,
+ Color32::LIGHT_YELLOW,
+ Color32::BROWN,
+ Color32::KHAKI,
+ Color32::GOLD,
+ Color32::LIGHT_GRAY,
+];
+
+// TODO(comc): World space to screen space conversion could be done in a shader.
+pub struct FrameOverlay<'a> {
+ frame: &'a Frame,
+ settings: &'a Settings,
+}
+
+impl<'a> FrameOverlay<'a> {
+ pub fn new(frame: &'a Frame, settings: &'a Settings) -> Self {
+ Self { frame, settings }
+ }
+
+ pub fn draw(&self, painter: &mut Painter) {
+ let view_settings = self.settings.sharable.view_mode.view_settings();
+
+ if self.settings.sharable.show_overlay {
+ if view_settings.show_transform_units {
+ self.draw_transform_units(painter);
+ }
+
+ if view_settings.show_coding_units {
+ self.draw_coding_units(painter);
+ }
+
+ if view_settings.show_superblocks {
+ self.draw_superblocks(painter);
+ }
+
+ if view_settings.show_prediction_modes {
+ self.draw_prediction_modes(painter);
+ }
+
+ if view_settings.show_transform_types {
+ self.draw_transform_modes(painter);
+ }
+ }
+
+ if (self.settings.sharable.show_overlay || self.settings.sharable.motion_field.show)
+ && view_settings.show_motion_vectors
+ {
+ self.draw_motion_vectors(painter);
+ }
+ }
+
+ fn is_in_bounds(&self, world_rect: Rect, screen_bounds: Rect) -> bool {
+ let world_bounds = self.settings.sharable.world_bounds;
+ // Because the aspect ratio of the viewport is not necessarily the same as the current frame, we might see extra than the plain world_bounds.
+ let extended_world_bounds = world_bounds.screen_rect_to_world(screen_bounds, screen_bounds);
+ extended_world_bounds.intersects(world_rect)
+ }
+
+ fn draw_transform_units(&self, painter: &mut Painter) -> Option<()> {
+ let style = &self.settings.persistent.style.overlay;
+ let world_bounds = self.settings.sharable.world_bounds;
+ let clip_rect = painter.clip_rect();
+ let shapes = self
+ .frame
+ .iter_transform_rects(self.settings.sharable.current_plane.to_plane())
+ .filter_map(|world_rect| {
+ if !self.is_in_bounds(world_rect, clip_rect) {
+ return None;
+ }
+ let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
+ if is_rect_too_small(screen_rect) {
+ return None;
+ }
+ let points = [
+ screen_rect.left_top(),
+ screen_rect.right_top(),
+ screen_rect.right_bottom(),
+ screen_rect.left_bottom(),
+ ];
+ Some(Shape::dashed_line(&points[..], style.transform_unit_stroke, 4.0, 4.0))
+ })
+ .flatten();
+ painter.extend(shapes);
+
+ None
+ }
+ fn draw_coding_units(&self, painter: &mut Painter) -> Option<()> {
+ let style = &self.settings.persistent.style.overlay;
+ let world_bounds = self.settings.sharable.world_bounds;
+ let clip_rect = painter.clip_rect();
+ let coding_unit_kind = self.frame.coding_unit_kind(self.settings.sharable.current_plane);
+ let shapes = self
+ .frame
+ .iter_coding_unit_rects(coding_unit_kind)
+ .filter_map(|world_rect| {
+ if !self.is_in_bounds(world_rect, clip_rect) {
+ return None;
+ }
+ let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
+ if is_rect_too_small(screen_rect) {
+ return None;
+ }
+ Some(Shape::rect_stroke(
+ screen_rect,
+ Rounding::ZERO,
+ style.coding_unit_stroke,
+ ))
+ });
+ painter.extend(shapes);
+ None
+ }
+ fn draw_motion_vectors(&self, painter: &mut Painter) -> Option<()> {
+ let _style = &self.settings.persistent.style.overlay;
+ let world_bounds = self.settings.sharable.world_bounds;
+ let clip_rect = painter.clip_rect();
+ let coding_unit_kind = self.frame.coding_unit_kind(self.settings.sharable.current_plane);
+ // Length of largest motion vector, in screen space.
+ let mut largest_mv = 1e-9;
+ let granularity = self.settings.sharable.motion_field.granularity;
+ let raw_shapes = self
+ .frame
+ .iter_coding_units(coding_unit_kind)
+ .filter_map(|ctx| {
+ let cu = ctx.coding_unit;
+ let world_rect = cu.rect();
+ let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
+ let Ok(prediction_mode) = cu.get_prediction_mode() else {
+ return None;
+ };
+ let Ok(mode) = self
+ .frame
+ .enum_lookup(ProtoEnumMapping::PredictionMode, prediction_mode.mode)
+ else {
+ return None;
+ };
+ // TODO(comc): Make this a method on PredictionParams or CodingUnit.
+ let is_motion = !mode.contains("_PRED");
+ if !is_motion {
+ return None;
+ }
+ let is_compound = mode.contains('_');
+ let num_mvs = if is_compound { 2 } else { 1 };
+ // let mv_center = world_rect.center();
+ let mvs = (0..num_mvs)
+ .filter_map(|i| {
+ let mv = prediction_mode.motion_vectors.get(i as usize)?;
+ let dx = mv.dx as f32 / MOTION_VECTOR_PRECISION;
+ let dy = mv.dy as f32 / MOTION_VECTOR_PRECISION;
+ let ref_frame = mv.ref_frame;
+ if ref_frame == -1 {
+ return None;
+ }
+
+ let order_hint = mv.ref_frame_order_hint;
+ // let mv_tip = mv_center + vec2(dx, dy);
+ let mv_world = vec2(dx, dy);
+ let mv_screen = mv_world * world_bounds.calc_scale(clip_rect);
+ // let mv_tip_screen = world_bounds.world_pos_to_screen(mv_tip, clip_rect);
+ // let screen_pos = screen_rect.center();
+ // let mv_vector = mv_tip_screen - screen_pos;
+ let magnitude = mv_screen.length();
+ if magnitude > largest_mv {
+ largest_mv = magnitude;
+ }
+ let is_future = order_hint > self.frame.frame_params.as_ref().unwrap().raw_display_index;
+ let color = match self.settings.sharable.motion_field.coloring {
+ MotionFieldColoring::RefFrames => {
+ let color_index = ref_frame as usize % 8;
+ REF_FRAME_COLORS[color_index]
+ }
+ MotionFieldColoring::PastFuture => REF_FRAME_COLORS[is_future as usize],
+ MotionFieldColoring::Monochrome => REF_FRAME_COLORS[0],
+ };
+ Some((mv_screen, color))
+ })
+ .collect_vec();
+
+ if !self.is_in_bounds(world_rect, clip_rect) {
+ return None;
+ }
+ if is_rect_too_small(screen_rect) {
+ return None;
+ }
+ if mvs.is_empty() {
+ return None;
+ }
+ let mv_rects = if self.settings.sharable.motion_field.show {
+ let mut rects = Vec::new();
+ let rows = world_rect.height() as i32;
+ let cols = world_rect.width() as i32;
+ let top = world_rect.top() as i32;
+ let left = world_rect.left() as i32;
+ let granularity_pixels = (granularity * 4) as i32;
+ let first_row = (granularity_pixels / 2 - top).rem_euclid(granularity_pixels);
+ let first_col = (granularity_pixels / 2 - left).rem_euclid(granularity_pixels);
+ for row in (first_row..rows).step_by(granularity_pixels as usize) {
+ let y = row + top;
+ for col in (first_col..cols).step_by(granularity_pixels as usize) {
+ let x = col + left;
+ let rect = Rect::from_center_size(
+ pos2(x as f32, y as f32),
+ vec2(granularity_pixels as f32, granularity_pixels as f32),
+ );
+ let screen_rect = world_bounds.world_rect_to_screen(rect, clip_rect);
+ rects.push(screen_rect);
+ }
+ }
+ rects
+ } else {
+ vec![screen_rect]
+ };
+
+ Some((mv_rects, mvs))
+ })
+ .collect_vec();
+
+ let longest_vector_world_space = granularity as f32 * 2.0;
+ let longest_vector_screen_space = world_bounds.calc_scale(clip_rect) * longest_vector_world_space;
+ let normalization_factor = longest_vector_screen_space / largest_mv;
+ let shapes = raw_shapes.iter().flat_map(|(rects, mvs)| {
+ let mut mv_shapes = Vec::new();
+ for screen_rect in rects {
+ for &(mut mv_vector, color) in mvs.iter() {
+ if self.settings.sharable.motion_field.normalize && self.settings.sharable.motion_field.show {
+ mv_vector *= normalization_factor;
+ }
+ mv_vector *= self.settings.sharable.motion_field.scale;
+ let screen_pos = screen_rect.center();
+ if self.settings.sharable.motion_field.show_origin {
+ mv_shapes.push(Shape::circle_filled(screen_pos, 1.0, color));
+ }
+ mv_shapes.push(Shape::line_segment(
+ [screen_pos, screen_pos + mv_vector],
+ Stroke::new(1.5, color),
+ ));
+ }
+ }
+ mv_shapes
+ });
+ painter.extend(shapes);
+ None
+ }
+
+ fn draw_superblocks(&self, painter: &mut Painter) -> Option<()> {
+ let style = &self.settings.persistent.style.overlay;
+ let world_bounds = self.settings.sharable.world_bounds;
+ let clip_rect = painter.clip_rect();
+ let shapes = self.frame.iter_superblock_rects().filter_map(|world_rect| {
+ if !self.is_in_bounds(world_rect, clip_rect) {
+ return None;
+ }
+ let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
+ if is_rect_too_small(screen_rect) {
+ return None;
+ }
+ Some(Shape::rect_stroke(screen_rect, Rounding::ZERO, style.superblock_stroke))
+ });
+ painter.extend(shapes);
+ None
+ }
+
+ fn draw_prediction_modes(&self, painter: &mut Painter) -> Option<()> {
+ let overlay_style = &self.settings.persistent.style.overlay;
+ let world_bounds = self.settings.sharable.world_bounds;
+ let clip_rect = painter.clip_rect();
+ let coding_unit_kind = self.frame.coding_unit_kind(self.settings.sharable.current_plane);
+ let ui_style = painter.ctx().style();
+ let shapes = self
+ .frame
+ .iter_coding_units(coding_unit_kind)
+ .filter_map(|ctx| {
+ let cu = ctx.coding_unit;
+ let world_rect = cu.rect();
+ if !self.is_in_bounds(world_rect, clip_rect) {
+ return None;
+ }
+ let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
+ if is_rect_too_small(screen_rect) || is_text_too_small(screen_rect) {
+ return None;
+ }
+
+ let Ok(prediction_mode) = cu.get_prediction_mode() else {
+ return None;
+ };
+ let mode_name = if coding_unit_kind == CodingUnitKind::ChromaOnly {
+ self.frame
+ .enum_lookup(ProtoEnumMapping::UvPredictionMode, prediction_mode.uv_mode)
+ } else {
+ self.frame
+ .enum_lookup(ProtoEnumMapping::PredictionMode, prediction_mode.mode)
+ };
+ let Ok(mode_name) = mode_name else {
+ return None;
+ };
+
+ let screen_pos = screen_rect.center();
+ let mode_name = mode_name.trim_end_matches("_PRED");
+ let uses_palette = match coding_unit_kind {
+ CodingUnitKind::Shared | CodingUnitKind::LumaOnly => {
+ prediction_mode.palette_count > 0
+ }
+ CodingUnitKind::ChromaOnly => prediction_mode.uv_palette_count > 0,
+ };
+ let uses_intrabc = match coding_unit_kind {
+ CodingUnitKind::Shared | CodingUnitKind::LumaOnly => {
+ prediction_mode.use_intrabc
+ }
+ CodingUnitKind::ChromaOnly => false
+ };
+ let mode_name = if uses_palette {
+ "PALETTE"
+ } else if uses_intrabc {
+ "INTRA_BC"
+ } else {
+ mode_name
+ };
+
+ let colors = if overlay_style.enable_text_shadows {
+ vec![Color32::BLACK, overlay_style.mode_name_color]
+ } else {
+ vec![overlay_style.mode_name_color]
+ };
+ painter.fonts(|f| {
+ Some(
+ colors
+ .into_iter()
+ .enumerate()
+ .map(|(i, color)| {
+ let pos_offset = vec2(i as f32, i as f32);
+ Shape::text(
+ f,
+ screen_pos + pos_offset,
+ Align2::CENTER_CENTER,
+ mode_name,
+ TextStyle::Body.resolve(&ui_style),
+ color,
+ )
+ })
+ .collect::<Vec<_>>(),
+ )
+ })
+ })
+ .flatten()
+ .collect::<Vec<_>>(); // Note: need to collect here since painter.fonts requires read-only access to painter, and we can't mutate in painter.extend at the same time.
+
+ painter.extend(shapes);
+ None
+ }
+
+ fn draw_transform_modes(&self, painter: &mut Painter) -> Option<()> {
+ let overlay_style = &self.settings.persistent.style.overlay;
+ let world_bounds = self.settings.sharable.world_bounds;
+ let clip_rect = painter.clip_rect();
+ let ui_style = painter.ctx().style();
+ let shapes = self
+ .frame
+ .iter_transform_units(self.settings.sharable.current_plane.to_plane())
+ .filter_map(|ctx| {
+ let tu = ctx.transform_unit;
+ let world_rect = tu.rect();
+ if !self.is_in_bounds(world_rect, clip_rect) {
+ return None;
+ }
+ let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
+ if is_rect_too_small(screen_rect) || is_text_too_small(screen_rect) {
+ return None;
+ }
+
+ let tx_type = tu.primary_tx_type_or_skip(self.frame);
+ let screen_pos = screen_rect.center();
+ let colors = if overlay_style.enable_text_shadows {
+ vec![Color32::BLACK, overlay_style.mode_name_color]
+ } else {
+ vec![overlay_style.mode_name_color]
+ };
+ painter.fonts(|f| {
+ Some(
+ colors
+ .into_iter()
+ .enumerate()
+ .map(|(i, color)| {
+ let pos_offset = vec2(i as f32, i as f32);
+ Shape::text(
+ f,
+ screen_pos + pos_offset,
+ Align2::CENTER_CENTER,
+ tx_type.clone(),
+ TextStyle::Body.resolve(&ui_style),
+ color,
+ )
+ })
+ .collect::<Vec<_>>(),
+ )
+ })
+ })
+ .flatten()
+ .collect::<Vec<_>>(); // Note: need to collect here since painter.fonts requires read-only access to painter, and we can't mutate in painter.extend at the same time.
+
+ painter.extend(shapes);
+ None
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/mod.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/mod.rs
new file mode 100644
index 0000000..08f2529
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/mod.rs
@@ -0,0 +1,277 @@
+mod frame_overlay;
+mod screen_bounds;
+mod selected_object;
+
+use avm_stats::{Frame, Plane, PlaneType, Spatial};
+use egui::{pos2, vec2, Color32, Mesh, PointerButton, Pos2, Rect, RichText, Rounding, Shape, Stroke, Ui, Vec2};
+use egui_plot::{Bar, BarChart, Plot};
+
+use crate::image_manager::ImageType;
+use crate::stream::CurrentFrame as _;
+use crate::views::RenderView;
+use crate::{app_state::AppState, image_manager::JET_COLORMAP};
+pub use frame_overlay::{FrameOverlay, REF_FRAME_COLORS};
+pub use screen_bounds::ScreenBounds;
+pub use selected_object::{SelectedObject, SelectedObjectKind};
+
+use super::ViewMode;
+
+pub struct FrameViewer;
+
+impl FrameViewer {
+ fn check_hovered_object(
+ &self,
+ state: &AppState,
+ frame: &Frame,
+ hover_pos_world: Pos2,
+ ) -> Option<SelectedObjectKind> {
+ let view_settings = state.settings.sharable.view_mode.view_settings();
+ if view_settings.allow_transform_unit_selection {
+ if let Some(hovered_transform_unit) = frame
+ .iter_transform_units(state.settings.sharable.current_plane.to_plane())
+ .find(|ctx| ctx.transform_unit.rect().contains(hover_pos_world))
+ {
+ return Some(SelectedObjectKind::TransformUnit(hovered_transform_unit.locator));
+ }
+ }
+ if view_settings.allow_coding_unit_selection {
+ let coding_unit_kind = frame.coding_unit_kind(state.settings.sharable.current_plane);
+ if let Some(hovered_coding_unit) = frame
+ .iter_coding_units(coding_unit_kind)
+ .find(|ctx| ctx.coding_unit.rect().contains(hover_pos_world))
+ {
+ return Some(SelectedObjectKind::CodingUnit(hovered_coding_unit.locator));
+ }
+ }
+ None
+ }
+}
+impl RenderView for FrameViewer {
+ fn title(&self) -> String {
+ "Frame View".into()
+ }
+
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ egui::warn_if_debug_build(ui);
+
+ let Some(frame) = state.stream.current_frame() else {
+ ui.spinner();
+ return Ok(());
+ };
+
+ // Okay to unwrap here since we already have a Frame, so we must also have a Stream.
+ let stream = state.stream.as_ref().unwrap();
+
+ let mut world_bounds = state.settings.sharable.world_bounds;
+ let mut set_selected_object = false;
+ let mut set_selected_object_parent = false;
+ let mut hover_pos_world = None;
+
+ if state.settings.sharable.view_mode == ViewMode::Transform
+ && matches!(
+ state.settings.sharable.current_plane,
+ PlaneType::Planar(Plane::U | Plane::V)
+ )
+ {
+ ui.label(
+ RichText::new("WARNING: Chroma transform tree data extraction is currently buggy.").color(Color32::RED),
+ );
+ ui.end_row();
+ }
+
+ let size = ui.available_size_before_wrap();
+ let (screen_bounds, response) = ui.allocate_exact_size(size, egui::Sense::drag());
+ let scale = world_bounds.calc_scale(screen_bounds);
+
+ if response.dragged_by(PointerButton::Primary) {
+ let delta = response.drag_delta() / -scale;
+ world_bounds = world_bounds.translate(delta);
+ } else if response.hover_pos().is_some() && ui.input(|i| i.scroll_delta) != Vec2::ZERO {
+ let delta = ui.input(|i| i.scroll_delta);
+
+ if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) {
+ if screen_bounds.contains(mouse_pos) {
+ let zoom = if delta.y < 0.0 {
+ f32::powf(1.001, -delta.y)
+ } else {
+ 1.0 / (f32::powf(1.001, delta.y))
+ };
+ let zoom_center = world_bounds.screen_pos_to_world(mouse_pos, screen_bounds);
+ world_bounds.zoom_point(zoom_center, zoom);
+ }
+ }
+ } else if response.hover_pos().is_some() {
+ if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) {
+ if response.double_clicked() && state.settings.selected_object_leaf.is_some() {
+ set_selected_object_parent = true;
+ } else if response.clicked() {
+ set_selected_object = true;
+ }
+ hover_pos_world = Some(world_bounds.screen_pos_to_world(mouse_pos, screen_bounds));
+ }
+ }
+
+ state.settings.sharable.world_bounds = world_bounds;
+ let width = frame.width();
+ let height = frame.height();
+ let view_settings = state.settings.sharable.view_mode.view_settings();
+
+ let image_type = ImageType::new(
+ state.settings.sharable.current_plane,
+ view_settings.pixel_type,
+ view_settings.pixel_type.is_delta() && state.settings.sharable.show_relative_delta,
+ view_settings.show_heatmap,
+ );
+
+ let Ok(texture_handle) = stream.images.get_or_create_image(
+ ui.ctx(),
+ &stream.pixel_data,
+ frame,
+ image_type,
+ &state.settings.sharable.heatmap_settings,
+ ) else {
+ return Ok(());
+ };
+
+ if state.settings.sharable.show_yuv {
+ let mut image_mesh = Mesh::with_texture(texture_handle.id());
+ let image_world = Rect::from_min_size(pos2(0.0, 0.0), vec2(width as f32, height as f32));
+ let image_screen = world_bounds.world_rect_to_screen(image_world, screen_bounds);
+ let image_uv = Rect::from_min_size(pos2(0.0, 0.0), vec2(1.0, 1.0));
+ image_mesh.add_rect_with_uv(image_screen, image_uv, Color32::WHITE);
+ ui.painter().with_clip_rect(response.rect).add(image_mesh);
+ }
+ let mut painter = ui.painter().with_clip_rect(response.rect);
+ let overlay = FrameOverlay::new(frame, &state.settings);
+ overlay.draw(&mut painter);
+
+ let style = &state.settings.persistent.style.overlay;
+ if state.settings.sharable.show_overlay {
+ if let Some(hover_pos_world) = hover_pos_world {
+ if let Some(hovered_object) = self.check_hovered_object(state, frame, hover_pos_world) {
+ if let Some(world_rect) = hovered_object.rect(frame) {
+ let screen_rect = world_bounds.world_rect_to_screen(world_rect, screen_bounds);
+ let mut already_selected = false;
+ if let Some(selected_object_leaf) = &state.settings.selected_object_leaf {
+ if selected_object_leaf == &hovered_object {
+ already_selected = true;
+ }
+ }
+ if set_selected_object && !already_selected {
+ state.settings.sharable.pixel_viewer_bounds =
+ Rect::from_min_size(pos2(0.0, 0.0), world_rect.size());
+ if matches!(hovered_object, SelectedObjectKind::TransformUnit(_)) {
+ state.settings.sharable.coeffs_viewer_bounds =
+ Rect::from_min_size(pos2(0.0, 0.0), world_rect.size());
+ }
+ state.settings.selected_object_leaf = Some(hovered_object.clone());
+ state.settings.selected_object = Some(SelectedObject::new(hovered_object));
+ }
+ if set_selected_object_parent && already_selected {
+ if let Some(SelectedObject {
+ kind: selected_object_kind,
+ ..
+ }) = &state.settings.selected_object
+ {
+ if let Some(parent) = selected_object_kind.get_parent(frame) {
+ state.settings.selected_object = Some(SelectedObject::new(parent));
+ }
+ }
+ }
+ painter.add(Shape::rect_stroke(
+ screen_rect,
+ Rounding::ZERO,
+ style.highlighted_object_stroke,
+ ));
+ }
+ } else if set_selected_object {
+ state.settings.selected_object = None;
+ state.settings.selected_object_leaf = None;
+ }
+ }
+ if let Some(selected_object) = &state.settings.selected_object {
+ if let Some(world_rect) = selected_object.rect(frame) {
+ let screen_rect = world_bounds.world_rect_to_screen(world_rect, screen_bounds);
+ painter.add(Shape::rect_stroke(
+ screen_rect,
+ Rounding::ZERO,
+ style.selected_object_stroke,
+ ));
+ }
+ }
+ }
+ // TODO(comc): Refactor into separate module.
+ if view_settings.show_heatmap {
+ let mut show_heatmap_legend = state.settings.sharable.show_heatmap_legend;
+ let log_scale = state.settings.sharable.heatmap_histogram_log_scale;
+ let ui_ctx = ui.ctx();
+ egui::Window::new("Heatmap Legend")
+ .id("Heatmap Legend".into())
+ .open(&mut show_heatmap_legend)
+ .default_width(400.0)
+ .default_height(400.0)
+ .resizable(true)
+ .collapsible(false)
+ .show(ui_ctx, |ui| {
+ Plot::new("heatmap_legend")
+ .show_background(false)
+ .show_axes([true, true])
+ .clamp_grid(true)
+ .show_grid(false)
+ .allow_boxed_zoom(false)
+ .allow_drag(false)
+ .allow_zoom(false)
+ .allow_scroll(false)
+ .show_x(false)
+ .show_y(false)
+ .x_axis_label("Bits / pixel")
+ .y_axis_formatter(move |value, _num_chars, _range| {
+ if log_scale {
+ (10_f64).powf(value).to_string()
+ } else {
+ value.to_string()
+ }
+ })
+ .show(ui, |plot_ui| {
+ if let Ok(heatmap) = stream.images.get_or_create_heatmap(
+ ui_ctx,
+ frame,
+ image_type,
+ &state.settings.sharable.heatmap_settings,
+ ) {
+ let histogram = heatmap.histogram;
+ plot_ui.bar_chart(BarChart::new(
+ histogram
+ .iter()
+ .enumerate()
+ .map(|(index, &value)| {
+ let mut value = value as f64;
+ if log_scale && value > 0.0 {
+ value = value.log10();
+ }
+ let bar_width = heatmap.bucket_width as f64;
+ let x = index as f64 * bar_width;
+ let colormap_index = 255.0 * (index as f64 / histogram.len() as f64);
+ let color = JET_COLORMAP[colormap_index as usize];
+ let fill = Color32::from_rgb(color[0], color[1], color[2]);
+ Bar {
+ name: "bar".into(),
+ orientation: egui_plot::Orientation::Vertical,
+ argument: x,
+ value,
+ base_offset: None,
+ bar_width: heatmap.bucket_width as f64,
+ stroke: Stroke::new(1.0, Color32::BLACK),
+ fill,
+ }
+ })
+ .collect(),
+ ));
+ }
+ });
+ });
+ state.settings.sharable.show_heatmap_legend = show_heatmap_legend;
+ }
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/screen_bounds.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/screen_bounds.rs
new file mode 100644
index 0000000..34924c3
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/screen_bounds.rs
@@ -0,0 +1,69 @@
+use egui::{pos2, Pos2, Rect, Vec2};
+
+pub trait ScreenBounds {
+ fn zoom_point(&mut self, center: Pos2, zoom_factor: f32);
+ fn calc_scale(&self, screen_bounds: Rect) -> f32;
+ fn translate(&mut self, delta: Vec2);
+ fn screen_pos_to_world(&self, screen_point: Pos2, screen_bounds: Rect) -> Pos2;
+ fn world_pos_to_screen(&self, world_point: Pos2, screen_bounds: Rect) -> Pos2;
+
+ fn screen_rect_to_world(&self, screen_rect: Rect, screen_bounds: Rect) -> Rect;
+ fn world_rect_to_screen(&self, world_rect: Rect, screen_bounds: Rect) -> Rect;
+}
+
+impl ScreenBounds for Rect {
+ fn zoom_point(&mut self, center: Pos2, zoom_factor: f32) {
+ let mut left = center.x - self.left();
+ let mut right = self.right() - center.x;
+ left *= zoom_factor;
+ right *= zoom_factor;
+ let mut top = center.y - self.top();
+ let mut bottom = self.bottom() - center.y;
+ top *= zoom_factor;
+ bottom *= zoom_factor;
+
+ self.set_left(center.x - left);
+ self.set_right(center.x + right);
+ self.set_top(center.y - top);
+ self.set_bottom(center.y + bottom);
+ }
+
+ fn calc_scale(&self, screen_bounds: Rect) -> f32 {
+ let x_scale = screen_bounds.width() / (self.right() - self.left());
+ let y_scale = screen_bounds.height() / (self.bottom() - self.top());
+ x_scale.min(y_scale)
+ }
+
+ fn translate(&mut self, delta: Vec2) {
+ self.set_left(self.left() - delta.x);
+ self.set_right(self.right() - delta.x);
+ self.set_top(self.top() - delta.y);
+ self.set_bottom(self.bottom() - delta.y);
+ }
+
+ fn screen_pos_to_world(&self, screen_point: Pos2, screen_bounds: Rect) -> Pos2 {
+ let scale = self.calc_scale(screen_bounds);
+ let world_x = (screen_point.x - screen_bounds.left()) / scale + self.left();
+ let world_y = (screen_point.y - screen_bounds.top()) / scale + self.top();
+ pos2(world_x, world_y)
+ }
+
+ fn world_pos_to_screen(&self, world_point: Pos2, screen_bounds: Rect) -> Pos2 {
+ let scale = self.calc_scale(screen_bounds);
+ let screen_x = (world_point.x - self.left()) * scale + screen_bounds.left();
+ let screen_y = (world_point.y - self.top()) * scale + screen_bounds.top();
+ pos2(screen_x, screen_y)
+ }
+
+ fn screen_rect_to_world(&self, screen_rect: Rect, screen_bounds: Rect) -> Rect {
+ let left_top = self.screen_pos_to_world(screen_rect.left_top(), screen_bounds);
+ let right_bottom = self.screen_pos_to_world(screen_rect.right_bottom(), screen_bounds);
+ Rect::from_min_max(left_top, right_bottom)
+ }
+
+ fn world_rect_to_screen(&self, world_rect: Rect, screen_bounds: Rect) -> Rect {
+ let left_top = self.world_pos_to_screen(world_rect.left_top(), screen_bounds);
+ let right_bottom = self.world_pos_to_screen(world_rect.right_bottom(), screen_bounds);
+ Rect::from_min_max(left_top, right_bottom)
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/selected_object.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/selected_object.rs
new file mode 100644
index 0000000..d342661
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/frame_viewer/selected_object.rs
@@ -0,0 +1,406 @@
+use avm_stats::{
+ CodingUnitKind, CodingUnitLocator, Frame, FrameError, PartitionLocator, Plane, ProtoEnumMapping, Spatial,
+ SuperblockLocator, SymbolContext, TransformUnitLocator, MISSING_SYMBOL_INFO, MOTION_VECTOR_PRECISION,
+};
+use egui::emath::Rect;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug)]
+pub enum CachedInfo<T> {
+ Missing,
+ Calculated(T),
+}
+
+#[derive(Clone, Debug)]
+pub struct SymbolEntry {
+ pub func: String,
+ pub bits: f32,
+ pub tags: Vec<String>,
+ pub file: String,
+ pub line: i32,
+ pub value: i32,
+}
+impl SymbolEntry {
+ pub fn new(ctx: SymbolContext) -> Self {
+ let bits = ctx.symbol.bits;
+ let value = ctx.symbol.value;
+ let sym_info = ctx.info.unwrap_or(&MISSING_SYMBOL_INFO);
+ let func: String = sym_info.source_function.clone();
+ let file = sym_info.source_file.clone();
+ let line = sym_info.source_line;
+ let tags = sym_info.tags.clone();
+
+ Self {
+ func,
+ bits,
+ tags,
+ file,
+ line,
+ value,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct FieldEntry {
+ pub name: String,
+ pub value: String,
+}
+
+impl FieldEntry {
+ fn new(name: String, value: String) -> Self {
+ Self { name, value }
+ }
+}
+
+#[derive(Debug)]
+pub struct SelectedObjectInfo {
+ pub fields: Vec<FieldEntry>,
+ pub symbols: Vec<SymbolEntry>,
+}
+
+impl SelectedObjectInfo {
+ fn get_symbols_coding_unit(locator: &CodingUnitLocator, frame: &Frame) -> Vec<SymbolEntry> {
+ let Some(ctx) = locator.try_resolve(frame) else {
+ return Vec::new();
+ };
+ let symbols: Vec<SymbolEntry> = ctx.iter_symbols().map(|sym| SymbolEntry::new(sym)).collect();
+ symbols
+ }
+
+ fn get_symbols_partition(locator: &PartitionLocator, frame: &Frame) -> Vec<SymbolEntry> {
+ let Some(ctx) = locator.try_resolve(frame) else {
+ return Vec::new();
+ };
+ let symbols: Vec<SymbolEntry> = ctx.iter_symbols().map(|sym| SymbolEntry::new(sym)).collect();
+ symbols
+ }
+
+ fn get_symbols_superblock(locator: &SuperblockLocator, frame: &Frame) -> Vec<SymbolEntry> {
+ let Some(ctx) = locator.try_resolve(frame) else {
+ return Vec::new();
+ };
+ let symbols: Vec<SymbolEntry> = ctx.iter_symbols(None).map(|sym| SymbolEntry::new(sym)).collect();
+ symbols
+ }
+
+ fn get_symbols_transform_unit(_locator: &TransformUnitLocator, _frame: &Frame) -> Vec<SymbolEntry> {
+ // TODO(comc): Transform units don't currently have symbol info tracked.
+ Vec::new()
+ }
+
+ fn calculate_coding_unit(locator: &CodingUnitLocator, frame: &Frame) -> Result<Vec<FieldEntry>, FrameError> {
+ let ctx = locator
+ .try_resolve(frame)
+ .ok_or(FrameError::Internal("Invalid coding unit locator".into()))?;
+ let coding_unit = ctx.coding_unit;
+
+ let mut fields = Vec::new();
+
+ fields.push(FieldEntry::new("Type".into(), "Coding block".into()));
+ let width = coding_unit.width();
+ let height = coding_unit.height();
+ fields.push(FieldEntry::new("Width".into(), width.to_string()));
+ fields.push(FieldEntry::new("Height".into(), height.to_string()));
+ let x = coding_unit.x();
+ let y = coding_unit.y();
+ fields.push(FieldEntry::new("Position".into(), format!("(x={}, y={})", x, y)));
+ if coding_unit.has_luma()? {
+ let mode_name = coding_unit.lookup_mode_name(frame)?;
+ let mode_is_directional = mode_name.ends_with("_PRED");
+ fields.push(FieldEntry::new("Prediction Mode".into(), mode_name));
+ // TODO(comc): Make this a method.
+ if mode_is_directional {
+ if let Some(delta) = coding_unit.luma_mode_angle_delta(frame) {
+ fields.push(FieldEntry::new("Angle delta".into(), delta.to_string()));
+ }
+ } else {
+ let motion_mode = coding_unit.get_prediction_mode()?.motion_mode;
+ if let Ok(motion_mode_name) = frame.enum_lookup(ProtoEnumMapping::MotionMode, motion_mode) {
+ // TODO(comc): Make this a method.
+ let is_compound = motion_mode_name.contains('_');
+ fields.push(FieldEntry::new("Motion mode".into(), motion_mode_name));
+ if let Ok(mv_prec_name) = coding_unit.lookup_motion_vector_precision_name(frame) {
+ fields.push(FieldEntry::new("MV precision".into(), mv_prec_name));
+ }
+ let num_mvs = if is_compound { 2 } else { 1 };
+ for i in 0..num_mvs {
+ if let Some(mv) = coding_unit.get_prediction_mode()?.motion_vectors.get(i as usize) {
+ let ref_frame = mv.ref_frame;
+ if ref_frame == -1 {
+ continue;
+ }
+ let dx = mv.dx as f32 / MOTION_VECTOR_PRECISION;
+ let dy = mv.dy as f32 / MOTION_VECTOR_PRECISION;
+ let mut order_hint = mv.ref_frame_order_hint.to_string();
+ if mv.ref_frame_is_tip {
+ order_hint = "TIP".to_string();
+ }
+ fields.push(FieldEntry::new(
+ "Motion vector".into(),
+ format!("{} ({}): dx={}, dy={}", ref_frame, order_hint, dx, dy),
+ ));
+ }
+ }
+ }
+ }
+ }
+ if coding_unit.has_chroma()? {
+ let uv_mode = coding_unit.lookup_uv_mode_name(frame)?;
+ if uv_mode != "UV_MODE_INVALID" {
+ fields.push(FieldEntry::new("UV Prediction Mode".into(), uv_mode));
+
+ if let Some(delta) = coding_unit.chroma_mode_angle_delta(frame) {
+ fields.push(FieldEntry::new("UV angle delta".into(), delta.to_string()));
+ }
+ }
+ }
+
+ // TODO(comc): Disambiguate skip mode vs skip txfm.
+ fields.push(FieldEntry::new("Skip Mode".into(), coding_unit.skip.to_string()));
+ fields.push(FieldEntry::new(
+ "Use Intra BC".into(),
+ coding_unit.get_prediction_mode()?.use_intrabc.to_string(),
+ ));
+ fields.push(FieldEntry::new("QIndex".into(), coding_unit.qindex.to_string()));
+ let bits = ctx.total_bits();
+ fields.push(FieldEntry::new("Bits".into(), bits.to_string()));
+ let num_transform_units = coding_unit.transform_planes[0].transform_units.len();
+ fields.push(FieldEntry::new(
+ "Transform units".into(),
+ num_transform_units.to_string(),
+ ));
+ Ok(fields)
+ }
+
+ fn calculate_transform_unit(locator: &TransformUnitLocator, frame: &Frame) -> Result<Vec<FieldEntry>, FrameError> {
+ let ctx = locator
+ .try_resolve(frame)
+ .ok_or(FrameError::Internal("Invalid transform unit locator".into()))?;
+ let transform_unit = ctx.transform_unit;
+ let mut fields = Vec::new();
+ let width = transform_unit.width();
+ let height = transform_unit.height();
+ fields.push(FieldEntry::new("Type".into(), "Transform block".into()));
+ fields.push(FieldEntry::new("Width".into(), width.to_string()));
+ fields.push(FieldEntry::new("Height".into(), height.to_string()));
+ let x = transform_unit.x();
+ let y = transform_unit.y();
+ fields.push(FieldEntry::new("Position".into(), format!("(x={}, y={})", x, y)));
+
+ let tx_type = transform_unit.primary_tx_type_or_skip(frame);
+ fields.push(FieldEntry::new("TX type".into(), tx_type));
+
+ Ok(fields)
+ }
+
+ fn calculate_partition(locator: &PartitionLocator, frame: &Frame) -> Result<Vec<FieldEntry>, FrameError> {
+ let ctx = locator
+ .try_resolve(frame)
+ .ok_or(FrameError::Internal("Invalid partition locator".into()))?;
+ let partition = ctx.partition;
+ let mut fields = Vec::new();
+ let width = partition.width();
+ let height = partition.height();
+ fields.push(FieldEntry::new("Type".into(), "Partition block".into()));
+ fields.push(FieldEntry::new("Width".into(), width.to_string()));
+ fields.push(FieldEntry::new("Height".into(), height.to_string()));
+ let x = partition.x();
+ let y = partition.y();
+ fields.push(FieldEntry::new("Position".into(), format!("(x={}, y={})", x, y)));
+
+ let partition_type = partition.partition_type;
+ let partition_type_name = frame
+ .enum_lookup(ProtoEnumMapping::PartitionType, partition_type)
+ .unwrap_or("UNKNOWN".into());
+ fields.push(FieldEntry::new("Partition type".into(), partition_type_name));
+ Ok(fields)
+ }
+
+ fn calculate_superblock(locator: &SuperblockLocator, frame: &Frame) -> Result<Vec<FieldEntry>, FrameError> {
+ let ctx = locator
+ .try_resolve(frame)
+ .ok_or(FrameError::Internal("Invalid superblock locator".into()))?;
+ let superblock = ctx.superblock;
+ let mut fields = Vec::new();
+ let width = superblock.width();
+ let height = superblock.height();
+ fields.push(FieldEntry::new("Type".into(), "Superblock".into()));
+ fields.push(FieldEntry::new("Width".into(), width.to_string()));
+ fields.push(FieldEntry::new("Height".into(), height.to_string()));
+ let x = superblock.x();
+ let y = superblock.y();
+ fields.push(FieldEntry::new("Position".into(), format!("(x={}, y={})", x, y)));
+ // TODO(comc): Switch between luma and chroma partition trees here.
+ if let Some(partition_root) = &superblock.luma_partition_tree {
+ let partition_type = partition_root.partition_type;
+ let partition_type_name = frame
+ .enum_lookup(ProtoEnumMapping::PartitionType, partition_type)
+ .unwrap_or("UNKNOWN".into());
+ fields.push(FieldEntry::new("Partition type".into(), partition_type_name));
+ }
+ Ok(fields)
+ }
+
+ fn calculate(kind: &SelectedObjectKind, frame: &Frame) -> Result<Self, FrameError> {
+ let fields = match kind {
+ SelectedObjectKind::TransformUnit(obj) => Self::calculate_transform_unit(obj, frame)?,
+ SelectedObjectKind::CodingUnit(obj) => Self::calculate_coding_unit(obj, frame)?,
+ SelectedObjectKind::Partition(obj) => Self::calculate_partition(obj, frame)?,
+ SelectedObjectKind::Superblock(obj) => Self::calculate_superblock(obj, frame)?,
+ };
+ // TODO(comc): Make children selectable.
+ // for (i, child) in kind.get_children(frame).iter().enumerate() {
+ // if let Some(rect) = child.rect(frame) {
+ // let width = rect.width();
+ // let height = rect.height();
+ // let x = rect.left();
+ // let y = rect.top();
+ // fields.push(FieldEntry::new(
+ // format!("Child {i}"),
+ // format!("{width}x{height} at ({x},{y})"),
+ // ));
+ // }
+ // }
+
+ let symbols = match kind {
+ SelectedObjectKind::TransformUnit(obj) => Self::get_symbols_transform_unit(obj, frame),
+ SelectedObjectKind::CodingUnit(obj) => Self::get_symbols_coding_unit(obj, frame),
+ SelectedObjectKind::Partition(obj) => Self::get_symbols_partition(obj, frame),
+ SelectedObjectKind::Superblock(obj) => Self::get_symbols_superblock(obj, frame),
+ };
+ Ok(Self { fields, symbols })
+ }
+}
+
+// TODO(comc): Option to show partition blocks at user-selected depth.
+#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
+pub enum SelectedObjectKind {
+ TransformUnit(TransformUnitLocator),
+ CodingUnit(CodingUnitLocator),
+ Partition(PartitionLocator),
+ Superblock(SuperblockLocator),
+}
+impl SelectedObjectKind {
+ pub fn rect(&self, frame: &Frame) -> Option<Rect> {
+ match self {
+ SelectedObjectKind::TransformUnit(obj) => obj.try_resolve(frame).map(|ctx| ctx.transform_unit.rect()),
+ SelectedObjectKind::CodingUnit(obj) => obj.try_resolve(frame).map(|ctx| ctx.coding_unit.rect()),
+ SelectedObjectKind::Partition(obj) => obj.try_resolve(frame).map(|ctx| ctx.partition.rect()),
+ SelectedObjectKind::Superblock(obj) => obj.try_resolve(frame).map(|ctx| ctx.superblock.rect()),
+ }
+ }
+
+ pub fn get_parent(&self, frame: &Frame) -> Option<SelectedObjectKind> {
+ match self {
+ SelectedObjectKind::TransformUnit(obj) => {
+ let ctx = obj.try_resolve(frame)?;
+ Some(SelectedObjectKind::CodingUnit(ctx.coding_unit_context.locator))
+ }
+
+ SelectedObjectKind::CodingUnit(obj) => {
+ let ctx = obj.try_resolve(frame)?;
+ let parent = ctx.find_parent_partition()?;
+ if parent.is_root() {
+ Some(SelectedObjectKind::Superblock(ctx.superblock_context.locator))
+ } else {
+ // Partition tree leaf nodes map directly to coding units. To get the actual parent, we need to go one more level up the hierarchy.
+ let actual_parent = parent.locator.parent().unwrap();
+ if actual_parent.is_root() {
+ Some(SelectedObjectKind::Superblock(ctx.superblock_context.locator))
+ } else {
+ Some(SelectedObjectKind::Partition(actual_parent))
+ }
+ }
+ }
+
+ SelectedObjectKind::Partition(obj) => {
+ let ctx = obj.try_resolve(frame)?;
+ if ctx.is_root() {
+ Some(SelectedObjectKind::Superblock(ctx.superblock_context.locator))
+ } else {
+ let parent = ctx.locator.parent().unwrap();
+ if parent.is_root() {
+ Some(SelectedObjectKind::Superblock(ctx.superblock_context.locator))
+ } else {
+ Some(SelectedObjectKind::Partition(ctx.locator.parent().unwrap()))
+ }
+ }
+ }
+ _ => None,
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn get_children(&self, frame: &Frame) -> Vec<SelectedObjectKind> {
+ let mut children = Vec::new();
+ match self {
+ SelectedObjectKind::TransformUnit(_obj) => {}
+ SelectedObjectKind::CodingUnit(obj) => {
+ if let Some(ctx) = obj.try_resolve(frame) {
+ let kind = ctx.locator.kind;
+ let plane = match kind {
+ CodingUnitKind::LumaOnly | CodingUnitKind::Shared => Plane::Y,
+ CodingUnitKind::ChromaOnly => Plane::U,
+ };
+ // TODO(comc): Iterate over all three planes depending on plane view settings.
+ children.extend(
+ ctx.iter_transform_units(plane)
+ .map(|transform_unit| SelectedObjectKind::TransformUnit(transform_unit.locator)),
+ )
+ }
+ }
+
+ SelectedObjectKind::Partition(obj) => {
+ if let Some(ctx) = obj.try_resolve(frame) {
+ for (i, child) in ctx.iter_direct_children().enumerate() {
+ if child.partition.is_leaf_node {
+ if let Some(coding_unit_range) = &child.partition.coding_unit_range {
+ let coding_unit_index = coding_unit_range.start as usize;
+ let locator = CodingUnitLocator::new(
+ ctx.superblock_context.locator,
+ ctx.locator.kind,
+ coding_unit_index,
+ );
+ children.push(SelectedObjectKind::CodingUnit(locator));
+ }
+ } else {
+ let mut locator = ctx.locator.clone();
+ locator.path_indices.push(i);
+ children.push(SelectedObjectKind::Partition(locator));
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+ children
+ }
+}
+
+pub struct SelectedObject {
+ pub kind: SelectedObjectKind,
+ pub info: CachedInfo<Result<SelectedObjectInfo, FrameError>>,
+}
+impl SelectedObject {
+ pub fn rect(&self, frame: &Frame) -> Option<Rect> {
+ self.kind.rect(frame)
+ }
+
+ // TODO(comc): Refactor.
+ pub fn get_or_calculate_info(&mut self, frame: &Frame) -> &Result<SelectedObjectInfo, FrameError> {
+ if matches!(self.info, CachedInfo::Missing) {
+ self.info = CachedInfo::Calculated(SelectedObjectInfo::calculate(&self.kind, frame));
+ }
+ let CachedInfo::Calculated(info) = &self.info else {
+ panic!("Info is not calculated.");
+ };
+ info
+ }
+ pub fn new(kind: SelectedObjectKind) -> Self {
+ Self {
+ kind,
+ info: CachedInfo::Missing,
+ }
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/menu_bar.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/menu_bar.rs
new file mode 100644
index 0000000..6120862
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/menu_bar.rs
@@ -0,0 +1,89 @@
+use egui::{RichText, Ui};
+
+use crate::app_state::AppState;
+use crate::settings::DistortionView;
+use crate::views::RenderView;
+use crate::views::ViewMode;
+
+pub struct MenuBar;
+
+impl RenderView for MenuBar {
+ fn title(&self) -> String {
+ "Menu".into()
+ }
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ egui::menu::bar(ui, |ui| {
+ ui.menu_button("File", |ui| {
+ if ui.button("Open Local Stream (.zip)").clicked() {
+ state.local_stream_manager.prompt_local_stream();
+ ui.close_menu();
+ }
+ if state.settings.sharable.show_remote_streams {
+ if ui.button("Open Remote Stream").clicked() {
+ state.settings.show_stream_select = true;
+ ui.close_menu();
+ }
+ if ui.button("Decode Stream on Server").clicked() {
+ state.settings.show_decode_progress = true;
+ state.server_decode_manager.prompt_stream();
+ ui.close_menu();
+ }
+ }
+ if ui.button("Open Demo Stream").clicked() {
+ state.local_stream_manager.load_demo_stream();
+ ui.close_menu();
+ }
+ });
+ ui.menu_button("View Mode", |ui| {
+ let current_mode = state.settings.sharable.view_mode;
+ let mut have_orig_yuv = false;
+ if let Some(stream) = state.stream.as_ref() {
+ if stream.have_orig_yuv() {
+ have_orig_yuv = true;
+ }
+ }
+
+ for view_mode in [
+ ViewMode::CodingFlow,
+ ViewMode::Prediction,
+ ViewMode::Transform,
+ ViewMode::Filters,
+ ViewMode::Distortion(DistortionView::Distortion),
+ ViewMode::Motion,
+ ViewMode::Heatmap,
+ ] {
+ let prefix = if current_mode == view_mode { "• " } else { " " };
+ let mut text = RichText::new(format!("{}{}", prefix, view_mode));
+ if current_mode == view_mode {
+ text = text.strong();
+ }
+ let enabled = !matches!(view_mode, ViewMode::Distortion(_)) || have_orig_yuv;
+ if ui.add_enabled(enabled, egui::Button::new(text)).clicked() {
+ state.settings.sharable.view_mode = view_mode;
+ ui.close_menu();
+ }
+ }
+ });
+
+ ui.menu_button("Settings", |ui| {
+ if ui.button("Edit Settings").clicked() {
+ state.settings.show_settings_window = true;
+ ui.close_menu();
+ }
+ });
+
+ ui.menu_button("Window", |ui| {
+ if ui.button("Decode Progress").clicked() {
+ state.settings.show_decode_progress = true;
+ ui.close_menu();
+ }
+
+ if ui.button("Performance").clicked() {
+ state.settings.show_performance_window = true;
+ ui.close_menu();
+ }
+ });
+ });
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/mod.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/mod.rs
new file mode 100644
index 0000000..0b74408
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/mod.rs
@@ -0,0 +1,39 @@
+mod block_info_viewer;
+mod coeffs_viewer;
+mod controls_viewer;
+mod decode_progress_viewer;
+mod detailed_pixel_viewer;
+mod drag_and_drop;
+mod frame_info_viewer;
+mod frame_select_viewer;
+mod frame_viewer;
+mod menu_bar;
+mod performance_viewer;
+mod render_view;
+mod settings_viewer;
+mod stats_viewer;
+mod stream_select_viewer;
+mod symbol_info_viewer;
+mod view_mode;
+
+pub use block_info_viewer::BlockInfoViewer;
+pub use coeffs_viewer::CoeffsViewer;
+pub use controls_viewer::ControlsViewer;
+pub use decode_progress_viewer::DecodeProgressViewer;
+pub use detailed_pixel_viewer::DetailedPixelViewer;
+pub use drag_and_drop::handle_drag_and_drop;
+pub use frame_info_viewer::FrameInfoViewer;
+pub use frame_select_viewer::FrameSelectViewer;
+pub use frame_viewer::{FrameViewer, ScreenBounds, SelectedObject, SelectedObjectKind};
+pub use menu_bar::MenuBar;
+pub use performance_viewer::{PerformanceHistory, PerformanceViewer};
+pub use render_view::RenderView;
+pub use settings_viewer::SettingsViewer;
+pub use stats_viewer::StatsViewer;
+pub use stream_select_viewer::StreamSelectViewer;
+pub use symbol_info_viewer::SymbolInfoViewer;
+pub use view_mode::ViewMode;
+
+pub const MIN_BLOCK_SIZE_TO_RENDER: f32 = 2.0;
+pub const MIN_BLOCK_HEIGHT_FOR_TEXT: f32 = 12.0;
+pub const MIN_BLOCK_WIDTH_FOR_TEXT: f32 = 30.0;
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/performance_viewer.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/performance_viewer.rs
new file mode 100644
index 0000000..e67031c
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/performance_viewer.rs
@@ -0,0 +1,61 @@
+use egui::util::History;
+use egui::Ui;
+
+use crate::app_state::AppState;
+use crate::views::render_view::RenderView;
+
+pub struct PerformanceViewer;
+
+pub struct PerformanceHistory {
+ frame_times: History<f32>,
+}
+
+impl Default for PerformanceHistory {
+ fn default() -> Self {
+ Self {
+ frame_times: History::new(2..100, 1.0),
+ }
+ }
+}
+
+impl PerformanceHistory {
+ pub fn on_new_frame(&mut self, now: f64, previous_frame_time: Option<f32>) {
+ let previous_frame_time = previous_frame_time.unwrap_or_default();
+ if let Some(latest) = self.frame_times.latest_mut() {
+ *latest = previous_frame_time;
+ }
+ self.frame_times.add(now, previous_frame_time);
+ }
+
+ pub fn mean_frame_time(&self) -> f32 {
+ self.frame_times.average().unwrap_or_default()
+ }
+
+ pub fn fps(&self) -> f32 {
+ 1.0 / self.frame_times.mean_time_interval().unwrap_or_default()
+ }
+
+ pub fn ui(&mut self, ui: &mut egui::Ui) {
+ ui.ctx().request_repaint();
+ ui.label(format!("FPS: {:.1}", self.fps()));
+ ui.label(format!(
+ "Mean CPU usage: {:.2} ms / frame",
+ 1e3 * self.mean_frame_time()
+ ));
+
+ let mem_use = re_memory::MemoryUse::capture();
+ if let Some(counted) = mem_use.counted {
+ ui.label(format!("Memory usage: {}MiB", counted / 1024 / 1024));
+ }
+ }
+}
+
+impl RenderView for PerformanceViewer {
+ fn title(&self) -> String {
+ "Performance".into()
+ }
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ state.performance_history.ui(ui);
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/render_view.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/render_view.rs
new file mode 100644
index 0000000..077c402
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/render_view.rs
@@ -0,0 +1,14 @@
+use egui::Ui;
+
+use crate::app_state::AppState;
+
+pub trait RenderView {
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()>;
+ fn title(&self) -> String;
+}
+
+impl PartialEq for dyn RenderView {
+ fn eq(&self, other: &Self) -> bool {
+ self.title() == other.title()
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/settings_viewer.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/settings_viewer.rs
new file mode 100644
index 0000000..1f69d47
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/settings_viewer.rs
@@ -0,0 +1,80 @@
+use egui::{TextEdit, Ui};
+
+use crate::app_state::AppState;
+use crate::settings::PersistentSettings;
+use crate::views::render_view::RenderView;
+
+pub struct SettingsViewer;
+
+impl RenderView for SettingsViewer {
+ fn title(&self) -> String {
+ "Settings".into()
+ }
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ let settings = &mut state.settings;
+ egui::ScrollArea::vertical().show(ui, |ui| {
+ if ui.button("Reset to defaults").clicked() {
+ settings.persistent = PersistentSettings::default();
+ }
+ ui.label("AVM source root URL:");
+ let text_edit = TextEdit::singleline(&mut settings.persistent.avm_source_url).desired_width(800.0);
+ text_edit.show(ui);
+
+ ui.horizontal(|ui| {
+ let mut apply_cache_strategy = settings.persistent.apply_cache_strategy;
+ let mut cache_strategy_limit = settings.persistent.cache_strategy_limit;
+ ui.checkbox(&mut apply_cache_strategy, "Limit number of frames kept in memory");
+ ui.add_enabled(
+ apply_cache_strategy,
+ egui::Slider::new(&mut cache_strategy_limit, 1..=100),
+ );
+ settings.persistent.apply_cache_strategy = apply_cache_strategy;
+ settings.persistent.cache_strategy_limit = cache_strategy_limit;
+ });
+
+ ui.checkbox(
+ &mut settings.persistent.update_sharable_url,
+ "Update URL with sharable state",
+ );
+
+ let style = &mut settings.persistent.style.overlay;
+ ui.horizontal(|ui| {
+ ui.label("Highlighted object color:");
+ ui.color_edit_button_srgba(&mut style.highlighted_object_stroke.color);
+ });
+
+ ui.horizontal(|ui| {
+ ui.label("Selected object color:");
+ ui.color_edit_button_srgba(&mut style.selected_object_stroke.color);
+ });
+
+ ui.horizontal(|ui| {
+ ui.label("Coding unit color:");
+ ui.color_edit_button_srgba(&mut style.coding_unit_stroke.color);
+ });
+
+ ui.horizontal(|ui| {
+ ui.label("Transform unit color:");
+ ui.color_edit_button_srgba(&mut style.transform_unit_stroke.color);
+ });
+
+ ui.horizontal(|ui| {
+ ui.label("Superblock color:");
+ ui.color_edit_button_srgba(&mut style.superblock_stroke.color);
+ });
+
+ ui.horizontal(|ui| {
+ ui.label("Mode name color:");
+ ui.color_edit_button_srgba(&mut style.mode_name_color);
+ });
+
+ ui.horizontal(|ui| {
+ ui.label("Pixel / coeffs viewer text color:");
+ ui.color_edit_button_srgba(&mut style.pixel_viewer_text_color);
+ });
+
+ ui.checkbox(&mut style.enable_text_shadows, "Enable text shadows");
+ });
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/stats_viewer/mod.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/stats_viewer/mod.rs
new file mode 100644
index 0000000..fa25a8c
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/stats_viewer/mod.rs
@@ -0,0 +1,235 @@
+mod pie_plot;
+
+use pie_plot::PiePlot;
+
+use anyhow::{anyhow, Context};
+use avm_stats::{FrameStatistic, StatSortMode};
+use egui::Ui;
+use log::warn;
+use std::fmt::Write;
+use wasm_bindgen::JsCast;
+use web_sys::HtmlElement;
+
+use crate::views::render_view::RenderView;
+use crate::{app_state::AppState, stream::CurrentFrame};
+pub struct StatsViewer;
+
+// TODO(comc): Move to common location.
+pub fn create_file_download(bytes: &[u8], file_name: &str) -> anyhow::Result<()> {
+ let uint8arr = js_sys::Uint8Array::new(&unsafe { js_sys::Uint8Array::view(bytes) }.into());
+ let array = js_sys::Array::new();
+ array.push(&uint8arr.buffer());
+ let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(
+ &array,
+ web_sys::BlobPropertyBag::new().type_("application/octet-stream"),
+ )
+ .map_err(|_| anyhow!("Blob error"))?;
+ let download_url = web_sys::Url::create_object_url_with_blob(&blob).map_err(|_| anyhow!("Blob error"))?;
+ let window = web_sys::window().context("No window")?;
+ let document = window.document().context("No document")?;
+ let body = document.body().context("No body")?;
+ let dummy = document.create_element("a").map_err(|_| anyhow!("Element error"))?;
+ let dummy: HtmlElement = dummy
+ .dyn_into::<web_sys::HtmlElement>()
+ .map_err(|_| anyhow!("Element error"))?;
+ body.append_child(&dummy).map_err(|_| anyhow!("Element error"))?;
+ dummy
+ .set_attribute("href", &download_url)
+ .map_err(|_| anyhow!("Element error"))?;
+ dummy
+ .set_attribute("download", file_name)
+ .map_err(|_| anyhow!("Element error"))?;
+ dummy
+ .set_attribute("style", "display: none")
+ .map_err(|_| anyhow!("Element error"))?;
+ dummy.click();
+ web_sys::Url::revoke_object_url(&download_url).map_err(|_| anyhow!("Revoke URL error"))?;
+ Ok(())
+}
+
+// TODO(comc): Scroll wheel zoom, export stats, bar chart mode, table mode.
+impl RenderView for StatsViewer {
+ fn title(&self) -> String {
+ "Frame Stats".into()
+ }
+
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ let Some(frame) = state.stream.current_frame() else {
+ // No frame loaded yet.
+ return Ok(());
+ };
+ let stats_settings = &mut state.settings.sharable.stats_settings;
+ let prev_state = (state.settings.sharable.selected_stat, stats_settings.clone());
+
+ let mut selected_stat = state.settings.sharable.selected_stat;
+ egui::ComboBox::from_label("Statistic")
+ .selected_text(selected_stat.name())
+ .show_ui(ui, |ui| {
+ for stat in &[
+ FrameStatistic::LumaModes,
+ FrameStatistic::ChromaModes,
+ FrameStatistic::BlockSizes,
+ FrameStatistic::Symbols,
+ FrameStatistic::PartitionSplit,
+ ] {
+ ui.selectable_value(&mut selected_stat, *stat, stat.name());
+ }
+ });
+ ui.end_row();
+ state.settings.sharable.selected_stat = selected_stat;
+ if matches!(selected_stat, FrameStatistic::PartitionSplit) {
+ ui.horizontal(|ui| {
+ ui.label("Block sizes:")
+ .on_hover_text("Comma separated list of block sizes, e.g. \"64x64,128x128\".");
+ ui.text_edit_singleline(&mut stats_settings.partition_split_block_sizes);
+ ui.end_row();
+ });
+ }
+
+ let mut export_data = false;
+ ui.horizontal(|ui| {
+ // TODO(comc): Find a way to implement screenshot on web.
+ // if ui.button("Save plot").clicked() {
+ // ui.ctx().send_viewport_cmd(egui::ViewportCommand::Screenshot);
+ // }
+ if ui.button("Export data").clicked() {
+ export_data = true;
+ }
+ });
+ let mut sort_by = stats_settings.sort_by;
+ egui::ComboBox::from_label("Sort mode")
+ .selected_text(sort_by.name())
+ .show_ui(ui, |ui| {
+ for sort_mode in &[StatSortMode::ByName, StatSortMode::ByValue] {
+ ui.selectable_value(&mut sort_by, *sort_mode, sort_mode.name());
+ }
+ });
+ ui.end_row();
+ stats_settings.sort_by = sort_by;
+
+ let mut show_relative_total = stats_settings.show_relative_total;
+ ui.checkbox(&mut show_relative_total, "Show relative total");
+ ui.end_row();
+ stats_settings.show_relative_total = show_relative_total;
+
+ ui.horizontal(|ui| {
+ let mut apply_limit_count = stats_settings.apply_limit_count;
+ let mut limit_count = stats_settings.limit_count;
+ ui.checkbox(&mut apply_limit_count, "Limit Top N");
+ ui.add_enabled(apply_limit_count, egui::Slider::new(&mut limit_count, 1..=50));
+ stats_settings.apply_limit_count = apply_limit_count;
+ stats_settings.limit_count = limit_count;
+ });
+ ui.end_row();
+
+ ui.horizontal(|ui| {
+ let mut apply_limit_frac = stats_settings.apply_limit_frac;
+ let mut limit_frac = stats_settings.limit_frac * 100.0;
+ ui.checkbox(&mut apply_limit_frac, "Threshold Percent");
+ ui.add_enabled(
+ apply_limit_frac,
+ egui::Slider::new(&mut limit_frac, 0.0..=50.0).step_by(0.1),
+ );
+ stats_settings.apply_limit_frac = apply_limit_frac;
+ stats_settings.limit_frac = limit_frac / 100.0;
+ });
+ ui.end_row();
+
+ ui.horizontal(|ui| {
+ ui.label("Include:");
+ let mut include_filter_exact_match = stats_settings.include_filter_exact_match;
+ ui.checkbox(&mut include_filter_exact_match, "Exact match");
+ ui.text_edit_singleline(&mut stats_settings.include_filter);
+ stats_settings.include_filter_exact_match = include_filter_exact_match;
+ });
+ ui.end_row();
+
+ ui.horizontal(|ui| {
+ ui.label("Exclude:");
+ let mut exclude_filter_exact_match = stats_settings.exclude_filter_exact_match;
+ ui.checkbox(&mut exclude_filter_exact_match, "Exact match");
+ ui.text_edit_singleline(&mut stats_settings.exclude_filter);
+ stats_settings.exclude_filter_exact_match = exclude_filter_exact_match;
+ });
+ ui.end_row();
+
+ let decimal_precision = match selected_stat {
+ FrameStatistic::LumaModes => 0,
+ FrameStatistic::ChromaModes => 0,
+ FrameStatistic::BlockSizes => 0,
+ FrameStatistic::Symbols => 2,
+ FrameStatistic::PartitionSplit => 0,
+ };
+
+ let pie_plot = PiePlot {
+ decimal_precision,
+ ..Default::default()
+ };
+
+ let changed = state.settings.sharable.selected_stat != prev_state.0 || *stats_settings != prev_state.1;
+ let calculate_data = changed || state.settings.cached_stat_data.is_none();
+
+ if calculate_data {
+ state.settings.cached_stat_data = Some(selected_stat.calculate(frame, stats_settings));
+ }
+ let _resp = pie_plot.show(ui, state.settings.cached_stat_data.as_ref().unwrap());
+ if export_data {
+ if let Some(stream) = &state.stream {
+ let file_name = format!(
+ "{}_frame_{:04}_{}.csv",
+ stream.stream_info.stream_name,
+ stream.current_frame_index,
+ selected_stat.name().replace(' ', "_")
+ );
+
+ let data = state.settings.cached_stat_data.as_ref().unwrap();
+ let total: f64 = data.iter().map(|sample| sample.value).sum();
+ let header = format!("{},Count,Percent\n", selected_stat.name());
+ let csv: String = data.iter().fold(String::new(), |mut output, sample| {
+ let _ = writeln!(output, "{},{},{}", sample.name, sample.value, sample.value / total);
+ output
+ });
+ let mut bytes: Vec<u8> = header.bytes().collect();
+ bytes.extend(csv.bytes());
+
+ if let Err(err) = create_file_download(&bytes, &file_name) {
+ warn!("Failed to create file download: {err:?}");
+ }
+ }
+ }
+
+ // TODO(comc): Find a way to implement screenshot on web.
+ // let screenshot = ui.ctx().input(|i| {
+ // for event in &i.raw.events {
+ // if let egui::Event::Screenshot { image, .. } = event {
+ // return Some(image.clone());
+ // }
+ // }
+ // None
+ // });
+
+ // if let Some(screenshot) = screenshot {
+ // let mut bytes = Vec::new();
+ // let png_encoder = image::codecs::png::PngEncoder::new(Cursor::new(&mut bytes));
+ // let pixels_per_point = ui.ctx().pixels_per_point();
+ // let plot = screenshot.region(&resp.response.rect, Some(pixels_per_point));
+ // match png_encoder.write_image(
+ // plot.as_raw(),
+ // plot.width() as u32,
+ // plot.height() as u32,
+ // image::ColorType::Rgba8,
+ // ) {
+ // Ok(_) => {
+ // if let Err(err) = create_file_download(&bytes, "test.png") {
+ // warn!("Failed to create file download: {err:?}");
+ // }
+ // }
+ // Err(err) => {
+ // warn!("Failed to encode PNG: {err:?}");
+ // }
+ // }
+ // }
+
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/stats_viewer/pie_plot.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/stats_viewer/pie_plot.rs
new file mode 100644
index 0000000..9e2a916
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/stats_viewer/pie_plot.rs
@@ -0,0 +1,98 @@
+use std::f64::consts::PI;
+
+use avm_stats::Sample;
+use egui::RichText;
+use egui_plot::{Plot, PlotPoint, PlotPoints, PlotResponse, Polygon, Text};
+
+pub struct PiePlot {
+ pub num_vertices: usize,
+ pub decimal_precision: usize,
+}
+
+impl Default for PiePlot {
+ fn default() -> Self {
+ Self {
+ num_vertices: 360,
+ decimal_precision: 2,
+ }
+ }
+}
+
+impl PiePlot {
+ pub fn show(&self, ui: &mut egui::Ui, data: &[Sample]) -> PlotResponse<()> {
+ let total: f64 = data.iter().map(|sample| sample.value).sum();
+ let mut cumulative_sum = 0.0;
+
+ Plot::new("pie_plot")
+ .show_background(false)
+ .show_axes([false; 2])
+ .clamp_grid(true)
+ .show_grid(false)
+ .allow_boxed_zoom(false)
+ .allow_drag(false)
+ .allow_zoom(false)
+ .allow_scroll(false)
+ .data_aspect(1.0)
+ .show_x(false)
+ .show_y(false)
+ .include_x(-1.1)
+ .include_x(1.1)
+ .include_y(-1.1)
+ .include_y(1.1)
+ .show(ui, |plot_ui| {
+ for Sample { name, value } in data.iter() {
+ let fraction = value / total;
+ let num_vertices = (self.num_vertices as f64 * fraction).ceil() as usize;
+ let start_angle = 2.0 * PI * cumulative_sum;
+ let end_angle = 2.0 * PI * (cumulative_sum + fraction);
+ cumulative_sum += fraction;
+ let mut points = vec![];
+
+ if data.len() > 1 {
+ points.push([0.0, 0.0]);
+ }
+
+ let angle_step = (end_angle - start_angle) / num_vertices as f64;
+ points.extend((0..=num_vertices).map(|i| {
+ let angle = start_angle + angle_step * i as f64;
+ [angle.sin(), angle.cos()]
+ }));
+
+ let center_angle = start_angle + (end_angle - start_angle) / 2.0;
+ let center_x = 0.75 * center_angle.sin();
+ let center_y = 0.75 * center_angle.cos();
+
+ let hovered = plot_ui
+ .pointer_coordinate()
+ .map(|pointer| {
+ let radius = pointer.y.hypot(pointer.x);
+ let mut theta = pointer.x.atan2(pointer.y);
+ if theta < 0.0 {
+ theta += 2.0 * PI;
+ }
+ radius < 1.0 && start_angle < theta && theta < end_angle
+ })
+ .unwrap_or_default();
+
+ plot_ui.polygon(Polygon::new(PlotPoints::new(points)).name(name).highlight(hovered));
+
+ let label = format!("{} - {:.2}%", name, 100.0 * value / total);
+ plot_ui.text(Text::new(PlotPoint::new(center_x, center_y), label));
+
+ if hovered {
+ let pointer = plot_ui.pointer_coordinate().unwrap();
+ let label = format!(
+ "{} - {:.2}% ({:.prec$}/{:.prec$})",
+ name,
+ 100.0 * fraction,
+ value,
+ total,
+ prec = self.decimal_precision
+ );
+
+ plot_ui.text(Text::new(pointer, RichText::new(label).heading()).name(name));
+ }
+ }
+ })
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/stream_select_viewer.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/stream_select_viewer.rs
new file mode 100644
index 0000000..53257cb
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/stream_select_viewer.rs
@@ -0,0 +1,70 @@
+use egui::load::Bytes;
+use egui::{Image, ImageSource, RichText, Sense, Ui};
+use egui_extras::{Column, TableBuilder};
+use itertools::Itertools;
+
+use crate::app_state::AppState;
+use crate::stream::Stream;
+use crate::views::render_view::RenderView;
+
+pub struct StreamSelectViewer;
+
+impl RenderView for StreamSelectViewer {
+ fn title(&self) -> String {
+ "Stream Select".into()
+ }
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ if ui.button("Refresh stream list").clicked() {
+ state.http_stream_manager.load_stream_list();
+ }
+ let mut set_stream = None;
+ let streams = state.http_stream_manager.streams.lock().unwrap();
+ TableBuilder::new(ui)
+ .column(Column::initial(600.0).resizable(true))
+ .column(Column::remainder().resizable(false).at_least(100.0))
+ .sense(Sense::click())
+ .striped(true)
+ .header(20.0, |mut header| {
+ header.col(|ui| {
+ ui.heading("Name");
+ });
+ header.col(|ui| {
+ ui.heading("Preview");
+ });
+ })
+ .body(|body| {
+ let streams: Vec<_> = streams
+ .iter()
+ .sorted_by_key(|stream| stream.stream_name.as_str())
+ .collect();
+ let num_rows: usize = streams.len();
+ body.rows(100.0, num_rows, |mut row| {
+ let stream_info = streams[row.index()];
+ row.col(|ui| {
+ let text = RichText::new(stream_info.stream_name.as_str()).heading();
+ ui.label(text);
+ });
+ row.col(|ui| {
+ if let Some(thumbnail_png) = &stream_info.thumbnail_png {
+ // TODO(comc): Make stream_info.thumbnail_png an Arc to avoid copy.
+ let thumbnail_png = thumbnail_png.clone();
+ let uri = format!("bytes://{}", stream_info.stream_name);
+ let image = Image::new(ImageSource::Bytes {
+ uri: uri.into(),
+ bytes: Bytes::Shared(thumbnail_png.into()),
+ });
+ ui.add(image);
+ }
+ });
+ if row.response().clicked() {
+ set_stream = Some(stream_info.clone());
+ }
+ });
+ });
+ if let Some(set_stream) = set_stream {
+ state.stream = Some(Stream::from_http(set_stream, false, 0, &state.settings.sharable.streams_url)?);
+ state.settings.show_stream_select = false;
+ }
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/symbol_info_viewer.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/symbol_info_viewer.rs
new file mode 100644
index 0000000..6fd8810
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/symbol_info_viewer.rs
@@ -0,0 +1,104 @@
+use crate::views::render_view::RenderView;
+use egui::{RichText, TextBuffer, Ui};
+use egui_extras::{Column, TableBuilder};
+
+use crate::app_state::AppState;
+use crate::stream::CurrentFrame;
+
+const AVM_SOURCE_ROOT: &str = "avm/";
+
+pub struct SymbolInfoViewer;
+
+impl RenderView for SymbolInfoViewer {
+ fn title(&self) -> String {
+ "Symbol Info".into()
+ }
+
+ fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
+ let Some(frame) = state.stream.current_frame() else {
+ // No frame loaded yet.
+ return Ok(());
+ };
+ let Some(selected_object) = &mut state.settings.selected_object else {
+ // No coding unit is selected.
+ return Ok(());
+ };
+ let Ok(info) = selected_object.get_or_calculate_info(frame) else {
+ return Ok(());
+ };
+
+ ui.horizontal(|ui| {
+ ui.label("Filter:");
+ ui.text_edit_singleline(&mut state.settings.sharable.symbol_info_filter);
+ });
+ ui.end_row();
+ ui.checkbox(&mut state.settings.sharable.symbol_info_show_tags, "Show tags");
+
+ ui.separator();
+ TableBuilder::new(ui)
+ .column(Column::initial(200.0).resizable(true))
+ .column(Column::initial(100.0).resizable(true))
+ .column(Column::remainder())
+ .striped(true)
+ .header(20.0, |mut header| {
+ header.col(|ui| {
+ ui.label(RichText::new("Symbol Type").strong());
+ });
+ header.col(|ui| {
+ ui.label(RichText::new("Value").strong());
+ });
+ header.col(|ui| {
+ ui.label(RichText::new("Bits").strong());
+ });
+ })
+ .body(|body| {
+ let symbols = info.symbols.clone();
+ let symbols = if state.settings.sharable.symbol_info_filter.is_empty() {
+ symbols
+ } else {
+ symbols
+ .into_iter()
+ .filter(|row| {
+ row.func.contains(&state.settings.sharable.symbol_info_filter)
+ || (state.settings.sharable.symbol_info_show_tags
+ && row
+ .tags
+ .iter()
+ .any(|tag| tag.contains(&state.settings.sharable.symbol_info_filter)))
+ })
+ .collect()
+ };
+ body.rows(20.0, symbols.len(), |mut row| {
+ let symbol = &symbols[row.index()];
+ row.col(|col| {
+ let prefix_start = symbol.file.rfind(AVM_SOURCE_ROOT);
+ let name = if symbol.tags.is_empty() || !state.settings.sharable.symbol_info_show_tags {
+ symbol.func.to_string()
+ } else {
+ format!("{} ({})", symbol.func, symbol.tags.join("+"))
+ };
+
+ if let Some(prefix_start) = prefix_start {
+ let relative_path = symbol
+ .file
+ .char_range(prefix_start + AVM_SOURCE_ROOT.len()..symbol.file.len());
+ let url = format!(
+ "{}/{}#L{}",
+ state.settings.persistent.avm_source_url, relative_path, symbol.line
+ );
+ col.hyperlink_to(name, url);
+ } else {
+ col.label(name);
+ }
+ });
+ row.col(|col| {
+ col.label(format!("{}", symbol.value));
+ });
+ row.col(|col| {
+ col.label(format!("{}", symbol.bits));
+ });
+ });
+ });
+ Ok(())
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_app/src/views/view_mode.rs b/tools/avm_analyzer/avm_analyzer_app/src/views/view_mode.rs
new file mode 100644
index 0000000..1b78509
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_app/src/views/view_mode.rs
@@ -0,0 +1,106 @@
+use avm_stats::PixelType;
+use serde::{Deserialize, Serialize};
+
+use crate::settings::DistortionView;
+
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum ViewMode {
+ #[default]
+ CodingFlow,
+ Prediction,
+ Transform,
+ Filters,
+ Distortion(DistortionView),
+ Motion,
+ Heatmap,
+}
+
+pub struct ViewSettings {
+ pub show_superblocks: bool,
+ pub show_coding_units: bool,
+ pub show_transform_units: bool,
+ pub show_prediction_modes: bool,
+ pub show_transform_types: bool,
+ pub show_motion_vectors: bool,
+ pub pixel_type: PixelType,
+ pub show_heatmap: bool,
+ pub allow_coding_unit_selection: bool,
+ pub allow_transform_unit_selection: bool,
+}
+
+impl std::fmt::Display for ViewMode {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let s = match self {
+ ViewMode::CodingFlow => "Coding Flow",
+ ViewMode::Prediction => "Prediction",
+ ViewMode::Transform => "Transform",
+ ViewMode::Filters => "Filters",
+ ViewMode::Distortion(_) => "Distortion",
+ ViewMode::Motion => "Motion",
+ ViewMode::Heatmap => "Heatmap",
+ };
+ write!(f, "{s}")
+ }
+}
+
+impl ViewMode {
+ pub fn view_settings(&self) -> ViewSettings {
+ let show_superblocks = true;
+ let show_coding_units = true;
+ let mut show_transform_units = false;
+ let mut show_prediction_modes: bool = false;
+ let mut show_transform_types: bool = false;
+ let mut show_motion_vectors: bool = false;
+ let mut pixel_type = PixelType::Reconstruction;
+ let mut show_heatmap = false;
+ let mut allow_coding_unit_selection = true;
+ let mut allow_transform_unit_selection = false;
+ match self {
+ ViewMode::CodingFlow => {
+ pixel_type = PixelType::Reconstruction;
+ }
+ ViewMode::Prediction => {
+ pixel_type = PixelType::Prediction;
+ show_transform_units = true;
+ show_prediction_modes = true;
+ }
+ ViewMode::Transform => {
+ pixel_type = PixelType::Residual;
+ show_transform_units = true;
+ show_transform_types = true;
+ allow_coding_unit_selection = false;
+ allow_transform_unit_selection = true;
+ }
+ ViewMode::Filters => {
+ pixel_type = PixelType::FilterDelta;
+ }
+ ViewMode::Distortion(distortion_view) => {
+ pixel_type = match distortion_view {
+ DistortionView::Distortion => PixelType::Distortion,
+ DistortionView::Original => PixelType::Original,
+ DistortionView::Reconstruction => PixelType::Reconstruction,
+ }
+ }
+ ViewMode::Motion => {
+ show_motion_vectors = true;
+ pixel_type = PixelType::Prediction;
+ }
+ ViewMode::Heatmap => {
+ show_heatmap = true;
+ }
+ }
+
+ ViewSettings {
+ show_superblocks,
+ show_coding_units,
+ show_transform_units,
+ show_prediction_modes,
+ show_transform_types,
+ show_motion_vectors,
+ pixel_type,
+ show_heatmap,
+ allow_coding_unit_selection,
+ allow_transform_unit_selection,
+ }
+ }
+}
diff --git a/tools/avm_analyzer/avm_analyzer_common/Cargo.toml b/tools/avm_analyzer/avm_analyzer_common/Cargo.toml
new file mode 100644
index 0000000..096687a
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_common/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "avm-analyzer-common"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
diff --git a/tools/avm_analyzer/avm_analyzer_common/src/lib.rs b/tools/avm_analyzer/avm_analyzer_common/src/lib.rs
new file mode 100644
index 0000000..56a6707
--- /dev/null
+++ b/tools/avm_analyzer/avm_analyzer_common/src/lib.rs
@@ -0,0 +1,62 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct ProgressRequest {
+ pub stream_name: String,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct DecodeProgress {
+ pub decoded_frames: usize,
+ pub total_frames: usize,
+}
+
+// TODO(comc): Add timeout state?
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub enum DecodeState {
+ /// Upload request sent by client, not yet acknowledged by server.
+ Uploading,
+ /// Upload was successful and the server sent confirmation.
+ UploadComplete,
+ /// Decode (extract_proto) is is progress.
+ Pending(DecodeProgress),
+ /// Decoding succeeded.
+ Complete(usize),
+ /// Decode failed for any reason.
+ Failed,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct ProgressResponse {
+ pub stream_name: String,
+ pub state: DecodeState,
+}
+