From d6a52459eff31bde1d66836dc6a5f20ec7b0c364 Mon Sep 17 00:00:00 2001 From: Darky-Lucera Date: Sun, 3 Mar 2019 23:34:38 +0100 Subject: [PATCH] added metal support for macosx version (based on rust_minifb) (#17) --- src/macosx/MacMiniFB.m | 201 +++++++++++++++++++++++++++++--- src/macosx/OSXWindow.h | 5 + src/macosx/OSXWindow.m | 2 + src/macosx/OSXWindowFrameView.h | 27 +++++ src/macosx/OSXWindowFrameView.m | 103 ++++++++++++++++ 5 files changed, 324 insertions(+), 14 deletions(-) diff --git a/src/macosx/MacMiniFB.m b/src/macosx/MacMiniFB.m index 076d2bc..4947d08 100644 --- a/src/macosx/MacMiniFB.m +++ b/src/macosx/MacMiniFB.m @@ -1,15 +1,121 @@ - #include "OSXWindow.h" +#include "OSXWindowFrameView.h" #include +#if defined(USE_METAL_API) +#include +#include +#endif #include #include "MiniFB.h" +#if defined(USE_METAL_API) +extern id g_metal_device; +extern id g_command_queue; +extern id g_library; +extern id g_pipeline_state; + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +NSString* g_shadersSrc = @ +" #include \n" + "using namespace metal;\n" + + "struct VertexOutput {\n" + "float4 pos [[position]];\n" + "float2 texcoord;\n" + "};\n" + + "vertex VertexOutput vertFunc(\n" + "unsigned int vID[[vertex_id]])\n" + "{\n" + "VertexOutput out;\n" + + "out.pos.x = (float)(vID / 2) * 4.0 - 1.0;\n" + "out.pos.y = (float)(vID % 2) * 4.0 - 1.0;\n" + "out.pos.z = 0.0;\n" + "out.pos.w = 1.0;\n" + + "out.texcoord.x = (float)(vID / 2) * 2.0;\n" + "out.texcoord.y = 1.0 - (float)(vID % 2) * 2.0;\n" + + "return out;\n" + "}\n" + + "fragment float4 fragFunc(VertexOutput input [[stage_in]],\n" + "texture2d colorTexture [[ texture(0) ]])\n" + "{\n" + "constexpr sampler textureSampler(mag_filter::nearest, min_filter::nearest);\n" + + // Sample the texture to obtain a color + "const half4 colorSample = colorTexture.sample(textureSampler, input.texcoord);\n" + + // We return the color of the texture + "return float4(colorSample);\n" + //"return half4(input.texcoord.x, input.texcoord.y, 0.0, 1.0);\n" + "}\n"; + +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if !defined(USE_METAL_API) void* g_updateBuffer = 0; int g_width = 0; int g_height = 0; -static OSXWindow* window_; +#endif +static OSXWindow *s_window; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if defined(USE_METAL_API) +static bool create_shaders() { + // Error + NSError* nsError = NULL; + NSError** nsErrorPtr = &nsError; + + id library = [g_metal_device newLibraryWithSource:g_shadersSrc + options:[[MTLCompileOptions alloc] init] + error:nsErrorPtr]; + + // Error update + if (nsError || !library) { + NSLog(@"Unable to create shaders %@", nsError); + return false; + } + + g_library = library; + NSLog(@"Names %@", [g_library functionNames]); + + id vertex_shader_func = [g_library newFunctionWithName:@"vertFunc"]; + id fragment_shader_func = [g_library newFunctionWithName:@"fragFunc"]; + + if (!vertex_shader_func) { + printf("Unable to get vertFunc!\n"); + return false; + } + + if (!fragment_shader_func) { + printf("Unable to get fragFunc!\n"); + return false; + } + + // Create a reusable pipeline state + MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipelineStateDescriptor.label = @"MyPipeline"; + pipelineStateDescriptor.vertexFunction = vertex_shader_func; + pipelineStateDescriptor.fragmentFunction = fragment_shader_func; + pipelineStateDescriptor.colorAttachments[0].pixelFormat = 80; //bgra8Unorm; + + NSError *error = NULL; + g_pipeline_state = [g_metal_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error]; + if (!g_pipeline_state) + { + NSLog(@"Failed to created pipeline state, error %@", error); + } + + return true; +} +#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -17,28 +123,91 @@ int mfb_open(const char* name, int width, int height) { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; +#if !defined(USE_METAL_API) g_width = width; g_height = height; - +#endif [NSApplication sharedApplication]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; +#if defined(USE_METAL_API) + g_metal_device = MTLCreateSystemDefaultDevice(); + + if (!g_metal_device) { + printf("Your device/OS doesn't support Metal."); + return -1; + } + + if (!create_shaders()) { + return -2; + } +#endif + NSWindowStyleMask styles = NSWindowStyleMaskResizable | NSWindowStyleMaskClosable | NSWindowStyleMaskTitled; NSRect rectangle = NSMakeRect(0, 0, width, height); - window_ = [[OSXWindow alloc] initWithContentRect:rectangle styleMask:styles backing:NSBackingStoreBuffered defer:NO]; + s_window = [[OSXWindow alloc] initWithContentRect:rectangle styleMask:styles backing:NSBackingStoreBuffered defer:NO]; - if (!window_) - return 0; + if (!s_window) + return -3; - [window_ setTitle:[NSString stringWithUTF8String:name]]; - [window_ setReleasedWhenClosed:NO]; - [window_ performSelectorOnMainThread:@selector(makeKeyAndOrderFront:) withObject:nil waitUntilDone:YES]; +#if defined(USE_METAL_API) + s_window->draw_buffer = malloc(width * height * 4); - [window_ center]; + if (!s_window->draw_buffer) + return -4; + + // Setup command queue + g_command_queue = [g_metal_device newCommandQueue]; + + WindowViewController* viewController = [WindowViewController new]; + + MTLTextureDescriptor* textureDescriptor = [[MTLTextureDescriptor alloc] init]; + + // Indicate that each pixel has a blue, green, red, and alpha channel, where each channel is + // an 8-bit unsigned normalized value (i.e. 0 maps to 0.0 and 255 maps to 1.0) + textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm; + + // Set the pixel dimensions of the texture + textureDescriptor.width = width; + textureDescriptor.height = height; + + // Create the texture from the device by using the descriptor + + for (int i = 0; i < MaxBuffersInFlight; ++i) { + viewController->m_texture_buffers[i] = [g_metal_device newTextureWithDescriptor:textureDescriptor]; + } + + // Used for syncing the CPU and GPU + viewController->m_semaphore = dispatch_semaphore_create(MaxBuffersInFlight); + viewController->m_draw_buffer = s_window->draw_buffer; + viewController->m_width = width; + viewController->m_height = height; + + MTKView* view = [[MTKView alloc] initWithFrame:rectangle]; + view.device = g_metal_device; + view.delegate = viewController; + view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + [s_window.contentView addSubview:view]; + + s_window->width = width; + s_window->height = height; + + //[s_window updateSize]; +#endif + + [s_window setTitle:[NSString stringWithUTF8String:name]]; + [s_window setReleasedWhenClosed:NO]; + [s_window performSelectorOnMainThread:@selector(makeKeyAndOrderFront:) withObject:nil waitUntilDone:YES]; + + [s_window center]; [NSApp activateIgnoringOtherApps:YES]; +#if defined(USE_METAL_API) + [NSApp finishLaunching]; +#endif + [pool drain]; return 1; @@ -50,8 +219,8 @@ void mfb_close() { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - if (window_) - [window_ close]; + if (s_window) + [s_window close]; [pool drain]; } @@ -83,7 +252,7 @@ static int updateEvents() } [pool release]; - if (window_->closed) + if (s_window->closed) state = -1; return state; @@ -93,8 +262,12 @@ static int updateEvents() int mfb_update(void* buffer) { +#if defined(USE_METAL_API) + memcpy(s_window->draw_buffer, buffer, s_window->width * s_window->height * 4); +#else g_updateBuffer = buffer; +#endif int state = updateEvents(); - [[window_ contentView] setNeedsDisplay:YES]; + [[s_window contentView] setNeedsDisplay:YES]; return state; } diff --git a/src/macosx/OSXWindow.h b/src/macosx/OSXWindow.h index 05eb51e..28bf33f 100644 --- a/src/macosx/OSXWindow.h +++ b/src/macosx/OSXWindow.h @@ -6,6 +6,11 @@ { NSView* childContentView; @public bool closed; +#if defined(USE_METAL_API) + @public int width; + @public int height; + @public void* draw_buffer; +#endif } @end diff --git a/src/macosx/OSXWindow.m b/src/macosx/OSXWindow.m index e1107d1..cd3d63c 100644 --- a/src/macosx/OSXWindow.m +++ b/src/macosx/OSXWindow.m @@ -5,6 +5,7 @@ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#if !defined(USE_METAL_API) - (id)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)windowStyle backing:(NSBackingStoreType)bufferingType @@ -42,6 +43,7 @@ } return self; } +#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/macosx/OSXWindowFrameView.h b/src/macosx/OSXWindowFrameView.h index 96408e9..92bb70f 100644 --- a/src/macosx/OSXWindowFrameView.h +++ b/src/macosx/OSXWindowFrameView.h @@ -1,7 +1,34 @@ #import +#if defined(USE_METAL_API) +#import + +// Number of textures in flight (tripple buffered) +static const int MaxBuffersInFlight = 3; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface WindowViewController : NSViewController +{ + @public id m_texture_buffers[MaxBuffersInFlight]; + @public int m_current_buffer; + @public void* m_draw_buffer; + @public int m_width; + @public int m_height; + // Used for syncing with CPU/GPU + @public dispatch_semaphore_t m_semaphore; +} + +@end +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + @interface OSXWindowFrameView : NSView { +#if defined(USE_METAL_API) + @private NSTrackingArea* trackingArea; +#endif } @end diff --git a/src/macosx/OSXWindowFrameView.m b/src/macosx/OSXWindowFrameView.m index 0961d41..2ff218e 100644 --- a/src/macosx/OSXWindowFrameView.m +++ b/src/macosx/OSXWindowFrameView.m @@ -1,7 +1,109 @@ #import "OSXWindowFrameView.h" +#if defined(USE_METAL_API) +#import + +id g_metal_device; +id g_command_queue; +id g_library; +id g_pipeline_state; + +@implementation WindowViewController + +-(void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size +{ + (void)view; + (void)size; + // resize +} + +-(void)drawInMTKView:(nonnull MTKView *)view +{ + // Wait to ensure only MaxBuffersInFlight number of frames are getting proccessed + // by any stage in the Metal pipeline (App, Metal, Drivers, GPU, etc) + dispatch_semaphore_wait(m_semaphore, DISPATCH_TIME_FOREVER); + + // Iterate through our Metal buffers, and cycle back to the first when we've written to MaxBuffersInFlight + m_current_buffer = (m_current_buffer + 1) % MaxBuffersInFlight; + + // Calculate the number of bytes per row of our image. + NSUInteger bytesPerRow = 4 * m_width; + MTLRegion region = { { 0, 0, 0 }, { m_width, m_height, 1 } }; + + // Copy the bytes from our data object into the texture + [m_texture_buffers[m_current_buffer] replaceRegion:region + mipmapLevel:0 withBytes:m_draw_buffer bytesPerRow:bytesPerRow]; + + // Create a new command buffer for each render pass to the current drawable + id commandBuffer = [g_command_queue commandBuffer]; + commandBuffer.label = @"minifb_command_buffer"; + + // Add completion hander which signals _inFlightSemaphore when Metal and the GPU has fully + // finished processing the commands we're encoding this frame. This indicates when the + // dynamic buffers filled with our vertices, that we're writing to this frame, will no longer + // be needed by Metal and the GPU, meaning we can overwrite the buffer contents without + // corrupting the rendering. + __block dispatch_semaphore_t block_sema = m_semaphore; + [commandBuffer addCompletedHandler:^(id buffer) + { + (void)buffer; + dispatch_semaphore_signal(block_sema); + }]; + + MTLRenderPassDescriptor* renderPassDescriptor = view.currentRenderPassDescriptor; + + if (renderPassDescriptor != nil) + { + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 0.0, 0.0, 1.0); + + // Create a render command encoder so we can render into something + id renderEncoder = + [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; + renderEncoder.label = @"minifb_command_encoder"; + + // Set render command encoder state + [renderEncoder setRenderPipelineState:g_pipeline_state]; + + [renderEncoder setFragmentTexture:m_texture_buffers[m_current_buffer] atIndex:0]; + + // Draw the vertices of our quads + [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle + vertexStart:0 + vertexCount:3]; + + // We're done encoding commands + [renderEncoder endEncoding]; + + // Schedule a present once the framebuffer is complete using the current drawable + [commandBuffer presentDrawable:view.currentDrawable]; + } + + // Finalize rendering here & push the command buffer to the GPU + [commandBuffer commit]; +} +@end +#endif + @implementation OSXWindowFrameView +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if defined(USE_METAL_API) +-(void)updateTrackingAreas +{ + if(trackingArea != nil) { + [self removeTrackingArea:trackingArea]; + [trackingArea release]; + } + + int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways); + trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds] + options:opts + owner:self + userInfo:nil]; + [self addTrackingArea:trackingArea]; +} +#else extern void* g_updateBuffer; extern int g_width; extern int g_height; @@ -47,6 +149,7 @@ extern int g_height; CGImageRelease(img); } +#endif @end