diff --git a/.gitignore b/.gitignore index 983c2fb..c3e76ca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /tests/semver-port/target/ /artifacts/ /linden/ +/crates/metacrate-grid-agent/linden/ .env .env.* !.env.example diff --git a/Cargo.lock b/Cargo.lock index 8dccba6..98d3b56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,92 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit_consumer" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit", + "accesskit_consumer 0.38.0", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_windows" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" +dependencies = [ + "accesskit", + "accesskit_consumer 0.35.0", + "hashbrown 0.16.1", + "static_assertions", + "windows", + "windows-core", +] + +[[package]] +name = "accesskit_winit" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_windows", + "raw-window-handle", + "winit", +] + [[package]] name = "adler2" version = "2.0.1" @@ -43,6 +129,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.5" @@ -80,6 +179,37 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.1", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.20", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + [[package]] name = "android_system_properties" version = "0.1.6" @@ -158,12 +288,42 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.52.0", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -212,6 +372,104 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "assert_type_match" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +dependencies = [ + "portable-atomic", +] + [[package]] name = "async-trait" version = "0.1.92" @@ -228,6 +486,19 @@ name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "atomicow" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301801c08259e328a1c7da556608c0c22687708831b22024dbd3a57ea741e6de" +dependencies = [ + "portable-atomic", + "portable-atomic-util", +] [[package]] name = "autocfg" @@ -283,6 +554,1182 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f09c37bc0e9f0924b7dae9988265ef3c76c88538f41a3b06caf4bed07cee5226" +[[package]] +name = "bevy" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfadbebfc6599aa59289b754ac30023959854304f3d393da2cf62bb3cd5df8f" +dependencies = [ + "bevy_internal", +] + +[[package]] +name = "bevy_a11y" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c02cd43f914ef7a0f5b16f8a3d88c378c07c92f1a8c32ac5ac4d4f73e55f9e2" +dependencies = [ + "accesskit", + "bevy_app", + "bevy_derive", + "bevy_ecs", + "bevy_reflect", +] + +[[package]] +name = "bevy_android" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae5ea5245a845cefa8efb5ff7c2a8a36cdebfb99596068780f340f0b5aba6689" +dependencies = [ + "android-activity", +] + +[[package]] +name = "bevy_animation" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc513aac5cda673fb138236e969b4514e0a9b9f93fd33288111f147baa8af623" +dependencies = [ + "bevy_animation_macros", + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_time", + "bevy_transform", + "bevy_utils", + "blake3", + "derive_more", + "downcast-rs 2.0.2", + "either", + "petgraph", + "ron", + "serde", + "smallvec", + "thiserror 2.0.20", + "thread_local", + "tracing", + "uuid", +] + +[[package]] +name = "bevy_animation_macros" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed2910d9ca5c3b48968a54c4571789e4e81935cf3c823383fcac34ca28e2b0e" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_anti_alias" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde973fc942cb596b03cae0dbece0a4d9dc2d2790bbc9852aa415affc1210d3a" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_core_pipeline", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_utils", + "tracing", +] + +[[package]] +name = "bevy_app" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd2aa472d43b4d081ef41d26300d17bd22cba6d68df2a223eb358782fa40bc6" +dependencies = [ + "bevy_derive", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "console_error_panic_hook", + "ctrlc", + "downcast-rs 2.0.2", + "log", + "thiserror 2.0.20", + "variadics_please", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "bevy_asset" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1624eddbf2293441b2158506aba75e523432b898d9da26eb6ed3e20af4c7fd38" +dependencies = [ + "async-broadcast", + "async-channel", + "async-fs", + "async-io", + "async-lock", + "atomicow", + "bevy_android", + "bevy_app", + "bevy_asset_macros", + "bevy_diagnostic", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags 2.13.1", + "blake3", + "crossbeam-channel", + "derive_more", + "disqualified", + "downcast-rs 2.0.2", + "either", + "futures-io", + "futures-lite", + "futures-util", + "js-sys", + "ron", + "serde", + "stackfuture", + "thiserror 2.0.20", + "tracing", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "bevy_asset_macros" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "735611c80e926e960d279cba2a6f6ab27a61291752ec3b26e76bf9a0957e11e1" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_camera" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de20d0babfb51e3907eb4eb56f1e36096a6f8c77acd997f5de611df4ee067d" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_reflect", + "bevy_transform", + "bevy_utils", + "bevy_window", + "derive_more", + "downcast-rs 2.0.2", + "serde", + "smallvec", + "thiserror 2.0.20", + "wgpu-types", +] + +[[package]] +name = "bevy_clipboard" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a9792c7f2e8b5b71b9572a6d98fca4ee88ed186c8a02e164540c461ae3a92e" +dependencies = [ + "arboard", + "bevy_app", + "bevy_ecs", + "bevy_log", + "bevy_platform", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "bevy_color" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5370beae13bea2db64744f370bb33566aa770dff91c2beae245ce82217fbbc" +dependencies = [ + "bevy_math", + "bevy_reflect", + "bytemuck", + "derive_more", + "encase", + "serde", + "thiserror 2.0.20", + "wgpu-types", +] + +[[package]] +name = "bevy_core_pipeline" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564f1ba766532e8eeb20059133ff6703e8dd5c3864b92378d0d1c541096ef12" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_image", + "bevy_light", + "bevy_log", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bitflags 2.13.1", + "indexmap", + "nonmax", +] + +[[package]] +name = "bevy_derive" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2a03321f3bd8016a041b3ae6afe83e636b3540fbc61d571bf069a21fda3231" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_diagnostic" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07a3a736f15486650c4c6866798a78d625c7063c991a3cc7b13f1577d89c82ae" +dependencies = [ + "atomic-waker", + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_tasks", + "bevy_time", + "const-fnv1a-hash", + "log", + "serde", +] + +[[package]] +name = "bevy_ecs" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a350b2e4c2e47f7903d688affb8a723b31bca63af9789f1da88703f1ba04e354" +dependencies = [ + "arrayvec", + "bevy_ecs_macros", + "bevy_platform", + "bevy_ptr", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags 2.13.1", + "bumpalo", + "concurrent-queue", + "derive_more", + "fixedbitset", + "indexmap", + "log", + "nonmax", + "serde", + "slotmap", + "smallvec", + "thiserror 2.0.20", + "variadics_please", +] + +[[package]] +name = "bevy_ecs_macro_logic" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a10fef45a3e7dad717ba03e8b5f7ecdf3b3fd1cd1c5e425793525938e90d5f" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_ecs_macros" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3916d712264b6f3ec9c964ba10719450ea371e537e202a2ffcfa9f6a5e68169d" +dependencies = [ + "bevy_ecs_macro_logic", + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_encase_derive" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be03820f64f2279ae9d826c882e4ef764516ff75ab7bdc39bb40801891533b89" +dependencies = [ + "bevy_macro_utils", + "encase_derive_impl", +] + +[[package]] +name = "bevy_gizmos" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac5bcad0b8d141ef64f4a89643f74f11827ea6ef48073192b39d69156ed1b92" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_ecs", + "bevy_gizmos_macros", + "bevy_input", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_reflect", + "bevy_time", + "bevy_transform", + "bevy_utils", + "bevy_window", +] + +[[package]] +name = "bevy_gizmos_macros" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b37bbaa6241480b119da406fc261c9cf85d435addad880ae564c9b0f54a89a28" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_gizmos_render" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea2b6266d210bca0aa3bcba7e17204e437b4e86cecd4021ab7dcd32da576a02" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_ecs", + "bevy_gizmos", + "bevy_image", + "bevy_log", + "bevy_material", + "bevy_math", + "bevy_mesh", + "bevy_pbr", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_transform", + "bevy_utils", + "bytemuck", + "tracing", +] + +[[package]] +name = "bevy_gltf" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d17df73b26adfb8be89f0716b8667c60660bdd2d589863b8901caca58c99841" +dependencies = [ + "async-lock", + "base64", + "bevy_animation", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_ecs", + "bevy_image", + "bevy_light", + "bevy_material", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_transform", + "bevy_world_serialization", + "fixedbitset", + "gltf", + "itertools 0.14.0", + "percent-encoding", + "serde", + "serde_json", + "smallvec", + "thiserror 2.0.20", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_image" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca63218585b83f98831214431449e9344b363349dd5cab98dae0b8129f0a0e2f" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_ecs", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_utils", + "bitflags 2.13.1", + "bytemuck", + "futures-lite", + "guillotiere", + "half", + "image", + "ktx2", + "rectangle-pack", + "ruzstd", + "serde", + "thiserror 2.0.20", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_input" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22e8b1119ff5c0b551cc11b672d25732c4e3eda72aceabc380595bd0e7bbe88" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "derive_more", + "log", + "smol_str", + "thiserror 2.0.20", +] + +[[package]] +name = "bevy_input_focus" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d5573eb503387f73d12acde3010d49c808c790d34691e2fb410fc54b8c72b9" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_reflect", + "bevy_window", + "log", + "thiserror 2.0.20", +] + +[[package]] +name = "bevy_internal" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba99ca2e501f14f4af3322d66ae41b5c5b6ebd3aac4d00cda47dea4c00290ae" +dependencies = [ + "bevy_a11y", + "bevy_android", + "bevy_animation", + "bevy_anti_alias", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_clipboard", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_gizmos", + "bevy_gizmos_render", + "bevy_gltf", + "bevy_image", + "bevy_input", + "bevy_input_focus", + "bevy_light", + "bevy_log", + "bevy_material", + "bevy_math", + "bevy_mesh", + "bevy_pbr", + "bevy_platform", + "bevy_post_process", + "bevy_ptr", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_sprite", + "bevy_sprite_render", + "bevy_state", + "bevy_tasks", + "bevy_text", + "bevy_time", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bevy_winit", + "bevy_world_serialization", +] + +[[package]] +name = "bevy_light" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f91fb900c2412c5cb7ff5083d428d8361a1d39b605df86eb259aff82f7ac45" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_ecs", + "bevy_gizmos", + "bevy_image", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_transform", + "bevy_utils", + "half", + "smallvec", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_log" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24488594832237c1e3abc823642823fa057b810c3a21e14e65bdbd15d1579d5" +dependencies = [ + "android_log-sys", + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_utils", + "tracing", + "tracing-log", + "tracing-oslog", + "tracing-subscriber", + "tracing-wasm", +] + +[[package]] +name = "bevy_macro_utils" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7164fe229422295bba15f1885048f34f4660db069aacb895208f996b0c7784fb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "toml_edit", +] + +[[package]] +name = "bevy_material" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3efcdb3ba04a57f6e05548884745684a2051e9692398915f57c5c276306b7f10" +dependencies = [ + "bevy_asset", + "bevy_derive", + "bevy_ecs", + "bevy_material_macros", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_shader", + "bevy_utils", + "encase", + "smallvec", + "thiserror 2.0.20", + "tracing", + "variadics_please", + "wgpu-types", +] + +[[package]] +name = "bevy_material_macros" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854f95c8f3374ef6242790de50b5cb8f5a601c698772d1447bf856067b53ee64" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_math" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ead494dcfaa9b4594eed7f09a27d868b7cc7fc9ebd655c5643322778f4a8a2" +dependencies = [ + "approx", + "arrayvec", + "bevy_reflect", + "derive_more", + "glam 0.32.1", + "itertools 0.14.0", + "libm", + "rand 0.10.2", + "rand_distr", + "serde", + "thiserror 2.0.20", + "variadics_please", +] + +[[package]] +name = "bevy_mesh" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2adbf8e5a5817c345986756eb0377e18a510e1d7685ef62d6c7bafcd3bab85e" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_derive", + "bevy_ecs", + "bevy_encase_derive", + "bevy_math", + "bevy_mikktspace", + "bevy_platform", + "bevy_reflect", + "bevy_transform", + "bitflags 2.13.1", + "bytemuck", + "derive_more", + "encase", + "glam 0.32.1", + "half", + "hexasphere", + "thiserror 2.0.20", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_mikktspace" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff34eb29ff4b8a8688bc7299f14fb6b597461ca80fec03ed7d22939ab33e48f" + +[[package]] +name = "bevy_pbr" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244ae7d618b51a59c913c36b0564a295cd85ea91a5e777b542bb7bdb846a23d6" +dependencies = [ + "arrayvec", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_gltf", + "bevy_image", + "bevy_light", + "bevy_log", + "bevy_material", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_tasks", + "bevy_transform", + "bevy_utils", + "bitflags 2.13.1", + "bytemuck", + "derive_more", + "fixedbitset", + "indexmap", + "nonmax", + "offset-allocator", + "smallvec", + "static_assertions", + "thiserror 2.0.20", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_platform" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd610a417e7b4a3b9602cf11264d16cbbff8eabe689c5fed8be4e84c6a2680a3" +dependencies = [ + "critical-section", + "foldhash 0.2.0", + "futures-channel", + "futures-lite", + "hashbrown 0.16.1", + "js-sys", + "portable-atomic", + "portable-atomic-util", + "serde", + "spin", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", + "windows-sys 0.61.2", +] + +[[package]] +name = "bevy_post_process" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb9340107f7a4292b3b1d138cffb2700f31cef580f5777f2e83f4f00eae4dc1" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_utils", + "smallvec", + "thiserror 2.0.20", + "tracing", +] + +[[package]] +name = "bevy_ptr" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1974de4b140f102fded747229ff4a61921acec07f02f0d1dcdadfe9ce177d0ff" + +[[package]] +name = "bevy_reflect" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b76c3d056da3e299b84652ccf914c487b786857225d8ffbc8fc9509441a9e6b" +dependencies = [ + "assert_type_match", + "bevy_platform", + "bevy_ptr", + "bevy_reflect_derive", + "bevy_utils", + "derive_more", + "disqualified", + "downcast-rs 2.0.2", + "erased-serde", + "foldhash 0.2.0", + "glam 0.32.1", + "indexmap", + "inventory", + "petgraph", + "serde", + "smallvec", + "smol_str", + "thiserror 2.0.20", + "uuid", + "variadics_please", + "wgpu-types", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc5f820ee52afeca6d3bb83f15c1bf61ea6196c840a761a21a8bc266a3b166e" +dependencies = [ + "bevy_macro_utils", + "indexmap", + "proc-macro2", + "quote", + "syn 2.0.119", + "uuid", +] + +[[package]] +name = "bevy_render" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc86a32b2eef9859e1e7d323c6d1a9150fe77aa17c30fd05abebc12f645929dc" +dependencies = [ + "async-channel", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_encase_derive", + "bevy_image", + "bevy_log", + "bevy_material", + "bevy_material_macros", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render_macros", + "bevy_shader", + "bevy_tasks", + "bevy_time", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bitflags 2.13.1", + "bytemuck", + "derive_more", + "downcast-rs 2.0.2", + "encase", + "glam 0.32.1", + "image", + "indexmap", + "itertools 0.14.0", + "js-sys", + "naga 29.0.4", + "nonmax", + "offset-allocator", + "send_wrapper", + "smallvec", + "thiserror 2.0.20", + "variadics_please", + "wasm-bindgen", + "weak-table", + "web-sys", + "wgpu", + "wgpu-types", +] + +[[package]] +name = "bevy_render_macros" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6be6eba0ce9770ae4576693804d6c605f8afd9c4769787414254efcf92196a90" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_shader" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ba4fa5c5102a9ad1b0508c5bf8a6d02a22d5dc13c23ba908ee6d3d16a357cd" +dependencies = [ + "bevy_asset", + "bevy_platform", + "bevy_reflect", + "bevy_utils", + "naga 29.0.4", + "naga_oil", + "serde", + "thiserror 2.0.20", + "tracing", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "bevy_sprite" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93e631436ff9c08613843d46521ee60f2fcb2eb5f4754eec59591ca817a2ba0" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_reflect", + "bevy_text", + "bevy_transform", + "bevy_window", + "radsort", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_sprite_render" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a13c775880fe8fb3e7928921b2a1b549fd92c17571e7ca5e1250e53f0e56c" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_material", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_sprite", + "bevy_text", + "bevy_transform", + "bevy_utils", + "bitflags 2.13.1", + "bytemuck", + "derive_more", + "fixedbitset", + "nonmax", + "tracing", +] + +[[package]] +name = "bevy_state" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f69ce6a0066eb4626165e2f8c85d3cf4808cfe4cb2027df8840d5cbfe8a3428d" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_state_macros", + "bevy_utils", + "log", + "variadics_please", +] + +[[package]] +name = "bevy_state_macros" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36dd49049023a0abbd0bd1e2b99580ed83a18c5351c64caba4c06e3b584cf9c" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_tasks" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f787d4ba7237b64b30bd2d690a8cbae1ba775e0eec326711c446654cb720e7c" +dependencies = [ + "async-channel", + "async-executor", + "async-task", + "atomic-waker", + "bevy_platform", + "concurrent-queue", + "crossbeam-queue", + "derive_more", + "futures-lite", + "heapless 0.9.3", + "web-task", +] + +[[package]] +name = "bevy_text" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b275f50fb01b1e2d411326a7750ecc2fe629c48b89997cf2a3c93fe786e076" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_clipboard", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_log", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_utils", + "parley", + "serde", + "smallvec", + "smol_str", + "swash", + "sys-locale", + "thiserror 2.0.20", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_time" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6896d83d7ed32a2f89da127b7aeddf01eb702e603897cf5ddac231480a1870e" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "crossbeam-channel", + "log", + "serde", +] + +[[package]] +name = "bevy_transform" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d94cc6065320ebeb58ae88a0863a4c4c2a642c6db698e283eb50be01a834ca5a" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_math", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "derive_more", + "serde", + "thiserror 2.0.20", +] + +[[package]] +name = "bevy_utils" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de51fa7ea2e95d4c2d1278c5629285d59eec951cc60c992964c03f30910ef5a1" +dependencies = [ + "async-channel", + "bevy_platform", + "disqualified", + "indexmap", + "thread_local", +] + +[[package]] +name = "bevy_window" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38373ee93a9248c14afe41d2025f4aa5e68b004b06ae7702816b180e72b39ea3" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "log", + "raw-window-handle", + "serde", +] + +[[package]] +name = "bevy_winit" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9c615f4194b1e00ba536892a3d3d3300c2a1f8e66112fdebd7077bf2744c9" +dependencies = [ + "accesskit", + "accesskit_winit", + "approx", + "bevy_a11y", + "bevy_android", + "bevy_app", + "bevy_derive", + "bevy_ecs", + "bevy_input", + "bevy_input_focus", + "bevy_log", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_window", + "js-sys", + "tracing", + "wasm-bindgen", + "web-sys", + "winit", +] + +[[package]] +name = "bevy_world_serialization" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36855264b9b5d229bde74f01730eb6251c72dc052404ae144a9295cc0abdefc1" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_derive", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_transform", + "bevy_utils", + "derive_more", + "ron", + "serde", + "thiserror 2.0.20", + "uuid", +] + [[package]] name = "bindgen" version = "0.72.1" @@ -292,7 +1739,7 @@ dependencies = [ "bitflags 2.13.1", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -314,9 +1761,9 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.10.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" dependencies = [ "bit-vec 0.9.1", ] @@ -347,6 +1794,23 @@ name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "bytemuck", + "serde_core", +] + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] [[package]] name = "block-buffer" @@ -366,13 +1830,35 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + [[package]] name = "block2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2", + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", ] [[package]] @@ -419,6 +1905,32 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + [[package]] name = "cc" version = "1.4.2" @@ -567,6 +2079,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "cmake" version = "0.1.58" @@ -586,12 +2107,25 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width 0.2.2", +] + [[package]] name = "codespan-reporting" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ + "serde", + "termcolor", "unicode-width 0.2.2", ] @@ -617,6 +2151,32 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", + "portable-atomic", +] + +[[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 = "const-fnv1a-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" + [[package]] name = "const-oid" version = "0.9.6" @@ -629,6 +2189,36 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "const_panic" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9603f79528ece8163c496f8932121cb36cfe46259e9c907bb3d8205139d7caa3" +dependencies = [ + "typewit", +] + +[[package]] +name = "const_soft_float" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ca1caa64ef4ed453e68bb3db612e51cf1b2f5b871337f0fcab1c8f87cc3dff" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "constgebra" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1aaf9b65849a68662ac6c0810c8893a765c960b907dd7cfab9c4a50bf764fbc" +dependencies = [ + "const_soft_float", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -638,6 +2228,16 @@ 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" version = "0.10.1" @@ -654,6 +2254,39 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "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 0.9.4", + "libc", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "coreaudio-rs" version = "0.14.2" @@ -675,7 +2308,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f77b11176c37874be37e8d691c946e31b2b8c357abce9526f6a99eb469e1028" dependencies = [ "alsa", - "block2", + "block2 0.6.2", "coreaudio-rs", "dasp_sample", "jni", @@ -686,13 +2319,13 @@ dependencies = [ "ndk-context", "num-derive", "num-traits", - "objc2", + "objc2 0.6.4", "objc2-audio-toolbox", "objc2-avf-audio", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", "web-sys", "windows", "windows-core", @@ -740,6 +2373,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -759,6 +2407,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.22" @@ -777,7 +2434,7 @@ dependencies = [ "document-features", "mio", "parking_lot", - "rustix", + "rustix 1.1.4", "signal-hook", "signal-hook-mio", "winapi", @@ -839,6 +2496,23 @@ dependencies = [ "cipher", ] +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix 0.31.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -944,6 +2618,7 @@ dependencies = [ "quote", "rustc_version", "syn 2.0.119", + "unicode-xid", ] [[package]] @@ -1027,6 +2702,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "dispatch2" version = "0.3.1" @@ -1034,7 +2715,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.13.1", - "objc2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", ] [[package]] @@ -1048,6 +2731,12 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "disqualified" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd" + [[package]] name = "dlib" version = "0.5.3" @@ -1066,6 +2755,24 @@ dependencies = [ "litrs", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + [[package]] name = "dunce" version = "1.0.5" @@ -1122,12 +2829,54 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encase" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1acbc46b960480c4959213714511e22f9a2e0d361520693bcf5057b5924f53e" +dependencies = [ + "const_panic", + "encase_derive", + "thiserror 2.0.20", +] + +[[package]] +name = "encase_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e30d38c47336a0afbdfde297b442944041cf6dccbb1a79eceea16abfb27a92a" +dependencies = [ + "encase_derive_impl", +] + +[[package]] +name = "encase_derive_impl" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495d24abe406f090f2ba8fc362ecaa8a001e192522aa75eae7b144163c3235ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -1138,6 +2887,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1203,6 +2987,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flagset" version = "0.4.7" @@ -1225,12 +3015,77 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37007738a80ea34f969af54a3390dd72cacdef654974cfd449c9f6f72dbaac10" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64eb721ca85a34323425f4041adc5d82704d3782d5f8f03793bc012419dce23" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontique" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c20b425addb8661e97fe1d51c4d8bcec3ec29ed6ad0db983976a7521276b8f7" +dependencies = [ + "hashbrown 0.17.1", + "linebender_resource_handle", + "memmap2", + "parlance", + "read-fonts 0.39.2", + "smallvec", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[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.2" @@ -1267,6 +3122,19 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.33" @@ -1362,6 +3230,16 @@ dependencies = [ "libm", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1421,23 +3299,25 @@ dependencies = [ "weezl", ] -[[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 = "glam" version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" +[[package]] +name = "glam" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" +dependencies = [ + "bytemuck", + "encase", + "libm", + "rand 0.10.2", + "serde_core", +] + [[package]] name = "glob" version = "0.3.4" @@ -1451,24 +3331,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" [[package]] -name = "glow" -version = "0.17.0" +name = "gltf" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7" dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", + "byteorder", + "gltf-json", + "lazy_static", + "serde_json", ] [[package]] -name = "glutin_wgl_sys" -version = "0.6.1" +name = "gltf-derive" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +checksum = "14070e711538afba5d6c807edb74bcb84e5dbb9211a3bf5dea0dfab5b24f4c51" dependencies = [ - "gl_generator", + "inflections", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gltf-json" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6176f9d60a7eab0a877e8e96548605dedbde9190a7ae1e80bbcc1c9af03ab14" +dependencies = [ + "gltf-derive", + "serde", + "serde_derive", + "serde_json", ] [[package]] @@ -1496,6 +3391,26 @@ dependencies = [ "windows", ] +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.1", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "group" version = "0.13.0" @@ -1507,18 +3422,42 @@ dependencies = [ "subtle", ] +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + [[package]] name = "half" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ + "bytemuck", "cfg-if", "crunchy", "num-traits", "zerocopy", ] +[[package]] +name = "harfrust" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551ed25397e4b444e89686602877d5cf3a7f6e3d548dcac37a8357d1e195f4df" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "core_maths", + "read-fonts 0.39.2", + "smallvec", +] + [[package]] name = "hash32" version = "0.3.1" @@ -1528,6 +3467,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -1536,7 +3484,9 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -1545,7 +3495,7 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1567,12 +3517,40 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "portable-atomic", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hexasphere" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177ea6330876de4ef08e184f827b98acc037614a4ce409902c5b0d53a9c2e951" +dependencies = [ + "constgebra", + "glam 0.32.1", + "tinyvec", +] + [[package]] name = "hexf-parse" version = "0.2.1" @@ -1770,6 +3748,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_locale" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + [[package]] name = "icu_locale_core" version = "2.2.0" @@ -1778,11 +3771,18 @@ checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" + [[package]] name = "icu_normalizer" version = "2.2.0" @@ -1831,6 +3831,8 @@ checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -1838,6 +3840,27 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_segmenter" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0794db0b1a86193ac9c48768d0e6c52c54448e0870ad87907d456ee0dac964" +dependencies = [ + "icu_collections", + "icu_locale", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a2c462a4d927d512f5f882a033ddd62f33a05bb9f230d98f736ac3dc85938f" + [[package]] name = "idna" version = "1.1.0" @@ -1859,6 +3882,19 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", +] + [[package]] name = "image-webp" version = "0.2.4" @@ -1877,8 +3913,16 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", + "serde", + "serde_core", ] +[[package]] +name = "inflections" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" + [[package]] name = "inout" version = "0.1.4" @@ -1888,6 +3932,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.12.1" @@ -1922,6 +3975,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -2078,22 +4140,14 @@ dependencies = [ ] [[package]] -name = "khronos-egl" -version = "6.0.0" +name = "ktx2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +checksum = "1d50be8a24c37debdf3170be6ca396d5b5b165f37f94837891adac735ca416b8" dependencies = [ - "libc", - "libloading", - "pkg-config", + "bitflags 2.13.1", ] -[[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.5.0" @@ -2128,7 +4182,10 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" dependencies = [ + "bitflags 2.13.1", "libc", + "plain", + "redox_syscall 0.9.2", ] [[package]] @@ -2354,6 +4411,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2411,6 +4480,15 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "md-5" version = "0.10.6" @@ -2427,6 +4505,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -2576,7 +4663,7 @@ dependencies = [ name = "metacrate-rendering-wgpu" version = "0.0.1" dependencies = [ - "wgpu", + "bevy", ] [[package]] @@ -2640,24 +4727,25 @@ dependencies = [ [[package]] name = "naga" -version = "30.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" +checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" dependencies = [ "arrayvec", - "bit-set 0.10.0", + "bit-set 0.9.1", "bitflags 2.13.1", "cfg-if", "cfg_aliases 0.2.2", "codespan-reporting 0.13.1", "half", - "hashbrown 0.17.1", + "hashbrown 0.16.1", + "hexf-parse", "indexmap", "libm", "log", - "naga-types", "num-traits", "once_cell", + "pp-rs", "rustc-hash 1.1.0", "spirv 0.4.0+sdk-1.4.341.0", "thiserror 2.0.20", @@ -2665,15 +4753,20 @@ dependencies = [ ] [[package]] -name = "naga-types" -version = "30.0.1" +name = "naga_oil" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" +checksum = "319f03062940ed85c03da020042618356bb8ca5263020322930883b05e30208e" dependencies = [ - "hashbrown 0.17.1", + "codespan-reporting 0.12.0", + "data-encoding", "indexmap", + "naga 29.0.4", + "regex", "rustc-hash 1.1.0", "thiserror 2.0.20", + "tracing", + "unicode-ident", ] [[package]] @@ -2687,6 +4780,7 @@ dependencies = [ "log", "ndk-sys", "num_enum", + "raw-window-handle", "thiserror 1.0.69", ] @@ -2749,6 +4843,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.8" @@ -2817,6 +4926,22 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + [[package]] name = "objc2" version = "0.6.4" @@ -2826,6 +4951,34 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-audio-toolbox" version = "0.3.2" @@ -2834,11 +4987,11 @@ checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ "bitflags 2.13.1", "libc", - "objc2", + "objc2 0.6.4", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", ] [[package]] @@ -2848,8 +5001,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" dependencies = [ "bitflags 2.13.1", - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2859,8 +5025,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ "bitflags 2.13.1", - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2870,10 +5047,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" dependencies = [ "dispatch2", - "objc2", + "objc2 0.6.4", "objc2-core-audio-types", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", ] [[package]] @@ -2883,7 +5060,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ "bitflags 2.13.1", - "objc2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -2892,8 +5081,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -2903,10 +5092,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.6.2", "dispatch2", "libc", - "objc2", + "objc2 0.6.4", ] [[package]] @@ -2917,19 +5106,43 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.13.1", "dispatch2", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + [[package]] name = "objc2-core-image" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", ] [[package]] @@ -2938,8 +5151,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -2949,7 +5162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.13.1", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", ] @@ -2960,6 +5173,19 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + [[package]] name = "objc2-foundation" version = "0.3.2" @@ -2967,9 +5193,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.1", - "block2", + "block2 0.6.2", "libc", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -2980,10 +5206,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.13.1", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", ] +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-metal" version = "0.3.2" @@ -2991,9 +5241,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ "bitflags 2.13.1", - "block2", - "objc2", - "objc2-foundation", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", ] [[package]] @@ -3003,11 +5266,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.13.1", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-metal", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit 0.2.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-core-location 0.2.2", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications 0.2.2", ] [[package]] @@ -3017,18 +5310,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.13.1", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", "objc2-core-foundation", "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", + "objc2-core-image 0.3.2", + "objc2-core-location 0.3.2", "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-user-notifications 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3037,8 +5354,18 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "offset-allocator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e234d535da3521eb95106f40f0b73483d80bfb3aacf27c40d7e2b72f1a3e00a2" +dependencies = [ + "log", + "nonmax", ] [[package]] @@ -3089,6 +5416,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + [[package]] name = "ordered-float" version = "5.5.0" @@ -3107,12 +5444,31 @@ dependencies = [ "android_system_properties", "log", "nix 0.31.3", - "objc2", - "objc2-foundation", - "objc2-ui-kit", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", "windows-sys 0.61.2", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + [[package]] name = "p256" version = "0.13.2" @@ -3137,6 +5493,12 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -3155,11 +5517,44 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "parlance" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6937eda350acc1a5d05872c3cbf99fe78619c269096e2be3d4a350058639d5" + +[[package]] +name = "parley" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fad031076f48f0d4d85ce1aea9b94b4e715a4d636a030a123038f8f5b5e4343" +dependencies = [ + "fontique", + "harfrust", + "hashbrown 0.17.1", + "icu_normalizer", + "icu_properties", + "icu_segmenter", + "linebender_resource_handle", + "parlance", + "parley_data", + "skrifa 0.42.1", +] + +[[package]] +name = "parley_data" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ab9ace3fad1b9ed603ddac5b595e69931fc50263d7e04e4055015b77b02da5" +dependencies = [ + "icu_properties", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3175,12 +5570,56 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", + "serde", + "serde_derive", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -3216,6 +5655,33 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -3260,6 +5726,8 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -3269,6 +5737,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "pp-rs" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb458bb7f6e250e6eb79d5026badc10a3ebb8f9a15d1fff0f13d17c71f4d6dee" +dependencies = [ + "unicode-xid", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3339,6 +5816,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.11" @@ -3417,6 +5903,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radsort" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "019b4b213425016d7d84a153c4c73afb0946fbb4840e4eece7ba8848b9d6da22" + [[package]] name = "rand" version = "0.9.5" @@ -3472,6 +5964,16 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_distr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" +dependencies = [ + "num-traits", + "rand 0.10.2", +] + [[package]] name = "rand_pcg" version = "0.10.2" @@ -3499,10 +6001,10 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" dependencies = [ - "objc2", + "objc2 0.6.4", "objc2-core-foundation", - "objc2-foundation", - "objc2-quartz-core", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", ] [[package]] @@ -3538,6 +6040,42 @@ dependencies = [ "yasna", ] +[[package]] +name = "read-fonts" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +dependencies = [ + "bytemuck", + "font-types 0.11.3", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.4", + "once_cell", +] + +[[package]] +name = "rectangle-pack" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb" + +[[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_syscall" version = "0.5.18" @@ -3547,6 +6085,15 @@ dependencies = [ "bitflags 2.13.1", ] +[[package]] +name = "redox_syscall" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c93da5bb2c5d4e6c0ef7abeead62c89169a0a4882bfb83ac892f2423aea2fe" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -3702,6 +6249,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" +[[package]] +name = "ron" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" +dependencies = [ + "bitflags 2.13.1", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + [[package]] name = "roxmltree" version = "0.21.1" @@ -3727,7 +6288,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" dependencies = [ - "heapless", + "heapless 0.8.0", "num-traits", "smallvec", ] @@ -3777,6 +6338,19 @@ dependencies = [ "nom 7.1.3", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -3786,7 +6360,7 @@ dependencies = [ "bitflags 2.13.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -3833,7 +6407,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", @@ -3872,6 +6446,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ruzstd" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" +dependencies = [ + "twox-hash", +] + [[package]] name = "ryu" version = "1.0.23" @@ -3896,6 +6479,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[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" @@ -3922,6 +6511,19 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + [[package]] name = "sctp-proto" version = "0.10.3" @@ -3958,7 +6560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.13.1", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3980,6 +6582,12 @@ version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.229" @@ -4090,6 +6698,15 @@ dependencies = [ "digest 0.11.3", ] +[[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 = "shlex" version = "1.3.0" @@ -4204,7 +6821,7 @@ dependencies = [ "jpeg-decoder", "jpeg-encoder", "parking_lot", - "png", + "png 0.17.16", "skia-rs-core", "skia-rs-paint", "thiserror 2.0.20", @@ -4219,7 +6836,7 @@ dependencies = [ "bitflags 2.13.1", "bytemuck", "getrandom 0.2.17", - "glam", + "glam 0.29.3", "smallvec", "thiserror 2.0.20", ] @@ -4272,6 +6889,26 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts 0.39.2", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + [[package]] name = "slab" version = "0.4.12" @@ -4293,6 +6930,40 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.1", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.6.5" @@ -4315,6 +6986,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +dependencies = [ + "portable-atomic", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -4361,6 +7041,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stackfuture" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115beb9c69db2393ff10b75a1b8587a51716e5551d015001e55320ed279d32f9" +dependencies = [ + "const_panic", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -4426,6 +7115,12 @@ dependencies = [ "time", ] +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "strsim" version = "0.11.1" @@ -4459,6 +7154,23 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa 0.44.0", + "yazi", + "zeno", +] + [[package]] name = "syn" version = "2.0.119" @@ -4501,6 +7213,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "tar" version = "0.4.46" @@ -4561,6 +7282,15 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.55" @@ -4591,6 +7321,31 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -4598,6 +7353,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -4792,6 +7548,70 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 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-oslog" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76902d2a8d5f9f55a81155c08971734071968c90f2d9bfe645fe700579b2950" +dependencies = [ + "cc", + "cfg-if", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom 8.0.0", + "petgraph", ] [[package]] @@ -4800,12 +7620,36 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "twox-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" + [[package]] name = "unicode-general-category" version = "1.1.0" @@ -4911,6 +7755,7 @@ checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "getrandom 0.4.3", "js-sys", + "serde_core", "uuid-rng-internal", "wasm-bindgen", ] @@ -4924,6 +7769,23 @@ dependencies = [ "getrandom 0.4.3", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "variadics_please" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -5065,6 +7927,103 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs 1.2.1", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.1", + "rustix 1.1.4", + "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.13.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + [[package]] name = "wayland-sys" version = "0.31.11" @@ -5073,10 +8032,15 @@ checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "dlib", "log", - "once_cell", "pkg-config", ] +[[package]] +name = "weak-table" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" + [[package]] name = "web-sys" version = "0.3.104" @@ -5087,6 +8051,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-task" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdc136a53ccd64a1211f107ccc34404769fbcc0f165f1afa065f5d88ab93538" +dependencies = [ + "async-task", + "cfg-if", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -5123,9 +8099,9 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "wgpu" -version = "30.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" dependencies = [ "arrayvec", "bitflags 2.13.1", @@ -5133,17 +8109,14 @@ dependencies = [ "cfg-if", "cfg_aliases 0.2.2", "document-features", - "hashbrown 0.17.1", - "js-sys", + "hashbrown 0.16.1", "log", + "naga 29.0.4", "portable-atomic", "profiling", "raw-window-handle", "smallvec", "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", "wgpu-core", "wgpu-hal", "wgpu-types", @@ -5151,22 +8124,21 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "30.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" +checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" dependencies = [ "arrayvec", - "bit-set 0.10.0", + "bit-set 0.9.1", "bit-vec 0.9.1", "bitflags 2.13.1", "bytemuck", "cfg_aliases 0.2.2", "document-features", - "hashbrown 0.17.1", + "hashbrown 0.16.1", "indexmap", "log", - "naga 30.0.1", - "naga-types", + "naga 29.0.4", "once_cell", "parking_lot", "portable-atomic", @@ -5176,7 +8148,6 @@ dependencies = [ "smallvec", "thiserror 2.0.20", "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-naga-bridge", @@ -5185,64 +8156,49 @@ dependencies = [ [[package]] name = "wgpu-core-deps-apple" -version = "30.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-emscripten" -version = "30.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86" +checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "30.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" +checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "30.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" +checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set 0.10.0", + "bit-set 0.9.1", "bitflags 2.13.1", - "block2", + "block2 0.6.2", "bytemuck", "cfg-if", "cfg_aliases 0.2.2", - "glow", - "glutin_wgl_sys", "gpu-allocator", - "hashbrown 0.17.1", - "js-sys", - "khronos-egl", + "gpu-descriptor", + "hashbrown 0.16.1", "libc", "libloading", "log", - "naga 30.0.1", - "naga-types", - "ndk-sys", - "objc2", + "naga 29.0.4", + "objc2 0.6.4", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-metal", - "objc2-quartz-core", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", "once_cell", "ordered-float", "parking_lot", @@ -5254,41 +8210,35 @@ dependencies = [ "raw-window-metal", "renderdoc-sys", "smallvec", - "static_assertions", "thiserror 2.0.20", - "wasm-bindgen", - "wayland-sys", - "web-sys", "wgpu-naga-bridge", "wgpu-types", "windows", "windows-core", - "windows-result", ] [[package]] name = "wgpu-naga-bridge" -version = "30.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" +checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" dependencies = [ - "naga 30.0.1", + "naga 29.0.4", "wgpu-types", ] [[package]] name = "wgpu-types" -version = "30.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" dependencies = [ "bitflags 2.13.1", "bytemuck", "js-sys", "log", - "naga-types", "raw-window-handle", - "static_assertions", + "serde", "web-sys", ] @@ -5521,6 +8471,58 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.1", + "block2 0.5.1", + "bytemuck", + "calloop", + "cfg_aliases 0.2.2", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + [[package]] name = "winnow" version = "1.0.4" @@ -5536,12 +8538,62 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.4", + "thiserror 2.0.20", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[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.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -5590,14 +8642,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] -name = "xml-rs" -version = "0.8.29" +name = "xcursor" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "yasna" @@ -5609,6 +8680,12 @@ dependencies = [ "time", ] +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + [[package]] name = "yoke" version = "0.8.3" @@ -5632,6 +8709,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + [[package]] name = "zerocopy" version = "0.8.56" @@ -5702,6 +8785,7 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] @@ -5710,6 +8794,7 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", diff --git a/config/grid-agent.example.yml b/config/grid-agent.example.yml index f665723..5a9a521 100644 --- a/config/grid-agent.example.yml +++ b/config/grid-agent.example.yml @@ -35,6 +35,9 @@ behavior: max_walk_distance_meters: 8 max_attention_distance_meters: 96 idle_look_enabled: true + +vision: + max_distance_meters: 64 reconnect: initial_delay_milliseconds: 1000 maximum_delay_seconds: 60 diff --git a/crates/metacrate-grid-agent/src/backend.rs b/crates/metacrate-grid-agent/src/backend.rs index 5b95df5..7df8910 100644 --- a/crates/metacrate-grid-agent/src/backend.rs +++ b/crates/metacrate-grid-agent/src/backend.rs @@ -175,6 +175,7 @@ impl LibremetaverseClientOwner { let network = client.network(); let _grid = client.grid(); let _terrain = client.terrain(); + let _objects = client.objects(); let agent = Arc::new( libremetaverse::AgentManager::new(Some(Arc::new(client.clone()))).map_err(|_| { BackendError::Configuration { diff --git a/crates/metacrate-grid-agent/src/config.rs b/crates/metacrate-grid-agent/src/config.rs index fbf8f4f..6f35cbd 100644 --- a/crates/metacrate-grid-agent/src/config.rs +++ b/crates/metacrate-grid-agent/src/config.rs @@ -318,6 +318,7 @@ pub struct AgentConfig { pub limits: Limits, pub storage_path: PathBuf, pub behavior: BehaviorSettings, + pub vision: crate::vision::VisionLimits, pub reconnect: crate::session::ReconnectPolicy, pub conversation: ConversationSettings, pub interaction: crate::interaction::InteractionSettings, @@ -362,6 +363,14 @@ impl AgentConfig { if !self.behavior.is_valid() { return Err(ConfigError::InvalidBehavior); } + if !self.vision.valid() { + return Err(ConfigError::UnsafeLimit { + field: "vision.max_distance_meters", + value: self.vision.max_distance_meters as usize, + minimum: 1, + maximum: 1_024, + }); + } if (!self.control.listen.ip().is_loopback() && self.control.remote_tls.is_none()) || !self.control.limits.is_valid() { @@ -1031,6 +1040,7 @@ struct FileConfig { limits: RawLimits, storage_path: Option, behavior: RawBehavior, + vision: RawVision, reconnect: RawReconnect, conversation: RawConversation, interaction: RawInteraction, @@ -1115,6 +1125,12 @@ struct RawBehavior { idle_look_enabled: Option, } +#[derive(Clone, Default, Deserialize)] +#[serde(default, deny_unknown_fields)] +struct RawVision { + max_distance_meters: Option, +} + #[derive(Clone, Default, Deserialize)] #[serde(default, deny_unknown_fields)] struct RawReconnect { @@ -1567,6 +1583,10 @@ fn resolve( max_attention_distance_meters: raw.behavior.max_attention_distance_meters.unwrap_or(96), idle_look_enabled: raw.behavior.idle_look_enabled.unwrap_or(true), }, + vision: crate::vision::VisionLimits { + max_distance_meters: raw.vision.max_distance_meters.unwrap_or(64), + ..crate::vision::VisionLimits::default() + }, reconnect, conversation, interaction, @@ -1964,6 +1984,35 @@ mod tests { assert!(paths.data_directory.ends_with("grid-agent")); } + #[test] + fn vision_distance_is_configurable_and_bounded() { + let configured = temporary_file( + "vision-distance.yml", + "llm:\n endpoint_url: https://llm.invalid/chat\n api_key: placeholder\nvision:\n max_distance_meters: 96\n", + ); + let config = ConfigLoader::new() + .with_file(&configured) + .with_environment(MapEnvironment::default()) + .load() + .expect("configured vision distance"); + assert_eq!(config.vision.max_distance_meters, 96); + + let unsafe_distance = temporary_file( + "unsafe-vision-distance.yml", + "llm:\n endpoint_url: https://llm.invalid/chat\n api_key: placeholder\nvision:\n max_distance_meters: 0\n", + ); + assert!(matches!( + ConfigLoader::new() + .with_file(&unsafe_distance) + .with_environment(MapEnvironment::default()) + .load(), + Err(ConfigError::UnsafeLimit { + field: "vision.max_distance_meters", + .. + }) + )); + } + #[cfg(unix)] #[test] fn group_readable_secret_files_fail_closed() { diff --git a/crates/metacrate-grid-agent/src/interaction.rs b/crates/metacrate-grid-agent/src/interaction.rs index cde2dd9..6ff2657 100644 --- a/crates/metacrate-grid-agent/src/interaction.rs +++ b/crates/metacrate-grid-agent/src/interaction.rs @@ -436,6 +436,7 @@ pub enum InteractionObservation { #[derive(Clone, Debug, Eq, PartialEq)] pub enum InteractionError { UnsafeLimits, + Persistence(String), MalformedInbound, QueueClosed, ObservationClosed, @@ -449,6 +450,7 @@ impl fmt::Display for InteractionError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(match self { Self::UnsafeLimits => "unsafe interaction limits", + Self::Persistence(_) => "interaction persistence failed", Self::MalformedInbound => "malformed inbound interaction", Self::QueueClosed => "interaction input queue is closed", Self::ObservationClosed => "interaction observation queue is closed", @@ -456,7 +458,11 @@ impl fmt::Display for InteractionError { Self::TaskPanicked => "interaction task panicked", Self::Conversation => "conversation memory rejected interaction data", Self::Boundary(_) => "interaction boundary rejected data", - }) + })?; + if let Self::Persistence(detail) = self { + write!(formatter, ": {detail}")?; + } + Ok(()) } } @@ -1986,6 +1992,8 @@ impl PolicyLlmResponder { .validate() .map_err(|_| InteractionError::UnsafeLimits)?; let active_tools = Arc::new(Mutex::new(BTreeMap::new())); + std::fs::create_dir_all(&storage_path) + .map_err(|error| InteractionError::Persistence(error.to_string()))?; let mut builder = mentra::Runtime::builder() .with_runtime_identifier(MENTRA_RUNTIME_IDENTIFIER) .with_store(mentra::runtime::HybridRuntimeStore::new( @@ -2000,10 +2008,10 @@ impl PolicyLlmResponder { } let runtime = builder .build() - .map_err(|_| InteractionError::UnsafeLimits)?; + .map_err(|error| InteractionError::Persistence(error.to_string()))?; let agents = runtime .resume(MENTRA_RUNTIME_IDENTIFIER) - .map_err(|_| InteractionError::UnsafeLimits)? + .map_err(|error| InteractionError::Persistence(error.to_string()))? .into_iter() .filter(|agent| agent.name().starts_with(MENTRA_AGENT_PREFIX)) .map(|agent| { diff --git a/crates/metacrate-grid-agent/src/main.rs b/crates/metacrate-grid-agent/src/main.rs index a7904fc..bb9098f 100644 --- a/crates/metacrate-grid-agent/src/main.rs +++ b/crates/metacrate-grid-agent/src/main.rs @@ -617,7 +617,7 @@ fn start_live_interactions( LibremetaverseScriptInventory, LlmClient, MemoryPolicyAudit, Observability, ObservabilityLimits, PerceptionBackend, PolicyAuditSink, PolicyGateway, PolicyLimits, PolicyLlmResponder, ScriptDeliveryBackend, ScriptDeliverySettings, SystemRoamingRandom, - ToolLoopLimits, UnifiedPolicyAudit, VisionAugmentedResponder, VisionLimits, VisionService, + ToolLoopLimits, UnifiedPolicyAudit, VisionAugmentedResponder, VisionService, behavior_policy_tools, build_policy_tools, landmark_policy_tools, perception_policy_tools, script_delivery_policy_tool, }; @@ -739,7 +739,7 @@ fn start_live_interactions( now, config.storage_path.join("mentra"), )?); - let vision_limits = VisionLimits::default(); + let vision_limits = config.vision; let vision = Arc::new( VisionService::new( Arc::new(LibremetaverseSceneSource::new(owner, vision_limits)), diff --git a/crates/metacrate-grid-agent/src/vision.rs b/crates/metacrate-grid-agent/src/vision.rs index 059b85c..65f4510 100644 --- a/crates/metacrate-grid-agent/src/vision.rs +++ b/crates/metacrate-grid-agent/src/vision.rs @@ -23,7 +23,15 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tokio::sync::Semaphore; -pub use metacrate_rendering_wgpu::{Camera as CameraPose, Triangle as SceneTriangle}; +pub use metacrate_rendering_wgpu::{ + Camera as CameraPose, Renderable as SceneRenderable, Texture as SceneTexture, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct SceneTriangle { + pub vertices: [[f32; 3]; 3], + pub colors_srgb: [[u8; 4]; 3], +} #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] @@ -40,6 +48,7 @@ pub struct SceneEntity { pub kind: SceneEntityKind, pub display_name: String, pub triangles: Vec, + pub renderables: Vec, pub texture_available: bool, } @@ -59,6 +68,7 @@ pub struct SceneSnapshot { pub region_name: String, pub camera: CameraPose, pub entities: Vec, + pub textures: Vec, pub completeness: SnapshotCompleteness, /// Work already performed by the scene source; validated again before rendering. pub texture_fetches: usize, @@ -70,6 +80,7 @@ pub struct SceneSnapshot { pub struct VisionLimits { pub width: u32, pub height: u32, + pub max_distance_meters: u32, pub max_entities: usize, pub max_triangles: usize, pub max_texture_fetches: usize, @@ -86,32 +97,34 @@ impl Default for VisionLimits { Self { width: 640, height: 360, - max_entities: 256, - max_triangles: 32_768, - max_texture_fetches: 128, - max_texture_bytes: 64 * 1024 * 1024, - max_decode_pixels: 16_777_216, + max_distance_meters: 64, + max_entities: 512, + max_triangles: 131_072, + max_texture_fetches: 1_024, + max_texture_bytes: 512 * 1024 * 1024, + max_decode_pixels: 134_217_728, max_jpeg_bytes: 512 * 1024, max_concurrent_captures: 1, - capture_timeout: Duration::from_secs(30), + capture_timeout: Duration::from_mins(2), minimum_interval: Duration::from_secs(5), } } } impl VisionLimits { - fn valid(self) -> bool { + pub(crate) fn valid(self) -> bool { (64..=640).contains(&self.width) && (64..=480).contains(&self.height) && self.width as usize * self.height as usize <= 307_200 - && (1..=512).contains(&self.max_entities) - && (1..=65_536).contains(&self.max_triangles) - && self.max_texture_fetches <= 128 - && self.max_texture_bytes <= 64 * 1024 * 1024 - && self.max_decode_pixels <= 16_777_216 + && (1..=1_024).contains(&self.max_distance_meters) + && (1..=1_024).contains(&self.max_entities) + && (1..=262_144).contains(&self.max_triangles) + && self.max_texture_fetches <= 1_024 + && self.max_texture_bytes <= 512 * 1024 * 1024 + && self.max_decode_pixels <= 134_217_728 && (4_096..=1024 * 1024).contains(&self.max_jpeg_bytes) && (1..=4).contains(&self.max_concurrent_captures) && !self.capture_timeout.is_zero() - && self.capture_timeout <= Duration::from_secs(30) + && self.capture_timeout <= Duration::from_mins(5) && self.minimum_interval <= Duration::from_mins(10) } } @@ -384,29 +397,30 @@ impl VisionService { let renderer = self .gpu .get_or_init(|| async { - metacrate_rendering_wgpu::Renderer::new() + tokio::task::spawn_blocking(bevy_renderer_new) .await .ok() + .and_then(Result::ok) .map(Arc::new) }) .await .clone(); if let Some(renderer) = renderer { - let mut triangles = scene + let mut renderables = scene .entities .iter() .flat_map(|entity| { entity - .triangles + .renderables .iter() .cloned() - .map(move |triangle| (entity.id.to_string(), triangle)) + .map(move |renderable| (entity.id.to_string(), renderable)) }) .collect::>(); - triangles.sort_by(|left, right| left.0.cmp(&right.0)); - let triangles = triangles + renderables.sort_by(|left, right| left.0.cmp(&right.0)); + let renderables = renderables .into_iter() - .map(|(_, triangle)| triangle) + .map(|(_, renderable)| renderable) .collect::>(); let gpu_scene = scene.clone(); let limits = self.limits; @@ -414,12 +428,15 @@ impl VisionService { renderer .render( gpu_scene.camera, - &triangles, + &renderables, + &gpu_scene.textures, environment_color(&gpu_scene), metacrate_rendering_wgpu::RenderLimits { width: limits.width, height: limits.height, max_triangles: limits.max_triangles, + max_texture_bytes: limits.max_texture_bytes, + max_texture_pixels: limits.max_decode_pixels, far_distance: 512, }, ) @@ -447,6 +464,12 @@ impl VisionService { } } +#[cfg(feature = "live-grid")] +fn bevy_renderer_new() +-> Result { + metacrate_rendering_wgpu::Renderer::new_blocking() +} + /// Type-erased runtime control surface. It deliberately exposes metadata and /// hashes, never captured image bytes. pub trait VisionControl: Send + Sync { @@ -921,6 +944,40 @@ pub struct LibremetaverseSceneSource { client: libremetaverse::GridClient, agent: Arc, limits: VisionLimits, + cache: Arc>, +} + +#[cfg(feature = "live-grid")] +#[derive(Default)] +struct SceneCache { + region_id: Option, + textures: std::collections::BTreeMap, + texture_pixels: usize, + geometry: std::collections::HashMap, + access: u64, +} + +#[cfg(feature = "live-grid")] +struct CachedTexture { + texture: Arc, + pixels: usize, + last_used: u64, +} + +#[cfg(feature = "live-grid")] +#[derive(Clone, PartialEq)] +struct GeometrySignature { + construction: libremetaverse::PrimitiveConstructionData, + sculpt: Option, + textures: Vec, + detail: libremetaverse::rendering::DetailLevel, +} + +#[cfg(feature = "live-grid")] +struct CachedGeometry { + signature: GeometrySignature, + mesh: Arc, + last_used: u64, } #[cfg(feature = "live-grid")] @@ -931,10 +988,34 @@ impl LibremetaverseSceneSource { client: owner.client().clone(), agent: owner.agent(), limits, + cache: Arc::new(Mutex::new(SceneCache::default())), } } } +#[cfg(feature = "live-grid")] +async fn request_asset_bytes( + assets: libremetaverse::AssetManager, + id: UUID, + asset_type: libremetaverse_types::AssetType, + cancellation: CancellationToken, +) -> Option> { + tokio::time::timeout( + Duration::from_secs(15), + assets.request_asset_with_uuid_asset_type_boolean_cancellation_token( + id, + asset_type, + false, + Some(cancellation), + ), + ) + .await + .ok() + .and_then(Result::ok) + .flatten() + .map(|asset| asset.asset_data) +} + #[cfg(feature = "live-grid")] impl SceneSource for LibremetaverseSceneSource { #[allow(clippy::too_many_lines)] @@ -975,68 +1056,97 @@ impl SceneSource for LibremetaverseSceneSource { let position = camera.position(); let forward = native_camera_forward(camera); let up = camera.up_axis(); - let entity_limit = self.limits.max_entities.saturating_sub(1); - let mut prims = { + let entity_limit = self.limits.max_entities.saturating_sub(2); + let camera_position = [position.x, position.y, position.z]; + let camera_forward = [forward.x, forward.y, forward.z]; + let maximum_distance = self.limits.max_distance_meters as f32; + let all_avatars = simulator + .objects_avatars + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .values() + .cloned() + .collect::>(); + let avatar_parents = all_avatars + .iter() + .map(|avatar| (avatar.local_id, (avatar.position, avatar.rotation))) + .collect::>(); + let mut avatars = all_avatars + .into_iter() + .filter(|avatar| { + scene_center_visible( + avatar.position, + camera_position, + camera_forward, + camera.vertical_fov_angle(), + self.limits.width as f32 / self.limits.height as f32, + maximum_distance, + ) + }) + .collect::>(); + avatars.sort_by(|left, right| { + scene_distance_squared(left.position, camera_position) + .total_cmp(&scene_distance_squared(right.position, camera_position)) + }); + let attachment_avatar_ids = avatars + .iter() + .filter(|avatar| { + scene_center_focused( + avatar.position, + camera_position, + camera_forward, + maximum_distance, + ) + }) + .map(|avatar| avatar.local_id) + .collect::>(); + let (mut world, mut attachments) = { let primitive_cache = simulator .objects_primitives .read() .unwrap_or_else(std::sync::PoisonError::into_inner); - let mut prims = primitive_cache - .values() - .filter(|prim| !prim.is_attachment) - .cloned() - .collect::>(); - for prim in &mut prims { - if let Some(parent) = primitive_cache.get(&prim.parent_id) { - prim.position = libremetaverse_types::Vector3::add_with_vector3_vector3( - parent.position, - libremetaverse_types::Vector3::mul_with_vector3_quaternion( - prim.position, - parent.rotation, - ), - ) - .unwrap_or(prim.position); - prim.rotation = - libremetaverse_types::Quaternion::mul_with_quaternion_quaternion( - parent.rotation, - prim.rotation, - ); - prim.parent_id = 0; + let mut world = Vec::new(); + let mut attachments = Vec::new(); + for prim in primitive_cache.values().cloned() { + let Some((prim, attachment_avatar)) = + world_primitive(prim, &primitive_cache, &avatar_parents) + else { + continue; + }; + if let Some(avatar) = attachment_avatar { + if attachment_avatar_ids.contains(&avatar) { + attachments.push(prim); + } + } else if scene_within_distance( + prim.position, + camera_position, + maximum_distance, + ) { + world.push(prim); } } - prims + (world, attachments) }; - let camera_position = [position.x, position.y, position.z]; - let camera_forward = [forward.x, forward.y, forward.z]; - // ponytail: center-plane culling is the bounded broad phase; use - // object bounds when the scene cache exposes reliable world AABBs. - prims.retain(|prim| { - scene_center_in_front(prim.position, camera_position, camera_forward) + world.sort_by(|left, right| { + scene_distance_squared(left.position, camera_position) + .total_cmp(&scene_distance_squared(right.position, camera_position)) }); - prims.sort_by(|left, right| { - scene_distance_score(left.position, camera_position, camera_forward).total_cmp( - &scene_distance_score(right.position, camera_position, camera_forward), - ) + attachments.sort_by(|left, right| { + scene_distance_squared(left.position, camera_position) + .total_cmp(&scene_distance_squared(right.position, camera_position)) }); - let objects_truncated = prims.len() > entity_limit; - prims.truncate(entity_limit); - let remaining = entity_limit.saturating_sub(prims.len()); - let (mut avatars, avatars_truncated) = { - let avatar_cache = simulator - .objects_avatars - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner); - ( - avatar_cache.values().cloned().collect::>(), - avatar_cache.len() > remaining, - ) - }; - avatars.sort_by(|left, right| { - scene_distance_score(left.position, camera_position, camera_forward).total_cmp( - &scene_distance_score(right.position, camera_position, camera_forward), - ) - }); - avatars.truncate(remaining); + let avatar_limit = avatars.len().min(entity_limit); + let object_limit = entity_limit.saturating_sub(avatar_limit); + let world_limit = world.len().min(object_limit); + let attachment_limit = attachments.len().min(object_limit - world_limit); + let objects_truncated = + world.len() > world_limit || attachments.len() > attachment_limit; + let avatars_truncated = avatars.len() > avatar_limit; + world.truncate(world_limit); + attachments.truncate(attachment_limit); + world.extend(attachments); + let prims = world; + avatars.truncate(avatar_limit); let region_id = simulator.region_id; let region_name = simulator.name.clone(); let water = simulator.water_height; @@ -1053,8 +1163,20 @@ impl SceneSource for LibremetaverseSceneSource { simulator.terrain_detail2, simulator.terrain_detail3, ]; - let mut texture_ids = terrain_texture_ids.to_vec(); + let mut texture_ids = terrain_texture_ids + .into_iter() + .filter(|id| *id != UUID::zero()) + .collect::>(); + let mut legacy_material_ids = Vec::new(); + let mut pbr_material_ids = Vec::new(); for prim in &prims { + if let Some(sculpt) = &prim.sculpt + && sculpt.type_() != libremetaverse_types::SculptType::Mesh + && sculpt.sculpt_texture != UUID::zero() + && !texture_ids.contains(&sculpt.sculpt_texture) + { + texture_ids.push(sculpt.sculpt_texture); + } if let Some(textures) = &prim.textures { let faces = textures .default_texture @@ -1065,6 +1187,14 @@ impl SceneSource for LibremetaverseSceneSource { if id != UUID::zero() && !texture_ids.contains(&id) { texture_ids.push(id); } + let legacy = face.material_id(); + if legacy != UUID::zero() && !legacy_material_ids.contains(&legacy) { + legacy_material_ids.push(legacy); + } + let pbr = face.render_material_id(); + if pbr != UUID::zero() && !pbr_material_ids.contains(&pbr) { + pbr_material_ids.push(pbr); + } } } } @@ -1080,56 +1210,147 @@ impl SceneSource for LibremetaverseSceneSource { } ids }); - texture_ids.retain(|id| *id != UUID::zero()); - texture_ids.truncate( - self.limits - .max_texture_fetches - .saturating_sub(mesh_ids.len()), - ); let assets = self.client.assets(); - let texture_fetches = texture_ids.len(); - let mut encoded_textures = Vec::new(); - let mut encoded_meshes = Vec::new(); + pbr_material_ids.truncate(self.limits.max_texture_fetches / 4); + let legacy_material_fetches = usize::from(!legacy_material_ids.is_empty()); + let legacy_materials = if legacy_material_ids.is_empty() { + Vec::new() + } else { + tokio::time::timeout( + Duration::from_secs(5), + self.client + .objects() + .request_materials_with_simulator_cancellation_token( + simulator.clone(), + Some(cancellation.clone()), + ), + ) + .await + .ok() + .and_then(Result::ok) + .map_or_else(Vec::new, Iterator::collect) + }; + for material in &legacy_materials { + for id in [material.normal_map(), material.specular_map()] { + if id != UUID::zero() && !texture_ids.contains(&id) { + texture_ids.push(id); + } + } + } let mut texture_bytes = 0usize; - let mut pending = tokio::task::JoinSet::new(); - for id in texture_ids { + let mut pbr_materials = Vec::new(); + let mut pending_materials = tokio::task::JoinSet::new(); + let mut material_jobs = std::collections::VecDeque::from(pbr_material_ids.clone()); + for _ in 0..16 { + let Some(id) = material_jobs.pop_front() else { + break; + }; let assets = assets.clone(); let token = cancellation.clone(); - pending.spawn(async move { - let asset = tokio::time::timeout( - Duration::from_secs(5), - assets.request_asset_with_uuid_asset_type_boolean_cancellation_token( + pending_materials.spawn(async move { + ( + id, + request_asset_bytes( + assets, id, - libremetaverse_types::AssetType::Texture, - false, - Some(token), - ), + libremetaverse_types::AssetType::Material, + token, + ) + .await, ) - .await - .ok() - .and_then(Result::ok) - .flatten(); - (false, id, asset) }); } - for id in mesh_ids { + while let Some(result) = pending_materials.join_next().await { + if cancellation.is_cancellation_requested() { + pending_materials.abort_all(); + return Err(VisionError::Cancelled); + } + if let Ok((id, Some(bytes))) = result + && texture_bytes.saturating_add(bytes.len()) <= self.limits.max_texture_bytes + { + texture_bytes += bytes.len(); + if let Ok(material) = + libremetaverse::assets::AssetMaterial::new_with_uuid_bytes(id, bytes) + { + for texture in material.texture_ids() { + if texture != UUID::zero() && !texture_ids.contains(&texture) { + texture_ids.push(texture); + } + } + pbr_materials.push((id, material)); + } + } + if let Some(id) = material_jobs.pop_front() { + let assets = assets.clone(); + let token = cancellation.clone(); + pending_materials.spawn(async move { + ( + id, + request_asset_bytes( + assets, + id, + libremetaverse_types::AssetType::Material, + token, + ) + .await, + ) + }); + } + } + texture_ids.retain(|id| *id != UUID::zero()); + let scene_texture_ids = texture_ids.clone(); + { + let mut cache = lock(&self.cache); + if cache.region_id != Some(region_id) { + cache.region_id = Some(region_id); + cache.textures.clear(); + cache.texture_pixels = 0; + cache.geometry.clear(); + cache.access = 0; + } + texture_ids.retain(|id| !cache.textures.contains_key(id)); + } + texture_ids.truncate( + self.limits.max_texture_fetches.saturating_sub( + mesh_ids + .len() + .saturating_add(pbr_material_ids.len()) + .saturating_add(legacy_material_fetches), + ), + ); + let texture_fetches = texture_ids + .len() + .saturating_add(mesh_ids.len()) + .saturating_add(pbr_material_ids.len()) + .saturating_add(legacy_material_fetches); + let mut encoded_textures = Vec::new(); + let mut encoded_meshes = Vec::new(); + let mut pending = tokio::task::JoinSet::new(); + let mut asset_jobs = std::collections::VecDeque::new(); + asset_jobs.extend(texture_ids.into_iter().map(|id| (false, id))); + asset_jobs.extend(mesh_ids.into_iter().map(|id| (true, id))); + for _ in 0..32 { + let Some((mesh, id)) = asset_jobs.pop_front() else { + break; + }; let assets = assets.clone(); let token = cancellation.clone(); pending.spawn(async move { - let asset = tokio::time::timeout( - Duration::from_secs(5), - assets.request_asset_with_uuid_asset_type_boolean_cancellation_token( + ( + mesh, + id, + request_asset_bytes( + assets, id, - libremetaverse_types::AssetType::Mesh, - false, - Some(token), - ), + if mesh { + libremetaverse_types::AssetType::Mesh + } else { + libremetaverse_types::AssetType::Texture + }, + token, + ) + .await, ) - .await - .ok() - .and_then(Result::ok) - .flatten(); - (true, id, asset) }); } while let Some(result) = pending.join_next().await { @@ -1137,39 +1358,66 @@ impl SceneSource for LibremetaverseSceneSource { pending.abort_all(); return Err(VisionError::Cancelled); } - let Ok((mesh, id, Some(asset))) = result else { - continue; - }; - if texture_bytes.saturating_add(asset.asset_data.len()) - <= self.limits.max_texture_bytes + if let Ok((mesh, id, Some(bytes))) = result + && texture_bytes.saturating_add(bytes.len()) <= self.limits.max_texture_bytes { - texture_bytes += asset.asset_data.len(); + texture_bytes += bytes.len(); if mesh { - encoded_meshes.push((id, asset.asset_data)); + encoded_meshes.push((id, bytes)); } else { - encoded_textures.push((id, asset.asset_data)); + encoded_textures.push((id, bytes)); } } + if let Some((mesh, id)) = asset_jobs.pop_front() { + let assets = assets.clone(); + let token = cancellation.clone(); + pending.spawn(async move { + ( + mesh, + id, + request_asset_bytes( + assets, + id, + if mesh { + libremetaverse_types::AssetType::Mesh + } else { + libremetaverse_types::AssetType::Texture + }, + token, + ) + .await, + ) + }); + } } let maximum = self.limits.max_triangles; let decode_pixels = self.limits.max_decode_pixels; let decode_bytes = self.limits.max_texture_bytes; - let (entities, decoded_texture_pixels) = tokio::task::spawn_blocking(move || { - native_entities( - prims, - avatars, - water, - region_size, - &terrain, - terrain_texture_ids, - maximum, - encoded_textures, - encoded_meshes, - (decode_bytes, decode_pixels), - ) - }) - .await - .map_err(|_| VisionError::InvalidScene)??; + let max_cached_geometry = self.limits.max_entities; + let cache = Arc::clone(&self.cache); + let (entities, textures, decoded_texture_pixels) = + tokio::task::spawn_blocking(move || { + native_entities( + prims, + avatars, + water, + region_size, + &terrain, + terrain_texture_ids, + maximum, + encoded_textures, + encoded_meshes, + legacy_materials, + pbr_materials, + (decode_bytes, decode_pixels), + cache.as_ref(), + &scene_texture_ids, + max_cached_geometry, + camera_position, + ) + }) + .await + .map_err(|_| VisionError::InvalidScene)??; if cancellation.is_cancellation_requested() { return Err(VisionError::Cancelled); } @@ -1197,6 +1445,7 @@ impl SceneSource for LibremetaverseSceneSource { vertical_fov_degrees: camera.vertical_fov_angle().to_degrees(), }, entities, + textures, completeness: SnapshotCompleteness { objects_truncated, avatars_truncated, @@ -1221,38 +1470,114 @@ pub(crate) fn native_camera_forward( } #[cfg(feature = "live-grid")] -pub(crate) fn scene_center_in_front( +pub(crate) fn scene_within_distance( position: libremetaverse_types::Vector3, camera: [f32; 3], - forward: [f32; 3], + maximum: f32, ) -> bool { - dot( - [ - position.x - camera[0], - position.y - camera[1], - position.z - camera[2], - ], - forward, - ) > 0.0 + scene_distance_squared(position, camera) <= maximum * maximum } #[cfg(feature = "live-grid")] -fn scene_distance_score( - position: libremetaverse_types::Vector3, - camera: [f32; 3], - forward: [f32; 3], -) -> f32 { +fn scene_distance_squared(position: libremetaverse_types::Vector3, camera: [f32; 3]) -> f32 { let delta = [ position.x - camera[0], position.y - camera[1], position.z - camera[2], ]; - let distance = dot(delta, delta); - if dot(delta, forward) > 0.0 { - distance - } else { - distance + 1_000_000.0 + dot(delta, delta) +} + +#[cfg(feature = "live-grid")] +pub(crate) fn scene_center_visible( + position: libremetaverse_types::Vector3, + camera: [f32; 3], + forward: [f32; 3], + vertical_fov: f32, + aspect: f32, + maximum: f32, +) -> bool { + let delta = [ + position.x - camera[0], + position.y - camera[1], + position.z - camera[2], + ]; + let distance_squared = dot(delta, delta); + if distance_squared > maximum * maximum { + return false; } + if distance_squared <= f32::EPSILON { + return true; + } + let horizontal_half_fov = ((vertical_fov * 0.5).tan() * aspect).atan(); + let cosine_limit = horizontal_half_fov.max(vertical_fov * 0.5).cos(); + dot(delta, forward) / distance_squared.sqrt() >= cosine_limit +} + +#[cfg(feature = "live-grid")] +pub(crate) fn scene_center_focused( + position: libremetaverse_types::Vector3, + camera: [f32; 3], + forward: [f32; 3], + maximum: f32, +) -> bool { + let delta = [ + position.x - camera[0], + position.y - camera[1], + position.z - camera[2], + ]; + let distance_squared = dot(delta, delta); + distance_squared <= maximum * maximum + && distance_squared > f32::EPSILON + && dot(delta, forward) / distance_squared.sqrt() >= 15_f32.to_radians().cos() +} + +#[cfg(feature = "live-grid")] +fn world_primitive( + mut prim: libremetaverse::Primitive, + primitives: &std::collections::HashMap, + avatars: &std::collections::BTreeMap< + u32, + ( + libremetaverse_types::Vector3, + libremetaverse_types::Quaternion, + ), + >, +) -> Option<(libremetaverse::Primitive, Option)> { + let mut parent_id = prim.parent_id; + for _ in 0..32 { + if parent_id == 0 { + prim.parent_id = 0; + return Some((prim, None)); + } + let (parent_position, parent_rotation, next_parent, attachment) = + if let Some(parent) = primitives.get(&parent_id) { + (parent.position, parent.rotation, parent.parent_id, None) + } else if let Some((position, rotation)) = avatars.get(&parent_id) { + (*position, *rotation, 0, Some(parent_id)) + } else { + return None; + }; + prim.position = libremetaverse_types::Vector3::add_with_vector3_vector3( + parent_position, + libremetaverse_types::Vector3::mul_with_vector3_quaternion( + prim.position, + parent_rotation, + ), + ) + .ok()?; + prim.rotation = libremetaverse_types::Quaternion::mul_with_quaternion_quaternion( + parent_rotation, + prim.rotation, + ); + if let Some(avatar) = attachment { + prim.parent_id = 0; + prim.is_attachment = true; + return Some((prim, Some(avatar))); + } + parent_id = next_parent; + } + None } #[cfg(feature = "live-grid")] @@ -1267,56 +1592,105 @@ fn native_entities( maximum: usize, encoded_textures: Vec<(UUID, Vec)>, encoded_meshes: Vec<(UUID, Vec)>, + legacy_materials: Vec, + pbr_materials: Vec<(UUID, libremetaverse::assets::AssetMaterial)>, decode_limits: (usize, usize), -) -> Result<(Vec, usize), VisionError> { - let (textures, pixels_consumed) = - decode_textures(encoded_textures, decode_limits.0, decode_limits.1); + cache: &Mutex, + scene_texture_ids: &[UUID], + max_cached_geometry: usize, + camera_position: [f32; 3], +) -> Result<(Vec, Vec, usize), VisionError> { + let (textures, pixels_consumed) = cached_textures( + cache, + scene_texture_ids, + encoded_textures, + decode_limits.0, + decode_limits.1, + ); let mesh_assets = encoded_meshes .into_iter() .collect::>(); + let texture_indices = textures + .keys() + .enumerate() + .map(|(index, id)| (*id, index)) + .collect::>(); + let legacy_materials = legacy_materials + .into_iter() + .map(|material| (material.id(), material)) + .collect::>(); + let pbr_materials = pbr_materials + .into_iter() + .collect::>(); let renderer = libremetaverse_rendering_simple::SimpleRenderer::new() .map_err(|_| VisionError::InvalidScene)?; let mut entities = Vec::new(); let mut triangles = 0usize; + let mut gpu_triangles = 0usize; + let avatar_triangle_reserve = avatars.len().saturating_mul(2).min(maximum / 4); + let object_triangle_limit = maximum + .saturating_div(2) + .saturating_sub(avatar_triangle_reserve); for prim in prims { + if triangles >= object_triangle_limit || gpu_triangles >= object_triangle_limit { + break; + } let name = prim.properties.as_ref().map_or_else( || "tracked object".to_owned(), |properties| properties.name.clone(), ); - let mesh = if let Some(sculpt) = &prim.sculpt - && sculpt.type_() == libremetaverse_types::SculptType::Mesh - { - let Some(bytes) = mesh_assets.get(&sculpt.sculpt_texture) else { - continue; - }; - let Ok(asset) = libremetaverse::assets::AssetMesh::new_with_uuid_bytes( - sculpt.sculpt_texture, - bytes.clone(), - ) else { - continue; - }; - let mut decoded = None; - if !libremetaverse::rendering::FacetedMesh::try_decode_from_asset( - prim.clone(), - asset, - libremetaverse::rendering::DetailLevel::Low, - &mut decoded, - ) { - continue; - } - let Some(mesh) = decoded else { continue }; - mesh - } else { - let Ok(mesh) = renderer - .generate_faceted_mesh(prim.clone(), libremetaverse::rendering::DetailLevel::Low) - else { - continue; - }; - mesh + let detail = scene_detail_level(&prim, camera_position); + let signature = GeometrySignature { + construction: prim.prim_data.clone(), + sculpt: prim.sculpt.clone(), + textures: prim + .textures + .as_ref() + .and_then(|textures| textures.get_bytes().ok()) + .unwrap_or_default(), + detail, }; + let mesh = cached_geometry(cache, prim.id, &signature).or_else(|| { + let generated = if let Some(sculpt) = &prim.sculpt { + if sculpt.type_() == libremetaverse_types::SculptType::Mesh { + let bytes = mesh_assets.get(&sculpt.sculpt_texture)?; + let asset = libremetaverse::assets::AssetMesh::new_with_uuid_bytes( + sculpt.sculpt_texture, + bytes.clone(), + ) + .ok()?; + let mut decoded = None; + if !libremetaverse::rendering::FacetedMesh::try_decode_from_asset( + prim.clone(), + asset, + detail, + &mut decoded, + ) { + return None; + } + decoded? + } else { + let image = textures.get(&sculpt.sculpt_texture)?.managed_image().ok()?; + renderer + .generate_faceted_sculpt_mesh(prim.clone(), image, detail) + .ok()? + } + } else { + renderer.generate_faceted_mesh(prim.clone(), detail).ok()? + }; + Some(store_geometry( + cache, + prim.id, + signature, + generated, + max_cached_geometry, + )) + }); + let Some(mesh) = mesh else { continue }; let mut output = Vec::new(); - let mut texture_available = false; - for face in mesh.faces { + let mut renderables = Vec::new(); + let mut texture_complete = true; + for face in &mesh.faces { let rgba = face.texture_face.rgba(); let tint = [ unit_byte(rgba.r), @@ -1325,7 +1699,43 @@ fn native_entities( unit_byte(rgba.a), ]; let texture = textures.get(&face.texture_face.texture_id()); - texture_available |= texture.is_some(); + if face.texture_face.texture_id() != UUID::zero() && texture.is_none() { + texture_complete = false; + continue; + } + let gpu_vertices = face + .vertices + .iter() + .map(|vertex| native_gpu_vertex(vertex, &prim)) + .collect::>(); + let gpu_indices = face + .indices + .chunks_exact(3) + .filter(|indices| { + indices + .iter() + .all(|index| usize::from(*index) < gpu_vertices.len()) + }) + .take(object_triangle_limit.saturating_sub(gpu_triangles)) + .flatten() + .map(|index| u32::from(*index)) + .collect::>(); + if !gpu_indices.is_empty() { + gpu_triangles += gpu_indices.len() / 3; + renderables.push(SceneRenderable { + mesh: metacrate_rendering_wgpu::Mesh { + vertices: gpu_vertices, + indices: gpu_indices, + }, + material: face_material( + &face.texture_face, + &texture_indices, + &legacy_materials, + &pbr_materials, + texture.is_some_and(|texture| texture.has_alpha()), + ), + }); + } 'triangle: for indices in face.indices.chunks_exact(3) { let mut vertices = [[0.0; 3]; 3]; let mut colors = [tint; 3]; @@ -1357,7 +1767,7 @@ fn native_entities( } let shade = triangle_shade(vertices); colors = colors.map(|color| shade_color(color, shade)); - if triangles >= maximum / 2 { + if triangles >= object_triangle_limit { break; } triangles += 1; @@ -1372,7 +1782,8 @@ fn native_entities( kind: SceneEntityKind::Object, display_name: name, triangles: output, - texture_available, + renderables, + texture_available: texture_complete, }); } for avatar in avatars { @@ -1402,6 +1813,9 @@ fn native_entities( colors_srgb: [[80, 140, 220, 255]; 3], }, ]; + let renderables = scene_renderable_from_triangles(&avatar_triangles) + .into_iter() + .collect(); triangles += 2; entities.push(SceneEntity { id: avatar.id, @@ -1411,6 +1825,7 @@ fn native_entities( crate::observability::pseudonymous_identifier(&avatar.id.to_string()) ), triangles: avatar_triangles, + renderables, texture_available: false, }); } @@ -1420,20 +1835,353 @@ fn native_entities( water, &textures, terrain_texture_ids, - maximum.saturating_sub(triangles), + maximum.saturating_sub(triangles).saturating_sub(2), ); if !terrain_triangles.is_empty() { + let renderables = scene_renderable_from_triangles(&terrain_triangles) + .into_iter() + .collect(); entities.push(SceneEntity { id: UUID::zero(), kind: SceneEntityKind::Terrain, display_name: "simulator terrain".into(), triangles: terrain_triangles, + renderables, texture_available: terrain_texture_ids .iter() .any(|id| textures.contains_key(id)), }); } - Ok((entities, pixels_consumed)) + let water_triangles = vec![ + SceneTriangle { + vertices: [ + [0.0, 0.0, water], + [region_size.0, 0.0, water], + [region_size.0, region_size.1, water], + ], + colors_srgb: [[35, 105, 165, 105]; 3], + }, + SceneTriangle { + vertices: [ + [0.0, 0.0, water], + [region_size.0, region_size.1, water], + [0.0, region_size.1, water], + ], + colors_srgb: [[35, 105, 165, 105]; 3], + }, + ]; + entities.push(SceneEntity { + id: UUID::zero(), + kind: SceneEntityKind::Environment, + display_name: "simulator water".into(), + renderables: scene_renderable_from_triangles(&water_triangles) + .into_iter() + .collect(), + triangles: water_triangles, + texture_available: true, + }); + let gpu_textures = textures + .values() + .map(|texture| texture.scene_texture()) + .collect(); + Ok((entities, gpu_textures, pixels_consumed)) +} + +#[cfg(feature = "live-grid")] +fn cached_geometry( + cache: &Mutex, + id: UUID, + signature: &GeometrySignature, +) -> Option> { + let mut cache = lock(cache); + cache.access = cache.access.wrapping_add(1); + let access = cache.access; + let entry = cache.geometry.get_mut(&id)?; + if entry.signature != *signature { + return None; + } + entry.last_used = access; + Some(Arc::clone(&entry.mesh)) +} + +#[cfg(feature = "live-grid")] +fn store_geometry( + cache: &Mutex, + id: UUID, + signature: GeometrySignature, + mesh: libremetaverse::rendering::FacetedMesh, + maximum: usize, +) -> Arc { + let mesh = Arc::new(mesh); + let mut cache = lock(cache); + cache.access = cache.access.wrapping_add(1); + if !cache.geometry.contains_key(&id) + && cache.geometry.len() >= maximum + && let Some(oldest) = cache + .geometry + .iter() + .min_by_key(|(_, geometry)| geometry.last_used) + .map(|(id, _)| *id) + { + cache.geometry.remove(&oldest); + } + let last_used = cache.access; + cache.geometry.insert( + id, + CachedGeometry { + signature, + mesh: Arc::clone(&mesh), + last_used, + }, + ); + mesh +} + +#[cfg(feature = "live-grid")] +fn scene_detail_level( + prim: &libremetaverse::Primitive, + camera: [f32; 3], +) -> libremetaverse::rendering::DetailLevel { + let distance = scene_distance_squared(prim.position, camera).sqrt(); + let extent = prim.scale.x.max(prim.scale.y).max(prim.scale.z); + if distance < 12.0 && extent < 16.0 { + libremetaverse::rendering::DetailLevel::High + } else { + libremetaverse::rendering::DetailLevel::Medium + } +} + +#[cfg(feature = "live-grid")] +fn native_gpu_vertex( + vertex: &libremetaverse::rendering::Vertex, + prim: &libremetaverse::Primitive, +) -> metacrate_rendering_wgpu::Vertex { + let scaled = libremetaverse_types::Vector3 { + x: vertex.position.x * prim.scale.x, + y: vertex.position.y * prim.scale.y, + z: vertex.position.z * prim.scale.z, + }; + let position = rotate_vector(scaled, prim.rotation); + let inverse_scaled_normal = libremetaverse_types::Vector3 { + x: vertex.normal.x / prim.scale.x.abs().max(f32::EPSILON), + y: vertex.normal.y / prim.scale.y.abs().max(f32::EPSILON), + z: vertex.normal.z / prim.scale.z.abs().max(f32::EPSILON), + }; + let normal = rotate_vector(inverse_scaled_normal, prim.rotation); + let normal_length = (normal.x * normal.x + normal.y * normal.y + normal.z * normal.z) + .sqrt() + .max(f32::EPSILON); + metacrate_rendering_wgpu::Vertex { + position: [ + position.x + prim.position.x, + position.y + prim.position.y, + position.z + prim.position.z, + ], + normal: [ + normal.x / normal_length, + normal.y / normal_length, + normal.z / normal_length, + ], + tex_coord: [vertex.tex_coord.x, vertex.tex_coord.y], + color_srgb: [255; 4], + } +} + +#[cfg(feature = "live-grid")] +#[allow(clippy::too_many_lines)] // Direct mapping of the two supported material models. +fn face_material( + face: &libremetaverse::PrimitiveTextureEntryFace, + textures: &std::collections::BTreeMap, + legacy_materials: &std::collections::BTreeMap, + pbr_materials: &std::collections::BTreeMap, + texture_has_alpha: bool, +) -> metacrate_rendering_wgpu::Material { + use metacrate_rendering_wgpu::{ + AlphaMode, BlinnPhongMaterial, Material, PbrMaterial, TextureSlot, UvTransform, + }; + if let Some(material) = pbr_materials.get(&face.render_material_id()) { + let ids = material.texture_ids(); + let transforms = material.texture_transforms(); + let slot = |index: usize| TextureSlot { + texture: ids.get(index).and_then(|id| textures.get(id)).copied(), + transform: transforms + .get(index) + .map_or_else(UvTransform::default, |transform| UvTransform { + scale: [transform.scale.x, transform.scale.y], + offset: [transform.offset.x, transform.offset.y], + rotation_radians: transform.rotation, + }), + }; + let base = material.base_color_factor(); + let tint = face.rgba(); + let emissive = material.emissive_factor(); + return Material::Pbr(PbrMaterial { + base_color_srgb: [ + unit_byte(base.r * tint.r), + unit_byte(base.g * tint.g), + unit_byte(base.b * tint.b), + unit_byte(base.a * tint.a), + ], + base_color: slot(libremetaverse::assets::AssetMaterial::TEXTURE_BASE_COLOR as usize), + normal: slot(libremetaverse::assets::AssetMaterial::TEXTURE_NORMAL as usize), + metallic_roughness: slot( + libremetaverse::assets::AssetMaterial::TEXTURE_METALLIC_ROUGHNESS as usize, + ), + emissive: slot(libremetaverse::assets::AssetMaterial::TEXTURE_EMISSIVE as usize), + metallic_factor: material.metallic_factor(), + roughness_factor: material.roughness_factor(), + emissive_factor_srgb: [ + unit_byte(emissive.x), + unit_byte(emissive.y), + unit_byte(emissive.z), + ], + double_sided: material.double_sided(), + alpha_mode: match material.alpha_mode() { + libremetaverse::assets::GltfAlphaMode::Opaque => AlphaMode::Opaque, + libremetaverse::assets::GltfAlphaMode::Blend => AlphaMode::Blend, + libremetaverse::assets::GltfAlphaMode::Mask => AlphaMode::Mask { + cutoff: material.alpha_cutoff(), + }, + }, + }); + } + let rgba = face.rgba(); + let legacy = legacy_materials.get(&face.material_id()); + let shininess = match face.shiny() { + libremetaverse::Shininess::None => 1.0, + libremetaverse::Shininess::Low => 24.0, + libremetaverse::Shininess::Medium => 64.0, + libremetaverse::Shininess::High => 128.0, + }; + Material::BlinnPhong(BlinnPhongMaterial { + diffuse_color_srgb: [ + unit_byte(rgba.r), + unit_byte(rgba.g), + unit_byte(rgba.b), + unit_byte(rgba.a), + ], + diffuse: TextureSlot { + texture: textures.get(&face.texture_id()).copied(), + transform: UvTransform { + scale: [face.repeat_u(), face.repeat_v()], + offset: [face.offset_u(), face.offset_v()], + rotation_radians: face.rotation(), + }, + }, + normal: legacy.map_or_else(TextureSlot::default, |material| TextureSlot { + texture: textures.get(&material.normal_map()).copied(), + transform: UvTransform { + scale: [ + material.normal_map_repeat_x() as f32, + material.normal_map_repeat_y() as f32, + ], + offset: [ + material.normal_map_offset_x() as f32, + material.normal_map_offset_y() as f32, + ], + rotation_radians: material.normal_map_rotation() as f32, + }, + }), + specular: legacy.map_or_else(TextureSlot::default, |material| TextureSlot { + texture: textures.get(&material.specular_map()).copied(), + transform: UvTransform { + scale: [ + material.specular_map_repeat_x() as f32, + material.specular_map_repeat_y() as f32, + ], + offset: [ + material.specular_map_offset_x() as f32, + material.specular_map_offset_y() as f32, + ], + rotation_radians: material.specular_map_rotation() as f32, + }, + }), + specular_color_srgb: legacy.map_or([255; 3], |material| { + let color = material.specular_color(); + [unit_byte(color.r), unit_byte(color.g), unit_byte(color.b)] + }), + shininess: legacy.map_or(shininess, |material| { + f32::from(material.specular_exponent()).max(1.0) + }), + environment_intensity: legacy.map_or(0.0, |material| { + f32::from(material.environment_intensity()) / 255.0 + }), + fullbright: face.fullbright() + || legacy.is_some_and(|material| { + material.diffuse_alpha_mode() + == libremetaverse::materials::LegacyMaterialAlphaMode::Emissive + }), + alpha_mode: legacy.map_or_else( + || { + if rgba.a < 0.999 || texture_has_alpha { + AlphaMode::Blend + } else { + AlphaMode::Opaque + } + }, + |material| match material.diffuse_alpha_mode() { + libremetaverse::materials::LegacyMaterialAlphaMode::Blend + | libremetaverse::materials::LegacyMaterialAlphaMode::Emissive => AlphaMode::Blend, + libremetaverse::materials::LegacyMaterialAlphaMode::Mask => AlphaMode::Mask { + cutoff: f32::from(material.alpha_mask_cutoff()) / 255.0, + }, + libremetaverse::materials::LegacyMaterialAlphaMode::Default => { + if rgba.a < 0.999 || texture_has_alpha { + AlphaMode::Blend + } else { + AlphaMode::Opaque + } + } + libremetaverse::materials::LegacyMaterialAlphaMode::None => AlphaMode::Opaque, + }, + ), + ..Default::default() + }) +} + +#[cfg(feature = "live-grid")] +pub(crate) fn scene_renderable_from_triangles( + triangles: &[SceneTriangle], +) -> Option { + if triangles.is_empty() { + return None; + } + let mut vertices = Vec::with_capacity(triangles.len().saturating_mul(3)); + for triangle in triangles { + let edge_a = + std::array::from_fn(|axis| triangle.vertices[1][axis] - triangle.vertices[0][axis]); + let edge_b = + std::array::from_fn(|axis| triangle.vertices[2][axis] - triangle.vertices[0][axis]); + let normal = normalize(cross(edge_a, edge_b)).unwrap_or([0.0, 0.0, 1.0]); + vertices.extend((0..3).map(|index| metacrate_rendering_wgpu::Vertex { + position: triangle.vertices[index], + normal, + tex_coord: [0.0; 2], + color_srgb: triangle.colors_srgb[index], + })); + } + let indices = (0..vertices.len()) + .map_while(|index| u32::try_from(index).ok()) + .collect(); + Some(SceneRenderable { + mesh: metacrate_rendering_wgpu::Mesh { vertices, indices }, + material: metacrate_rendering_wgpu::Material::BlinnPhong( + metacrate_rendering_wgpu::BlinnPhongMaterial { + fullbright: true, + double_sided: true, + alpha_mode: if triangles + .iter() + .flat_map(|triangle| triangle.colors_srgb) + .any(|color| color[3] != 255) + { + metacrate_rendering_wgpu::AlphaMode::Blend + } else { + metacrate_rendering_wgpu::AlphaMode::Opaque + }, + ..Default::default() + }, + ), + }) } #[cfg(feature = "live-grid")] @@ -1459,6 +2207,173 @@ impl DecodedTexture { self.alpha.get(index).copied().unwrap_or(255), ] } + + fn has_alpha(&self) -> bool { + self.alpha.iter().any(|alpha| *alpha != 255) + } + + fn scene_texture(&self) -> SceneTexture { + let pixels = self.width.saturating_mul(self.height); + let mut rgba = Vec::with_capacity(pixels.saturating_mul(4)); + for index in 0..pixels { + rgba.extend_from_slice(&[ + self.red[index], + self.green.get(index).copied().unwrap_or(self.red[index]), + self.blue.get(index).copied().unwrap_or(self.red[index]), + self.alpha.get(index).copied().unwrap_or(255), + ]); + } + SceneTexture { + width: self.width.try_into().unwrap_or(u32::MAX), + height: self.height.try_into().unwrap_or(u32::MAX), + rgba, + } + } + + fn managed_image(&self) -> Result { + let width = self + .width + .try_into() + .map_err(|_| VisionError::InvalidScene)?; + let height = self + .height + .try_into() + .map_err(|_| VisionError::InvalidScene)?; + let channels = libremetaverse_imaging::ManagedImageImageChannels::COLOR + | if self.alpha.is_empty() { + libremetaverse_imaging::ManagedImageImageChannels::default() + } else { + libremetaverse_imaging::ManagedImageImageChannels::ALPHA + }; + let image = libremetaverse_imaging::ManagedImage { + alpha: self.alpha.clone(), + blue: self.blue.clone(), + bump: Vec::new(), + channels, + green: self.green.clone(), + height, + red: self.red.clone(), + width, + }; + image.validate().map_err(|_| VisionError::InvalidScene)?; + Ok(image) + } +} + +#[cfg(feature = "live-grid")] +fn cached_textures( + cache: &Mutex, + wanted: &[UUID], + encoded: Vec<(UUID, Vec)>, + max_bytes: usize, + max_cached_pixels: usize, +) -> (std::collections::BTreeMap>, usize) { + const MAX_PIXELS_PER_CAPTURE: usize = 16_777_216; + let (decoded, _) = decode_textures( + encoded, + max_bytes, + max_cached_pixels.min(MAX_PIXELS_PER_CAPTURE), + ); + let mut cache = lock(cache); + store_decoded_textures(&mut cache, wanted, decoded, max_cached_pixels); + let textures = wanted + .iter() + .filter_map(|id| { + cache + .textures + .get(id) + .map(|texture| (*id, Arc::clone(&texture.texture))) + }) + .collect::>(); + let pixels = textures + .values() + .map(|texture| texture.width.saturating_mul(texture.height)) + .sum(); + (textures, pixels) +} + +#[cfg(feature = "live-grid")] +fn store_decoded_textures( + cache: &mut SceneCache, + wanted: &[UUID], + decoded: std::collections::BTreeMap, + max_cached_pixels: usize, +) { + cache.access = cache.access.wrapping_add(1); + let access = cache.access; + for id in wanted { + if let Some(entry) = cache.textures.get_mut(id) { + entry.last_used = access; + } + } + for (id, texture) in decoded { + let pixels = texture.width.saturating_mul(texture.height); + if cache.textures.contains_key(&id) || pixels > max_cached_pixels { + continue; + } + while cache.texture_pixels.saturating_add(pixels) > max_cached_pixels { + let Some(oldest) = cache + .textures + .iter() + .filter(|(id, _)| !wanted.contains(id)) + .min_by_key(|(_, texture)| texture.last_used) + .map(|(id, _)| *id) + else { + break; + }; + if let Some(removed) = cache.textures.remove(&oldest) { + cache.texture_pixels = cache.texture_pixels.saturating_sub(removed.pixels); + } + } + if cache.texture_pixels.saturating_add(pixels) > max_cached_pixels { + continue; + } + cache.texture_pixels += pixels; + cache.textures.insert( + id, + CachedTexture { + texture: Arc::new(texture), + pixels, + last_used: access, + }, + ); + } +} + +#[cfg(all(test, feature = "live-grid"))] +mod texture_cache_tests { + use super::*; + + #[test] + fn evicts_content_outside_the_current_scene() { + let texture = || DecodedTexture { + width: 2, + height: 2, + red: vec![0; 4], + green: vec![0; 4], + blue: vec![0; 4], + alpha: vec![255; 4], + }; + let old = UUID::new_with_string("00000000-0000-4000-8000-000000000001".to_owned()).unwrap(); + let current = + UUID::new_with_string("00000000-0000-4000-8000-000000000002".to_owned()).unwrap(); + let mut cache = SceneCache::default(); + store_decoded_textures( + &mut cache, + &[old], + std::collections::BTreeMap::from([(old, texture())]), + 4, + ); + store_decoded_textures( + &mut cache, + &[current], + std::collections::BTreeMap::from([(current, texture())]), + 4, + ); + assert!(!cache.textures.contains_key(&old)); + assert!(cache.textures.contains_key(¤t)); + assert_eq!(cache.texture_pixels, 4); + } } #[cfg(feature = "live-grid")] @@ -1474,6 +2389,7 @@ fn decode_textures( break; } let options = libremetaverse_imaging::J2kDecodeOptions::default() + .with_discard_levels(1) .with_limits(max_bytes.min(bytes.len().max(1)), remaining_pixels); let Ok(image) = libremetaverse_imaging::RustJ2kCodec::decode_bytes(&bytes, options) else { continue; @@ -1529,7 +2445,7 @@ fn terrain_triangles( terrain: &[libremetaverse::TerrainPatch], region_size: (f32, f32), water: f32, - textures: &std::collections::BTreeMap, + textures: &std::collections::BTreeMap>, texture_ids: [UUID; 4], maximum: usize, ) -> Vec { diff --git a/crates/metacrate-grid-agent/src/vision_tests.rs b/crates/metacrate-grid-agent/src/vision_tests.rs index 5c361c0..86a7b96 100644 --- a/crates/metacrate-grid-agent/src/vision_tests.rs +++ b/crates/metacrate-grid-agent/src/vision_tests.rs @@ -32,6 +32,7 @@ impl SceneSource for FakeScene { fn uuid(value: u32) -> UUID { UUID::new_with_string(format!("00000000-0000-4000-8000-{value:012}")).unwrap() } + fn triangle(z: f32, color: [u8; 4]) -> SceneTriangle { SceneTriangle { vertices: [[-1.0, -1.0, z], [1.0, -1.0, z], [0.0, 1.0, z]], @@ -85,24 +86,68 @@ fn scene_faces_are_shaded_without_changing_alpha() { #[cfg(feature = "live-grid")] #[test] -fn scene_broad_phase_rejects_object_centers_behind_the_camera() { - assert!(scene_center_in_front( - libremetaverse_types::Vector3 { - x: 4.0, - y: 0.0, - z: 0.0 - }, - [0.0; 3], - [1.0, 0.0, 0.0] - )); - assert!(!scene_center_in_front( +fn scene_visibility_uses_configured_distance_and_avatar_view() { + assert!(scene_within_distance( libremetaverse_types::Vector3 { x: -4.0, y: 0.0, z: 0.0 }, [0.0; 3], - [1.0, 0.0, 0.0] + 64.0, + )); + assert!(!scene_within_distance( + libremetaverse_types::Vector3 { + x: 65.0, + y: 0.0, + z: 0.0 + }, + [0.0; 3], + 64.0, + )); + assert!(scene_center_visible( + libremetaverse_types::Vector3 { + x: 4.0, + y: 0.0, + z: 0.0 + }, + [0.0; 3], + [1.0, 0.0, 0.0], + 60_f32.to_radians(), + 16.0 / 9.0, + 64.0, + )); + assert!(!scene_center_visible( + libremetaverse_types::Vector3 { + x: -4.0, + y: 0.0, + z: 0.0 + }, + [0.0; 3], + [1.0, 0.0, 0.0], + 60_f32.to_radians(), + 16.0 / 9.0, + 64.0, + )); + assert!(scene_center_focused( + libremetaverse_types::Vector3 { + x: 4.0, + y: 0.0, + z: 0.0 + }, + [0.0; 3], + [1.0, 0.0, 0.0], + 64.0, + )); + assert!(!scene_center_focused( + libremetaverse_types::Vector3 { + x: 4.0, + y: 4.0, + z: 0.0 + }, + [0.0; 3], + [1.0, 0.0, 0.0], + 64.0, )); } fn scene() -> SceneSnapshot { @@ -123,6 +168,7 @@ fn scene() -> SceneSnapshot { kind: SceneEntityKind::Resident, display_name: "Private Resident".into(), triangles: vec![triangle(4.0, [255, 0, 0, 255])], + renderables: Vec::new(), texture_available: false, }, SceneEntity { @@ -130,9 +176,11 @@ fn scene() -> SceneSnapshot { kind: SceneEntityKind::Object, display_name: "Near".into(), triangles: vec![triangle(2.0, [0, 255, 0, 255])], + renderables: Vec::new(), texture_available: true, }, ], + textures: Vec::new(), completeness: SnapshotCompleteness { textures_missing: 1, terrain_available: false, @@ -190,7 +238,7 @@ async fn golden_scene_is_deterministic_depth_ordered_and_privacy_marked() { #[cfg(feature = "live-grid")] #[tokio::test(flavor = "multi_thread")] async fn offscreen_wgpu_renders_depth_ordered_scene_when_an_adapter_is_available() { - let Ok(renderer) = metacrate_rendering_wgpu::Renderer::new().await else { + let Ok(renderer) = metacrate_rendering_wgpu::Renderer::new_blocking() else { return; }; let limits = VisionLimits { @@ -205,15 +253,21 @@ async fn offscreen_wgpu_renders_depth_ordered_scene_when_an_adapter_is_available .iter() .flat_map(|entity| entity.triangles.iter().cloned()) .collect::>(); + let renderables = scene_renderable_from_triangles(&triangles) + .into_iter() + .collect::>(); let rgba = tokio::task::spawn_blocking(move || { renderer.render( scene.camera, - &triangles, + &renderables, + &scene.textures, [72, 96, 120, 255], metacrate_rendering_wgpu::RenderLimits { width: limits.width, height: limits.height, max_triangles: limits.max_triangles, + max_texture_bytes: limits.max_texture_bytes, + max_texture_pixels: limits.max_decode_pixels, far_distance: 512, }, ) diff --git a/crates/metacrate-grid-agent/tests/dependency_policy.rs b/crates/metacrate-grid-agent/tests/dependency_policy.rs index a7bf4d6..e556c76 100644 --- a/crates/metacrate-grid-agent/tests/dependency_policy.rs +++ b/crates/metacrate-grid-agent/tests/dependency_policy.rs @@ -104,7 +104,8 @@ fn vision_path_selects_only_reviewed_rust_renderers_and_codec() { assert!(manifest.contains("metacrate-rendering-wgpu =")); let renderer = fs::read_to_string(root.join("../metacrate-rendering-wgpu/Cargo.toml")) .expect("wgpu renderer manifest"); - assert!(renderer.contains("wgpu = { version = \"30.0.1\"")); + assert!(renderer.contains("bevy = { version = \"0.19.1\", default-features = false")); + assert!(renderer.contains("wgpu = [\"bevy-engine\"]")); } #[test] diff --git a/crates/metacrate-grid-agent/tests/live_llm_vision.rs b/crates/metacrate-grid-agent/tests/live_llm_vision.rs new file mode 100644 index 0000000..a857468 --- /dev/null +++ b/crates/metacrate-grid-agent/tests/live_llm_vision.rs @@ -0,0 +1,91 @@ +use base64::Engine as _; +use mentra::{BuiltinProvider, ContentBlock, ModelInfo, Runtime}; +use metacrate_grid_agent::{AgentConfig, LlmClient}; +use std::{collections::BTreeMap, error::Error, path::PathBuf}; + +#[tokio::test] +#[ignore = "sends the live renderer JPEG to the configured vision model through Mentra SSE"] +async fn luna_describes_live_renderer_evidence() -> Result<(), Box> { + let environment = live_environment()?; + let mut connection = AgentConfig::offline( + required(&environment, "OPENAPI_URL")?, + required(&environment, "OPENAPI_KEY")?, + )? + .llm; + connection.model = Some(required(&environment, "OPENAPI_MODEL")?.to_owned()); + let client = LlmClient::new(connection); + let runtime = Runtime::empty_builder() + .with_store(mentra::runtime::VolatileRuntimeStore::default()) + .with_registered_provider(client.mentra_provider()) + .build()?; + let root = std::env::temp_dir().join(format!("metacrate-live-vision-{}", std::process::id())); + let mut config = mentra::AgentConfig { + system: Some("You are in a virtual world.".to_owned()), + ..Default::default() + }; + config.compaction.transcript_dir = root.join("transcripts"); + config.task.tasks_dir = root.join("tasks"); + config.team.team_dir = root.join("teams"); + config.workspace.base_dir = root; + let mut agent = runtime.spawn_with_config( + "live-render-review", + ModelInfo::new(client.configured_model(), BuiltinProvider::OpenAI), + config, + )?; + let jpeg = std::fs::read( + std::env::var_os("METACRATE_LIVE_RENDER_OUTPUT").map_or_else( + || std::env::temp_dir().join("metacrate-live-render.jpg"), + PathBuf::from, + ), + )?; + let image = format!( + "data:image/jpeg;base64,{}", + base64::engine::general_purpose::STANDARD.encode(jpeg) + ); + let response = agent + .send(vec![ + ContentBlock::text( + "Describe this rendered virtual-world scene. State whether it contains recognizable textured terrain and mesh scenery, and identify any obvious rendering corruption.", + ), + ContentBlock::image_url(image), + ]) + .await?; + for block in response.content { + if let ContentBlock::Text { text } = block { + println!("LIVE_LLM_VISION={text}"); + } + } + Ok(()) +} + +fn live_environment() -> Result, Box> { + let mut values = std::env::vars().collect::>(); + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.env"); + for line in std::fs::read_to_string(path)?.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let Some((key, value)) = line.split_once('=') else { + continue; + }; + values.entry(key.trim().to_owned()).or_insert_with(|| { + value + .trim() + .trim_matches(|character| character == '\'' || character == '"') + .to_owned() + }); + } + Ok(values) +} + +fn required<'a>( + environment: &'a BTreeMap, + key: &'static str, +) -> Result<&'a str, Box> { + environment + .get(key) + .filter(|value| !value.is_empty()) + .map(String::as_str) + .ok_or_else(|| format!("missing live test setting {key}").into()) +} diff --git a/crates/metacrate-grid-agent/tests/live_renderer.rs b/crates/metacrate-grid-agent/tests/live_renderer.rs new file mode 100644 index 0000000..684739f --- /dev/null +++ b/crates/metacrate-grid-agent/tests/live_renderer.rs @@ -0,0 +1,443 @@ +#![cfg(feature = "live-grid")] + +use libremetaverse_types::{Vector3, compat::CancellationTokenSource}; +use metacrate_grid_agent::{ + EndpointUrl, GridConnection, GridSession, GridSessionBackend, LibremetaverseClientOwner, + LibremetaverseSceneSource, SecretString, SessionSignal, VisionLimits, VisionService, +}; +use std::{ + collections::BTreeMap, + error::Error, + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "logs two dedicated accounts into the configured live grid and writes a JPEG"] +#[allow(clippy::too_many_lines)] // The one end-to-end live workflow stays readable in execution order. +async fn live_bevy_capture_renders_the_target_accounts_scene() -> Result<(), Box> { + let environment = live_environment()?; + let target_owner = LibremetaverseClientOwner::new()?; + let renderer_owner = LibremetaverseClientOwner::new()?; + let (mut target_session, target_cancel) = login( + &target_owner, + &environment, + "GRID_TEST_UNPRIVILEGED_USER", + "GRID_TEST_UNPRIVILEGED_PASSWORD", + ) + .await?; + let (mut renderer_session, renderer_cancel) = match login( + &renderer_owner, + &environment, + "GRID_TEST_AUTHORIZED_USER", + "GRID_TEST_AUTHORIZED_PASSWORD", + ) + .await + { + Ok(login) => login, + Err(error) => { + let _ = target_session.logout(target_cancel.token()).await; + return Err(error); + } + }; + + let result = async { + wait_ready("target", &mut target_session, &target_cancel).await?; + wait_ready("renderer", &mut renderer_session, &renderer_cancel).await?; + let renderer_start_simulator = renderer_owner + .client() + .network() + .current_sim() + .ok_or("renderer start simulator unavailable")?; + let renderer_start_handle = renderer_start_simulator.handle; + let renderer_start_position = conventional_start(&renderer_start_simulator); + let target_simulator = target_owner + .client() + .network() + .current_sim() + .ok_or("target simulator unavailable")?; + let target_position = target_owner.agent().sim_position(); + let arrival = Vector3 { + x: target_position.x - 10.0, + y: target_position.y - 10.0, + z: target_position.z + 2.0, + }; + if !renderer_owner + .agent() + .teleport_with_u_int64_vector3_vector3_cancellation_token( + target_simulator.handle, + arrival, + target_position, + Some(renderer_cancel.token()), + ) + .await? + { + return Err("teleport did not complete".into()); + } + tokio::time::sleep(Duration::from_secs(5)).await; + if !renderer_owner + .agent() + .teleport_with_u_int64_vector3_vector3_cancellation_token( + renderer_start_handle, + renderer_start_position, + Vector3 { + x: renderer_start_position.x + 10.0, + y: renderer_start_position.y, + z: renderer_start_position.z, + }, + Some(renderer_cancel.token()), + ) + .await? + { + return Err("return teleport did not complete".into()); + } + tokio::time::sleep(Duration::from_secs(5)).await; + let mut renderer_simulator = renderer_owner + .client() + .network() + .current_sim() + .ok_or("renderer simulator unavailable")?; + let mut settled_position = renderer_owner.agent().sim_position(); + if let Some(ground) = terrain_ground( + &renderer_simulator, + renderer_start_position.x, + renderer_start_position.y, + ) { + let destination = Vector3 { + x: renderer_start_position.x, + y: renderer_start_position.y, + z: ground + 2.0, + }; + if (settled_position.z - destination.z).abs() > 1.0 { + if !renderer_owner + .agent() + .teleport_with_u_int64_vector3_vector3_cancellation_token( + renderer_start_handle, + destination, + Vector3 { + x: destination.x + 10.0, + ..destination + }, + Some(renderer_cancel.token()), + ) + .await? + { + return Err("ground-settle teleport did not complete".into()); + } + tokio::time::sleep(Duration::from_secs(5)).await; + renderer_simulator = renderer_owner + .client() + .network() + .current_sim() + .ok_or("settled renderer simulator unavailable")?; + settled_position = renderer_owner.agent().sim_position(); + } + } + wait_scene_settled(&renderer_owner).await?; + let mut view_forward = Vector3::unit_x(); + if let Some((destination, forward)) = scenic_location(&renderer_simulator) { + if !renderer_owner + .agent() + .teleport_with_u_int64_vector3_vector3_cancellation_token( + renderer_start_handle, + destination, + Vector3 { + x: destination.x + forward.x, + y: destination.y + forward.y, + z: destination.z + forward.z, + }, + Some(renderer_cancel.token()), + ) + .await? + { + return Err("scenic teleport did not complete".into()); + } + wait_scene_settled(&renderer_owner).await?; + renderer_simulator = renderer_owner + .client() + .network() + .current_sim() + .ok_or("scenic renderer simulator unavailable")?; + settled_position = renderer_owner.agent().sim_position(); + view_forward = forward; + } + { + let prims = renderer_simulator + .objects_primitives + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let largest_scale = prims.values().fold(0.0_f32, |largest, prim| { + largest.max(prim.scale.x.max(prim.scale.y).max(prim.scale.z)) + }); + println!( + "LIVE_SCENE_CACHE=prims:{} roots:{} children:{} attachments:{} sculpted:{} largest_scale:{largest_scale:.2}", + prims.len(), + prims.values().filter(|prim| prim.parent_id == 0).count(), + prims.values().filter(|prim| prim.parent_id != 0).count(), + prims.values().filter(|prim| prim.is_attachment).count(), + prims.values().filter(|prim| prim.sculpt.is_some()).count(), + ); + println!( + "LIVE_SCENE_GEOMETRY=mesh:{} sculpt:{} legacy:{}", + prims + .values() + .filter(|prim| prim.sculpt.as_ref().is_some_and(|sculpt| sculpt.type_() == libremetaverse_types::SculptType::Mesh)) + .count(), + prims + .values() + .filter(|prim| prim.sculpt.as_ref().is_some_and(|sculpt| sculpt.type_() != libremetaverse_types::SculptType::Mesh)) + .count(), + prims.values().filter(|prim| prim.sculpt.is_none()).count(), + ); + } + let (camera_position, camera_target) = avatar_camera(settled_position, view_forward)?; + renderer_owner + .agent() + .movement + .camera + .look_at_with_vector3_vector3(camera_position, camera_target)?; + + let limits = VisionLimits { + minimum_interval: Duration::ZERO, + ..VisionLimits::default() + }; + let vision = VisionService::new( + Arc::new(LibremetaverseSceneSource::new(&renderer_owner, limits)), + limits, + )? + .prefer_gpu(); + vision.set_generation(1); + let mut capture = None; + let output = std::env::var_os("METACRATE_LIVE_RENDER_OUTPUT").map_or_else( + || std::env::temp_dir().join("metacrate-live-render.jpg"), + PathBuf::from, + ); + let frames = std::env::var("METACRATE_LIVE_RENDER_FRAMES") + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(8) + .clamp(1, 16); + for frame in 1..=frames { + let next = vision + .capture( + &format!("live-renderer-evidence-{frame}"), + 1, + renderer_cancel.token(), + ) + .await?; + println!("LIVE_RENDER_FRAME_{frame}_SUMMARY={}", next.summary); + std::fs::write(&output, &next.jpeg)?; + capture = Some(next); + } + let capture = capture.ok_or("live renderer produced no frame")?; + println!("LIVE_RENDER_OUTPUT={}", output.display()); + println!("LIVE_RENDER_SHA256={}", capture.image_sha256); + Ok::<(), Box>(()) + } + .await; + + let _ = renderer_session.logout(renderer_cancel.token()).await; + let _ = target_session.logout(target_cancel.token()).await; + result +} + +async fn login( + owner: &LibremetaverseClientOwner, + environment: &BTreeMap, + user: &'static str, + password: &'static str, +) -> Result<(Box, CancellationTokenSource), Box> { + let cancellation = CancellationTokenSource::new(); + let backend = owner.session_backend(GridConnection { + login_url: EndpointUrl::parse("grid.login_url", &required(environment, "GRID_LOGIN_URL")?)?, + avatar_name: required(environment, user)?, + password: SecretString::new("grid.password", required(environment, password)?)?, + })?; + let session = backend.login(1, cancellation.token()).await?; + Ok((session, cancellation)) +} + +async fn wait_ready( + account: &str, + session: &mut Box, + cancellation: &CancellationTokenSource, +) -> Result<(), Box> { + let signal = tokio::time::timeout( + Duration::from_secs(30), + session.next_signal(cancellation.token()), + ) + .await + .map_err(|_| format!("timed out waiting for {account} session readiness"))?; + match signal { + SessionSignal::Ready => Ok(()), + signal => Err(format!("unexpected {account} session signal: {signal:?}").into()), + } +} + +async fn wait_scene_settled(owner: &LibremetaverseClientOwner) -> Result<(), Box> { + let deadline = Instant::now() + Duration::from_secs(30); + let minimum = Instant::now() + Duration::from_secs(10); + let mut previous = 0; + let mut stable = 0; + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + let simulator = owner + .client() + .network() + .current_sim() + .ok_or("simulator unavailable while settling scene")?; + let count = simulator + .objects_primitives + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .len(); + if count == previous && count != 0 { + stable += 1; + } else { + stable = 0; + previous = count; + } + if (Instant::now() >= minimum && stable >= 3) || Instant::now() >= deadline { + return Ok(()); + } + } +} + +fn live_environment() -> Result, Box> { + let mut values = std::env::vars().collect::>(); + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.env"); + for line in std::fs::read_to_string(path)?.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let Some((key, value)) = line.split_once('=') else { + continue; + }; + values.entry(key.trim().to_owned()).or_insert_with(|| { + value + .trim() + .trim_matches(|character| character == '\'' || character == '"') + .to_owned() + }); + } + Ok(values) +} + +fn required( + environment: &BTreeMap, + key: &'static str, +) -> Result> { + environment + .get(key) + .filter(|value| !value.is_empty()) + .cloned() + .ok_or_else(|| format!("missing live test setting {key}").into()) +} + +#[allow(clippy::cast_precision_loss)] // Simulator dimensions are small region coordinates. +fn conventional_start(simulator: &libremetaverse::Simulator) -> Vector3 { + let x = 128.0_f32.min(simulator.size_x as f32 - 2.0); + let y = 128.0_f32.min(simulator.size_y as f32 - 2.0); + Vector3 { x, y, z: 50.0 } +} + +#[allow( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_sign_loss +)] // Coordinates are already clamped inside the non-negative simulator extent. +fn terrain_ground(simulator: &libremetaverse::Simulator, x: f32, y: f32) -> Option { + let patches = simulator + .terrain + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let patch_x = (x as usize / 16) as i32; + let patch_y = (y as usize / 16) as i32; + let local_x = x as usize % 16; + let local_y = y as usize % 16; + patches + .iter() + .find(|patch| patch.x == patch_x && patch.y == patch_y) + .and_then(|patch| patch.data.get(local_y * 16 + local_x)) + .copied() +} + +#[allow(clippy::cast_precision_loss)] // Terrain patch indexes are small region coordinates. +fn scenic_location(simulator: &libremetaverse::Simulator) -> Option<(Vector3, Vector3)> { + let roots = simulator + .objects_primitives + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .values() + .filter(|prim| prim.parent_id == 0) + .cloned() + .collect::>(); + let patches = simulator + .terrain + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner); + patches + .iter() + .filter(|patch| patch.data.len() == 256) + .filter_map(|patch| { + let position = Vector3 { + x: patch.x as f32 * 16.0 + 8.0, + y: patch.y as f32 * 16.0 + 8.0, + z: patch.data[8 * 16 + 8] + 2.0, + }; + if roots.iter().any(|prim| { + let extent = prim.scale.x.max(prim.scale.y).max(prim.scale.z); + let dx = prim.position.x - position.x; + let dy = prim.position.y - position.y; + let dz = prim.position.z - position.z; + (dx * dx + dy * dy + dz * dz).sqrt() < extent * 0.5 + 3.0 + }) { + return None; + } + let mut score = 0.0_f32; + let mut forward = Vector3::zero(); + for prim in &roots { + let dx = prim.position.x - position.x; + let dy = prim.position.y - position.y; + let horizontal = (dx * dx + dy * dy).sqrt(); + let dz = prim.position.z - position.z; + let extent = prim.scale.x.max(prim.scale.y).max(prim.scale.z); + if !(10.0..=64.0).contains(&horizontal) + || dz.abs() > 20.0 + || !(2.0..=32.0).contains(&extent) + { + continue; + } + let weight = extent.min(10.0) / horizontal; + score += extent.min(10.0); + forward.x += dx * weight; + forward.y += dy * weight; + forward.z += dz.clamp(-horizontal * 0.35, horizontal * 0.35) * weight; + } + (score > 0.0).then_some((score, position, forward)) + }) + .max_by(|left, right| left.0.total_cmp(&right.0)) + .map(|(_, position, forward)| (position, forward)) +} + +fn avatar_camera(avatar: Vector3, forward: Vector3) -> Result<(Vector3, Vector3), Box> { + let length = (forward.x * forward.x + forward.y * forward.y + forward.z * forward.z).sqrt(); + if !length.is_finite() || length < f32::EPSILON { + return Err("renderer start direction unavailable".into()); + } + let position = Vector3 { + x: avatar.x, + y: avatar.y, + z: avatar.z + 2.0, + }; + Ok(( + position, + Vector3 { + x: position.x + forward.x / length * 30.0, + y: position.y + forward.y / length * 30.0, + z: position.z + forward.z / length * 30.0, + }, + )) +} diff --git a/crates/metacrate-rendering-wgpu/Cargo.toml b/crates/metacrate-rendering-wgpu/Cargo.toml index 18ab200..71e61c8 100644 --- a/crates/metacrate-rendering-wgpu/Cargo.toml +++ b/crates/metacrate-rendering-wgpu/Cargo.toml @@ -9,11 +9,13 @@ description = "Reusable headless wgpu scene renderer and projection camera" publish = false [dependencies] -wgpu = { version = "30.0.1", default-features = false, features = ["std", "dx12", "metal", "gles", "vulkan", "wgsl"], optional = true } +bevy = { version = "0.19.1", default-features = false, features = ["3d_bevy_render", "default_app", "multi_threaded", "pbr_specular_textures"], optional = true } [features] default = [] -wgpu = ["dep:wgpu"] +bevy-engine = ["dep:bevy"] +wgpu = ["bevy-engine"] +windowed = ["bevy-engine", "bevy/bevy_winit", "bevy/wayland", "bevy/x11"] [lints] workspace = true diff --git a/crates/metacrate-rendering-wgpu/src/gpu.rs b/crates/metacrate-rendering-wgpu/src/gpu.rs new file mode 100644 index 0000000..ed3ab7c --- /dev/null +++ b/crates/metacrate-rendering-wgpu/src/gpu.rs @@ -0,0 +1,1100 @@ +use super::{ + AlphaMode as SceneAlphaMode, BlinnPhongMaterial, Camera, Material, PbrMaterial, RenderError, + RenderLimits, Renderable, Texture, +}; +use bevy::{ + app::SubApps, + asset::RenderAssetUsages, + camera::{PerspectiveProjection, Projection, RenderTarget}, + image::{ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor}, + material::AlphaMode, + mesh::{Indices, MeshVertexBufferLayoutRef, PrimitiveTopology}, + pbr::{ExtendedMaterial, MaterialExtension, MaterialPipeline, MaterialPipelineKey}, + prelude::*, + reflect::TypePath, + render::{ + RenderPlugin, + render_resource::{ + AsBindGroup, Extent3d, PollType, RenderPipelineDescriptor, ShaderType, + SpecializedMeshPipelineError, TextureDimension, TextureFormat, TextureUsages, + }, + renderer::RenderDevice, + view::screenshot::{Screenshot, ScreenshotCaptured}, + }, + shader::{Shader, ShaderRef}, + window::ExitCondition, +}; +use std::{ + collections::HashMap, + hash::{Hash, Hasher}, + marker::PhantomData, + sync::{Mutex, mpsc}, + thread, + time::Duration, +}; + +enum RenderCommand { + Frame { + camera: Camera, + renderables: Vec, + textures: Vec, + background_srgb: [u8; 4], + limits: RenderLimits, + reply: mpsc::SyncSender, RenderError>>, + }, + Stop, +} + +/// Reusable renderer facade. Bevy and its ECS stay on a dedicated OS thread. +pub struct Renderer { + commands: mpsc::SyncSender, + thread: Mutex>>, +} + +impl Renderer { + pub fn new_blocking() -> Result { + let (commands, receiver) = mpsc::sync_channel(1); + let (ready_sender, ready_receiver) = mpsc::sync_channel(1); + let handle = thread::Builder::new() + .name("metacrate-bevy-renderer".into()) + .spawn(move || { + let backend = BevyBackend::new(); + match backend { + Ok(mut backend) => { + let _ = ready_sender.send(Ok(())); + while let Ok(command) = receiver.recv() { + match command { + RenderCommand::Frame { + camera, + renderables, + textures, + background_srgb, + limits, + reply, + } => { + let _ = reply.send(backend.render( + camera, + &renderables, + &textures, + background_srgb, + limits, + )); + } + RenderCommand::Stop => break, + } + } + } + Err(error) => { + let _ = ready_sender.send(Err(error)); + } + } + }) + .map_err(|error| RenderError::Device(error.to_string()))?; + ready_receiver + .recv_timeout(Duration::from_secs(30)) + .map_err(|_| RenderError::TimedOut)??; + Ok(Self { + commands, + thread: Mutex::new(Some(handle)), + }) + } + + pub fn render( + &self, + camera: Camera, + renderables: &[Renderable], + textures: &[Texture], + background_srgb: [u8; 4], + limits: RenderLimits, + ) -> Result, RenderError> { + validate_scene(camera, renderables, textures, limits)?; + let (sender, receiver) = mpsc::sync_channel(1); + self.commands + .try_send(RenderCommand::Frame { + camera, + renderables: renderables.to_vec(), + textures: textures.to_vec(), + background_srgb, + limits, + reply: sender, + }) + .map_err(|error| match error { + mpsc::TrySendError::Full(_) => RenderError::TimedOut, + mpsc::TrySendError::Disconnected(_) => { + RenderError::Device("renderer thread stopped".into()) + } + })?; + receiver + .recv_timeout(Duration::from_mins(2)) + .map_err(|_| RenderError::TimedOut)? + } +} + +impl Drop for Renderer { + fn drop(&mut self) { + let stop_accepted = self.commands.try_send(RenderCommand::Stop).is_ok(); + let handle = self + .thread + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take(); + if stop_accepted && let Some(handle) = handle { + let _ = handle.join(); + } + } +} + +#[derive(Component)] +struct FrameEntity; + +struct LegacyMaterialPlugin; +struct PbrMaterialPlugin; + +const LEGACY_SHADER_HANDLE: Handle = Handle::Uuid( + bevy::asset::uuid::Uuid::from_u128(0x1b31_1fe4_6b1b_47e1_9a4b_1018_c8e7_094f), + PhantomData, +); +const PBR_SHADER_HANDLE: Handle = Handle::Uuid( + bevy::asset::uuid::Uuid::from_u128(0xb5c9_1d3a_a566_449f_b864_b3c0_d5f9_e684), + PhantomData, +); + +impl Plugin for LegacyMaterialPlugin { + fn build(&self, app: &mut App) { + bevy::asset::load_internal_asset!( + app, + LEGACY_SHADER_HANDLE, + "legacy_material.wgsl", + Shader::from_wgsl + ); + app.add_plugins(MaterialPlugin::::default()); + } +} + +impl Plugin for PbrMaterialPlugin { + fn build(&self, app: &mut App) { + bevy::asset::load_internal_asset!( + app, + PBR_SHADER_HANDLE, + "pbr_material.wgsl", + Shader::from_wgsl + ); + app.add_plugins(MaterialPlugin::::default()); + } +} + +#[derive(Clone, Copy, Debug, ShaderType)] +struct LegacyUniform { + diffuse_color: LinearRgba, + specular_color: LinearRgba, + diffuse_scale_offset: Vec4, + normal_scale_offset: Vec4, + specular_scale_offset: Vec4, + rotations: Vec4, + surface: Vec4, + camera_position: Vec4, +} + +#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +struct LegacyBevyMaterial { + #[uniform(0)] + uniform: LegacyUniform, + #[texture(1)] + #[sampler(2)] + diffuse: Option>, + #[texture(3)] + #[sampler(4)] + normal: Option>, + #[texture(5)] + #[sampler(6)] + specular: Option>, + alpha_mode: AlphaMode, +} + +impl bevy::pbr::Material for LegacyBevyMaterial { + fn fragment_shader() -> ShaderRef { + LEGACY_SHADER_HANDLE.into() + } + + fn alpha_mode(&self) -> AlphaMode { + self.alpha_mode + } + + fn specialize( + _pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayoutRef, + _key: MaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + descriptor.primitive.cull_mode = None; + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, ShaderType)] +struct PbrUniform { + base_scale_offset: Vec4, + normal_scale_offset: Vec4, + metallic_roughness_scale_offset: Vec4, + emissive_scale_offset: Vec4, + rotations: Vec4, + maps: Vec4, +} + +#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +struct PbrExtension { + #[uniform(100)] + uniform: PbrUniform, + #[texture(101)] + #[sampler(102)] + base_color: Option>, + #[texture(103)] + #[sampler(104)] + normal: Option>, + #[texture(105)] + #[sampler(106)] + metallic_roughness: Option>, + #[texture(107)] + #[sampler(108)] + emissive: Option>, +} + +impl MaterialExtension for PbrExtension { + fn fragment_shader() -> ShaderRef { + PBR_SHADER_HANDLE.into() + } +} + +type PbrBevyMaterial = ExtendedMaterial; + +#[derive(Clone)] +struct GpuTexture { + srgb: Handle, + linear: Handle, +} + +struct CachedGpuTexture { + source: Texture, + gpu: GpuTexture, +} + +struct CachedGpuMesh { + source: super::Mesh, + gpu: Handle, +} + +struct BevyBackend { + apps: SubApps, + frame_entities: Vec, + texture_handles: Vec, + texture_cache: HashMap>, + mesh_cache: HashMap>, + pbr_handles: Vec>, + legacy_handles: Vec>, + target: Option>, +} + +impl BevyBackend { + fn new() -> Result { + let render_plugin = RenderPlugin { + synchronous_pipeline_compilation: true, + ..default() + }; + let window_plugin = WindowPlugin { + primary_window: None, + exit_condition: ExitCondition::DontExit, + ..default() + }; + let mut app = App::new(); + let plugins = DefaultPlugins + .set(window_plugin) + .set(render_plugin) + .set(ImagePlugin::default_linear()); + #[cfg(feature = "windowed")] + let plugins = plugins.disable::(); + app.add_plugins(plugins) + .add_plugins((LegacyMaterialPlugin, PbrMaterialPlugin)); + app.finish(); + app.cleanup(); + if !app.world().contains_resource::() { + return Err(RenderError::AdapterUnavailable); + } + Ok(Self { + apps: std::mem::take(app.sub_apps_mut()), + frame_entities: Vec::new(), + texture_handles: Vec::new(), + texture_cache: HashMap::new(), + mesh_cache: HashMap::new(), + pbr_handles: Vec::new(), + legacy_handles: Vec::new(), + target: None, + }) + } + + #[allow(clippy::too_many_lines)] + fn render( + &mut self, + camera: Camera, + renderables: &[Renderable], + textures: &[Texture], + background_srgb: [u8; 4], + limits: RenderLimits, + ) -> Result, RenderError> { + self.clear_frame(); + self.apps + .main + .world_mut() + .insert_resource(ClearColor(Color::srgba_u8( + background_srgb[0], + background_srgb[1], + background_srgb[2], + background_srgb[3], + ))); + self.texture_handles = add_textures( + self.apps.main.world_mut(), + textures, + &mut self.texture_cache, + ); + let mesh_handles = add_meshes( + self.apps.main.world_mut(), + renderables, + &mut self.mesh_cache, + )?; + for (renderable, mesh) in renderables.iter().zip(mesh_handles) { + let entity = match &renderable.material { + Material::BlinnPhong(material) => { + let material = self + .apps + .main + .world_mut() + .resource_mut::>() + .add(legacy_as_bevy(material, &self.texture_handles, camera)); + let entity = self + .apps + .main + .world_mut() + .spawn(( + Mesh3d(mesh.clone()), + MeshMaterial3d(material.clone()), + FrameEntity, + )) + .id(); + self.legacy_handles.push(material); + entity + } + Material::Pbr(material) => { + let material = self + .apps + .main + .world_mut() + .resource_mut::>() + .add(pbr_as_bevy(material, &self.texture_handles)); + let entity = self + .apps + .main + .world_mut() + .spawn(( + Mesh3d(mesh.clone()), + MeshMaterial3d(material.clone()), + FrameEntity, + )) + .id(); + self.pbr_handles.push(material); + entity + } + }; + self.frame_entities.push(entity); + } + let target = self.new_target(limits.width, limits.height); + let transform = camera_transform(camera)?; + let camera_entity = self + .apps + .main + .world_mut() + .spawn(( + Camera3d::default(), + target.clone(), + Projection::Perspective(PerspectiveProjection { + fov: camera.vertical_fov_degrees.to_radians(), + near: 0.05, + #[allow(clippy::cast_precision_loss)] + far: limits.far_distance as f32, + ..Default::default() + }), + transform, + FrameEntity, + )) + .id(); + self.frame_entities.push(camera_entity); + let light = self + .apps + .main + .world_mut() + .spawn(( + DirectionalLight { + illuminance: 12_000.0, + shadow_maps_enabled: false, + ..Default::default() + }, + Transform::from_xyz(40.0, 80.0, 30.0).looking_at(Vec3::ZERO, Vec3::Y), + FrameEntity, + )) + .id(); + self.frame_entities.push(light); + self.capture(&target) + } + + fn new_target(&mut self, width: u32, height: u32) -> RenderTarget { + if let Some(old) = self.target.take() { + self.apps + .main + .world_mut() + .resource_mut::>() + .remove(&old); + } + let mut image = Image::new_uninit( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::RENDER_WORLD, + ); + image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; + let handle = self + .apps + .main + .world_mut() + .resource_mut::>() + .add(image); + self.target = Some(handle.clone()); + handle.into() + } + + fn capture(&mut self, target: &RenderTarget) -> Result, RenderError> { + for _ in 0..2 { + self.update_and_wait()?; + } + let image = target.as_image().ok_or(RenderError::InvalidScene)?.clone(); + let (sender, receiver) = mpsc::sync_channel(1); + let screenshot = self + .apps + .main + .world_mut() + .spawn(Screenshot::image(image)) + .observe(move |event: On| { + if let Some(bytes) = &event.image.data { + let _ = sender.try_send(bytes.clone()); + } + }) + .id(); + self.frame_entities.push(screenshot); + for _ in 0..8 { + self.update_and_wait()?; + if let Ok(bytes) = receiver.try_recv() { + return Ok(bytes); + } + } + Err(RenderError::Readback) + } + + fn update_and_wait(&mut self) -> Result<(), RenderError> { + self.apps.update(); + self.apps + .main + .world() + .resource::() + .wgpu_device() + .poll(PollType::Wait { + submission_index: None, + timeout: Some(Duration::from_secs(10)), + }) + .map(|_| ()) + .map_err(|_| RenderError::TimedOut) + } + + fn clear_frame(&mut self) { + for entity in self.frame_entities.drain(..) { + let _ = self.apps.main.world_mut().despawn(entity); + } + let world = self.apps.main.world_mut(); + self.texture_handles.clear(); + let mut materials = world.resource_mut::>(); + for handle in self.pbr_handles.drain(..) { + materials.remove(&handle); + } + let mut materials = world.resource_mut::>(); + for handle in self.legacy_handles.drain(..) { + materials.remove(&handle); + } + } +} + +fn add_textures( + world: &mut World, + textures: &[Texture], + cache: &mut HashMap>, +) -> Vec { + let mut assets = world.resource_mut::>(); + let mut next = HashMap::with_capacity(textures.len()); + let handles = textures + .iter() + .map(|texture| { + let key = texture_key(texture); + if let Some(cached) = next.get(&key).and_then(|bucket: &Vec| { + bucket.iter().find(|cached| cached.source == *texture) + }) { + return cached.gpu.clone(); + } + if let Some(cached) = cache.get_mut(&key).and_then(|bucket| { + bucket + .iter() + .position(|cached| cached.source == *texture) + .map(|position| bucket.swap_remove(position)) + }) { + let gpu = cached.gpu.clone(); + next.entry(key).or_default().push(cached); + return gpu; + } + let (rgba, mip_level_count) = rgba_mip_chain(texture); + let image = |format| { + let mut image = Image::new_uninit( + Extent3d { + width: texture.width, + height: texture.height, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + format, + RenderAssetUsages::RENDER_WORLD, + ); + image.data = Some(rgba.clone()); + image.texture_descriptor.mip_level_count = mip_level_count; + image + }; + let sampler = || { + ImageSampler::Descriptor(ImageSamplerDescriptor { + address_mode_u: ImageAddressMode::Repeat, + address_mode_v: ImageAddressMode::Repeat, + address_mode_w: ImageAddressMode::Repeat, + mag_filter: ImageFilterMode::Linear, + min_filter: ImageFilterMode::Linear, + mipmap_filter: ImageFilterMode::Linear, + ..Default::default() + }) + }; + let mut srgb = image(TextureFormat::Rgba8UnormSrgb); + srgb.sampler = sampler(); + let mut linear = image(TextureFormat::Rgba8Unorm); + linear.sampler = sampler(); + let gpu = GpuTexture { + srgb: assets.add(srgb), + linear: assets.add(linear), + }; + next.entry(key).or_default().push(CachedGpuTexture { + source: texture.clone(), + gpu: gpu.clone(), + }); + gpu + }) + .collect(); + for bucket in cache.drain().map(|(_, bucket)| bucket) { + for cached in bucket { + assets.remove(&cached.gpu.srgb); + assets.remove(&cached.gpu.linear); + } + } + *cache = next; + handles +} + +fn texture_key(texture: &Texture) -> u64 { + let mut hash = std::collections::hash_map::DefaultHasher::new(); + texture.width.hash(&mut hash); + texture.height.hash(&mut hash); + texture.rgba.hash(&mut hash); + hash.finish() +} + +fn rgba_mip_chain(texture: &Texture) -> (Vec, u32) { + let mut width = texture.width as usize; + let mut height = texture.height as usize; + let mut level = texture.rgba.clone(); + let mut output = level.clone(); + let mut levels = 1_u32; + while width > 1 || height > 1 { + let next_width = width.div_ceil(2); + let next_height = height.div_ceil(2); + let mut next = vec![0_u8; next_width * next_height * 4]; + for y in 0..next_height { + for x in 0..next_width { + let mut channels = [0_u32; 4]; + let mut samples = 0_u32; + for source_y in y * 2..(y * 2 + 2).min(height) { + for source_x in x * 2..(x * 2 + 2).min(width) { + let source = (source_y * width + source_x) * 4; + for channel in 0..4 { + channels[channel] += u32::from(level[source + channel]); + } + samples += 1; + } + } + let target = (y * next_width + x) * 4; + for channel in 0..4 { + next[target + channel] = u8::try_from(channels[channel] / samples) + .expect("the average of bytes fits in a byte"); + } + } + } + output.extend_from_slice(&next); + level = next; + width = next_width; + height = next_height; + levels += 1; + } + (output, levels) +} + +fn add_mesh(world: &mut World, renderable: &Renderable) -> Result, RenderError> { + let positions = renderable + .mesh + .vertices + .iter() + .map(|vertex| world_vector(vertex.position).to_array()) + .collect::>(); + let normals = renderable + .mesh + .vertices + .iter() + .map(|vertex| world_vector(vertex.normal).normalize_or_zero().to_array()) + .collect::>(); + let tex_coords = renderable + .mesh + .vertices + .iter() + .map(|vertex| vertex.tex_coord) + .collect::>(); + let colors = renderable + .mesh + .vertices + .iter() + .map(|vertex| scene_color(vertex.color_srgb).to_linear().to_f32_array()) + .collect::>(); + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::RENDER_WORLD, + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, tex_coords) + .with_inserted_attribute(Mesh::ATTRIBUTE_COLOR, colors) + .with_inserted_indices(Indices::U32(renderable.mesh.indices.clone())); + mesh.generate_tangents() + .map_err(|_| RenderError::InvalidScene)?; + Ok(world.resource_mut::>().add(mesh)) +} + +fn add_meshes( + world: &mut World, + renderables: &[Renderable], + cache: &mut HashMap>, +) -> Result>, RenderError> { + let mut next = HashMap::with_capacity(renderables.len()); + let mut handles = Vec::with_capacity(renderables.len()); + for renderable in renderables { + let key = mesh_key(&renderable.mesh); + if let Some(cached) = next.get(&key).and_then(|bucket: &Vec| { + bucket + .iter() + .find(|cached| cached.source == renderable.mesh) + }) { + handles.push(cached.gpu.clone()); + continue; + } + if let Some(cached) = cache.get_mut(&key).and_then(|bucket| { + bucket + .iter() + .position(|cached| cached.source == renderable.mesh) + .map(|position| bucket.swap_remove(position)) + }) { + handles.push(cached.gpu.clone()); + next.entry(key).or_default().push(cached); + continue; + } + let gpu = add_mesh(world, renderable)?; + handles.push(gpu.clone()); + next.entry(key).or_default().push(CachedGpuMesh { + source: renderable.mesh.clone(), + gpu, + }); + } + let mut assets = world.resource_mut::>(); + for bucket in cache.drain().map(|(_, bucket)| bucket) { + for cached in bucket { + assets.remove(&cached.gpu); + } + } + *cache = next; + Ok(handles) +} + +fn mesh_key(mesh: &super::Mesh) -> u64 { + let mut hash = std::collections::hash_map::DefaultHasher::new(); + for vertex in &mesh.vertices { + for value in vertex + .position + .iter() + .chain(&vertex.normal) + .chain(&vertex.tex_coord) + { + value.to_bits().hash(&mut hash); + } + vertex.color_srgb.hash(&mut hash); + } + mesh.indices.hash(&mut hash); + hash.finish() +} + +fn legacy_as_bevy( + material: &BlinnPhongMaterial, + textures: &[GpuTexture], + camera: Camera, +) -> LegacyBevyMaterial { + let transform = |slot: super::TextureSlot| { + Vec4::new( + slot.transform.scale[0], + slot.transform.scale[1], + slot.transform.offset[0], + slot.transform.offset[1], + ) + }; + let cutoff = match material.alpha_mode { + SceneAlphaMode::Mask { cutoff } => cutoff.clamp(0.0, 1.0), + _ => -1.0, + }; + LegacyBevyMaterial { + uniform: LegacyUniform { + diffuse_color: scene_color(material.diffuse_color_srgb).to_linear(), + specular_color: Color::srgb_u8( + material.specular_color_srgb[0], + material.specular_color_srgb[1], + material.specular_color_srgb[2], + ) + .to_linear(), + diffuse_scale_offset: transform(material.diffuse), + normal_scale_offset: transform(material.normal), + specular_scale_offset: transform(material.specular), + rotations: Vec4::new( + material.diffuse.transform.rotation_radians, + material.normal.transform.rotation_radians, + material.specular.transform.rotation_radians, + material.shininess.max(1.0), + ), + surface: Vec4::new( + material.environment_intensity.clamp(0.0, 1.0), + if material.fullbright { 1.0 } else { 0.0 }, + if material.normal.texture.is_some() { + 1.0 + } else { + 0.0 + }, + cutoff, + ), + camera_position: world_vector(camera.position).extend(1.0), + }, + diffuse: srgb_texture(material.diffuse.texture, textures), + normal: linear_texture(material.normal.texture, textures), + specular: srgb_texture(material.specular.texture, textures), + alpha_mode: alpha_mode(material.alpha_mode), + } +} + +fn pbr_as_bevy(material: &PbrMaterial, textures: &[GpuTexture]) -> PbrBevyMaterial { + let transform = |slot: super::TextureSlot| { + Vec4::new( + slot.transform.scale[0], + slot.transform.scale[1], + slot.transform.offset[0], + slot.transform.offset[1], + ) + }; + PbrBevyMaterial { + base: StandardMaterial { + base_color: scene_color(material.base_color_srgb), + metallic: material.metallic_factor.clamp(0.0, 1.0), + perceptual_roughness: material.roughness_factor.clamp(0.04, 1.0), + emissive: Color::srgb_u8( + material.emissive_factor_srgb[0], + material.emissive_factor_srgb[1], + material.emissive_factor_srgb[2], + ) + .to_linear(), + double_sided: material.double_sided, + cull_mode: (!material.double_sided) + .then_some(bevy::render::render_resource::Face::Back), + alpha_mode: alpha_mode(material.alpha_mode), + ..Default::default() + }, + extension: PbrExtension { + uniform: PbrUniform { + base_scale_offset: transform(material.base_color), + normal_scale_offset: transform(material.normal), + metallic_roughness_scale_offset: transform(material.metallic_roughness), + emissive_scale_offset: transform(material.emissive), + rotations: Vec4::new( + material.base_color.transform.rotation_radians, + material.normal.transform.rotation_radians, + material.metallic_roughness.transform.rotation_radians, + material.emissive.transform.rotation_radians, + ), + maps: Vec4::new( + bool_float(material.base_color.texture.is_some()), + bool_float(material.normal.texture.is_some()), + bool_float(material.metallic_roughness.texture.is_some()), + bool_float(material.emissive.texture.is_some()), + ), + }, + base_color: srgb_texture(material.base_color.texture, textures), + normal: linear_texture(material.normal.texture, textures), + metallic_roughness: linear_texture(material.metallic_roughness.texture, textures), + emissive: srgb_texture(material.emissive.texture, textures), + }, + } +} + +fn srgb_texture(index: Option, textures: &[GpuTexture]) -> Option> { + index.and_then(|index| textures.get(index).map(|texture| texture.srgb.clone())) +} + +fn linear_texture(index: Option, textures: &[GpuTexture]) -> Option> { + index.and_then(|index| textures.get(index).map(|texture| texture.linear.clone())) +} + +const fn bool_float(value: bool) -> f32 { + if value { 1.0 } else { 0.0 } +} + +fn scene_color(color: [u8; 4]) -> Color { + Color::srgba_u8(color[0], color[1], color[2], color[3]) +} + +fn alpha_mode(mode: SceneAlphaMode) -> AlphaMode { + match mode { + SceneAlphaMode::Opaque => AlphaMode::Opaque, + SceneAlphaMode::Mask { cutoff } => AlphaMode::Mask(cutoff.clamp(0.0, 1.0)), + SceneAlphaMode::Blend => AlphaMode::Blend, + } +} + +fn camera_transform(camera: Camera) -> Result { + let forward = world_vector(camera.forward).normalize_or_zero(); + let up = world_vector(camera.up).normalize_or_zero(); + if forward.length_squared() < 0.5 || up.length_squared() < 0.5 { + return Err(RenderError::InvalidScene); + } + Ok(Transform::from_translation(world_vector(camera.position)).looking_to(forward, up)) +} + +fn world_vector(value: [f32; 3]) -> Vec3 { + Vec3::new(value[0], value[2], -value[1]) +} + +fn validate_scene( + camera: Camera, + renderables: &[Renderable], + textures: &[Texture], + limits: RenderLimits, +) -> Result<(), RenderError> { + camera.basis()?; + if limits.width == 0 || limits.height == 0 || limits.far_distance == 0 { + return Err(RenderError::InvalidScene); + } + let triangle_count = renderables + .iter() + .try_fold(0usize, |total, renderable| { + total.checked_add(renderable.mesh.indices.len() / 3) + }) + .ok_or(RenderError::ResourceLimit)?; + let (texture_bytes, texture_pixels) = + textures + .iter() + .try_fold((0usize, 0usize), |(bytes, pixels), texture| { + let count = usize::try_from(texture.width) + .ok() + .and_then(|width| { + usize::try_from(texture.height) + .ok() + .and_then(|height| width.checked_mul(height)) + }) + .ok_or(RenderError::ResourceLimit)?; + if count == 0 || texture.rgba.len() != count.saturating_mul(4) { + return Err(RenderError::InvalidScene); + } + Ok(( + bytes + .checked_add(texture.rgba.len()) + .ok_or(RenderError::ResourceLimit)?, + pixels + .checked_add(count) + .ok_or(RenderError::ResourceLimit)?, + )) + })?; + if triangle_count > limits.max_triangles + || texture_bytes > limits.max_texture_bytes + || texture_pixels > limits.max_texture_pixels + { + return Err(RenderError::ResourceLimit); + } + if renderables.iter().any(|renderable| { + renderable.mesh.indices.iter().any(|index| { + usize::try_from(*index).map_or(true, |index| index >= renderable.mesh.vertices.len()) + }) || renderable.mesh.vertices.iter().any(|vertex| { + vertex + .position + .iter() + .chain(vertex.normal.iter()) + .chain(vertex.tex_coord.iter()) + .any(|value| !value.is_finite()) + }) + }) { + return Err(RenderError::InvalidScene); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Mesh as SceneMesh, TextureSlot, UvTransform, Vertex}; + use std::sync::Arc; + + #[test] + fn mip_chain_reaches_one_pixel_and_averages_rgba() { + let texture = Texture { + width: 2, + height: 2, + rgba: vec![ + 0, 0, 0, 0, 100, 100, 100, 100, 200, 200, 200, 200, 255, 255, 255, 255, + ], + }; + let (mips, levels) = rgba_mip_chain(&texture); + assert_eq!(levels, 2); + assert_eq!(mips.len(), 20); + assert_eq!(&mips[16..], &[138, 138, 138, 138]); + } + + #[test] + fn bevy_headless_renders_repeated_frames_through_the_public_api() { + let renderer = match Renderer::new_blocking() { + Ok(renderer) => Arc::new(renderer), + Err(RenderError::AdapterUnavailable | RenderError::Device(_)) => return, + Err(error) => panic!("unexpected renderer initialization failure: {error}"), + }; + let camera = Camera::look_at([0.0; 3], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0], 60.0).unwrap(); + let frames = [ + renderable(Material::BlinnPhong(BlinnPhongMaterial { + diffuse_color_srgb: [255, 20, 20, 255], + diffuse: TextureSlot { + texture: Some(0), + ..Default::default() + }, + normal: TextureSlot { + texture: Some(1), + ..Default::default() + }, + specular: TextureSlot { + texture: Some(0), + ..Default::default() + }, + fullbright: true, + double_sided: true, + ..Default::default() + })), + renderable(Material::Pbr(PbrMaterial { + base_color_srgb: [20, 40, 255, 255], + base_color: texture_slot(0, 0.2), + normal: texture_slot(1, -0.2), + metallic_roughness: texture_slot(2, 0.3), + emissive: texture_slot(3, -0.3), + metallic_factor: 0.0, + roughness_factor: 0.7, + double_sided: true, + ..Default::default() + })), + ]; + let textures = [ + solid_texture([220, 180, 120, 255]), + solid_texture([128, 128, 255, 255]), + solid_texture([255, 180, 0, 255]), + solid_texture([30, 20, 10, 255]), + ]; + for renderable in frames { + let rgba = renderer + .render( + camera, + &[renderable], + &textures, + [0, 0, 0, 255], + RenderLimits { + width: 96, + height: 64, + max_triangles: 2, + max_texture_bytes: 64, + max_texture_pixels: 16, + far_distance: 64, + }, + ) + .unwrap(); + assert_eq!(rgba.len(), 96 * 64 * 4); + assert!( + rgba.chunks_exact(4).any(|pixel| pixel[..3] != [0, 0, 0]), + "captured only black pixels; channel maxima are {:?}", + (0..4) + .map(|channel| rgba.chunks_exact(4).map(|pixel| pixel[channel]).max()) + .collect::>() + ); + } + } + + fn texture_slot(texture: usize, rotation_radians: f32) -> TextureSlot { + TextureSlot { + texture: Some(texture), + transform: UvTransform { + scale: [1.5, 0.75], + offset: [0.1, -0.1], + rotation_radians, + }, + } + } + + fn solid_texture(pixel: [u8; 4]) -> Texture { + Texture { + width: 2, + height: 2, + rgba: pixel.repeat(4), + } + } + + fn renderable(material: Material) -> Renderable { + let vertex = |position, tex_coord| Vertex { + position, + normal: [-1.0, 0.0, 0.0], + tex_coord, + color_srgb: [255; 4], + }; + Renderable { + mesh: SceneMesh { + vertices: vec![ + vertex([3.0, -0.8, -0.8], [0.0, 0.0]), + vertex([3.0, 0.8, -0.8], [1.0, 0.0]), + vertex([3.0, 0.8, 0.8], [1.0, 1.0]), + vertex([3.0, -0.8, 0.8], [0.0, 1.0]), + ], + indices: vec![0, 1, 2, 0, 2, 3], + }, + material, + } + } +} diff --git a/crates/metacrate-rendering-wgpu/src/legacy_material.wgsl b/crates/metacrate-rendering-wgpu/src/legacy_material.wgsl new file mode 100644 index 0000000..8913c85 --- /dev/null +++ b/crates/metacrate-rendering-wgpu/src/legacy_material.wgsl @@ -0,0 +1,64 @@ +#import bevy_pbr::forward_io::{VertexOutput, FragmentOutput} + +struct LegacyUniform { + diffuse_color: vec4, + specular_color: vec4, + diffuse_scale_offset: vec4, + normal_scale_offset: vec4, + specular_scale_offset: vec4, + rotations: vec4, + surface: vec4, + camera_position: vec4, +} + +@group(#{MATERIAL_BIND_GROUP}) @binding(0) var material: LegacyUniform; +@group(#{MATERIAL_BIND_GROUP}) @binding(1) var diffuse_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(2) var diffuse_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(3) var normal_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(4) var normal_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(5) var specular_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(6) var specular_sampler: sampler; + +fn transformed_uv(uv: vec2, scale_offset: vec4, rotation: f32) -> vec2 { + let centered = (uv - vec2(0.5)) * scale_offset.xy; + let sine = sin(rotation); + let cosine = cos(rotation); + return vec2( + centered.x * cosine - centered.y * sine, + centered.x * sine + centered.y * cosine, + ) + vec2(0.5) + scale_offset.zw; +} + +@fragment +fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput { + let diffuse_uv = transformed_uv(in.uv, material.diffuse_scale_offset, material.rotations.x); + let texel = textureSample(diffuse_texture, diffuse_sampler, diffuse_uv); + var base = material.diffuse_color * texel * in.color; + if material.surface.w >= 0.0 && base.a < material.surface.w { + discard; + } + + var normal = normalize(in.world_normal) * select(-1.0, 1.0, is_front); + if material.surface.z > 0.5 { + let normal_uv = transformed_uv(in.uv, material.normal_scale_offset, material.rotations.y); + let tangent_normal = textureSample(normal_texture, normal_sampler, normal_uv).xyz * 2.0 - 1.0; + let tangent = normalize(in.world_tangent.xyz); + let bitangent = normalize(cross(normal, tangent)) * in.world_tangent.w; + normal = normalize(mat3x3(tangent, bitangent, normal) * tangent_normal); + } + + let light_direction = normalize(vec3(0.35, 0.82, 0.45)); + let view_direction = normalize(material.camera_position.xyz - in.world_position.xyz); + let half_direction = normalize(light_direction + view_direction); + let diffuse_light = max(dot(normal, light_direction), 0.0); + let specular_uv = transformed_uv(in.uv, material.specular_scale_offset, material.rotations.z); + let specular_map = textureSample(specular_texture, specular_sampler, specular_uv).rgb; + let specular_light = pow(max(dot(normal, half_direction), 0.0), material.rotations.w); + let lit = base.rgb * (0.55 + 0.45 * diffuse_light) + + material.specular_color.rgb * specular_map * specular_light + + material.specular_color.rgb * material.surface.x * 0.15; + + var out: FragmentOutput; + out.color = vec4(select(lit, base.rgb, material.surface.y > 0.5), base.a); + return out; +} diff --git a/crates/metacrate-rendering-wgpu/src/lib.rs b/crates/metacrate-rendering-wgpu/src/lib.rs index 7aaad78..66c6a39 100644 --- a/crates/metacrate-rendering-wgpu/src/lib.rs +++ b/crates/metacrate-rendering-wgpu/src/lib.rs @@ -1,4 +1,4 @@ -//! Cross-platform projection camera and headless `wgpu` triangle renderer. +//! Reusable, windowless `OpenSim` scene rendering on stable `wgpu`. #![allow(clippy::missing_errors_doc)] @@ -19,14 +19,9 @@ impl Camera { up: [f32; 3], vertical_fov_degrees: f32, ) -> Result { - let forward = normalize([ - target[0] - position[0], - target[1] - position[1], - target[2] - position[2], - ])?; let camera = Self { position, - forward, + forward: normalize(sub(target, position))?, up: normalize(up)?, vertical_fov_degrees, }; @@ -51,10 +46,149 @@ impl Camera { } } +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Vertex { + pub position: [f32; 3], + pub normal: [f32; 3], + pub tex_coord: [f32; 2], + pub color_srgb: [u8; 4], +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Mesh { + pub vertices: Vec, + pub indices: Vec, +} + #[derive(Clone, Debug, PartialEq)] -pub struct Triangle { - pub vertices: [[f32; 3]; 3], - pub colors_srgb: [[u8; 4]; 3], +pub struct Texture { + pub width: u32, + pub height: u32, + pub rgba: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct UvTransform { + pub scale: [f32; 2], + pub offset: [f32; 2], + pub rotation_radians: f32, +} + +impl Default for UvTransform { + fn default() -> Self { + Self { + scale: [1.0; 2], + offset: [0.0; 2], + rotation_radians: 0.0, + } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct TextureSlot { + pub texture: Option, + pub transform: UvTransform, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum AlphaMode { + #[default] + Opaque, + Mask { + cutoff: f32, + }, + Blend, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BlinnPhongMaterial { + pub diffuse_color_srgb: [u8; 4], + pub diffuse: TextureSlot, + pub normal: TextureSlot, + pub specular: TextureSlot, + pub specular_color_srgb: [u8; 3], + pub shininess: f32, + pub environment_intensity: f32, + pub fullbright: bool, + pub double_sided: bool, + pub alpha_mode: AlphaMode, +} + +impl Default for BlinnPhongMaterial { + fn default() -> Self { + Self { + diffuse_color_srgb: [255; 4], + diffuse: TextureSlot::default(), + normal: TextureSlot::default(), + specular: TextureSlot::default(), + specular_color_srgb: [0; 3], + shininess: 32.0, + environment_intensity: 0.0, + fullbright: false, + double_sided: false, + alpha_mode: AlphaMode::Opaque, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PbrMaterial { + pub base_color_srgb: [u8; 4], + pub base_color: TextureSlot, + pub normal: TextureSlot, + /// glTF convention: roughness in G and metallic in B. + pub metallic_roughness: TextureSlot, + pub emissive: TextureSlot, + pub metallic_factor: f32, + pub roughness_factor: f32, + pub emissive_factor_srgb: [u8; 3], + pub double_sided: bool, + pub alpha_mode: AlphaMode, +} + +impl Default for PbrMaterial { + fn default() -> Self { + Self { + base_color_srgb: [255; 4], + base_color: TextureSlot::default(), + normal: TextureSlot::default(), + metallic_roughness: TextureSlot::default(), + emissive: TextureSlot::default(), + metallic_factor: 1.0, + roughness_factor: 1.0, + emissive_factor_srgb: [0; 3], + double_sided: false, + alpha_mode: AlphaMode::Opaque, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Material { + BlinnPhong(BlinnPhongMaterial), + Pbr(PbrMaterial), +} + +impl Default for Material { + fn default() -> Self { + Self::BlinnPhong(BlinnPhongMaterial::default()) + } +} + +impl Material { + #[must_use] + pub fn alpha_mode(&self) -> AlphaMode { + match self { + Self::BlinnPhong(material) => material.alpha_mode, + Self::Pbr(material) => material.alpha_mode, + } + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Renderable { + pub mesh: Mesh, + pub material: Material, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -62,16 +196,19 @@ pub struct RenderLimits { pub width: u32, pub height: u32, pub max_triangles: usize, + pub max_texture_bytes: usize, + pub max_texture_pixels: usize, pub far_distance: u32, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum RenderError { AdapterUnavailable, InvalidScene, ResourceLimit, TimedOut, Readback, + Device(String), } impl std::fmt::Display for RenderError { @@ -82,18 +219,27 @@ impl std::fmt::Display for RenderError { Self::ResourceLimit => "render resource limit exceeded", Self::TimedOut => "wgpu render timed out", Self::Readback => "wgpu readback failed", - }) + Self::Device(_) => "wgpu device failed", + })?; + if let Self::Device(detail) = self { + write!(formatter, ": {detail}")?; + } + Ok(()) } } impl std::error::Error for RenderError {} +fn sub(left: [f32; 3], right: [f32; 3]) -> [f32; 3] { + std::array::from_fn(|axis| left[axis] - right[axis]) +} + fn normalize(value: [f32; 3]) -> Result<[f32; 3], RenderError> { let length = dot(value, value).sqrt(); - if length < 0.0001 { + if length < 0.0001 || !length.is_finite() { return Err(RenderError::InvalidScene); } - Ok([value[0] / length, value[1] / length, value[2] / length]) + Ok(value.map(|component| component / length)) } fn dot(left: [f32; 3], right: [f32; 3]) -> f32 { @@ -109,318 +255,30 @@ fn cross(left: [f32; 3], right: [f32; 3]) -> [f32; 3] { } #[cfg(feature = "wgpu")] -pub struct Renderer { - device: wgpu::Device, - queue: wgpu::Queue, - pipeline: wgpu::RenderPipeline, -} - +mod gpu; #[cfg(feature = "wgpu")] -const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 2] = - wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x4]; - -#[cfg(feature = "wgpu")] -impl Renderer { - pub async fn new() -> Result { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::LowPower, - ..Default::default() - }) - .await - .map_err(|_| RenderError::AdapterUnavailable)?; - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor { - label: Some("metacrate-rendering-wgpu"), - required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::downlevel_defaults(), - memory_hints: wgpu::MemoryHints::MemoryUsage, - ..Default::default() - }) - .await - .map_err(|_| RenderError::AdapterUnavailable)?; - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("metacrate-rendering-wgpu-shader"), - source: wgpu::ShaderSource::Wgsl( - r" -struct VertexInput { @location(0) position: vec3, @location(1) color: vec4, }; -struct VertexOutput { @builtin(position) position: vec4, @location(0) color: vec4, }; -@vertex fn vs_main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.position = vec4(input.position, 1.0); - output.color = input.color; - return output; -} -@fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { return input.color; } -" - .into(), - ), - }); - let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("metacrate-rendering-wgpu-layout"), - bind_group_layouts: &[], - immediate_size: 0, - }); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("metacrate-rendering-wgpu-pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - compilation_options: wgpu::PipelineCompilationOptions::default(), - buffers: &[Some(wgpu::VertexBufferLayout { - array_stride: 7 * std::mem::size_of::() as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &VERTEX_ATTRIBUTES, - })], - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - cull_mode: None, - ..Default::default() - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: wgpu::TextureFormat::Depth32Float, - depth_write_enabled: Some(true), - depth_compare: Some(wgpu::CompareFunction::Less), - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState::default(), - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - compilation_options: wgpu::PipelineCompilationOptions::default(), - targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba8UnormSrgb, - blend: None, - write_mask: wgpu::ColorWrites::ALL, - })], - }), - multiview_mask: None, - cache: None, - }); - Ok(Self { - device, - queue, - pipeline, - }) - } - - #[allow(clippy::too_many_lines)] - pub fn render( - &self, - camera: Camera, - triangles: &[Triangle], - background_srgb: [u8; 4], - limits: RenderLimits, - ) -> Result, RenderError> { - let (vertices, vertex_count) = gpu_vertices(camera, triangles, limits)?; - let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("metacrate-rendering-wgpu-vertices"), - size: vertices.len().max(4) as u64, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - if !vertices.is_empty() { - self.queue.write_buffer(&vertex_buffer, 0, &vertices); - } - let extent = wgpu::Extent3d { - width: limits.width, - height: limits.height, - depth_or_array_layers: 1, - }; - let color = self.device.create_texture(&wgpu::TextureDescriptor { - label: Some("metacrate-rendering-wgpu-color"), - size: extent, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], - }); - let depth = self.device.create_texture(&wgpu::TextureDescriptor { - label: Some("metacrate-rendering-wgpu-depth"), - size: extent, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Depth32Float, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }); - let row_bytes = limits - .width - .checked_mul(4) - .ok_or(RenderError::ResourceLimit)?; - let padded_row_bytes = row_bytes.div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) - * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; - let readback = self.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("metacrate-rendering-wgpu-readback"), - size: u64::from(padded_row_bytes) - .checked_mul(u64::from(limits.height)) - .ok_or(RenderError::ResourceLimit)?, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }); - let color_view = color.create_view(&wgpu::TextureViewDescriptor::default()); - let depth_view = depth.create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("metacrate-rendering-wgpu-commands"), - }); - { - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("metacrate-rendering-wgpu-pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &color_view, - depth_slice: None, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: f64::from(background_srgb[0]) / 255.0, - g: f64::from(background_srgb[1]) / 255.0, - b: f64::from(background_srgb[2]) / 255.0, - a: f64::from(background_srgb[3]) / 255.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &depth_view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: wgpu::StoreOp::Discard, - }), - stencil_ops: None, - }), - ..Default::default() - }); - if vertex_count != 0 { - pass.set_pipeline(&self.pipeline); - pass.set_vertex_buffer(0, vertex_buffer.slice(..)); - pass.draw(0..vertex_count, 0..1); - } - } - encoder.copy_texture_to_buffer( - wgpu::TexelCopyTextureInfo { - texture: &color, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyBufferInfo { - buffer: &readback, - layout: wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(padded_row_bytes), - rows_per_image: Some(limits.height), - }, - }, - extent, - ); - let submission = self.queue.submit([encoder.finish()]); - let slice = readback.slice(..); - let (sender, receiver) = std::sync::mpsc::sync_channel(1); - slice.map_async(wgpu::MapMode::Read, move |result| { - let _ = sender.send(result); - }); - self.device - .poll(wgpu::PollType::Wait { - submission_index: Some(submission), - timeout: Some(std::time::Duration::from_secs(5)), - }) - .map_err(|_| RenderError::TimedOut)?; - receiver - .recv_timeout(std::time::Duration::from_secs(1)) - .map_err(|_| RenderError::TimedOut)? - .map_err(|_| RenderError::Readback)?; - let mapped = slice - .get_mapped_range() - .map_err(|_| RenderError::Readback)?; - let mut rgba = Vec::with_capacity(row_bytes as usize * limits.height as usize); - for row in mapped - .chunks_exact(padded_row_bytes as usize) - .take(limits.height as usize) - { - rgba.extend_from_slice(&row[..row_bytes as usize]); - } - drop(mapped); - readback.unmap(); - Ok(rgba) - } -} - -#[cfg(feature = "wgpu")] -#[allow(clippy::cast_precision_loss)] // Render dimensions and distance are bounded well below f32's exact integer range. -fn gpu_vertices( - camera: Camera, - triangles: &[Triangle], - limits: RenderLimits, -) -> Result<(Vec, u32), RenderError> { - if limits.width == 0 - || limits.height == 0 - || limits.far_distance == 0 - || triangles.len() > limits.max_triangles - || triangles - .iter() - .flat_map(|triangle| triangle.vertices.iter().flatten()) - .any(|value| !value.is_finite()) - { - return Err(RenderError::InvalidScene); - } - let basis = camera.basis()?; - let aspect = limits.width as f32 / limits.height as f32; - let focal = 1.0 / (camera.vertical_fov_degrees.to_radians() / 2.0).tan(); - let mut bytes = Vec::with_capacity(triangles.len() * 3 * 7 * std::mem::size_of::()); - let mut count = 0u32; - for triangle in triangles { - let projected = triangle.vertices.map(|world| { - let delta = [ - world[0] - camera.position[0], - world[1] - camera.position[1], - world[2] - camera.position[2], - ]; - let z = dot(delta, basis.2); - [ - dot(delta, basis.0) * focal / aspect / z, - dot(delta, basis.1) * focal / z, - (z / limits.far_distance as f32).clamp(0.0, 1.0), - z, - ] - }); - if projected.iter().any(|vertex| vertex[3] <= 0.05) { - continue; - } - for (vertex, color) in projected.into_iter().zip(triangle.colors_srgb) { - let color = color.map(|channel| f32::from(channel) / 255.0); - for value in vertex[..3].iter().chain(color.iter()) { - bytes.extend_from_slice(&value.to_ne_bytes()); - } - count = count.checked_add(1).ok_or(RenderError::ResourceLimit)?; - } - } - Ok((bytes, count)) -} +pub use gpu::Renderer; #[cfg(test)] mod tests { use super::*; #[test] - fn look_at_builds_a_valid_projection_camera() { + fn camera_and_both_material_models_are_explicit() { let camera = Camera::look_at([1.0, 2.0, 3.0], [5.0, 2.0, 3.0], [0.0, 0.0, 1.0], 60.0).unwrap(); assert!( camera .forward - .into_iter() + .iter() .zip([1.0, 0.0, 0.0]) .all(|(actual, expected)| (actual - expected).abs() < f32::EPSILON) ); assert!(Camera::look_at([0.0; 3], [0.0; 3], [0.0, 0.0, 1.0], 60.0).is_err()); + assert!(matches!(Material::default(), Material::BlinnPhong(_))); + assert!(matches!( + Material::Pbr(PbrMaterial::default()), + Material::Pbr(_) + )); } } diff --git a/crates/metacrate-rendering-wgpu/src/pbr_material.wgsl b/crates/metacrate-rendering-wgpu/src/pbr_material.wgsl new file mode 100644 index 0000000..2d692bc --- /dev/null +++ b/crates/metacrate-rendering-wgpu/src/pbr_material.wgsl @@ -0,0 +1,85 @@ +#import bevy_pbr::{ + forward_io::{VertexOutput, FragmentOutput}, + pbr_fragment::pbr_input_from_standard_material, + pbr_functions::{alpha_discard, apply_normal_mapping, apply_pbr_lighting, calculate_tbn_mikktspace, main_pass_post_lighting_processing}, + pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT, +} + +struct PbrExtension { + base_scale_offset: vec4, + normal_scale_offset: vec4, + metallic_roughness_scale_offset: vec4, + emissive_scale_offset: vec4, + rotations: vec4, + maps: vec4, +} + +@group(#{MATERIAL_BIND_GROUP}) @binding(100) var extension: PbrExtension; +@group(#{MATERIAL_BIND_GROUP}) @binding(101) var base_color_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(102) var base_color_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(103) var normal_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(104) var normal_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(105) var metallic_roughness_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(106) var metallic_roughness_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(107) var emissive_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(108) var emissive_sampler: sampler; + +fn transformed_uv(uv: vec2, scale_offset: vec4, rotation: f32) -> vec2 { + let centered = (uv - vec2(0.5)) * scale_offset.xy; + let sine = sin(rotation); + let cosine = cos(rotation); + return vec2( + centered.x * cosine - centered.y * sine, + centered.x * sine + centered.y * cosine, + ) + vec2(0.5) + scale_offset.zw; +} + +@fragment +fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput { + var pbr_input = pbr_input_from_standard_material(in, is_front); + + if extension.maps.x > 0.5 { + let uv = transformed_uv(in.uv, extension.base_scale_offset, extension.rotations.x); + pbr_input.material.base_color *= textureSample(base_color_texture, base_color_sampler, uv); + } + if extension.maps.y > 0.5 { + let uv = transformed_uv(in.uv, extension.normal_scale_offset, extension.rotations.y); + let sampled_normal = textureSample(normal_texture, normal_sampler, uv).rgb; + let tbn = calculate_tbn_mikktspace(pbr_input.world_normal, in.world_tangent); + let double_sided = (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u; + pbr_input.N = apply_normal_mapping( + pbr_input.material.flags, + tbn, + double_sided, + is_front, + sampled_normal, + ); + } + if extension.maps.z > 0.5 { + let uv = transformed_uv( + in.uv, + extension.metallic_roughness_scale_offset, + extension.rotations.z, + ); + let metallic_roughness = textureSample( + metallic_roughness_texture, + metallic_roughness_sampler, + uv, + ); + pbr_input.material.perceptual_roughness *= metallic_roughness.g; + pbr_input.material.metallic *= metallic_roughness.b; + } + if extension.maps.w > 0.5 { + let uv = transformed_uv(in.uv, extension.emissive_scale_offset, extension.rotations.w); + pbr_input.material.emissive *= textureSample(emissive_texture, emissive_sampler, uv); + } + + pbr_input.material.base_color = alpha_discard( + pbr_input.material, + pbr_input.material.base_color, + ); + var out: FragmentOutput; + out.color = apply_pbr_lighting(pbr_input); + out.color = main_pass_post_lighting_processing(pbr_input, out.color); + return out; +} diff --git a/docs/grid-agent-llm.md b/docs/grid-agent-llm.md index 987d4f3..796c329 100644 --- a/docs/grid-agent-llm.md +++ b/docs/grid-agent-llm.md @@ -57,21 +57,31 @@ facility, not a requirement for autonomous operation. ## Images Vision captures are reconstructed viewport images rather than screenshots. -The live service keeps one headless `wgpu` device, renders the current scene to -an offscreen color/depth target, reads the frame back, encodes it directly as -JPEG, and attaches it as a Mentra image content block. GPU setup and readback -run outside the simulator event path. If no compatible adapter is available or -a frame fails, the same scene is rendered by the deterministic software -rasterizer. The default 320x180 frame, quality, entity, triangle, texture-fetch, -decoded-pixel, byte, rate, and concurrency limits are validated before runtime. -JPEG avoids the much larger PNG payloads. +`metacrate-rendering-wgpu` owns a dedicated-thread, headless Bevy/wgpu device, +renders the current scene to an offscreen color/depth target, reads the frame +back, encodes it directly as JPEG, and attaches it as a Mentra image content +block. GPU setup, scene conversion, and readback run outside the simulator event +path. If no compatible adapter is available or a frame fails, the deterministic +software rasterizer handles the same snapshot. The default 640x360 frame and +the entity, triangle, texture, JPEG, time, and concurrency safety ceilings are +validated before runtime. JPEG avoids the much larger PNG payloads. -The current renderer handles legacy prim geometry, approximate texture colors, -simple avatars, and flat terrain/water. `behavior_camera_set` gives an -authorized model a bounded position, target, and vertical field of view; -`behavior_camera_reset` restores the avatar-facing view. Viewer-grade -mesh/sculpt rendering, full UV materials, lighting, authoritative terrain, and -avatar/attachment fidelity remain tracked in the renderer issue. +The live source reconstructs authoritative terrain heights and detail textures, +legacy prims, sculpt maps, uploaded meshes, linksets, legacy Blinn-Phong and +PBR materials, independent UV transforms, water, and privacy-marked avatar +cards. It keeps bounded decoded-texture and geometry caches per region; Bevy +also retains unchanged GPU textures and meshes between frames. Textures are +decoded through the pure-Rust JPEG-2000 path, reduced to viewport-appropriate +resolution, and uploaded with filtered mip chains. The view radius is +configurable as `vision.max_distance_meters` and defaults to 64 meters. World +objects are selected by that distance; avatar attachments are rigged through +their linkset hierarchy and included only when the camera is focused on the +avatar, after world-object capacity is reserved. + +`behavior_camera_set` gives an authorized model a bounded position, target, and +vertical field of view; `behavior_camera_reset` restores the avatar-facing +view. Full avatar body/attachment mesh fidelity and baking remain separately +tracked work. ## Bounds and cancellation diff --git a/docs/grid-agent-operations.md b/docs/grid-agent-operations.md index 2263efc..f48ca57 100644 --- a/docs/grid-agent-operations.md +++ b/docs/grid-agent-operations.md @@ -226,9 +226,10 @@ The example records all current queue, message, conversation, tool, behavior, reconnect, and interaction defaults. Important defaults include 256 grid events, 32 control commands, 512 observations, four concurrent inference requests, 16 tool calls, 512 active senders/sessions, a two-minute model window, and bounded -10-second shutdown. Vision defaults to a 320x180 headless-wgpu JPEG with a -deterministic software fallback and bounded entities, triangles, texture work, -JPEG bytes, time, and concurrency. Authorized `behavior_camera_set` calls are +10-second shutdown. Vision defaults to a 640x360 headless Bevy/wgpu JPEG, a +64-meter configurable view radius, a persistent 512 MiB decoded-texture cache, +and a deterministic software fallback with bounded entities, triangles, asset +work, JPEG bytes, time, and concurrency. Authorized `behavior_camera_set` calls are limited to 96 meters from the avatar by default, require a distinct target, and accept a 20-120 degree vertical field of view; `behavior_camera_reset` restores the avatar-facing 60-degree view.