255 lines
8.4 KiB
Objective-C
255 lines
8.4 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_internal.h"
|
|
#include"macro_helpers.h"
|
|
#include"osx_app.h"
|
|
|
|
#define LOG_SUBSYSTEM "Graphics"
|
|
|
|
static const u32 MP_MTL_MAX_DRAWABLES_IN_FLIGHT = 3;
|
|
|
|
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;
|
|
|
|
dispatch_semaphore_t drawableSemaphore;
|
|
|
|
} mg_mtl_surface;
|
|
|
|
void mg_mtl_surface_destroy(mg_surface_data* interface)
|
|
{
|
|
mg_mtl_surface* surface = (mg_mtl_surface*)interface;
|
|
|
|
@autoreleasepool
|
|
{
|
|
[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_drawable_and_command_buffer(mg_mtl_surface* surface)
|
|
{@autoreleasepool{
|
|
/*WARN(martin): this is super important
|
|
When the app is put in the background, it seems that if there are buffers in flight, the drawables to
|
|
can be leaked. This causes the gpu to allocate more and more drawables, until the app crashes.
|
|
(note: the drawable objects themselves are released once the app comes back to the forefront, but the
|
|
memory allocated in the GPU is never freed...)
|
|
|
|
In background the gpu seems to create drawable if none is available instead of actually
|
|
blocking on nextDrawable. These drawable never get freed.
|
|
This is not a problem if our shader is fast enough, since a previous drawable becomes
|
|
available before we finish the frame. But we want to protect against it anyway
|
|
|
|
The normal blocking mechanism of nextDrawable seems useless, so we implement our own scheme by
|
|
counting the number of drawables available with a semaphore that gets decremented here and
|
|
incremented in the presentedHandler of the drawable.
|
|
Thus we ensure that we don't consume more drawables than we are able to draw.
|
|
|
|
//TODO: we _also_ should stop trying to render if we detect that the app is in the background
|
|
or occluded, but we can't count only on this because there is a potential race between the
|
|
notification of background mode and the rendering.
|
|
|
|
//TODO: We should set a reasonable timeout and skip the frame and log an error in case we are stalled
|
|
for too long.
|
|
*/
|
|
dispatch_semaphore_wait(surface->drawableSemaphore, DISPATCH_TIME_FOREVER);
|
|
|
|
surface->drawable = [surface->mtlLayer nextDrawable];
|
|
ASSERT(surface->drawable != nil);
|
|
|
|
//TODO: make this a weak reference if we use ARC
|
|
dispatch_semaphore_t semaphore = surface->drawableSemaphore;
|
|
[surface->drawable addPresentedHandler:^(id<MTLDrawable> drawable){
|
|
dispatch_semaphore_signal(semaphore);
|
|
}];
|
|
|
|
//NOTE(martin): create a command buffer
|
|
surface->commandBuffer = [surface->commandQueue commandBuffer];
|
|
|
|
[surface->commandBuffer retain];
|
|
[surface->drawable retain];
|
|
}}
|
|
|
|
void mg_mtl_surface_prepare(mg_surface_data* interface)
|
|
{
|
|
mg_mtl_surface* surface = (mg_mtl_surface*)interface;
|
|
mg_mtl_surface_acquire_drawable_and_command_buffer(surface);
|
|
}
|
|
|
|
void mg_mtl_surface_present(mg_surface_data* interface)
|
|
{
|
|
mg_mtl_surface* surface = (mg_mtl_surface*)interface;
|
|
@autoreleasepool
|
|
{
|
|
//NOTE(martin): present drawable and commit command buffer
|
|
[surface->commandBuffer presentDrawable: surface->drawable];
|
|
[surface->commandBuffer commit];
|
|
[surface->commandBuffer waitUntilCompleted];
|
|
|
|
//NOTE(martin): acquire next frame resources
|
|
[surface->commandBuffer release];
|
|
surface->commandBuffer = nil;
|
|
[surface->drawable release];
|
|
surface->drawable = nil;
|
|
}
|
|
}
|
|
|
|
void mg_mtl_surface_swap_interval(mg_surface_data* interface, int swap)
|
|
{
|
|
mg_mtl_surface* surface = (mg_mtl_surface*)interface;
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//TODO
|
|
////////////////////////////////////////////////////////////////
|
|
}
|
|
|
|
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);
|
|
|
|
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;
|
|
|
|
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.backend = MG_BACKEND_METAL;
|
|
surface->interface.destroy = mg_mtl_surface_destroy;
|
|
surface->interface.prepare = mg_mtl_surface_prepare;
|
|
surface->interface.present = mg_mtl_surface_present;
|
|
surface->interface.swapInterval = mg_mtl_surface_swap_interval;
|
|
|
|
surface->interface.setFrame = mg_mtl_surface_set_frame;
|
|
|
|
@autoreleasepool
|
|
{
|
|
surface->drawableSemaphore = dispatch_semaphore_create(MP_MTL_MAX_DRAWABLES_IN_FLIGHT);
|
|
|
|
//-----------------------------------------------------------
|
|
//NOTE(martin): create a mtl device and a mtl layer and
|
|
//-----------------------------------------------------------
|
|
|
|
surface->device = MTLCreateSystemDefaultDevice();
|
|
[surface->device retain];
|
|
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->backend == MG_BACKEND_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->backend == MG_BACKEND_METAL)
|
|
{
|
|
mg_mtl_surface* mtlSurface = (mg_mtl_surface*)surfaceData;
|
|
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->backend == MG_BACKEND_METAL)
|
|
{
|
|
mg_mtl_surface* mtlSurface = (mg_mtl_surface*)surfaceData;
|
|
return(mtlSurface->commandBuffer);
|
|
}
|
|
else
|
|
{
|
|
return(nil);
|
|
}
|
|
}
|
|
|
|
|
|
#undef LOG_SUBSYSTEM
|