From 66981e4facbfd737fbaa6e71db034a887a198897 Mon Sep 17 00:00:00 2001 From: Chris Hocking Date: Sat, 19 Oct 2024 11:40:38 +1100 Subject: [PATCH] Simplified how we render error messages --- Source/Gyroflow/Plugin/GyroflowConstants.h | 12 +- Source/Gyroflow/Plugin/GyroflowPlugIn.h | 2 + Source/Gyroflow/Plugin/GyroflowPlugIn.m | 453 ++++++++------------- 3 files changed, 194 insertions(+), 273 deletions(-) diff --git a/Source/Gyroflow/Plugin/GyroflowConstants.h b/Source/Gyroflow/Plugin/GyroflowConstants.h index e8a01d11..6d99d71a 100644 --- a/Source/Gyroflow/Plugin/GyroflowConstants.h +++ b/Source/Gyroflow/Plugin/GyroflowConstants.h @@ -113,7 +113,17 @@ enum { kFxError_PlugInStateIsNil, // Plugin State is `nil` kFxError_UnsupportedPixelFormat, // Unsupported Pixel Format kFxError_FailedToCreatePluginState, // Failed to create plugin state - kFxError_CommandQueueWasNilDuringShowErrorMessage // Command Queue was `nil` during a show error message render. + kFxError_CommandQueueWasNilDuringShowErrorMessage, // Command Queue was `nil` during a show error message render. + + //--------------------------------------------------------- + // Error Messages: + //--------------------------------------------------------- + kFxError_AssetLoadFailed, // Failed to load error message asset + kFxError_ImageCreationFailed, // Failed to create image for error messages + kFxError_CIContextCreationFailed, // Failed to create Core Image context for error messages + kFxError_IOSurfaceCreationFailed, // Failed to create IOSurface for error messages + kFxError_MetalDeviceNotFound, // Failed to get Metal Device for error messages + kFxError_CommandBufferCreationFailed, // Failed to create Command Buffer for error messages }; #endif /* GyroflowConstants_h */ diff --git a/Source/Gyroflow/Plugin/GyroflowPlugIn.h b/Source/Gyroflow/Plugin/GyroflowPlugIn.h index 8592d2e4..faf8e779 100644 --- a/Source/Gyroflow/Plugin/GyroflowPlugIn.h +++ b/Source/Gyroflow/Plugin/GyroflowPlugIn.h @@ -39,6 +39,8 @@ #import #import +#import + //--------------------------------------------------------- // Metal Performance Shaders for scaling: //--------------------------------------------------------- diff --git a/Source/Gyroflow/Plugin/GyroflowPlugIn.m b/Source/Gyroflow/Plugin/GyroflowPlugIn.m index 0fb02ae7..ac8ec49e 100644 --- a/Source/Gyroflow/Plugin/GyroflowPlugIn.m +++ b/Source/Gyroflow/Plugin/GyroflowPlugIn.m @@ -1539,321 +1539,230 @@ - (BOOL)renderErrorMessageWithID:(NSString*)errorMessageID fullHeight:(float)fullHeight fullWidth:(float)fullWidth outError:(NSError * _Nullable * _Nullable)outError - outputHeight:(float)outputHeight outputWidth:(float)outputWidth - sourceImages:(NSArray * _Nonnull)sourceImages -{ - MetalDeviceCache* deviceCache = [MetalDeviceCache deviceCache]; - MTLPixelFormat pixelFormat = [MetalDeviceCache MTLPixelFormatForImageTile:destinationImage]; - id commandQueue = [deviceCache commandQueueWithRegistryID:destinationImage.deviceRegistryID - pixelFormat:pixelFormat]; - if (commandQueue == nil) - { - NSString *errorMessage = @"FATAL ERROR: commandQueue was nil when attempting to show an error message."; + outputHeight:(float)outputHeight + outputWidth:(float)outputWidth + sourceImages:(NSArray * _Nonnull)sourceImages { + + // ------------------------------------------------------------ + // Load the PNG image from assets using NSImage: + // ------------------------------------------------------------ + NSImage *nsImage = [NSImage imageNamed:errorMessageID]; + if (!nsImage) { + NSString *errorMessage = [NSString stringWithFormat:@"FATAL ERROR: Failed to load asset: %@", errorMessageID]; NSLog(@"[Gyroflow Toolbox Renderer] %@", errorMessage); if (outError != NULL) { *outError = [NSError errorWithDomain:FxPlugErrorDomain - code:kFxError_CommandQueueWasNilDuringShowErrorMessage + code:kFxError_AssetLoadFailed userInfo:@{ NSLocalizedDescriptionKey : errorMessage }]; } return NO; } - - id commandBuffer = [commandQueue commandBuffer]; - commandBuffer.label = @"Gyroflow Toolbox Error Command Buffer"; - [commandBuffer enqueue]; - + //--------------------------------------------------------- - // Load the texture from our "Assets": + // Get CGImage from NSImage: //--------------------------------------------------------- - NSImage *nsImage = [NSImage imageNamed:errorMessageID]; - CGImageRef image = [nsImage CGImageForProposedRect:NULL context:nil hints:nil]; - - if (image == NULL) { - NSString *errorMessage = [NSString stringWithFormat:@"FATAL ERROR: Failed to get the following asset from the asset catalog: %@", errorMessageID]; + CGImageRef cgImage = [nsImage CGImageForProposedRect:NULL context:nil hints:nil]; + if (!cgImage) { + NSString *errorMessage = [NSString stringWithFormat:@"FATAL ERROR: Unable to create CGImage from asset: %@", errorMessageID]; NSLog(@"[Gyroflow Toolbox Renderer] %@", errorMessage); if (outError != NULL) { *outError = [NSError errorWithDomain:FxPlugErrorDomain - code:kFxError_CommandQueueWasNilDuringShowErrorMessage + code:kFxError_AssetLoadFailed userInfo:@{ NSLocalizedDescriptionKey : errorMessage }]; } return NO; } //--------------------------------------------------------- - // Get the image width and height: - //--------------------------------------------------------- - size_t width = CGImageGetWidth(image); - size_t height = CGImageGetHeight(image); - - //--------------------------------------------------------- - // Create a bitmap context in RGBA format: - //--------------------------------------------------------- - void *bitmapData = malloc(width * height * 4); - CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); - CGContextRef context = CGBitmapContextCreate(bitmapData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); - - //--------------------------------------------------------- - // Draw the image into the context: - //--------------------------------------------------------- - CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); - - //--------------------------------------------------------- - // Now bitmapData contains the image data in RGBA format: + // Flip the image: //--------------------------------------------------------- - MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm_sRGB width:width height:height mipmapped:NO]; - id inputTexture = [commandQueue.device newTextureWithDescriptor:textureDescriptor]; - [inputTexture replaceRegion:MTLRegionMake2D(0, 0, width, height) mipmapLevel:0 withBytes:bitmapData bytesPerRow:4 * width]; - - //--------------------------------------------------------- - // Cleanup: - //--------------------------------------------------------- - CGContextRelease(context); - CGColorSpaceRelease(colorSpace); - free(bitmapData); + if (cgImage) { + //--------------------------------------------------------- + // Create a CGContext to flip the image vertically: + //--------------------------------------------------------- + CGSize imageSize = CGSizeMake(CGImageGetWidth(cgImage), CGImageGetHeight(cgImage)); + CGContextRef bitmapContext = CGBitmapContextCreate(NULL, imageSize.width, imageSize.height, 8, 0, CGImageGetColorSpace(cgImage), kCGImageAlphaPremultipliedLast); - //--------------------------------------------------------- - // Get the outputTexture from FxPlug: - //--------------------------------------------------------- - id outputTexture = [destinationImage metalTextureForDevice:[deviceCache deviceWithRegistryID:destinationImage.deviceRegistryID]]; - - //--------------------------------------------------------- - // If square pixels, we'll manipulate the height and y - // axis manually: - //--------------------------------------------------------- - float correctedHeight = outputHeight; - float differenceBetweenHeights = 0; - if (fullHeight == outputHeight) { - correctedHeight = ((float)inputTexture.height/(float)inputTexture.width) * outputWidth; - differenceBetweenHeights = (outputHeight - correctedHeight) / 2; - } - - //--------------------------------------------------------- - // Use a "Metal Performance Shader" to scale the texture - // to the correct size. Note, we're using the full width - // and height, to compensate for non-square pixels: - //--------------------------------------------------------- - id scaledInputTexture = nil; - if (fullHeight != outputHeight) { - //--------------------------------------------------------- - // Create a new Command Buffer for scale transform: + // Apply a vertical flip transformation: //--------------------------------------------------------- - id scaleCommandBuffer = [commandQueue commandBuffer]; - scaleCommandBuffer.label = @"Gyroflow Toolbox Scale Command Buffer"; - [scaleCommandBuffer enqueue]; - + CGContextTranslateCTM(bitmapContext, 0, imageSize.height); + CGContextScaleCTM(bitmapContext, 1.0, -1.0); + //--------------------------------------------------------- - // Create a new texture for the scaled image: + // Draw the image into the flipped context: //--------------------------------------------------------- - MTLTextureDescriptor *scaleTextureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:inputTexture.pixelFormat - width:fullWidth - height:fullHeight - mipmapped:NO]; - - scaledInputTexture = [inputTexture.device newTextureWithDescriptor:scaleTextureDescriptor]; - + CGContextDrawImage(bitmapContext, CGRectMake(0, 0, imageSize.width, imageSize.height), cgImage); + //--------------------------------------------------------- - // Work out how much to scale/re-position: + // Create a flipped CGImage: //--------------------------------------------------------- - float scaleX = (float)(fullWidth / inputTexture.width); - float scaleY = (float)(fullHeight / inputTexture.height); - - if (scaleX > scaleY) { - scaleX = scaleY; - } else { - scaleY = scaleX; - } - - float translateX = (float)((fullWidth - inputTexture.width * scaleX) / 2); - float translateY = (float)((fullHeight - inputTexture.height * scaleY) / 2); - - MPSScaleTransform transform; - transform.scaleX = scaleX; // The horizontal scale factor. - transform.scaleY = scaleY; // The vertical scale factor. - transform.translateX = translateX; // The horizontal translation factor. - transform.translateY = translateY; // The vertical translation factor. - + CGImageRef flippedCGImage = CGBitmapContextCreateImage(bitmapContext); + //--------------------------------------------------------- - // A filter that resizes and changes the aspect ratio of - // an image: + // Release the context: //--------------------------------------------------------- - MPSImageBilinearScale *filter = [[[MPSImageBilinearScale alloc] initWithDevice:commandQueue.device] autorelease]; - [filter setScaleTransform:&transform]; - [filter encodeToCommandBuffer:scaleCommandBuffer sourceTexture:inputTexture destinationTexture:scaledInputTexture]; - + CGContextRelease(bitmapContext); + //--------------------------------------------------------- - // Commits the scale command buffer for execution: + // Use flippedCGImage instead of cgImage: //--------------------------------------------------------- - [scaleCommandBuffer commit]; + cgImage = flippedCGImage; + } + + // ------------------------------------------------------------ + // Create a CIImage from the CGImage and apply transformations: + // ------------------------------------------------------------ + CIImage *ciImage = [CIImage imageWithCGImage:cgImage]; + if (!ciImage) { + NSString *errorMessage = [NSString stringWithFormat:@"FATAL ERROR: Failed to create CIImage from CGImage: %@", errorMessageID]; + NSLog(@"[Gyroflow Toolbox Renderer] %@", errorMessage); + if (outError != NULL) { + *outError = [NSError errorWithDomain:FxPlugErrorDomain + code:kFxError_ImageCreationFailed + userInfo:@{ NSLocalizedDescriptionKey : errorMessage }]; + } + return NO; } //--------------------------------------------------------- - // Setup Render Pass: - //--------------------------------------------------------- - MTLRenderPassColorAttachmentDescriptor* colorAttachmentDescriptor = [[MTLRenderPassColorAttachmentDescriptor alloc] init]; - colorAttachmentDescriptor.texture = outputTexture; - colorAttachmentDescriptor.clearColor = MTLClearColorMake(1.0, 1.0, 1.0, 1.0); // White - colorAttachmentDescriptor.loadAction = MTLLoadActionClear; - MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; - renderPassDescriptor.colorAttachments [ 0 ] = colorAttachmentDescriptor; - id commandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; - - //--------------------------------------------------------- - // Calculate the vertex coordinates and the texture - // coordinates: - //--------------------------------------------------------- - float textureLeft = (destinationImage.tilePixelBounds.left - destinationImage.imagePixelBounds.left) / fullWidth; - float textureRight = (destinationImage.tilePixelBounds.right - destinationImage.imagePixelBounds.left) / fullWidth; - float textureBottom = (destinationImage.tilePixelBounds.bottom - destinationImage.imagePixelBounds.bottom) / fullHeight; - float textureTop = (destinationImage.tilePixelBounds.top - destinationImage.imagePixelBounds.bottom) / fullHeight; - - Vertex2D vertices[] = { - { { outputWidth / 2.0f, -outputHeight / 2.0f }, { textureRight, textureTop } }, - { { -outputWidth / 2.0f, -outputHeight / 2.0f }, { textureLeft, textureTop } }, - { { outputWidth / 2.0f, outputHeight / 2.0f }, { textureRight, textureBottom } }, - { { -outputWidth / 2.0f, outputHeight / 2.0f }, { textureLeft, textureBottom } } - }; - - //--------------------------------------------------------- - // Setup our viewport: - // - // MTLViewport: A 3D rectangular region for the viewport - // clipping. - //--------------------------------------------------------- - MTLViewport viewport = { - 0, differenceBetweenHeights, outputWidth, correctedHeight, -1.0, 1.0 - }; - - //--------------------------------------------------------- - // Sets the viewport used for transformations and clipping: - //--------------------------------------------------------- - [commandEncoder setViewport:viewport]; - - //--------------------------------------------------------- - // Setup our Render Pipeline State. - // - // MTLRenderPipelineState: An object that contains graphics - // functions and configuration state to use in a render - // command. - //--------------------------------------------------------- - id pipelineState = [deviceCache pipelineStateWithRegistryID:sourceImages[0].deviceRegistryID - pixelFormat:pixelFormat]; - - //--------------------------------------------------------- - // Sets the current render pipeline state object: - //--------------------------------------------------------- - [commandEncoder setRenderPipelineState:pipelineState]; - - //--------------------------------------------------------- - // Sets a block of data for the vertex shader: - //--------------------------------------------------------- - [commandEncoder setVertexBytes:vertices - length:sizeof(vertices) - atIndex:BVI_Vertices]; - - //--------------------------------------------------------- - // Set the viewport size: - //--------------------------------------------------------- - simd_uint2 viewportSize = { - (unsigned int)(outputWidth), - (unsigned int)(outputHeight) - }; - - //--------------------------------------------------------- - // Sets a block of data for the vertex shader: - //--------------------------------------------------------- - [commandEncoder setVertexBytes:&viewportSize - length:sizeof(viewportSize) - atIndex:BVI_ViewportSize]; - - //--------------------------------------------------------- - // Sets a texture for the fragment function at an index - // in the texture argument table: + // Get destination IOSurface for Metal: //--------------------------------------------------------- - if (scaledInputTexture != nil) { + IOSurfaceRef destinationIOSurface = (__bridge IOSurfaceRef)(destinationImage.ioSurface); + if (!destinationIOSurface) { + NSString *errorMessage = @"FATAL ERROR: Failed to get IOSurface from destination image."; + NSLog(@"[Gyroflow Toolbox Renderer] %@", errorMessage); + if (outError != NULL) { + *outError = [NSError errorWithDomain:FxPlugErrorDomain + code:kFxError_IOSurfaceCreationFailed + userInfo:@{ NSLocalizedDescriptionKey : errorMessage }]; + } + return NO; + } + + CGSize outputSize = CGSizeMake(IOSurfaceGetWidth(destinationIOSurface), IOSurfaceGetHeight(destinationIOSurface)); + + // ------------------------------------------------------------ + // Obtain the Metal device through the MetalDeviceCache: + // ------------------------------------------------------------ + MetalDeviceCache *deviceCache = [MetalDeviceCache deviceCache]; + id metalDevice = [deviceCache deviceWithRegistryID:destinationImage.deviceRegistryID]; + if (!metalDevice) { + NSString *errorMessage = @"FATAL ERROR: Failed to get Metal device from cache."; + NSLog(@"[Gyroflow Toolbox Renderer] %@", errorMessage); + if (outError != NULL) { + *outError = [NSError errorWithDomain:FxPlugErrorDomain + code:kFxError_MetalDeviceNotFound + userInfo:@{ NSLocalizedDescriptionKey : errorMessage }]; + } + return NO; + } + + // ------------------------------------------------------------ + // Create a Metal-based CIContext for rendering: + // ------------------------------------------------------------ + CIContext *ciContext = [CIContext contextWithMTLDevice:metalDevice]; + if (!ciContext) { + NSString *errorMessage = @"FATAL ERROR: Failed to create CIContext."; + NSLog(@"[Gyroflow Toolbox Renderer] %@", errorMessage); + if (outError != NULL) { + *outError = [NSError errorWithDomain:FxPlugErrorDomain + code:kFxError_CIContextCreationFailed + userInfo:@{ NSLocalizedDescriptionKey : errorMessage }]; + } + return NO; + } + + // ------------------------------------------------------------ + // Obtain the Metal command queue: + // ------------------------------------------------------------ + id commandQueue = [deviceCache commandQueueWithRegistryID:destinationImage.deviceRegistryID + pixelFormat:[MetalDeviceCache MTLPixelFormatForImageTile:destinationImage]]; + + if (!commandQueue) { + NSString *errorMessage = @"FATAL ERROR: Failed to obtain a Metal command queue."; + NSLog(@"[Gyroflow Toolbox Renderer] %@", errorMessage); + if (outError != NULL) { + *outError = [NSError errorWithDomain:FxPlugErrorDomain + code:kFxError_CommandQueueWasNilDuringShowErrorMessage + userInfo:@{ NSLocalizedDescriptionKey : errorMessage }]; + } + return NO; + } + + id commandBuffer = [commandQueue commandBuffer]; + if (!commandBuffer) { + //--------------------------------------------------------- - // Use our scaled input texture for non-square pixels: + // Return the command queue before aborting: //--------------------------------------------------------- - [commandEncoder setFragmentTexture:scaledInputTexture - atIndex:BTI_InputImage]; - } else { + [deviceCache returnCommandQueueToCache:commandQueue]; + + NSString *errorMessage = @"FATAL ERROR: Failed to create a Metal command buffer."; + NSLog(@"[Gyroflow Toolbox Renderer] %@", errorMessage); + if (outError != NULL) { + *outError = [NSError errorWithDomain:FxPlugErrorDomain + code:kFxError_CommandBufferCreationFailed + userInfo:@{ NSLocalizedDescriptionKey : errorMessage }]; + } + return NO; + } + + commandBuffer.label = @"Gyroflow Toolbox Error Command Buffer"; + [commandBuffer enqueue]; + + // ------------------------------------------------------------ + // Scale the CIImage to fit within the output size: + // ------------------------------------------------------------ + CGFloat aspectRatio = ciImage.extent.size.width / ciImage.extent.size.height; + CGFloat scaledWidth = outputSize.height * aspectRatio; + CGAffineTransform scaleTransform = CGAffineTransformMakeScale(scaledWidth / ciImage.extent.size.width, + outputSize.height / ciImage.extent.size.height); + CIImage *scaledCIImage = [ciImage imageByApplyingTransform:scaleTransform]; + + // ------------------------------------------------------------ + // Translate to centre it horizontally: + // ------------------------------------------------------------ + CGFloat translateX = (outputSize.width - scaledCIImage.extent.size.width) / 2; + CIImage *finalImage = [scaledCIImage imageByApplyingTransform:CGAffineTransformMakeTranslation(translateX, 0)]; + + // ------------------------------------------------------------ + // Create a render destination from IOSurface: + // ------------------------------------------------------------ + CIRenderDestination *renderDestination = [[CIRenderDestination alloc] initWithIOSurface:(__bridge IOSurface *)destinationIOSurface]; + renderDestination.alphaMode = CIRenderDestinationAlphaUnpremultiplied; + + // ------------------------------------------------------------ + // Start rendering task to the destination: + // ------------------------------------------------------------ + NSError *renderError = nil; + [ciContext startTaskToRender:finalImage toDestination:renderDestination error:&renderError]; + + if (renderError) { //--------------------------------------------------------- - // Use the data straight from the MTLBuffer for square - // pixels to avoid any extra processing: + // Return the command queue before aborting: //--------------------------------------------------------- - [commandEncoder setFragmentTexture:inputTexture - atIndex:BTI_InputImage]; + [deviceCache returnCommandQueueToCache:commandQueue]; + + NSString *errorMessage = [NSString stringWithFormat:@"ERROR: Failed to render error message image: %@", renderError.localizedDescription]; + NSLog(@"[Gyroflow Toolbox Renderer] %@", errorMessage); + if (outError != NULL) { + *outError = renderError; + } + return NO; } - - //--------------------------------------------------------- - // drawPrimitives: Encodes a command to render one instance - // of primitives using vertex data in contiguous array - // elements. - // - // MTLPrimitiveTypeTriangleStrip: For every three adjacent - // vertices, rasterize a triangle. - //--------------------------------------------------------- - [commandEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip - vertexStart:0 - vertexCount:4]; - - //--------------------------------------------------------- - // Declares that all command generation from the encoder - // is completed. After `endEncoding` is called, the - // command encoder has no further use. You cannot encode - // any other commands with this encoder. - //--------------------------------------------------------- - [commandEncoder endEncoding]; - - //--------------------------------------------------------- - // Commits the command buffer for execution. - // After you call the commit method, the MTLDevice schedules - // and executes the commands in the command buffer. If you - // haven’t already enqueued the command buffer with a call - // to enqueue, calling this function also enqueues the - // command buffer. The GPU executes the command buffer - // after any command buffers enqueued before it on the same - // command queue. - // - // You can only commit a command buffer once. You can’t - // commit a command buffer if the command buffer has an - // active command encoder. Once you commit a command buffer, - // you may not encode additional commands into it, nor can - // you add a schedule or completion handler. - //--------------------------------------------------------- + + // ------------------------------------------------------------ + // Commit the command buffer and wait for it to complete. + // ------------------------------------------------------------ [commandBuffer commit]; - - //--------------------------------------------------------- - // Blocks execution of the current thread until execution - // of the command buffer is completed. - //--------------------------------------------------------- - [commandBuffer waitUntilCompleted]; - - //--------------------------------------------------------- - // Release the `colorAttachmentDescriptor` we created - // earlier: - //--------------------------------------------------------- - [colorAttachmentDescriptor release]; - - //--------------------------------------------------------- - // Release the Input Texture: - //--------------------------------------------------------- - if (inputTexture != nil) { - [inputTexture setPurgeableState:MTLPurgeableStateEmpty]; - [inputTexture release]; - inputTexture = nil; - } - if (scaledInputTexture != nil) { - [scaledInputTexture setPurgeableState:MTLPurgeableStateEmpty]; - [scaledInputTexture release]; - scaledInputTexture = nil; - } - + [commandBuffer waitUntilScheduled]; + //--------------------------------------------------------- - // Return the Command Queue back to the cache: + // Return the command queue before aborting: //--------------------------------------------------------- [deviceCache returnCommandQueueToCache:commandQueue]; - + return YES; }