use windows::Win32::Graphics::Direct3D12::*;
use windows::Win32::Graphics::Dxgi::Common::*;
use crate::gfx::hdr_output::HdrEncoding;
use crate::gfx::image_decode::{self, PixelLayout};
use super::com;
use super::context::DxContext;
use super::texture::{create_buffer, one_shot_submit, transition_barrier};
impl DxContext {
pub(crate) fn capture_screenshot(&mut self, path: &str) -> Result<String, String> {
let Some(back_idx) = self.swapchain.last_present_index else {
return Err("screenshot: no frame has been presented yet".into());
};
let back_buffer = self
.swapchain
.back_buffers
.get(back_idx)
.ok_or("screenshot: stale back-buffer index")?
.clone();
let width = self.extent.output_width;
let height = self.extent.output_height;
if width == 0 || height == 0 {
return Err("screenshot: zero-sized swapchain".into());
}
self.wait_idle();
let tex_desc = D3D12_RESOURCE_DESC {
Dimension: D3D12_RESOURCE_DIMENSION_TEXTURE2D,
Alignment: 0,
Width: width as u64,
Height: height,
DepthOrArraySize: 1,
MipLevels: 1,
Format: self.swapchain.format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Layout: D3D12_TEXTURE_LAYOUT_UNKNOWN,
Flags: D3D12_RESOURCE_FLAG_NONE,
};
let mut layout = D3D12_PLACED_SUBRESOURCE_FOOTPRINT::default();
let mut row_count: u32 = 0;
let mut row_size: u64 = 0;
let mut total_size: u64 = 0;
unsafe {
self.device.GetCopyableFootprints(
&tex_desc,
0,
1,
0,
Some(&mut layout),
Some(&mut row_count),
Some(&mut row_size),
Some(&mut total_size),
);
}
let readback = create_buffer(
&self.alloc,
total_size,
D3D12_HEAP_TYPE_READBACK,
D3D12_RESOURCE_STATE_COPY_DEST,
)?;
let dst_loc = D3D12_TEXTURE_COPY_LOCATION {
pResource: com::borrowed(&readback),
Type: D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT,
Anonymous: D3D12_TEXTURE_COPY_LOCATION_0 {
PlacedFootprint: layout,
},
};
let src_loc = D3D12_TEXTURE_COPY_LOCATION {
pResource: com::borrowed(&back_buffer),
Type: D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX,
Anonymous: D3D12_TEXTURE_COPY_LOCATION_0 {
SubresourceIndex: 0,
},
};
one_shot_submit(&self.device, &self.command_queue, |cmd| unsafe {
let to_src = transition_barrier(
&back_buffer,
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_COPY_SOURCE,
);
cmd.ResourceBarrier(&[to_src]);
cmd.CopyTextureRegion(&dst_loc, 0, 0, 0, &src_loc, None);
let to_present = transition_barrier(
&back_buffer,
D3D12_RESOURCE_STATE_COPY_SOURCE,
D3D12_RESOURCE_STATE_PRESENT,
);
cmd.ResourceBarrier(&[to_present]);
})?;
let row_pitch = layout.Footprint.RowPitch as usize;
let tight_row = row_size as usize;
let mut map_ptr = std::ptr::null_mut::<std::ffi::c_void>();
unsafe { readback.Map(0, None, Some(&mut map_ptr)) }
.map_err(|e| format!("screenshot: map readback: {e}"))?;
let mut packed = vec![0u8; tight_row * height as usize];
for row in 0..height as usize {
let src = unsafe { (map_ptr as *const u8).add(row * row_pitch) };
let src_slice = unsafe { std::slice::from_raw_parts(src, tight_row) };
packed[row * tight_row..(row + 1) * tight_row].copy_from_slice(src_slice);
}
unsafe { readback.Unmap(0, None) };
let rgba = image_decode::decode_to_rgba8(
&packed,
classify(self.swapchain.format, self.hdr_encoding),
);
encode_png(path, width, height, &rgba)?;
Ok(path.to_string())
}
}
#[cfg(test)]
fn swapchain_bytes_per_pixel(format: DXGI_FORMAT) -> u32 {
match format {
DXGI_FORMAT_R16G16B16A16_FLOAT => 8,
_ => 4,
}
}
fn classify(format: DXGI_FORMAT, encoding: Option<HdrEncoding>) -> PixelLayout {
match format {
DXGI_FORMAT_R16G16B16A16_FLOAT => PixelLayout::Rgba16F {
scrgb: !matches!(encoding, Some(HdrEncoding::Pq)),
},
DXGI_FORMAT_B8G8R8A8_UNORM | DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => PixelLayout::Bgra8,
_ => PixelLayout::Rgba8,
}
}
fn encode_png(path: &str, width: u32, height: u32, rgba: &[u8]) -> Result<(), String> {
let file =
std::fs::File::create(path).map_err(|e| format!("screenshot: create {path}: {e}"))?;
let mut encoder = png::Encoder::new(std::io::BufWriter::new(file), width, height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder
.write_header()
.map_err(|e| format!("screenshot: png header: {e}"))?;
writer
.write_image_data(rgba)
.map_err(|e| format!("screenshot: png data: {e}"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bytes_per_pixel_matches_swapchain_formats() {
assert_eq!(swapchain_bytes_per_pixel(DXGI_FORMAT_B8G8R8A8_UNORM), 4);
assert_eq!(swapchain_bytes_per_pixel(DXGI_FORMAT_R8G8B8A8_UNORM), 4);
assert_eq!(swapchain_bytes_per_pixel(DXGI_FORMAT_R16G16B16A16_FLOAT), 8);
}
#[test]
fn classify_maps_swapchain_formats_to_pixel_layouts() {
assert_eq!(
classify(DXGI_FORMAT_B8G8R8A8_UNORM, None),
PixelLayout::Bgra8
);
assert_eq!(
classify(DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, None),
PixelLayout::Bgra8
);
assert_eq!(
classify(DXGI_FORMAT_R8G8B8A8_UNORM, None),
PixelLayout::Rgba8
);
assert_eq!(
classify(
DXGI_FORMAT_R16G16B16A16_FLOAT,
Some(HdrEncoding::ExtendedLinear)
),
PixelLayout::Rgba16F { scrgb: true }
);
assert_eq!(
classify(DXGI_FORMAT_R16G16B16A16_FLOAT, Some(HdrEncoding::Pq)),
PixelLayout::Rgba16F { scrgb: false }
);
assert_eq!(
classify(DXGI_FORMAT_R16G16B16A16_FLOAT, None),
PixelLayout::Rgba16F { scrgb: true }
);
}
}