#![deny(unsafe_op_in_unsafe_fn)]
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::{MTLDevice, MTLPixelFormat};
use objc2_metal_kit::MTKView;
use objc2_quartz_core::CAMetalLayer;
use crate::gfx::hdr_output::HdrOutputMode;
use crate::metal::context::{take_embedded_pump_events, take_embedded_view};
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 fullscreen: std::sync::Arc<std::sync::atomic::AtomicBool>,
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,
}
#[derive(Clone, Copy)]
pub(crate) struct HdrRequest {
pub display_requested: bool,
pub pq_requested: bool,
}
pub(crate) struct ExistingWindow {
pub window: Option<Retained<NSWindow>>,
pub mtk_view: Retained<MTKView>,
pub pump_events: bool,
pub fullscreen: std::sync::Arc<std::sync::atomic::AtomicBool>,
pub window_delegate: Option<Retained<crate::appkit::window_delegate::WindowDelegate>>,
}
pub(crate) fn setup_window_and_view(
mtm: objc2::MainThreadMarker,
device: &ProtocolObject<dyn MTLDevice>,
config: WindowConfig,
hdr: HdrRequest,
reuse: Option<ExistingWindow>,
) -> Result<WindowSetup, String> {
let WindowConfig {
title,
width,
height,
title_bar,
geometry_less,
capture_enabled,
} = config;
let HdrRequest {
display_requested: hdr_display_requested,
pq_requested: hdr_pq_requested,
} = hdr;
if let Some(ex) = reuse {
let max_edr = measure_max_edr(mtm);
let hdr_mode = HdrOutputMode::resolve(hdr_display_requested, hdr_pq_requested, max_edr);
configure_mtk_view(&ex.mtk_view, hdr_mode, capture_enabled);
let drawable = ex.mtk_view.drawableSize();
let initial_w = if geometry_less {
1
} else if drawable.width > 0.0 {
drawable.width as u32
} else {
width.max(1)
};
let initial_h = if geometry_less {
1
} else if drawable.height > 0.0 {
drawable.height as u32
} else {
height.max(1)
};
return Ok(WindowSetup {
window: ex.window,
mtk_view: ex.mtk_view,
pump_events: ex.pump_events,
initial_w,
initial_h,
fullscreen: ex.fullscreen,
window_delegate: ex.window_delegate,
hdr_mode,
});
}
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_ptr = take_embedded_view();
let pump_events = embedded_ptr.is_null() || take_embedded_pump_events();
let (window, mtk_view, fullscreen, window_delegate) = if embedded_ptr.is_null() {
let window = crate::appkit::chrome::create_window(mtm, title, width, height, title_bar)?;
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, capture_enabled);
window.setContentView(Some(&mtk_view));
let (delegate, fullscreen) =
crate::appkit::window_delegate::attach_fullscreen_delegate(mtm, &window);
NSApplication::sharedApplication(mtm).activate();
window.makeKeyAndOrderFront(None);
(Some(window), mtk_view, fullscreen, Some(delegate))
} else {
let parent: &NSView = unsafe { &*(embedded_ptr as *const NSView) };
let bounds = parent.bounds();
let mtk_view = MTKView::initWithFrame_device(MTKView::alloc(mtm), bounds, Some(device));
configure_mtk_view(&mtk_view, hdr_mode, capture_enabled);
mtk_view.setAutoresizingMask(
NSAutoresizingMaskOptions::ViewWidthSizable
| NSAutoresizingMaskOptions::ViewHeightSizable,
);
parent.addSubview(&mtk_view);
(
None,
mtk_view,
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
None,
)
};
let initial_drawable_size = mtk_view.drawableSize();
let initial_w = if geometry_less {
1
} else if initial_drawable_size.width > 0.0 {
initial_drawable_size.width as u32
} else {
width.max(1)
};
let initial_h = if geometry_less {
1
} else if initial_drawable_size.height > 0.0 {
initial_drawable_size.height as u32
} else {
height.max(1)
};
Ok(WindowSetup {
window,
mtk_view,
pump_events,
initial_w,
initial_h,
fullscreen,
window_delegate,
hdr_mode,
})
}
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: crate::gfx::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 {
crate::gfx::hdr_output::HdrEncoding::ExtendedLinear => (
unsafe { kCGColorSpaceExtendedLinearDisplayP3 },
"kCGColorSpaceExtendedLinearDisplayP3",
),
crate::gfx::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 colour space (HDR output may \
look desaturated)",
label
),
}
metal_layer.setWantsExtendedDynamicRangeContent(true);
metal_layer.setPixelFormat(MTLPixelFormat::RGBA16Float);
}