orca/src/metal_surface.m

298 lines
9.5 KiB
Mathematica
Raw Normal View History

2022-08-19 18:55:00 +00:00
/************************************************************//**
*
* @file: graphics.m
* @author: Martin Fouilleul
* @date: 12/07/2020
* @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_METAL_MAX_DRAWABLES_IN_FLIGHT = 3;
typedef struct mg_metal_surface
{
mg_surface_info interface;
// permanent metal resources
NSView* view;
id<MTLDevice> device;
CAMetalLayer* metalLayer;
id<MTLCommandQueue> commandQueue;
// transient metal resources
id<CAMetalDrawable> drawable;
id<MTLCommandBuffer> commandBuffer;
dispatch_semaphore_t drawableSemaphore;
} mg_metal_surface;
void mg_metal_surface_acquire_drawable_and_command_buffer(mg_metal_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->metalLayer 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_metal_surface_prepare(mg_surface_info* interface)
{
mg_metal_surface* surface = (mg_metal_surface*)interface;
mg_metal_surface_acquire_drawable_and_command_buffer(surface);
@autoreleasepool
{
MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDescriptor.colorAttachments[0].texture = surface->drawable.texture;
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0,0.0,0.0,0.0);
id<MTLRenderCommandEncoder> renderEncoder = [surface->commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
[renderEncoder endEncoding];
}
}
void mg_metal_surface_present(mg_surface_info* interface)
{
mg_metal_surface* surface = (mg_metal_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_metal_surface_destroy(mg_surface_info* interface)
{
mg_metal_surface* surface = (mg_metal_surface*)interface;
@autoreleasepool
{
[surface->commandQueue release];
[surface->metalLayer release];
[surface->device release];
}
}
void mg_metal_surface_resize(mg_surface_info* interface, u32 width, u32 height)
{
mg_metal_surface* surface = (mg_metal_surface*)interface;
@autoreleasepool
{
//TODO(martin): actually detect scaling
CGRect frame = {{0, 0}, {width, height}};
[surface->view setFrame: frame];
f32 scale = surface->metalLayer.contentsScale;
CGSize drawableSize = (CGSize){.width = width * scale, .height = height * scale};
surface->metalLayer.drawableSize = drawableSize;
}
}
void mg_metal_surface_set_hidden(mg_surface_info* interface, bool hidden)
{
mg_metal_surface* surface = (mg_metal_surface*)interface;
@autoreleasepool
{
[CATransaction begin];
[CATransaction setDisableActions:YES];
[surface->metalLayer setHidden:hidden];
[CATransaction commit];
}
}
vec2 mg_metal_surface_get_size(mg_surface_info* interface)
{
mg_metal_surface* surface = (mg_metal_surface*)interface;
@autoreleasepool
{
//TODO(martin): actually detect scaling
CGRect frame = surface->view.frame;
return((vec2){frame.size.width, frame.size.height});
}
}
void* mg_metal_surface_get_os_resource(mg_surface_info* interface)
{
mg_metal_surface* surface = (mg_metal_surface*)interface;
return((void*)surface->metalLayer);
}
static const f32 MG_METAL_SURFACE_CONTENTS_SCALING = 2;
2022-12-26 17:33:31 +00:00
mg_surface mg_metal_surface_create_for_window(mp_window window)
2022-08-19 18:55:00 +00:00
{
2022-12-26 17:33:31 +00:00
mp_window_data* windowData = mp_window_ptr_from_handle(window);
if(!windowData)
2022-08-19 18:55:00 +00:00
{
return(mg_surface_nil());
}
else
{
mg_metal_surface* surface = (mg_metal_surface*)malloc(sizeof(mg_metal_surface));
//NOTE(martin): setup interface functions
surface->interface.backend = MG_BACKEND_METAL;
surface->interface.destroy = mg_metal_surface_destroy;
surface->interface.prepare = mg_metal_surface_prepare;
surface->interface.present = mg_metal_surface_present;
surface->interface.resize = mg_metal_surface_resize;
surface->interface.setHidden = mg_metal_surface_set_hidden;
surface->interface.getSize = mg_metal_surface_get_size;
surface->interface.getOSResource = mg_metal_surface_get_os_resource;
@autoreleasepool
{
2022-12-26 17:33:31 +00:00
NSRect frame = [[windowData->osx.nsWindow contentView] frame];
surface->view = [[NSView alloc] initWithFrame: frame];
[surface->view setWantsLayer:YES];
[[windowData->osx.nsWindow contentView] addSubview: surface->view];
2022-08-19 18:55:00 +00:00
surface->drawableSemaphore = dispatch_semaphore_create(MP_METAL_MAX_DRAWABLES_IN_FLIGHT);
//-----------------------------------------------------------
//NOTE(martin): create a metal device and a metal layer and
//-----------------------------------------------------------
surface->device = MTLCreateSystemDefaultDevice();
[surface->device retain];
surface->metalLayer = [CAMetalLayer layer];
[surface->metalLayer retain];
[surface->metalLayer setOpaque:NO];
surface->metalLayer.device = surface->device;
//-----------------------------------------------------------
//NOTE(martin): set the size and scaling
//-----------------------------------------------------------
CGSize size = surface->view.bounds.size;
size.width *= MG_METAL_SURFACE_CONTENTS_SCALING;
size.height *= MG_METAL_SURFACE_CONTENTS_SCALING;
surface->metalLayer.drawableSize = size;
surface->metalLayer.contentsScale = MG_METAL_SURFACE_CONTENTS_SCALING;
surface->metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
surface->view.wantsLayer = YES;
surface->view.layer = surface->metalLayer;
//NOTE(martin): handling resizing
surface->view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
surface->metalLayer.autoresizingMask = kCALayerHeightSizable | kCALayerWidthSizable;
surface->metalLayer.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;
}
mg_surface handle = mg_surface_alloc_handle((mg_surface_info*)surface);
return(handle);
}
}
2022-12-26 17:33:31 +00:00
void* mg_metal_surface_layer(mg_surface surface)
2022-08-19 18:55:00 +00:00
{
2022-12-26 17:33:31 +00:00
mg_surface_info* surfaceData = mg_surface_ptr_from_handle(surface);
if(surfaceData && surfaceData->backend == MG_BACKEND_METAL)
2022-08-19 18:55:00 +00:00
{
2022-12-26 17:33:31 +00:00
mg_metal_surface* metalSurface = (mg_metal_surface*)surfaceData;
return(metalSurface->metalLayer);
2022-08-19 18:55:00 +00:00
}
else
{
2022-12-26 17:33:31 +00:00
return(nil);
2022-08-19 18:55:00 +00:00
}
}
2022-12-26 17:33:31 +00:00
void* mg_metal_surface_drawable(mg_surface surface)
{
mg_surface_info* surfaceData = mg_surface_ptr_from_handle(surface);
if(surfaceData && surfaceData->backend == MG_BACKEND_METAL)
{
mg_metal_surface* metalSurface = (mg_metal_surface*)surfaceData;
return(metalSurface->drawable);
}
else
{
return(nil);
}
}
void* mg_metal_surface_command_buffer(mg_surface surface)
{
mg_surface_info* surfaceData = mg_surface_ptr_from_handle(surface);
if(surfaceData && surfaceData->backend == MG_BACKEND_METAL)
{
mg_metal_surface* metalSurface = (mg_metal_surface*)surfaceData;
return(metalSurface->commandBuffer);
}
else
{
return(nil);
}
}
2022-08-19 18:55:00 +00:00
#undef LOG_SUBSYSTEM