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, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct StartDecodeResponse { + pub stream_info: AvmStreamInfo, +} + +pub const PROTO_PATH_FRAME_PLACEHOLDER: &str = "{FRAME}"; +pub const DEFAULT_PROTO_PATH_FRAME_SUFFIX_FIRST: &str = "_frame_0000.pb"; +pub const DEFAULT_PROTO_PATH_FRAME_SUFFIX_TEMPLATE: &str = "_frame_{FRAME}.pb"; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct AvmStreamInfo { + pub stream_name: String, + pub proto_path_template: String, + pub num_frames: usize, + pub thumbnail_png: Option<Vec<u8>>, +} + +impl AvmStreamInfo { + pub fn get_proto_path(&self, index: usize) -> String { + self.proto_path_template + .replace(PROTO_PATH_FRAME_PLACEHOLDER, &format!("{index:04}")) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct AvmStreamList { + pub streams: Vec<AvmStreamInfo>, +}
diff --git a/tools/avm_analyzer/avm_analyzer_server/Cargo.toml b/tools/avm_analyzer/avm_analyzer_server/Cargo.toml new file mode 100644 index 0000000..4cb7120 --- /dev/null +++ b/tools/avm_analyzer/avm_analyzer_server/Cargo.toml
@@ -0,0 +1,24 @@ +[package] +name = "avm-analyzer-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.79" +async-fs = "2.1.0" +async-process = "2.0.1" +avm-analyzer-common = { path = "../avm_analyzer_common" } +avm-stats = { path = "../avm_stats" } +axum = { version = "0.7", features = ["multipart"] } +clap = { version = "4.4.18", features = ["derive"] } +futures-lite = "2.2.0" +image = "0.24.8" +prost = { version = "0.11", features = ["prost-derive"]} +prost-types = "0.11" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1.0", features = ["full"] } +tower = "0.4.13" +tower-http = { version = "0.5.0", features = ["cors", "fs", "limit", "trace", "timeout"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] }
diff --git a/tools/avm_analyzer/avm_analyzer_server/src/main.rs b/tools/avm_analyzer/avm_analyzer_server/src/main.rs new file mode 100644 index 0000000..ebf71b9 --- /dev/null +++ b/tools/avm_analyzer/avm_analyzer_server/src/main.rs
@@ -0,0 +1,474 @@ +use anyhow::anyhow; +use async_process::{Command, Stdio}; +use avm_analyzer_common::{ + AvmStreamInfo, AvmStreamList, DecodeProgress, DecodeState, ProgressRequest, ProgressResponse, StartDecodeResponse, + DEFAULT_PROTO_PATH_FRAME_SUFFIX_TEMPLATE, +}; +use avm_stats::{Frame, PixelPlane, PixelType, Plane}; +use axum::{ + extract::{DefaultBodyLimit, Multipart, Query, State}, + http::StatusCode, + response::{IntoResponse, Response}, + routing::{get, post}, + Json, Router, +}; +use clap::Parser; +use futures_lite::{io::BufReader, prelude::*}; +use image::{imageops::FilterType, DynamicImage, Rgb, RgbImage}; +use prost::Message; + +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::time::Duration; +use std::{ + collections::HashMap, + io::Error, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, +}; +use tower::ServiceBuilder; +use tower_http::cors::CorsLayer; +use tower_http::limit::RequestBodyLimitLayer; +use tower_http::services::ServeDir; +use tower_http::timeout::TimeoutLayer; + +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +const PROTO_DIR_SUFFIX: &str = "_protos"; +const MAX_UPLOAD_SIZE: usize = 100 * 1024 * 1024; // 100MiB +#[derive(Parser, Debug)] +#[command(version)] +struct Args { + // TODO(comc): Combine with dump_obu into a single avm_build dir? + // TODO(comc): Allow multiple different build versions, stored by git hash. + /// Path to extract_proto binary. + #[arg(long)] + extract_proto: String, + + /// Path to dump_obu binary. Used to check the number of frames in a stream. + #[arg(long)] + dump_obu: String, + + /// Path to store decoded streams. + #[arg(long)] + working_dir: String, + + /// Path to frontend app root. + #[arg(long)] + frontend_root: String, + + /// Port. + #[arg(short, long, default_value_t = 8080)] + port: u16, + + /// IP address to bind to. Running on a workstation directly, we typically want this to be 127.0.0.1. Running within docker, 0.0.0.0 is necessary. + #[arg(short, long, default_value = "127.0.0.1")] + ip: String, + + /// Upload requests will timeout in this many seconds. + #[arg(short, long, default_value_t = 5)] + timeout_seconds: u32, +} + +#[derive(Clone)] +struct DecodeInfo { + state: DecodeState, + paths: Vec<String>, +} +impl DecodeInfo { + fn new(total_frames: usize) -> Self { + Self { + state: DecodeState::Pending(DecodeProgress { + total_frames, + decoded_frames: 0, + }), + paths: Vec::new(), + } + } + // TODO(comc): Check frame_path matches template. + fn add_frame(&mut self, frame_path: &str) { + match &mut self.state { + DecodeState::Pending(progress) => progress.decoded_frames += 1, + _ => panic!("Can't add frame to finished decode."), + } + self.paths.push(frame_path.into()); + } +} + +struct PendingStreams { + streams: HashMap<String, DecodeInfo>, +} + +impl PendingStreams { + fn new() -> Self { + Self { + streams: HashMap::new(), + } + } +} + +fn find_existing_streams(root: &Path) -> anyhow::Result<Vec<AvmStreamInfo>> { + tracing::info!("Looking for existing streams in {root:?}"); + let mut streams = Vec::new(); + for entry in fs::read_dir(root)? { + let mut proto_count = 0; + let entry = entry?; + let path = entry.path(); + let path_str = path.to_string_lossy().to_string(); + if path.is_file() && path_str.ends_with("_thumbnail.png") { + let thumbnail_bytes = std::fs::read(entry.path())?; + let path = entry.path(); + let file_name = path.file_name().unwrap().to_string_lossy(); + let stream_name = file_name.strip_suffix("_thumbnail.png").unwrap(); + let proto_dir = root.join(format!("{stream_name}{PROTO_DIR_SUFFIX}")); + for maybe_proto in fs::read_dir(proto_dir)? { + let maybe_proto = maybe_proto?; + let maybe_proto_path = maybe_proto.path(); + let maybe_proto_path_name = maybe_proto_path.to_string_lossy().to_string(); + if maybe_proto_path.is_file() && maybe_proto_path_name.ends_with(".pb") { + proto_count += 1; + } + } + let proto_path_template = + format!("{stream_name}{PROTO_DIR_SUFFIX}/{stream_name}{DEFAULT_PROTO_PATH_FRAME_SUFFIX_TEMPLATE}"); + let stream_info = AvmStreamInfo { + num_frames: proto_count, + stream_name: stream_name.into(), + proto_path_template, + thumbnail_png: Some(thumbnail_bytes), + }; + tracing::info!("Found existing stream with {proto_count} frames: {stream_name}"); + streams.push(stream_info); + } + } + Ok(streams) +} + +#[derive(Clone)] +struct ServerConfig { + working_dir_path: PathBuf, + extract_proto_path: PathBuf, + dump_obu_path: PathBuf, +} + +#[derive(Clone)] +struct ServerState { + config: ServerConfig, + pending_streams: Arc<Mutex<PendingStreams>>, + finished_streams: Arc<Mutex<Vec<AvmStreamInfo>>>, +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + let timeout_service = + ServiceBuilder::new().layer(TimeoutLayer::new(Duration::from_secs(args.timeout_seconds as u64))); + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "debug".into())) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let frontend_path = Path::new(&args.frontend_root); + let working_dir_path = Path::new(&args.working_dir); + let existing_streams = match find_existing_streams(working_dir_path) { + Err(err) => { + tracing::warn!("Error finding existing streams: {err:?}"); + Vec::new() + } + Ok(streams) => streams, + }; + let state = ServerState { + config: ServerConfig { + working_dir_path: working_dir_path.into(), + extract_proto_path: Path::new(&args.extract_proto).into(), + dump_obu_path: Path::new(&args.dump_obu).into(), + }, + pending_streams: Arc::new(Mutex::new(PendingStreams::new())), + finished_streams: Arc::new(Mutex::new(existing_streams)), + }; + // build our application with some routes + let app = Router::new() + .route("/upload", post(upload_stream)) + .route("/progress", get(check_progress)) + .route("/stream_list", get(get_stream_list)) + .with_state(state) + .nest_service("/streams", ServeDir::new(working_dir_path)) + .nest_service("/", ServeDir::new(frontend_path)) + .layer(DefaultBodyLimit::disable()) + .layer(RequestBodyLimitLayer::new(MAX_UPLOAD_SIZE)) + .layer(CorsLayer::permissive()) + .layer(timeout_service) + .layer(tower_http::trace::TraceLayer::new_for_http()); + + let listener = tokio::net::TcpListener::bind(format!("{}:{}", args.ip, args.port)) + .await + .unwrap(); + tracing::debug!("listening on {}", listener.local_addr().unwrap()); + axum::serve(listener, app).await.unwrap(); +} + +async fn check_progress( + State(state): State<ServerState>, + request: Query<ProgressRequest>, +) -> Result<impl IntoResponse, ServerError> { + let pending_streams = state.pending_streams.lock().unwrap(); + tracing::info!("check_progress {:?}", pending_streams.streams.keys()); + let Some(stream_info) = pending_streams.streams.get(&request.stream_name) else { + return Err(anyhow!("Unknown stream.").into()); + }; + + Ok(Json(ProgressResponse { + stream_name: request.stream_name.to_owned(), + state: stream_info.state.clone(), + })) +} + +async fn get_stream_list(State(state): State<ServerState>) -> Result<impl IntoResponse, ServerError> { + let streams = state.finished_streams.lock().unwrap(); + Ok(Json(AvmStreamList { + streams: streams.clone(), + })) +} + +async fn upload_stream( + State(state): State<ServerState>, + mut multipart: Multipart, +) -> Result<impl IntoResponse, ServerError> { + // tracing::info!("upload_stream: {multipart:?}"); + // TODO(comc): ok_or instead of unwrap. + if let Some(field) = multipart.next_field().await.expect("Multipart upload failure.") { + // Name field is unused. Filename is used instead. + let _name = field.name().unwrap().to_string(); + let file_name = field.file_name().unwrap().to_string(); + let _content_type = field.content_type().unwrap().to_string(); + let data = field.bytes().await.unwrap(); + tracing::debug!("Decoding {file_name}: {} bytes", data.len()); + + let stream_path = std::path::Path::new(&file_name); + let stream_path_local = state.config.working_dir_path.join(stream_path); + // stream_name should always be the filename of the stream without the file extension. + let stream_name = stream_path.file_stem().unwrap().to_string_lossy().to_string(); + async_fs::write(stream_path_local.as_path(), data).await?; + + let num_frames = check_num_frames(state.config.dump_obu_path.as_path(), stream_path_local.as_path()).await?; + tracing::debug!("Frames: {num_frames}"); + spawn_extract_proto( + state.config.working_dir_path.as_path(), + state.config.extract_proto_path.as_path(), + stream_path_local.as_path(), + num_frames, + state.pending_streams.clone(), + state.finished_streams.clone(), + )?; + + let proto_path_template = + format!("{stream_name}{PROTO_DIR_SUFFIX}/{stream_name}{DEFAULT_PROTO_PATH_FRAME_SUFFIX_TEMPLATE}"); + let stream_info = AvmStreamInfo { + stream_name, + proto_path_template, + num_frames, + thumbnail_png: None, + }; + return Ok(Json(StartDecodeResponse { stream_info })); + } + Err(anyhow!("No file received.").into()) +} + +async fn check_num_frames(dump_obu_path: &Path, stream: &Path) -> Result<usize, Error> { + let mut child = Command::new(dump_obu_path) + .arg(stream) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + let mut lines = BufReader::new(child.stdout.take().unwrap()).lines(); + let mut count = 0; + while let Some(line) = lines.next().await { + if line?.contains("OBU_FRAME") { + count += 1; + } + } + Ok(count) +} + +// TODO(comc): Refactor this common code out of avm_analyzer_app (probably into avm_stats). +async fn create_thumbnail(first_frame: &Path, thumbnail_out: &Path) { + let first_frame = first_frame.to_owned(); + let thumbnail_out = thumbnail_out.to_owned(); + match tokio::task::spawn_blocking(move || { + tracing::info!("Creating thumbnail: {first_frame:?} --> {thumbnail_out:?}"); + let frame = std::fs::read(first_frame).unwrap(); + let frame = Frame::decode(frame.as_slice()).unwrap(); + let mut planes = Vec::new(); + for i in 0..3 { + planes.push(PixelPlane::create_from_frame(&frame, Plane::from_i32(i), PixelType::Reconstruction).unwrap()); + } + + let width = planes[0].width as usize; + let height = planes[0].height as usize; + let mut img = RgbImage::new(width as u32, height as u32); + 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; + img.put_pixel(j as u32, i as u32, Rgb([r, g, b])); + } + } + let img = DynamicImage::ImageRgb8(img); + let resized = img.resize(64, 64, FilterType::CatmullRom); + match resized.save(thumbnail_out.clone()) { + Ok(_) => {} + Err(err) => { + tracing::warn!("Error resizing thumbnail: {err:?}"); + } + } + // Make thumbnail accessible to all, evne if docker container creates it as root. + let metadata = fs::metadata(thumbnail_out.clone()).unwrap(); + let mut current_permissions = metadata.permissions(); + current_permissions.set_mode(0o644); + fs::set_permissions(thumbnail_out, current_permissions) + }) + .await + { + Ok(_) => {} + Err(err) => { + tracing::warn!("Error creating thumbnail: {err:?}"); + } + } +} + +async fn load_thumbnail(thumbnail_path: &Path) -> anyhow::Result<Vec<u8>> { + let thumbnail_path = thumbnail_path.to_owned(); + let bytes = tokio::task::spawn_blocking(move || std::fs::read(thumbnail_path)).await; + match bytes { + Err(err) => Err(err.into()), + Ok(Err(err)) => Err(err.into()), + Ok(Ok(bytes)) => Ok(bytes), + } +} + +// TODO(comc): Check for existing finished and pending decodes before spawning new jobs. +fn spawn_extract_proto( + working_dir_path: &Path, + extract_proto_path: &Path, + stream_path: &Path, + total_frames: usize, + pending_streams: Arc<Mutex<PendingStreams>>, + finished_streams: Arc<Mutex<Vec<AvmStreamInfo>>>, +) -> Result<impl IntoResponse, ServerError> { + let extract_proto_path = extract_proto_path.to_owned(); + let stream_name = stream_path.file_stem().unwrap().to_string_lossy().to_string(); + let output_path = working_dir_path.join(format!("{stream_name}{PROTO_DIR_SUFFIX}")); + tracing::info!("Creating proto output path: {output_path:?}"); + std::fs::create_dir_all(&output_path)?; + // TODO(comc): Frontend option to force new encode, or use existing. + pending_streams + .lock() + .unwrap() + .streams + .insert(stream_name.clone(), DecodeInfo::new(total_frames)); + let stream_path = stream_path.to_owned(); + let working_dir_path = working_dir_path.to_owned(); + tokio::spawn(async move { + let mut child = Command::new(extract_proto_path) + .arg("--stream") + .arg(stream_path) + .arg("--output_folder") + .arg(output_path) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + let mut lines = BufReader::new(child.stdout.take().unwrap()).lines(); + while let Some(Ok(line)) = lines.next().await { + if line.starts_with("Wrote:") { + let parts: Vec<_> = line.split(' ').filter(|s| !s.is_empty()).collect(); + let mut pending_streams = pending_streams.lock().unwrap(); + let stream_info = pending_streams.streams.get_mut(&stream_name).unwrap(); + let frame_path = parts.last().unwrap(); + tracing::debug!("Frame: {}", frame_path); + stream_info.add_frame(frame_path); + } + } + let status = child.status().await; + tracing::debug!("Status: {:?}", status); + + let decode_info = { + let pending_streams = pending_streams.lock().unwrap(); + pending_streams.streams[&stream_name].clone() + }; + let success = if let Ok(status) = status { + status.success() + } else { + false + }; + if success { + let mut stream_info = { + let num_frames = decode_info.paths.len(); + let mut pending_streams = pending_streams.lock().unwrap(); + let decode_info = pending_streams.streams.get_mut(&stream_name).unwrap(); + // TODO(comc): Update client with actual number of frames, which may be different because of TIP / non-showable frames. + decode_info.state = DecodeState::Complete(num_frames); + + let proto_path_template = + format!("{stream_name}{PROTO_DIR_SUFFIX}/{stream_name}{DEFAULT_PROTO_PATH_FRAME_SUFFIX_TEMPLATE}"); + AvmStreamInfo { + stream_name: stream_name.clone(), + proto_path_template, + num_frames, + thumbnail_png: None, + } + }; + let thumbnail_path = working_dir_path.join(format!("{stream_name}_thumbnail.png")); + let proto_path = working_dir_path.join(stream_info.get_proto_path(0)); + create_thumbnail(&proto_path, &thumbnail_path).await; + match load_thumbnail(&thumbnail_path).await { + Ok(thumbnail_bytes) => stream_info.thumbnail_png = Some(thumbnail_bytes), + Err(err) => { + tracing::warn!("Unable to load thumbnail: {thumbnail_path:?} {err:?}"); + } + } + + // TODO(comc): Overwrite existing stream_info if name already exists. Currently duplicate streams are sent in streams_list. + let mut finished_streams = finished_streams.lock().unwrap(); + finished_streams.push(stream_info.clone()); + } else { + let mut pending_streams = pending_streams.lock().unwrap(); + let decode_info = pending_streams.streams.get_mut(&stream_name).unwrap(); + decode_info.state = DecodeState::Failed; + } + }); + Ok(()) +} + +struct ServerError(anyhow::Error); + +impl IntoResponse for ServerError { + fn into_response(self) -> Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", self.0), + ) + .into_response() + } +} + +impl<E> From<E> for ServerError +where + E: Into<anyhow::Error>, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +}
diff --git a/tools/avm_analyzer/avm_frame.proto b/tools/avm_analyzer/avm_frame.proto deleted file mode 100644 index abb1852..0000000 --- a/tools/avm_analyzer/avm_frame.proto +++ /dev/null
@@ -1,268 +0,0 @@ -/* - * Copyright (c) 2023, Alliance for Open Media. All rights reserved - * - * This source code is subject to the terms of the BSD 3-Clause Clear License - * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause Clear - * License was not distributed with this source code in the LICENSE file, you - * can obtain it at aomedia.org/license/software-license/bsd-3-c-c/. If the - * Alliance for Open Media Patent License 1.0 was not distributed with this - * source code in the PATENTS file, you can obtain it at - * aomedia.org/license/patent-license/. - */ - -// Protobuf messages for frame data extraction from AVM streams. - -syntax = "proto3"; - -package avm.tools; - -// Metadata for a symbol, including source information (C file/line/function). -message SymbolInfo { - // Unique identifier for this symbol type. Note: these ids aren't stable - // across frames. - int32 id = 1; - string source_file = 2; - int32 source_line = 3; - string source_function = 4; - // Additional arbitrary tags that can be added per symbol. For example, if a - // symbol type is decoded for both luma and chroma, there might be a "luma" or - // "chroma" tag on the symbol. - repeated string tags = 5; -} - -message Symbol { - // Unique ID for each "type" of symbol. Can be looked up in symbol_info (see - // definition of Frame message) to get metadata for each symbol. - int32 info_id = 1; - // Raw value of this symbol - int32 value = 2; - // Entropy coding mode for this symbol (e.g. literal bit, CDF, etc...) - int32 coding_mode = 3; - float bits = 4; -} - -// Range of symbols that make up some object. Symbols are stored at the -// superblock level, but lower-level constructs (e.g. transform blocks) can -// refer to a range in its superblock's symbol list for the exact subsequence of -// symbols that created it. Note that end is exclusive, e.g. (start = -// 5, end = 9) would refer to symbols 5, 6, 7, 8 in the superblock. -message SymbolRange { - uint32 start = 1; - uint32 end = 2; -} - -// Size of a coding block, in pixel units -message BlockSize { - int32 width = 1; - int32 height = 2; - // av1/common/enums.h enum value that corresponds to this block size - int32 enum_value = 3; -} - -// This contains the same fields as BlockSize, but is a distinct type since the -// meaning of its enum values is different than BlockSize. -message TransformSize { - int32 width = 1; - int32 height = 2; - // av1/common/enums.h enum value that corresponds to this transform block size - int32 enum_value = 3; -} - -// Absolute position within a frame, in pixel units -message Position { - int32 x = 1; - int32 y = 2; -} - -message TransformUnit { - Position position = 1; - int32 tx_type = 2; - TransformSize size = 3; - int32 skip = 4; - repeated int32 quantized_coeffs = 5; - repeated int32 dequantized_coeffs = 6; - repeated int32 dequantizer_values = 7; - SymbolRange symbol_range = 8; -} - -message TransformPlane { - int32 plane = 1; - repeated TransformUnit transform_units = 2; -} - -message MotionVector { - int32 ref_frame = 1; - sint32 dx = 2; - sint32 dy = 3; - SymbolRange symbol_range = 4; - int32 ref_frame_order_hint = 5; - bool ref_frame_is_tip = 6; - bool ref_frame_is_inter = 7; -} - -message PredictionParams { - int32 mode = 1; - int32 uv_mode = 2; - int32 angle_delta = 3; - repeated MotionVector motion_vectors = 4; - bool use_intrabc = 5; - int32 palette_count = 6; - int32 uv_palette_count = 7; - int32 compound_type = 8; - int32 motion_mode = 9; - int32 interpolation_filter = 10; - int32 cfl_alpha_idx = 11; - int32 cfl_alpha_sign = 12; - int32 uv_angle_delta = 13; - int32 motion_vector_precision = 14; -} - -message PixelBuffer { - int32 width = 1; - int32 height = 2; - int32 bit_depth = 3; - repeated uint32 pixels = 4; -} - -message PixelData { - int32 plane = 1; - // Original source pixels before encoding. Not available in the bitstream, so - // the source YUV needs to be passed in separately to the extract_proto tool. - PixelBuffer original = 2; - PixelBuffer reconstruction = 3; - PixelBuffer prediction = 4; - // Reconstructed pixels BEFORE any filters are applied. - PixelBuffer pre_filtered = 5; -} - -// Leaf of the partition tree -message CodingUnit { - Position position = 1; - BlockSize size = 2; - bool skip = 3; - PredictionParams prediction_mode = 4; - // TODO(comc): Support transform tree partition (max depth = 2?) - // With SDP enabled, for the luma partition tree, exactly one plane will be present. - // With SDP enabled, for the chroma partition tree, exactly two planes (U, V) will be present. - // With SDP disabled, only a single shared partition tree exists, and all three planes will be present. - repeated TransformPlane transform_planes = 5; - SymbolRange symbol_range = 6; - int32 qindex = 7; - int32 segment_id = 8; - int32 cdef_level = 9; - int32 cdef_strength = 10; -} - -// Range of coding units that make up a block at some level in the partition -// tree. Note that end is exclusive, e.g. (start = 5, end = 9) would refer to -// coding units 5, 6, 7, 8 in the superblock. -message CodingUnitRange { - uint32 start = 1; - uint32 end = 2; -} - -message Partition { - Position position = 1; - BlockSize size = 2; - int32 partition_type = 3; - repeated Partition children = 4; - // If this partition has children, coding_units will be a range representing - // the coding units comprising all its children. If this is a leaf node, - // coding_units will refer to exactly one CodingUnit, i.e. the range start is - // equal to the range end. - CodingUnitRange coding_unit_range = 5; - SymbolRange symbol_range = 6; - // True if this partition has more children, or false if it contains exactly - // one coding unit. - bool is_leaf_node = 7; -} - -message Superblock { - Position position = 1; - BlockSize size = 2; - Partition luma_partition_tree = 3; - Partition chroma_partition_tree = 4; - // Is SDP (semi-decoupled partitioning) enabled? - bool has_separate_chroma_partition_tree = 5; - // If this frame does not use SDP, all coding units will be stored in - // coding_units_shared and coding_units_chroma will be empty. - // If this frame uses SDP, the luma coding units will be stored in - // coding_units_shared, and the chroma coding units will be stored in - // coding_units_chroma. - repeated CodingUnit coding_units_shared = 6; - repeated CodingUnit coding_units_chroma = 7; - repeated Symbol symbols = 8; - repeated PixelData pixel_data = 9; -} - -// Map C enum values to names. This is done rather than just using proto enums -// for a few reasons: -// - Proto3 enums REQUIRE a zero value, and strongly recommend it's used as an -// unknown / unspecified value. This doesn't map cleanly to the AVM enums. -// - AVM's enums can evolve over time, or even within the same anchor if -// different experiments / defines are used. Defining this enum mapping is -// more maintainable than having a separate source of truth in this proto -// schema. -message EnumMappings { - map<int32, string> transform_type_mapping = 1; - map<int32, string> entropy_coding_mode_mapping = 2; - map<int32, string> interpolation_filter_mapping = 3; - map<int32, string> prediction_mode_mapping = 4; - map<int32, string> uv_prediction_mode_mapping = 5; - map<int32, string> motion_mode_mapping = 6; - map<int32, string> transform_size_mapping = 7; - map<int32, string> block_size_mapping = 8; - map<int32, string> partition_type_mapping = 9; - map<int32, string> frame_type_mapping = 10; - map<int32, string> tip_mode_mapping = 11; - map<int32, string> motion_vector_precision_mapping = 12; -} - -// TODO(comc): Add tile info and refactor FrameParams if necessary -message FrameParams { - int32 frame_type = 1; - int32 width = 2; - int32 height = 3; - int32 decode_index = 4; - // Global display index, unique within the whole stream. - int32 display_index = 5; - BlockSize superblock_size = 6; - bool show_frame = 7; - int32 base_qindex = 8; - int32 bit_depth = 9; - // Raw display index, may not be unique within the whole stream (e.g. if the stream contains more than one sequence). - int32 raw_display_index = 10; -} - -message StreamParams { - string stream_hash = 1; - string stream_name = 2; - float frame_rate = 3; - // Note: these are present both here and in FrameParms. For most streams we - // care about, the frame dimensions will be the same across every frame. For - // streams with variable-sized frames, these fields can be omitted. - int32 width = 4; - int32 height = 5; - string avm_version = 6; - map<string, string> encoder_args = 7; -} - -message TipFrameParams { - int32 tip_mode = 1; - repeated PixelData pixel_data = 2; -} - -message Frame { - // Note: StreamParams encapsulates all parameters that are common to the - // entire stream, e.g. the encoder version and args used to produce it. Since - // the storage granularity of these protos is individual frames, not entire - // streams, it is stored as a field of the Frame message. Identical - // StreamParams will be present on each individual Frame message that make up - // one stream. - StreamParams stream_params = 1; - FrameParams frame_params = 2; - repeated Superblock superblocks = 3; - map<int32, SymbolInfo> symbol_info = 4; - EnumMappings enum_mappings = 5; - TipFrameParams tip_frame_params = 6; -}
diff --git a/tools/avm_analyzer/avm_stats/Cargo.toml b/tools/avm_analyzer/avm_stats/Cargo.toml new file mode 100644 index 0000000..996b19f --- /dev/null +++ b/tools/avm_analyzer/avm_stats/Cargo.toml
@@ -0,0 +1,24 @@ +[package] +name = "avm-stats" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +num = "0.4.0" +num-derive = "0.3" +num-traits = "0.2" +serde = { version = "1", features = ["derive"] } +serde_json = "1.0" +emath = "0.25.0" +prost = { version = "0.11", features = ["prost-derive"]} +prost-types = "0.11" +thiserror = "1.0" +itertools = "0.10" +clap = { version = "4.4.11", features = ["derive"] } +once_cell = "1.19.0" +log = "0.4" +ordered-float = "4.2.0" + +[build-dependencies] +prost-build = "0.11"
diff --git a/tools/avm_analyzer/avm_stats/build.rs b/tools/avm_analyzer/avm_stats/build.rs new file mode 100644 index 0000000..6a30aa1 --- /dev/null +++ b/tools/avm_analyzer/avm_stats/build.rs
@@ -0,0 +1,8 @@ +use std::io::Result; + +fn main() -> Result<()> { + prost_build::Config::new() + .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") + .compile_protos(&["../../extract_proto/avm_frame.proto"], &["../../extract_proto"])?; + Ok(()) +}
diff --git a/tools/avm_analyzer/avm_stats/src/avm_proto.rs b/tools/avm_analyzer/avm_stats/src/avm_proto.rs new file mode 100644 index 0000000..6733548 --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/avm_proto.rs
@@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/avm.tools.rs"));
diff --git a/tools/avm_analyzer/avm_stats/src/coding_unit.rs b/tools/avm_analyzer/avm_stats/src/coding_unit.rs new file mode 100644 index 0000000..8959e2a --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/coding_unit.rs
@@ -0,0 +1,185 @@ +use crate::{ + CodingUnit, Frame, FrameError, PartitionContext, Plane, PredictionParams, ProtoEnumMapping, SuperblockContext, + SuperblockLocator, SymbolContext, SymbolRange, TransformUnitContext, TransformUnitLocator, +}; + +use serde::{Deserialize, Serialize}; +use FrameError::BadCodingUnit; +impl CodingUnit { + pub fn plane_index(&self, plane: Plane) -> Result<usize, FrameError> { + match plane { + Plane::Y => Ok(0), + Plane::U | Plane::V => { + match self.transform_planes.len() { + // Split luma and chroma partition trees + 2 => Ok(plane.to_usize() - 1), + // Unified luma and chroma partition tree + 3 => Ok(plane.to_usize()), + _ => Err(BadCodingUnit(format!( + "Unexpected number of transform planes: got {}, expected 2 or 3 for plane {plane:?}", + self.transform_planes.len() + ))), + } + } + } + } + + pub fn has_chroma(&self) -> Result<bool, FrameError> { + let num_transform_planes = self.transform_planes.len(); + match num_transform_planes { + 2 | 3 => Ok(true), + 1 => Ok(false), + _ => Err(BadCodingUnit(format!( + "Unexpected number of transform planes: {num_transform_planes}" + ))), + } + } + + pub fn has_luma(&self) -> Result<bool, FrameError> { + let num_transform_planes = self.transform_planes.len(); + match num_transform_planes { + 1 | 3 => Ok(true), + 2 => Ok(false), + _ => Err(BadCodingUnit(format!( + "Unexpected number of transform planes: {num_transform_planes}" + ))), + } + } + + pub fn get_prediction_mode(&self) -> Result<&PredictionParams, FrameError> { + self.prediction_mode + .as_ref() + .ok_or(BadCodingUnit("Missing prediction mode.".into())) + } + + pub fn get_symbol_range(&self) -> Result<&SymbolRange, FrameError> { + self.symbol_range + .as_ref() + .ok_or(BadCodingUnit("Missing symbol range.".into())) + } + + pub fn lookup_mode_name(&self, frame: &Frame) -> Result<String, FrameError> { + let mode = self.get_prediction_mode()?; + frame.enum_lookup(ProtoEnumMapping::PredictionMode, mode.mode) + } + + pub fn luma_mode_angle_delta(&self, frame: &Frame) -> Option<i32> { + if let Ok(mode) = self.lookup_mode_name(frame) { + if mode.ends_with("_PRED") { + return self.prediction_mode.as_ref().map(|mode| mode.angle_delta); + } + } + None + } + + pub fn lookup_uv_mode_name(&self, frame: &Frame) -> Result<String, FrameError> { + let mode = self.get_prediction_mode()?; + frame.enum_lookup(ProtoEnumMapping::UvPredictionMode, mode.uv_mode) + } + + pub fn chroma_mode_angle_delta(&self, frame: &Frame) -> Option<i32> { + if let Ok(mode) = self.lookup_uv_mode_name(frame) { + if mode.ends_with("_PRED") { + return self.prediction_mode.as_ref().map(|mode| mode.uv_angle_delta); + } + } + None + } + + pub fn lookup_motion_vector_precision_name(&self, frame: &Frame) -> Result<String, FrameError> { + let mode = self.get_prediction_mode()?; + frame.enum_lookup(ProtoEnumMapping::MotionVectorPrecision, mode.motion_vector_precision) + } +} + +/// Which planes this coding unit contains. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub enum CodingUnitKind { + /// Coding unit contains all three planes. Equivalent to the shared partition tree type. + Shared, + /// Coding unit contains only luma. + LumaOnly, + /// Coding unit contains only chroma. + ChromaOnly, +} + +/// Index of a coding unit within its parent superblock. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct CodingUnitLocator { + /// Index of parent superblock within the frame. + pub superblock: SuperblockLocator, + /// Either Shared or ChromaOnly. Note that LumaOnly refers to the same underlying buffer as Shared. + pub kind: CodingUnitKind, + /// Index of coding unit with its parent superblock. + pub index: usize, +} + +impl CodingUnitLocator { + pub fn new(superblock: SuperblockLocator, kind: CodingUnitKind, index: usize) -> Self { + Self { + superblock, + kind, + index, + } + } + /// Convert this index into a `CodingUnitContext`. + pub fn try_resolve<'a>(&self, frame: &'a Frame) -> Option<CodingUnitContext<'a>> { + let superblock_context = self.superblock.try_resolve(frame)?; + let coding_unit = match self.kind { + CodingUnitKind::Shared => superblock_context.superblock.coding_units_shared.get(self.index), + CodingUnitKind::LumaOnly => superblock_context.superblock.coding_units_shared.get(self.index), + CodingUnitKind::ChromaOnly => superblock_context.superblock.coding_units_chroma.get(self.index), + }; + + coding_unit.map(|coding_unit| CodingUnitContext { + coding_unit, + superblock_context, + locator: *self, + }) + } + + pub fn resolve<'a>(&self, frame: &'a Frame) -> CodingUnitContext<'a> { + self.try_resolve(frame).unwrap() + } +} + +/// Context about a coding unit during iteration. +#[derive(Copy, Clone)] +pub struct CodingUnitContext<'a> { + /// Coding unit being iterated over. + pub coding_unit: &'a CodingUnit, + /// Superblock that owns this coding unit. + pub superblock_context: SuperblockContext<'a>, + /// The index of this coding unit within its parent superblock. + pub locator: CodingUnitLocator, +} + +impl<'a> CodingUnitContext<'a> { + pub fn iter_symbols(&self) -> impl Iterator<Item = SymbolContext<'a>> { + let symbol_range = self.coding_unit.symbol_range.clone().unwrap_or_default(); + self.superblock_context.iter_symbols(Some(symbol_range)) + } + + pub fn total_bits(&self) -> f32 { + self.iter_symbols().map(|sym| sym.symbol.bits).sum() + } + + pub fn iter_transform_units(self, plane: Plane) -> impl Iterator<Item = TransformUnitContext<'a>> { + let plane_index = self.coding_unit.plane_index(plane); + self.coding_unit.transform_planes[plane_index.unwrap()] + .transform_units + .iter() + .enumerate() + .map(move |(index, transform_unit)| TransformUnitContext { + transform_unit, + coding_unit_context: self, + locator: TransformUnitLocator::new(self.locator, plane, index), + }) + } + + pub fn find_parent_partition(&self) -> Option<PartitionContext<'a>> { + self.superblock_context + .root_partition(self.locator.kind) + .and_then(|root| root.find_coding_unit_parent(self.coding_unit)) + } +}
diff --git a/tools/avm_analyzer/avm_stats/src/constants.rs b/tools/avm_analyzer/avm_stats/src/constants.rs new file mode 100644 index 0000000..970f22b --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/constants.rs
@@ -0,0 +1,2 @@ +// TODO(comc): This can vary by frame. +pub const MOTION_VECTOR_PRECISION: f32 = 8.0;
diff --git a/tools/avm_analyzer/avm_stats/src/frame.rs b/tools/avm_analyzer/avm_stats/src/frame.rs new file mode 100644 index 0000000..969bfd8 --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/frame.rs
@@ -0,0 +1,195 @@ +use crate::{ + CodingUnitContext, CodingUnitKind, EnumMappings, Frame, FrameError, PartitionContext, Plane, PlaneType, Spatial, + SuperblockContext, SuperblockLocator, SymbolContext, TransformUnitContext, +}; + +pub enum ProtoEnumMapping { + TransformType, + EntropyCodingMode, + InterpolationFilter, + PredictionMode, + UvPredictionMode, + MotionMode, + TransformSize, + BlockSize, + PartitionType, + FrameType, + TipMode, + MotionVectorPrecision, +} + +impl Frame { + pub fn iter_coding_units(&self, kind: CodingUnitKind) -> impl Iterator<Item = CodingUnitContext> + '_ { + self.iter_superblocks().flat_map(move |ctx| ctx.iter_coding_units(kind)) + } + + /// Whether this frame has separate luma and chroma partition trees (i.e. semi-decoupled partitioning - SDP). + /// + /// This is stored at the superblock level, but each superblock is assumed to have the same SDP setting. + pub fn has_separate_chroma_partition_tree(&self) -> bool { + if let Some(sb) = self.superblocks.first() { + sb.has_separate_chroma_partition_tree + } else { + false + } + } + + pub fn coding_unit_kind(&self, plane_type: PlaneType) -> CodingUnitKind { + match plane_type { + PlaneType::Rgb => CodingUnitKind::Shared, + PlaneType::Planar(Plane::Y) => { + if self.has_separate_chroma_partition_tree() { + CodingUnitKind::LumaOnly + } else { + CodingUnitKind::Shared + } + } + PlaneType::Planar(Plane::U | Plane::V) => { + if self.has_separate_chroma_partition_tree() { + CodingUnitKind::ChromaOnly + } else { + CodingUnitKind::Shared + } + } + } + } + + pub fn iter_coding_unit_rects(&self, kind: CodingUnitKind) -> impl Iterator<Item = emath::Rect> + '_ { + self.iter_coding_units(kind).map(|ctx| ctx.coding_unit.rect()) + } + + pub fn iter_transform_units(&self, plane: Plane) -> impl Iterator<Item = TransformUnitContext> { + let kind = self.coding_unit_kind(PlaneType::Planar(plane)); + self.iter_coding_units(kind) + .flat_map(move |ctx| ctx.iter_transform_units(plane)) + } + + pub fn iter_transform_rects(&self, plane: Plane) -> impl Iterator<Item = emath::Rect> + '_ { + self.iter_transform_units(plane).map(|ctx| ctx.transform_unit.rect()) + } + + pub fn iter_superblocks(&self) -> impl Iterator<Item = SuperblockContext> { + self.superblocks + .iter() + .enumerate() + .map(|(i, superblock)| SuperblockContext { + superblock, + frame: self, + locator: SuperblockLocator::new(i), + }) + } + + pub fn iter_partitions(&self, kind: CodingUnitKind) -> impl Iterator<Item = PartitionContext> { + self.iter_superblocks() + .flat_map(move |superblock_context| superblock_context.iter_partitions(kind)) + } + + fn get_enum_mappings(&self) -> Result<&EnumMappings, FrameError> { + self.enum_mappings + .as_ref() + .ok_or(FrameError::BadFrame("Missing enum mappings.".into())) + } + pub fn enum_lookup(&self, enum_type: ProtoEnumMapping, value: i32) -> Result<String, FrameError> { + use FrameError::*; + let enum_mappings = self.get_enum_mappings()?; + match enum_type { + ProtoEnumMapping::TransformType => enum_mappings + .transform_type_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing transform type value: {value}"))), + ProtoEnumMapping::EntropyCodingMode => enum_mappings + .entropy_coding_mode_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing entropy coding mode value: {value}"))), + ProtoEnumMapping::InterpolationFilter => enum_mappings + .interpolation_filter_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing interpolation filter value: {value}"))), + ProtoEnumMapping::PredictionMode => enum_mappings + .prediction_mode_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing prediction mode value: {value}"))), + ProtoEnumMapping::UvPredictionMode => enum_mappings + .uv_prediction_mode_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing UV prediction mode value: {value}"))), + ProtoEnumMapping::MotionMode => enum_mappings + .motion_mode_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing motion mode value: {value}"))), + ProtoEnumMapping::TransformSize => enum_mappings + .transform_size_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing transform size value: {value}"))), + ProtoEnumMapping::BlockSize => enum_mappings + .block_size_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing block size value: {value}"))), + ProtoEnumMapping::PartitionType => enum_mappings + .partition_type_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing partition type value: {value}"))), + ProtoEnumMapping::FrameType => enum_mappings + .frame_type_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing frame type value: {value}"))), + ProtoEnumMapping::TipMode => enum_mappings + .tip_mode_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing TIP mode value: {value}"))), + ProtoEnumMapping::MotionVectorPrecision => enum_mappings + .motion_vector_precision_mapping + .get(&value) + .ok_or(BadFrame(format!("Missing MV precision value: {value}"))), + } + .cloned() + } + + pub fn iter_superblock_rects(&self) -> impl Iterator<Item = emath::Rect> + '_ { + self.iter_superblocks() + .map(|superblock_context| superblock_context.superblock.rect()) + } + + pub fn iter_symbols(&self) -> impl Iterator<Item = SymbolContext> { + self.iter_superblocks() + .flat_map(move |superblock_context| superblock_context.iter_symbols(None)) + } + + pub fn bit_depth(&self) -> u8 { + self.frame_params + .as_ref() + .map_or(0, |frame_params| frame_params.bit_depth as u8) + } + + pub fn decode_index(&self) -> usize { + self.frame_params + .as_ref() + .map_or(0, |frame_params| frame_params.decode_index as usize) + } + + pub fn display_index(&self) -> usize { + self.frame_params + .as_ref() + .map_or(0, |frame_params| frame_params.display_index as usize) + } + + pub fn frame_type_name(&self) -> String { + if let Some(frame_params) = self.frame_params.as_ref() { + let frame_type = frame_params.frame_type; + if let Ok(name) = self.enum_lookup(ProtoEnumMapping::FrameType, frame_type) { + return name; + } + } + "UNKNOWN".into() + } + + pub fn tip_mode_name(&self) -> String { + if let Some(tip_frame_params) = self.tip_frame_params.as_ref() { + let tip_mode = tip_frame_params.tip_mode; + if let Ok(name) = self.enum_lookup(ProtoEnumMapping::TipMode, tip_mode) { + return name; + } + } + "UNKNOWN".into() + } +}
diff --git a/tools/avm_analyzer/avm_stats/src/frame_error.rs b/tools/avm_analyzer/avm_stats/src/frame_error.rs new file mode 100644 index 0000000..259b648 --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/frame_error.rs
@@ -0,0 +1,23 @@ +use thiserror::Error; + +#[derive(Error, Debug, Clone)] +pub enum FrameError { + #[error("Badly formed frame: {0}")] + BadFrame(String), + #[error("Badly formed superblock: {0}")] + BadSuperblock(String), + #[error("Badly formed coding unit: {0}")] + BadCodingUnit(String), + #[error("Badly formed transform unit: {0}")] + BadTransformUnit(String), + #[error("Badly formed symbol: {0}")] + BadSymbol(String), + #[error("Badly formed pixel buffer: {0}")] + BadPixelBuffer(String), + #[error("Missing pixel buffer: {0}")] + MissingPixelBuffer(String), + #[error("Internal error: {0}")] + Internal(String), + #[error("Unknown frame error: {0}")] + Unknown(String), +}
diff --git a/tools/avm_analyzer/avm_stats/src/heatmap.rs b/tools/avm_analyzer/avm_stats/src/heatmap.rs new file mode 100644 index 0000000..dad3e80 --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/heatmap.rs
@@ -0,0 +1,99 @@ +use itertools::{Itertools, MinMaxResult}; +use serde::{Deserialize, Serialize}; + +use crate::{CodingUnitKind, Frame, FrameError, Spatial}; +// TODO(comc): Allow filtering by symbol type. +// TODO(comc): Consider some way of handling this for TIP frames, e.g. weighted average of the two reference frames? +pub const DEFAULT_HISTROGRAM_BUCKETS: usize = 32; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct HeatmapSettings { + pub symbol_filter: String, + pub histogram_buckets: usize, + pub coding_unit_kind: CodingUnitKind, +} + +impl Default for HeatmapSettings { + fn default() -> Self { + Self { + symbol_filter: "".to_string(), + histogram_buckets: DEFAULT_HISTROGRAM_BUCKETS, + coding_unit_kind: CodingUnitKind::Shared, + } + } +} + +#[derive(Clone)] +pub struct Heatmap { + pub width: usize, + pub height: usize, + pub data: Vec<u8>, + pub min_value: f32, + pub max_value: f32, + pub bucket_width: f32, + pub histogram: Vec<f32>, +} + +pub fn calculate_heatmap(frame: &Frame, settings: &HeatmapSettings) -> Result<Heatmap, FrameError> { + let width = frame.width() as usize; + let height = frame.height() as usize; + let mut heatmap = vec![0.0; width * height]; + // TODO(comc): Option to iterate over both luma and chroma symbols and add them up (for SDP frames). + let bit_rects = frame.iter_coding_units(settings.coding_unit_kind).map(|ctx| { + let cu = ctx.coding_unit; + let bits = ctx.iter_symbols().filter_map(|sym| { + if settings.symbol_filter.is_empty() || sym.info.unwrap().source_function.contains(&settings.symbol_filter) + { + Some(sym.symbol.bits) + } else { + None + } + }); + let sum: f32 = bits.sum(); + let y0 = cu.y() as usize; + let y1 = y0 + cu.height() as usize; + let x0 = cu.x() as usize; + let x1 = x0 + cu.width() as usize; + + Ok::<_, FrameError>((y0.min(height), y1.min(height), x0.min(width), x1.min(width), sum)) + }); + for bit_rect in bit_rects.flatten() { + let (y0, y1, x0, x1, bits) = bit_rect; + let area = ((y1 - y0) * (x1 - x0)) as f32; + for y in y0..y1 { + for x in x0..x1 { + let index = y * width + x; + heatmap[index] = bits / area; + } + } + } + let mut min = 0.0; + let mut max = 255.0; + match heatmap.iter().minmax() { + MinMaxResult::NoElements | MinMaxResult::OneElement(_) => {} + MinMaxResult::MinMax(&min_v, &max_v) => { + min = min_v; + max = max_v; + } + }; + let mut histogram = vec![0.0; settings.histogram_buckets]; + heatmap.iter().for_each(|&x| { + let frac = (x - min) / (max - min); + let bucket = (frac * settings.histogram_buckets as f32) as usize; + let bucket = bucket.min(settings.histogram_buckets - 1); + histogram[bucket] += 1.0; + }); + let heatmap: Vec<u8> = heatmap + .iter() + .map(|&x| (255.0 * (x - min) / (max - min)) as u8) + .collect(); + Ok(Heatmap { + width, + height, + data: heatmap, + min_value: min, + max_value: max, + bucket_width: (max - min) / settings.histogram_buckets as f32, + histogram, + }) +}
diff --git a/tools/avm_analyzer/avm_stats/src/lib.rs b/tools/avm_analyzer/avm_stats/src/lib.rs new file mode 100644 index 0000000..1fc8a92 --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/lib.rs
@@ -0,0 +1,29 @@ +pub mod avm_proto; +pub mod coding_unit; +pub mod constants; +pub mod frame; +pub mod frame_error; +pub mod heatmap; +pub mod partition; +pub mod pixels; +pub mod plane; +pub mod spatial; +pub mod stats; +pub mod superblock; +pub mod symbol; +pub mod transform_unit; + +pub use avm_proto::*; +pub use coding_unit::*; +pub use constants::*; +pub use frame::*; +pub use frame_error::*; +pub use heatmap::*; +pub use partition::*; +pub use pixels::*; +pub use plane::*; +pub use spatial::*; +pub use stats::*; +pub use superblock::*; +pub use symbol::*; +pub use transform_unit::*;
diff --git a/tools/avm_analyzer/avm_stats/src/main.rs b/tools/avm_analyzer/avm_stats/src/main.rs new file mode 100644 index 0000000..89fdd3a --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/main.rs
@@ -0,0 +1,19 @@ +use avm_stats::Frame; +use clap::Parser; +use prost::Message; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Path to protobuf file. + #[arg(short, long)] + proto: String, +} + +fn main() -> Result<(), anyhow::Error> { + let args = Args::parse(); + let frame = std::fs::read(args.proto).unwrap(); + let frame = Frame::decode(frame.as_slice()).unwrap(); + println!("{:?}", frame.superblocks.len()); + Ok(()) +}
diff --git a/tools/avm_analyzer/avm_stats/src/partition.rs b/tools/avm_analyzer/avm_stats/src/partition.rs new file mode 100644 index 0000000..97b1d8e --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/partition.rs
@@ -0,0 +1,151 @@ +use serde::{Deserialize, Serialize}; + +use crate::{CodingUnit, Partition, Spatial, SuperblockContext}; + +use crate::{CodingUnitKind, Frame, SuperblockLocator, SymbolContext}; + +pub struct PartitionIterator<'a> { + pub stack: Vec<(PartitionContext<'a>, usize)>, + pub max_depth: Option<usize>, +} + +impl<'a> PartitionIterator<'a> { + fn new(root: PartitionContext<'a>) -> Self { + Self { + stack: vec![(root, 0)], + max_depth: None, + } + } + + fn with_max_depth(root: PartitionContext<'a>, max_depth: usize) -> Self { + Self { + stack: vec![(root, 0)], + max_depth: Some(max_depth), + } + } +} + +impl<'a> Iterator for PartitionIterator<'a> { + type Item = PartitionContext<'a>; + fn next(&mut self) -> Option<Self::Item> { + let (current, depth) = self.stack.pop()?; + let max_depth = self.max_depth.unwrap_or(usize::MAX); + let child_depth = depth + 1; + if child_depth <= max_depth { + self.stack + .extend(current.partition.children.iter().enumerate().rev().map(|(i, child)| { + let mut child_context = current.clone(); + child_context.partition = child; + child_context.locator.path_indices.push(i); + (child_context, child_depth) + })); + } + Some(current) + } +} + +// TODO(comc): Handle shared vs luma differently at the shared level of the partition tree. +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct PartitionLocator { + pub path_indices: Vec<usize>, + pub kind: CodingUnitKind, + pub superblock: SuperblockLocator, +} + +impl PartitionLocator { + pub fn new(path_indices: Vec<usize>, kind: CodingUnitKind, superblock: SuperblockLocator) -> Self { + Self { + path_indices, + kind, + superblock, + } + } + + pub fn try_resolve<'a>(&self, frame: &'a Frame) -> Option<PartitionContext<'a>> { + let superblock_context = self.superblock.try_resolve(frame)?; + let mut current = match self.kind { + CodingUnitKind::Shared | CodingUnitKind::LumaOnly => { + superblock_context.superblock.luma_partition_tree.as_ref()? + } + CodingUnitKind::ChromaOnly => superblock_context.superblock.chroma_partition_tree.as_ref()?, + }; + for index in self.path_indices.iter() { + if let Some(child) = current.children.get(*index) { + current = child; + } else { + return None; + } + } + Some(PartitionContext { + partition: current, + superblock_context, + locator: self.clone(), + }) + } + + pub fn resolve(self, frame: &Frame) -> PartitionContext { + self.try_resolve(frame).unwrap() + } + + pub fn is_root(&self) -> bool { + self.path_indices.is_empty() + } + + pub fn parent(&self) -> Option<PartitionLocator> { + if self.is_root() { + None + } else { + let mut parent = self.clone(); + parent.path_indices.pop(); + Some(parent) + } + } +} + +/// Context about a partition block during iteration. +#[derive(Clone)] +pub struct PartitionContext<'a> { + /// Partition block being iterated over. + pub partition: &'a Partition, + /// Superblock that owns this partition block. + pub superblock_context: SuperblockContext<'a>, + /// The index of this partition block within its parent superblock. + pub locator: PartitionLocator, +} + +impl<'a> PartitionContext<'a> { + // Note: Also yields self. + pub fn iter(&self) -> impl Iterator<Item = PartitionContext<'a>> { + PartitionIterator::new(self.clone()) + } + + // Note: Also yields self. + pub fn iter_with_max_depth(&self, max_depth: usize) -> impl Iterator<Item = PartitionContext<'a>> { + PartitionIterator::with_max_depth(self.clone(), max_depth) + } + + pub fn iter_direct_children(&self) -> impl Iterator<Item = PartitionContext<'a>> { + self.iter_with_max_depth(1).skip(1) + } + + pub fn iter_symbols(&self) -> impl Iterator<Item = SymbolContext<'a>> { + let symbol_range = self.partition.symbol_range.clone().unwrap_or_default(); + self.superblock_context.iter_symbols(Some(symbol_range)) + } + + pub fn find_coding_unit_parent(&self, coding_unit: &'a CodingUnit) -> Option<PartitionContext<'a>> { + if self.partition.rect() == coding_unit.rect() { + return Some(self.clone()); + } + for child in self.iter_direct_children() { + if child.partition.rect().contains_rect(coding_unit.rect()) { + return child.find_coding_unit_parent(coding_unit); + } + } + None + } + + pub fn is_root(&self) -> bool { + self.locator.is_root() + } +}
diff --git a/tools/avm_analyzer/avm_stats/src/pixels.rs b/tools/avm_analyzer/avm_stats/src/pixels.rs new file mode 100644 index 0000000..4f218ff --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/pixels.rs
@@ -0,0 +1,359 @@ +use crate::FrameError; +use crate::Plane; +use crate::{Frame, PixelBuffer, Spatial, Superblock}; +use std::fmt; + +/// Where in the codec pipeline a pixel buffer was sampled from. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum PixelType { + /// Pre-encode pixels (may not always be available). + Original, + /// Intra or inter predicted pixels. + Prediction, + /// Reconstructed pixels BEFORE filtering. + PreFiltered, + /// Final reconstructed pixels AFTER filtering. + Reconstruction, + /// Residual, i.e. (PreFiltered - Prediction). + Residual, + /// The effect of the in-loop filtering, i.e. (Reconstruction - PreFiltered). + FilterDelta, + /// (Original - Reconstruction) - depends on Original pixels being available. + Distortion, +} + +impl PixelType { + /// Whether this pixel type represents a difference between two other pixel types. + pub fn is_delta(&self) -> bool { + match self { + Self::Original | Self::Prediction | Self::PreFiltered | Self::Reconstruction => false, + Self::Residual | Self::FilterDelta | Self::Distortion => true, + } + } +} + +impl PixelBuffer { + /// Retrieves a pixel from the buffer, and compensates for bit_depth adjustment if necessary. + /// `desired_bit_depth` will typically be the bit_depth of the stream itself. The underlying + /// buffer may have a different bit_depth in the case of original YUV pixels. + pub fn get_pixel(&self, x: i32, y: i32, desired_bit_depth: u8) -> Result<i16, FrameError> { + use FrameError::*; + let stride = self.width; + let index = (y * stride + x) as usize; + let mut pixel = *self.pixels.get(index).ok_or_else(|| { + BadPixelBuffer(format!( + "Out of bounds access (x={x}, y={y}) on pixel buffer (width={}, height={}).", + self.width, self.height + )) + })?; + if (self.bit_depth as u8) < desired_bit_depth { + let left_shift = desired_bit_depth - self.bit_depth as u8; + pixel <<= left_shift; + } + else if (self.bit_depth as u8) > desired_bit_depth { + let right_shift = self.bit_depth as u8 - desired_bit_depth; + pixel >>= right_shift; + } + Ok(pixel as i16) + } +} + +/// Reference to a pixel buffer, or two pixel buffers in the case of a delta pixel type. +#[derive(Debug, Clone)] +pub enum PixelBufferRef<'a> { + Single(&'a PixelBuffer), + Delta(&'a PixelBuffer, &'a PixelBuffer), +} + +impl<'a> PixelBufferRef<'a> { + pub fn new_single(buf: &'a PixelBuffer) -> Self { + Self::Single(buf) + } + pub fn new_delta(buf_1: &'a PixelBuffer, buf_2: &'a PixelBuffer) -> Self { + Self::Delta(buf_1, buf_2) + } + + /// Assumes both underlying buffers have the same width. + pub fn width(&self) -> i32 { + match self { + Self::Single(buf) => buf.width, + Self::Delta(buf_1, _) => buf_1.width, + } + } + + /// Assumes both underlying buffers have the same height. + pub fn height(&self) -> i32 { + match self { + Self::Single(buf) => buf.height, + Self::Delta(buf_1, _) => buf_1.height, + } + } + + /// Get a pixel from the underlying buffer(s), or a `FrameError` if OoB access occurs. + pub fn get_pixel(&self, x: i32, y: i32, desired_bit_depth: u8) -> Result<i16, FrameError> { + match self { + Self::Single(buf) => { + buf.get_pixel(x, y, desired_bit_depth) + } + Self::Delta(buf_1, buf_2) => { + let pixel_1 = buf_1.get_pixel(x, y, desired_bit_depth)?; + let pixel_2 = buf_2.get_pixel(x, y, desired_bit_depth)?; + Ok(pixel_1 - pixel_2) + } + } + } +} + +impl fmt::Display for PixelType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let text = match self { + PixelType::Original => "Original YUV", + PixelType::Prediction => "Prediction", + PixelType::PreFiltered => "Prefiltered", + PixelType::Reconstruction => "Reconstruction", + PixelType::Residual => "Residual", + PixelType::FilterDelta => "Filter Delta", + PixelType::Distortion => "Distortion", + }; + write!(f, "{text}") + } +} + +/// Pixel data for a single plane (Y, U or V) and single pixel type. +pub struct PixelPlane { + pub bit_depth: u8, + pub width: i32, + pub height: i32, + pub pixels: Vec<i16>, + pub plane: Plane, + pub pixel_type: PixelType, +} + +impl PixelPlane { + fn create_from_tip_frame(frame: &Frame, plane: Plane, pixel_type: PixelType) -> Result<Self, FrameError> { + use FrameError::*; + let tip_params = frame.tip_frame_params.as_ref().unwrap(); + let width = plane.subsampled(frame.width()); + let height = plane.subsampled(frame.height()); + let bit_depth = frame.bit_depth(); + let mut pixels = vec![0; (width * height) as usize]; + + let pixel_data = tip_params + .pixel_data + .get(plane.to_i32() as usize) + .ok_or(BadFrame("Missing pixel data in tip frame.".into()))?; + let pixel_buffer = match pixel_type { + PixelType::Original => { + PixelBufferRef::new_single(pixel_data.original.as_ref().ok_or(MissingPixelBuffer(format!( + "Original pixel data for plane {} not present", + plane.to_usize() + )))?) + } + + PixelType::Reconstruction => { + PixelBufferRef::new_single(pixel_data.reconstruction.as_ref().ok_or(MissingPixelBuffer(format!( + "Reconstruction pixel data for plane {} not present", + plane.to_usize() + )))?) + } + + PixelType::Distortion => { + let original = pixel_data.original.as_ref().ok_or(MissingPixelBuffer(format!( + "Original pixel data for plane {} not present", + plane.to_usize() + )))?; + let reconstruction = pixel_data.reconstruction.as_ref().ok_or(MissingPixelBuffer(format!( + "Reconstruction pixel data for plane {} not present", + plane.to_usize() + )))?; + PixelBufferRef::new_delta(original, reconstruction) + } + + _ => { + return Err(FrameError::Internal(format!( + "Tried to retrieve invalid single pixel buffer type ({pixel_type:?}) from TIP params." + ))) + } + }; + + for y in 0..height { + for x in 0..width { + let index = (y * width + x) as usize; + pixels[index] = pixel_buffer.get_pixel(x, y, bit_depth)?; + } + } + Ok(Self { + bit_depth, + width, + height, + pixels, + plane, + pixel_type, + }) + } + + fn create_from_superblocks(frame: &Frame, plane: Plane, pixel_type: PixelType) -> Result<Self, FrameError> { + use FrameError::*; + let width = plane.subsampled(frame.width()); + let height = plane.subsampled(frame.height()); + let bit_depth = frame.bit_depth(); + let mut pixels = vec![0; (width * height) as usize]; + + for sb_ctx in frame.iter_superblocks() { + let sb = sb_ctx.superblock; + let sb_width = plane.subsampled(sb.width()); + let sb_height = plane.subsampled(sb.height()); + + if sb_width <= 0 || sb_height <= 0 { + return Err(BadSuperblock(format!("Invalid dimensions: {sb_width}x{sb_height}"))); + } + + let sb_x = plane.subsampled(sb.x()); + let sb_y = plane.subsampled(sb.y()); + if sb_x < 0 || sb_x >= width || sb_y < 0 || sb_y >= height { + return Err(BadSuperblock(format!("Outside frame bounds: x={sb_x}, y={sb_y}"))); + } + + let remaining_width = width - sb_x; + let remaining_height = height - sb_y; + let cropped_sb_width = sb_width.min(remaining_width); + let cropped_sb_height = sb_height.min(remaining_height); + + let pixel_buffer = sb.get_pixels(plane, pixel_type)?; + + if cropped_sb_width > pixel_buffer.width() || cropped_sb_height > pixel_buffer.height() { + return Err(BadPixelBuffer(format!( + "Expected pixel buffer shape: ({}x{}), Actual: ({}x{})", + cropped_sb_width, + cropped_sb_height, + pixel_buffer.width(), + pixel_buffer.height(), + ))); + } + + for rel_y in 0..sb_height { + let abs_y = sb_y + rel_y; + // Clip on frame bottom edge if frame height isn't a multiple of superblock size. + if abs_y >= height { + break; + } + for rel_x in 0..sb_width { + let abs_x = sb_x + rel_x; + // Clip on frame right edge if frame width isn't a multiple of superblock size. + if abs_x >= width { + break; + } + let dest_index = (abs_y * width + abs_x) as usize; + pixels[dest_index] = pixel_buffer.get_pixel(rel_x, rel_y, bit_depth)?; + } + } + } + Ok(Self { + bit_depth, + width, + height, + pixels, + plane, + pixel_type, + }) + } + + pub fn create_from_frame(frame: &Frame, plane: Plane, pixel_type: PixelType) -> Result<Self, FrameError> { + if let Some(tip_params) = &frame.tip_frame_params { + // TODO(comc): Const for this 2. + if tip_params.tip_mode == 2 { + return Self::create_from_tip_frame(frame, plane, pixel_type); + } + } + Self::create_from_superblocks(frame, plane, pixel_type) + } +} + +impl Superblock { + /// Retrieves a single `PixelBuffer` from this superblock. + pub fn get_single_pixel_buffer(&self, plane: Plane, pixel_type: PixelType) -> Result<&PixelBuffer, FrameError> { + use FrameError::*; + let pixel_data = self.pixel_data.get(plane.to_usize()).ok_or(MissingPixelBuffer(format!( + "Pixel data for plane {} not present ({} total)", + plane.to_usize(), + self.pixel_data.len() + )))?; + + let pixels = match pixel_type { + PixelType::Original => pixel_data.original.as_ref().ok_or(MissingPixelBuffer(format!( + "Original pixel data for plane {} not present", + plane.to_usize() + )))?, + + PixelType::Prediction => pixel_data.prediction.as_ref().ok_or(MissingPixelBuffer(format!( + "Prediction pixel data for plane {} not present", + plane.to_usize() + )))?, + + PixelType::PreFiltered => pixel_data.pre_filtered.as_ref().ok_or(MissingPixelBuffer(format!( + "Pre-filtered pixel data for plane {} not present", + plane.to_usize() + )))?, + + PixelType::Reconstruction => pixel_data.reconstruction.as_ref().ok_or(MissingPixelBuffer(format!( + "Reconstruction pixel data for plane {} not present", + plane.to_usize() + )))?, + + _ => { + return Err(FrameError::Internal(format!( + "Tried to retrieve invalid single pixel buffer type ({pixel_type:?}) from protobuf superblock." + ))) + } + }; + let width = pixels.width; + let height = pixels.height; + let num_pixels = width * height; + let actual_pixels = pixels.pixels.len() as i32; + if num_pixels != actual_pixels { + return Err(FrameError::BadPixelBuffer(format!( + "Pixel buffer contains {actual_pixels} pixels, but dimensions require {num_pixels} pixels ({}x{})", + width, height + ))); + } + Ok(pixels) + } + + pub fn get_pixels(&self, plane: Plane, pixel_type: PixelType) -> Result<PixelBufferRef, FrameError> { + if pixel_type.is_delta() { + let (buf_1, buf_2) = match pixel_type { + PixelType::Residual => { + let pre_filtered = self.get_single_pixel_buffer(plane, PixelType::PreFiltered)?; + let prediction = self.get_single_pixel_buffer(plane, PixelType::Prediction)?; + (pre_filtered, prediction) + } + PixelType::FilterDelta => { + let reconstruction = self.get_single_pixel_buffer(plane, PixelType::Reconstruction)?; + let pre_filtered = self.get_single_pixel_buffer(plane, PixelType::PreFiltered)?; + (reconstruction, pre_filtered) + } + PixelType::Distortion => { + let original = self.get_single_pixel_buffer(plane, PixelType::Original)?; + let reconstruction = self.get_single_pixel_buffer(plane, PixelType::Reconstruction)?; + (original, reconstruction) + } + _ => { + return Err(FrameError::Internal(format!( + "Tried to retrieve invalid pixel delta type: {pixel_type:?}" + ))); + } + }; + + if buf_1.width != buf_2.width || buf_1.height != buf_2.height { + return Err(FrameError::BadPixelBuffer(format!( + "Mismatched dimensions: {}x{} vs {}x{}", + buf_1.width, buf_1.height, buf_2.width, buf_2.height + ))); + } + Ok(PixelBufferRef::new_delta(buf_1, buf_2)) + } else { + let buf = self.get_single_pixel_buffer(plane, pixel_type)?; + Ok(PixelBufferRef::new_single(buf)) + } + } +}
diff --git a/tools/avm_analyzer/avm_stats/src/plane.rs b/tools/avm_analyzer/avm_stats/src/plane.rs new file mode 100644 index 0000000..025dd5f --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/plane.rs
@@ -0,0 +1,91 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize, Deserialize)] +pub enum Plane { + Y, + U, + V, +} +impl Plane { + pub fn as_str(&self) -> &str { + match self { + Plane::Y => "Y plane", + Plane::U => "U plane", + Plane::V => "V plane", + } + } + + pub fn from_i32(i: i32) -> Self { + match i { + 0 => Plane::Y, + 1 => Plane::U, + 2 => Plane::V, + _ => panic!("Bad plane id: {i}"), + } + } + + pub fn to_i32(&self) -> i32 { + match self { + Plane::Y => 0, + Plane::U => 1, + Plane::V => 2, + } + } + + pub fn to_usize(&self) -> usize { + self.to_i32() as usize + } + + pub fn is_chroma(&self) -> bool { + match self { + Plane::Y => false, + Plane::U | Plane::V => true, + } + } + + pub fn subsampled(&self, dimension: i32) -> i32 { + if self.is_chroma() { + (dimension + 1) / 2 + } else { + dimension + } + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum PlaneType { + Planar(Plane), + #[default] + Rgb, +} + +impl PlaneType { + // For partition tree selection + pub fn use_chroma(&self) -> bool { + match self { + PlaneType::Rgb | PlaneType::Planar(Plane::Y) => false, + PlaneType::Planar(Plane::U) | PlaneType::Planar(Plane::V) => true, + } + } + + pub fn to_plane(&self) -> Plane { + match self { + PlaneType::Rgb => Plane::Y, + PlaneType::Planar(plane) => *plane, + } + } +} + +impl fmt::Display for PlaneType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let text = match self { + PlaneType::Planar(Plane::Y) => "Y", + PlaneType::Planar(Plane::U) => "U", + PlaneType::Planar(Plane::V) => "V", + PlaneType::Rgb => "YUV", + }; + write!(f, "{text}") + } +}
diff --git a/tools/avm_analyzer/avm_stats/src/spatial.rs b/tools/avm_analyzer/avm_stats/src/spatial.rs new file mode 100644 index 0000000..d0dd00c --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/spatial.rs
@@ -0,0 +1,109 @@ +use crate::{CodingUnit, Frame, Partition, Superblock, TransformUnit}; + +pub trait Spatial { + fn width(&self) -> i32; + fn height(&self) -> i32; + fn x(&self) -> i32; + fn y(&self) -> i32; + fn rect(&self) -> emath::Rect { + emath::Rect::from_min_size( + emath::pos2(self.x() as f32, self.y() as f32), + emath::vec2(self.width() as f32, self.height() as f32), + ) + } + fn size_name(&self) -> String { + format!("{}x{}", self.width(), self.height()) + } +} + +// TODO(comc): Could use a macro to implement each of these. +impl Spatial for TransformUnit { + fn width(&self) -> i32 { + // TODO(comc): This is very messy. Add derive Default to prost build script. + self.size.as_ref().map_or(0, |size| size.width) + } + + fn height(&self) -> i32 { + self.size.as_ref().map_or(0, |size| size.height) + } + + fn x(&self) -> i32 { + self.position.as_ref().map_or(0, |position| position.x) + } + + fn y(&self) -> i32 { + self.position.as_ref().map_or(0, |position| position.y) + } +} + +impl Spatial for CodingUnit { + fn width(&self) -> i32 { + self.size.as_ref().map_or(0, |size| size.width) + } + + fn height(&self) -> i32 { + self.size.as_ref().map_or(0, |size| size.height) + } + + fn x(&self) -> i32 { + self.position.as_ref().map_or(0, |position| position.x) + } + + fn y(&self) -> i32 { + self.position.as_ref().map_or(0, |position| position.y) + } +} + +impl Spatial for Partition { + fn width(&self) -> i32 { + self.size.as_ref().map_or(0, |size| size.width) + } + + fn height(&self) -> i32 { + self.size.as_ref().map_or(0, |size| size.height) + } + + fn x(&self) -> i32 { + self.position.as_ref().map_or(0, |position| position.x) + } + + fn y(&self) -> i32 { + self.position.as_ref().map_or(0, |position| position.y) + } +} + +impl Spatial for Superblock { + fn width(&self) -> i32 { + self.size.as_ref().map_or(0, |size| size.width) + } + + fn height(&self) -> i32 { + self.size.as_ref().map_or(0, |size| size.height) + } + + fn x(&self) -> i32 { + self.position.as_ref().map_or(0, |position| position.x) + } + + fn y(&self) -> i32 { + self.position.as_ref().map_or(0, |position| position.y) + } +} + +impl Spatial for Frame { + fn width(&self) -> i32 { + self.frame_params.as_ref().map_or(0, |frame_params| frame_params.width) + } + + fn height(&self) -> i32 { + self.frame_params.as_ref().map_or(0, |frame_params| frame_params.height) + } + + fn x(&self) -> i32 { + 0 + } + + fn y(&self) -> i32 { + 0 + } +}
diff --git a/tools/avm_analyzer/avm_stats/src/stats.rs b/tools/avm_analyzer/avm_stats/src/stats.rs new file mode 100644 index 0000000..abcf844 --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/stats.rs
@@ -0,0 +1,331 @@ +use std::collections::{HashMap, HashSet}; + +use itertools::Itertools; + +use ordered_float::OrderedFloat; +use serde::{Deserialize, Serialize}; + +use crate::{CodingUnitKind, Frame, Plane, PlaneType, ProtoEnumMapping, Spatial}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub enum FrameStatistic { + LumaModes, + ChromaModes, + BlockSizes, + Symbols, + PartitionSplit, +} + +#[derive(Default, Debug, Deserialize, Serialize, PartialEq)] +pub struct StatsFilter { + pub include: Vec<String>, + pub exclude: Vec<String>, +} + +impl StatsFilter { + fn from_comma_separated(include: &str, exclude: &str) -> Self { + Self { + include: include + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(), + exclude: exclude + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(), + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub enum StatSortMode { + Unsorted, + ByName, + ByValue, +} +impl StatSortMode { + pub fn name(&self) -> &'static str { + match self { + Self::Unsorted => "Unsorted", + Self::ByName => "By name", + Self::ByValue => "By value", + } + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct StatsSettings { + pub sort_by: StatSortMode, + // Using separate bool + value fields rather than an Option to make the UI design a bit more intuitive (e.g. checkbox + disabled number input). + pub apply_limit_count: bool, + pub limit_count: usize, + pub apply_limit_frac: bool, + pub limit_frac: f32, + pub include_filter: String, + pub exclude_filter: String, + pub include_filter_exact_match: bool, + pub exclude_filter_exact_match: bool, + pub show_relative_total: bool, + // Comma separated list of block sizes to include for partition split stats. + pub partition_split_block_sizes: String, +} + +impl Default for StatsSettings { + fn default() -> Self { + Self { + sort_by: StatSortMode::ByValue, + apply_limit_count: false, + limit_count: 20, + apply_limit_frac: false, + limit_frac: 0.01, + include_filter: "".into(), + exclude_filter: "".into(), + include_filter_exact_match: false, + exclude_filter_exact_match: false, + show_relative_total: false, + partition_split_block_sizes: "".into(), + } + } +} + +#[derive(Debug)] +pub struct Sample { + pub name: String, + pub value: f64, +} +impl Sample { + pub fn new(name: String, value: f64) -> Self { + Self { name, value } + } +} + +impl FrameStatistic { + fn luma_modes(&self, frame: &Frame) -> HashMap<String, f64> { + let modes = frame.iter_coding_units(CodingUnitKind::Shared).map(|ctx| { + let cu = ctx.coding_unit; + let prediction_mode = cu.prediction_mode.as_ref().unwrap(); + frame + .enum_lookup(ProtoEnumMapping::PredictionMode, prediction_mode.mode) + .unwrap_or("UNKNOWN".into()) + }); + let mut modes_map: HashMap<String, f64> = HashMap::new(); + + for mode in modes { + *modes_map.entry(mode).or_default() += 1.0; + } + + modes_map + } + + fn chroma_modes(&self, frame: &Frame) -> HashMap<String, f64> { + let kind = frame.coding_unit_kind(PlaneType::Planar(Plane::U)); + let modes = frame.iter_coding_units(kind).map(|ctx| { + let cu = ctx.coding_unit; + let prediction_mode = cu.prediction_mode.as_ref().unwrap(); + frame + .enum_lookup(ProtoEnumMapping::UvPredictionMode, prediction_mode.uv_mode) + .unwrap_or("UNKNOWN".into()) + }); + let mut modes_map: HashMap<String, f64> = HashMap::new(); + + for mode in modes { + *modes_map.entry(mode).or_default() += 1.0; + } + + modes_map + } + + fn block_sizes(&self, frame: &Frame) -> HashMap<String, f64> { + let sizes = frame.iter_coding_units(CodingUnitKind::Shared).map(|ctx| { + let cu = ctx.coding_unit; + let w = cu.width(); + let h = cu.height(); + format!("{w}x{h}") + }); + let mut sizes_map: HashMap<String, f64> = HashMap::new(); + + for size in sizes { + *sizes_map.entry(size).or_default() += 1.0; + } + sizes_map + } + + fn partition_split(&self, frame: &Frame, settings: &StatsSettings) -> HashMap<String, f64> { + // TODO(comc): Add settings option for partition kind. + let filter = StatsFilter::from_comma_separated(&settings.partition_split_block_sizes, ""); + let splits = frame.iter_partitions(CodingUnitKind::Shared).filter_map(|ctx| { + let partition = ctx.partition; + let size = partition.size_name(); + if !filter.include.is_empty() && !filter.include.iter().any(|incl| &size == incl) { + return None; + } + let partition_type = frame + .enum_lookup(ProtoEnumMapping::PartitionType, partition.partition_type) + .unwrap_or("UNKNOWN".into()); + Some(partition_type) + }); + let mut splits_map: HashMap<String, f64> = HashMap::new(); + + for split in splits { + *splits_map.entry(split).or_default() += 1.0; + } + splits_map + } + + fn symbols(&self, frame: &Frame) -> HashMap<String, f64> { + let mut symbols: HashMap<String, f64> = HashMap::new(); + // TODO(comc): Use iter_symbols. Add iter_symbols method for partition blocks as well. + let sbs = frame.iter_superblocks().map(|sb_ctx| { + let sb = sb_ctx.superblock; + let mut symbols_sb: HashMap<String, f64> = HashMap::new(); + for symbol in sb.symbols.iter() { + let info = symbol.info_id; + let info = &frame.symbol_info[&info]; + let name = info.source_function.clone(); + let bits = symbol.bits; + *symbols_sb.entry(name.clone()).or_default() += bits as f64; + } + symbols_sb + }); + for symbols_sb in sbs { + for (name, bits) in symbols_sb { + *symbols.entry(name.clone()).or_default() += bits; + } + } + symbols + } + + fn apply_settings(&self, mapping: HashMap<String, f64>, settings: &StatsSettings) -> Vec<Sample> { + let mut samples: Vec<_> = mapping + .into_iter() + .map(|(name, value)| Sample::new(name, value)) + .collect(); + let filter: StatsFilter = StatsFilter::from_comma_separated(&settings.include_filter, &settings.exclude_filter); + let total: f64 = samples.iter().map(|sample| sample.value).sum(); + let mut other = 0.0; + samples.retain(|Sample { name, value }| { + let mut keep = true; + // TODO(comc): Make this a method of StatsFilter. + if !filter.include.is_empty() { + if settings.include_filter_exact_match { + if !filter.include.contains(name) { + keep = false; + } + } else if !filter.include.iter().any(|incl| name.contains(incl)) { + keep = false; + } + } + if settings.exclude_filter_exact_match { + if filter.exclude.contains(name) { + keep = false + } + } else if filter.exclude.iter().any(|excl| name.contains(excl)) { + keep = false; + } + if !keep { + other += value; + } + keep + }); + + let filtered_total: f64 = samples.iter().map(|sample| sample.value).sum(); + + let top_n = if settings.apply_limit_count { + let top_n: HashSet<_> = samples + .iter() + .sorted_by_key(|sample| (OrderedFloat(sample.value), &sample.name)) // name used as a tie-breaker. + .rev() + .map(|sample| sample.name.clone()) + .take(settings.limit_count) + .collect(); + Some(top_n) + } else { + None + }; + + samples.retain(|Sample { name, value }| { + let mut keep = true; + if settings.apply_limit_frac { + let frac = if settings.show_relative_total { + *value / filtered_total + } else { + *value / total + }; + if frac < settings.limit_frac as f64 { + keep = false; + } + } + + if let Some(top_n) = &top_n { + if !top_n.contains(name) { + keep = false; + } + } + + if !keep { + other += value; + } + keep + }); + + if !settings.show_relative_total && other > 0.0 { + samples.push(Sample::new("Other".into(), other)); + } + match settings.sort_by { + StatSortMode::ByName => samples + .into_iter() + .sorted_by_key(|sample| sample.name.clone()) + .collect(), + StatSortMode::ByValue => samples + .into_iter() + .sorted_by_key(|sample| (OrderedFloat(sample.value), sample.name.clone())) // name used as a tie-breaker. + .collect(), + StatSortMode::Unsorted => samples, + } + } + + pub fn calculate(&self, frame: &Frame, settings: &StatsSettings) -> Vec<Sample> { + let mapping = match self { + FrameStatistic::LumaModes => self.luma_modes(frame), + FrameStatistic::ChromaModes => self.chroma_modes(frame), + FrameStatistic::BlockSizes => self.block_sizes(frame), + FrameStatistic::Symbols => self.symbols(frame), + FrameStatistic::PartitionSplit => self.partition_split(frame, settings), + }; + self.apply_settings(mapping, settings) + } + + pub fn name(&self) -> &'static str { + match self { + FrameStatistic::LumaModes => "Luma modes", + FrameStatistic::ChromaModes => "Chroma modes", + FrameStatistic::BlockSizes => "Block sizes", + FrameStatistic::Symbols => "Symbols", + FrameStatistic::PartitionSplit => "Partition split", + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_exclude_filter() { + let mapping: HashMap<String, f64> = [("ABC", 1.0), ("AAA", 2.0)] + .iter() + .map(|(k, v)| (k.to_string(), *v)) + .collect(); + let settings = StatsSettings { + exclude_filter: "A".into(), + exclude_filter_exact_match: true, + show_relative_total: true, + ..Default::default() + }; + let samples = FrameStatistic::Symbols.apply_settings(mapping, &settings); + assert_eq!(samples.len(), 2); + } +}
diff --git a/tools/avm_analyzer/avm_stats/src/superblock.rs b/tools/avm_analyzer/avm_stats/src/superblock.rs new file mode 100644 index 0000000..66f933b --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/superblock.rs
@@ -0,0 +1,87 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + CodingUnitContext, CodingUnitKind, CodingUnitLocator, Frame, PartitionContext, PartitionIterator, PartitionLocator, + Superblock, SymbolContext, SymbolRange, +}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct SuperblockLocator { + pub index: usize, +} + +impl SuperblockLocator { + pub fn new(index: usize) -> Self { + Self { index } + } + pub fn try_resolve<'a>(&self, frame: &'a Frame) -> Option<SuperblockContext<'a>> { + frame.superblocks.get(self.index).map(|superblock| SuperblockContext { + superblock, + frame, + locator: *self, + }) + } + pub fn resolve<'a>(&self, frame: &'a Frame) -> SuperblockContext<'a> { + self.try_resolve(frame).unwrap() + } +} + +#[derive(Copy, Clone, Debug)] +pub struct SuperblockContext<'a> { + pub superblock: &'a Superblock, + pub frame: &'a Frame, + pub locator: SuperblockLocator, +} + +impl<'a> SuperblockContext<'a> { + pub fn iter_partitions(&self, kind: CodingUnitKind) -> impl Iterator<Item = PartitionContext<'a>> { + let root = match kind { + CodingUnitKind::Shared | CodingUnitKind::LumaOnly => self.superblock.luma_partition_tree.as_ref().unwrap(), + CodingUnitKind::ChromaOnly => self.superblock.chroma_partition_tree.as_ref().unwrap(), + }; + + let root_locator = PartitionLocator::new(Vec::new(), kind, self.locator); + let root_context = PartitionContext { + partition: root, + superblock_context: *self, + locator: root_locator, + }; + PartitionIterator { + stack: vec![(root_context, 0)], + max_depth: None, + } + } + + pub fn root_partition(&self, kind: CodingUnitKind) -> Option<PartitionContext<'a>> { + self.iter_partitions(kind).next() + } + + // Consuming self simplifies lifetime management in caller. + pub fn iter_coding_units(self, kind: CodingUnitKind) -> impl Iterator<Item = CodingUnitContext<'a>> { + let coding_units = match kind { + CodingUnitKind::Shared | CodingUnitKind::LumaOnly => self.superblock.coding_units_shared.iter(), + CodingUnitKind::ChromaOnly => self.superblock.coding_units_chroma.iter(), + }; + coding_units + .enumerate() + .map(move |(index, coding_unit)| CodingUnitContext { + coding_unit, + superblock_context: self, + locator: CodingUnitLocator::new(self.locator, kind, index), + }) + } + + pub fn iter_symbols(&self, range: Option<SymbolRange>) -> impl Iterator<Item = SymbolContext<'a>> { + let range = range.unwrap_or(SymbolRange { + start: 0, + end: self.superblock.symbols.len() as u32, + }); + self.superblock.symbols[range.start as usize..range.end as usize] + .iter() + .map(|sym| SymbolContext { + symbol: sym, + info: self.frame.symbol_info.get(&sym.info_id), + superblock: self.superblock, + }) + } +}
diff --git a/tools/avm_analyzer/avm_stats/src/symbol.rs b/tools/avm_analyzer/avm_stats/src/symbol.rs new file mode 100644 index 0000000..886c8c6 --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/symbol.rs
@@ -0,0 +1,34 @@ +use crate::{Superblock, Symbol, SymbolInfo}; +use once_cell::sync::Lazy; + +pub static MISSING_SYMBOL_INFO: Lazy<SymbolInfo> = Lazy::new(|| SymbolInfo { + id: -1, + source_file: "UNKNOWN".into(), + source_line: -1, + source_function: "UNKNOWN".into(), + tags: Vec::new(), +}); + +#[derive(Copy, Clone)] +pub struct SymbolContext<'a> { + pub symbol: &'a Symbol, + pub info: Option<&'a SymbolInfo>, + pub superblock: &'a Superblock, +} + +impl<'a> SymbolContext<'a> { + // pub fn from_coding_unit_context( + // transform_unit: &'a TransformUnit, + // plane: Plane, + // transform_unit_index: usize, + // coding_unit_context: CodingUnitContext<'a>, + // ) -> Self { + // let index = TransformUnitIndex::new(coding_unit_context.index, plane, transform_unit_index); + // Self { + // transform_unit, + // coding_unit: coding_unit_context.coding_unit, + // superblock: coding_unit_context.superblock, + // index, + // } + // } +}
diff --git a/tools/avm_analyzer/avm_stats/src/transform_unit.rs b/tools/avm_analyzer/avm_stats/src/transform_unit.rs new file mode 100644 index 0000000..310ab6f --- /dev/null +++ b/tools/avm_analyzer/avm_stats/src/transform_unit.rs
@@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; + +use crate::{CodingUnitContext, CodingUnitLocator, Frame, Plane, ProtoEnumMapping, TransformUnit}; + +// TX blocks larger than 32x32 have all coefficients other than the top-left 32x32 set to 0. +pub const MAX_COEFFS_SIZE: usize = 32; + +impl TransformUnit { + pub fn primary_tx_type_or_skip(&self, frame: &Frame) -> String { + let tx_type = self.tx_type; + // Only lower 4-bits used for primary transform. Upper bits are IST. + let tx_type = tx_type & 0xF; + if self.skip == 1 { + "SKIP".to_owned() + } else { + frame + .enum_lookup(ProtoEnumMapping::TransformType, tx_type) + .unwrap_or(format!("UNKNOWN ({tx_type})")) + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct TransformUnitLocator { + pub coding_unit: CodingUnitLocator, + pub plane: Plane, + /// Index of this tranform unit with its parent. + pub index: usize, +} + +// Note: Converting plane to a usize does not automatically get the correct index into a coding unit's transform planes. +// e.g., in SDP mode, the chroma coding units will have two planes, but with plane IDs (1, 2), not (0, 1). + +impl TransformUnitLocator { + pub fn new(coding_unit: CodingUnitLocator, plane: Plane, index: usize) -> Self { + Self { + coding_unit, + plane, + index, + } + } + + pub fn try_resolve<'a>(&self, frame: &'a Frame) -> Option<TransformUnitContext<'a>> { + let coding_unit_context = self.coding_unit.try_resolve(frame)?; + let plane_index = coding_unit_context.coding_unit.plane_index(self.plane).ok()?; + let transform_unit = coding_unit_context + .coding_unit + .transform_planes + .get(plane_index)? + .transform_units + .get(self.index)?; + Some(TransformUnitContext { + transform_unit, + coding_unit_context, + locator: *self, + }) + } + + pub fn resolve<'a>(&self, frame: &'a Frame) -> TransformUnitContext<'a> { + self.try_resolve(frame).unwrap() + } +} + +#[derive(Copy, Clone)] +pub struct TransformUnitContext<'a> { + pub transform_unit: &'a TransformUnit, + pub coding_unit_context: CodingUnitContext<'a>, + pub locator: TransformUnitLocator, +}
diff --git a/tools/avm_analyzer/build_avm.sh b/tools/avm_analyzer/build_avm.sh new file mode 100755 index 0000000..0fcb9cb --- /dev/null +++ b/tools/avm_analyzer/build_avm.sh
@@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +while [[ "$#" -gt 0 ]]; do + case $1 in + --avm_build_dir) avm_build_dir="$2"; shift ;; + --avm_source_dir) avm_source_dir="$2"; shift ;; + *) echo "Unknown arg: $1"; exit 1 ;; + esac + shift +done + +if [[ -z ${avm_build_dir} || -z ${avm_source_dir} ]]; then + echo "Usage: ./build_avm.sh --avm_source_dir <AVM_GIT_ROOT> --avm_build_dir <OUTPUT_PATH>" + exit 1 +fi +mkdir -p ${avm_build_dir} + +cd ${avm_build_dir} +cmake ${avm_source_dir} -DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_COMPILER=gcc \ + -DCMAKE_BUILD_TYPE=Release -DCONFIG_ACCOUNTING=1 -DCONFIG_INSPECTION=1 \ + -DCONFIG_EXTRACT_PROTO=1 +make -j
diff --git a/tools/avm_analyzer/build_avm_docker.sh b/tools/avm_analyzer/build_avm_docker.sh new file mode 100755 index 0000000..e3ef6f2 --- /dev/null +++ b/tools/avm_analyzer/build_avm_docker.sh
@@ -0,0 +1,21 @@ +#!/bin/bash +set -e +GIT_ROOT=$(git rev-parse --show-toplevel) + +while [[ "$#" -gt 0 ]]; do + case $1 in + --avm_build_dir) avm_build_dir="$2"; shift ;; + *) echo "Unknown arg: $1"; exit 1 ;; + esac + shift +done + +if [[ -z ${avm_build_dir} ]]; then + echo "Usage: ./build_avm_docker.sh --avm_build_dir <OUTPUT_PATH>" + exit 1 +fi +mkdir -p ${avm_build_dir} + +AVM_BUILD_CMD="/scripts/build_avm.sh --avm_build_dir /avm_build --avm_source_dir /avm" + +docker run -it --rm -v ${GIT_ROOT}:/avm:ro -v $(realpath ${avm_build_dir}):/avm_build avm_analyzer_runtime bash -c "${AVM_BUILD_CMD}"
diff --git a/tools/avm_analyzer/check.sh b/tools/avm_analyzer/check.sh new file mode 100755 index 0000000..6e563bd --- /dev/null +++ b/tools/avm_analyzer/check.sh
@@ -0,0 +1,12 @@ +#!/bin/bash +set -eux + +cargo clippy -p avm-stats +cargo clippy -p avm-analyzer-app --target wasm32-unknown-unknown +cargo clippy -p avm-analyzer-common +cargo clippy -p avm-analyzer-server +cargo fmt --all + +cargo test -p avm-stats +cargo test -p avm-analyzer-common +cargo test -p avm-analyzer-server \ No newline at end of file
diff --git a/tools/avm_analyzer/convert_stream.sh b/tools/avm_analyzer/convert_stream.sh new file mode 100755 index 0000000..01bf7f0 --- /dev/null +++ b/tools/avm_analyzer/convert_stream.sh
@@ -0,0 +1,32 @@ +#!/bin/bash +set -xe + +while [[ "$#" -gt 0 ]]; do + case $1 in + -b|--avm_build_dir) avm_build_dir="$2"; shift ;; + -s|--stream) stream="$2"; shift ;; + -o|--output) output="$2"; shift ;; + -y|--yuv) yuv="$2"; shift ;; + -n|--limit) limit="$2"; shift ;; + *) echo "Unknown arg: $1"; exit 1 ;; + esac + shift +done + +if [[ -z ${avm_build_dir} || -z ${stream} || -z ${output} ]]; then + echo "Usage: ./convert_stream.sh --avm_build_dir <AVM_BUILD_PATH> --stream <STREAM_PATH> --output <ZIP_OUTPUT_PATH> [--yuv <PATH_TO_ORIG_YUV>] [--limit <NUM_FRAMES>]" + exit 1 +fi + +orig_yuv_arg="" +if [[ -n ${yuv} ]]; then +orig_yuv_arg="--orig_yuv=${yuv}" +fi +limit_arg="" +if [[ -n ${limit} ]]; then +limit_arg="--limit=${limit}" +fi +tmpdir=$(mktemp -d) +${avm_build_dir}/extract_proto --stream ${stream} --output_folder ${tmpdir} ${orig_yuv_arg} ${limit_arg} +zip --filesync --recurse-paths --junk-paths ${output} ${tmpdir}/*.pb +rm ${tmpdir}/*.pb
diff --git a/tools/avm_analyzer/launch_server_docker.sh b/tools/avm_analyzer/launch_server_docker.sh new file mode 100755 index 0000000..1a75046 --- /dev/null +++ b/tools/avm_analyzer/launch_server_docker.sh
@@ -0,0 +1,48 @@ +#!/bin/bash +set -e +GIT_ROOT=$(git rev-parse --show-toplevel) + +port=8080 +build_avm=0 +build_docker=1 +while [[ "$#" -gt 0 ]]; do + case $1 in + --port) port="$2"; shift ;; + --streams_dir) streams_dir="$2"; shift ;; + --avm_build_dir) build_avm=0; avm_build_dir="$2"; shift ;; + --build_avm_standalone) build_avm=1 ;; + --nobuild) build_docker=0 ;; + *) echo "Unknown arg: $1"; exit 1 ;; + esac + shift +done + +if [[ -z ${streams_dir} ]]; then + echo "Usage: ./launch_server_docker.sh --streams_dir <STREAMS_PATH> [--nobuild] [--avm_build_dir <CUSTOM_AVM_BUILD_DIR>] [--port <PORT>] [--build_avm_standalone]" + exit 1 +fi +# Note: When switching between local and docker builds, there may be permissions errors on the streams dir since the file owner with docker will be root. +# Wiping the dir before switching is recommended. +mkdir -p ${streams_dir} + +if [[ ${build_docker} -eq 1 ]]; then +# Build the runtime base image first. This contains no Rust dependencies, only what's needed to build libavm. +docker build -t avm_analyzer_runtime -f Dockerfile.runtime ${GIT_ROOT} +docker build -t avm_analyzer -f Dockerfile.builder ${GIT_ROOT} +fi + +if [[ ${build_avm} -eq 1 ]]; then +./build_avm_docker.sh --avm_build_dir ${avm_build_dir} +fi + +avm_build_dir_mount=() +if [[ -n ${avm_build_dir} ]]; then +avm_build_dir_mount=("-v $(realpath ${avm_build_dir}):/avm_build:ro") +fi + +SERVER_CMD="/app/avm-analyzer-server --extract-proto /avm_build/extract_proto\ + --dump-obu /avm_build/tools/dump_obu --working-dir /streams\ + --frontend-root /app/dist --ip 0.0.0.0 --port 8080" + + +docker run -it --init --rm -p 127.0.0.1:${port}:8080 -v ${streams_dir}:/streams ${avm_build_dir_mount[@]} avm_analyzer ${SERVER_CMD}
diff --git a/tools/avm_analyzer/launch_server_local.sh b/tools/avm_analyzer/launch_server_local.sh new file mode 100755 index 0000000..0930915 --- /dev/null +++ b/tools/avm_analyzer/launch_server_local.sh
@@ -0,0 +1,38 @@ +#!/bin/bash +set -e +trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT +GIT_ROOT=$(git rev-parse --show-toplevel) + +port=8080 +# Use cargo watch / trunk watch to automatically rebuild on source changes. +watch=0 + +while [[ "$#" -gt 0 ]]; do + case $1 in + -p|--port) port="$2"; shift ;; + -s|--streams_dir) streams_dir="$2"; shift ;; + -a|--avm_build_dir) avm_build_dir="$2"; shift ;; + -w|--watch) watch="1" ;; + *) echo "Unknown arg: $1"; exit 1 ;; + esac + shift +done + +if [[ -z ${streams_dir} || -z ${avm_build_dir} ]]; then + echo "Usage: ./launch_server_docker.sh --streams_dir <STREAMS_PATH> --avm_build_dir <AVM_BUILD_PATH> [--port <PORT>]" + exit 1 +fi +mkdir -p ${streams_dir} + +SERVER_ARGS=(--extract-proto ${avm_build_dir}/extract_proto\ + --dump-obu ${avm_build_dir}/tools/dump_obu --working-dir ${streams_dir}\ + --frontend-root ${GIT_ROOT}/tools/avm_analyzer/avm_analyzer_app/dist --ip "127.0.0.1" --port ${port}) + +if [[ ${watch} -eq 1 ]]; then +trunk watch --release ${GIT_ROOT}/tools/avm_analyzer/avm_analyzer_app/index.html & +WATCH_CMD="run --release --bin avm-analyzer-server -- ${SERVER_ARGS[@]}" +cargo watch -w avm_analyzer_server -x "${WATCH_CMD}" +else +trunk build --release ${GIT_ROOT}/tools/avm_analyzer/avm_analyzer_app/index.html +cargo run --release --bin avm-analyzer-server -- ${SERVER_ARGS[@]} +fi \ No newline at end of file
diff --git a/tools/avm_analyzer/rustfmt.toml b/tools/avm_analyzer/rustfmt.toml new file mode 100644 index 0000000..3da0ba7 --- /dev/null +++ b/tools/avm_analyzer/rustfmt.toml
@@ -0,0 +1,4 @@ +reorder_imports = true +format_strings = true +indent_style = "Visual" +max_width = 120 \ No newline at end of file