diff --git a/crates/gpui_platform/Cargo.toml b/crates/gpui_platform/Cargo.toml index a41cb17672..a502ff138d 100644 --- a/crates/gpui_platform/Cargo.toml +++ b/crates/gpui_platform/Cargo.toml @@ -14,7 +14,7 @@ path = "src/gpui_platform.rs" [features] default = [] font-kit = ["gpui_macos/font-kit"] -test-support = ["gpui/test-support", "gpui_macos/test-support"] +test-support = ["gpui/test-support", "gpui_macos/test-support", "gpui_windows/test-support"] screen-capture = ["gpui/screen-capture", "gpui_macos/screen-capture", "gpui_windows/screen-capture", "gpui_linux/screen-capture"] runtime_shaders = ["gpui_macos/runtime_shaders"] wayland = ["gpui_linux/wayland"] diff --git a/crates/gpui_windows/src/directx_renderer.rs b/crates/gpui_windows/src/directx_renderer.rs index 5719c12f2d..8590407e61 100644 --- a/crates/gpui_windows/src/directx_renderer.rs +++ b/crates/gpui_windows/src/directx_renderer.rs @@ -337,11 +337,24 @@ impl DirectXRenderer { // and so likely do not have the textures anymore that are required for drawing return Ok(()); } + self.render(scene, background_appearance)?; + self.present() + } + + /// Clear the render target for `background_appearance` and encode every + /// primitive batch of `scene` into it, without presenting. Shared by + /// [`draw`](Self::draw) (which then presents) and + /// [`render_to_image`](Self::render_to_image) (which reads the target back + /// instead), so the two cannot drift. + fn render( + &mut self, + scene: &Scene, + background_appearance: WindowBackgroundAppearance, + ) -> Result<()> { self.pre_draw(&match background_appearance { WindowBackgroundAppearance::Opaque => [1.0f32; 4], _ => [0.0f32; 4], })?; - self.upload_scene_buffers(scene)?; let annotation = self @@ -388,7 +401,85 @@ impl DirectXRenderer { ) })?; } - self.present() + Ok(()) + } + + /// Render `scene` to an offscreen CPU image **without presenting** so + /// the window need never be shown or visible (the macOS headless path + /// goes through MetalRenderer; this is the Windows analogue). Draws into + /// the existing render target, copies it into a `D3D11_USAGE_STAGING` + /// texture, maps it, and converts BGRA to RGBA. + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn render_to_image( + &mut self, + scene: &Scene, + background_appearance: WindowBackgroundAppearance, + ) -> Result { + // A pending device-lost recovery (`skip_draws`) leaves the atlas holding + // tile references from the previous device; drawing before the forced + // re-render rebuilds them panics in `DirectXAtlasState::texture`. + anyhow::ensure!( + !self.skip_draws, + "render_to_image unavailable while recovering from a lost device" + ); + self.render(scene, background_appearance)?; + + let devices = self.devices.as_ref().context("devices missing")?; + let device = &devices.device; + let context = &devices.device_context; + let resources = self.resources.as_ref().context("resources missing")?; + let render_target = resources + .render_target + .as_ref() + .context("render target missing")?; + + // A CPU-readable copy of the render target. + let mut desc = D3D11_TEXTURE2D_DESC::default(); + unsafe { render_target.GetDesc(&mut desc) }; + let width = desc.Width; + let height = desc.Height; + let staging_desc = D3D11_TEXTURE2D_DESC { + Usage: D3D11_USAGE_STAGING, + BindFlags: 0, + CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32, + MiscFlags: 0, + MipLevels: 1, + ArraySize: 1, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + ..desc + }; + let mut staging: Option = None; + unsafe { device.CreateTexture2D(&staging_desc, None, Some(&mut staging))? }; + let staging = staging.context("creating staging texture")?; + unsafe { context.CopyResource(&staging, render_target) }; + + let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); + unsafe { context.Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))? }; + let row_bytes = (width as usize) * 4; + let mut pixels = vec![0u8; row_bytes * height as usize]; + // SAFETY: `Map` succeeded, so `pData` points at `RowPitch * height` + // readable bytes for as long as the mapping is held, and `RowPitch >= + // row_bytes` (it only ever adds trailing padding). `pixels` is sized + // `row_bytes * height`, so every copy stays in bounds on both sides, + // and the regions cannot overlap (`pixels` is a fresh allocation). + unsafe { + let src = mapped.pData as *const u8; + for row in 0..height as usize { + let s = src.add(row * mapped.RowPitch as usize); + let d = pixels.as_mut_ptr().add(row * row_bytes); + std::ptr::copy_nonoverlapping(s, d, row_bytes); + } + context.Unmap(&staging, 0); + } + // The render target is BGRA; image::RgbaImage expects RGBA. + for px in pixels.chunks_exact_mut(4) { + px.swap(0, 2); + } + image::RgbaImage::from_raw(width, height, pixels) + .context("Failed to build RgbaImage from staging readback") } pub(crate) fn resize(&mut self, new_size: Size) -> Result<()> { diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index af240716c9..d81efacc8c 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -996,6 +996,14 @@ impl PlatformWindow for WindowsWindow { .log_err(); } + #[cfg(any(test, feature = "test-support"))] + fn render_to_image(&self, scene: &Scene) -> anyhow::Result { + self.state + .renderer + .borrow_mut() + .render_to_image(scene, self.state.background_appearance.get()) + } + fn sprite_atlas(&self) -> Arc { self.state.renderer.borrow().sprite_atlas() }