From 0e2c981b8db60a72d0bec53fac33b4e6b1b0b6c6 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 17 Jul 2026 14:20:43 +0200 Subject: [PATCH] =?UTF-8?q?fix(renderer):=20the=20deferred=20path=20honors?= =?UTF-8?q?=20the=20render-target=20override=20=E2=80=94=20render-to-textu?= =?UTF-8?q?re=20works=20for=203D=20apps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit begin_texture_mode's override was only consulted by the simple 2D end_frame; end_frame_with_scene — the path every 3D app actually runs — acquired the surface unconditionally and presented, so render-to-texture silently no-oped for 3D content since Q1: the frame went to the screen and the RT kept its initial contents. (Found by the editor's thumbnail work: even a bare magenta clear never reached the texture.) Now: when the override is armed, the deferred frame's final passes (post/compose chain + 2D overlay — all depth-less, so no attachment-size hazards) write the RT view, nothing is acquired or presented, and the screenshot branch defers to the next real frame. All intermediate passes are unchanged. Proven end-to-end: the editor renders model thumbnails into 128px targets via dedicated frames and draws them through drawTexturePro — the first successful RT sampling in the engine. Follow-up: a golden test for the RT round-trip in the native suite. Claude-Session: https://claude.ai/code/session_01PmL9WgNMkAgvpYSHEZga8K --- native/shared/src/renderer/mod.rs | 48 ++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/native/shared/src/renderer/mod.rs b/native/shared/src/renderer/mod.rs index 13f935d..115d913 100644 --- a/native/shared/src/renderer/mod.rs +++ b/native/shared/src/renderer/mod.rs @@ -10498,16 +10498,41 @@ impl Renderer { self.flush_model_uniforms(); profiler.end("joint_flush"); + // Q1-RT: the deferred path honors the render-target override too. + // When begin_texture_mode armed a target, this frame's FINAL passes + // (post/compose chain + 2D overlay) write into it instead of the + // swapchain, and nothing acquires or presents. The simple 2D + // end_frame always honored the override; this path ignored it, so + // render-to-texture silently no-oped for every 3D app — the frame + // rendered to the surface and the RT stayed at its initial contents + // (found via the editor's thumbnail work, 2026-07-17). + let rt_color = self.rt_color_view.take(); + let rt_depth = self.rt_depth_view.take(); + let using_rt = rt_color.is_some(); + profiler.begin("surface_acquire"); - let output = match self.acquire_frame() { - Some(t) => t, - None => { - profiler.end("surface_acquire"); - return; + let output = if using_rt { + None + } else { + match self.acquire_frame() { + Some(t) => Some(t), + None => { + profiler.end("surface_acquire"); + self.rt_color_view = rt_color; + self.rt_depth_view = rt_depth; + return; + } } }; profiler.end("surface_acquire"); - let view = self.frame_texture(&output).create_view(&wgpu::TextureViewDescriptor::default()); + let view = if let Some(ref rt_view) = rt_color { + rt_view.clone() + } else { + self.frame_texture(output.as_ref().unwrap()).create_view(&wgpu::TextureViewDescriptor::default()) + }; + // Restore so the views persist until end_texture_mode clears them. + self.rt_color_view = rt_color; + self.rt_depth_view = rt_depth; let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("bloom_encoder"), @@ -11078,11 +11103,12 @@ impl Renderer { // If screenshot requested, copy rendered texture to staging buffer before submitting. // Synchronous GPU readback is not available on WASM (device.poll(Wait) blocks). + // RT frames have no surface — the request stays pending for the next real frame. #[cfg(not(target_arch = "wasm32"))] - if self.screenshot_requested { + if self.screenshot_requested && !using_rt { eprintln!("bloom: screenshot readback branch running"); // Use actual texture dimensions (accounts for Retina/DPI scaling) - let tex_size = self.frame_texture(&output).size(); + let tex_size = self.frame_texture(output.as_ref().unwrap()).size(); let width = tex_size.width; let height = tex_size.height; let bytes_per_pixel = 4u32; @@ -11099,7 +11125,7 @@ impl Renderer { encoder.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { - texture: self.frame_texture(&output), + texture: self.frame_texture(output.as_ref().unwrap()), mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, @@ -11192,7 +11218,9 @@ impl Renderer { } profiler.begin("swap_present"); - self.present_frame(output); + if let Some(out) = output { + self.present_frame(out); + } profiler.end("swap_present"); // After present: swap TAA ping-pong + advance the jitter