#![deny(unsafe_op_in_unsafe_fn)]
use objc2_metal::{
MTLBlitCommandEncoder as _, MTLCommandBuffer as _, MTLCommandEncoder as _,
MTLCommandQueue as _, MTLDevice as _, MTLOrigin, MTLPixelFormat, MTLRegion, MTLSize,
MTLStorageMode, MTLTexture as _,
};
use crate::gfx::hdr_output::HdrEncoding;
use crate::gfx::image_decode::{self, PixelLayout};
use super::context::MtlContext;
use super::descriptors::TextureDesc;
impl MtlContext {
pub(in crate::metal) fn capture_screenshot(&mut self, path: &str) -> Result<String, String> {
let src = self
.last_present_texture
.clone()
.ok_or("screenshot: no frame has been presented yet (capture is cn debug only)")?;
let width = src.width();
let height = src.height();
if width == 0 || height == 0 {
return Err("screenshot: zero-sized drawable".into());
}
let desc = TextureDesc {
format: self.swap_pixel_format,
width,
height,
storage: MTLStorageMode::Shared,
..Default::default()
}
.build();
let staging = self
.device
.newTextureWithDescriptor(&desc)
.ok_or("screenshot: failed to create staging texture")?;
let cmd_buf = self
.command_queue
.commandBuffer()
.ok_or("screenshot: failed to get command buffer")?;
let blit = cmd_buf
.blitCommandEncoder()
.ok_or("screenshot: failed to get blit encoder")?;
unsafe {
blit.copyFromTexture_sourceSlice_sourceLevel_sourceOrigin_sourceSize_toTexture_destinationSlice_destinationLevel_destinationOrigin(
&src,
0,
0,
MTLOrigin { x: 0, y: 0, z: 0 },
MTLSize { width, height, depth: 1 },
&staging,
0,
0,
MTLOrigin { x: 0, y: 0, z: 0 },
);
}
blit.endEncoding();
cmd_buf.commit();
cmd_buf.waitUntilCompleted();
let bytes_per_pixel = swapchain_bytes_per_pixel(self.swap_pixel_format) as usize;
let bytes_per_row = width * bytes_per_pixel;
let mut raw = vec![0u8; bytes_per_row * height];
let region = MTLRegion {
origin: MTLOrigin { x: 0, y: 0, z: 0 },
size: MTLSize {
width,
height,
depth: 1,
},
};
unsafe {
staging.getBytes_bytesPerRow_fromRegion_mipmapLevel(
std::ptr::NonNull::new(raw.as_mut_ptr() as *mut std::ffi::c_void)
.ok_or("screenshot: null readback pointer")?,
bytes_per_row,
region,
0,
);
}
let rgba = image_decode::decode_to_rgba8(
&raw,
classify(self.swap_pixel_format, self.hdr.encoding),
);
encode_png(path, width as u32, height as u32, &rgba)?;
Ok(path.to_string())
}
}
fn swapchain_bytes_per_pixel(format: MTLPixelFormat) -> u32 {
match format {
MTLPixelFormat::RGBA16Float => 8,
_ => 4,
}
}
fn classify(format: MTLPixelFormat, encoding: Option<HdrEncoding>) -> PixelLayout {
match format {
MTLPixelFormat::RGBA16Float => PixelLayout::Rgba16F {
scrgb: !matches!(encoding, Some(HdrEncoding::Pq)),
},
MTLPixelFormat::BGRA8Unorm | MTLPixelFormat::BGRA8Unorm_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(MTLPixelFormat::BGRA8Unorm), 4);
assert_eq!(swapchain_bytes_per_pixel(MTLPixelFormat::RGBA8Unorm), 4);
assert_eq!(swapchain_bytes_per_pixel(MTLPixelFormat::RGBA16Float), 8);
}
#[test]
fn classify_maps_swapchain_formats_to_pixel_layouts() {
assert_eq!(
classify(MTLPixelFormat::BGRA8Unorm, None),
PixelLayout::Bgra8
);
assert_eq!(
classify(MTLPixelFormat::BGRA8Unorm_sRGB, None),
PixelLayout::Bgra8
);
assert_eq!(
classify(MTLPixelFormat::RGBA8Unorm, None),
PixelLayout::Rgba8
);
assert_eq!(
classify(
MTLPixelFormat::RGBA16Float,
Some(HdrEncoding::ExtendedLinear)
),
PixelLayout::Rgba16F { scrgb: true }
);
assert_eq!(
classify(MTLPixelFormat::RGBA16Float, Some(HdrEncoding::Pq)),
PixelLayout::Rgba16F { scrgb: false }
);
assert_eq!(
classify(MTLPixelFormat::RGBA16Float, None),
PixelLayout::Rgba16F { scrgb: true }
);
}
}