// Graphics layer abstraction. #ifndef ED_GFX_INCLUDED #define ED_GFX_INCLUDED #include #include "ed_array.h" #include "string.h" bool keep_running = true; #if defined(__APPLE__) #include #include #include #include #include #include @interface EDGFXView : NSView @property NSTrackingArea *tracking_area; @end @interface AppDelegate : NSObject @end @interface WindowDelegate : NSObject @end #define wrapIdArray(T) \ typedef id _##T; \ arrayTemplate(_##T); wrapIdArray(MTLRenderPipelineState); wrapIdArray(MTLBuffer); wrapIdArray(MTLTexture); #endif typedef struct { float position[4]; float size[4]; float border_size[4]; float color[4]; } GpuUiRect; arrayTemplate(GpuUiRect); typedef struct { float atlas_position[2]; float size[2]; float position[2]; float y_offset; float _haha_alignment; float color[4]; } GpuGlyph; arrayTemplate(GpuGlyph); typedef struct { float screen_size[2]; float font_size[2]; } GpuUniformParams; #if defined(__APPLE__) typedef struct { NSApplication *application; NSWindow *window; EDGFXView *view; bool keep_running; bool refresh_now; int mouse_x, mouse_y; bool mouse_left_down, mouse_right_down; // Metal objects id device; CAMetalLayer *metal_layer; id library; id command_queue; array(_MTLRenderPipelineState) pipelines; array(_MTLBuffer) buffers; array(_MTLTexture) textures; } _metal_gfx_context; #elif __linux__ #include "wayland-crap/xdg-shell.h" #include #include #include #include #include #include #include #include #include "file_io.h" // And I thought MacOS needed a lot of state to create a window arrayTemplate(GLuint); typedef struct { struct wl_display *display; struct wl_registry *registry; struct wl_surface *surface; struct wl_compositor *compositor; struct wl_shm *shared_memory; struct wl_shm_pool *shared_memory_pool; struct xdg_wm_base *wm_base; struct wl_buffer *buffer; struct xdg_surface *xdg_surface; struct xdg_toplevel *xdg_toplevel; struct wl_seat *seat; struct wl_pointer *pointer; uint8_t *pixels; EGLDisplay egl_display; EGLConfig egl_config; EGLSurface egl_surface; EGLContext egl_context; struct wl_egl_window *egl_window; int mouse_x, mouse_y; int mouse_left_down, mouse_right_down; GLuint ui_rect_vertex_shader; GLuint ui_rect_fragment_shader; GLuint text_atlas_vertex_shader; GLuint text_atlas_fragment_shader; GLuint ui_rect_shader_program; GLuint text_atlas_shader_program; array(GLuint) buffers; array(GLuint) textures; } _opengl_gfx_context_wayland; static void _opengl_gfx_present_wayland(_opengl_gfx_context_wayland *cx); typedef struct { Display *display; Window window; int screen; } _opengl_gfx_context_x11; #endif typedef void (*_gfx_frame_func)(int mouse_x, int mouse_y, bool mouse_left_down, bool mouse_right_down); typedef struct { #if defined(__APPLE__) _metal_gfx_context backend; #elif __linux__ // TODO: be able to use X11 or Wayland at runtime _opengl_gfx_context_wayland backend; #else #error "Unsupported platform" #endif uint32_t frame_width; uint32_t frame_height; _gfx_frame_func frame_func; array(GpuUiRect) gpu_ui_rects; array(GpuGlyph) gpu_glyphs; array(GpuGlyph) glyph_cache; } 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); void gfx_update_buffer(gfx_context_t *cx, size_t buffer_index, const void *data, size_t len); @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 { _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)]; _gfx_context.backend.refresh_now = true; _gfx_context.backend.view.needsDisplay = true; } @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; } - (id)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; NSRect rect = NSMakeRect(0, 0, _gfx_context.frame_width = _gfx_context.backend.window.contentView.frame.size.width, _gfx_context.frame_height = _gfx_context.backend.window.contentView.frame.size.height); self.tracking_area = [[NSTrackingArea alloc] initWithRect:rect options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow) owner:self userInfo:nil]; [self addTrackingArea:self.tracking_area]; return self; } - (void)mouseDown:(NSEvent *)event { if ((unsigned long)event.type == NSEventTypeLeftMouseDown) { _gfx_context.backend.mouse_left_down = true; } if (event.type == NSEventTypeRightMouseUp) { _gfx_context.backend.mouse_right_down = true; } _gfx_context.gpu_glyphs.size = 0; _gfx_context.gpu_ui_rects.size = 0; _gfx_context.frame_func(_gfx_context.backend.mouse_x, _gfx_context.frame_height - _gfx_context.backend.mouse_y, _gfx_context.backend.mouse_left_down, _gfx_context.backend.mouse_right_down); [self setNeedsDisplay:YES]; } - (void)mouseUp:(NSEvent *)event { if ((unsigned long)event.type == NSEventTypeLeftMouseUp) { _gfx_context.backend.mouse_left_down = false; } if (event.type == NSEventTypeRightMouseDown) { _gfx_context.backend.mouse_right_down = false; } _gfx_context.gpu_glyphs.size = 0; _gfx_context.gpu_ui_rects.size = 0; _gfx_context.frame_func(_gfx_context.backend.mouse_x, _gfx_context.frame_height - _gfx_context.backend.mouse_y, _gfx_context.backend.mouse_left_down, _gfx_context.backend.mouse_right_down); [self setNeedsDisplay:YES]; } - (void)mouseDragged:(NSEvent *)event { NSPoint location = [self convertPoint:[event locationInWindow] fromView:nil]; _gfx_context.backend.mouse_x = location.x; _gfx_context.backend.mouse_y = location.y; _gfx_context.gpu_glyphs.size = 0; _gfx_context.gpu_ui_rects.size = 0; _gfx_context.frame_func(_gfx_context.backend.mouse_x, _gfx_context.frame_height - _gfx_context.backend.mouse_y, _gfx_context.backend.mouse_left_down, _gfx_context.backend.mouse_right_down); [self setNeedsDisplay:YES]; } - (void)mouseMoved:(NSEvent *)event { NSPoint location = [self convertPoint:[event locationInWindow] fromView:nil]; _gfx_context.backend.mouse_x = location.x; _gfx_context.backend.mouse_y = location.y; [self setNeedsDisplay:YES]; [self displayIfNeeded]; [_gfx_context.backend.metal_layer setNeedsDisplay]; [_gfx_context.backend.metal_layer displayIfNeeded]; } - (void)updateTrackingAreas { [self removeTrackingArea:self.tracking_area]; [self.tracking_area release]; NSRect rect = NSMakeRect(0, 0, _gfx_context.frame_width = _gfx_context.backend.window.contentView.frame.size.width, _gfx_context.frame_height = _gfx_context.backend.window.contentView.frame.size.height); self.tracking_area = [[NSTrackingArea alloc] initWithRect:rect options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow) owner:self userInfo:nil]; [self addTrackingArea:self.tracking_area]; } @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 = @"chat - [Slack Sux]"; 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, }; } static 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]; } static void _metal_gfx_present(_metal_gfx_context *cx) { _gfx_context.gpu_glyphs.size = 0; _gfx_context.gpu_ui_rects.size = 0; _gfx_context.frame_func(cx->mouse_x, _gfx_context.frame_height - cx->mouse_y, cx->mouse_left_down, cx->mouse_right_down); if (_gfx_context.gpu_glyphs.size > 0) { gfx_update_buffer(&_gfx_context, 2, _gfx_context.gpu_glyphs.data, _gfx_context.gpu_glyphs.size * sizeof(GpuGlyph)); } if (_gfx_context.gpu_ui_rects.size > 0) { gfx_update_buffer(&_gfx_context, 4, _gfx_context.gpu_ui_rects.data, _gfx_context.gpu_ui_rects.size * sizeof(GpuUiRect)); } GpuUniformParams gpu_uniform_params = { .screen_size = { (float)_gfx_context.frame_width, (float)_gfx_context.frame_height, }, .font_size = { (float)_FONT_WIDTH, (float)_FONT_HEIGHT, }, }; gfx_update_buffer(&_gfx_context, 3, &gpu_uniform_params, sizeof(GpuUniformParams)); @autoreleasepool { 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.1, 0.1, 0.1, 1); render_pass_desc.colorAttachments[0].storeAction = MTLStoreActionStore; id encoder = [command_buffer renderCommandEncoderWithDescriptor:render_pass_desc]; if (_gfx_context.gpu_ui_rects.size > 0) { // 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 [encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle indexCount:6 indexType:MTLIndexTypeUInt16 indexBuffer:cx->buffers.data[1] indexBufferOffset:0 instanceCount:_gfx_context.gpu_ui_rects.size]; } if (_gfx_context.gpu_glyphs.size > 0) { // 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]; [encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle indexCount:6 indexType:MTLIndexTypeUInt16 indexBuffer:cx->buffers.data[1] indexBufferOffset:0 instanceCount:_gfx_context.gpu_glyphs.size]; } [encoder endEncoding]; // FIXME: `afterMinimumDuration` causes the weird re-size scaling, but I // need to figure why the heck the NSView doesn't get the rendered // contents unless `afterMinimumDuration` is here if (cx->refresh_now) { [command_buffer presentDrawable:drawable]; cx->refresh_now = false; } else { [command_buffer presentDrawable:drawable afterMinimumDuration:1.0 / 144.0]; } [command_buffer commit]; [command_buffer waitUntilScheduled]; } } static 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; } static 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; } static 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; } static 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); } #elif __linux__ #include static void _wayland_pointer_enter(void *data, struct wl_pointer *pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y) { // fprintf(stderr, "pointer enter: %d, %d\n", x, y); } static void _wayland_pointer_leave(void *data, struct wl_pointer *pointer, uint32_t serial, struct wl_surface *surface) { // fprintf(stderr, "pointer leave\n"); } static void _wayland_pointer_button(void *data, struct wl_pointer *pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { _opengl_gfx_context_wayland *cx = data; if (state == WL_POINTER_BUTTON_STATE_PRESSED) { if (button == BTN_LEFT) { cx->mouse_left_down = true; } else if (button == BTN_RIGHT) { cx->mouse_right_down = true; } } else if (state == WL_POINTER_BUTTON_STATE_RELEASED) { if (button == BTN_LEFT) { cx->mouse_left_down = false; } else if (button == BTN_RIGHT) { cx->mouse_right_down = false; } } } static void _wayland_pointer_axis(void *data, struct wl_pointer *pointer, uint32_t time, uint32_t axis, wl_fixed_t value) {} static void _wayland_pointer_motion(void *data, struct wl_pointer *pointer, uint32_t time, wl_fixed_t x, wl_fixed_t y) { _opengl_gfx_context_wayland *cx = data; cx->mouse_x = wl_fixed_to_int(x); cx->mouse_y = wl_fixed_to_int(y); } static const struct wl_pointer_listener pointer_listener = { .enter = _wayland_pointer_enter, .leave = _wayland_pointer_leave, .motion = _wayland_pointer_motion, .button = _wayland_pointer_button, .axis = _wayland_pointer_axis, }; static void _wayland_xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height, struct wl_array *states) {} static void _wayland_xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel) { keep_running = false; } static const struct xdg_toplevel_listener xdg_toplevel_listener = { _wayland_xdg_toplevel_configure, _wayland_xdg_toplevel_close, }; static void _wayland_xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial) { xdg_surface_ack_configure(xdg_surface, serial); } static const struct xdg_surface_listener xdg_surface_listener = { _wayland_xdg_surface_configure, }; static void _wayland_xdg_wm_base_ping(void *data, struct xdg_wm_base *shell, uint32_t serial) { xdg_wm_base_pong(shell, serial); } static const struct xdg_wm_base_listener xdg_wm_base_listener = { _wayland_xdg_wm_base_ping, }; static void _wayland_registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { _opengl_gfx_context_wayland *d = data; fprintf(stderr, "global: %s\n", interface); if (strcmp(interface, "wl_compositor") == 0) { d->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 3); } else if (strcmp(interface, "wl_shm") == 0) { d->shared_memory = wl_registry_bind(registry, name, &wl_shm_interface, 1); } else if (strcmp(interface, "xdg_wm_base") == 0) { d->wm_base = wl_registry_bind(registry, name, &xdg_wm_base_interface, 1); } else if (strcmp(interface, "wl_seat") == 0) { d->seat = wl_registry_bind(registry, name, &wl_seat_interface, 1); } } static void _wayland_registry_handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) {} static const struct wl_registry_listener registry_listener = { _wayland_registry_handle_global, _wayland_registry_handle_global_remove, }; static void _opengl_gfx_message_callback(GLenum source, GLenum type, GLenum id, GLenum severity, GLsizei length, const GLchar *message, const void *user_param) { fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n", type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "", type, severity, message); } static void _opengl_gfx_check_shader_error(string msg, GLuint shader, GLuint status) { GLint good = 0; glGetShaderiv(shader, status, &good); if (good == GL_FALSE) { GLint max_length = 0; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &max_length); uint8_t *log_buffer = malloc(max_length + 1); glGetShaderInfoLog(shader, max_length, &max_length, log_buffer); glDeleteShader(shader); fprintf(stderr, "%.*s: %.*s\n", msg.len, msg.data, max_length, log_buffer); exit(1); } } static void _opengl_gfx_check_shader_program_error(string msg, GLuint shader_program, GLuint status) { GLint good = 0; glGetProgramiv(shader_program, status, &good); if (good == GL_FALSE) { GLint max_length = 0; glGetProgramiv(shader_program, GL_INFO_LOG_LENGTH, &max_length); uint8_t *log_buffer = malloc(max_length + 1); glGetProgramInfoLog(shader_program, max_length, &max_length, log_buffer); glDeleteProgram(shader_program); fprintf(stderr, "%.*s: %.*s\n", msg.len, msg.data, max_length, log_buffer); exit(1); } } static GLuint _opengl_gfx_compile_shader(string file_path, GLuint shader_type) { GLuint shader = glCreateShader(shader_type); size_t shader_file_size = get_file_size(file_path); uint8_t *shader_file_data = malloc(shader_file_size + 1); load_file(file_path, shader_file_size, shader_file_data); shader_file_data[shader_file_size] = 0; // fprintf(stderr, "%s\n", shader_file_data); glShaderSource(shader, 1, &shader_file_data, NULL); glCompileShader(shader); _opengl_gfx_check_shader_error(_String("failed to compile shader"), shader, GL_COMPILE_STATUS); return shader; } static _opengl_gfx_context_wayland _opengl_gfx_init_context_wayland(uint32_t width, uint32_t height) { _opengl_gfx_context_wayland cx = {0}; cx.display = wl_display_connect(NULL); if (!cx.display) { fprintf(stderr, "Failed to connect to Wayland display\n"); exit(1); } struct wl_registry *registry = wl_display_get_registry(cx.display); wl_registry_add_listener(registry, ®istry_listener, &cx); // wait for all the globals to be registered wl_display_roundtrip(cx.display); xdg_wm_base_add_listener(cx.wm_base, &xdg_wm_base_listener, &_gfx_context.backend); cx.pointer = wl_seat_get_pointer(cx.seat); wl_pointer_add_listener(cx.pointer, &pointer_listener, &_gfx_context.backend); cx.surface = wl_compositor_create_surface(cx.compositor); cx.xdg_surface = xdg_wm_base_get_xdg_surface(cx.wm_base, cx.surface); xdg_surface_add_listener(cx.xdg_surface, &xdg_surface_listener, &_gfx_context.backend); cx.xdg_toplevel = xdg_surface_get_toplevel(cx.xdg_surface); xdg_toplevel_add_listener(cx.xdg_toplevel, &xdg_toplevel_listener, &_gfx_context.backend); xdg_toplevel_set_title(cx.xdg_toplevel, "chat - [Slack sux]"); xdg_toplevel_set_app_id(cx.xdg_toplevel, "nl.spacegirl.a_chat_client"); wl_surface_commit(cx.surface); wl_display_roundtrip(cx.display); int buffer_size = width * height * 4; int fd = syscall(SYS_memfd_create, "buffer", 0); ftruncate(fd, buffer_size); cx.pixels = mmap(NULL, buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); for (int i = 0; i < buffer_size; i++) { cx.pixels[i] = 0; } cx.shared_memory_pool = wl_shm_create_pool(cx.shared_memory, fd, buffer_size); cx.buffer = wl_shm_pool_create_buffer(cx.shared_memory_pool, 0, width, height, width * 4, WL_SHM_FORMAT_ARGB8888); wl_surface_attach(cx.surface, cx.buffer, 0, 0); wl_surface_commit(cx.surface); /* Init EGL */ EGLint major, minor, count, n, size; EGLConfig *configs; EGLint config_attribs[] = { EGL_SURFACE_TYPE, EGL_WINDOW_BIT, // EGL_RED_SIZE, 8, // EGL_BLUE_SIZE, 8, // EGL_GREEN_SIZE, 8, // EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, // EGL_NONE, }; static const EGLint context_attribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; cx.egl_display = eglGetDisplay(cx.display); if (cx.egl_display == EGL_NO_DISPLAY) { fprintf(stderr, "Failed to create EGL display\n"); exit(1); } fprintf(stderr, "Created EGL display\n"); if (eglInitialize(cx.egl_display, &major, &minor) != EGL_TRUE) { fprintf(stderr, "Failed to initialize EGL display\n"); exit(1); } fprintf(stderr, "EGL major: %d, minor: %d\n", major, minor); eglGetConfigs(cx.egl_display, NULL, 0, &count); configs = calloc(count, sizeof(EGLConfig)); eglChooseConfig(cx.egl_display, config_attribs, configs, count, &n); for (int i = 0; i < n; ++i) { eglGetConfigAttrib(cx.egl_display, configs[i], EGL_BUFFER_SIZE, &size); fprintf(stderr, "EGL Buffer size: %d\n", size); eglGetConfigAttrib(cx.egl_display, configs[i], EGL_RED_SIZE, &size); fprintf(stderr, "EGL Red size: %d\n", size); cx.egl_config = configs[i]; break; } eglBindAPI(EGL_OPENGL_API); cx.egl_context = eglCreateContext(cx.egl_display, cx.egl_config, EGL_NO_CONTEXT, context_attribs); cx.egl_window = wl_egl_window_create(cx.surface, width, height); if (cx.egl_window == EGL_NO_SURFACE) { fprintf(stderr, "Failed to create EGL window\n"); exit(1); } fprintf(stderr, "Created EGL window\n"); cx.egl_surface = eglCreateWindowSurface(cx.egl_display, cx.egl_config, cx.egl_window, NULL); if (eglMakeCurrent(cx.egl_display, cx.egl_surface, cx.egl_surface, cx.egl_context) != EGL_TRUE) { fprintf(stderr, "eglMakeCurrent() failed\n"); } glEnable(GL_DEBUG_OUTPUT); glDebugMessageCallback(_opengl_gfx_message_callback, NULL); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendFunc(GL_DST_ALPHA, GL_ONE_MINUS_SRC_ALPHA); cx.ui_rect_vertex_shader = _opengl_gfx_compile_shader( _String("shaders/ui_rect_vertex.glsl"), GL_VERTEX_SHADER); cx.ui_rect_fragment_shader = _opengl_gfx_compile_shader( _String("shaders/ui_rect_fragment.glsl"), GL_FRAGMENT_SHADER); cx.ui_rect_shader_program = glCreateProgram(); glAttachShader(cx.ui_rect_shader_program, cx.ui_rect_vertex_shader); glAttachShader(cx.ui_rect_shader_program, cx.ui_rect_fragment_shader); glLinkProgram(cx.ui_rect_shader_program); _opengl_gfx_check_shader_program_error( _String("failed to link ui_rect shader program"), cx.ui_rect_shader_program, GL_LINK_STATUS); cx.text_atlas_vertex_shader = _opengl_gfx_compile_shader( _String("shaders/text_atlas_vertex.glsl"), GL_VERTEX_SHADER); cx.text_atlas_fragment_shader = _opengl_gfx_compile_shader( _String("shaders/text_atlas_fragment.glsl"), GL_FRAGMENT_SHADER); cx.text_atlas_shader_program = glCreateProgram(); glAttachShader(cx.text_atlas_shader_program, cx.text_atlas_vertex_shader); glAttachShader(cx.text_atlas_shader_program, cx.text_atlas_fragment_shader); glLinkProgram(cx.text_atlas_shader_program); _opengl_gfx_check_shader_program_error( _String("failed to link text_atlas shader program"), cx.text_atlas_shader_program, GL_LINK_STATUS); cx.buffers = newArray(GLuint, 8); cx.textures = newArray(GLuint, 8); /* ******** */ return cx; } static _opengl_gfx_context_x11 _opengl_gfx_init_context_11(uint32_t width, uint32_t height) { Display *display = XOpenDisplay(NULL); if (display == NULL) { fprintf(stderr, "Failed to open X display\n"); exit(1); } int screen = DefaultScreen(display); Window window = XCreateSimpleWindow( display, RootWindow(display, screen), 0, 0, width, height, 1, BlackPixel(display, screen), WhitePixel(display, screen)); XSelectInput(display, window, ExposureMask | KeyPressMask | ButtonPressMask | ButtonReleaseMask); XMapWindow(display, window); return (_opengl_gfx_context_x11){ .display = display, .window = window, .screen = screen, }; } static void _opengl_gfx_send_events_wayland(_opengl_gfx_context_wayland *cx) { wl_display_dispatch(cx->display); // TODO: don't just render like crazy, limit framerate _opengl_gfx_present_wayland(cx); } static void _opengl_gfx_send_events_x11(_opengl_gfx_context_x11 *cx) { XEvent e; XNextEvent(cx->display, &e); if (e.type == Expose) { XFillRectangle(cx->display, cx->window, DefaultGC(cx->display, cx->screen), 20, 20, 10, 10); } if (e.type == KeyPress) { keep_running = false; XCloseDisplay(cx->display); } } static void _opengl_gfx_present_wayland(_opengl_gfx_context_wayland *cx) { _gfx_context.gpu_glyphs.size = 0; _gfx_context.gpu_ui_rects.size = 0; _gfx_context.frame_func(cx->mouse_x, cx->mouse_y, cx->mouse_left_down, cx->mouse_right_down); if (_gfx_context.gpu_glyphs.size > 0) { gfx_update_buffer(&_gfx_context, 2, _gfx_context.gpu_glyphs.data, _gfx_context.gpu_glyphs.size * sizeof(GpuGlyph)); } if (_gfx_context.gpu_ui_rects.size > 0) { gfx_update_buffer(&_gfx_context, 4, _gfx_context.gpu_ui_rects.data, _gfx_context.gpu_ui_rects.size * sizeof(GpuUiRect)); } GpuUniformParams gpu_uniform_params = { .screen_size = { (float)_gfx_context.frame_width, (float)_gfx_context.frame_height, }, .font_size = { (float)_FONT_WIDTH, (float)_FONT_HEIGHT, }, }; gfx_update_buffer(&_gfx_context, 3, &gpu_uniform_params, sizeof(GpuUniformParams)); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glViewport(0, 0, _gfx_context.frame_width, _gfx_context.frame_height); if (_gfx_context.gpu_ui_rects.size > 0) { glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, cx->buffers.data[0]); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, cx->buffers.data[4]); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, cx->buffers.data[3]); glUseProgram(cx->ui_rect_shader_program); const uint16_t indices[] = {0, 1, 2, 0, 2, 3}; glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices, _gfx_context.gpu_ui_rects.size); } if (_gfx_context.gpu_glyphs.size > 0) { glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, cx->buffers.data[0]); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, cx->buffers.data[2]); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, cx->buffers.data[3]); glUseProgram(cx->text_atlas_shader_program); const uint16_t indices[] = {0, 1, 2, 0, 2, 3}; glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices, _gfx_context.gpu_glyphs.size); } glFlush(); if (eglSwapBuffers(cx->egl_display, cx->egl_surface) != EGL_TRUE) { fprintf(stderr, "eglSwapBuffers() failed\n"); } } static size_t _opengl_gfx_push_texture_buffer_wayland(_opengl_gfx_context_wayland *cx, uint32_t width, uint32_t height, const void *data, size_t len) { pushArray(GLuint, &cx->textures, 0); glCreateTextures(GL_TEXTURE_2D, 1, &cx->textures.data[cx->textures.size - 1]); glTextureParameteri(cx->textures.data[cx->textures.size - 1], GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTextureParameteri(cx->textures.data[cx->textures.size - 1], GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTextureStorage2D(cx->textures.data[cx->textures.size - 1], 1, GL_RGBA8, width, height); glTextureSubImage2D(cx->textures.data[cx->textures.size - 1], 0, 0, 0, width, height, GL_RED, GL_UNSIGNED_BYTE, data); glBindTextureUnit(cx->textures.size - 1, cx->textures.data[cx->textures.size - 1]); return cx->textures.size - 1; } static void _opengl_gfx_resize_texture_buffer_wayland(_opengl_gfx_context_wayland *cx, uint32_t width, size_t texture_index, uint32_t height) { // TODO assert(false && "_opengl_gfx_resize_texture_buffer_wayland unimplemented"); } size_t _opengl_gfx_push_vertex_buffer_wayland(_opengl_gfx_context_wayland *cx, const void *data, size_t len) { pushArray(GLuint, &cx->buffers, 0); glCreateBuffers(1, &cx->buffers.data[cx->buffers.size - 1]); glNamedBufferStorage(cx->buffers.data[cx->buffers.size - 1], len, data, GL_DYNAMIC_STORAGE_BIT); return cx->buffers.size - 1; } static size_t _opengl_gfx_allocate_vertex_buffer_wayland(_opengl_gfx_context_wayland *cx, size_t len) { pushArray(GLuint, &cx->buffers, 0); glCreateBuffers(1, &cx->buffers.data[cx->buffers.size - 1]); glNamedBufferStorage(cx->buffers.data[cx->buffers.size - 1], len, (void *)0, GL_DYNAMIC_STORAGE_BIT); return cx->buffers.size - 1; } static void _opengl_gfx_update_buffer_wayland(_opengl_gfx_context_wayland *cx, size_t buffer_index, const void *data, size_t len) { glNamedBufferSubData(cx->buffers.data[buffer_index], 0, len, data); } #endif void gfx_run_events(gfx_context_t *cx) { #if defined(__APPLE__) return _metal_gfx_send_events(&cx->backend); #elif __linux__ return _opengl_gfx_send_events_wayland(&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); #elif __linux__ return _opengl_gfx_push_texture_buffer_wayland(&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); #elif __linux__ return _opengl_gfx_push_vertex_buffer_wayland(&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); #elif __linux__ return _opengl_gfx_allocate_vertex_buffer_wayland(&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); #elif __linux__ return _opengl_gfx_update_buffer_wayland(&cx->backend, buffer_index, data, len); #else #error "Unsupported graphics backend" #endif } void gfx_queue_char(gfx_context_t *cx, uint8_t character, float position[2]) { GpuGlyph glyph; if (character >= 32) { glyph = cx->glyph_cache.data[character - 32]; } else { glyph = cx->glyph_cache.data[cx->glyph_cache.size - 1]; } glyph.position[0] = position[0]; glyph.position[1] = position[1]; pushArray(GpuGlyph, &cx->gpu_glyphs, glyph); } void gfx_queue_text(gfx_context_t *cx, string text, float position[2], float max_x, float max_y, float color[4]) { float x = 0; float y = 0; for (size_t i = 0; i < text.len; ++i) { GpuGlyph glyph; if (text.data[i] >= 32) { glyph = cx->glyph_cache.data[text.data[i] - 32]; } else { glyph = cx->glyph_cache.data[cx->glyph_cache.size - 1]; } size_t j; float j_x = 0; for (j = i; j < text.len; ++j) { GpuGlyph glyph; if (text.data[j] >= 32) { glyph = cx->glyph_cache.data[text.data[j] - 32]; } else { glyph = cx->glyph_cache.data[cx->glyph_cache.size - 1]; } if (text.data[j] == 32) { break; } j_x += glyph.size[0] / 2 + 4; } // if (x + j_x >= max_x) { // x = 0; // y += 24; // } // if (y >= max_y) { // break; // } glyph.color[0] = color[0]; glyph.color[1] = color[1]; glyph.color[2] = color[2]; glyph.color[3] = color[3]; glyph.position[0] = x + position[0]; glyph.position[1] = y + position[1]; x += glyph.size[0] / 2 + 4; pushArray(GpuGlyph, &cx->gpu_glyphs, glyph); } } void gfx_queue_ui_rect(gfx_context_t *cx, float position[2], float size[2], float border_size, float color[4]) { GpuUiRect rect = (GpuUiRect){.position = {position[0], position[1]}, .size = {size[0], size[1]}, .border_size = {border_size, border_size}, .color = {color[0], color[1], color[2], color[3]}}; pushArray(GpuUiRect, &cx->gpu_ui_rects, rect); } void gfx_add_glyph(gfx_context_t *cx, GpuGlyph glyph) { pushArray(GpuGlyph, &cx->glyph_cache, glyph); } 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); #elif __linux__ _opengl_gfx_init_context_wayland(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; _gfx_context.gpu_ui_rects = newArray(GpuUiRect, 2000); _gfx_context.gpu_glyphs = newArray(GpuGlyph, 8192 * 32); _gfx_context.glyph_cache = newArray(GpuGlyph, 97); const float vertices[] = { // positions texture coords -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, }; const uint16_t indices[] = {0, 1, 2, 0, 2, 3}; // NOTE: the order of these matter gfx_push_vertex_buffer(&_gfx_context, vertices, sizeof(vertices)); gfx_push_vertex_buffer(&_gfx_context, indices, sizeof(indices)); gfx_allocate_vertex_buffer(&_gfx_context, _gfx_context.gpu_glyphs.capacity * sizeof(GpuGlyph)); gfx_allocate_vertex_buffer(&_gfx_context, sizeof(GpuUniformParams)); gfx_allocate_vertex_buffer(&_gfx_context, _gfx_context.gpu_ui_rects.capacity * sizeof(GpuUiRect)); return &_gfx_context; } #endif #endif