// Graphics layer abstraction. #ifndef ED_GFX_INCLUDED #define ED_GFX_INCLUDED #include #include #include #include #include #include #include #include "ed_array.h" bool keep_running = true; @interface EDGFXView : NSView @end @interface AppDelegate : NSObject @end @interface WindowDelegate : NSObject @end #define wrapIdArray(T) typedef id _ ## T;\ arrayTemplate(_ ## T); wrapIdArray(MTLRenderPipelineState); wrapIdArray(MTLBuffer); wrapIdArray(MTLTexture); #if defined(__APPLE__) typedef struct { NSApplication *application; NSWindow *window; EDGFXView *view; bool keep_running; // Metal objects id device; CAMetalLayer *metal_layer; id library; id command_queue; array(_MTLRenderPipelineState) pipelines; array(_MTLBuffer) buffers; array(_MTLTexture) textures; } _metal_gfx_context; #endif typedef void (*_gfx_frame_func)(); typedef struct { #if defined(__APPLE__) _metal_gfx_context backend; #else #error "Unsupported platform" #endif uint32_t frame_width; uint32_t frame_height; _gfx_frame_func frame_func; } gfx_context_t; static gfx_context_t _gfx_context; #ifdef ED_GFX_IMPLEMENTATION #if defined(__APPLE__) static void _metal_gfx_present(_metal_gfx_context *cx); static void _metal_gfx_send_events(_metal_gfx_context *cx); @implementation AppDelegate - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { keep_running = false; return NSTerminateCancel; } - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { keep_running = false; return NO; } @end @implementation WindowDelegate - (BOOL)windowShouldClose:(NSApplication *)sender { keep_running = false; return YES; } - (void)windowDidResize:(NSNotification *)notification { NSLog(@"did resize\n"); _gfx_context.frame_width = _gfx_context.backend.window.contentView.frame.size.width; _gfx_context.frame_height = _gfx_context.backend.window.contentView.frame.size.height; CGFloat scale = _gfx_context.backend.metal_layer.contentsScale; [_gfx_context.backend.metal_layer setDrawableSize:CGSizeMake(_gfx_context.frame_width * scale, _gfx_context.frame_height * scale)]; _metal_gfx_present(&_gfx_context.backend); } @end @implementation EDGFXView - (BOOL)isOpaque { return YES; } - (void)updateLayer { _metal_gfx_present(&_gfx_context.backend); } - (BOOL)wantsLayer { return YES; } - (BOOL)wantsUpdateLayer { return YES; } - (NSViewLayerContentsRedrawPolicy)layerContentsRedrawPolicy { return NSViewLayerContentsRedrawOnSetNeedsDisplay; } @end static _metal_gfx_context _metal_gfx_init_context(uint32_t width, uint32_t height) { NSApplication *application = [NSApplication sharedApplication]; if (application == NULL) { fprintf(stderr, "NSApplication:sharedApplication failed\n"); exit(1); } NSString *title = @"editor - [C is Illegal]"; NSRect rect = NSMakeRect(0, 0, width, height); NSWindow *window = [[NSWindow alloc] initWithContentRect:rect styleMask:NSWindowStyleMaskTitled|NSWindowStyleMaskClosable|NSWindowStyleMaskMiniaturizable|NSWindowStyleMaskResizable backing:NSBackingStoreBuffered defer:NO]; EDGFXView *view = [[EDGFXView alloc] initWithFrame:rect]; [view updateTrackingAreas]; [window setTitle:title]; [window setContentView:view]; [window setDelegate:[[WindowDelegate alloc] init]]; [window makeKeyAndOrderFront:NULL]; // TODO: make this work // if (application.mainMenu == NULL) { // NSMenu *menu = [[NSMenu alloc] initWithTitle:@"an_editor"]; // if (menu == NULL) { // fprintf(stderr, "failed to create application menu\n"); // exit(1); // } // application.mainMenu = menu; // } [application setDelegate:[[AppDelegate alloc] init]]; [application setActivationPolicy:NSApplicationActivationPolicyRegular]; [application setPresentationOptions:NSApplicationPresentationDefault]; [application finishLaunching]; id device = MTLCreateSystemDefaultDevice(); CAMetalLayer *metal_layer = [CAMetalLayer layer]; metal_layer.device = device; metal_layer.pixelFormat = MTLPixelFormatRGBA8Unorm; metal_layer.frame = CGRectMake(0, 0, width, height); metal_layer.needsDisplayOnBoundsChange = YES; metal_layer.presentsWithTransaction = YES; metal_layer.autoresizingMask = kCALayerWidthSizable|kCALayerHeightSizable; // TODO: set this to the display dpi scale metal_layer.contentsScale = 2.0; view.wantsLayer = YES; [view.layer addSublayer:metal_layer]; view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay; NSError *libraryError = NULL; NSURL *libraryURL = [[NSBundle mainBundle] URLForResource:@"./shaders" withExtension:@"metallib"]; if (libraryURL == NULL) { fprintf(stderr, "Couldn't find library file\n"); exit(1); } id library = [device newLibraryWithURL:libraryURL error:&libraryError]; if (library == NULL) { if (libraryError.description != NULL) { NSLog(@"Error description: %@\n", libraryError.description); } exit(1); } id command_queue = [device newCommandQueue]; id vertex_func = [library newFunctionWithName:@"vs_main"]; id fragment_func = [library newFunctionWithName:@"fs_main"]; id ui_rect_vertex_func = [library newFunctionWithName:@"ui_rect_vs"]; id ui_rect_fragment_func = [library newFunctionWithName:@"ui_rect_fs"]; MTLRenderPipelineDescriptor *pipeline_descriptor = [[MTLRenderPipelineDescriptor alloc] init]; [pipeline_descriptor setVertexFunction:vertex_func]; [pipeline_descriptor setFragmentFunction:fragment_func]; pipeline_descriptor.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm; pipeline_descriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; pipeline_descriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorSourceAlpha; pipeline_descriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; pipeline_descriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha; pipeline_descriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; pipeline_descriptor.colorAttachments[0].blendingEnabled = true; MTLRenderPipelineDescriptor *ui_rect_pipeline_descriptor = [[MTLRenderPipelineDescriptor alloc] init]; [ui_rect_pipeline_descriptor setVertexFunction:ui_rect_vertex_func]; [ui_rect_pipeline_descriptor setFragmentFunction:ui_rect_fragment_func]; ui_rect_pipeline_descriptor.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm; ui_rect_pipeline_descriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; ui_rect_pipeline_descriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorSourceAlpha; ui_rect_pipeline_descriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; ui_rect_pipeline_descriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha; ui_rect_pipeline_descriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; ui_rect_pipeline_descriptor.colorAttachments[0].blendingEnabled = true; NSError *pipeline_error = NULL; array(_MTLRenderPipelineState) pipelines = newArray(_MTLRenderPipelineState, 2); pushArray(_MTLRenderPipelineState, &pipelines, [device newRenderPipelineStateWithDescriptor:pipeline_descriptor error:&pipeline_error]); if (pipeline_error != NULL) { if (pipeline_error.description != NULL) { NSLog(@"Error description: %@\n", pipeline_error.description); } exit(1); } pushArray(_MTLRenderPipelineState, &pipelines, [device newRenderPipelineStateWithDescriptor:ui_rect_pipeline_descriptor error:&pipeline_error]); array(_MTLBuffer) buffers = newArray(_MTLBuffer, 8); array(_MTLTexture) textures = newArray(_MTLTexture, 8); if (pipeline_error != NULL) { if (pipeline_error.description != NULL) { NSLog(@"Error description: %@\n", pipeline_error.description); } exit(1); } return (_metal_gfx_context) { .application = application, .window = window, .view = view, .keep_running = true, .device = device, .metal_layer = metal_layer, .library = library, .command_queue = command_queue, .pipelines = pipelines, .buffers = buffers, .textures = textures, }; } void _metal_gfx_send_events(_metal_gfx_context *cx) { NSEvent *event = [cx->application nextEventMatchingMask:NSEventMaskAny untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES]; [cx->application sendEvent:event]; [cx->application updateWindows]; } void _metal_gfx_present(_metal_gfx_context *cx) { _gfx_context.frame_func(); id drawable = [cx->metal_layer nextDrawable]; id command_buffer = [cx->command_queue commandBuffer]; MTLRenderPassDescriptor *render_pass_desc = [MTLRenderPassDescriptor renderPassDescriptor]; render_pass_desc.colorAttachments[0].texture = drawable.texture; render_pass_desc.colorAttachments[0].loadAction = MTLLoadActionClear; render_pass_desc.colorAttachments[0].clearColor = MTLClearColorMake(0,0,0,1); render_pass_desc.colorAttachments[0].storeAction = MTLStoreActionStore; id encoder = [command_buffer renderCommandEncoderWithDescriptor:render_pass_desc]; // UI Text [encoder setRenderPipelineState:cx->pipelines.data[0]]; // FIXME: allow these to be described by the user instead of hardcoded [encoder setVertexBuffer:cx->buffers.data[0] offset:0 atIndex:0]; // vertices [encoder setVertexBuffer:cx->buffers.data[2] offset:0 atIndex:1]; // glyph data [encoder setVertexBuffer:cx->buffers.data[3] offset:0 atIndex:2]; // uniforms [encoder setFragmentTexture:cx->textures.data[0] atIndex:0]; // TODO: get instance count properly [encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle indexCount:6 indexType:MTLIndexTypeUInt16 indexBuffer:cx->buffers.data[1] indexBufferOffset:0 instanceCount:36]; // UI Rects [encoder setRenderPipelineState:cx->pipelines.data[1]]; // FIXME: allow these to be described by the user instead of hardcoded [encoder setVertexBuffer:cx->buffers.data[0] offset:0 atIndex:0]; // vertices [encoder setVertexBuffer:cx->buffers.data[4] offset:0 atIndex:1]; // ui rects [encoder setVertexBuffer:cx->buffers.data[3] offset:0 atIndex:2]; // uniforms // TODO: get instance count properly [encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle indexCount:6 indexType:MTLIndexTypeUInt16 indexBuffer:cx->buffers.data[1] indexBufferOffset:0 instanceCount:3]; [encoder endEncoding]; [command_buffer presentDrawable:drawable]; [command_buffer commit]; [command_buffer waitUntilScheduled]; } size_t _metal_gfx_push_texture_buffer(_metal_gfx_context *cx, uint32_t width, uint32_t height, const void *data, size_t len) { MTLTextureDescriptor *texture_desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatR8Unorm width:width height:height mipmapped:false]; _MTLTexture texture = [cx->device newTextureWithDescriptor:texture_desc]; MTLRegion region = MTLRegionMake2D(0, 0, width, height); [texture replaceRegion:region mipmapLevel:0 slice:0 withBytes:data bytesPerRow:width*sizeof(uint8_t) bytesPerImage:len]; pushArray(_MTLTexture, &cx->textures, texture); return cx->textures.size-1; } void _metal_gfx_resize_texture_buffer(_metal_gfx_context *cx, uint32_t width, size_t texture_index, uint32_t height) { [cx->textures.data[texture_index] setPurgeableState:MTLPurgeableStateEmpty]; [cx->textures.data[texture_index] release]; MTLTextureDescriptor *texture_desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatR8Unorm width:width height:height mipmapped:false]; cx->textures.data[texture_index] = [cx->device newTextureWithDescriptor:texture_desc]; } size_t _metal_gfx_push_vertex_buffer(_metal_gfx_context *cx, const void *data, size_t len) { pushArray(_MTLBuffer, &cx->buffers, [cx->device newBufferWithBytes:data length:len options:MTLResourceStorageModeShared] ); return cx->buffers.size-1; } size_t _metal_gfx_allocate_vertex_buffer(_metal_gfx_context *cx, size_t len) { pushArray(_MTLBuffer, &cx->buffers, [cx->device newBufferWithLength:len options:MTLResourceStorageModeShared] ); return cx->buffers.size-1; } void _metal_gfx_update_buffer(_metal_gfx_context *cx, size_t buffer_index, const void *data, size_t len) { void *buffer_contents = [cx->buffers.data[buffer_index] contents]; // FIXME: actually check to see if this will fit in the buffer memcpy(buffer_contents, data, len); } #endif void * gfx_init_context(_gfx_frame_func frame_func, uint32_t width, uint32_t height) { __auto_type backend = #if defined(__APPLE__) _metal_gfx_init_context(width, height); #else #error "Unsupported graphics backend" #endif _gfx_context.backend = backend; _gfx_context.frame_func = frame_func; _gfx_context.frame_width = width; _gfx_context.frame_height = height; return &_gfx_context; } void gfx_run_events(gfx_context_t *cx) { #if defined(__APPLE__) return _metal_gfx_send_events(&cx->backend); #else #error "Unsupported graphics backend" #endif } size_t gfx_push_texture_buffer(gfx_context_t *cx, uint32_t width, uint32_t height, const void *data, size_t len) { #if defined(__APPLE__) return _metal_gfx_push_texture_buffer(&cx->backend, width, height, data, len); #else #error "Unsupported graphics backend" #endif } size_t gfx_push_vertex_buffer(gfx_context_t *cx, const void *data, size_t len) { #if defined(__APPLE__) return _metal_gfx_push_vertex_buffer(&cx->backend, data, len); #else #error "Unsupported graphics backend" #endif } size_t gfx_allocate_vertex_buffer(gfx_context_t *cx, size_t len) { #if defined(__APPLE__) return _metal_gfx_allocate_vertex_buffer(&cx->backend, len); #else #error "Unsupported graphics backend" #endif } // FIXME: abstract different backends void gfx_update_buffer(gfx_context_t *cx, size_t buffer_index, const void *data, size_t len) { #if defined(__APPLE__) return _metal_gfx_update_buffer(&cx->backend, buffer_index, data, len); #else #error "Unsupported graphics backend" #endif } #endif #endif