{"article":{"slug":"bevy-0-20","title":"Bevy 0.20","subtitle":null,"summary":"Thanks to 227 contributors, 817 pull requests, community reviewers, and our generous donors, we're happy to announce the Bevy 0.20 release on crates.io!","content_type":"changelog","language":"en","canonical_url":"https://bevy.org/news/bevy-0-20/","author":{"name":"Bevy Contributors","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Bevy","url":"https://bevy.org","listing_slug":null,"listing":null},"topics":[{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Rust","slug":"rust","url":"https://listedarticles.com/topics/rust"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":5381,"reading_minutes":23,"published_at":"2026-10-08T00:00:00.000Z","added_at":"2026-10-09T03:11:12.992Z","updated_at":"2026-10-09T03:11:12.992Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/bevy-0-20","markdown_url":"https://listedarticles.com/articles/bevy-0-20.md","example":false,"citation":"Bevy Contributors, Bevy. \"Bevy 0.20.\" 8 Oct 2026. https://bevy.org/news/bevy-0-20/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://bevy.org/news/bevy-0-20/"},"body_markdown":"## Posted on October 8, 2026 by Bevy Contributors\n\nThanks to **227** contributors, **817** pull requests, community reviewers, and our **generous donors**, we're happy to announce the **Bevy 0.20** release on crates.io!\n\nFor those who don't know, Bevy is a refreshingly simple data-driven game engine built in Rust. You can check out our Quick Start Guide to try it today. It's free and open source forever! You can grab the full source code on GitHub. Check out Bevy Assets for a collection of community-developed plugins, games, and learning resources.\n\nTo update an existing Bevy App or Plugin to **Bevy 0.20**, check out our 0.19 to 0.20 Migration Guide.\n\nSince our last release a few months ago we've added a *ton* of new features, bug fixes, and quality of life tweaks, but here are some of the highlights:\n\n- **Solari and DLSS** : Solari, Bevy's realtime pathtraced renderer, is now faster, more accurate, supports more Bevy rendering features, and runs on macOS via Metal!\n- **BSN Syntax Improvements** : BSN, Bevy's new scene system, had some syntax changes that made it*much* easier to read and compose\n- **Ready Event** : BSN scene entities now trigger an observable`Ready` event when all of their children have been spawned.\n- **More UI Widgets** : Bevy Feathers, Bevy's opinionated editor-centric UI toolkit, now has Color Input, Scrollable List View, Dropdown Selection, and Lazy Menu widgets. The Number Input widget is now scrubbable / draggable, and we've added Headless Tab Widgets.\n- **WESL Shaders** : Bevy has officially adopted the WESL shader language (a standardized extension of WGSL). WESL is already an improvement over Bevy's old custom WGSL dialect, and we've been working with the WESL team to plan out the future of shader development in Bevy.\n- **Sprite Materials and Extended 2D Materials** : It is now possible to create custom shader materials for Sprites, and 2D mesh materials can now be extended like they can in 3D.\n- **Pan Orbit Camera** : Bevy now has a \"pan orbit camera\", making it possible to navigate scenes in a CAD-like way.\n\n## Solari and DLSS #\n\nSolari, Bevy's realtime pathtraced renderer, has seen major improvements to pretty much every aspect of the plugin!\n\nRead JMS55's blog for the technical details, or continue reading below for the high level overview.\n\n### Improved Image Quality #\n\nThanks to improvements in our ReSTIR implementation, rendering is now mostly unbiased, leading to much more accurate lighting.\n\nAdditionally, thanks to some other changes, moving objects no longer have shadows that lag behind, and reflections now look significantly less shimmery in motion, especially for non-metallic materials.\n\n### Improved Performance #\n\nDLSS-RR has gotten very good in recent updates, and for many scenes, ReSTIR costs a decent chunk of performance, and does not significantly improve image quality.\n\nAs a result, we've decided to make ReSTIR optional, and turn it **off by default**.\n\nIf you were using Solari in Bevy 0.19, check if the loss of ReSTIR affects your scene, and if so re-enable `SolariLighting::restir`.\n\nWith ReSTIR off, expect reduced shadow quality and missing shadows in motion in scenes with many lights. We are exploring cheaper ways of improving light sampling, without ReSTIR, to improve this in the future.\n\nIn addition, Solari's scene management code is now retained (similar to retained render world optimizations in previous versions of Bevy), and overall much more optimized, leading to *significantly* reduced CPU costs.\n\nYou may also want to take a look at the new fields in `SolariLighting`. While we aim to set reasonable defaults that will work well across a wide variety of games, there are now many knobs (world cache size, per-pixel light sample count, temporal accumulation, and path tracing bounce count) that can be tweaked to improve performance or quality for your specific scene, project and hardware.\n\n### Improved Compatibility #\n\nSolari now supports lighting from `Atmosphere` and `EnvironmentMapLight`s on cameras, in addition to the existing support for `DirectionalLight` and emissive meshes. We're hoping to add support for the remaining `PointLight`, `SpotLight`, and `RectLight` types in the near future.\n\nSolari now also runs on macOS, but note that there is currently no built-in denoiser included in `bevy_solari` for macOS. MetalFX Ray Reconstruction might be a possible solution in the future (contributions welcome!)\n\n### DLSS Updates #\n\nFinally, our `dlss_wgpu` crate has been updated to support the latest version of DLSS, bringing support for DLSS-RR 4.5, which significantly improves denoising quality in Solari.\n\nIf you were using DLSS in Bevy 0.19, make sure to download and setup the newest version of the DLSS SDK, else you will run into compiler errors.\n\n## BSN Syntax Improvements #\n\nBSN landed with a few idiosyncrasies that caused friction in practice. We made some changes to BSN's syntax this cycle in the interest of improving its ergonomics and clarity. After this, the syntax *should* largely be nailed down.\n\n### Explicit scene syntax #\n\nAll scene references now require `@` prefixes:\n\n```\n// Before\nbsn! {\n    scene_variable\n    scene_function()\n    @SceneComponent\n    {scene_expression}\n}\n// After\nbsn! {\n    @scene_variable\n    @scene_function()\n    @SceneComponent\n    @{scene_expression}\n}\n```\nIn addition to making it easier to spot scene inclusions (and unifying the syntax across cases), this freed us up to make component values *much* easier to work with!\n\n### No more `template_value` wrappers! #\n\nYou can now remove all of those pesky `template_value` wrappers from your component values:\n\n```\n// Before\nbsn! {\n    template_value(component_variable)\n    template_value(component_function())\n}\n// After\nbsn! {\n    component_variable\n    component_function()\n}\n```\n### Enums \"just work\" #\n\nEnums no longer require `VariantDefaults` or `FromTemplate`, provided they implement `Default` and `Clone`:\n\n```\n// Before\n#[derive(Component, Default, Clone, VariantDefaults)]\nenum Foo {\n    A { x: u32, y: u32 },\n    #[default]\n    B,\n}\nbsn! {\n    Foo::B\n}\n// After\n#[derive(Component, Default, Clone)]\nenum Foo {\n    A { x: u32, y: u32 },\n    #[default]\n    B,\n}\nbsn! {\n    Foo::B\n}\n```\nIf you were using an enum that didn't support `VariantDefaults`, you can now remove the `template_value` wrapper:\n\n```\n// Before\nbsn! {\n    template_value(Foo::A)\n}\n// After\nbsn! {\n    Foo::A\n}\n```\nThe removal of `VariantDefaults` does mean that enums must now have every field specified:\n\n```\n// Before (y field is initialized to its default value)\nbsn! {\n    Foo::A { x: 1 }\n}\n// After (y field must be manually specified)\nbsn! {\n    Foo::A { x: 1, y: 0 }\n}\n```\nWe believe this tradeoff is worth it, as it increases BSN's compatibility with arbitrary Rust enums. Rust doesn't support \"enum variant defaults\" anyway!\n\n### Chained method support #\n\nThe \"builder pattern\" (and chained methods generally) previously required a `template_value` wrapper. This can now be removed:\n\n```\n// Before\nbsn! {\n    template_value(Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))\n}\n// After\nbsn! {\n    Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y)\n}\n```\nAdditionally, you can now remove the `template_value` wrapper in cases like this:\n\n```\n// Before\nbsn! {\n    template_value(node.clone())\n}\n// After\nbsn! {\n    node.clone()\n}\n```\nIn general, you should now be able to remove all `template_value` instances from your BSN declarations!\n\n### Improved list syntax #\n\nBSN previously used commas to separate entities, with optional `()` around entities to make the boundaries clearer. This resulted in a lot of syntax noise, line noise, and over-indentation:\n\n```\nbsn! {\n    Node \n    Children [\n        (\n            #OkButton\n            @button(\"Ok\")\n        ),\n        (\n            #CancelButton\n            @button(\"Cancel\")\n        ),\n    ]\n}\n```\nTo avoid this, many developers opted for this syntax instead, which made it very hard to visually distinguish entities:\n\n```\nbsn! {\n    Node \n    Children [\n        #OkButton\n        @button(\"Ok\"),\n        #CancelButton\n        @button(\"Cancel\"),\n    ]\n}\n```\nBSN now uses `--` to separate entities in a list:\n\n```\nbsn! {\n    Node \n    Children [\n        #OkButton\n        @button(\"Ok\")\n        --\n        #CancelButton\n        @button(\"Cancel\")\n    ]\n}\n```\nThis gives us the best of all worlds: entities are visually distinct, and there is no over-indentation, line noise, or syntax noise (the stats when compared to other competitors in the \"markup format\" space are very competitive!). Both `()` and `,` have been deprecated in this context.\n\nUsing `[]` and `()` for `bsn_list!` (and `bsn!`) is now discouraged / warned against (ex: `bsn_list! []`), as it can result in poor rustfmt autoformatting. Instead, use `bsn_list! {}`, which is the only syntax that `rustfmt` won't touch. Don't worry, we plan to build a BSN auto-formatter!\n\n```\nbsn_list! {\n    #Ok @button(\"Ok\")\n    --\n    #Cancel @button(\"Cancel\")\n}\n```\n## `Ready` Event #\n\nWe landed BSN, Bevy's next generation scene system, in our last release. It was missing a key piece though: the ability to easily run logic when a scene is fully \"ready\" and spawned (ex: all dependencies have loaded, the full hierarchy is present, and all of the initial components are inserted in the scene). This is a critical piece for building cohesive, standalone, composable scenes. It is also necessary to properly layer Bevy logic on top of *other* scene representations (like glTF).\n\nThe closest we had was the `Add` event for a given component, which runs \"top down\" (meaning children are not available). We needed a \"bottom up\" equivalent to enable building logic that relies on the complete loaded and spawned scene.\n\nThe solution is pretty straightforward: trigger a new `Ready` event for each entity in a spawned scene *after* the full spawn logic has run for that entity (including its descendants).\n\nThis enables the following:\n\n```\n#[derive(SceneComponent, Default, Clone)]\nstruct Widget;\nimpl Widget {\n    fn scene() -> impl Scene {\n        bsn! {\n            Node { width: px(100), height: px(100) }\n            on(|ready: On<Ready>| {\n                info!(\"The full scene, including 'widget.bsn' contents, is available here\")\n            })\n            Children [\n                Text(\"hello\")\n                --\n                :\"widget.bsn\"\n            ]\n        }\n    }\n}\nworld.spawn(bsn! { @Widget })\n```\n## More Feathers Widgets #\n\nFeathers, Bevy's opinionated editor-centric UI toolkit, now has more widgets for you to play with:\n\n### Color Input #\n\nBevy now has a compact color input selector that displays a color picker widget popup when clicked. This includes a color wheel selector, RGB, and HSL selectors, and a recently used colors grid.\n\n### List View / Scrollbar #\n\nA scrollable, selectable list view.\n\n### Dropdown Selection #\n\nA selection field that when clicked, displays a dropdown containing a list of options to select.\n\n### Lazy Menu #\n\nSpawns a menu popup when the menu is opened and *despawns* it when it is closed. This is in contrast to the normal Menu widget, which just *hides* the menu.\n\n### Number Input Widget Scrubbing / Dragging #\n\nThe `FeathersNumberInput` widget has been expanded to support both normal text input and scrubbing / dragging. There is a configurable \"hard limit\" (minimum and maximum value via any input method) and \"soft limit\" (minimum and maximum value via dragging), in addition to control over floating point precision and step sizes.\n\n## Headless Tab Widgets #\n\n`bevy_ui_widgets` now has headless (bring-your-own-visuals) tab behavior: a `TabList` container and `Tab` headers.\n\nSelection is managed \"externally\". `SelectedTab` on the list holds the selected tab; interaction emits `ValueChange<Option<Entity>>` as a request, applied by the app or by the optional `tablist_self_update` observer.\n\nTabs support keyboard shortcuts and integrate with Bevy's focus, interaction, and accessibility systems.\n\n```\nbsn! {\n    TabList\n    SelectedTab(Some(first_tab))\n    on(tablist_self_update)\n    Children [\n        Tab Children [ Text(\"General\") ]\n        --\n        Tab Children [ Text(\"Rendering\") ]\n    ]\n}\n```\nSee the `headless_tabs` example for controlled and self-updating tab lists in both orientations.\n\n## WESL Shaders #\n\nBevy's shaders are now written in WESL and the old \"Custom Bevy Extended WGSL\" language support has been removed.\n\nWESL is a language standard that extends WGSL to add important usability features like modules, imports, conditional compilation, and more. You can see what that looks like (and render pretty shader toys!) live in your browser in the WESL Playground.\n\nBevy has historically handled these things in our own custom WGSL dialect, but we believe it is better for the wider shader ecosystem (and for us) to adopt a common standard where we can pool resources on language improvements, module ecosystems, and IDE tooling. We've been working closely with the WESL team to evolve the standard in a way that fits well into the Bevy picture.\n\nA critical part of that tooling is language server protocol support, in the form of wgsl-analyzer. That means syntax highlighting, go-to-definition, inlay hints, code folding, formatting and more, once installed for your IDE of choice.\n\nCustom shaders in the old Bevy WGSL dialect need to be translated to WESL and renamed from `.wgsl` to `.wesl`. Plain WGSL files with no preprocessor directives will keep working.\n\n### Before: Custom Bevy Extended WGSL #\n\n```\n#import bevy_pbr::forward_io::VertexOutput\n#import \"shaders/util.wgsl\"::hsv_to_rgb\n#ifdef VERTEX_COLORS\nvar<private> tint: vec4<f32>;\n#endif\n@group(2) @binding(#{MATERIAL_BIND_GROUP}) var<uniform> color: vec4<f32>;\n```\n### After: WESL #\n\n```\nimport bevy_pbr::render::forward_io::VertexOutput;\nimport super::util::hsv_to_rgb;\n@if(VERTEX_COLORS)\nvar<private> tint: vec4<f32>;\n@group(2) @binding(constants::MATERIAL_BIND_GROUP) var<uniform> color: vec4<f32>;\n```\n## Mesh Shaders #\n\nMesh shaders are now integrated with Bevy's pipeline cache and are available for advanced users to take advantage of. Mesh shaders can be used to render:\n\n- Meshlets generated using tools like meshoptimizer\n- Procedural grass with dynamic level-of-detail, as seen here\n- Voxels, like nvidium\n- Particles\n- and more\n\nMesh shaders, at a high level, replace the classic vertex shader with a compute shader. This allows generating geometry directly on the GPU and passing those generated primitives directly to the fragment shader without using multiple pipelines or intermediary buffers (to pass data from a compute shader to a render pipeline).\n\nA `MeshPipeline` contains:\n\n- an optional task shader (also known as amplification shader)\n- a mesh shader\n- a fragment shader\n\nThe new `MeshPipelineDescriptor` can be used to define a `MeshPipeline`. That `MeshPipeline` is then used as a `RenderPipeline`, which allows the re-use of Bevy's lower level rendering APIs such as `RenderContext::begin_tracked_render_pass` to take advantage of the new `draw_mesh_tasks` APIs.\n\n```\nlet mut pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {\n    label: Some(\"custom_mesh_shader_pass\"),\n    color_attachments: &[Some(target.get_color_attachment())],\n    depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)),\n    ..default()\n});\npass.set_render_pipeline(mesh_pipeline);\npass.set_bind_group(0, &bind_group, &[view_uniform_offset.offset]);\n// draw_mesh_tasks dispatches the task shader if there is one,\n// or dispatches the mesh shader if there is no task shader.\npass.draw_mesh_tasks(1, 1, 1);\n```\nIt is notable that mesh shaders are an advanced graphics approach with platform-specific performance considerations, and that this is the initial base support for the feature. Higher level user APIs, and easy integration with Bevy's `StandardMaterial`, are left to future work.\n\nMesh shaders are not supported on web platforms.\n\nCheck out the new `mesh_shader_intro` example for more usage examples.\n\n## Sprite Materials #\n\nUntil now, Bevy's sprite renderer has been lacking a major feature: the ability to extend it with custom shaders! With this release, it's now possible to create custom materials for sprites by implementing the `MaterialExtension2d` trait, inserting the `SpriteMaterial` component and adding the `SpriteMaterialPlugin` to your app.\n\nThe shader can use functions exported from `bevy_sprite_render::sprite_mesh::functions`, including:\n\n```\n// Samples the sprite's final color, including the tint and alpha discard, at a given UV.\nfn sample_final_color(uv: vec2<f32>, instance_index: u32) -> vec4<f32>;\n// Samples the sprite's texture without tint and alpha discard at a given UV.\nfn sample_sprite_texture(uv: vec2<f32>, instance_index: u32) -> vec4<f32>;\n// Applies tint and alpha discard to the sprite's color.\nfn get_final_color(sprite_color: vec4<f32>, instance_index: u32) -> vec4<f32>;\n```\nCheck out the `sprite_material` example to see it in action!\n\n## 2D Extended Materials #\n\nBevy now provides a 2D analog to 3D's `ExtendedMaterial`, which can be used to extend an existing material by implementing the `MaterialExtension2d` trait:\n\n```\n#[derive(AsBindGroup, Reflect, Clone)]\nstruct MyMaterial {\n    #[uniform(20)]\n    value: Vec4,\n}\nimpl MaterialExtension2d for MyMaterial {\n    fn fragment_shader() -> Option<ShaderRef> {\n        Some(\"my_material.wesl\".into())\n    }\n}\n```\nThis material can now be used in an `ExtendedMaterial2d` struct:\n\n```\nlet handle = materials.add(ExtendedMaterial2d {\n    base: ColorMaterial::from_color(Color::WHITE),\n    extension: MyMaterial {\n        value: Vec4::ZERO,\n    },\n});\ncommands.spawn((\n    Mesh2d,\n    MeshMaterial2d(handle),\n));\n```\n## Sprite Render Backend Unification #\n\nThe sprite render backend was replaced by a new backend that reuses a lot of the infrastructure made for 3D. This resulted in improved performance in many cases and also makes future maintenance and improvements easier.\n\n## Pan Orbit Camera #\n\nWe have upstreamed the awesome `bevy_editor_cam` made by @aevyrie as the new `PanOrbitCamera` in our `bevy_camera_controller` crate!\n\n### Usage #\n\nAdd `MeshPickingPlugin` and `DefaultPanOrbitCameraPlugins`:\n\n```\napp.add_plugins((\n    MeshPickingPlugin,\n    DefaultPanOrbitCameraPlugins,\n))\n```\nThen add the `PanOrbitCamera` component on any 3D camera.\n\n```\ncommands.spawn((\n    Camera3d::default(),\n    PanOrbitCamera::default(),\n))\n```\nFull functionality is shown in the `camera/pan_orbit_camera_cad` example.\n\n## Weak System Ordering with `chain_weak` #\n\nOrdering large groups of systems with `.chain()` is convenient, but it can be overly strict. If system set `X` is chained before system set `Y`, every system in `X` must finish before *any* system in `Y` can start, even when the systems involved never touch the same data. This often leaves worker threads idle while they wait for a handful of stragglers at the end of a system set, a pattern that shows up frequently in the render world.\n\nThe new `chain_weak()`, `before_weak()`, and `after_weak()` functions provide a looser alternative. Like their regular counterparts, they request an ordering between successive elements, however that ordering is only kept between systems whose data accesses actually conflict. Systems that don't conflict are left unordered and may run in any order, including in parallel.\n\n```\nschedule.configure_sets(\n    (\n        ExtractCommands,\n        PrepareMeshes,\n        CreateViews,\n        Specialize,\n        PrepareViews,\n        Queue,\n        PhaseSort,\n        Prepare,\n        Render,\n        Cleanup,\n        PostCleanup,\n    )\n        .chain_weak(),\n);\n```\nWhen two weakly-ordered systems actually conflict on their data access, a normal ordering is kept between them, so the earlier one still runs first. Two systems that conflict only through a non-conflicting system between them in the chain stay ordered as well. Non-conflicting systems, however, are left free to run in any order and overlap for increased parallelism!\n\nTwo kinds of system are treated as always conflicting, so their ordering is always kept: an earlier system that produces deferred effects such as `Commands` (so the later system observes them, with an `ApplyDeferred` sync point inserted as usual), and exclusive systems (which cannot overlap anything regardless).\n\nBecause the scheduler can only see accesses it tracks, dependencies expressed through interior mutability on read-only accesses, global state, or other untracked methods are **not** respected. Use `chain_weak` only when your systems don't rely on such hidden ordering, otherwise stick with `chain`.\n\n## Contextual Theming #\n\nFeathers now supports \"contextual theming\", meaning that the theme variables can change depending on the parent entity. So widgets that are inside of a dialog box or subpanel can have different colors than widgets that are on a regular panel or window background.\n\nThe design follows that of popular web toolkits like MUI, Radix, or Chakra. There's a new component, `ThemeContext`, which lets you select which color scheme the widget's descendants should use; currently the available schemes are `Base`, `Higher`, `Highest`, and `Floating`, which correspond to the design plans for the Bevy scene editor.\n\nThe theme context is used in conjunction with a new kind of design token, named `SemanticToken`. The lookup process for a color now requires two stages: the `ThemeToken` is converted into a `SemanticToken`, and then the combination of `SemanticToken` and `ThemeContext` is used to look up a color.\n\nIn addition to allowing context-specific color choices, this also makes it easier to design new themes! Instead of having to tediously choose colors for a hundred different theme tokens, the set of semantic tokens is much smaller, and the relationship between token and color is much more intuitive.\n\n## `Val::Em` and `Val::Rem` #\n\nBevy UI now supports `em` and `rem` as sizing units. `em` is the current font size (represented by an `EmSize` component), `rem` is a global \"root\" font size (represented by the existing `RemSize` resource).\n\n`EmSize` is derived from `TextFont` when one is on the same entity; propagating it down the hierarchy is left to your app.\n\nThis is especially useful if you might want to vary your text size after authoring your UIs, for example as an accessibility feature or just to improve your UI on different devices.\n\n```\nbsn! {\n    Node { width: em(10) }\n    Text(\"Hello\")\n    TextFont { font_size: FontSize::Rem(1.5) }\n}\n```\nThe default font-size is now `rem(1)` rather than `px(20)`. This is a no-op if you're not changing `RemSize` but it means your text will scale by default when you do.\n\n## Per-Column Change Ticks #\n\nComponents can now opt-in to \"column summary change ticks\":\n\n```\n#[derive(Component)]\n#[component(summary_tick)]\nstruct MyComponent {\n    /* fields here */\n}\n```\nWhen enabled, this will store a \"column change tick\" in addition to a \"per-entity change tick\", which allows cheaply skipping the whole column of entities when querying for changes, rather than needing to check every entity's component to see if it has changed.\n\nThis makes mutations more expensive, as they need to write both the column change tick and the entity change tick, but for entities whose changes are queried often, but change infrequently, this tradeoff can easily be worth it! We've seen change ticks result in a 132x speedup in our GPU mesh extraction code!\n\n## `FixedNode` #\n\n`FixedNode` is a new marker component for Bevy UI.\n\nA UI node entity with the `FixedNode` component is positioned relative to the target camera's viewport rather than its parent element. `FixedNode`s don't inherit their parent's layout, clipping or transform context. They behave like a \"root node\".\n\n## Elliptical Border Radius #\n\nBevy UI can now draw nodes with elliptical border geometry.\n\nThe fields of `BorderRadius` are now `CornerRadius`s to enable different radius to be set for each axis.\n\n```\nlet a = BorderRadius::all(CornerRadius::circular(vh(10.)));\nlet b = BorderRadius::all(vh(10.)); // a == b\nlet c = BorderRadius::top_right(CornerRadius::new(px(10.), px(20.)));\n```\n## Schedule Randomization #\n\nBefore a schedule runs (and therefore, your systems), it first computes the system run order based on their ordering constraints (`.before()`, `.after()`, `.chain()`) and system sets. However, in addition to this, the schedule must also resolve **conflicts** - if system A and system B both mutate component C, and there's no ordering between A and B, the schedule needs to pick one to run first. So far, the rule has been that this is non-deterministic.\n\nIn practice though, schedules pick the order of these conflicting systems \"deterministically, but arbitrarily\". Put simply, your systems might accidentally be in the right order, but making an unrelated change to the graph might suddenly put it in the wrong order. This problem can be very difficult to detect.\n\nIntroducing schedule randomization! This will randomize the order of systems while maintaining any explicit system ordering constraints. Once the `debug` feature is enabled, `ScheduleBuildSettings` will include a `shuffle_seed` field, that users can set to randomize their schedules. For example:\n\n```\nApp::new()\n    .add_plugins(DefaultPlugins)\n    .edit_schedule(Update, |schedule| {\n        // Make sure to add the `rand` crate with `cargo add rand`.\n        let rng_seed: u64 = rand::random();\n        // Consider logging out the seed, so you can reproduce the error if you find a bug!\n        info!(\"Randomizing Update schedule with seed={rng_seed}\");\n        schedule.set_build_settings(ScheduleBuildSettings {\n            shuffle_seed: Some(rng_seed),\n            ..Default::default()\n        });\n    })\n    .run();\n```\nThis can be used for \"property testing\", to verify that your systems satisfy some property despite different orderings of systems.\n\nThere are some caveats however. Currently, when using `auto_insert_apply_deferred`, systems with commands are always placed before the earliest sync point they can. This means that although your systems may not have the correct ordering, they might \"accidentally\" have the correct ordering because of which sync point it uses. We hope to fix this in the future.\n\nIn addition, the multi-threaded executor executes systems greedily: it looks for the first unexecuted system whose dependencies are finished and that has no other conflicting systems running. The result is that even if the shuffle results in the order `(A, B, C)`, `C` could run before `B` if `A` and `B` conflict. **This can be desirable to test**, but consider using the single-threaded executor to avoid this case.\n\nThis tool is complementary to the existing system order ambiguity detection, which analyzes the graph of systems statically. Ambiguity detection cheaply generates a (sometimes large!) list of potential problems, not all of which may correspond to meaningful bugs in your project. Real test failures in some permitted orderings give you more actionable information about which of these problems are real, *and* the correct ordering. Furthermore, ambiguity detection can have false negatives, typically when ambiguities are incorrectly ignored, or in the presence of interior mutability mechanisms that do not require write-access (from the scheduler's perspective).\n\n## Catching Panics #\n\nFor long-running programs, crashing can be unacceptable. If, for example, there is a bug in one of your image editor's tools, it's better for that tool to fail or to produce wrong results than to lose all your unsaved work.\n\nBevy's systems, commands and observers are able to return errors. You can either set an error handler case-by-case, or let the `FallbackErrorHandler` deal with it. But this used to only work for explicitly returned errors: Panics used to bring down the entire app.\n\nIn Bevy 0.20, these panics now get turned into errors and passed to the fallback error handler. By default this re-panics, but now you can choose whether to log an error and continue, or whatever else you want.\n\n## Faster Bulk Despawning #\n\nSometimes, you just want to despawn a *ton* of things at once. This is reasonably common: Bevy's own `DespawnOnEnter` and `DespawnOnExit` allow you to quickly clean up entities as you swap the state of your game, tidying up menus or resetting the game after a loss. While this isn't that much work in total, it's concentrated all at once: if that process is slow, you could see hitches, or longer loading screens.\n\nIf you use the new `despawn_all<F: QueryFilter>` command (or one of its siblings) to batch this work, the ECS can speed things up through reduced overhead: sharing steps across related operations.\n\n| Entities | `despawn` | `despawn_all` | Speedup | \n|---|---|---|---|\n| 100 | 3.68 µs | 2.84 µs | 1.30× | \n| 1,000 | 25.2 µs | 15.3 µs | 1.65× | \n| 10,000 | 254.9 µs | 149.7 µs | 1.70× | \n| 100,000 | 3.17 ms | 2.07 ms | 1.53× | \n\n*Median of five benchmark runs, AMD Ryzen 9 9950X3D.*\n\nIf you're using `DespawnOnEnter` or `DespawnOnExit` you'll see this performance gain for free; no changes to your code needed.\n\n## Better Texture Compression #\n\nTextures are a huge part of the memory footprint for most 3D games. Smaller textures means smaller downloads, faster loads and bigger scenes. Bevy 0.20 tackles this on two fronts, with an improved compression approach and automatic mipmap generation during asset processing.\n\nBevy's `CompressedImageSaver` asset processor has been significantly upgraded with a new compression backend powered by the `ctt` library. The new `compressed_image_saver` feature compresses textures into BCn formats (for desktop GPUs) or ASTC formats (for mobile GPUs), producing higher-quality output than the previous Basis Universal approach: more bang for the byte. The compressor automatically selects the best output format based on the input texture's channel count and type — for example, single-channel textures get BC4, HDR textures get BC6H, and standard RGBA textures get BC7.\n\nTry out the new `compressed_image_saver` example to see it in action.\n\n### Automatic Mipmap Generation #\n\nNo more manually generating mipmaps (scaled down versions of each texture for viewing at a distance)! The new backend automatically produces a full mip chain during compression. This means less aliasing when textures are viewed at a distance and better GPU cache utilization — all for free, just by running your textures through the asset processor.\n\n### Image compression on other platforms #\n\nTo target mobile GPUs, set the `BEVY_COMPRESSED_IMAGE_SAVER_ASTC` environment variable with your desired block size (e.g. `4x4`, `6x6`, `8x8`). Larger blocks give smaller files at the cost of quality. All 14 ASTC block sizes are supported.\n\nThe previous Basis Universal compression behavior has been moved to the `compressed_image_saver_universal` feature. This remains the best choice for cross-platform distribution (including WebGPU), since UASTC can be transcoded at load time to whatever format the target GPU supports.\n\n## Bevy Error Context Messages #\n\nSimilar to the popular `anyhow` crate, `BevyError` now provides an ergonomic way to attach extra context to an error using the `context` method, which also allows creating a `Result<T, BevyError>` from an `Option<T>`.\n\nThis makes it easier to trace back errors with human-readable messages without looking at verbose backtraces.\n\n```\nfn fallible() -> Result<(), BevyError> {\n    // This produces the error message `Failed to parse number: invalid digit found in string`\n    let parsed: usize = \"I am not a number\"\n        .parse()\n        .context(\"Failed to parse number\")?;\n    Ok(())\n}\n```\n`with_context` may be used to produce the context message with a closure instead.\n\nIf multiple `context`s are stacked on top of each other, you see all of them when an error is logged. If we set up our error contexts like so:\n\n```\nfn parse_package() -> Result<Package, BevyError> {\n    let path = \"package.json\";\n    let package = std::fs::read_to_string(path)\n        .with_context(|| format!(\"Failed to read {path}\"))?;\n    serde_json::from_str(&package)?\n}\nfn load_package() -> Result<(), BevyError> {\n    let package = parse_package().context(\"Failed to parse package.json\")?;\n    // Use `package`...\n}\n```\nThe following error will be produced if `package.json` is missing:\n\n```\nFailed to parse package.json\nCaused by:\n    Failed to read package.json\n    No such file or directory (os error 2)\n```\n## `InlineBox` and `InlineImage` #\n\nFlowing text around elements allows for the creation of more complex UI elements. The newly introduced `InlineBox` component allows space to be reserved within text layouts for custom content. To intersperse images with text, spawn an entity with the `InlineImage` component.\n\n## What's Next? #\n\nNo matter how many features we add, the flock will always demand *more*. Game engines, unfortunately, are never *done*.\n\nLet us peer deep into the mists of time, and see what other features Bevy has in flight! Like usual, many of these features are \"essential components of a Bevy scene editor\", even if they are not \"the editor itself\". That allows us to ship useful bits and pieces incrementally, and polish them while we put it all together.\n\n- **.bsn asset format:** With the syntax stabilized, it's time to bring BSN to the file system, creating a human-readable, hot-reloadable file format designed for tool-driven (read: editor) authoring.\n- **Assets as Entities:** While our asset handling has been steadily improving, it is still a separate data model. We're working on representing assets as entities, giving them access to the full expressive power of the ECS (including event observers and relationships), providing direct support for defining assets in BSN, and easing the learning curve (as assets are accessed like any other ECS data).\n- **Remote inspector:** Browse, modify and mutate entities from external tools, on your machine or on a different device!\n- **More powerful required components:** Wish you could pull in assets, vary values based on other entities / components, or reference resources in required components? Us too: we're hoping to integrate required components with the`Template` trait that powers BSN, bells and whistles included.\n- **Mutually exclusive components:** A long requested feature:*statically* ensure that your`Player` is never a`Camera` , creating invariants that can be counted on.\n- **HDR (High Dynamic Range) display support:** Bevy: now in even more colors!\n\n## Support Bevy #\n\nBevy will always be free and open-source, but it isn't free to make! Because Bevy is free, we rely on the generosity of the Bevy community to fund our efforts. If you are a happy user of Bevy or you believe in our mission, please consider donating to the Bevy Foundation... every bit helps!\n\n## Contributors #\n\nA huge thanks to the 226 contributors that made this release (and associated docs) possible! In random order:\n\n- @Trashtalk217\n- @GroveDG\n- @Mysvac\n- @gagnus\n- @holg\n- @goodartistscopy\n- @stevehello166\n- @cookie1170\n- @l-monninger\n- @codaishin\n- @CodingDaniel1\n- @kfc35\n- @alphadragon2\n- @CraftSpider\n- @laundmo\n- @redstrate\n- @chris-hain\n- drewbluewasabi\n- @JasmineLowen\n- @swoobie\n- @ickshonpe\n- @cBournhonesque\n- @Bluefinger\n- @ItsDoot\n- acabrera\n- @pcwalton\n- @Sigma-dev\n- @hymm\n- @blamelessgames\n- @JeroenHoogers\n- @mate-h\n- @akshitj11\n- @Igor-dvr\n- @jbuehler23\n- @venhelhardt\n- @VictorElHajj\n- @greeble-dev\n- @zaidzdz\n- @nyfair\n- @urben1680\n- @komadori\n- @JMS55\n- Dahmen issam\n- @viridia\n- @Satellile\n- @GageHowe\n- @hukasu\n- @mgi388\n- @LeandroVandari\n- @Cyannide\n- @nyaalexx\n- @bytemuck\n- @MrGVSV\n- @tmstorey\n- @agluszak\n- Duncan Fairbanks\n- @0xEgao\n- @Poico\n- @bryancostanich\n- @plasmagrenade\n- @Miguel0312\n- @yunusey\n- @RCoder01\n- @bmisiak\n- @alice-i-cecile\n- @PizzaLvr49\n- @bonsairobo\n- @KategoryBee\n- @Shatur\n- @yh1970\n- @morr\n- @ElliottjPierce\n- @ParvePalial\n- @jannik4\n- @piedoom\n- @abrni\n- @rysb-dev\n- @HeartofPhos\n- @andyrift\n- @lkolbly\n- @francisdb\n- @MarcGuiselin\n- @tylerrussin\n- @IRSMsoso\n- @Tatsuya0330\n- @mockersf\n- @kpreid\n- @Lampan-git\n- @JamJomJim\n- @nfagerlund\n- @unclepomedev\n- Patrick Walton\n- @Rynibami\n- @dloukadakis\n- @PJB3005\n- @MalekiRe\n- @zen-zap\n- @andriyDev\n- @CrazyRoka\n- @akriegman\n- @Kyriota\n- @EmbersArc\n- @SOF3\n- @XSWare\n- @perry-blueberry\n- @DoubleThoughtTheProgrammer\n- @stuartparmenter\n- @bugsweeper\n- @UkoeHB\n- @cachebag\n- @tychedelia\n- @musjj\n- @Kees-van-Beilen\n- @franpereira\n- @justDeeevin\n- @etorresh\n- @NiklasEi\n- @voidreamer\n- @shunkie\n- @DataTriny\n- @eswartz\n- Christopher Hain\n- @dylansechet\n- @atlv24\n- @SolidStateDj\n- @alisterd51\n- @zincdev0\n- @kristoff3r\n- @raldone01\n- @kiana1kaslana\n- @mbremner\n- @robojeb\n- @Person-93\n- @MonaMayrhofer\n- @ncbray\n- @SpecificProtagonist\n- @jieyouxu\n- @rparrett\n- @alinv0\n- @tevans-3\n- @moosama76\n- @WeiTheShinobi\n- @CupOfTeaJay\n- @Cannedfood\n- @stinkytoe\n- @beicause\n- @drewbluewasabi\n- @MickHarrigan\n- @Aceeri\n- @SkiFire13\n- @Pnoenix\n- @infinitalo\n- @MrVintage710\n- @janis-bhm\n- @PeteMichaud\n- @malfuu\n- @eugineerd\n- @liamaharon\n- @Gingeh\n- @blaind\n- @qoh\n- @cart\n- @MoRusty\n- @Nuxssss\n- @miguelraz\n- @loreball\n- @chronicl\n- @amtep\n- @Victoronz\n- @issam3105\n- @SarthakSingh31\n- @davidgraymi\n- Sigma\n- @stevesloan\n- @da-x\n- @DGriffin91\n- @coreh\n- @rectalogic\n- @larsraph\n- @lomirus\n- @sokunrotanak\n- @ariofrio\n- @mnmaita\n- @Farori\n- @Schmarni-Dev\n- @hxYuki\n- @ekwoka\n- @fjkorf\n- @JonasJebing\n- @Zeophlite\n- @IceSentry\n- @CyberspaceDreamn\n- @VitalyAnkh\n- @nuts-rice\n- @yilin0518\n- @JaySpruce\n- @jxcv0\n- @Opprop35\n- @samoylovfp\n- @MatrixFrog\n- @ByteBaker\n- @doonv\n- @hoijui\n- @kaio-matos\n- @robtfm\n- @davewa\n- @ChristopherBiscardi\n- @Supremesv715\n- @ethanuppal\n- @mansiverma897993\n- @BenjaminBrienen\n- @codecnotsupported\n- @Jengamon\n- @B0ryskart0n\n- @DavidCrossman\n- @joaoconceicao12\n- @chescock\n- @taearls\n- @tylercritchlow\n- @razlani\n- @Elabajaba\n- @Henktorius\n- @taishi-sama\n- @sk0g\n- @rewin123\n- @Visse\n\nFor those interested in a complete changelog, you can see the entire log (and linked pull requests) via the relevant commit history.","body_html":"<h2 id=\"posted-on-october-8-2026-by-bevy-contributors\">Posted on October 8, 2026 by Bevy Contributors</h2>\n<p>Thanks to <strong>227</strong> contributors, <strong>817</strong> pull requests, community reviewers, and our <strong>generous donors</strong>, we&#39;re happy to announce the <strong>Bevy 0.20</strong> release on crates.io!</p>\n<p>For those who don&#39;t know, Bevy is a refreshingly simple data-driven game engine built in Rust. You can check out our Quick Start Guide to try it today. It&#39;s free and open source forever! You can grab the full source code on GitHub. Check out Bevy Assets for a collection of community-developed plugins, games, and learning resources.</p>\n<p>To update an existing Bevy App or Plugin to <strong>Bevy 0.20</strong>, check out our 0.19 to 0.20 Migration Guide.</p>\n<p>Since our last release a few months ago we&#39;ve added a <em>ton</em> of new features, bug fixes, and quality of life tweaks, but here are some of the highlights:</p>\n<ul><li><strong>Solari and DLSS</strong> : Solari, Bevy&#39;s realtime pathtraced renderer, is now faster, more accurate, supports more Bevy rendering features, and runs on macOS via Metal!</li><li><strong>BSN Syntax Improvements</strong> : BSN, Bevy&#39;s new scene system, had some syntax changes that made it<em>much</em> easier to read and compose</li><li><strong>Ready Event</strong> : BSN scene entities now trigger an observable<code>Ready</code> event when all of their children have been spawned.</li><li><strong>More UI Widgets</strong> : Bevy Feathers, Bevy&#39;s opinionated editor-centric UI toolkit, now has Color Input, Scrollable List View, Dropdown Selection, and Lazy Menu widgets. The Number Input widget is now scrubbable / draggable, and we&#39;ve added Headless Tab Widgets.</li><li><strong>WESL Shaders</strong> : Bevy has officially adopted the WESL shader language (a standardized extension of WGSL). WESL is already an improvement over Bevy&#39;s old custom WGSL dialect, and we&#39;ve been working with the WESL team to plan out the future of shader development in Bevy.</li><li><strong>Sprite Materials and Extended 2D Materials</strong> : It is now possible to create custom shader materials for Sprites, and 2D mesh materials can now be extended like they can in 3D.</li><li><strong>Pan Orbit Camera</strong> : Bevy now has a &quot;pan orbit camera&quot;, making it possible to navigate scenes in a CAD-like way.</li></ul>\n<h2 id=\"solari-and-dlss\">Solari and DLSS</h2>\n<p>Solari, Bevy&#39;s realtime pathtraced renderer, has seen major improvements to pretty much every aspect of the plugin!</p>\n<p>Read JMS55&#39;s blog for the technical details, or continue reading below for the high level overview.</p>\n<h3 id=\"improved-image-quality\">Improved Image Quality</h3>\n<p>Thanks to improvements in our ReSTIR implementation, rendering is now mostly unbiased, leading to much more accurate lighting.</p>\n<p>Additionally, thanks to some other changes, moving objects no longer have shadows that lag behind, and reflections now look significantly less shimmery in motion, especially for non-metallic materials.</p>\n<h3 id=\"improved-performance\">Improved Performance</h3>\n<p>DLSS-RR has gotten very good in recent updates, and for many scenes, ReSTIR costs a decent chunk of performance, and does not significantly improve image quality.</p>\n<p>As a result, we&#39;ve decided to make ReSTIR optional, and turn it <strong>off by default</strong>.</p>\n<p>If you were using Solari in Bevy 0.19, check if the loss of ReSTIR affects your scene, and if so re-enable <code>SolariLighting::restir</code>.</p>\n<p>With ReSTIR off, expect reduced shadow quality and missing shadows in motion in scenes with many lights. We are exploring cheaper ways of improving light sampling, without ReSTIR, to improve this in the future.</p>\n<p>In addition, Solari&#39;s scene management code is now retained (similar to retained render world optimizations in previous versions of Bevy), and overall much more optimized, leading to <em>significantly</em> reduced CPU costs.</p>\n<p>You may also want to take a look at the new fields in <code>SolariLighting</code>. While we aim to set reasonable defaults that will work well across a wide variety of games, there are now many knobs (world cache size, per-pixel light sample count, temporal accumulation, and path tracing bounce count) that can be tweaked to improve performance or quality for your specific scene, project and hardware.</p>\n<h3 id=\"improved-compatibility\">Improved Compatibility</h3>\n<p>Solari now supports lighting from <code>Atmosphere</code> and <code>EnvironmentMapLight</code>s on cameras, in addition to the existing support for <code>DirectionalLight</code> and emissive meshes. We&#39;re hoping to add support for the remaining <code>PointLight</code>, <code>SpotLight</code>, and <code>RectLight</code> types in the near future.</p>\n<p>Solari now also runs on macOS, but note that there is currently no built-in denoiser included in <code>bevy_solari</code> for macOS. MetalFX Ray Reconstruction might be a possible solution in the future (contributions welcome!)</p>\n<h3 id=\"dlss-updates\">DLSS Updates</h3>\n<p>Finally, our <code>dlss_wgpu</code> crate has been updated to support the latest version of DLSS, bringing support for DLSS-RR 4.5, which significantly improves denoising quality in Solari.</p>\n<p>If you were using DLSS in Bevy 0.19, make sure to download and setup the newest version of the DLSS SDK, else you will run into compiler errors.</p>\n<h2 id=\"bsn-syntax-improvements\">BSN Syntax Improvements</h2>\n<p>BSN landed with a few idiosyncrasies that caused friction in practice. We made some changes to BSN&#39;s syntax this cycle in the interest of improving its ergonomics and clarity. After this, the syntax <em>should</em> largely be nailed down.</p>\n<h3 id=\"explicit-scene-syntax\">Explicit scene syntax</h3>\n<p>All scene references now require <code>@</code> prefixes:</p>\n<pre><code>// Before\nbsn! {\n    scene_variable\n    scene_function()\n    @SceneComponent\n    {scene_expression}\n}\n// After\nbsn! {\n    @scene_variable\n    @scene_function()\n    @SceneComponent\n    @{scene_expression}\n}</code></pre>\n<p>In addition to making it easier to spot scene inclusions (and unifying the syntax across cases), this freed us up to make component values <em>much</em> easier to work with!</p>\n<h3 id=\"no-more-template-value-wrappers\">No more <code>template_value</code> wrappers!</h3>\n<p>You can now remove all of those pesky <code>template_value</code> wrappers from your component values:</p>\n<pre><code>// Before\nbsn! {\n    template_value(component_variable)\n    template_value(component_function())\n}\n// After\nbsn! {\n    component_variable\n    component_function()\n}</code></pre>\n<h3 id=\"enums-just-work\">Enums &quot;just work&quot;</h3>\n<p>Enums no longer require <code>VariantDefaults</code> or <code>FromTemplate</code>, provided they implement <code>Default</code> and <code>Clone</code>:</p>\n<pre><code>// Before\n#[derive(Component, Default, Clone, VariantDefaults)]\nenum Foo {\n    A { x: u32, y: u32 },\n    #[default]\n    B,\n}\nbsn! {\n    Foo::B\n}\n// After\n#[derive(Component, Default, Clone)]\nenum Foo {\n    A { x: u32, y: u32 },\n    #[default]\n    B,\n}\nbsn! {\n    Foo::B\n}</code></pre>\n<p>If you were using an enum that didn&#39;t support <code>VariantDefaults</code>, you can now remove the <code>template_value</code> wrapper:</p>\n<pre><code>// Before\nbsn! {\n    template_value(Foo::A)\n}\n// After\nbsn! {\n    Foo::A\n}</code></pre>\n<p>The removal of <code>VariantDefaults</code> does mean that enums must now have every field specified:</p>\n<pre><code>// Before (y field is initialized to its default value)\nbsn! {\n    Foo::A { x: 1 }\n}\n// After (y field must be manually specified)\nbsn! {\n    Foo::A { x: 1, y: 0 }\n}</code></pre>\n<p>We believe this tradeoff is worth it, as it increases BSN&#39;s compatibility with arbitrary Rust enums. Rust doesn&#39;t support &quot;enum variant defaults&quot; anyway!</p>\n<h3 id=\"chained-method-support\">Chained method support</h3>\n<p>The &quot;builder pattern&quot; (and chained methods generally) previously required a <code>template_value</code> wrapper. This can now be removed:</p>\n<pre><code>// Before\nbsn! {\n    template_value(Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))\n}\n// After\nbsn! {\n    Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y)\n}</code></pre>\n<p>Additionally, you can now remove the <code>template_value</code> wrapper in cases like this:</p>\n<pre><code>// Before\nbsn! {\n    template_value(node.clone())\n}\n// After\nbsn! {\n    node.clone()\n}</code></pre>\n<p>In general, you should now be able to remove all <code>template_value</code> instances from your BSN declarations!</p>\n<h3 id=\"improved-list-syntax\">Improved list syntax</h3>\n<p>BSN previously used commas to separate entities, with optional <code>()</code> around entities to make the boundaries clearer. This resulted in a lot of syntax noise, line noise, and over-indentation:</p>\n<pre><code>bsn! {\n    Node \n    Children [\n        (\n            #OkButton\n            @button(&quot;Ok&quot;)\n        ),\n        (\n            #CancelButton\n            @button(&quot;Cancel&quot;)\n        ),\n    ]\n}</code></pre>\n<p>To avoid this, many developers opted for this syntax instead, which made it very hard to visually distinguish entities:</p>\n<pre><code>bsn! {\n    Node \n    Children [\n        #OkButton\n        @button(&quot;Ok&quot;),\n        #CancelButton\n        @button(&quot;Cancel&quot;),\n    ]\n}</code></pre>\n<p>BSN now uses <code>--</code> to separate entities in a list:</p>\n<pre><code>bsn! {\n    Node \n    Children [\n        #OkButton\n        @button(&quot;Ok&quot;)\n        --\n        #CancelButton\n        @button(&quot;Cancel&quot;)\n    ]\n}</code></pre>\n<p>This gives us the best of all worlds: entities are visually distinct, and there is no over-indentation, line noise, or syntax noise (the stats when compared to other competitors in the &quot;markup format&quot; space are very competitive!). Both <code>()</code> and <code>,</code> have been deprecated in this context.</p>\n<p>Using <code>[]</code> and <code>()</code> for <code>bsn_list!</code> (and <code>bsn!</code>) is now discouraged / warned against (ex: <code>bsn_list! []</code>), as it can result in poor rustfmt autoformatting. Instead, use <code>bsn_list! {}</code>, which is the only syntax that <code>rustfmt</code> won&#39;t touch. Don&#39;t worry, we plan to build a BSN auto-formatter!</p>\n<pre><code>bsn_list! {\n    #Ok @button(&quot;Ok&quot;)\n    --\n    #Cancel @button(&quot;Cancel&quot;)\n}</code></pre>\n<h2 id=\"ready-event\"><code>Ready</code> Event</h2>\n<p>We landed BSN, Bevy&#39;s next generation scene system, in our last release. It was missing a key piece though: the ability to easily run logic when a scene is fully &quot;ready&quot; and spawned (ex: all dependencies have loaded, the full hierarchy is present, and all of the initial components are inserted in the scene). This is a critical piece for building cohesive, standalone, composable scenes. It is also necessary to properly layer Bevy logic on top of <em>other</em> scene representations (like glTF).</p>\n<p>The closest we had was the <code>Add</code> event for a given component, which runs &quot;top down&quot; (meaning children are not available). We needed a &quot;bottom up&quot; equivalent to enable building logic that relies on the complete loaded and spawned scene.</p>\n<p>The solution is pretty straightforward: trigger a new <code>Ready</code> event for each entity in a spawned scene <em>after</em> the full spawn logic has run for that entity (including its descendants).</p>\n<p>This enables the following:</p>\n<pre><code>#[derive(SceneComponent, Default, Clone)]\nstruct Widget;\nimpl Widget {\n    fn scene() -&gt; impl Scene {\n        bsn! {\n            Node { width: px(100), height: px(100) }\n            on(|ready: On&lt;Ready&gt;| {\n                info!(&quot;The full scene, including &#39;widget.bsn&#39; contents, is available here&quot;)\n            })\n            Children [\n                Text(&quot;hello&quot;)\n                --\n                :&quot;widget.bsn&quot;\n            ]\n        }\n    }\n}\nworld.spawn(bsn! { @Widget })</code></pre>\n<h2 id=\"more-feathers-widgets\">More Feathers Widgets</h2>\n<p>Feathers, Bevy&#39;s opinionated editor-centric UI toolkit, now has more widgets for you to play with:</p>\n<h3 id=\"color-input\">Color Input</h3>\n<p>Bevy now has a compact color input selector that displays a color picker widget popup when clicked. This includes a color wheel selector, RGB, and HSL selectors, and a recently used colors grid.</p>\n<h3 id=\"list-view-scrollbar\">List View / Scrollbar</h3>\n<p>A scrollable, selectable list view.</p>\n<h3 id=\"dropdown-selection\">Dropdown Selection</h3>\n<p>A selection field that when clicked, displays a dropdown containing a list of options to select.</p>\n<h3 id=\"lazy-menu\">Lazy Menu</h3>\n<p>Spawns a menu popup when the menu is opened and <em>despawns</em> it when it is closed. This is in contrast to the normal Menu widget, which just <em>hides</em> the menu.</p>\n<h3 id=\"number-input-widget-scrubbing-dragging\">Number Input Widget Scrubbing / Dragging</h3>\n<p>The <code>FeathersNumberInput</code> widget has been expanded to support both normal text input and scrubbing / dragging. There is a configurable &quot;hard limit&quot; (minimum and maximum value via any input method) and &quot;soft limit&quot; (minimum and maximum value via dragging), in addition to control over floating point precision and step sizes.</p>\n<h2 id=\"headless-tab-widgets\">Headless Tab Widgets</h2>\n<p><code>bevy_ui_widgets</code> now has headless (bring-your-own-visuals) tab behavior: a <code>TabList</code> container and <code>Tab</code> headers.</p>\n<p>Selection is managed &quot;externally&quot;. <code>SelectedTab</code> on the list holds the selected tab; interaction emits <code>ValueChange&lt;Option&lt;Entity&gt;&gt;</code> as a request, applied by the app or by the optional <code>tablist_self_update</code> observer.</p>\n<p>Tabs support keyboard shortcuts and integrate with Bevy&#39;s focus, interaction, and accessibility systems.</p>\n<pre><code>bsn! {\n    TabList\n    SelectedTab(Some(first_tab))\n    on(tablist_self_update)\n    Children [\n        Tab Children [ Text(&quot;General&quot;) ]\n        --\n        Tab Children [ Text(&quot;Rendering&quot;) ]\n    ]\n}</code></pre>\n<p>See the <code>headless_tabs</code> example for controlled and self-updating tab lists in both orientations.</p>\n<h2 id=\"wesl-shaders\">WESL Shaders</h2>\n<p>Bevy&#39;s shaders are now written in WESL and the old &quot;Custom Bevy Extended WGSL&quot; language support has been removed.</p>\n<p>WESL is a language standard that extends WGSL to add important usability features like modules, imports, conditional compilation, and more. You can see what that looks like (and render pretty shader toys!) live in your browser in the WESL Playground.</p>\n<p>Bevy has historically handled these things in our own custom WGSL dialect, but we believe it is better for the wider shader ecosystem (and for us) to adopt a common standard where we can pool resources on language improvements, module ecosystems, and IDE tooling. We&#39;ve been working closely with the WESL team to evolve the standard in a way that fits well into the Bevy picture.</p>\n<p>A critical part of that tooling is language server protocol support, in the form of wgsl-analyzer. That means syntax highlighting, go-to-definition, inlay hints, code folding, formatting and more, once installed for your IDE of choice.</p>\n<p>Custom shaders in the old Bevy WGSL dialect need to be translated to WESL and renamed from <code>.wgsl</code> to <code>.wesl</code>. Plain WGSL files with no preprocessor directives will keep working.</p>\n<h3 id=\"before-custom-bevy-extended-wgsl\">Before: Custom Bevy Extended WGSL</h3>\n<pre><code>#import bevy_pbr::forward_io::VertexOutput\n#import &quot;shaders/util.wgsl&quot;::hsv_to_rgb\n#ifdef VERTEX_COLORS\nvar&lt;private&gt; tint: vec4&lt;f32&gt;;\n#endif\n@group(2) @binding(#{MATERIAL_BIND_GROUP}) var&lt;uniform&gt; color: vec4&lt;f32&gt;;</code></pre>\n<h3 id=\"after-wesl\">After: WESL</h3>\n<pre><code>import bevy_pbr::render::forward_io::VertexOutput;\nimport super::util::hsv_to_rgb;\n@if(VERTEX_COLORS)\nvar&lt;private&gt; tint: vec4&lt;f32&gt;;\n@group(2) @binding(constants::MATERIAL_BIND_GROUP) var&lt;uniform&gt; color: vec4&lt;f32&gt;;</code></pre>\n<h2 id=\"mesh-shaders\">Mesh Shaders</h2>\n<p>Mesh shaders are now integrated with Bevy&#39;s pipeline cache and are available for advanced users to take advantage of. Mesh shaders can be used to render:</p>\n<ul><li>Meshlets generated using tools like meshoptimizer</li><li>Procedural grass with dynamic level-of-detail, as seen here</li><li>Voxels, like nvidium</li><li>Particles</li><li>and more</li></ul>\n<p>Mesh shaders, at a high level, replace the classic vertex shader with a compute shader. This allows generating geometry directly on the GPU and passing those generated primitives directly to the fragment shader without using multiple pipelines or intermediary buffers (to pass data from a compute shader to a render pipeline).</p>\n<p>A <code>MeshPipeline</code> contains:</p>\n<ul><li>an optional task shader (also known as amplification shader)</li><li>a mesh shader</li><li>a fragment shader</li></ul>\n<p>The new <code>MeshPipelineDescriptor</code> can be used to define a <code>MeshPipeline</code>. That <code>MeshPipeline</code> is then used as a <code>RenderPipeline</code>, which allows the re-use of Bevy&#39;s lower level rendering APIs such as <code>RenderContext::begin_tracked_render_pass</code> to take advantage of the new <code>draw_mesh_tasks</code> APIs.</p>\n<pre><code>let mut pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {\n    label: Some(&quot;custom_mesh_shader_pass&quot;),\n    color_attachments: &amp;[Some(target.get_color_attachment())],\n    depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)),\n    ..default()\n});\npass.set_render_pipeline(mesh_pipeline);\npass.set_bind_group(0, &amp;bind_group, &amp;[view_uniform_offset.offset]);\n// draw_mesh_tasks dispatches the task shader if there is one,\n// or dispatches the mesh shader if there is no task shader.\npass.draw_mesh_tasks(1, 1, 1);</code></pre>\n<p>It is notable that mesh shaders are an advanced graphics approach with platform-specific performance considerations, and that this is the initial base support for the feature. Higher level user APIs, and easy integration with Bevy&#39;s <code>StandardMaterial</code>, are left to future work.</p>\n<p>Mesh shaders are not supported on web platforms.</p>\n<p>Check out the new <code>mesh_shader_intro</code> example for more usage examples.</p>\n<h2 id=\"sprite-materials\">Sprite Materials</h2>\n<p>Until now, Bevy&#39;s sprite renderer has been lacking a major feature: the ability to extend it with custom shaders! With this release, it&#39;s now possible to create custom materials for sprites by implementing the <code>MaterialExtension2d</code> trait, inserting the <code>SpriteMaterial</code> component and adding the <code>SpriteMaterialPlugin</code> to your app.</p>\n<p>The shader can use functions exported from <code>bevy_sprite_render::sprite_mesh::functions</code>, including:</p>\n<pre><code>// Samples the sprite&#39;s final color, including the tint and alpha discard, at a given UV.\nfn sample_final_color(uv: vec2&lt;f32&gt;, instance_index: u32) -&gt; vec4&lt;f32&gt;;\n// Samples the sprite&#39;s texture without tint and alpha discard at a given UV.\nfn sample_sprite_texture(uv: vec2&lt;f32&gt;, instance_index: u32) -&gt; vec4&lt;f32&gt;;\n// Applies tint and alpha discard to the sprite&#39;s color.\nfn get_final_color(sprite_color: vec4&lt;f32&gt;, instance_index: u32) -&gt; vec4&lt;f32&gt;;</code></pre>\n<p>Check out the <code>sprite_material</code> example to see it in action!</p>\n<h2 id=\"2d-extended-materials\">2D Extended Materials</h2>\n<p>Bevy now provides a 2D analog to 3D&#39;s <code>ExtendedMaterial</code>, which can be used to extend an existing material by implementing the <code>MaterialExtension2d</code> trait:</p>\n<pre><code>#[derive(AsBindGroup, Reflect, Clone)]\nstruct MyMaterial {\n    #[uniform(20)]\n    value: Vec4,\n}\nimpl MaterialExtension2d for MyMaterial {\n    fn fragment_shader() -&gt; Option&lt;ShaderRef&gt; {\n        Some(&quot;my_material.wesl&quot;.into())\n    }\n}</code></pre>\n<p>This material can now be used in an <code>ExtendedMaterial2d</code> struct:</p>\n<pre><code>let handle = materials.add(ExtendedMaterial2d {\n    base: ColorMaterial::from_color(Color::WHITE),\n    extension: MyMaterial {\n        value: Vec4::ZERO,\n    },\n});\ncommands.spawn((\n    Mesh2d,\n    MeshMaterial2d(handle),\n));</code></pre>\n<h2 id=\"sprite-render-backend-unification\">Sprite Render Backend Unification</h2>\n<p>The sprite render backend was replaced by a new backend that reuses a lot of the infrastructure made for 3D. This resulted in improved performance in many cases and also makes future maintenance and improvements easier.</p>\n<h2 id=\"pan-orbit-camera\">Pan Orbit Camera</h2>\n<p>We have upstreamed the awesome <code>bevy_editor_cam</code> made by @aevyrie as the new <code>PanOrbitCamera</code> in our <code>bevy_camera_controller</code> crate!</p>\n<h3 id=\"usage\">Usage</h3>\n<p>Add <code>MeshPickingPlugin</code> and <code>DefaultPanOrbitCameraPlugins</code>:</p>\n<pre><code>app.add_plugins((\n    MeshPickingPlugin,\n    DefaultPanOrbitCameraPlugins,\n))</code></pre>\n<p>Then add the <code>PanOrbitCamera</code> component on any 3D camera.</p>\n<pre><code>commands.spawn((\n    Camera3d::default(),\n    PanOrbitCamera::default(),\n))</code></pre>\n<p>Full functionality is shown in the <code>camera/pan_orbit_camera_cad</code> example.</p>\n<h2 id=\"weak-system-ordering-with-chain-weak\">Weak System Ordering with <code>chain_weak</code></h2>\n<p>Ordering large groups of systems with <code>.chain()</code> is convenient, but it can be overly strict. If system set <code>X</code> is chained before system set <code>Y</code>, every system in <code>X</code> must finish before <em>any</em> system in <code>Y</code> can start, even when the systems involved never touch the same data. This often leaves worker threads idle while they wait for a handful of stragglers at the end of a system set, a pattern that shows up frequently in the render world.</p>\n<p>The new <code>chain_weak()</code>, <code>before_weak()</code>, and <code>after_weak()</code> functions provide a looser alternative. Like their regular counterparts, they request an ordering between successive elements, however that ordering is only kept between systems whose data accesses actually conflict. Systems that don&#39;t conflict are left unordered and may run in any order, including in parallel.</p>\n<pre><code>schedule.configure_sets(\n    (\n        ExtractCommands,\n        PrepareMeshes,\n        CreateViews,\n        Specialize,\n        PrepareViews,\n        Queue,\n        PhaseSort,\n        Prepare,\n        Render,\n        Cleanup,\n        PostCleanup,\n    )\n        .chain_weak(),\n);</code></pre>\n<p>When two weakly-ordered systems actually conflict on their data access, a normal ordering is kept between them, so the earlier one still runs first. Two systems that conflict only through a non-conflicting system between them in the chain stay ordered as well. Non-conflicting systems, however, are left free to run in any order and overlap for increased parallelism!</p>\n<p>Two kinds of system are treated as always conflicting, so their ordering is always kept: an earlier system that produces deferred effects such as <code>Commands</code> (so the later system observes them, with an <code>ApplyDeferred</code> sync point inserted as usual), and exclusive systems (which cannot overlap anything regardless).</p>\n<p>Because the scheduler can only see accesses it tracks, dependencies expressed through interior mutability on read-only accesses, global state, or other untracked methods are <strong>not</strong> respected. Use <code>chain_weak</code> only when your systems don&#39;t rely on such hidden ordering, otherwise stick with <code>chain</code>.</p>\n<h2 id=\"contextual-theming\">Contextual Theming</h2>\n<p>Feathers now supports &quot;contextual theming&quot;, meaning that the theme variables can change depending on the parent entity. So widgets that are inside of a dialog box or subpanel can have different colors than widgets that are on a regular panel or window background.</p>\n<p>The design follows that of popular web toolkits like MUI, Radix, or Chakra. There&#39;s a new component, <code>ThemeContext</code>, which lets you select which color scheme the widget&#39;s descendants should use; currently the available schemes are <code>Base</code>, <code>Higher</code>, <code>Highest</code>, and <code>Floating</code>, which correspond to the design plans for the Bevy scene editor.</p>\n<p>The theme context is used in conjunction with a new kind of design token, named <code>SemanticToken</code>. The lookup process for a color now requires two stages: the <code>ThemeToken</code> is converted into a <code>SemanticToken</code>, and then the combination of <code>SemanticToken</code> and <code>ThemeContext</code> is used to look up a color.</p>\n<p>In addition to allowing context-specific color choices, this also makes it easier to design new themes! Instead of having to tediously choose colors for a hundred different theme tokens, the set of semantic tokens is much smaller, and the relationship between token and color is much more intuitive.</p>\n<h2 id=\"val-em-and-val-rem\"><code>Val::Em</code> and <code>Val::Rem</code></h2>\n<p>Bevy UI now supports <code>em</code> and <code>rem</code> as sizing units. <code>em</code> is the current font size (represented by an <code>EmSize</code> component), <code>rem</code> is a global &quot;root&quot; font size (represented by the existing <code>RemSize</code> resource).</p>\n<p><code>EmSize</code> is derived from <code>TextFont</code> when one is on the same entity; propagating it down the hierarchy is left to your app.</p>\n<p>This is especially useful if you might want to vary your text size after authoring your UIs, for example as an accessibility feature or just to improve your UI on different devices.</p>\n<pre><code>bsn! {\n    Node { width: em(10) }\n    Text(&quot;Hello&quot;)\n    TextFont { font_size: FontSize::Rem(1.5) }\n}</code></pre>\n<p>The default font-size is now <code>rem(1)</code> rather than <code>px(20)</code>. This is a no-op if you&#39;re not changing <code>RemSize</code> but it means your text will scale by default when you do.</p>\n<h2 id=\"per-column-change-ticks\">Per-Column Change Ticks</h2>\n<p>Components can now opt-in to &quot;column summary change ticks&quot;:</p>\n<pre><code>#[derive(Component)]\n#[component(summary_tick)]\nstruct MyComponent {\n    /* fields here */\n}</code></pre>\n<p>When enabled, this will store a &quot;column change tick&quot; in addition to a &quot;per-entity change tick&quot;, which allows cheaply skipping the whole column of entities when querying for changes, rather than needing to check every entity&#39;s component to see if it has changed.</p>\n<p>This makes mutations more expensive, as they need to write both the column change tick and the entity change tick, but for entities whose changes are queried often, but change infrequently, this tradeoff can easily be worth it! We&#39;ve seen change ticks result in a 132x speedup in our GPU mesh extraction code!</p>\n<h2 id=\"fixednode\"><code>FixedNode</code></h2>\n<p><code>FixedNode</code> is a new marker component for Bevy UI.</p>\n<p>A UI node entity with the <code>FixedNode</code> component is positioned relative to the target camera&#39;s viewport rather than its parent element. <code>FixedNode</code>s don&#39;t inherit their parent&#39;s layout, clipping or transform context. They behave like a &quot;root node&quot;.</p>\n<h2 id=\"elliptical-border-radius\">Elliptical Border Radius</h2>\n<p>Bevy UI can now draw nodes with elliptical border geometry.</p>\n<p>The fields of <code>BorderRadius</code> are now <code>CornerRadius</code>s to enable different radius to be set for each axis.</p>\n<pre><code>let a = BorderRadius::all(CornerRadius::circular(vh(10.)));\nlet b = BorderRadius::all(vh(10.)); // a == b\nlet c = BorderRadius::top_right(CornerRadius::new(px(10.), px(20.)));</code></pre>\n<h2 id=\"schedule-randomization\">Schedule Randomization</h2>\n<p>Before a schedule runs (and therefore, your systems), it first computes the system run order based on their ordering constraints (<code>.before()</code>, <code>.after()</code>, <code>.chain()</code>) and system sets. However, in addition to this, the schedule must also resolve <strong>conflicts</strong> - if system A and system B both mutate component C, and there&#39;s no ordering between A and B, the schedule needs to pick one to run first. So far, the rule has been that this is non-deterministic.</p>\n<p>In practice though, schedules pick the order of these conflicting systems &quot;deterministically, but arbitrarily&quot;. Put simply, your systems might accidentally be in the right order, but making an unrelated change to the graph might suddenly put it in the wrong order. This problem can be very difficult to detect.</p>\n<p>Introducing schedule randomization! This will randomize the order of systems while maintaining any explicit system ordering constraints. Once the <code>debug</code> feature is enabled, <code>ScheduleBuildSettings</code> will include a <code>shuffle_seed</code> field, that users can set to randomize their schedules. For example:</p>\n<pre><code>App::new()\n    .add_plugins(DefaultPlugins)\n    .edit_schedule(Update, |schedule| {\n        // Make sure to add the `rand` crate with `cargo add rand`.\n        let rng_seed: u64 = rand::random();\n        // Consider logging out the seed, so you can reproduce the error if you find a bug!\n        info!(&quot;Randomizing Update schedule with seed={rng_seed}&quot;);\n        schedule.set_build_settings(ScheduleBuildSettings {\n            shuffle_seed: Some(rng_seed),\n            ..Default::default()\n        });\n    })\n    .run();</code></pre>\n<p>This can be used for &quot;property testing&quot;, to verify that your systems satisfy some property despite different orderings of systems.</p>\n<p>There are some caveats however. Currently, when using <code>auto_insert_apply_deferred</code>, systems with commands are always placed before the earliest sync point they can. This means that although your systems may not have the correct ordering, they might &quot;accidentally&quot; have the correct ordering because of which sync point it uses. We hope to fix this in the future.</p>\n<p>In addition, the multi-threaded executor executes systems greedily: it looks for the first unexecuted system whose dependencies are finished and that has no other conflicting systems running. The result is that even if the shuffle results in the order <code>(A, B, C)</code>, <code>C</code> could run before <code>B</code> if <code>A</code> and <code>B</code> conflict. <strong>This can be desirable to test</strong>, but consider using the single-threaded executor to avoid this case.</p>\n<p>This tool is complementary to the existing system order ambiguity detection, which analyzes the graph of systems statically. Ambiguity detection cheaply generates a (sometimes large!) list of potential problems, not all of which may correspond to meaningful bugs in your project. Real test failures in some permitted orderings give you more actionable information about which of these problems are real, <em>and</em> the correct ordering. Furthermore, ambiguity detection can have false negatives, typically when ambiguities are incorrectly ignored, or in the presence of interior mutability mechanisms that do not require write-access (from the scheduler&#39;s perspective).</p>\n<h2 id=\"catching-panics\">Catching Panics</h2>\n<p>For long-running programs, crashing can be unacceptable. If, for example, there is a bug in one of your image editor&#39;s tools, it&#39;s better for that tool to fail or to produce wrong results than to lose all your unsaved work.</p>\n<p>Bevy&#39;s systems, commands and observers are able to return errors. You can either set an error handler case-by-case, or let the <code>FallbackErrorHandler</code> deal with it. But this used to only work for explicitly returned errors: Panics used to bring down the entire app.</p>\n<p>In Bevy 0.20, these panics now get turned into errors and passed to the fallback error handler. By default this re-panics, but now you can choose whether to log an error and continue, or whatever else you want.</p>\n<h2 id=\"faster-bulk-despawning\">Faster Bulk Despawning</h2>\n<p>Sometimes, you just want to despawn a <em>ton</em> of things at once. This is reasonably common: Bevy&#39;s own <code>DespawnOnEnter</code> and <code>DespawnOnExit</code> allow you to quickly clean up entities as you swap the state of your game, tidying up menus or resetting the game after a loss. While this isn&#39;t that much work in total, it&#39;s concentrated all at once: if that process is slow, you could see hitches, or longer loading screens.</p>\n<p>If you use the new <code>despawn_all&lt;F: QueryFilter&gt;</code> command (or one of its siblings) to batch this work, the ECS can speed things up through reduced overhead: sharing steps across related operations.</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Entities</th><th><code>despawn</code></th><th><code>despawn_all</code></th><th>Speedup</th></tr></thead><tbody><tr><td>100</td><td>3.68 µs</td><td>2.84 µs</td><td>1.30×</td></tr><tr><td>1,000</td><td>25.2 µs</td><td>15.3 µs</td><td>1.65×</td></tr><tr><td>10,000</td><td>254.9 µs</td><td>149.7 µs</td><td>1.70×</td></tr><tr><td>100,000</td><td>3.17 ms</td><td>2.07 ms</td><td>1.53×</td></tr></tbody></table></div>\n<p><em>Median of five benchmark runs, AMD Ryzen 9 9950X3D.</em></p>\n<p>If you&#39;re using <code>DespawnOnEnter</code> or <code>DespawnOnExit</code> you&#39;ll see this performance gain for free; no changes to your code needed.</p>\n<h2 id=\"better-texture-compression\">Better Texture Compression</h2>\n<p>Textures are a huge part of the memory footprint for most 3D games. Smaller textures means smaller downloads, faster loads and bigger scenes. Bevy 0.20 tackles this on two fronts, with an improved compression approach and automatic mipmap generation during asset processing.</p>\n<p>Bevy&#39;s <code>CompressedImageSaver</code> asset processor has been significantly upgraded with a new compression backend powered by the <code>ctt</code> library. The new <code>compressed_image_saver</code> feature compresses textures into BCn formats (for desktop GPUs) or ASTC formats (for mobile GPUs), producing higher-quality output than the previous Basis Universal approach: more bang for the byte. The compressor automatically selects the best output format based on the input texture&#39;s channel count and type — for example, single-channel textures get BC4, HDR textures get BC6H, and standard RGBA textures get BC7.</p>\n<p>Try out the new <code>compressed_image_saver</code> example to see it in action.</p>\n<h3 id=\"automatic-mipmap-generation\">Automatic Mipmap Generation</h3>\n<p>No more manually generating mipmaps (scaled down versions of each texture for viewing at a distance)! The new backend automatically produces a full mip chain during compression. This means less aliasing when textures are viewed at a distance and better GPU cache utilization — all for free, just by running your textures through the asset processor.</p>\n<h3 id=\"image-compression-on-other-platforms\">Image compression on other platforms</h3>\n<p>To target mobile GPUs, set the <code>BEVY_COMPRESSED_IMAGE_SAVER_ASTC</code> environment variable with your desired block size (e.g. <code>4x4</code>, <code>6x6</code>, <code>8x8</code>). Larger blocks give smaller files at the cost of quality. All 14 ASTC block sizes are supported.</p>\n<p>The previous Basis Universal compression behavior has been moved to the <code>compressed_image_saver_universal</code> feature. This remains the best choice for cross-platform distribution (including WebGPU), since UASTC can be transcoded at load time to whatever format the target GPU supports.</p>\n<h2 id=\"bevy-error-context-messages\">Bevy Error Context Messages</h2>\n<p>Similar to the popular <code>anyhow</code> crate, <code>BevyError</code> now provides an ergonomic way to attach extra context to an error using the <code>context</code> method, which also allows creating a <code>Result&lt;T, BevyError&gt;</code> from an <code>Option&lt;T&gt;</code>.</p>\n<p>This makes it easier to trace back errors with human-readable messages without looking at verbose backtraces.</p>\n<pre><code>fn fallible() -&gt; Result&lt;(), BevyError&gt; {\n    // This produces the error message `Failed to parse number: invalid digit found in string`\n    let parsed: usize = &quot;I am not a number&quot;\n        .parse()\n        .context(&quot;Failed to parse number&quot;)?;\n    Ok(())\n}</code></pre>\n<p><code>with_context</code> may be used to produce the context message with a closure instead.</p>\n<p>If multiple <code>context</code>s are stacked on top of each other, you see all of them when an error is logged. If we set up our error contexts like so:</p>\n<pre><code>fn parse_package() -&gt; Result&lt;Package, BevyError&gt; {\n    let path = &quot;package.json&quot;;\n    let package = std::fs::read_to_string(path)\n        .with_context(|| format!(&quot;Failed to read {path}&quot;))?;\n    serde_json::from_str(&amp;package)?\n}\nfn load_package() -&gt; Result&lt;(), BevyError&gt; {\n    let package = parse_package().context(&quot;Failed to parse package.json&quot;)?;\n    // Use `package`...\n}</code></pre>\n<p>The following error will be produced if <code>package.json</code> is missing:</p>\n<pre><code>Failed to parse package.json\nCaused by:\n    Failed to read package.json\n    No such file or directory (os error 2)</code></pre>\n<h2 id=\"inlinebox-and-inlineimage\"><code>InlineBox</code> and <code>InlineImage</code></h2>\n<p>Flowing text around elements allows for the creation of more complex UI elements. The newly introduced <code>InlineBox</code> component allows space to be reserved within text layouts for custom content. To intersperse images with text, spawn an entity with the <code>InlineImage</code> component.</p>\n<h2 id=\"what-s-next\">What&#39;s Next?</h2>\n<p>No matter how many features we add, the flock will always demand <em>more</em>. Game engines, unfortunately, are never <em>done</em>.</p>\n<p>Let us peer deep into the mists of time, and see what other features Bevy has in flight! Like usual, many of these features are &quot;essential components of a Bevy scene editor&quot;, even if they are not &quot;the editor itself&quot;. That allows us to ship useful bits and pieces incrementally, and polish them while we put it all together.</p>\n<ul><li><strong>.bsn asset format:</strong> With the syntax stabilized, it&#39;s time to bring BSN to the file system, creating a human-readable, hot-reloadable file format designed for tool-driven (read: editor) authoring.</li><li><strong>Assets as Entities:</strong> While our asset handling has been steadily improving, it is still a separate data model. We&#39;re working on representing assets as entities, giving them access to the full expressive power of the ECS (including event observers and relationships), providing direct support for defining assets in BSN, and easing the learning curve (as assets are accessed like any other ECS data).</li><li><strong>Remote inspector:</strong> Browse, modify and mutate entities from external tools, on your machine or on a different device!</li><li><strong>More powerful required components:</strong> Wish you could pull in assets, vary values based on other entities / components, or reference resources in required components? Us too: we&#39;re hoping to integrate required components with the<code>Template</code> trait that powers BSN, bells and whistles included.</li><li><strong>Mutually exclusive components:</strong> A long requested feature:<em>statically</em> ensure that your<code>Player</code> is never a<code>Camera</code> , creating invariants that can be counted on.</li><li><strong>HDR (High Dynamic Range) display support:</strong> Bevy: now in even more colors!</li></ul>\n<h2 id=\"support-bevy\">Support Bevy</h2>\n<p>Bevy will always be free and open-source, but it isn&#39;t free to make! Because Bevy is free, we rely on the generosity of the Bevy community to fund our efforts. If you are a happy user of Bevy or you believe in our mission, please consider donating to the Bevy Foundation... every bit helps!</p>\n<h2 id=\"contributors\">Contributors</h2>\n<p>A huge thanks to the 226 contributors that made this release (and associated docs) possible! In random order:</p>\n<ul><li>@Trashtalk217</li><li>@GroveDG</li><li>@Mysvac</li><li>@gagnus</li><li>@holg</li><li>@goodartistscopy</li><li>@stevehello166</li><li>@cookie1170</li><li>@l-monninger</li><li>@codaishin</li><li>@CodingDaniel1</li><li>@kfc35</li><li>@alphadragon2</li><li>@CraftSpider</li><li>@laundmo</li><li>@redstrate</li><li>@chris-hain</li><li>drewbluewasabi</li><li>@JasmineLowen</li><li>@swoobie</li><li>@ickshonpe</li><li>@cBournhonesque</li><li>@Bluefinger</li><li>@ItsDoot</li><li>acabrera</li><li>@pcwalton</li><li>@Sigma-dev</li><li>@hymm</li><li>@blamelessgames</li><li>@JeroenHoogers</li><li>@mate-h</li><li>@akshitj11</li><li>@Igor-dvr</li><li>@jbuehler23</li><li>@venhelhardt</li><li>@VictorElHajj</li><li>@greeble-dev</li><li>@zaidzdz</li><li>@nyfair</li><li>@urben1680</li><li>@komadori</li><li>@JMS55</li><li>Dahmen issam</li><li>@viridia</li><li>@Satellile</li><li>@GageHowe</li><li>@hukasu</li><li>@mgi388</li><li>@LeandroVandari</li><li>@Cyannide</li><li>@nyaalexx</li><li>@bytemuck</li><li>@MrGVSV</li><li>@tmstorey</li><li>@agluszak</li><li>Duncan Fairbanks</li><li>@0xEgao</li><li>@Poico</li><li>@bryancostanich</li><li>@plasmagrenade</li><li>@Miguel0312</li><li>@yunusey</li><li>@RCoder01</li><li>@bmisiak</li><li>@alice-i-cecile</li><li>@PizzaLvr49</li><li>@bonsairobo</li><li>@KategoryBee</li><li>@Shatur</li><li>@yh1970</li><li>@morr</li><li>@ElliottjPierce</li><li>@ParvePalial</li><li>@jannik4</li><li>@piedoom</li><li>@abrni</li><li>@rysb-dev</li><li>@HeartofPhos</li><li>@andyrift</li><li>@lkolbly</li><li>@francisdb</li><li>@MarcGuiselin</li><li>@tylerrussin</li><li>@IRSMsoso</li><li>@Tatsuya0330</li><li>@mockersf</li><li>@kpreid</li><li>@Lampan-git</li><li>@JamJomJim</li><li>@nfagerlund</li><li>@unclepomedev</li><li>Patrick Walton</li><li>@Rynibami</li><li>@dloukadakis</li><li>@PJB3005</li><li>@MalekiRe</li><li>@zen-zap</li><li>@andriyDev</li><li>@CrazyRoka</li><li>@akriegman</li><li>@Kyriota</li><li>@EmbersArc</li><li>@SOF3</li><li>@XSWare</li><li>@perry-blueberry</li><li>@DoubleThoughtTheProgrammer</li><li>@stuartparmenter</li><li>@bugsweeper</li><li>@UkoeHB</li><li>@cachebag</li><li>@tychedelia</li><li>@musjj</li><li>@Kees-van-Beilen</li><li>@franpereira</li><li>@justDeeevin</li><li>@etorresh</li><li>@NiklasEi</li><li>@voidreamer</li><li>@shunkie</li><li>@DataTriny</li><li>@eswartz</li><li>Christopher Hain</li><li>@dylansechet</li><li>@atlv24</li><li>@SolidStateDj</li><li>@alisterd51</li><li>@zincdev0</li><li>@kristoff3r</li><li>@raldone01</li><li>@kiana1kaslana</li><li>@mbremner</li><li>@robojeb</li><li>@Person-93</li><li>@MonaMayrhofer</li><li>@ncbray</li><li>@SpecificProtagonist</li><li>@jieyouxu</li><li>@rparrett</li><li>@alinv0</li><li>@tevans-3</li><li>@moosama76</li><li>@WeiTheShinobi</li><li>@CupOfTeaJay</li><li>@Cannedfood</li><li>@stinkytoe</li><li>@beicause</li><li>@drewbluewasabi</li><li>@MickHarrigan</li><li>@Aceeri</li><li>@SkiFire13</li><li>@Pnoenix</li><li>@infinitalo</li><li>@MrVintage710</li><li>@janis-bhm</li><li>@PeteMichaud</li><li>@malfuu</li><li>@eugineerd</li><li>@liamaharon</li><li>@Gingeh</li><li>@blaind</li><li>@qoh</li><li>@cart</li><li>@MoRusty</li><li>@Nuxssss</li><li>@miguelraz</li><li>@loreball</li><li>@chronicl</li><li>@amtep</li><li>@Victoronz</li><li>@issam3105</li><li>@SarthakSingh31</li><li>@davidgraymi</li><li>Sigma</li><li>@stevesloan</li><li>@da-x</li><li>@DGriffin91</li><li>@coreh</li><li>@rectalogic</li><li>@larsraph</li><li>@lomirus</li><li>@sokunrotanak</li><li>@ariofrio</li><li>@mnmaita</li><li>@Farori</li><li>@Schmarni-Dev</li><li>@hxYuki</li><li>@ekwoka</li><li>@fjkorf</li><li>@JonasJebing</li><li>@Zeophlite</li><li>@IceSentry</li><li>@CyberspaceDreamn</li><li>@VitalyAnkh</li><li>@nuts-rice</li><li>@yilin0518</li><li>@JaySpruce</li><li>@jxcv0</li><li>@Opprop35</li><li>@samoylovfp</li><li>@MatrixFrog</li><li>@ByteBaker</li><li>@doonv</li><li>@hoijui</li><li>@kaio-matos</li><li>@robtfm</li><li>@davewa</li><li>@ChristopherBiscardi</li><li>@Supremesv715</li><li>@ethanuppal</li><li>@mansiverma897993</li><li>@BenjaminBrienen</li><li>@codecnotsupported</li><li>@Jengamon</li><li>@B0ryskart0n</li><li>@DavidCrossman</li><li>@joaoconceicao12</li><li>@chescock</li><li>@taearls</li><li>@tylercritchlow</li><li>@razlani</li><li>@Elabajaba</li><li>@Henktorius</li><li>@taishi-sama</li><li>@sk0g</li><li>@rewin123</li><li>@Visse</li></ul>\n<p>For those interested in a complete changelog, you can see the entire log (and linked pull requests) via the relevant commit history.</p>","headings":[{"level":2,"text":"Posted on October 8, 2026 by Bevy Contributors","id":"posted-on-october-8-2026-by-bevy-contributors"},{"level":2,"text":"Solari and DLSS","id":"solari-and-dlss"},{"level":3,"text":"Improved Image Quality","id":"improved-image-quality"},{"level":3,"text":"Improved Performance","id":"improved-performance"},{"level":3,"text":"Improved Compatibility","id":"improved-compatibility"},{"level":3,"text":"DLSS Updates","id":"dlss-updates"},{"level":2,"text":"BSN Syntax Improvements","id":"bsn-syntax-improvements"},{"level":3,"text":"Explicit scene syntax","id":"explicit-scene-syntax"},{"level":3,"text":"No more template_value wrappers!","id":"no-more-template-value-wrappers"},{"level":3,"text":"Enums \"just work\"","id":"enums-just-work"},{"level":3,"text":"Chained method support","id":"chained-method-support"},{"level":3,"text":"Improved list syntax","id":"improved-list-syntax"},{"level":2,"text":"Ready Event","id":"ready-event"},{"level":2,"text":"More Feathers Widgets","id":"more-feathers-widgets"},{"level":3,"text":"Color Input","id":"color-input"},{"level":3,"text":"List View / Scrollbar","id":"list-view-scrollbar"},{"level":3,"text":"Dropdown Selection","id":"dropdown-selection"},{"level":3,"text":"Lazy Menu","id":"lazy-menu"},{"level":3,"text":"Number Input Widget Scrubbing / Dragging","id":"number-input-widget-scrubbing-dragging"},{"level":2,"text":"Headless Tab Widgets","id":"headless-tab-widgets"},{"level":2,"text":"WESL Shaders","id":"wesl-shaders"},{"level":3,"text":"Before: Custom Bevy Extended WGSL","id":"before-custom-bevy-extended-wgsl"},{"level":3,"text":"After: WESL","id":"after-wesl"},{"level":2,"text":"Mesh Shaders","id":"mesh-shaders"},{"level":2,"text":"Sprite Materials","id":"sprite-materials"},{"level":2,"text":"2D Extended Materials","id":"2d-extended-materials"},{"level":2,"text":"Sprite Render Backend Unification","id":"sprite-render-backend-unification"},{"level":2,"text":"Pan Orbit Camera","id":"pan-orbit-camera"},{"level":3,"text":"Usage","id":"usage"},{"level":2,"text":"Weak System Ordering with chain_weak","id":"weak-system-ordering-with-chain-weak"},{"level":2,"text":"Contextual Theming","id":"contextual-theming"},{"level":2,"text":"Val::Em and Val::Rem","id":"val-em-and-val-rem"},{"level":2,"text":"Per-Column Change Ticks","id":"per-column-change-ticks"},{"level":2,"text":"FixedNode","id":"fixednode"},{"level":2,"text":"Elliptical Border Radius","id":"elliptical-border-radius"},{"level":2,"text":"Schedule Randomization","id":"schedule-randomization"},{"level":2,"text":"Catching Panics","id":"catching-panics"},{"level":2,"text":"Faster Bulk Despawning","id":"faster-bulk-despawning"},{"level":2,"text":"Better Texture Compression","id":"better-texture-compression"},{"level":3,"text":"Automatic Mipmap Generation","id":"automatic-mipmap-generation"},{"level":3,"text":"Image compression on other platforms","id":"image-compression-on-other-platforms"},{"level":2,"text":"Bevy Error Context Messages","id":"bevy-error-context-messages"},{"level":2,"text":"InlineBox and InlineImage","id":"inlinebox-and-inlineimage"},{"level":2,"text":"What's Next?","id":"what-s-next"},{"level":2,"text":"Support Bevy","id":"support-bevy"},{"level":2,"text":"Contributors","id":"contributors"}]}}