From f26b438c220edb811bde9e437ce10a11d226a467 Mon Sep 17 00:00:00 2001 From: James Liu Date: Thu, 8 Feb 2024 16:34:04 -0800 Subject: [PATCH 01/13] Cache the QueryState used to drop swapchain TextureViews (#11781) # Objective While profiling around to validate the results of #9172, I noticed that `present_frames` can take a significant amount of time. Digging into the cause, it seems like we're creating a new `QueryState` from scratch every frame. This involves scanning the entire World's metadata instead of just updating its view of the world. ## Solution Use a `SystemState` argument to cache the `QueryState` to avoid this construction cost. ## Performance Against `many_foxes`, this seems to cut the time spent in `present_frames` by nearly almost 2x. Yellow is this PR, red is main. ![image](https://github.com/bevyengine/bevy/assets/3137680/2b02bbe0-6219-4255-958d-b690e37e7fba) --- crates/bevy_render/src/renderer/mod.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 3fae24e1e477d..c645f0811f0f0 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -13,7 +13,7 @@ use crate::{ settings::{WgpuSettings, WgpuSettingsPriority}, view::{ExtractedWindows, ViewTarget}, }; -use bevy_ecs::prelude::*; +use bevy_ecs::{prelude::*, system::SystemState}; use bevy_time::TimeSender; use bevy_utils::Instant; use std::sync::Arc; @@ -22,7 +22,7 @@ use wgpu::{ }; /// Updates the [`RenderGraph`] with all of its nodes and then runs it to render the entire frame. -pub fn render_system(world: &mut World) { +pub fn render_system(world: &mut World, state: &mut SystemState>>) { world.resource_scope(|world, mut graph: Mut| { graph.update(world); }); @@ -59,10 +59,7 @@ pub fn render_system(world: &mut World) { // Remove ViewTarget components to ensure swap chain TextureViews are dropped. // If all TextureViews aren't dropped before present, acquiring the next swap chain texture will fail. - let view_entities = world - .query_filtered::>() - .iter(world) - .collect::>(); + let view_entities = state.get(world).iter().collect::>(); for view_entity in view_entities { world.entity_mut(view_entity).remove::(); } From 5313730534684408b6281fb2727144d6890524b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 22:01:31 -0800 Subject: [PATCH 02/13] Bump peter-evans/create-pull-request from 5 to 6 (#11712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 5 to 6.
Release notes

Sourced from peter-evans/create-pull-request's releases.

Create Pull Request v6.0.0

Behaviour changes

  • The default values for author and committer have changed. See "What's new" below for details. If you are overriding the default values you will not be affected by this change.
  • On completion, the action now removes the temporary git remote configuration it adds when using push-to-fork. This should not affect you unless you were using the temporary configuration for some other purpose after the action completes.

What's new

  • Updated runtime to Node.js 20
    • The action now requires a minimum version of v2.308.0 for the Actions runner. Update self-hosted runners to v2.308.0 or later to ensure compatibility.
  • The default value for author has been changed to ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>. The change adds the ${{ github.actor_id }}+ prefix to the email address to align with GitHub's standard format for the author email address.
  • The default value for committer has been changed to github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>. This is to align with the default GitHub Actions bot user account.
  • Adds input git-token, the Personal Access Token (PAT) that the action will use for git operations. This input defaults to the value of token. Use this input if you would like the action to use a different token for git operations than the one used for the GitHub API.
  • push-to-fork now supports pushing to sibling repositories in the same network.
  • Previously, when using push-to-fork, the action did not remove temporary git remote configuration it adds during execution. This has been fixed and the configuration is now removed when the action completes.
  • If the pull request body is truncated due to exceeding the maximum length, the action will now suffix the body with the message "...[Pull request body truncated]" to indicate that the body has been truncated.
  • The action now uses --unshallow only when necessary, rather than as a default argument of git fetch. This should improve performance, particularly for large git repositories with extensive commit history.
  • The action can now be executed on one GitHub server and create pull requests on a different GitHub server. Server products include GitHub hosted (github.com), GitHub Enterprise Server (GHES), and GitHub Enterprise Cloud (GHEC). For example, the action can be executed on GitHub hosted and create pull requests on a GHES or GHEC instance.

What's Changed

New Contributors

Full Changelog: https://github.com/peter-evans/create-pull-request/compare/v5.0.2...v6.0.0

Create Pull Request v5.0.2

⚙️ Fixes an issue that occurs when using push-to-fork and both base and head repositories are in the same org/user account.

What's Changed

Full Changelog: https://github.com/peter-evans/create-pull-request/compare/v5.0.1...v5.0.2

Create Pull Request v5.0.1

What's Changed

Full Changelog: https://github.com/peter-evans/create-pull-request/compare/v5.0.0...v5.0.1

Commits
  • b1ddad2 feat: v6 (#2717)
  • bb80902 build(deps-dev): bump @​types/node from 18.19.8 to 18.19.10 (#2712)
  • e0037d4 build(deps): bump peter-evans/create-or-update-comment from 3 to 4 (#2702)
  • 94b1f99 build(deps): bump peter-evans/find-comment from 2 to 3 (#2703)
  • 69c27ea build(deps-dev): bump ts-jest from 29.1.1 to 29.1.2 (#2685)
  • 7ea722a build(deps-dev): bump prettier from 3.2.2 to 3.2.4 (#2684)
  • 5ee839a build(deps-dev): bump @​types/node from 18.19.7 to 18.19.8 (#2683)
  • 60fc256 build(deps-dev): bump eslint-plugin-prettier from 5.1.2 to 5.1.3 (#2660)
  • 0c67723 build(deps-dev): bump @​types/node from 18.19.5 to 18.19.7 (#2661)
  • 4e288e8 build(deps-dev): bump prettier from 3.1.1 to 3.2.2 (#2659)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=peter-evans/create-pull-request&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/post-release.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index a7cce2e0380e7..b9adb6f38e545 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -49,7 +49,7 @@ jobs: --exclude build-wasm-example - name: Create PR - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: delete-branch: true base: "main" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20c3ffa04d59f..81d88fd58367f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: --exclude build-wasm-example - name: Create PR - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: delete-branch: true base: "main" From f4dab8a4e86f965919dc7525d72dff51be70b857 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Thu, 8 Feb 2024 23:35:35 -0800 Subject: [PATCH 03/13] Multithreaded render command encoding (#9172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Encoding many GPU commands (such as in a renderpass with many draws, such as the main opaque pass) onto a `wgpu::CommandEncoder` is very expensive, and takes a long time. - To improve performance, we want to perform the command encoding for these heavy passes in parallel. ## Solution - `RenderContext` can now queue up "command buffer generation tasks" which are closures that will generate a command buffer when called. - When finalizing the render context to produce the final list of command buffers, these tasks are run in parallel on the `ComputeTaskPool` to produce their corresponding command buffers. - The general idea is that the node graph will run in serial, but in a node, instead of doing rendering work, you can add tasks to do render work in parallel with other node's tasks that get ran at the end of the graph execution. ## Nodes Parallelized - `MainOpaquePass3dNode` - `PrepassNode` - `DeferredGBufferPrepassNode` - `ShadowPassNode` (One task per view) ## Future Work - For large number of draws calls, might be worth further subdividing passes into 2+ tasks. - Extend this to UI, 2d, transparent, and transmissive nodes? - Needs testing - small command buffers are inefficient - it may be worth reverting to the serial command encoder usage for render phases with few items. - All "serial" (traditional) rendering work must finish before parallel rendering tasks (the new stuff) can start to run. - There is still only one submission to the graphics queue at the end of the graph execution. There is still no ability to submit work earlier. ## Performance Improvement Thanks to @Elabajaba for testing on Bistro. ![image](https://github.com/bevyengine/bevy/assets/47158642/be50dafa-85eb-4da5-a5cd-c0a044f1e76f) TLDR: Without shadow mapping, this PR has no impact. _With_ shadow mapping, this PR gives **~40 more fps** than main. --- ## Changelog - `MainOpaquePass3dNode`, `PrepassNode`, `DeferredGBufferPrepassNode`, and each shadow map within `ShadowPassNode` are now encoded in parallel, giving _greatly_ increased CPU performance, mainly when shadow mapping is enabled. - Does not work on WASM or AMD+Windows+Vulkan. - Added `RenderContext::add_command_buffer_generation_task()`. - `RenderContext::new()` now takes adapter info - Some render graph and Node related types and methods now have additional lifetime constraints. ## Migration Guide `RenderContext::new()` now takes adapter info - Some render graph and Node related types and methods now have additional lifetime constraints. --------- Co-authored-by: Elabajaba Co-authored-by: François --- .../src/core_3d/main_opaque_pass_3d_node.rs | 101 ++++++++++-------- .../bevy_core_pipeline/src/deferred/node.rs | 66 +++++++----- crates/bevy_core_pipeline/src/prepass/node.rs | 77 +++++++------ crates/bevy_pbr/src/render/light.rs | 36 ++++--- crates/bevy_render/src/render_graph/node.rs | 20 ++-- .../bevy_render/src/renderer/graph_runner.rs | 9 +- crates/bevy_render/src/renderer/mod.rs | 88 +++++++++++++-- 7 files changed, 260 insertions(+), 137 deletions(-) diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs index 856040e3cf442..5b7d1315e8849 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs @@ -6,8 +6,8 @@ use bevy_ecs::{prelude::World, query::QueryItem}; use bevy_render::{ camera::ExtractedCamera, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, - render_phase::RenderPhase, - render_resource::{PipelineCache, RenderPassDescriptor, StoreOp}, + render_phase::{RenderPhase, TrackedRenderPass}, + render_resource::{CommandEncoderDescriptor, PipelineCache, RenderPassDescriptor, StoreOp}, renderer::RenderContext, view::{ViewDepthTexture, ViewTarget, ViewUniformOffset}, }; @@ -31,10 +31,10 @@ impl ViewNode for MainOpaquePass3dNode { &'static ViewUniformOffset, ); - fn run( + fn run<'w>( &self, graph: &mut RenderGraphContext, - render_context: &mut RenderContext, + render_context: &mut RenderContext<'w>, ( camera, opaque_phase, @@ -44,52 +44,69 @@ impl ViewNode for MainOpaquePass3dNode { skybox_pipeline, skybox_bind_group, view_uniform_offset, - ): QueryItem, - world: &World, + ): QueryItem<'w, Self::ViewQuery>, + world: &'w World, ) -> Result<(), NodeRunError> { - // Run the opaque pass, sorted by pipeline key and mesh id to greatly improve batching. - // NOTE: Scoped to drop the mutable borrow of render_context - #[cfg(feature = "trace")] - let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered(); + let color_attachments = [Some(target.get_color_attachment())]; + let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store)); - // Setup render pass - let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { - label: Some("main_opaque_pass_3d"), - color_attachments: &[Some(target.get_color_attachment())], - depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)), - timestamp_writes: None, - occlusion_query_set: None, - }); + let view_entity = graph.view_entity(); + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered(); - if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); - } + // Command encoder setup + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("main_opaque_pass_3d_command_encoder"), + }); - let view_entity = graph.view_entity(); + // Render pass setup + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("main_opaque_pass_3d"), + color_attachments: &color_attachments, + depth_stencil_attachment, + timestamp_writes: None, + occlusion_query_set: None, + }); + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } - // Opaque draws - opaque_phase.render(&mut render_pass, world, view_entity); + // Opaque draws + if !opaque_phase.items.is_empty() { + #[cfg(feature = "trace")] + let _opaque_main_pass_3d_span = info_span!("opaque_main_pass_3d").entered(); + opaque_phase.render(&mut render_pass, world, view_entity); + } - // Alpha draws - if !alpha_mask_phase.items.is_empty() { - alpha_mask_phase.render(&mut render_pass, world, view_entity); - } + // Alpha draws + if !alpha_mask_phase.items.is_empty() { + #[cfg(feature = "trace")] + let _alpha_mask_main_pass_3d_span = info_span!("alpha_mask_main_pass_3d").entered(); + alpha_mask_phase.render(&mut render_pass, world, view_entity); + } - // Draw the skybox using a fullscreen triangle - if let (Some(skybox_pipeline), Some(SkyboxBindGroup(skybox_bind_group))) = - (skybox_pipeline, skybox_bind_group) - { - let pipeline_cache = world.resource::(); - if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_pipeline.0) { - render_pass.set_render_pipeline(pipeline); - render_pass.set_bind_group( - 0, - &skybox_bind_group.0, - &[view_uniform_offset.offset, skybox_bind_group.1], - ); - render_pass.draw(0..3, 0..1); + // Skybox draw using a fullscreen triangle + if let (Some(skybox_pipeline), Some(SkyboxBindGroup(skybox_bind_group))) = + (skybox_pipeline, skybox_bind_group) + { + let pipeline_cache = world.resource::(); + if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_pipeline.0) { + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group( + 0, + &skybox_bind_group.0, + &[view_uniform_offset.offset, skybox_bind_group.1], + ); + render_pass.draw(0..3, 0..1); + } } - } + + drop(render_pass); + command_encoder.finish() + }); Ok(()) } diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index 4e1005febf186..c2acfe53861cd 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -2,7 +2,8 @@ use bevy_ecs::prelude::*; use bevy_ecs::query::QueryItem; use bevy_render::render_graph::ViewNode; -use bevy_render::render_resource::StoreOp; +use bevy_render::render_phase::TrackedRenderPass; +use bevy_render::render_resource::{CommandEncoderDescriptor, StoreOp}; use bevy_render::{ camera::ExtractedCamera, render_graph::{NodeRunError, RenderGraphContext}, @@ -33,21 +34,19 @@ impl ViewNode for DeferredGBufferPrepassNode { &'static ViewPrepassTextures, ); - fn run( + fn run<'w>( &self, graph: &mut RenderGraphContext, - render_context: &mut RenderContext, + render_context: &mut RenderContext<'w>, ( camera, opaque_deferred_phase, alpha_mask_deferred_phase, view_depth_texture, view_prepass_textures, - ): QueryItem, - world: &World, + ): QueryItem<'w, Self::ViewQuery>, + world: &'w World, ) -> Result<(), NodeRunError> { - let view_entity = graph.view_entity(); - let mut color_attachments = vec![]; color_attachments.push( view_prepass_textures @@ -107,49 +106,64 @@ impl ViewNode for DeferredGBufferPrepassNode { .map(|deferred_lighting_pass_id| deferred_lighting_pass_id.get_attachment()), ); + // If all color attachments are none: clear the color attachment list so that no fragment shader is required if color_attachments.iter().all(Option::is_none) { - // All attachments are none: clear the attachment list so that no fragment shader is required. color_attachments.clear(); } - { - // Set up the pass descriptor with the depth attachment and optional color attachments. - let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + let depth_stencil_attachment = Some(view_depth_texture.get_attachment(StoreOp::Store)); + + let view_entity = graph.view_entity(); + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _deferred_span = info_span!("deferred").entered(); + + // Command encoder setup + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("deferred_command_encoder"), + }); + + // Render pass setup + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { label: Some("deferred"), color_attachments: &color_attachments, - depth_stencil_attachment: Some(view_depth_texture.get_attachment(StoreOp::Store)), + depth_stencil_attachment, timestamp_writes: None, occlusion_query_set: None, }); - + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); } - // Always run deferred pass to ensure the deferred gbuffer and deferred_lighting_pass_id are cleared. - { - // Run the prepass, sorted front-to-back. + // Opaque draws + if !opaque_deferred_phase.items.is_empty() { #[cfg(feature = "trace")] let _opaque_prepass_span = info_span!("opaque_deferred").entered(); opaque_deferred_phase.render(&mut render_pass, world, view_entity); } + // Alpha masked draws if !alpha_mask_deferred_phase.items.is_empty() { - // Run the deferred, sorted front-to-back. #[cfg(feature = "trace")] let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred").entered(); alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity); } - } - if let Some(prepass_depth_texture) = &view_prepass_textures.depth { - // Copy depth buffer to texture. - render_context.command_encoder().copy_texture_to_texture( - view_depth_texture.texture.as_image_copy(), - prepass_depth_texture.texture.texture.as_image_copy(), - view_prepass_textures.size, - ); - } + drop(render_pass); + + // Copy prepass depth to the main depth texture + if let Some(prepass_depth_texture) = &view_prepass_textures.depth { + command_encoder.copy_texture_to_texture( + view_depth_texture.texture.as_image_copy(), + prepass_depth_texture.texture.texture.as_image_copy(), + view_prepass_textures.size, + ); + } + + command_encoder.finish() + }); Ok(()) } diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index baaed25bd2871..928183ad7d010 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -1,12 +1,10 @@ use bevy_ecs::prelude::*; use bevy_ecs::query::QueryItem; -use bevy_render::render_graph::ViewNode; -use bevy_render::render_resource::StoreOp; use bevy_render::{ camera::ExtractedCamera, - render_graph::{NodeRunError, RenderGraphContext}, - render_phase::RenderPhase, - render_resource::RenderPassDescriptor, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_phase::{RenderPhase, TrackedRenderPass}, + render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp}, renderer::RenderContext, view::ViewDepthTexture, }; @@ -31,10 +29,10 @@ impl ViewNode for PrepassNode { Option<&'static DeferredPrepass>, ); - fn run( + fn run<'w>( &self, graph: &mut RenderGraphContext, - render_context: &mut RenderContext, + render_context: &mut RenderContext<'w>, ( camera, opaque_prepass_phase, @@ -42,11 +40,9 @@ impl ViewNode for PrepassNode { view_depth_texture, view_prepass_textures, deferred_prepass, - ): QueryItem, - world: &World, + ): QueryItem<'w, Self::ViewQuery>, + world: &'w World, ) -> Result<(), NodeRunError> { - let view_entity = graph.view_entity(); - let mut color_attachments = vec![ view_prepass_textures .normal @@ -56,55 +52,72 @@ impl ViewNode for PrepassNode { .motion_vectors .as_ref() .map(|motion_vectors_texture| motion_vectors_texture.get_attachment()), - // Use None in place of Deferred attachments + // Use None in place of deferred attachments None, None, ]; + // If all color attachments are none: clear the color attachment list so that no fragment shader is required if color_attachments.iter().all(Option::is_none) { - // all attachments are none: clear the attachment list so that no fragment shader is required color_attachments.clear(); } - { - // Set up the pass descriptor with the depth attachment and optional color attachments - let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + let depth_stencil_attachment = Some(view_depth_texture.get_attachment(StoreOp::Store)); + + let view_entity = graph.view_entity(); + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _prepass_span = info_span!("prepass").entered(); + + // Command encoder setup + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("prepass_command_encoder"), + }); + + // Render pass setup + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { label: Some("prepass"), color_attachments: &color_attachments, - depth_stencil_attachment: Some(view_depth_texture.get_attachment(StoreOp::Store)), + depth_stencil_attachment, timestamp_writes: None, occlusion_query_set: None, }); + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); } - // Always run opaque pass to ensure screen is cleared - { - // Run the prepass, sorted front-to-back + // Opaque draws + if !opaque_prepass_phase.items.is_empty() { #[cfg(feature = "trace")] let _opaque_prepass_span = info_span!("opaque_prepass").entered(); opaque_prepass_phase.render(&mut render_pass, world, view_entity); } + // Alpha masked draws if !alpha_mask_prepass_phase.items.is_empty() { - // Run the prepass, sorted front-to-back #[cfg(feature = "trace")] let _alpha_mask_prepass_span = info_span!("alpha_mask_prepass").entered(); alpha_mask_prepass_phase.render(&mut render_pass, world, view_entity); } - } - if deferred_prepass.is_none() { - // Copy if deferred isn't going to - if let Some(prepass_depth_texture) = &view_prepass_textures.depth { - // Copy depth buffer to texture - render_context.command_encoder().copy_texture_to_texture( - view_depth_texture.texture.as_image_copy(), - prepass_depth_texture.texture.texture.as_image_copy(), - view_prepass_textures.size, - ); + + drop(render_pass); + + // Copy prepass depth to the main depth texture if deferred isn't going to + if deferred_prepass.is_none() { + if let Some(prepass_depth_texture) = &view_prepass_textures.depth { + command_encoder.copy_texture_to_texture( + view_depth_texture.texture.as_image_copy(), + prepass_depth_texture.texture.texture.as_image_copy(), + view_prepass_textures.size, + ); + } } - } + + command_encoder.finish() + }); + Ok(()) } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index ae6c97995d65e..fa40a03a37c56 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -16,6 +16,8 @@ use bevy_render::{ Extract, }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; use bevy_utils::{ nonmax::NonMaxU32, tracing::{error, warn}, @@ -1780,11 +1782,11 @@ impl Node for ShadowPassNode { self.view_light_query.update_archetypes(world); } - fn run( + fn run<'w>( &self, graph: &mut RenderGraphContext, - render_context: &mut RenderContext, - world: &World, + render_context: &mut RenderContext<'w>, + world: &'w World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); if let Ok(view_lights) = self.main_view_query.get_manual(world, view_entity) { @@ -1794,22 +1796,32 @@ impl Node for ShadowPassNode { .get_manual(world, view_light_entity) .unwrap(); - if shadow_phase.items.is_empty() { - continue; - } + let depth_stencil_attachment = + Some(view_light.depth_attachment.get_attachment(StoreOp::Store)); + + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _shadow_pass_span = info_span!("shadow_pass").entered(); + + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("shadow_pass_command_encoder"), + }); - let mut render_pass = - render_context.begin_tracked_render_pass(RenderPassDescriptor { + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { label: Some(&view_light.pass_name), color_attachments: &[], - depth_stencil_attachment: Some( - view_light.depth_attachment.get_attachment(StoreOp::Store), - ), + depth_stencil_attachment, timestamp_writes: None, occlusion_query_set: None, }); + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); - shadow_phase.render(&mut render_pass, world, view_light_entity); + shadow_phase.render(&mut render_pass, world, view_light_entity); + + drop(render_pass); + command_encoder.finish() + }); } } diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index 7dc07fcd24ced..577071759ce9c 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -77,11 +77,11 @@ pub trait Node: Downcast + Send + Sync + 'static { /// Runs the graph node logic, issues draw calls, updates the output slots and /// optionally queues up subgraphs for execution. The graph data, input and output values are /// passed via the [`RenderGraphContext`]. - fn run( + fn run<'w>( &self, graph: &mut RenderGraphContext, - render_context: &mut RenderContext, - world: &World, + render_context: &mut RenderContext<'w>, + world: &'w World, ) -> Result<(), NodeRunError>; } @@ -346,12 +346,12 @@ pub trait ViewNode { /// Runs the graph node logic, issues draw calls, updates the output slots and /// optionally queues up subgraphs for execution. The graph data, input and output values are /// passed via the [`RenderGraphContext`]. - fn run( + fn run<'w>( &self, graph: &mut RenderGraphContext, - render_context: &mut RenderContext, - view_query: QueryItem, - world: &World, + render_context: &mut RenderContext<'w>, + view_query: QueryItem<'w, Self::ViewQuery>, + world: &'w World, ) -> Result<(), NodeRunError>; } @@ -388,11 +388,11 @@ where self.node.update(world); } - fn run( + fn run<'w>( &self, graph: &mut RenderGraphContext, - render_context: &mut RenderContext, - world: &World, + render_context: &mut RenderContext<'w>, + world: &'w World, ) -> Result<(), NodeRunError> { let Ok(view) = self.view_query.get_manual(world, graph.view_entity()) else { return Ok(()); diff --git a/crates/bevy_render/src/renderer/graph_runner.rs b/crates/bevy_render/src/renderer/graph_runner.rs index 49319127bd96b..3db56919b1247 100644 --- a/crates/bevy_render/src/renderer/graph_runner.rs +++ b/crates/bevy_render/src/renderer/graph_runner.rs @@ -57,10 +57,11 @@ impl RenderGraphRunner { graph: &RenderGraph, render_device: RenderDevice, queue: &wgpu::Queue, + adapter: &wgpu::Adapter, world: &World, finalizer: impl FnOnce(&mut wgpu::CommandEncoder), ) -> Result<(), RenderGraphRunnerError> { - let mut render_context = RenderContext::new(render_device); + let mut render_context = RenderContext::new(render_device, adapter.get_info()); Self::run_graph(graph, None, &mut render_context, world, &[], None)?; finalizer(render_context.command_encoder()); @@ -72,11 +73,11 @@ impl RenderGraphRunner { Ok(()) } - fn run_graph( + fn run_graph<'w>( graph: &RenderGraph, sub_graph: Option, - render_context: &mut RenderContext, - world: &World, + render_context: &mut RenderContext<'w>, + world: &'w World, inputs: &[SlotValue], view_entity: Option, ) -> Result<(), RenderGraphRunnerError> { diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index c645f0811f0f0..fa4377a405c47 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -2,6 +2,7 @@ mod graph_runner; mod render_device; use bevy_derive::{Deref, DerefMut}; +use bevy_tasks::ComputeTaskPool; use bevy_utils::tracing::{error, info, info_span}; pub use graph_runner::*; pub use render_device::*; @@ -29,11 +30,13 @@ pub fn render_system(world: &mut World, state: &mut SystemState(); let render_device = world.resource::(); let render_queue = world.resource::(); + let render_adapter = world.resource::(); if let Err(e) = RenderGraphRunner::run( graph, render_device.clone(), // TODO: is this clone really necessary? &render_queue.0, + &render_adapter.0, world, |encoder| { crate::view::screenshot::submit_screenshot_commands(world, encoder); @@ -298,19 +301,31 @@ pub async fn initialize_renderer( /// /// The [`RenderDevice`] is used to create render resources and the /// the [`CommandEncoder`] is used to record a series of GPU operations. -pub struct RenderContext { +pub struct RenderContext<'w> { render_device: RenderDevice, command_encoder: Option, - command_buffers: Vec, + command_buffer_queue: Vec>, + force_serial: bool, } -impl RenderContext { +impl<'w> RenderContext<'w> { /// Creates a new [`RenderContext`] from a [`RenderDevice`]. - pub fn new(render_device: RenderDevice) -> Self { + pub fn new(render_device: RenderDevice, adapter_info: AdapterInfo) -> Self { + // HACK: Parallel command encoding is currently bugged on AMD + Windows + Vulkan with wgpu 0.19.1 + #[cfg(target_os = "windows")] + let force_serial = + adapter_info.driver.contains("AMD") && adapter_info.backend == wgpu::Backend::Vulkan; + #[cfg(not(target_os = "windows"))] + let force_serial = { + drop(adapter_info); + false + }; + Self { render_device, command_encoder: None, - command_buffers: Vec::new(), + command_buffer_queue: Vec::new(), + force_serial, } } @@ -342,25 +357,76 @@ impl RenderContext { TrackedRenderPass::new(&self.render_device, render_pass) } - /// Append a [`CommandBuffer`] to the queue. + /// Append a [`CommandBuffer`] to the command buffer queue. /// /// If present, this will flush the currently unflushed [`CommandEncoder`] - /// into a [`CommandBuffer`] into the queue before append the provided + /// into a [`CommandBuffer`] into the queue before appending the provided /// buffer. pub fn add_command_buffer(&mut self, command_buffer: CommandBuffer) { self.flush_encoder(); - self.command_buffers.push(command_buffer); + + self.command_buffer_queue + .push(QueuedCommandBuffer::Ready(command_buffer)); + } + + /// Append a function that will generate a [`CommandBuffer`] to the + /// command buffer queue, to be ran later. + /// + /// If present, this will flush the currently unflushed [`CommandEncoder`] + /// into a [`CommandBuffer`] into the queue before appending the provided + /// buffer. + pub fn add_command_buffer_generation_task( + &mut self, + task: impl FnOnce(RenderDevice) -> CommandBuffer + 'w + Send, + ) { + self.flush_encoder(); + + self.command_buffer_queue + .push(QueuedCommandBuffer::Task(Box::new(task))); } - /// Finalizes the queue and returns the queue of [`CommandBuffer`]s. + /// Finalizes and returns the queue of [`CommandBuffer`]s. + /// + /// This function will wait until all command buffer generation tasks are complete + /// by running them in parallel (where supported). pub fn finish(mut self) -> Vec { self.flush_encoder(); - self.command_buffers + + let mut command_buffers = Vec::with_capacity(self.command_buffer_queue.len()); + let mut task_based_command_buffers = ComputeTaskPool::get().scope(|task_pool| { + for (i, queued_command_buffer) in self.command_buffer_queue.into_iter().enumerate() { + match queued_command_buffer { + QueuedCommandBuffer::Ready(command_buffer) => { + command_buffers.push((i, command_buffer)); + } + QueuedCommandBuffer::Task(command_buffer_generation_task) => { + let render_device = self.render_device.clone(); + if self.force_serial { + command_buffers + .push((i, command_buffer_generation_task(render_device))); + } else { + task_pool.spawn(async move { + (i, command_buffer_generation_task(render_device)) + }); + } + } + } + } + }); + command_buffers.append(&mut task_based_command_buffers); + command_buffers.sort_unstable_by_key(|(i, _)| *i); + command_buffers.into_iter().map(|(_, cb)| cb).collect() } fn flush_encoder(&mut self) { if let Some(encoder) = self.command_encoder.take() { - self.command_buffers.push(encoder.finish()); + self.command_buffer_queue + .push(QueuedCommandBuffer::Ready(encoder.finish())); } } } + +enum QueuedCommandBuffer<'w> { + Ready(CommandBuffer), + Task(Box CommandBuffer + 'w + Send>), +} From 0ebba278f764c0716e0f41c119805612f1e73c67 Mon Sep 17 00:00:00 2001 From: Doonv <58695417+doonv@users.noreply.github.com> Date: Fri, 9 Feb 2024 22:13:28 +0200 Subject: [PATCH 04/13] Ignore screenshots generated by `screenshot` example (#11797) # Objective The screenshots generated by the `screenshot` example need to be removed manually before commiting changes. ## Solution `.gitignore` the screenshots generated by the `screenshot` example. I also added the `**/` prefix so the screenshots get ignored regardless of whether of where the example is run from. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 79d54dbf2a6cb..c97fdeedaf45b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ dxil.dll # Generated by "examples/scene/scene.rs" assets/scenes/load_scene_example-new.scn.ron +# Generated by "examples/window/screenshot.rs" +**/screenshot-*.png + assets/**/*.meta crates/bevy_asset/imported_assets imported_assets From e0c296ee149341f00836664169df681833c65125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Lescaudey=20de=20Maneville?= Date: Fri, 9 Feb 2024 21:36:32 +0100 Subject: [PATCH 05/13] Optional ImageScaleMode (#11780) > Follow up to #11600 and #10588 @mockersf expressed some [valid concerns](https://github.com/bevyengine/bevy/pull/11600#issuecomment-1932796498) about the current system this PR attempts to fix: The `ComputedTextureSlices` reacts to asset change in both `bevy_sprite` and `bevy_ui`, meaning that if the `ImageScaleMode` is inserted by default in the bundles, we will iterate through most 2d items every time an asset is updated. # Solution - `ImageScaleMode` only has two variants: `Sliced` and `Tiled`. I removed the `Stretched` default - `ImageScaleMode` is no longer part of any bundle, but the relevant bundles explain that this additional component can be inserted This way, the *absence* of `ImageScaleMode` means the image will be stretched, and its *presence* will include the entity to the various slicing systems Optional components in bundles would make this more straigthfoward # Additional work Should I add new bundles with the `ImageScaleMode` component ? --- crates/bevy_sprite/src/bundle.rs | 12 ++-- crates/bevy_sprite/src/sprite.rs | 7 +-- .../src/texture_slice/computed_slices.rs | 13 ++-- crates/bevy_ui/src/node_bundles.rs | 17 ++++-- crates/bevy_ui/src/texture_slice.rs | 11 +--- examples/2d/sprite_slice.rs | 60 +++++++++---------- examples/2d/sprite_tile.rs | 12 ++-- examples/ui/ui_texture_slice.rs | 28 +++++---- 8 files changed, 77 insertions(+), 83 deletions(-) diff --git a/crates/bevy_sprite/src/bundle.rs b/crates/bevy_sprite/src/bundle.rs index e6c48ecea036e..83b86a5c854e1 100644 --- a/crates/bevy_sprite/src/bundle.rs +++ b/crates/bevy_sprite/src/bundle.rs @@ -1,4 +1,4 @@ -use crate::{texture_atlas::TextureAtlas, ImageScaleMode, Sprite}; +use crate::{Sprite, TextureAtlas}; use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; use bevy_render::{ @@ -8,12 +8,16 @@ use bevy_render::{ use bevy_transform::components::{GlobalTransform, Transform}; /// A [`Bundle`] of components for drawing a single sprite from an image. +/// +/// # Extra behaviours +/// +/// You may add the following components to enable additional behaviours +/// - [`ImageScaleMode`](crate::ImageScaleMode) to enable either slicing or tiling of the texture +/// - [`TextureAtlas`] to draw specific sections of a sprite sheet, (See [`SpriteSheetBundle`]) #[derive(Bundle, Clone, Default)] pub struct SpriteBundle { /// Specifies the rendering properties of the sprite, such as color tint and flip. pub sprite: Sprite, - /// Controls how the image is altered when scaled. - pub scale_mode: ImageScaleMode, /// The local transform of the sprite, relative to its parent. pub transform: Transform, /// The absolute transform of the sprite. This should generally not be written to directly. @@ -41,8 +45,6 @@ pub struct SpriteBundle { pub struct SpriteSheetBundle { /// Specifies the rendering properties of the sprite, such as color tint and flip. pub sprite: Sprite, - /// Controls how the image is altered when scaled. - pub scale_mode: ImageScaleMode, /// The local transform of the sprite, relative to its parent. pub transform: Transform, /// The absolute transform of the sprite. This should generally not be written to directly. diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index c62d6da82d4f3..9c164d9a2c202 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -32,12 +32,9 @@ pub struct Sprite { } /// Controls how the image is altered when scaled. -#[derive(Component, Debug, Default, Clone, Reflect)] -#[reflect(Component, Default)] +#[derive(Component, Debug, Clone, Reflect)] +#[reflect(Component)] pub enum ImageScaleMode { - /// The entire texture stretches when its dimensions change. This is the default option. - #[default] - Stretched, /// The texture will be cut in 9 slices, keeping the texture in proportions on resize Sliced(TextureSlicer), /// The texture will be repeated if stretched beyond `stretched_value` diff --git a/crates/bevy_sprite/src/texture_slice/computed_slices.rs b/crates/bevy_sprite/src/texture_slice/computed_slices.rs index 4e17fd0f2d4e7..cc5954c3ccb1d 100644 --- a/crates/bevy_sprite/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite/src/texture_slice/computed_slices.rs @@ -8,7 +8,7 @@ use bevy_render::texture::Image; use bevy_transform::prelude::*; use bevy_utils::HashSet; -/// Component storing texture slices for sprite entities with a tiled or sliced [`ImageScaleMode`] +/// Component storing texture slices for sprite entities with a [`ImageScaleMode`] /// /// This component is automatically inserted and updated #[derive(Debug, Clone, Component)] @@ -62,9 +62,7 @@ impl ComputedTextureSlices { /// Generates sprite slices for a `sprite` given a `scale_mode`. The slices /// will be computed according to the `image_handle` dimensions or the sprite rect. /// -/// Returns `None` if either: -/// - The scale mode is [`ImageScaleMode::Stretched`] -/// - The image asset is not loaded +/// Returns `None` if the image asset is not loaded #[must_use] fn compute_sprite_slices( sprite: &Sprite, @@ -72,9 +70,6 @@ fn compute_sprite_slices( image_handle: &Handle, images: &Assets, ) -> Option { - if let ImageScaleMode::Stretched = scale_mode { - return None; - } let image_size = images.get(image_handle).map(|i| { Vec2::new( i.texture_descriptor.size.width as f32, @@ -82,7 +77,6 @@ fn compute_sprite_slices( ) })?; let slices = match scale_mode { - ImageScaleMode::Stretched => unreachable!(), ImageScaleMode::Sliced(slicer) => slicer.compute_slices( sprite.rect.unwrap_or(Rect { min: Vec2::ZERO, @@ -110,7 +104,7 @@ fn compute_sprite_slices( } /// System reacting to added or modified [`Image`] handles, and recompute sprite slices -/// on matching sprite entities +/// on matching sprite entities with a [`ImageScaleMode`] component pub(crate) fn compute_slices_on_asset_event( mut commands: Commands, mut events: EventReader>, @@ -140,6 +134,7 @@ pub(crate) fn compute_slices_on_asset_event( } /// System reacting to changes on relevant sprite bundle components to compute the sprite slices +/// on matching sprite entities with a [`ImageScaleMode`] component pub(crate) fn compute_slices_on_sprite_change( mut commands: Commands, images: Res>, diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 639142e18118d..964fd61c514ca 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -13,7 +13,7 @@ use bevy_render::{ prelude::Color, view::{InheritedVisibility, ViewVisibility, Visibility}, }; -use bevy_sprite::{ImageScaleMode, TextureAtlas}; +use bevy_sprite::TextureAtlas; #[cfg(feature = "bevy_text")] use bevy_text::{BreakLineOn, JustifyText, Text, TextLayoutInfo, TextSection, TextStyle}; use bevy_transform::prelude::{GlobalTransform, Transform}; @@ -76,6 +76,11 @@ impl Default for NodeBundle { } /// A UI node that is an image +/// +/// # Extra behaviours +/// +/// You may add the following components to enable additional behaviours +/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture #[derive(Bundle, Debug, Default)] pub struct ImageBundle { /// Describes the logical size of the node @@ -95,8 +100,6 @@ pub struct ImageBundle { /// /// This component is set automatically pub image_size: UiImageSize, - /// Controls how the image is altered when scaled. - pub scale_mode: ImageScaleMode, /// Whether this node should block interaction with lower nodes pub focus_policy: FocusPolicy, /// The transform of the node @@ -288,6 +291,11 @@ where } /// A UI node that is a button +/// +/// # Extra behaviours +/// +/// You may add the following components to enable additional behaviours +/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture #[derive(Bundle, Clone, Debug)] pub struct ButtonBundle { /// Describes the logical size of the node @@ -309,8 +317,6 @@ pub struct ButtonBundle { pub border_color: BorderColor, /// The image of the node pub image: UiImage, - /// Controls how the image is altered when scaled. - pub scale_mode: ImageScaleMode, /// The transform of the node /// /// This component is automatically managed by the UI layout system. @@ -347,7 +353,6 @@ impl Default for ButtonBundle { inherited_visibility: Default::default(), view_visibility: Default::default(), z_index: Default::default(), - scale_mode: ImageScaleMode::default(), } } } diff --git a/crates/bevy_ui/src/texture_slice.rs b/crates/bevy_ui/src/texture_slice.rs index 25aa791be278c..2c77f43499cf8 100644 --- a/crates/bevy_ui/src/texture_slice.rs +++ b/crates/bevy_ui/src/texture_slice.rs @@ -77,9 +77,7 @@ impl ComputedTextureSlices { /// Generates sprite slices for a `sprite` given a `scale_mode`. The slices /// will be computed according to the `image_handle` dimensions or the sprite rect. /// -/// Returns `None` if either: -/// - The scale mode is [`ImageScaleMode::Stretched`] -/// - The image asset is not loaded +/// Returns `None` if the image asset is not loaded #[must_use] fn compute_texture_slices( draw_area: Vec2, @@ -87,9 +85,6 @@ fn compute_texture_slices( image_handle: &UiImage, images: &Assets, ) -> Option { - if let ImageScaleMode::Stretched = scale_mode { - return None; - } let image_size = images.get(&image_handle.texture).map(|i| { Vec2::new( i.texture_descriptor.size.width as f32, @@ -101,7 +96,6 @@ fn compute_texture_slices( max: image_size, }; let slices = match scale_mode { - ImageScaleMode::Stretched => unreachable!(), ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)), ImageScaleMode::Tiled { tile_x, @@ -120,7 +114,7 @@ fn compute_texture_slices( } /// System reacting to added or modified [`Image`] handles, and recompute sprite slices -/// on matching sprite entities +/// on matching sprite entities with a [`ImageScaleMode`] component pub(crate) fn compute_slices_on_asset_event( mut commands: Commands, mut events: EventReader>, @@ -157,6 +151,7 @@ pub(crate) fn compute_slices_on_asset_event( } /// System reacting to changes on relevant sprite bundle components to compute the sprite slices +/// on matching sprite entities with a [`ImageScaleMode`] component pub(crate) fn compute_slices_on_image_change( mut commands: Commands, images: Res>, diff --git a/examples/2d/sprite_slice.rs b/examples/2d/sprite_slice.rs index 34fcfbcb2bdc9..c2cf40130b091 100644 --- a/examples/2d/sprite_slice.rs +++ b/examples/2d/sprite_slice.rs @@ -25,89 +25,85 @@ fn spawn_sprites( ) { let cases = [ // Reference sprite - ( - "Original texture", - style.clone(), - Vec2::splat(100.0), - ImageScaleMode::default(), - ), + ("Original texture", style.clone(), Vec2::splat(100.0), None), // Scaled regular sprite ( "Stretched texture", style.clone(), Vec2::new(100.0, 200.0), - ImageScaleMode::default(), + None, ), // Stretched Scaled sliced sprite ( "Stretched and sliced", style.clone(), Vec2::new(100.0, 200.0), - ImageScaleMode::Sliced(TextureSlicer { + Some(ImageScaleMode::Sliced(TextureSlicer { border: BorderRect::square(slice_border), center_scale_mode: SliceScaleMode::Stretch, ..default() - }), + })), ), // Scaled sliced sprite ( "Sliced and Tiled", style.clone(), Vec2::new(100.0, 200.0), - ImageScaleMode::Sliced(TextureSlicer { + Some(ImageScaleMode::Sliced(TextureSlicer { border: BorderRect::square(slice_border), center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.5 }, sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, ..default() - }), + })), ), // Scaled sliced sprite horizontally ( "Sliced and Tiled", style.clone(), Vec2::new(300.0, 200.0), - ImageScaleMode::Sliced(TextureSlicer { + Some(ImageScaleMode::Sliced(TextureSlicer { border: BorderRect::square(slice_border), center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.3 }, ..default() - }), + })), ), // Scaled sliced sprite horizontally with max scale ( "Sliced and Tiled with corner constraint", style, Vec2::new(300.0, 200.0), - ImageScaleMode::Sliced(TextureSlicer { + Some(ImageScaleMode::Sliced(TextureSlicer { border: BorderRect::square(slice_border), center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.1 }, sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, max_corner_scale: 0.2, - }), + })), ), ]; for (label, text_style, size, scale_mode) in cases { position.x += 0.5 * size.x; - commands - .spawn(SpriteBundle { - transform: Transform::from_translation(position), - texture: texture_handle.clone(), - sprite: Sprite { - custom_size: Some(size), - ..default() - }, - scale_mode, + let mut cmd = commands.spawn(SpriteBundle { + transform: Transform::from_translation(position), + texture: texture_handle.clone(), + sprite: Sprite { + custom_size: Some(size), + ..default() + }, + ..default() + }); + if let Some(scale_mode) = scale_mode { + cmd.insert(scale_mode); + } + cmd.with_children(|builder| { + builder.spawn(Text2dBundle { + text: Text::from_section(label, text_style).with_justify(JustifyText::Center), + transform: Transform::from_xyz(0., -0.5 * size.y - 10., 0.0), + text_anchor: bevy::sprite::Anchor::TopCenter, ..default() - }) - .with_children(|builder| { - builder.spawn(Text2dBundle { - text: Text::from_section(label, text_style).with_justify(JustifyText::Center), - transform: Transform::from_xyz(0., -0.5 * size.y - 10., 0.0), - text_anchor: bevy::sprite::Anchor::TopCenter, - ..default() - }); }); + }); position.x += 0.5 * size.x + gap; } } diff --git a/examples/2d/sprite_tile.rs b/examples/2d/sprite_tile.rs index 016517505e955..c9b0fe5ecb811 100644 --- a/examples/2d/sprite_tile.rs +++ b/examples/2d/sprite_tile.rs @@ -26,15 +26,17 @@ fn setup(mut commands: Commands, asset_server: Res) { current: 128.0, speed: 50.0, }); - commands.spawn(SpriteBundle { - texture: asset_server.load("branding/icon.png"), - scale_mode: ImageScaleMode::Tiled { + commands.spawn(( + SpriteBundle { + texture: asset_server.load("branding/icon.png"), + ..default() + }, + ImageScaleMode::Tiled { tile_x: true, tile_y: true, stretch_value: 0.5, // The image will tile every 128px }, - ..default() - }); + )); } fn animate(mut sprites: Query<&mut Sprite>, mut state: ResMut, time: Res