Files
test2/intern/ghost/intern/GHOST_ContextCGL.mm
Jason Fielder 7fbc9e9428 Fix: Metal: Memory leaks identified by Instruments and Xcode memory graph.
Running Xcode memory graphs and the Instruments tools revealed
memory leaks caused, in the main, by over-retained objects.
This removes the unnecessary 'retains' and adds some asserts
to guard against over-retaining in the future.

There are a few memory leaks remaining involving PyUnicode_DecodeUTF8
but I am unable to identify the cause of these at this time.

Authored by Apple: James McCarthy

Pull Request: https://projects.blender.org/blender/blender/pulls/129117
2024-11-01 11:56:51 +01:00

432 lines
13 KiB
Plaintext

/* SPDX-FileCopyrightText: 2013 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup GHOST
*
* Definition of GHOST_ContextCGL class.
*/
/* Don't generate OpenGL deprecation warning. This is a known thing, and is not something easily
* solvable in a short term. */
#ifdef __clang__
# pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
#include "GHOST_ContextCGL.hh"
#import <Cocoa/Cocoa.h>
#import <Metal/Metal.h>
#import <QuartzCore/QuartzCore.h>
#include <cassert>
#include <vector>
static const MTLPixelFormat METAL_FRAMEBUFFERPIXEL_FORMAT_EDR = MTLPixelFormatRGBA16Float;
static void ghost_fatal_error_dialog(const char *msg)
{
@autoreleasepool {
NSString *message = [NSString stringWithFormat:@"Error opening window:\n%s", msg];
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = @"Blender";
alert.informativeText = message;
alert.alertStyle = NSAlertStyleCritical;
[alert addButtonWithTitle:@"Quit"];
[alert runModal];
}
exit(1);
}
MTLCommandQueue *GHOST_ContextCGL::s_sharedMetalCommandQueue = nil;
int GHOST_ContextCGL::s_sharedCount = 0;
GHOST_ContextCGL::GHOST_ContextCGL(bool stereoVisual,
NSView *metalView,
CAMetalLayer *metalLayer,
int debug)
: GHOST_Context(stereoVisual),
m_metalView(metalView),
m_metalLayer(metalLayer),
m_metalRenderPipeline(nil),
m_debug(debug)
{
@autoreleasepool {
/* Initialize Metal Swap-chain. */
current_swapchain_index = 0;
for (int i = 0; i < METAL_SWAPCHAIN_SIZE; i++) {
m_defaultFramebufferMetalTexture[i].texture = nil;
m_defaultFramebufferMetalTexture[i].index = i;
}
if (m_metalView) {
m_ownsMetalDevice = false;
metalInit();
}
else {
/* Prepare offscreen GHOST Context Metal device. */
id<MTLDevice> metalDevice = MTLCreateSystemDefaultDevice();
if (m_debug) {
printf("Selected Metal Device: %s\n", [metalDevice.name UTF8String]);
}
m_ownsMetalDevice = true;
if (metalDevice) {
m_metalLayer = [[CAMetalLayer alloc] init];
m_metalLayer.edgeAntialiasingMask = 0;
m_metalLayer.masksToBounds = NO;
m_metalLayer.opaque = YES;
m_metalLayer.framebufferOnly = YES;
m_metalLayer.presentsWithTransaction = NO;
[m_metalLayer removeAllAnimations];
m_metalLayer.device = metalDevice;
m_metalLayer.allowsNextDrawableTimeout = NO;
/* Enable EDR support. This is done by:
* 1. Using a floating point render target, so that values outside 0..1 can be used
* 2. Informing the OS that we are EDR aware, and intend to use values outside 0..1
* 3. Setting the extended sRGB color space so that the OS knows how to interpret the
* values.
*/
m_metalLayer.wantsExtendedDynamicRangeContent = YES;
m_metalLayer.pixelFormat = METAL_FRAMEBUFFERPIXEL_FORMAT_EDR;
const CFStringRef name = kCGColorSpaceExtendedSRGB;
CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(name);
m_metalLayer.colorspace = colorspace;
CGColorSpaceRelease(colorspace);
metalInit();
}
else {
ghost_fatal_error_dialog(
"[ERROR] Failed to create Metal device for offscreen GHOST Context.\n");
}
}
/* Initialize swap-interval. */
mtl_SwapInterval = 60;
}
}
GHOST_ContextCGL::~GHOST_ContextCGL()
{
metalFree();
if (m_ownsMetalDevice) {
if (m_metalLayer) {
[m_metalLayer release];
m_metalLayer = nil;
}
}
assert(s_sharedCount);
s_sharedCount--;
[s_sharedMetalCommandQueue release];
if (s_sharedCount == 0) {
s_sharedMetalCommandQueue = nil;
}
}
GHOST_TSuccess GHOST_ContextCGL::swapBuffers()
{
if (m_metalView) {
metalSwapBuffers();
}
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_ContextCGL::setSwapInterval(int interval)
{
mtl_SwapInterval = interval;
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_ContextCGL::getSwapInterval(int &intervalOut)
{
intervalOut = mtl_SwapInterval;
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_ContextCGL::activateDrawingContext()
{
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_ContextCGL::releaseDrawingContext()
{
return GHOST_kSuccess;
}
unsigned int GHOST_ContextCGL::getDefaultFramebuffer()
{
/* NOTE(Metal): This is not valid. */
return 0;
}
GHOST_TSuccess GHOST_ContextCGL::updateDrawingContext()
{
if (m_metalView) {
metalUpdateFramebuffer();
return GHOST_kSuccess;
}
return GHOST_kFailure;
}
id<MTLTexture> GHOST_ContextCGL::metalOverlayTexture()
{
/* Increment Swap-chain - Only needed if context is requesting a new texture */
current_swapchain_index = (current_swapchain_index + 1) % METAL_SWAPCHAIN_SIZE;
/* Ensure backing texture is ready for current swapchain index */
updateDrawingContext();
/* Return texture. */
return m_defaultFramebufferMetalTexture[current_swapchain_index].texture;
}
MTLCommandQueue *GHOST_ContextCGL::metalCommandQueue()
{
return s_sharedMetalCommandQueue;
}
MTLDevice *GHOST_ContextCGL::metalDevice()
{
id<MTLDevice> device = m_metalLayer.device;
return (MTLDevice *)device;
}
void GHOST_ContextCGL::metalRegisterPresentCallback(void (*callback)(
MTLRenderPassDescriptor *, id<MTLRenderPipelineState>, id<MTLTexture>, id<CAMetalDrawable>))
{
this->contextPresentCallback = callback;
}
GHOST_TSuccess GHOST_ContextCGL::initializeDrawingContext()
{
@autoreleasepool {
if (m_metalView) {
metalInitFramebuffer();
}
}
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_ContextCGL::releaseNativeHandles()
{
m_metalView = nil;
return GHOST_kSuccess;
}
void GHOST_ContextCGL::metalInit()
{
@autoreleasepool {
id<MTLDevice> device = m_metalLayer.device;
/* Create a command queue for blit/present operation.
* NOTE: All context should share a single command queue
* to ensure correct ordering of work submitted from multiple contexts. */
if (s_sharedMetalCommandQueue == nil) {
s_sharedMetalCommandQueue = (MTLCommandQueue *)[device
newCommandQueueWithMaxCommandBufferCount:GHOST_ContextCGL::max_command_buffer_count];
}
/* Ensure active GHOSTContext retains a reference to the shared context. */
[s_sharedMetalCommandQueue retain];
s_sharedCount++;
/* Create shaders for blit operation. */
NSString *source = @R"msl(
using namespace metal;
struct Vertex {
float4 position [[position]];
float2 texCoord [[attribute(0)]];
};
vertex Vertex vertex_shader(uint v_id [[vertex_id]]) {
Vertex vtx;
vtx.position.x = float(v_id & 1) * 4.0 - 1.0;
vtx.position.y = float(v_id >> 1) * 4.0 - 1.0;
vtx.position.z = 0.0;
vtx.position.w = 1.0;
vtx.texCoord = vtx.position.xy * 0.5 + 0.5;
return vtx;
}
constexpr sampler s {};
fragment float4 fragment_shader(Vertex v [[stage_in]],
texture2d<float> t [[texture(0)]]) {
/* Final blit should ensure alpha is 1.0. This resolves
* rendering artifacts for blitting of final back-buffer. */
float4 out_tex = t.sample(s, v.texCoord);
out_tex.a = 1.0;
return out_tex;
}
)msl";
MTLCompileOptions *options = [[[MTLCompileOptions alloc] init] autorelease];
options.languageVersion = MTLLanguageVersion1_1;
NSError *error = nil;
id<MTLLibrary> library = [device newLibraryWithSource:source options:options error:&error];
if (error) {
ghost_fatal_error_dialog(
"GHOST_ContextCGL::metalInit: newLibraryWithSource:options:error: failed!");
}
/* Create a render pipeline for blit operation. */
MTLRenderPipelineDescriptor *desc = [[[MTLRenderPipelineDescriptor alloc] init] autorelease];
desc.fragmentFunction = [library newFunctionWithName:@"fragment_shader"];
desc.vertexFunction = [library newFunctionWithName:@"vertex_shader"];
[desc.colorAttachments objectAtIndexedSubscript:0].pixelFormat =
METAL_FRAMEBUFFERPIXEL_FORMAT_EDR;
/* Ensure library is released. */
[library autorelease];
m_metalRenderPipeline = (MTLRenderPipelineState *)[device
newRenderPipelineStateWithDescriptor:desc
error:&error];
if (error) {
ghost_fatal_error_dialog(
"GHOST_ContextCGL::metalInit: newRenderPipelineStateWithDescriptor:error: failed!");
}
/* Create a render pipeline to composite things rendered with Metal on top
* of the frame-buffer contents. Uses the same vertex and fragment shader
* as the blit above, but with alpha blending enabled. */
desc.label = @"Metal Overlay";
desc.colorAttachments[0].blendingEnabled = YES;
desc.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
desc.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
if (error) {
ghost_fatal_error_dialog(
"GHOST_ContextCGL::metalInit: newRenderPipelineStateWithDescriptor:error: failed (when "
"creating the Metal overlay pipeline)!");
}
[desc.fragmentFunction release];
[desc.vertexFunction release];
}
}
void GHOST_ContextCGL::metalFree()
{
if (m_metalRenderPipeline) {
[m_metalRenderPipeline release];
m_metalRenderPipeline = nil;
}
for (int i = 0; i < METAL_SWAPCHAIN_SIZE; i++) {
if (m_defaultFramebufferMetalTexture[i].texture) {
[m_defaultFramebufferMetalTexture[i].texture release];
m_defaultFramebufferMetalTexture[i].texture = nil;
}
}
}
void GHOST_ContextCGL::metalInitFramebuffer()
{
updateDrawingContext();
}
void GHOST_ContextCGL::metalUpdateFramebuffer()
{
@autoreleasepool {
const NSRect bounds = [m_metalView bounds];
const NSSize backingSize = [m_metalView convertSizeToBacking:bounds.size];
const size_t width = size_t(backingSize.width);
const size_t height = size_t(backingSize.height);
if (m_defaultFramebufferMetalTexture[current_swapchain_index].texture &&
m_defaultFramebufferMetalTexture[current_swapchain_index].texture.width == width &&
m_defaultFramebufferMetalTexture[current_swapchain_index].texture.height == height)
{
return;
}
/* Free old texture */
[m_defaultFramebufferMetalTexture[current_swapchain_index].texture release];
id<MTLDevice> device = m_metalLayer.device;
MTLTextureDescriptor *overlayDesc = [MTLTextureDescriptor
texture2DDescriptorWithPixelFormat:METAL_FRAMEBUFFERPIXEL_FORMAT_EDR
width:width
height:height
mipmapped:NO];
overlayDesc.storageMode = MTLStorageModePrivate;
overlayDesc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
id<MTLTexture> overlayTex = [device newTextureWithDescriptor:overlayDesc];
if (!overlayTex) {
ghost_fatal_error_dialog(
"GHOST_ContextCGL::metalUpdateFramebuffer: failed to create Metal overlay texture!");
}
else {
overlayTex.label = [NSString
stringWithFormat:@"Metal Overlay for GHOST Context %p", this]; //@"";
}
m_defaultFramebufferMetalTexture[current_swapchain_index].texture = overlayTex;
/* Clear texture on create */
id<MTLCommandBuffer> cmdBuffer = [s_sharedMetalCommandQueue commandBuffer];
MTLRenderPassDescriptor *passDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
{
auto attachment = [passDescriptor.colorAttachments objectAtIndexedSubscript:0];
attachment.texture = m_defaultFramebufferMetalTexture[current_swapchain_index].texture;
attachment.loadAction = MTLLoadActionClear;
attachment.clearColor = MTLClearColorMake(0.294, 0.294, 0.294, 1.000);
attachment.storeAction = MTLStoreActionStore;
}
{
id<MTLRenderCommandEncoder> enc = [cmdBuffer
renderCommandEncoderWithDescriptor:passDescriptor];
[enc endEncoding];
}
[cmdBuffer commit];
m_metalLayer.drawableSize = CGSizeMake(CGFloat(width), CGFloat(height));
}
}
void GHOST_ContextCGL::metalSwapBuffers()
{
@autoreleasepool {
updateDrawingContext();
id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable];
if (!drawable) {
return;
}
MTLRenderPassDescriptor *passDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
{
auto attachment = [passDescriptor.colorAttachments objectAtIndexedSubscript:0];
attachment.texture = drawable.texture;
attachment.loadAction = MTLLoadActionClear;
attachment.clearColor = MTLClearColorMake(1.0, 0.294, 0.294, 1.000);
attachment.storeAction = MTLStoreActionStore;
}
assert(contextPresentCallback);
assert(m_defaultFramebufferMetalTexture[current_swapchain_index].texture != nil);
(*contextPresentCallback)(passDescriptor,
(id<MTLRenderPipelineState>)m_metalRenderPipeline,
m_defaultFramebufferMetalTexture[current_swapchain_index].texture,
drawable);
}
}