From 1e652006b00e48b2672c03fa8962f524806f8cba Mon Sep 17 00:00:00 2001 From: Lorenzo Torres Date: Mon, 11 Aug 2025 11:23:23 +0200 Subject: [PATCH] Implemented platform support for MacOS --- build.zig | 20 ++++++ src/macos.zig | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/window.m | 87 ++++++++++++++++++++++++++ 3 files changed, 275 insertions(+) create mode 100644 src/macos.zig create mode 100644 src/window.m diff --git a/build.zig b/build.zig index 46a8ede..deb122f 100644 --- a/build.zig +++ b/build.zig @@ -86,6 +86,26 @@ pub fn build(b: *std.Build) void { } b.installArtifact(exe); }, + .macos => { + const exe = b.addExecutable(.{ + .name = "sideros", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/macos.zig"), + .target = target, + .optimize = optimize, + }), + }); + exe.root_module.addIncludePath(b.path("src")); + exe.linkSystemLibrary("vulkan"); + exe.root_module.addSystemIncludePath(.{ .cwd_relative = "/usr/local/include" }); + exe.root_module.addLibraryPath(.{ .cwd_relative = "/usr/local/lib" }); + exe.root_module.addCSourceFile(.{.file = b.path("src/window.m")}); + exe.root_module.linkFramework("Cocoa", .{}); + exe.root_module.linkFramework("Metal", .{}); + exe.root_module.linkFramework("QuartzCore", .{}); + exe.linkLibrary(sideros); + b.installArtifact(exe); + }, else => { std.debug.panic("Compilation not implemented for OS: {any}\n", .{target.result.os.tag}); }, diff --git a/src/macos.zig b/src/macos.zig new file mode 100644 index 0000000..5162ed9 --- /dev/null +++ b/src/macos.zig @@ -0,0 +1,168 @@ +const std = @import("std"); +const sideros = @cImport({ + @cInclude("sideros_api.h"); +}); + +const c = @cImport({ + @cInclude("vulkan/vulkan.h"); + @cInclude("vulkan/vulkan_macos.h"); + @cInclude("vulkan/vulkan_metal.h"); +}); + +const builtin = @import("builtin"); +const debug = (builtin.mode == .Debug); + +const validation_layers: []const [*c]const u8 = if (!debug) &[0][*c]const u8{} else &[_][*c]const u8{ + "VK_LAYER_KHRONOS_validation", +}; + +const device_extensions: []const [*c]const u8 = &[_][*c]const u8{ + c.VK_KHR_SWAPCHAIN_EXTENSION_NAME, +}; + +const Error = error{ + initialization_failed, + extension_not_present, + incompatible_driver, + layer_not_present, + out_of_memory, + unknown_error, +}; + +fn mapError(result: c_int) !void { + return switch (result) { + c.VK_SUCCESS => {}, + c.VK_ERROR_INITIALIZATION_FAILED => Error.initialization_failed, + c.VK_ERROR_EXTENSION_NOT_PRESENT => Error.extension_not_present, + c.VK_ERROR_INCOMPATIBLE_DRIVER => Error.incompatible_driver, + c.VK_ERROR_LAYER_NOT_PRESENT => Error.layer_not_present, + c.VK_ERROR_OUT_OF_DEVICE_MEMORY => Error.out_of_memory, + else => Error.unknown_error, + }; +} + +extern fn create_window() void; +extern fn poll_cocoa_events() void; +extern fn is_window_closed() bool; +extern fn get_metal_layer() *anyopaque; + +fn vulkan_init_instance(allocator: std.mem.Allocator, handle: *c.VkInstance) !void { + const extensions = [_][*c]const u8{ c.VK_MVK_MACOS_SURFACE_EXTENSION_NAME, c.VK_KHR_SURFACE_EXTENSION_NAME, c.VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME }; + + // Querry avaliable extensions size + var avaliableExtensionsCount: u32 = 0; + _ = c.vkEnumerateInstanceExtensionProperties(null, &avaliableExtensionsCount, null); + // Actually querry avaliable extensions + const avaliableExtensions = try allocator.alloc(c.VkExtensionProperties, avaliableExtensionsCount); + defer allocator.free(avaliableExtensions); + _ = c.vkEnumerateInstanceExtensionProperties(null, &avaliableExtensionsCount, avaliableExtensions.ptr); + + // Check the extensions we want against the extensions the user has + for (extensions) |need_ext| { + var found = false; + const needName = std.mem.sliceTo(need_ext, 0); + for (avaliableExtensions) |useable_ext| { + const extensionName = useable_ext.extensionName[0..std.mem.indexOf(u8, &useable_ext.extensionName, &[_]u8{0}).?]; + + if (std.mem.eql(u8, needName, extensionName)) { + found = true; + break; + } + } + if (!found) { + std.debug.panic("ERROR: Needed vulkan extension {s} not found\n", .{need_ext}); + } + } + + // Querry avaliable layers size + var avaliableLayersCount: u32 = 0; + _ = c.vkEnumerateInstanceLayerProperties(&avaliableLayersCount, null); + // Actually querry avaliable layers + const availableLayers = try allocator.alloc(c.VkLayerProperties, avaliableLayersCount); + defer allocator.free(availableLayers); + _ = c.vkEnumerateInstanceLayerProperties(&avaliableLayersCount, availableLayers.ptr); + + // Every layer we do have we add to this list, if we don't have it no worries just print a message and continue + var newLayers = std.ArrayList([*c]const u8).init(allocator); + defer newLayers.deinit(); + // Loop over layers we want + for (validation_layers) |want_layer| { + var found = false; + for (availableLayers) |useable_validation| { + const layer_name: [*c]const u8 = &useable_validation.layerName; + if (std.mem.eql(u8, std.mem.sliceTo(want_layer, 0), std.mem.sliceTo(layer_name, 0))) { + found = true; + break; + } + } + if (!found) { + std.debug.print("WARNING: Compiled in debug mode, but wanted validation layer {s} not found.\n", .{want_layer}); + std.debug.print("NOTE: Validation layer will be removed from the wanted validation layers\n", .{}); + } else { + try newLayers.append(want_layer); + } + } + + const app_info: c.VkApplicationInfo = .{ + .sType = c.VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pApplicationName = "sideros", + .applicationVersion = c.VK_MAKE_VERSION(1, 0, 0), + .engineVersion = c.VK_MAKE_VERSION(1, 0, 0), + .pEngineName = "sideros", + .apiVersion = c.VK_MAKE_VERSION(1, 3, 0), + }; + + const instance_info: c.VkInstanceCreateInfo = .{ + .sType = c.VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pApplicationInfo = &app_info, + .enabledExtensionCount = @intCast(extensions.len), + .flags = c.VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR, + .ppEnabledExtensionNames = @ptrCast(extensions[0..]), + .enabledLayerCount = @intCast(newLayers.items.len), + .ppEnabledLayerNames = newLayers.items.ptr, + }; + + try mapError(c.vkCreateInstance(&instance_info, null, handle)); +} + +fn vulkan_init_surface(instance: c.VkInstance, layer: *anyopaque, handle: *c.VkSurfaceKHR) !void { + const create_info: c.VkMacOSSurfaceCreateInfoMVK = .{ + .sType = c.VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK, + .pView = layer, + }; + try mapError(c.vkCreateMacOSSurfaceMVK(instance, &create_info, null, handle)); +} + +fn vulkan_init(allocator: std.mem.Allocator, layer: *anyopaque) !sideros.GameInit { + var gameInit: sideros.GameInit = undefined; + + try vulkan_init_instance(allocator, &gameInit.instance); + try vulkan_init_surface(@ptrCast(gameInit.instance), layer, &gameInit.surface); + + return gameInit; +} + +// TODO: actually clean up these +fn vulkan_cleanup(gameInit: sideros.GameInit) void { + _ = gameInit; + //c.vkDestroySurfaceKHR(gameInit.instance, gameInit.surface, null); + //c.vkDestroyInstance(gameInit.instance, null); +} + +pub fn main() !void { + create_window(); + const layer = get_metal_layer(); + + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer if (gpa.deinit() != .ok) @panic("Memory leaked"); + + const gameInit = try vulkan_init(allocator, layer); + defer vulkan_cleanup(gameInit); + + sideros.sideros_init(gameInit); + + while (!is_window_closed()) { + poll_cocoa_events(); + } +} diff --git a/src/window.m b/src/window.m new file mode 100644 index 0000000..5311a72 --- /dev/null +++ b/src/window.m @@ -0,0 +1,87 @@ +#import +#import + +static NSWindow *window = nil; +static bool window_closed = false; + +@interface WindowDelegate : NSObject +@end + +@implementation WindowDelegate +- (void)windowWillClose:(NSNotification *)notification { + window_closed = true; +} +@end + +static WindowDelegate *window_delegate = nil; + +@interface MetalView : NSView +@end + +@implementation MetalView ++ (Class)layerClass { + return [CAMetalLayer class]; +} +- (instancetype)initWithFrame:(NSRect)frame { + if ((self = [super initWithFrame:frame])) { + self.wantsLayer = YES; + self.layer = [CAMetalLayer layer]; + } + return self; +} +@end + +static MetalView *metal_view = nil; + +static void initialize_app(void) { + static BOOL initialized = NO; + if (!initialized) { + [NSApplication sharedApplication]; + initialized = YES; + } +} + +void create_window(void) { + initialize_app(); + window_closed = false; + if (window != nil) { + [window makeKeyAndOrderFront:nil]; + return; + } + + NSRect frame = NSMakeRect(100, 100, 800, 600); + + window = [[NSWindow alloc] + initWithContentRect:frame + styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskResizable) + backing:NSBackingStoreBuffered + defer:NO]; + + [window setTitle:@"Sideros"]; + + metal_view = [[MetalView alloc] initWithFrame:frame]; + [window setContentView:metal_view]; + NSLog(@"Layer class: %@", NSStringFromClass([metal_view.layer class])); + + window_delegate = [[WindowDelegate alloc] init]; + [window setDelegate:window_delegate]; + + [window makeKeyAndOrderFront:nil]; + [window orderFrontRegardless]; +} + +void poll_cocoa_events(void) { + @autoreleasepool { + NSEvent *event; + while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:[NSDate distantPast] + inMode:NSDefaultRunLoopMode + dequeue:YES])) { + [NSApp sendEvent:event]; + } + } +} + +bool is_window_closed(void) { return window_closed; } +void *get_metal_layer(void) { return (__bridge void *)metal_view.layer; }