orca/src/mtl_surface.m

283 lines
8.5 KiB
Objective-C

/************************************************************//**
*
* @file: mtl_surface.m
* @author: Martin Fouilleul
* @date: 12/07/2023
* @revision:
*
*****************************************************************/
#import<Metal/Metal.h>
#import <QuartzCore/QuartzCore.h>
#import<QuartzCore/CAMetalLayer.h>
#include<simd/simd.h>
#include"graphics_surface.h"
#include"macro_helpers.h"
#include"osx_app.h"
typedef struct mg_mtl_surface
{
mg_surface_data interface;
// permanent mtl resources
id<MTLDevice> device;
CAMetalLayer* mtlLayer;
id<MTLCommandQueue> commandQueue;
// transient metal resources
id<CAMetalDrawable> drawable;
id<MTLCommandBuffer> commandBuffer;
} mg_mtl_surface;
void mg_mtl_surface_destroy(mg_surface_data* interface)
{
mg_mtl_surface* surface = (mg_mtl_surface*)interface;
@autoreleasepool
{
//NOTE: when GPU frame capture is enabled, if we release resources before all work is completed,
// libMetalCapture crashes... the following hack avoids this crash by enqueuing a last (empty)
// command buffer and waiting for it to complete, ensuring all previous buffers have completed
id<MTLCommandBuffer> endBuffer = [surface->commandQueue commandBuffer];
[endBuffer commit];
[endBuffer waitUntilCompleted];
[surface->commandQueue release];
[surface->mtlLayer removeFromSuperlayer];
[surface->mtlLayer release];
[surface->device release];
}
//NOTE: we don't use mp_layer_cleanup here, because the CAMetalLayer is taken care off by the surface itself
}
void mg_mtl_surface_acquire_command_buffer(mg_mtl_surface* surface)
{
if(surface->commandBuffer == nil)
{
surface->commandBuffer = [surface->commandQueue commandBuffer];
[surface->commandBuffer retain];
}
}
void mg_mtl_surface_acquire_drawable(mg_mtl_surface* surface)
{@autoreleasepool{
/*WARN(martin):
//TODO: we should stop trying to render if we detect that the app is in the background
or occluded
//TODO: We should set a reasonable timeout and skip the frame and log an error in case we are stalled
for too long.
*/
//NOTE: returned drawable could be nil if we stall for more than 1s, although that never seem to happen in practice?
if(surface->drawable == nil)
{
surface->drawable = [surface->mtlLayer nextDrawable];
if(surface->drawable)
{
[surface->drawable retain];
}
}
}}
void mg_mtl_surface_prepare(mg_surface_data* interface)
{
mg_mtl_surface* surface = (mg_mtl_surface*)interface;
mg_mtl_surface_acquire_command_buffer(surface);
}
void mg_mtl_surface_present(mg_surface_data* interface)
{
mg_mtl_surface* surface = (mg_mtl_surface*)interface;
@autoreleasepool
{
if(surface->commandBuffer != nil)
{
if(surface->drawable != nil)
{
[surface->commandBuffer presentDrawable: surface->drawable];
[surface->drawable release];
surface->drawable = nil;
}
[surface->commandBuffer commit];
[surface->commandBuffer release];
surface->commandBuffer = nil;
}
}
}
void mg_mtl_surface_swap_interval(mg_surface_data* interface, int swap)
{
mg_mtl_surface* surface = (mg_mtl_surface*)interface;
@autoreleasepool
{
[surface->mtlLayer setDisplaySyncEnabled: (swap ? YES : NO)];
}
}
void mg_mtl_surface_set_frame(mg_surface_data* interface, mp_rect frame)
{
mg_mtl_surface* surface = (mg_mtl_surface*)interface;
mg_osx_surface_set_frame(interface, frame);
vec2 scale = mg_osx_surface_contents_scaling(interface);
CGRect cgFrame = {{frame.x, frame.y}, {frame.w, frame.h}};
// dispatch_async(dispatch_get_main_queue(),
// ^{
[CATransaction begin];
[CATransaction setDisableActions:YES];
[surface->mtlLayer setFrame: cgFrame];
[CATransaction commit];
// });
CGSize drawableSize = (CGSize){.width = frame.w * scale.x, .height = frame.h * scale.y};
surface->mtlLayer.drawableSize = drawableSize;
}
//TODO fix that according to real scaling, depending on the monitor settings
static const f32 MG_MTL_SURFACE_CONTENTS_SCALING = 2;
//NOTE: max frames in flight (n frames being queued on the GPU while we're working on the n+1 frame).
// for triple buffering, there's 2 frames being queued on the GPU while we're working on the 3rd frame
static const int MG_MTL_MAX_FRAMES_IN_FLIGHT = 2;
mg_surface_data* mg_mtl_surface_create_for_window(mp_window window)
{
mg_mtl_surface* surface = 0;
mp_window_data* windowData = mp_window_ptr_from_handle(window);
if(windowData)
{
surface = (mg_mtl_surface*)malloc(sizeof(mg_mtl_surface));
mg_surface_init_for_window((mg_surface_data*)surface, windowData);
//NOTE(martin): setup interface functions
surface->interface.api = MG_METAL;
surface->interface.destroy = mg_mtl_surface_destroy;
surface->interface.prepare = mg_mtl_surface_prepare;
surface->interface.deselect = 0;
surface->interface.present = mg_mtl_surface_present;
surface->interface.swapInterval = mg_mtl_surface_swap_interval;
surface->interface.setFrame = mg_mtl_surface_set_frame;
@autoreleasepool
{
//-----------------------------------------------------------
//NOTE(martin): create a mtl device and a mtl layer and
//-----------------------------------------------------------
/*WARN(martin):
Calling MTLCreateDefaultSystemDevice(), as advised by the doc, hangs Discord's screen sharing...
The workaround I found, which doesn't make sense, is to set the mtlLayer.device to mtlLayer.preferredDevice,
even if mtlLayer.preferredDevice is the same value as returned by MTLCreateDefaultSystemDevice().
This works for now and allows screen sharing while using orca, but we'll have to revisit this when we want
more control over what GPU gets used.
*/
surface->device = nil;
//Select a discrete GPU, if possible
NSArray<id<MTLDevice>>* devices = MTLCopyAllDevices();
for(id<MTLDevice> device in devices)
{
if(!device.isRemovable && !device.isLowPower)
{
surface->device = device;
break;
}
}
if(surface->device == nil)
{
log_warning("Couldn't select a discrete GPU, using first available device\n");
surface->device = devices[0];
}
//surface->device = MTLCreateSystemDefaultDevice();
surface->mtlLayer = [CAMetalLayer layer];
[surface->mtlLayer retain];
surface->mtlLayer.device = surface->device;
[surface->mtlLayer setOpaque:NO];
[surface->interface.layer.caLayer addSublayer: (CALayer*)surface->mtlLayer];
//-----------------------------------------------------------
//NOTE(martin): set the size and scaling
//-----------------------------------------------------------
NSRect frame = [[windowData->osx.nsWindow contentView] frame];
CGSize size = frame.size;
surface->mtlLayer.frame = (CGRect){{0, 0}, size};
size.width *= MG_MTL_SURFACE_CONTENTS_SCALING;
size.height *= MG_MTL_SURFACE_CONTENTS_SCALING;
surface->mtlLayer.drawableSize = size;
surface->mtlLayer.contentsScale = MG_MTL_SURFACE_CONTENTS_SCALING;
surface->mtlLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
//NOTE(martin): handling resizing
// surface->mtlLayer.autoresizingMask = kCALayerHeightSizable | kCALayerWidthSizable;
// surface->mtlLayer.needsDisplayOnBoundsChange = YES;
//-----------------------------------------------------------
//NOTE(martin): create a command queue
//-----------------------------------------------------------
surface->commandQueue = [surface->device newCommandQueue];
[surface->commandQueue retain];
//NOTE(martin): command buffer and drawable are set on demand and at the end of each present() call
surface->drawable = nil;
surface->commandBuffer = nil;
}
}
return((mg_surface_data*)surface);
}
void* mg_mtl_surface_layer(mg_surface surface)
{
mg_surface_data* surfaceData = mg_surface_data_from_handle(surface);
if(surfaceData && surfaceData->api == MG_METAL)
{
mg_mtl_surface* mtlSurface = (mg_mtl_surface*)surfaceData;
return(mtlSurface->mtlLayer);
}
else
{
return(nil);
}
}
void* mg_mtl_surface_drawable(mg_surface surface)
{
mg_surface_data* surfaceData = mg_surface_data_from_handle(surface);
if(surfaceData && surfaceData->api == MG_METAL)
{
mg_mtl_surface* mtlSurface = (mg_mtl_surface*)surfaceData;
mg_mtl_surface_acquire_drawable(mtlSurface);
return(mtlSurface->drawable);
}
else
{
return(nil);
}
}
void* mg_mtl_surface_command_buffer(mg_surface surface)
{
mg_surface_data* surfaceData = mg_surface_data_from_handle(surface);
if(surfaceData && surfaceData->api == MG_METAL)
{
mg_mtl_surface* mtlSurface = (mg_mtl_surface*)surfaceData;
mg_mtl_surface_acquire_command_buffer(mtlSurface);
return(mtlSurface->commandBuffer);
}
else
{
return(nil);
}
}