diff --git a/Cargo.toml b/Cargo.toml index d06f251e497ee..4c417d9eb70af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -199,6 +199,9 @@ serialize = ["bevy_internal/serialize"] # Enables multithreaded parallelism in the engine. Disabling it forces all engine tasks to run on a single thread. multi-threaded = ["bevy_internal/multi-threaded"] +# Use async-io's implementation of block_on instead of futures-lite's implementation. This is preferred if your application uses async-io. +async-io = ["bevy_internal/async-io"] + # Wayland display server support wayland = ["bevy_internal/wayland"] diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 566de2686c9ca..195bc71d5dd4e 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -424,7 +424,7 @@ impl App { /// Setup the application to manage events of type `T`. /// /// This is done by adding a [`Resource`] of type [`Events::`], - /// and inserting an [`update_system`](Events::update_system) into [`First`]. + /// and inserting an [`event_update_system`] into [`First`]. /// /// See [`Events`] for defining events. /// @@ -440,13 +440,18 @@ impl App { /// # /// app.add_event::(); /// ``` + /// + /// [`event_update_system`]: bevy_ecs::event::event_update_system pub fn add_event(&mut self) -> &mut Self where T: Event, { if !self.world.contains_resource::>() { - self.init_resource::>() - .add_systems(First, Events::::update_system); + self.init_resource::>().add_systems( + First, + bevy_ecs::event::event_update_system:: + .run_if(bevy_ecs::event::event_update_condition::), + ); } self } diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 2e67be23deca5..1228740bea578 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -166,7 +166,7 @@ impl AssetProcessor { let processor = _processor.clone(); std::thread::spawn(move || { processor.process_assets(); - futures_lite::future::block_on(processor.listen_for_source_change_events()); + bevy_tasks::block_on(processor.listen_for_source_change_events()); }); } } @@ -190,7 +190,7 @@ impl AssetProcessor { }); // This must happen _after_ the scope resolves or it will happen "too early" // Don't move this into the async scope above! process_assets is a blocking/sync function this is fine - futures_lite::future::block_on(self.finish_processing_assets()); + bevy_tasks::block_on(self.finish_processing_assets()); let end_time = std::time::Instant::now(); debug!("Processing finished in {:?}", end_time - start_time); } diff --git a/crates/bevy_core_pipeline/src/skybox/skybox.wgsl b/crates/bevy_core_pipeline/src/skybox/skybox.wgsl index d3bc9c5faa571..856cde7d96eaf 100644 --- a/crates/bevy_core_pipeline/src/skybox/skybox.wgsl +++ b/crates/bevy_core_pipeline/src/skybox/skybox.wgsl @@ -1,12 +1,36 @@ #import bevy_render::view View +#import bevy_pbr::utils coords_to_viewport_uv @group(0) @binding(0) var skybox: texture_cube; @group(0) @binding(1) var skybox_sampler: sampler; @group(0) @binding(2) var view: View; +fn coords_to_ray_direction(position: vec2, viewport: vec4) -> vec3 { + // Using world positions of the fragment and camera to calculate a ray direction + // break down at large translations. This code only needs to know the ray direction. + // The ray direction is along the direction from the camera to the fragment position. + // In view space, the camera is at the origin, so the view space ray direction is + // along the direction of the fragment position - (0,0,0) which is just the + // fragment position. + // Use the position on the near clipping plane to avoid -inf world position + // because the far plane of an infinite reverse projection is at infinity. + let view_position_homogeneous = view.inverse_projection * vec4( + coords_to_viewport_uv(position, viewport) * vec2(2.0, -2.0) + vec2(-1.0, 1.0), + 1.0, + 1.0, + ); + let view_ray_direction = view_position_homogeneous.xyz / view_position_homogeneous.w; + // Transforming the view space ray direction by the view matrix, transforms the + // direction to world space. Note that the w element is set to 0.0, as this is a + // vector direction, not a position, That causes the matrix multiplication to ignore + // the translations from the view matrix. + let ray_direction = (view.view * vec4(view_ray_direction, 0.0)).xyz; + + return normalize(ray_direction); +} + struct VertexOutput { - @builtin(position) clip_position: vec4, - @location(0) world_position: vec3, + @builtin(position) position: vec4, }; // 3 | 2. @@ -29,21 +53,14 @@ fn skybox_vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { 0.25, 0.5 ) * 4.0 - vec4(1.0); - // Use the position on the near clipping plane to avoid -inf world position - // because the far plane of an infinite reverse projection is at infinity. - // NOTE: The clip position has a w component equal to 1.0 so we don't need - // to apply a perspective divide to it before inverse-projecting it. - let world_position_homogeneous = view.inverse_view_proj * vec4(clip_position.xy, 1.0, 1.0); - let world_position = world_position_homogeneous.xyz / world_position_homogeneous.w; - return VertexOutput(clip_position, world_position); + return VertexOutput(clip_position); } @fragment fn skybox_fragment(in: VertexOutput) -> @location(0) vec4 { - // The skybox cubemap is sampled along the direction from the camera world - // position, to the fragment world position on the near clipping plane - let ray_direction = in.world_position - view.world_position; - // cube maps are left-handed so we negate the z coordinate + let ray_direction = coords_to_ray_direction(in.position.xy, view.viewport); + + // Cube maps are left-handed so we negate the z coordinate. return textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0)); } diff --git a/crates/bevy_ecs/examples/events.rs b/crates/bevy_ecs/examples/events.rs index af342f1fca8f0..7be6795880775 100644 --- a/crates/bevy_ecs/examples/events.rs +++ b/crates/bevy_ecs/examples/events.rs @@ -16,7 +16,7 @@ fn main() { #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] pub struct FlushEvents; - schedule.add_systems(Events::::update_system.in_set(FlushEvents)); + schedule.add_systems(bevy_ecs::event::event_update_system::.in_set(FlushEvents)); // Add systems sending and receiving events after the events are flushed. schedule.add_systems(( diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 0494555b04e65..781bbde448cd0 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -103,7 +103,7 @@ struct EventInstance { /// This collection is meant to be paired with a system that calls /// [`Events::update`] exactly once per update/frame. /// -/// [`Events::update_system`] is a system that does this, typically initialized automatically using +/// [`event_update_system`] is a system that does this, typically initialized automatically using /// [`add_event`](https://docs.rs/bevy/*/bevy/app/struct.App.html#method.add_event). /// [`EventReader`]s are expected to read events from this collection at least once per loop/frame. /// Events will persist across a single frame boundary and so ordering of event producers and @@ -251,11 +251,6 @@ impl Events { iter.map(|e| e.event) } - /// A system that calls [`Events::update`] once per frame. - pub fn update_system(mut events: ResMut) { - events.update(); - } - #[inline] fn reset_start_event_count(&mut self) { self.events_a.start_event_count = self.event_count; @@ -754,6 +749,17 @@ impl<'a, E: Event> ExactSizeIterator for EventIteratorWithId<'a, E> { } } +/// A system that calls [`Events::update`] once per frame. +pub fn event_update_system(mut events: ResMut>) { + events.update(); +} + +/// A run condition that checks if the event's [`event_update_system`] +/// needs to run or not. +pub fn event_update_condition(events: Res>) -> bool { + !events.events_a.is_empty() || !events.events_b.is_empty() +} + #[cfg(test)] mod tests { use crate::system::assert_is_read_only_system; diff --git a/crates/bevy_ecs/src/reflect/component.rs b/crates/bevy_ecs/src/reflect/component.rs index f91ac8e900225..c86f34b22b3d6 100644 --- a/crates/bevy_ecs/src/reflect/component.rs +++ b/crates/bevy_ecs/src/reflect/component.rs @@ -279,7 +279,7 @@ impl FromType for ReflectComponent { }, reflect_unchecked_mut: |entity| { // SAFETY: reflect_unchecked_mut is an unsafe function pointer used by - // `reflect_unchecked_mut` which must be called with an UnsafeEntityCell with access to the the component `C` on the `entity` + // `reflect_unchecked_mut` which must be called with an UnsafeEntityCell with access to the component `C` on the `entity` unsafe { entity.get_mut::().map(|c| Mut { value: c.value as &mut dyn Reflect, diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 021964311e294..1327d2ea52d17 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -867,7 +867,7 @@ pub mod common_conditions { /// # let mut world = World::new(); /// # world.init_resource::(); /// # world.init_resource::>(); - /// # app.add_systems(Events::::update_system.before(my_system)); + /// # app.add_systems(bevy_ecs::event::event_update_system::.before(my_system)); /// /// app.add_systems( /// my_system.run_if(on_event::()), diff --git a/crates/bevy_hierarchy/src/components/parent.rs b/crates/bevy_hierarchy/src/components/parent.rs index 7bccf33ed6e1e..68ba8c7c91ed0 100644 --- a/crates/bevy_hierarchy/src/components/parent.rs +++ b/crates/bevy_hierarchy/src/components/parent.rs @@ -23,6 +23,16 @@ impl Parent { pub fn get(&self) -> Entity { self.0 } + + /// Gets the parent [`Entity`] as a slice of length 1. + /// + /// Useful for making APIs that require a type or homogenous storage + /// for both [`Children`] & [`Parent`] that is agnostic to edge direction. + /// + /// [`Children`]: super::children::Children + pub fn as_slice(&self) -> &[Entity] { + std::slice::from_ref(&self.0) + } } // TODO: We need to impl either FromWorld or Default so Parent can be registered as Reflect. diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 1a71d43819078..41b06c2311ae9 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -63,6 +63,7 @@ shader_format_spirv = ["bevy_render/shader_format_spirv"] serialize = ["bevy_core/serialize", "bevy_input/serialize", "bevy_time/serialize", "bevy_window/serialize", "bevy_transform/serialize", "bevy_math/serialize", "bevy_scene?/serialize"] multi-threaded = ["bevy_asset/multi-threaded", "bevy_ecs/multi-threaded", "bevy_tasks/multi-threaded"] +async-io = ["bevy_tasks/async-io"] # Display server protocol support (X11 is enabled by default) wayland = ["bevy_winit/wayland"] diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index f979765c5d471..47c861c155789 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -254,7 +254,7 @@ pub struct StandardMaterial { /// - It will look weird on bent/non-planar surfaces. /// - The depth of the pixel does not reflect its visual position, resulting /// in artifacts for depth-dependent features such as fog or SSAO. - /// - For the same reason, the the geometry silhouette will always be + /// - For the same reason, the geometry silhouette will always be /// the one of the actual geometry, not the parallaxed version, resulting /// in awkward looks on intersecting parallaxed surfaces. /// diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index e11209f1e8eba..372757f935148 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -668,9 +668,16 @@ pub fn prepare_previous_view_projection_uniforms( With, >, ) { - view_uniforms.uniforms.clear(); - - for (entity, camera, maybe_previous_view_proj) in &views { + let views_iter = views.iter(); + let view_count = views_iter.len(); + let Some(mut writer) = + view_uniforms + .uniforms + .get_writer(view_count, &render_device, &render_queue) + else { + return; + }; + for (entity, camera, maybe_previous_view_proj) in views_iter { let view_projection = match maybe_previous_view_proj { Some(previous_view) => previous_view.clone(), None => PreviousViewProjection { @@ -680,13 +687,9 @@ pub fn prepare_previous_view_projection_uniforms( commands .entity(entity) .insert(PreviousViewProjectionUniformOffset { - offset: view_uniforms.uniforms.push(view_projection), + offset: writer.write(&view_projection), }); } - - view_uniforms - .uniforms - .write_buffer(&render_device, &render_queue); } #[derive(Default, Resource)] diff --git a/crates/bevy_pbr/src/render/fog.rs b/crates/bevy_pbr/src/render/fog.rs index 10fb61ff2bcc9..4df01418f1d30 100644 --- a/crates/bevy_pbr/src/render/fog.rs +++ b/crates/bevy_pbr/src/render/fog.rs @@ -52,9 +52,15 @@ pub fn prepare_fog( mut fog_meta: ResMut, views: Query<(Entity, Option<&FogSettings>), With>, ) { - fog_meta.gpu_fogs.clear(); - - for (entity, fog) in &views { + let views_iter = views.iter(); + let view_count = views_iter.len(); + let Some(mut writer) = fog_meta + .gpu_fogs + .get_writer(view_count, &render_device, &render_queue) + else { + return; + }; + for (entity, fog) in views_iter { let gpu_fog = if let Some(fog) = fog { match &fog.falloff { FogFalloff::Linear { start, end } => GpuFog { @@ -103,13 +109,9 @@ pub fn prepare_fog( // This is later read by `SetMeshViewBindGroup` commands.entity(entity).insert(ViewFogUniformOffset { - offset: fog_meta.gpu_fogs.push(gpu_fog), + offset: writer.write(&gpu_fog), }); } - - fog_meta - .gpu_fogs - .write_buffer(&render_device, &render_queue); } /// Inserted on each `Entity` with an `ExtractedView` to keep track of its offset diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index c488ca23a16d1..feec375ddc3d3 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -666,7 +666,15 @@ pub fn prepare_lights( point_lights: Query<(Entity, &ExtractedPointLight)>, directional_lights: Query<(Entity, &ExtractedDirectionalLight)>, ) { - light_meta.view_gpu_lights.clear(); + let views_iter = views.iter(); + let views_count = views_iter.len(); + let Some(mut view_gpu_lights_writer) = + light_meta + .view_gpu_lights + .get_writer(views_count, &render_device, &render_queue) + else { + return; + }; // Pre-calculate for PointLights let cube_face_projection = @@ -1197,14 +1205,10 @@ pub fn prepare_lights( lights: view_lights, }, ViewLightsUniformOffset { - offset: light_meta.view_gpu_lights.push(gpu_lights), + offset: view_gpu_lights_writer.write(&gpu_lights), }, )); } - - light_meta - .view_gpu_lights - .write_buffer(&render_device, &render_queue); } // this must match CLUSTER_COUNT_SIZE in pbr.wgsl diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index cedc0c8ba3fd3..bb9eb2343e565 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,13 +1,17 @@ use crate::{ - environment_map, prepass, render::morph::MorphInstances, EnvironmentMapLight, FogMeta, - GlobalLightMeta, GpuFog, GpuLights, GpuPointLights, LightMeta, MaterialBindGroupId, - NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform, + environment_map, prepass, + render::{ + morph::{no_automatic_morph_batching, MorphIndices}, + skin::no_automatic_skin_batching, + }, + EnvironmentMapLight, FogMeta, GlobalLightMeta, GpuFog, GpuLights, GpuPointLights, LightMeta, + MaterialBindGroupId, NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform, ScreenSpaceAmbientOcclusionTextures, Shadow, ShadowSamplers, ViewClusterBindings, ViewFogUniformOffset, ViewLightsUniformOffset, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS, }; -use bevy_app::Plugin; -use bevy_asset::{load_internal_asset, AssetId, Assets, Handle}; +use bevy_app::{Plugin, PostUpdate}; +use bevy_asset::{load_internal_asset, AssetId, Handle}; use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transparent3d}, prepass::ViewPrepassTextures, @@ -21,7 +25,7 @@ use bevy_ecs::{ query::{QueryItem, ROQueryItem}, system::{lifetimeless::*, SystemParamItem, SystemState}, }; -use bevy_math::{Affine3, Mat4, Vec2, Vec4}; +use bevy_math::{Affine3, Vec2, Vec4}; use bevy_render::{ batching::{ batch_and_prepare_render_phase, write_batched_instance_buffer, GetBatchData, @@ -29,7 +33,6 @@ use bevy_render::{ }, globals::{GlobalsBuffer, GlobalsUniform}, mesh::{ - skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, GpuBufferInfo, InnerMeshVertexBufferLayout, Mesh, MeshVertexBufferLayout, VertexAttributeDescriptor, }, @@ -50,17 +53,15 @@ use bevy_utils::{tracing::error, HashMap, Hashed, PassHashMap}; use crate::render::{ morph::{extract_morphs, prepare_morphs, MorphUniform}, + skin::{extract_skins, prepare_skins, SkinUniform}, MeshLayouts, }; +use super::skin::SkinIndices; + #[derive(Default)] pub struct MeshRenderPlugin; -/// Maximum number of joints supported for skinned meshes. -pub const MAX_JOINTS: usize = 256; -const JOINT_SIZE: usize = std::mem::size_of::(); -pub(crate) const JOINT_BUFFER_SIZE: usize = MAX_JOINTS * JOINT_SIZE; - pub const MESH_VERTEX_OUTPUT: Handle = Handle::weak_from_u128(2645551199423808407); pub const MESH_VIEW_TYPES_HANDLE: Handle = Handle::weak_from_u128(8140454348013264787); pub const MESH_VIEW_BINDINGS_HANDLE: Handle = Handle::weak_from_u128(9076678235888822571); @@ -112,17 +113,22 @@ impl Plugin for MeshRenderPlugin { load_internal_asset!(app, SKINNING_HANDLE, "skinning.wgsl", Shader::from_wgsl); load_internal_asset!(app, MORPH_HANDLE, "morph.wgsl", Shader::from_wgsl); + app.add_systems( + PostUpdate, + (no_automatic_skin_batching, no_automatic_morph_batching), + ); + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() .init_resource::() + .init_resource::() + .init_resource::() .init_resource::() + .init_resource::() .add_systems( ExtractSchedule, - (extract_meshes, extract_skinned_meshes, extract_morphs), + (extract_meshes, extract_skins, extract_morphs), ) .add_systems( Render, @@ -136,7 +142,7 @@ impl Plugin for MeshRenderPlugin { .in_set(RenderSet::PrepareResources), write_batched_instance_buffer:: .in_set(RenderSet::PrepareResourcesFlush), - prepare_skinned_meshes.in_set(RenderSet::PrepareResources), + prepare_skins.in_set(RenderSet::PrepareResources), prepare_morphs.in_set(RenderSet::PrepareResources), prepare_mesh_bind_group.in_set(RenderSet::PrepareBindGroups), prepare_mesh_view_bind_groups.in_set(RenderSet::PrepareBindGroups), @@ -304,86 +310,6 @@ pub fn extract_meshes( commands.insert_or_spawn_batch(entities); } -#[derive(Component)] -pub struct SkinnedMeshJoints { - pub index: u32, -} - -impl SkinnedMeshJoints { - #[inline] - pub fn build( - skin: &SkinnedMesh, - inverse_bindposes: &Assets, - joints: &Query<&GlobalTransform>, - buffer: &mut BufferVec, - ) -> Option { - let inverse_bindposes = inverse_bindposes.get(&skin.inverse_bindposes)?; - let start = buffer.len(); - let target = start + skin.joints.len().min(MAX_JOINTS); - buffer.extend( - joints - .iter_many(&skin.joints) - .zip(inverse_bindposes.iter()) - .take(MAX_JOINTS) - .map(|(joint, bindpose)| joint.affine() * *bindpose), - ); - // iter_many will skip any failed fetches. This will cause it to assign the wrong bones, - // so just bail by truncating to the start. - if buffer.len() != target { - buffer.truncate(start); - return None; - } - - // Pad to 256 byte alignment - while buffer.len() % 4 != 0 { - buffer.push(Mat4::ZERO); - } - Some(Self { - index: start as u32, - }) - } - - /// Updated index to be in address space based on [`SkinnedMeshUniform`] size. - pub fn to_buffer_index(mut self) -> Self { - self.index *= std::mem::size_of::() as u32; - self - } -} - -#[derive(Default, Resource, Deref, DerefMut)] -pub struct SkinnedMeshJointsInstances(PassHashMap); - -pub fn extract_skinned_meshes( - mut skinned_mesh_joints_instances: ResMut, - mut uniform: ResMut, - query: Extract>, - inverse_bindposes: Extract>>, - joint_query: Extract>, -) { - skinned_mesh_joints_instances.clear(); - uniform.buffer.clear(); - - let mut last_start = 0; - - for (entity, view_visibility, skin) in &query { - if !view_visibility.get() { - continue; - } - // PERF: This can be expensive, can we move this to prepare? - if let Some(skinned_joints) = - SkinnedMeshJoints::build(skin, &inverse_bindposes, &joint_query, &mut uniform.buffer) - { - last_start = last_start.max(skinned_joints.index as usize); - skinned_mesh_joints_instances.insert(entity, skinned_joints.to_buffer_index()); - } - } - - // Pad out the buffer to ensure that there's enough space for bindings - while uniform.buffer.len() - last_start < MAX_JOINTS { - uniform.buffer.push(Mat4::ZERO); - } -} - #[derive(Resource, Clone)] pub struct MeshPipeline { pub view_layout: BindGroupLayout, @@ -1076,7 +1002,7 @@ pub fn prepare_mesh_bind_group( mesh_pipeline: Res, render_device: Res, mesh_uniforms: Res>, - skinned_mesh_uniform: Res, + skins_uniform: Res, weights_uniform: Res, ) { groups.reset(); @@ -1086,7 +1012,7 @@ pub fn prepare_mesh_bind_group( }; groups.model_only = Some(layouts.model_only(&render_device, &model)); - let skin = skinned_mesh_uniform.buffer.buffer(); + let skin = skins_uniform.buffer.buffer(); if let Some(skin) = skin { groups.skinned = Some(layouts.skinned(&render_device, &model, skin)); } @@ -1105,41 +1031,6 @@ pub fn prepare_mesh_bind_group( } } -// NOTE: This is using BufferVec because it is using a trick to allow a fixed-size array -// in a uniform buffer to be used like a variable-sized array by only writing the valid data -// into the buffer, knowing the number of valid items starting from the dynamic offset, and -// ignoring the rest, whether they're valid for other dynamic offsets or not. This trick may -// be supported later in encase, and then we should make use of it. - -#[derive(Resource)] -pub struct SkinnedMeshUniform { - pub buffer: BufferVec, -} - -impl Default for SkinnedMeshUniform { - fn default() -> Self { - Self { - buffer: BufferVec::new(BufferUsages::UNIFORM), - } - } -} - -pub fn prepare_skinned_meshes( - render_device: Res, - render_queue: Res, - mut skinned_mesh_uniform: ResMut, -) { - if skinned_mesh_uniform.buffer.is_empty() { - return; - } - - let len = skinned_mesh_uniform.buffer.len(); - skinned_mesh_uniform.buffer.reserve(len, &render_device); - skinned_mesh_uniform - .buffer - .write_buffer(&render_device, &render_queue); -} - #[derive(Component)] pub struct MeshViewBindGroup { pub value: BindGroup, @@ -1340,8 +1231,8 @@ impl RenderCommand

