Implemented platform support for MacOS

This commit is contained in:
Lorenzo Torres 2025-08-11 11:23:23 +02:00
parent 7836ebfcd3
commit 1e652006b0
3 changed files with 275 additions and 0 deletions

View file

@ -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});
},

168
src/macos.zig Normal file
View file

@ -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();
}
}

87
src/window.m Normal file
View file

@ -0,0 +1,87 @@
#import <Cocoa/Cocoa.h>
#import <QuartzCore/CAMetalLayer.h>
static NSWindow *window = nil;
static bool window_closed = false;
@interface WindowDelegate : NSObject <NSWindowDelegate>
@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; }