#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::render::backend_init::{EmbeddedSurface, SwapchainConfig};
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::hdr_output;
use concinnity_core::render::hdr_output::HdrOutputMode;
use objc2::MainThreadOnly;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_app_kit::{NSApplication, NSAutoresizingMaskOptions, NSScreen, NSView, NSWindow};
use objc2_core_graphics::{
CGColorSpace, kCGColorSpaceDisplayP3_PQ, kCGColorSpaceExtendedLinearDisplayP3,
};
use objc2_metal::{MTLCreateSystemDefaultDevice, MTLDevice, MTLPixelFormat};
use objc2_metal_kit::MTKView;
use objc2_quartz_core::CAMetalLayer;
use crate::metal::allocator::DeviceAllocator;
use crate::metal::context::{MtlHardware, WindowState};
use crate::metal::graph_queues::GraphQueues;
pub(crate) struct WindowSetup {
pub window: Option<Retained<NSWindow>>,
pub mtk_view: Retained<MTKView>,
pub pump_events: bool,
pub initial_w: u32,
pub initial_h: u32,
pub signals: std::sync::Arc<crate::appkit::window_delegate::WindowSignals>,
pub window_delegate: Option<Retained<crate::appkit::window_delegate::WindowDelegate>>,
pub hdr_mode: HdrOutputMode,
}
pub(crate) struct WindowConfig<'a> {
pub title: &'a str,
pub width: u32,
pub height: u32,
pub title_bar: bool,
pub geometry_less: bool,
pub capture_enabled: bool,
pub embedded: Option<EmbeddedSurface>,
}
#[derive(Clone, Copy)]
pub(crate) struct HdrRequest {
pub display_requested: bool,
pub pq_requested: bool,
}
pub(super) fn setup(
reuse: Option<MtlHardware>,
config: WindowConfig,
hdr: HdrRequest,
frames_in_flight: usize,
vsync: bool,
) -> RenderResult<(MtlHardware, (u32, u32))> {
let mtm = objc2::MainThreadMarker::new().ok_or_else(|| {
RenderError::Other("MtlContext::new must be called from the main thread".into())
})?;
let swapchain_config = SwapchainConfig {
frames_in_flight: frames_in_flight.max(1),
hdr_display: hdr.display_requested,
hdr_pq: hdr.pq_requested,
};
let title_bar = config.title_bar;
let (hw, initial_w, initial_h) = match reuse {
Some(mut hw) => {
let view = &hw
.window
.as_ref()
.ok_or_else(|| {
RenderError::Other("reload_world: handed-over hardware has no window".into())
})?
.view;
let (hdr_mode, initial_w, initial_h) = reconfigure_view(mtm, view, config, hdr);
hw.swap_pixel_format = swap_pixel_format(hdr_mode);
hw.hdr_mode = hdr_mode;
hw.swapchain_config = swapchain_config;
(hw, initial_w, initial_h)
}
None => {
let device = MTLCreateSystemDefaultDevice()
.ok_or_else(|| RenderError::Other("no default Metal device".into()))?;
let command_queue = device
.newCommandQueue()
.ok_or_else(|| RenderError::Other("failed to create Metal command queue".into()))?;
let allocator = DeviceAllocator::new(&device, frames_in_flight);
let WindowSetup {
window,
mtk_view,
pump_events,
initial_w,
initial_h,
signals,
window_delegate,
hdr_mode,
} = setup_window_and_view(mtm, &device, config, hdr)?;
let graph_queues = GraphQueues::new(&device);
if graph_queues.is_none() {
tracing::warn!(
"metal: no async-compute queue, submitting the render graph on one queue"
);
}
let window = WindowState {
appkit: crate::appkit::AppKitWindow::new(crate::appkit::AppKitWindowParts {
window,
view: Retained::into_super(mtk_view.clone()),
title_bar,
pump_events,
signals,
window_delegate,
}),
view: mtk_view,
};
let hw = MtlHardware {
device,
allocator,
command_queue,
graph_queues,
swap_pixel_format: swap_pixel_format(hdr_mode),
hdr_mode,
swapchain_config,
window: Some(window),
};
(hw, initial_w, initial_h)
}
};
if let Some(w) = &hw.window {
set_display_sync(&w.view, vsync);
}
Ok((hw, (initial_w, initial_h)))
}
pub(crate) fn setup_window_and_view(
mtm: objc2::MainThreadMarker,
device: &ProtocolObject<dyn MTLDevice>,
config: WindowConfig,
hdr: HdrRequest,
) -> RenderResult<WindowSetup> {
let max_edr = measure_max_edr(mtm);
let hdr_mode = HdrOutputMode::resolve(hdr.display_requested, hdr.pq_requested, max_edr);
if hdr.display_requested && !hdr_mode.is_hdr() {
tracing::warn!(
"HDR display requested but the active display reports max EDR \
multiplier {:.3}: falling back to SDR (BGRA8Unorm) output",
max_edr
);
} else if let HdrOutputMode::Hdr { encoding, .. } = hdr_mode {
tracing::info!(
"HDR display output enabled: max EDR multiplier {:.3}, encoding={:?}",
max_edr,
encoding,
);
}
let embedded = config.embedded;
let pump_events = embedded.is_none_or(|s| s.pump_events);
let (window, mtk_view, signals, window_delegate) = if let Some(surface) = embedded {
let parent: &NSView = unsafe { surface.view.cast::<NSView>().as_ref() };
let bounds = parent.bounds();
let mtk_view = MTKView::initWithFrame_device(MTKView::alloc(mtm), bounds, Some(device));
configure_mtk_view(&mtk_view, hdr_mode, config.capture_enabled);
mtk_view.setAutoresizingMask(
NSAutoresizingMaskOptions::ViewWidthSizable
| NSAutoresizingMaskOptions::ViewHeightSizable,
);
parent.addSubview(&mtk_view);
(None, mtk_view, std::sync::Arc::default(), None)
} else {
let window = crate::appkit::chrome::create_window(
mtm,
config.title,
config.width,
config.height,
config.title_bar,
)
.map_err(RenderError::Other)?;
let content_rect = window.contentRectForFrameRect(window.frame());
let mtk_view =
MTKView::initWithFrame_device(MTKView::alloc(mtm), content_rect, Some(device));
configure_mtk_view(&mtk_view, hdr_mode, config.capture_enabled);
window.setContentView(Some(&mtk_view));
let (delegate, signals) =
crate::appkit::window_delegate::attach_window_delegate(mtm, &window);
NSApplication::sharedApplication(mtm).activate();
window.makeKeyAndOrderFront(None);
(Some(window), mtk_view, signals, Some(delegate))
};
let drawable = mtk_view.drawableSize();
let (initial_w, initial_h) = initial_target_size((drawable.width, drawable.height), &config);
Ok(WindowSetup {
window,
mtk_view,
pump_events,
initial_w,
initial_h,
signals,
window_delegate,
hdr_mode,
})
}
pub(crate) fn reconfigure_view(
mtm: objc2::MainThreadMarker,
mtk_view: &MTKView,
config: WindowConfig,
hdr: HdrRequest,
) -> (HdrOutputMode, u32, u32) {
let max_edr = measure_max_edr(mtm);
let hdr_mode = HdrOutputMode::resolve(hdr.display_requested, hdr.pq_requested, max_edr);
configure_mtk_view(mtk_view, hdr_mode, config.capture_enabled);
let drawable = mtk_view.drawableSize();
let (initial_w, initial_h) = initial_target_size((drawable.width, drawable.height), &config);
(hdr_mode, initial_w, initial_h)
}
fn initial_target_size(drawable: (f64, f64), config: &WindowConfig) -> (u32, u32) {
if config.geometry_less {
return (1, 1);
}
let pick = |live: f64, requested: u32| {
if live > 0.0 {
live as u32
} else {
requested.max(1)
}
};
(
pick(drawable.0, config.width),
pick(drawable.1, config.height),
)
}
pub(crate) fn measure_max_edr(mtm: objc2::MainThreadMarker) -> f32 {
let screens = NSScreen::screens(mtm);
let mut best: f64 = 1.0;
for i in 0..screens.count() {
let s = screens.objectAtIndex(i);
let v = s.maximumPotentialExtendedDynamicRangeColorComponentValue();
if v > best {
best = v;
}
}
best as f32
}
fn configure_mtk_view(mtk_view: &MTKView, hdr_mode: HdrOutputMode, capture_enabled: bool) {
mtk_view.setPaused(true);
mtk_view.setEnableSetNeedsDisplay(false);
let swap_fmt = swap_pixel_format(hdr_mode);
mtk_view.setColorPixelFormat(swap_fmt);
mtk_view.setDepthStencilPixelFormat(MTLPixelFormat::Invalid);
mtk_view.setSampleCount(1);
if capture_enabled {
mtk_view.setFramebufferOnly(false);
}
if let HdrOutputMode::Hdr { encoding, .. } = hdr_mode {
configure_hdr_layer(mtk_view, encoding);
}
}
pub(crate) fn swap_pixel_format(hdr_mode: HdrOutputMode) -> MTLPixelFormat {
if hdr_mode.is_hdr() {
MTLPixelFormat::RGBA16Float
} else {
MTLPixelFormat::BGRA8Unorm
}
}
pub(crate) fn set_display_sync(mtk_view: &MTKView, on: bool) {
let Some(layer) = mtk_view.layer() else {
return;
};
if let Some(metal_layer) = layer.downcast_ref::<CAMetalLayer>() {
metal_layer.setDisplaySyncEnabled(on);
}
}
fn configure_hdr_layer(mtk_view: &MTKView, encoding: hdr_output::HdrEncoding) {
let Some(layer) = mtk_view.layer() else {
tracing::warn!(
"HDR display requested but MTKView has no backing layer: falling back to SDR"
);
return;
};
let metal_layer: &CAMetalLayer = match layer.downcast_ref::<CAMetalLayer>() {
Some(l) => l,
None => {
tracing::warn!(
"HDR display requested but the MTKView layer is not a CAMetalLayer: falling \
back to SDR"
);
return;
}
};
let (name, label): (_, &str) = match encoding {
hdr_output::HdrEncoding::ExtendedLinear => (
unsafe { kCGColorSpaceExtendedLinearDisplayP3 },
"kCGColorSpaceExtendedLinearDisplayP3",
),
hdr_output::HdrEncoding::Pq => (
unsafe { kCGColorSpaceDisplayP3_PQ },
"kCGColorSpaceDisplayP3_PQ",
),
};
let colorspace = CGColorSpace::with_name(Some(name));
match colorspace.as_deref() {
Some(cs) => metal_layer.setColorspace(Some(cs)),
None => tracing::warn!(
"{} unavailable: leaving CAMetalLayer at default color space (HDR output may \
look desaturated)",
label
),
}
metal_layer.setWantsExtendedDynamicRangeContent(true);
metal_layer.setPixelFormat(MTLPixelFormat::RGBA16Float);
}
#[cfg(test)]
mod tests {
use super::*;
fn config(width: u32, height: u32, geometry_less: bool) -> WindowConfig<'static> {
WindowConfig {
title: "",
width,
height,
title_bar: true,
geometry_less,
capture_enabled: false,
embedded: None,
}
}
#[test]
fn live_drawable_sets_the_target_size() {
assert_eq!(
initial_target_size((2048.0, 1536.0), &config(1024, 768, false)),
(2048, 1536)
);
}
#[test]
fn missing_drawable_falls_back_to_the_requested_size() {
assert_eq!(
initial_target_size((0.0, 0.0), &config(1024, 768, false)),
(1024, 768)
);
}
#[test]
fn requested_size_is_at_least_one_pixel() {
assert_eq!(
initial_target_size((0.0, 0.0), &config(0, 0, false)),
(1, 1)
);
}
#[test]
fn geometry_less_world_clamps_to_one_pixel() {
assert_eq!(
initial_target_size((2048.0, 1536.0), &config(1024, 768, true)),
(1, 1)
);
}
}