use anyhow::Result;
use foreign_types::ForeignTypeRef;
use gpui_engine::{ImportedTextureHandle, MetalTexture};
use metal::MTLPixelFormat;
pub trait MetalTextureExt {
fn to_imported_handle(&self) -> Result<ImportedTextureHandle>;
}
impl MetalTextureExt for metal::TextureRef {
fn to_imported_handle(&self) -> Result<ImportedTextureHandle> {
let pixel_format = self.pixel_format();
anyhow::ensure!(
matches!(
pixel_format,
MTLPixelFormat::BGRA8Unorm_sRGB | MTLPixelFormat::RGBA8Unorm_sRGB
),
"an imported texture must be declared sRGB, the colour space the fragment decodes and \
re-encodes, but this one is {pixel_format:?}"
);
anyhow::ensure!(
self.usage().contains(metal::MTLTextureUsage::ShaderRead),
"an imported texture must be created with MTLTextureUsage::ShaderRead"
);
Ok(ImportedTextureHandle::new(MetalTexture(
self.as_ptr() as *mut std::ffi::c_void
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metal_renderer::{InstanceBufferPool, MetalRenderer};
use gpui_engine::{CustomRenderPrimitive, GpuRenderer, Scene, SceneRenderer};
use gpui_platform::{Bounds, ContentMask, Corners, DevicePixels, Point, Size};
use metal::{MTLOrigin, MTLRegion, MTLSize, MTLStorageMode, MTLTextureUsage};
use parking_lot::Mutex;
use std::sync::Arc;
fn test_renderer() -> Option<MetalRenderer> {
metal::Device::system_default()?;
Some(MetalRenderer::new_headless(Arc::new(Mutex::new(
InstanceBufferPool::default(),
))))
}
fn renderer_device(renderer: &MetalRenderer) -> metal::Device {
GpuRenderer::device(renderer).expect("the Metal renderer lends its device through the seam")
}
fn imported_texture(
device: &metal::Device,
pixel_format: MTLPixelFormat,
bgra: [u8; 4],
) -> metal::Texture {
let descriptor = metal::TextureDescriptor::new();
descriptor.set_width(1);
descriptor.set_height(1);
descriptor.set_pixel_format(pixel_format);
descriptor.set_usage(MTLTextureUsage::ShaderRead);
descriptor.set_storage_mode(MTLStorageMode::Shared);
let texture = device.new_texture(&descriptor);
texture.replace_region(
MTLRegion {
origin: MTLOrigin { x: 0, y: 0, z: 0 },
size: MTLSize {
width: 1,
height: 1,
depth: 1,
},
},
0,
bgra.as_ptr() as *const std::ffi::c_void,
4,
);
texture
}
fn imported_texture_scene(
handle: ImportedTextureHandle,
viewport: Size<DevicePixels>,
) -> Scene {
let bounds = Bounds {
origin: Point {
x: 0.0.into(),
y: 0.0.into(),
},
size: Size {
width: (viewport.width.0 as f32).into(),
height: (viewport.height.0 as f32).into(),
},
};
let mut scene = Scene::default();
scene.custom.push(CustomRenderPrimitive::Texture {
order: 0,
handle,
bounds,
content_mask: ContentMask { bounds },
radii: Corners::default(),
opacity: 1.0,
flip_v: false,
});
scene
}
#[test]
fn an_imported_texture_round_trips_its_srgb_bytes() -> anyhow::Result<()> {
let viewport = Size {
width: DevicePixels(8),
height: DevicePixels(8),
};
let Some(mut renderer) = test_renderer() else {
log::warn!("no Metal device available to render offscreen; skipping");
return Ok(());
};
let fixture = [200u8, 100, 50, 255];
let device = renderer_device(&renderer);
let texture = imported_texture(
&device,
MTLPixelFormat::BGRA8Unorm_sRGB,
[fixture[2], fixture[1], fixture[0], fixture[3]],
);
let scene = imported_texture_scene(texture.as_ref().to_imported_handle()?, viewport);
let pixels = SceneRenderer::render_scene_to_image(&mut renderer, &scene, viewport)?;
assert_eq!((pixels.width(), pixels.height()), (8, 8));
for (index, pixel) in pixels.data().chunks_exact(4).enumerate() {
assert_eq!(pixel, fixture, "pixel {index} did not round trip");
}
Ok(())
}
#[test]
fn an_imported_texture_in_the_wrong_format_is_refused() {
let Some(renderer) = test_renderer() else {
log::warn!("no Metal device available; skipping");
return;
};
let device = renderer_device(&renderer);
let texture = imported_texture(&device, MTLPixelFormat::BGRA8Unorm, [50, 100, 200, 255]);
let error = texture
.as_ref()
.to_imported_handle()
.expect_err("a non-sRGB texture is not an imported texture");
assert!(
error.to_string().contains("sRGB"),
"the refusal should name the declaration it wants: {error}"
);
}
}