From 258019b24e438e958c6c97f08cf94fb4eba9645f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sun, 16 Nov 2025 14:51:20 -0800 Subject: [PATCH 1/6] Only present when RenderTarget has been written to. --- .../bevy_core_pipeline/src/upscaling/node.rs | 30 +++++++++++++--- crates/bevy_render/src/renderer/mod.rs | 35 +++++++++--------- .../src/texture/texture_attachment.rs | 7 ++++ crates/bevy_render/src/view/mod.rs | 5 +++ crates/bevy_render/src/view/window/mod.rs | 36 ++++++++++++++++--- 5 files changed, 87 insertions(+), 26 deletions(-) diff --git a/crates/bevy_core_pipeline/src/upscaling/node.rs b/crates/bevy_core_pipeline/src/upscaling/node.rs index 3d7b2c9905068..1750e78d2d1b1 100644 --- a/crates/bevy_core_pipeline/src/upscaling/node.rs +++ b/crates/bevy_core_pipeline/src/upscaling/node.rs @@ -1,6 +1,7 @@ use crate::{blit::BlitPipeline, upscaling::ViewUpscalingPipeline}; -use bevy_camera::{CameraOutputMode, ClearColor, ClearColorConfig}; +use bevy_camera::{CameraOutputMode, ClearColor, ClearColorConfig, NormalizedRenderTarget}; use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_render::view::{ExtractedWindow, ExtractedWindows}; use bevy_render::{ camera::ExtractedCamera, diagnostic::RecordDiagnostics, @@ -36,10 +37,31 @@ impl ViewNode for UpscalingNode { let diagnostics = render_context.diagnostic_recorder(); + // check if the window size or present mode has changed for this camera + // if so, we need to write to the upscaling target to make sure it's updated + // otherwise it'll be stretched from the previous size (ugly) + let mut needs_present = false; + if let Some(camera) = camera { + if let Some(NormalizedRenderTarget::Window(window)) = camera.target { + let extracted_windows = world.resource::(); + let Some(extracted_window) = extracted_windows.get(&window.entity()) else { + return Ok(()); + }; + if extracted_window.present_mode_changed || extracted_window.size_changed { + needs_present = true + } + } + } + let clear_color = if let Some(camera) = camera { - match camera.output_mode { - CameraOutputMode::Write { clear_color, .. } => clear_color, - CameraOutputMode::Skip => return Ok(()), + match (camera.output_mode, needs_present) { + (CameraOutputMode::Write { clear_color, .. }, _) => clear_color, + // this may not be totally correct as the user could have configured a non-default + // color the last time they wrote, but we need to clear with something, especially + // if we are changing sizes. if you care about this case, enable write when resizing + // with your custom clear color + (CameraOutputMode::Skip, true) => ClearColorConfig::Default, + (CameraOutputMode::Skip, false) => return Ok(()), } } else { ClearColorConfig::Default diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index cbc93d3b2a5c3..45a0420368580 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -17,9 +17,11 @@ use crate::{ view::{ExtractedWindows, ViewTarget}, }; use alloc::sync::Arc; +use bevy_camera::NormalizedRenderTarget; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemState}; use bevy_platform::time::Instant; +use bevy_render::camera::ExtractedCamera; use bevy_time::TimeSender; use bevy_window::RawHandleWrapperHolder; use tracing::{debug, error, info, info_span, warn}; @@ -29,7 +31,10 @@ 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, state: &mut SystemState>>) { +pub fn render_system( + world: &mut World, + state: &mut SystemState>, +) { world.resource_scope(|world, mut graph: Mut| { graph.update(world); }); @@ -77,23 +82,17 @@ pub fn render_system(world: &mut World, state: &mut SystemState>(); - for view_entity in view_entities { - world.entity_mut(view_entity).remove::(); - } - - let mut windows = world.resource_mut::(); - for window in windows.values_mut() { - if let Some(surface_texture) = window.swap_chain_texture.take() { - // TODO(clean): winit docs recommends calling pre_present_notify before this. - // though `present()` doesn't present the frame, it schedules it to be presented - // by wgpu. - // https://docs.rs/winit/0.29.9/wasm32-unknown-unknown/winit/window/struct.Window.html#method.pre_present_notify - surface_texture.present(); + world.resource_scope(|world, mut windows: Mut| { + let views = state.get(world); + for (view_target, camera) in views.iter() { + if let Some(NormalizedRenderTarget::Window(window)) = camera.target { + if view_target.needs_present() { + let window = windows.get_mut(&window.entity()).unwrap(); + window.present(); + } + } } - } + }); #[cfg(feature = "tracing-tracy")] tracing::event!( @@ -110,7 +109,7 @@ pub fn render_system(world: &mut World, state: &mut SystemState { - panic!("The TimeSender channel should always be empty during render. You might need to add the bevy::core::time_system to your app.",); + panic!("The TimeSender channel should always be empty during render. You might need to add the bevy::core::time_system to your app.", ); } bevy_time::TrySendError::Disconnected(_) => { // ignore disconnected errors, the main world probably just got dropped during shutdown diff --git a/crates/bevy_render/src/texture/texture_attachment.rs b/crates/bevy_render/src/texture/texture_attachment.rs index cf0e057db0f21..269c2f1422820 100644 --- a/crates/bevy_render/src/texture/texture_attachment.rs +++ b/crates/bevy_render/src/texture/texture_attachment.rs @@ -159,4 +159,11 @@ impl OutputColorAttachment { }, } } + + /// Returns `true` if this attachment has been written to by a render pass. + // we re-use is_first_call atomic to track usage, which assumes that calls to get_attachment + // are always consumed by a render pass that writes to the attachment + pub fn needs_present(&self) -> bool { + !self.is_first_call.load(Ordering::SeqCst) + } } diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index ea95b7fad725b..9a3d610087c63 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -830,6 +830,11 @@ impl ViewTarget { self.out_texture.get_attachment(clear_color) } + /// Whether the final texture this view will render to needs to be presented. + pub fn needs_present(&self) -> bool { + self.out_texture.needs_present() + } + /// The format of the final texture this view will render to #[inline] pub fn out_texture_format(&self) -> TextureFormat { diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 0b6cacea90e62..ae5389e906820 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -76,6 +76,20 @@ impl ExtractedWindow { )); self.swap_chain_texture = Some(SurfaceTexture::from(frame)); } + + fn has_swapchain_texture(&self) -> bool { + self.swap_chain_texture_view.is_some() && self.swap_chain_texture.is_some() + } + + pub fn present(&mut self) { + if let Some(surface_texture) = self.swap_chain_texture.take() { + // TODO(clean): winit docs recommends calling pre_present_notify before this. + // though `present()` doesn't present the frame, it schedules it to be presented + // by wgpu. + // https://docs.rs/winit/0.29.9/wasm32-unknown-unknown/winit/window/struct.Window.html#method.pre_present_notify + surface_texture.present(); + } + } } #[derive(Default, Resource)] @@ -130,8 +144,13 @@ fn extract_windows( alpha_mode: window.composite_alpha_mode, }); - // NOTE: Drop the swap chain frame here - extracted_window.swap_chain_texture_view = None; + if extracted_window.swap_chain_texture.is_none() { + // If we called present on the previous swap-chain texture last update, + // then drop the swap chain frame here, otherwise we can keep it for the + // next update as an optimization. `prepare_windows` will only acquire a new + // swap chain texture if needed. + extracted_window.swap_chain_texture_view = None; + } extracted_window.size_changed = new_width != extracted_window.physical_width || new_height != extracted_window.physical_height; extracted_window.present_mode_changed = @@ -221,6 +240,11 @@ pub fn prepare_windows( continue; }; + // We didn't present the previous frame, so we can keep using our existing swapchain texture. + if window.has_swapchain_texture() && !window.size_changed && !window.present_mode_changed { + continue; + } + // A recurring issue is hitting `wgpu::SurfaceError::Timeout` on certain Linux // mesa driver implementations. This seems to be a quirk of some drivers. // We'd rather keep panicking when not on Linux mesa, because in those case, @@ -300,13 +324,13 @@ pub fn create_surfaces( // By accessing a NonSend resource, we tell the scheduler to put this system on the main thread, // which is necessary for some OS's #[cfg(any(target_os = "macos", target_os = "ios"))] _marker: bevy_ecs::system::NonSendMarker, - windows: Res, + mut windows: ResMut, mut window_surfaces: ResMut, render_instance: Res, render_adapter: Res, render_device: Res, ) { - for window in windows.windows.values() { + for window in windows.windows.values_mut() { let data = window_surfaces .surfaces .entry(window.entity) @@ -383,6 +407,10 @@ pub fn create_surfaces( }); if window.size_changed || window.present_mode_changed { + // normally this is dropped on present but we double check here to be safe as failure to + // drop it will cause validation errors in wgpu + drop(window.swap_chain_texture.take()); + data.configuration.width = window.physical_width; data.configuration.height = window.physical_height; data.configuration.present_mode = match window.present_mode { From f757c3554d0604ffb9a2e7811a0225cd0ff84f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sun, 16 Nov 2025 23:51:14 -0800 Subject: [PATCH 2/6] Don't unwrap. --- crates/bevy_render/src/renderer/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 45a0420368580..27ba7b4da77db 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -87,7 +87,9 @@ pub fn render_system( for (view_target, camera) in views.iter() { if let Some(NormalizedRenderTarget::Window(window)) = camera.target { if view_target.needs_present() { - let window = windows.get_mut(&window.entity()).unwrap(); + let Some(window) = windows.get_mut(&window.entity()) else { + continue; + }; window.present(); } } From 74fdaa55a0e9d01babb972410e8f034391392c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sun, 16 Nov 2025 23:52:01 -0800 Subject: [PATCH 3/6] Ci. --- crates/bevy_core_pipeline/src/upscaling/node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_core_pipeline/src/upscaling/node.rs b/crates/bevy_core_pipeline/src/upscaling/node.rs index 1750e78d2d1b1..25d472a7a0dac 100644 --- a/crates/bevy_core_pipeline/src/upscaling/node.rs +++ b/crates/bevy_core_pipeline/src/upscaling/node.rs @@ -1,7 +1,7 @@ use crate::{blit::BlitPipeline, upscaling::ViewUpscalingPipeline}; use bevy_camera::{CameraOutputMode, ClearColor, ClearColorConfig, NormalizedRenderTarget}; use bevy_ecs::{prelude::*, query::QueryItem}; -use bevy_render::view::{ExtractedWindow, ExtractedWindows}; +use bevy_render::view::ExtractedWindows; use bevy_render::{ camera::ExtractedCamera, diagnostic::RecordDiagnostics, From 20515a9072c48520be3970bf532231e3f3451985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 17 Nov 2025 10:21:29 -0800 Subject: [PATCH 4/6] Actually, on second thought, the user should be responsible for turning things on during resize. --- .../bevy_core_pipeline/src/upscaling/node.rs | 30 +++---------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/crates/bevy_core_pipeline/src/upscaling/node.rs b/crates/bevy_core_pipeline/src/upscaling/node.rs index 25d472a7a0dac..3d7b2c9905068 100644 --- a/crates/bevy_core_pipeline/src/upscaling/node.rs +++ b/crates/bevy_core_pipeline/src/upscaling/node.rs @@ -1,7 +1,6 @@ use crate::{blit::BlitPipeline, upscaling::ViewUpscalingPipeline}; -use bevy_camera::{CameraOutputMode, ClearColor, ClearColorConfig, NormalizedRenderTarget}; +use bevy_camera::{CameraOutputMode, ClearColor, ClearColorConfig}; use bevy_ecs::{prelude::*, query::QueryItem}; -use bevy_render::view::ExtractedWindows; use bevy_render::{ camera::ExtractedCamera, diagnostic::RecordDiagnostics, @@ -37,31 +36,10 @@ impl ViewNode for UpscalingNode { let diagnostics = render_context.diagnostic_recorder(); - // check if the window size or present mode has changed for this camera - // if so, we need to write to the upscaling target to make sure it's updated - // otherwise it'll be stretched from the previous size (ugly) - let mut needs_present = false; - if let Some(camera) = camera { - if let Some(NormalizedRenderTarget::Window(window)) = camera.target { - let extracted_windows = world.resource::(); - let Some(extracted_window) = extracted_windows.get(&window.entity()) else { - return Ok(()); - }; - if extracted_window.present_mode_changed || extracted_window.size_changed { - needs_present = true - } - } - } - let clear_color = if let Some(camera) = camera { - match (camera.output_mode, needs_present) { - (CameraOutputMode::Write { clear_color, .. }, _) => clear_color, - // this may not be totally correct as the user could have configured a non-default - // color the last time they wrote, but we need to clear with something, especially - // if we are changing sizes. if you care about this case, enable write when resizing - // with your custom clear color - (CameraOutputMode::Skip, true) => ClearColorConfig::Default, - (CameraOutputMode::Skip, false) => return Ok(()), + match camera.output_mode { + CameraOutputMode::Write { clear_color, .. } => clear_color, + CameraOutputMode::Skip => return Ok(()), } } else { ClearColorConfig::Default From d79a1724ddafe0115410f22a3fbc8611d42cbb09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 17 Nov 2025 10:21:57 -0800 Subject: [PATCH 5/6] Clippy. --- crates/bevy_render/src/renderer/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 27ba7b4da77db..d0e988ae170f2 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -85,13 +85,13 @@ pub fn render_system( world.resource_scope(|world, mut windows: Mut| { let views = state.get(world); for (view_target, camera) in views.iter() { - if let Some(NormalizedRenderTarget::Window(window)) = camera.target { - if view_target.needs_present() { - let Some(window) = windows.get_mut(&window.entity()) else { - continue; - }; - window.present(); - } + if let Some(NormalizedRenderTarget::Window(window)) = camera.target + && view_target.needs_present() + { + let Some(window) = windows.get_mut(&window.entity()) else { + continue; + }; + window.present(); } } }); From fe8ab7bb582e41ec76c55b92051b2f7de86bc64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 17 Nov 2025 14:53:28 -0800 Subject: [PATCH 6/6] Update crates/bevy_render/src/renderer/mod.rs Co-authored-by: atlv --- crates/bevy_render/src/renderer/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index d0e988ae170f2..af716707fc44e 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -111,7 +111,7 @@ pub fn render_system( if let Err(error) = time_sender.0.try_send(Instant::now()) { match error { bevy_time::TrySendError::Full(_) => { - panic!("The TimeSender channel should always be empty during render. You might need to add the bevy::core::time_system to your app.", ); + panic!("The TimeSender channel should always be empty during render. You might need to add the bevy::core::time_system to your app."); } bevy_time::TrySendError::Disconnected(_) => { // ignore disconnected errors, the main world probably just got dropped during shutdown