for SetMeshBindGroup { type Param = ( SRes, SRes, - SRes, - SRes, + SRes, + SRes, ); type ViewWorldQuery = (); type ItemWorldQuery = (); @@ -1351,7 +1242,7 @@ impl RenderCommand

for SetMeshBindGroup { item: &P, _view: (), _item_query: (), - (bind_groups, mesh_instances, skin_instances, morph_instances): SystemParamItem< + (bind_groups, mesh_instances, skin_indices, morph_indices): SystemParamItem< 'w, '_, Self::Param, @@ -1360,16 +1251,16 @@ impl RenderCommand

for SetMeshBindGroup { ) -> RenderCommandResult { let bind_groups = bind_groups.into_inner(); let mesh_instances = mesh_instances.into_inner(); - let skin_instances = skin_instances.into_inner(); - let morph_instances = morph_instances.into_inner(); + let skin_indices = skin_indices.into_inner(); + let morph_indices = morph_indices.into_inner(); let entity = &item.entity(); let Some(mesh) = mesh_instances.get(entity) else { return RenderCommandResult::Success; }; - let skin_index = skin_instances.get(entity); - let morph_index = morph_instances.get(entity); + let skin_index = skin_indices.get(entity); + let morph_index = morph_indices.get(entity); let is_skinned = skin_index.is_some(); let is_morphed = morph_index.is_some(); diff --git a/crates/bevy_pbr/src/render/mesh_bindings.rs b/crates/bevy_pbr/src/render/mesh_bindings.rs index e46af242fdf85..dcc01e1aa4c8b 100644 --- a/crates/bevy_pbr/src/render/mesh_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_bindings.rs @@ -1,5 +1,6 @@ //! Bind group layout related definitions for the mesh pipeline. +use bevy_math::Mat4; use bevy_render::{ mesh::morph::MAX_MORPH_WEIGHTS, render_resource::{ @@ -9,13 +10,17 @@ use bevy_render::{ renderer::RenderDevice, }; +use crate::render::skin::MAX_JOINTS; + const MORPH_WEIGHT_SIZE: usize = std::mem::size_of::(); pub const MORPH_BUFFER_SIZE: usize = MAX_MORPH_WEIGHTS * MORPH_WEIGHT_SIZE; +const JOINT_SIZE: usize = std::mem::size_of::(); +pub(crate) const JOINT_BUFFER_SIZE: usize = MAX_JOINTS * JOINT_SIZE; + /// Individual layout entries. mod layout_entry { - use super::MORPH_BUFFER_SIZE; - use crate::render::mesh::JOINT_BUFFER_SIZE; + use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE}; use crate::MeshUniform; use bevy_render::{ render_resource::{ @@ -66,8 +71,7 @@ mod layout_entry { /// Individual [`BindGroupEntry`](bevy_render::render_resource::BindGroupEntry) /// for bind groups. mod entry { - use super::MORPH_BUFFER_SIZE; - use crate::render::mesh::JOINT_BUFFER_SIZE; + use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE}; use bevy_render::render_resource::{ BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, TextureView, }; diff --git a/crates/bevy_pbr/src/render/mod.rs b/crates/bevy_pbr/src/render/mod.rs index 5448850d30723..b9d0d239c3874 100644 --- a/crates/bevy_pbr/src/render/mod.rs +++ b/crates/bevy_pbr/src/render/mod.rs @@ -3,8 +3,10 @@ mod light; pub(crate) mod mesh; mod mesh_bindings; mod morph; +mod skin; pub use fog::*; pub use light::*; pub use mesh::*; pub use mesh_bindings::MeshLayouts; +pub use skin::{extract_skins, prepare_skins, SkinIndex, SkinUniform, MAX_JOINTS}; diff --git a/crates/bevy_pbr/src/render/morph.rs b/crates/bevy_pbr/src/render/morph.rs index 9fed9440b3333..107e8b5630034 100644 --- a/crates/bevy_pbr/src/render/morph.rs +++ b/crates/bevy_pbr/src/render/morph.rs @@ -3,6 +3,7 @@ use std::{iter, mem}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; use bevy_render::{ + batching::NoAutomaticBatching, mesh::morph::{MeshMorphWeights, MAX_MORPH_WEIGHTS}, render_resource::{BufferUsages, BufferVec}, renderer::{RenderDevice, RenderQueue}, @@ -16,10 +17,15 @@ use bytemuck::Pod; pub struct MorphIndex { pub(super) index: u32, } + +#[derive(Default, Resource, Deref, DerefMut)] +pub struct MorphIndices(PassHashMap); + #[derive(Resource)] pub struct MorphUniform { pub buffer: BufferVec, } + impl Default for MorphUniform { fn default() -> Self { Self { @@ -29,21 +35,22 @@ impl Default for MorphUniform { } pub fn prepare_morphs( - device: Res, - queue: Res, + render_device: Res, + render_queue: Res, mut uniform: ResMut, ) { if uniform.buffer.is_empty() { return; } - let buffer = &mut uniform.buffer; - buffer.reserve(buffer.len(), &device); - buffer.write_buffer(&device, &queue); + let len = uniform.buffer.len(); + uniform.buffer.reserve(len, &render_device); + uniform.buffer.write_buffer(&render_device, &render_queue); } const fn can_align(step: usize, target: usize) -> bool { step % target == 0 || target % step == 0 } + const WGPU_MIN_ALIGN: usize = 256; /// Align a [`BufferVec`] to `N` bytes by padding the end with `T::default()` values. @@ -70,15 +77,14 @@ fn add_to_alignment(buffer: &mut BufferVec) { buffer.extend(iter::repeat_with(T::default).take(ts_to_add)); } -#[derive(Default, Resource, Deref, DerefMut)] -pub struct MorphInstances(PassHashMap); - +// Notes on implementation: see comment on top of the extract_skins system in skin module. +// This works similarly, but for `f32` instead of `Mat4` pub fn extract_morphs( - mut morph_instances: ResMut, + mut morph_indices: ResMut, mut uniform: ResMut, query: Extract>, ) { - morph_instances.clear(); + morph_indices.clear(); uniform.buffer.clear(); for (entity, view_visibility, morph_weights) in &query { @@ -92,6 +98,17 @@ pub fn extract_morphs( add_to_alignment::(&mut uniform.buffer); let index = (start * mem::size_of::()) as u32; - morph_instances.insert(entity, MorphIndex { index }); + morph_indices.insert(entity, MorphIndex { index }); + } +} + +// NOTE: Because morph targets require per-morph target texture bindings, they cannot +// currently be batched. +pub fn no_automatic_morph_batching( + mut commands: Commands, + query: Query, Without)>, +) { + for entity in &query { + commands.entity(entity).insert(NoAutomaticBatching); } } diff --git a/crates/bevy_pbr/src/render/skin.rs b/crates/bevy_pbr/src/render/skin.rs new file mode 100644 index 0000000000000..aa3bd028e1ace --- /dev/null +++ b/crates/bevy_pbr/src/render/skin.rs @@ -0,0 +1,150 @@ +use bevy_asset::Assets; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::prelude::*; +use bevy_math::Mat4; +use bevy_render::{ + batching::NoAutomaticBatching, + mesh::skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, + render_resource::{BufferUsages, BufferVec}, + renderer::{RenderDevice, RenderQueue}, + view::ViewVisibility, + Extract, +}; +use bevy_transform::prelude::GlobalTransform; +use bevy_utils::PassHashMap; + +/// Maximum number of joints supported for skinned meshes. +pub const MAX_JOINTS: usize = 256; + +#[derive(Component)] +pub struct SkinIndex { + pub index: u32, +} + +impl SkinIndex { + /// Index to be in address space based on [`SkinUniform`] size. + const fn new(start: usize) -> Self { + SkinIndex { + index: (start * std::mem::size_of::()) as u32, + } + } +} + +#[derive(Default, Resource, Deref, DerefMut)] +pub struct SkinIndices(PassHashMap); + +// Notes on implementation: see comment on top of the `extract_skins` system. +#[derive(Resource)] +pub struct SkinUniform { + pub buffer: BufferVec, +} + +impl Default for SkinUniform { + fn default() -> Self { + Self { + buffer: BufferVec::new(BufferUsages::UNIFORM), + } + } +} + +pub fn prepare_skins( + render_device: Res, + render_queue: Res, + mut uniform: ResMut, +) { + if uniform.buffer.is_empty() { + return; + } + + let len = uniform.buffer.len(); + uniform.buffer.reserve(len, &render_device); + uniform.buffer.write_buffer(&render_device, &render_queue); +} + +// Notes on implementation: +// We define the uniform binding as an array, N> in the shader, +// where N is the maximum number of Mat4s we can fit in the uniform binding, +// which may be as little as 16kB or 64kB. But, we may not need all N. +// We may only need, for example, 10. +// +// If we used uniform buffers ‘normally’ then we would have to write a full +// binding of data for each dynamic offset binding, which is wasteful, makes +// the buffer much larger than it needs to be, and uses more memory bandwidth +// to transfer the data, which then costs frame time So @superdump came up +// with this design: just bind data at the specified offset and interpret +// the data at that offset as an array regardless of what is there. +// +// So instead of writing N Mat4s when you only need 10, you write 10, and +// then pad up to the next dynamic offset alignment. Then write the next. +// And for the last dynamic offset binding, make sure there is a full binding +// of data after it so that the buffer is of size +// `last dynamic offset` + `array>`. +// +// Then when binding the first dynamic offset, the first 10 entries in the array +// are what you expect, but if you read the 11th you’re reading ‘invalid’ data +// which could be padding or could be from the next binding. +// +// In this way, we can pack ‘variable sized arrays’ into uniform buffer bindings +// which normally only support fixed size arrays. You just have to make sure +// in the shader that you only read the values that are valid for that binding. +pub fn extract_skins( + mut skin_indices: ResMut, + mut uniform: ResMut, + query: Extract>, + inverse_bindposes: Extract>>, + joints: Extract>, +) { + uniform.buffer.clear(); + let mut last_start = 0; + + // PERF: This can be expensive, can we move this to prepare? + for (entity, view_visibility, skin) in &query { + if !view_visibility.get() { + continue; + } + let buffer = &mut uniform.buffer; + let Some(inverse_bindposes) = inverse_bindposes.get(&skin.inverse_bindposes) else { + continue; + }; + let start = buffer.len(); + + let target = start + skin.joints.len().min(MAX_JOINTS); + buffer.extend( + joints + .iter_many(&skin.joints) + .zip(inverse_bindposes.iter()) + .take(MAX_JOINTS) + .map(|(joint, bindpose)| joint.affine() * *bindpose), + ); + // iter_many will skip any failed fetches. This will cause it to assign the wrong bones, + // so just bail by truncating to the start. + if buffer.len() != target { + buffer.truncate(start); + continue; + } + last_start = last_start.max(start); + + // Pad to 256 byte alignment + while buffer.len() % 4 != 0 { + buffer.push(Mat4::ZERO); + } + + skin_indices.insert(entity, SkinIndex::new(start)); + } + + // Pad out the buffer to ensure that there's enough space for bindings + while uniform.buffer.len() - last_start < MAX_JOINTS { + uniform.buffer.push(Mat4::ZERO); + } +} + +// NOTE: The skinned joints uniform buffer has to be bound at a dynamic offset per +// entity and so cannot currently be batched. +pub fn no_automatic_skin_batching( + mut commands: Commands, + query: Query, Without)>, +) { + for entity in &query { + commands.entity(entity).insert(NoAutomaticBatching); + } +} diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index 059843871c1b4..a855d52862ccc 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -132,24 +132,27 @@ fn prepare_uniform_components( ) where C: ShaderType + WriteInto + Clone, { - component_uniforms.uniforms.clear(); - let entities = components - .iter() + let components_iter = components.iter(); + let count = components_iter.len(); + let Some(mut writer) = + component_uniforms + .uniforms + .get_writer(count, &render_device, &render_queue) + else { + return; + }; + let entities = components_iter .map(|(entity, component)| { ( entity, DynamicUniformIndex:: { - index: component_uniforms.uniforms.push(component.clone()), + index: writer.write(component), marker: PhantomData, }, ) }) .collect::>(); commands.insert_or_spawn_batch(entities); - - component_uniforms - .uniforms - .write_buffer(&render_device, &render_queue); } /// This plugin extracts the components into the "render world". diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 266a3e148c2d4..7dc222e1e701e 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -156,7 +156,7 @@ impl RenderAssets { } } -/// This system extracts all crated or modified assets of the corresponding [`RenderAsset`] type +/// This system extracts all created or modified assets of the corresponding [`RenderAsset`] type /// into the "render world". fn extract_render_asset( mut commands: Commands, diff --git a/crates/bevy_render/src/render_resource/uniform_buffer.rs b/crates/bevy_render/src/render_resource/uniform_buffer.rs index 4c1ad61b2aeb8..3876866b639d3 100644 --- a/crates/bevy_render/src/render_resource/uniform_buffer.rs +++ b/crates/bevy_render/src/render_resource/uniform_buffer.rs @@ -1,14 +1,17 @@ -use std::marker::PhantomData; +use std::{marker::PhantomData, num::NonZeroU64}; use crate::{ render_resource::Buffer, renderer::{RenderDevice, RenderQueue}, }; use encase::{ - internal::WriteInto, DynamicUniformBuffer as DynamicUniformBufferWrapper, ShaderType, + internal::{AlignmentValue, BufferMut, WriteInto}, + DynamicUniformBuffer as DynamicUniformBufferWrapper, ShaderType, UniformBuffer as UniformBufferWrapper, }; -use wgpu::{util::BufferInitDescriptor, BindingResource, BufferBinding, BufferUsages}; +use wgpu::{ + util::BufferInitDescriptor, BindingResource, BufferBinding, BufferDescriptor, BufferUsages, +}; /// Stores data to be transferred to the GPU and made accessible to shaders as a uniform buffer. /// @@ -240,6 +243,67 @@ impl DynamicUniformBuffer { self.changed = true; } + /// Creates a writer that can be used to directly write elements into the target buffer. + /// + /// This method uses less memory and performs fewer memory copies using over [`push`] and [`write_buffer`]. + /// + /// `max_count` *must* be greater than or equal to the number of elements that are to be written to the buffer, or + /// the writer will panic while writing. Dropping the writer will schedule the buffer write into the provided + /// [`RenderQueue`](crate::renderer::RenderQueue). + /// + /// If there is no GPU-side buffer allocated to hold the data currently stored, or if a GPU-side buffer previously + /// allocated does not have enough capacity to hold `max_count` elements, a new GPU-side buffer is created. + /// + /// Returns `None` if there is no allocated GPU-side buffer, and `max_count` is 0. + /// + /// [`push`]: Self::push + /// [`write_buffer`]: Self::write_buffer + #[inline] + pub fn get_writer<'a>( + &'a mut self, + max_count: usize, + device: &RenderDevice, + queue: &'a RenderQueue, + ) -> Option> { + let alignment = + AlignmentValue::new(device.limits().min_uniform_buffer_offset_alignment as u64); + let mut capacity = self.buffer.as_deref().map(wgpu::Buffer::size).unwrap_or(0); + let size = alignment + .round_up(T::min_size().get()) + .checked_mul(max_count as u64) + .unwrap(); + + if capacity < size || self.changed { + let buffer = device.create_buffer(&BufferDescriptor { + label: self.label.as_deref(), + usage: self.buffer_usage, + size, + mapped_at_creation: false, + }); + capacity = buffer.size(); + self.buffer = Some(buffer); + self.changed = false; + } + + if let Some(buffer) = self.buffer.as_deref() { + let buffer_view = queue + .write_buffer_with(buffer, 0, NonZeroU64::new(buffer.size())?) + .unwrap(); + Some(DynamicUniformBufferWriter { + buffer: encase::DynamicUniformBuffer::new_with_alignment( + QueueWriteBufferViewWrapper { + capacity: capacity as usize, + buffer_view, + }, + alignment.get(), + ), + _marker: PhantomData, + }) + } else { + None + } + } + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`](crate::renderer::RenderDevice) /// and the provided [`RenderQueue`](crate::renderer::RenderQueue). /// @@ -268,3 +332,38 @@ impl DynamicUniformBuffer { self.scratch.set_offset(0); } } + +/// A writer that can be used to directly write elements into the target buffer. +/// +/// For more information, see [`DynamicUniformBuffer::get_writer`]. +pub struct DynamicUniformBufferWriter<'a, T> { + buffer: encase::DynamicUniformBuffer>, + _marker: PhantomData T>, +} + +impl<'a, T: ShaderType + WriteInto> DynamicUniformBufferWriter<'a, T> { + pub fn write(&mut self, value: &T) -> u32 { + self.buffer.write(value).unwrap() as u32 + } +} + +/// A wrapper to work around the orphan rule so that [`wgpu::QueueWriteBufferView`] can implement +/// [`encase::internal::BufferMut`]. +struct QueueWriteBufferViewWrapper<'a> { + buffer_view: wgpu::QueueWriteBufferView<'a>, + // Must be kept separately and cannot be retrieved from buffer_view, as the read-only access will + // invoke a panic. + capacity: usize, +} + +impl<'a> BufferMut for QueueWriteBufferViewWrapper<'a> { + #[inline] + fn capacity(&self) -> usize { + self.capacity + } + + #[inline] + fn write(&mut self, offset: usize, val: &[u8; N]) { + self.buffer_view.write(offset, val); + } +} diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 012a62f76d80c..9759684148324 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -356,8 +356,15 @@ pub fn prepare_view_uniforms( Option<&MipBias>, )>, ) { - view_uniforms.uniforms.clear(); - + let view_iter = views.iter(); + let view_count = view_iter.len(); + let Some(mut writer) = + view_uniforms + .uniforms + .get_writer(view_count, &render_device, &render_queue) + else { + return; + }; for (entity, camera, temporal_jitter, mip_bias) in &views { let viewport = camera.viewport.as_vec4(); let unjittered_projection = camera.projection; @@ -380,7 +387,7 @@ pub fn prepare_view_uniforms( }; let view_uniforms = ViewUniformOffset { - offset: view_uniforms.uniforms.push(ViewUniform { + offset: writer.write(&ViewUniform { view_proj, unjittered_view_proj: unjittered_projection * inverse_view, inverse_view_proj: view * inverse_projection, @@ -397,10 +404,6 @@ pub fn prepare_view_uniforms( commands.entity(entity).insert(view_uniforms); } - - view_uniforms - .uniforms - .write_buffer(&render_device, &render_queue); } #[derive(Clone)] diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index d7d2f8fb6c8da..c4607fbcf87ce 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -15,6 +15,7 @@ multi-threaded = [] futures-lite = "1.4.0" async-executor = "1.3.0" async-channel = "1.4.2" +async-io = { version = "1.13.0", optional = true } async-task = "4.2.0" concurrent-queue = "2.0.0" diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index b97d2e9df466b..d4b68e2096bc6 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -28,6 +28,11 @@ mod thread_executor; #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] pub use thread_executor::{ThreadExecutor, ThreadExecutorTicker}; +#[cfg(feature = "async-io")] +pub use async_io::block_on; +#[cfg(not(feature = "async-io"))] +pub use futures_lite::future::block_on; + mod iter; pub use iter::ParallelIterator; @@ -35,6 +40,7 @@ pub use iter::ParallelIterator; pub mod prelude { #[doc(hidden)] pub use crate::{ + block_on, iter::ParallelIterator, slice::{ParallelSlice, ParallelSliceMut}, usages::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool}, diff --git a/crates/bevy_tasks/src/task_pool.rs b/crates/bevy_tasks/src/task_pool.rs index 90afe4b4bc2e3..5562a5abc0a47 100644 --- a/crates/bevy_tasks/src/task_pool.rs +++ b/crates/bevy_tasks/src/task_pool.rs @@ -9,9 +9,10 @@ use std::{ use async_task::FallibleTask; use concurrent_queue::ConcurrentQueue; -use futures_lite::{future, FutureExt}; +use futures_lite::FutureExt; use crate::{ + block_on, thread_executor::{ThreadExecutor, ThreadExecutorTicker}, Task, }; @@ -176,7 +177,7 @@ impl TaskPool { local_executor.tick().await; } }; - future::block_on(ex.run(tick_forever.or(shutdown_rx.recv()))) + block_on(ex.run(tick_forever.or(shutdown_rx.recv()))) }); if let Ok(value) = res { // Use unwrap_err because we expect a Closed error @@ -379,7 +380,7 @@ impl TaskPool { if spawned.is_empty() { Vec::new() } else { - future::block_on(async move { + block_on(async move { let get_results = async { let mut results = Vec::with_capacity(spawned.len()); while let Ok(task) = spawned.pop() { @@ -661,7 +662,7 @@ where T: 'scope, { fn drop(&mut self) { - future::block_on(async { + block_on(async { while let Ok(task) = self.spawned.pop() { task.cancel().await; } diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index f0ca798d8934b..730d978c1477c 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -4,6 +4,7 @@ use bevy_asset::{AssetEvent, AssetId}; use bevy_asset::{Assets, Handle}; use bevy_ecs::prelude::*; use bevy_math::Vec2; +use bevy_reflect::Reflect; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; use bevy_utils::FloatOrd; @@ -40,7 +41,7 @@ pub struct FontAtlasSet { font_atlases: HashMap>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Reflect)] pub struct GlyphAtlasInfo { pub texture_atlas: Handle, pub glyph_index: usize, diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs index fe1e2d7203a1b..9aab58ee977f8 100644 --- a/crates/bevy_text/src/glyph_brush.rs +++ b/crates/bevy_text/src/glyph_brush.rs @@ -1,6 +1,7 @@ use ab_glyph::{Font as _, FontArc, Glyph, PxScaleFont, ScaleFont as _}; use bevy_asset::{AssetId, Assets}; use bevy_math::{Rect, Vec2}; +use bevy_reflect::Reflect; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; use bevy_utils::tracing::warn; @@ -158,7 +159,7 @@ impl GlyphBrush { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Reflect)] pub struct PositionedGlyph { pub position: Vec2, pub size: Vec2, diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 6a83a446479b6..a93aa546f913f 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -6,8 +6,11 @@ use crate::{ use ab_glyph::PxScale; use bevy_asset::{AssetId, Assets, Handle}; use bevy_ecs::component::Component; +use bevy_ecs::prelude::ReflectComponent; use bevy_ecs::system::Resource; use bevy_math::Vec2; +use bevy_reflect::prelude::ReflectDefault; +use bevy_reflect::Reflect; use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; use bevy_utils::HashMap; @@ -22,7 +25,8 @@ pub struct TextPipeline { /// Render information for a corresponding [`Text`](crate::Text) component. /// /// Contains scaled glyphs and their size. Generated via [`TextPipeline::queue_text`]. -#[derive(Component, Clone, Default, Debug)] +#[derive(Component, Clone, Default, Debug, Reflect)] +#[reflect(Component, Default)] pub struct TextLayoutInfo { pub glyphs: Vec, pub logical_size: Vec2, diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 90603577b1d65..fbf7a986381d6 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -551,8 +551,8 @@ mod tests { world.despawn(ui_entity); - // `ui_layout_system` will recieve a `RemovedComponents` event for `ui_entity` - // and remove `ui_entity` from `ui_node` from the internal layout tree + // `ui_layout_system` will receive a `RemovedComponents` event for `ui_entity` + // and remove `ui_entity` from `ui_node` from the internal layout tree ui_schedule.run(&mut world); let ui_surface = world.resource::(); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index ae9f9f41885d9..1cee4a52a1c05 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -14,6 +14,8 @@ pub mod widget; use bevy_derive::{Deref, DerefMut}; use bevy_reflect::Reflect; #[cfg(feature = "bevy_text")] +use bevy_text::TextLayoutInfo; +#[cfg(feature = "bevy_text")] mod accessibility; mod focus; mod geometry; @@ -40,6 +42,8 @@ pub mod prelude { } use crate::prelude::UiCameraConfig; +#[cfg(feature = "bevy_text")] +use crate::widget::TextFlags; use bevy_app::prelude::*; use bevy_asset::Assets; use bevy_ecs::prelude::*; @@ -126,6 +130,10 @@ impl Plugin for UiPlugin { PreUpdate, ui_focus_system.in_set(UiSystem::Focus).after(InputSystem), ); + + #[cfg(feature = "bevy_text")] + app.register_type::() + .register_type::(); // add these systems to front because these must run before transform update systems #[cfg(feature = "bevy_text")] app.add_systems( diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 7488d5dc41271..ac527d2172f80 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -159,6 +159,9 @@ pub struct AtlasImageBundle { #[cfg(feature = "bevy_text")] /// A UI node that is text +/// +/// The positioning of this node is controlled by the UI layout system. If you need manual control, +/// use [`Text2dBundle`](bevy_text::Text2dBundle). #[derive(Bundle, Debug)] pub struct TextBundle { /// Describes the logical size of the node diff --git a/crates/bevy_utils/src/label.rs b/crates/bevy_utils/src/label.rs index 019965804e58f..9d993c5993809 100644 --- a/crates/bevy_utils/src/label.rs +++ b/crates/bevy_utils/src/label.rs @@ -73,7 +73,7 @@ macro_rules! define_boxed_label { ($label_trait_name:ident) => { /// A strongly-typed label. pub trait $label_trait_name: 'static + Send + Sync + ::std::fmt::Debug { - /// Return's the [`TypeId`] of this label, or the the ID of the + /// Return's the [`TypeId`] of this label, or the ID of the /// wrapped label type for `Box` diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 10bdd8fe8b5ca..b77b633dae62b 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1054,7 +1054,7 @@ pub struct EnabledButtons { /// /// macOS note: When [`Window`] `resizable` member is set to `false` /// the maximize button will be disabled regardless of this value. - /// Additionaly, when `resizable` is set to `true` the window will + /// Additionally, when `resizable` is set to `true` the window will /// be maximized when its bar is double-clicked regardless of whether /// the maximize button is enabled or not. pub maximize: bool, diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index d6ea22e368388..a13c158709bfe 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -67,7 +67,7 @@ pub static ANDROID_APP: std::sync::OnceLock = std::sync::OnceLock::n /// events. /// /// This plugin will add systems and resources that sync with the `winit` backend and also -/// replace the exising [`App`] runner with one that constructs an [event loop](EventLoop) to +/// replace the existing [`App`] runner with one that constructs an [event loop](EventLoop) to /// receive window and input events from the OS. #[derive(Default)] pub struct WinitPlugin; @@ -366,7 +366,7 @@ pub fn winit_runner(mut app: App) { match event { event::Event::NewEvents(start_cause) => match start_cause { StartCause::Init => { - #[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] + #[cfg(any(target_os = "ios", target_os = "macos"))] { #[cfg(not(target_arch = "wasm32"))] let ( diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 9c7ee1701fa5d..f3d618fb0da5e 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -43,6 +43,7 @@ The default feature set enables most of the expected features of a game engine, |feature name|description| |-|-| |accesskit_unix|Enable AccessKit on Unix backends (currently only works with experimental screen readers and forks.)| +|async-io|Use async-io's implementation of block_on instead of futures-lite's implementation. This is preferred if your application uses async-io.| |basis-universal|Basis Universal compressed texture support| |bevy_ci_testing|Enable systems that allow for automated testing on CI| |bevy_dynamic_plugin|Plugin for dynamic loading (using [libloading](https://crates.io/crates/libloading))| diff --git a/examples/async_tasks/async_compute.rs b/examples/async_tasks/async_compute.rs index 7ab793809776e..9fad4d30a9c2e 100644 --- a/examples/async_tasks/async_compute.rs +++ b/examples/async_tasks/async_compute.rs @@ -3,7 +3,7 @@ use bevy::{ prelude::*, - tasks::{AsyncComputeTaskPool, Task}, + tasks::{block_on, AsyncComputeTaskPool, Task}, }; use futures_lite::future; use rand::Rng; @@ -88,7 +88,7 @@ fn handle_tasks( box_material_handle: Res, ) { for (entity, mut task) in &mut transform_tasks { - if let Some(transform) = future::block_on(future::poll_once(&mut task.0)) { + if let Some(transform) = block_on(future::poll_once(&mut task.0)) { // Add our new PbrBundle of components to our tagged entity commands.entity(entity).insert(PbrBundle { mesh: box_mesh_handle.clone(),