2025 Graphics & GamesSpatial Computing
WWDC25 · 25 min · Graphics & Games / Spatial Computing
What’s new in Metal rendering for immersive apps
Discover the latest improvements in Metal rendering for immersive apps with Compositor Services. Learn how to add hover effects to highlight your app’s interactive elements, and how to render in higher fidelity with dynamic render quality. Find out about the new progressive immersion style. And explore how you can bring immersive experiences to macOS apps by directly rendering Metal content from Mac to Vision Pro. To get the most out of this session, first watch “Discover Metal for immersive apps” from WWDC23.
Watch at developer.apple.com ↗Chapters
Code shown on screen · 15 snippets
Scene render loop
// Scene render loop
extension Renderer {
func renderFrame(with scene: MyScene) {
guard let frame = layerRenderer.queryNextFrame() else { return }
frame.startUpdate()
scene.performFrameIndependentUpdates()
frame.endUpdate()
let drawables = frame.queryDrawables()
guard !drawables.isEmpty else { return }
guard let timing = frame.predictTiming() else { return }
LayerRenderer.Clock().wait(until: timing.optimalInputTime)
frame.startSubmission()
scene.render(to: drawable)
frame.endSubmission()
}
} Layer configuration
// Layer configuration
struct MyConfiguration: CompositorLayerConfiguration {
func makeConfiguration(capabilities: LayerRenderer.Capabilities,
configuration: inout LayerRenderer.Configuration) {
// Configure other aspects of LayerRenderer
let trackingAreasFormat: MTLPixelFormat = .r8Uint
if capabilities.supportedTrackingAreasFormats.contains(trackingAreasFormat) {
configuration.trackingAreasFormat = trackingAreasFormat
}
}
} Object render function
// Object render function
extension MyObject {
func render(drawable: Drawable, renderEncoder: MTLRenderCommandEncoder) {
var renderValue: LayerRenderer.Drawable.TrackingArea.RenderValue? = nil
if self.isInteractive {
let trackingArea = drawable.addTrackingArea(identifier: self.identifier)
if self.usesHoverEffect {
trackingArea.addHoverEffect(.automatic)
}
renderValue = trackingArea.renderValue
}
self.draw(with: commandEncoder, trackingAreaRenderValue: renderValue)
}
} Metal fragment shader
// Metal fragment shader
struct FragmentOut
{
float4 color [[color(0)]];
uint16_t trackingAreaRenderValue [[color(1)]];
};
fragment FragmentOut fragmentShader( /* ... */ )
{
// ...
return FragmentOut {
float4(outColor, 1.0),
uniforms.trackingAreaRenderValue
};
} Event processing
// Event processing
extension Renderer {
func processEvent(_ event: SpatialEventCollection.Event) {
let object = scene.objects.first {
$0.identifier == event.trackingAreaIdentifier
}
if let object {
object.performAction()
}
}
} Quality constants
// Quality constants
extension MyScene {
struct Constants {
static let menuRenderQuality: LayerRenderer.RenderQuality = .init(0.8)
static let worldRenderQuality: LayerRenderer.RenderQuality = .init(0.6)
static var maxRenderQuality: LayerRenderer.RenderQuality { menuRenderQuality }
}
} Layer configuration
// Layer configuration
struct MyConfiguration: CompositorLayerConfiguration {
func makeConfiguration(capabilities: LayerRenderer.Capabilities,
configuration: inout LayerRenderer.Configuration) {
// Configure other aspects of LayerRenderer
if configuration.isFoveationEnabled {
configuration.maxRenderQuality = MyScene.Constants.maxRenderQuality
}
} Set runtime render quality
// Set runtime render quality
extension MyScene {
var renderQuality: LayerRenderer.RenderQuality {
switch type {
case .world: Constants.worldRenderQuality
case .menu: Constants.menuRenderQuality
}
}
}
extension Renderer {
func adjustRenderQuality(for scene: MyScene) {
guard layerRenderer.configuration.isFoveationEnabled else {
return;
}
layerRenderer.renderQuality = scene.renderQuality
}
} SwiftUI immersion style
// SwiftUI immersion style
@main
struct MyApp: App {
var immersionStyle: ImmersionStyle
var body: some Scene {
ImmersiveSpace(id: "MyImmersiveSpace") {
CompositorLayer(configuration: MyConfiguration()) { layerRenderer in
Renderer.startRenderLoop(layerRenderer)
}
}
.immersionStyle(selection: $immersionStyle, in: .progressive, .full)
}
} Layer configuration
// Layer configuration
struct MyConfiguration: CompositorLayerConfiguration {
func makeConfiguration(capabilities: LayerRenderer.Capabilities,
configuration: inout LayerRenderer.Configuration) {
// Configure other aspects of LayerRenderer
if configuration.layout == .layered {
let stencilFormat: MTLPixelFormat = .stencil8
if capabilities.drawableRenderContextSupportedStencilFormats.contains(
stencilFormat
) {
configuration.drawableRenderContextStencilFormat = stencilFormat
}
configuration.drawableRenderContextRasterSampleCount = 1
}
}
} Render loop
// Render loop
struct Renderer {
let portalStencilValue: UInt8 = 200 // Value not used in other stencil operations
func renderFrame(with scene: MyScene,
drawable: LayerRenderer.Drawable,
commandBuffer: MTLCommandBuffer) {
let drawableRenderContext = drawable.addRenderContext(commandBuffer: commandBuffer)
let renderEncoder = configureRenderPass(commandBuffer: commandBuffer)
drawableRenderContext.drawMaskOnStencilAttachment(commandEncoder: renderEncoder,
value: portalStencilValue)
renderEncoder.setStencilReferenceValue(UInt32(portalStencilValue))
scene.render(to: drawable, renderEncoder: renderEncoder)
drawableRenderContext.endEncoding(commandEncoder: commandEncoder)
drawable.encodePresent(commandBuffer: commandBuffer)
}
} App structure
// App structure
@main
struct MyImmersiveMacApp: App {
var immersionStyle: ImmersionStyle = .full
var body: some Scene {
WindowGroup {
MyAppContent()
}
RemoteImmersiveSpace(id: "MyRemoteImmersiveSpace") {
MyCompositorContent()
}
.immersionStyle(selection: $immersionStyle, in: .full, .progressive)
}
} App UI
// App UI
struct MyAppContent: View {
(\.supportsRemoteScenes) private var supportsRemoteScenes
(\.openImmersiveSpace) private var openImmersiveSpace
private var spaceState: OpenImmersiveSpaceAction.Result?
var body: some View {
if !supportsRemoteScenes {
Text("Remote SwiftUI scenes are not supported on this Mac.")
} else if spaceState != nil {
MySpaceStateView($spaceState)
} else {
Button("Open remote immersive space") {
Task {
spaceState = await openImmersiveSpace(id: "MyRemoteImmersiveSpace")
}
}
}
}
} Compositor content and ARKit session
// Compositor content and ARKit session
struct MyCompositorContent: CompositorContent {
(\.remoteDeviceIdentifier) private var remoteDeviceIdentifier
var body: some CompositorContent {
CompositorLayer(configuration: MyConfiguration()) { layerRenderer in
guard let remoteDeviceIdentifier else { return }
let arSession = ARKitSession(device: remoteDeviceIdentifier)
Renderer.startRenderLoop(layerRenderer, arSession)
}
}
} C interoperability
// Swift
let remoteDevice: ar_device_t = remoteDeviceIdentifier.cDevice
Renderer.start_rendering(layerRenderer, remoteDevice)
// C
void start_rendering(cp_layer_renderer_t layer_renderer, ar_device_t remoteDevice) {
ar_session_t session = ar_session_create_with_device(remoteDevice);
// ...
} Resources
Related sessions
-
39 min -
18 min -
20 min -
21 min -
24 min -
27 min -
32 min -
26 min