mach/src/platform/native.zig
2022-09-14 18:15:41 -07:00

680 lines
24 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const glfw = @import("glfw");
const gpu = @import("gpu");
const app_pkg = @import("app");
const Core = @import("../Core.zig");
const structs = @import("../structs.zig");
const enums = @import("../enums.zig");
const util = @import("util.zig");
const common = @import("common.zig");
comptime {
common.checkApplication(app_pkg);
}
const App = app_pkg.App;
pub const scope_levels = if (@hasDecl(App, "scope_levels")) App.scope_levels else [0]std.log.ScopeLevel{};
pub const log_level = if (@hasDecl(App, "log_level")) App.log_level else std.log.default_level;
pub const Platform = struct {
window: glfw.Window,
core: *Core,
backend_type: gpu.BackendType,
allocator: std.mem.Allocator,
events: EventQueue = .{},
user_ptr: UserPtr = undefined,
last_window_size: structs.Size,
last_framebuffer_size: structs.Size,
last_position: glfw.Window.Pos,
wait_event_timeout: f64 = 0.0,
cursors: [@typeInfo(enums.MouseCursor).Enum.fields.len]?glfw.Cursor =
std.mem.zeroes([@typeInfo(enums.MouseCursor).Enum.fields.len]?glfw.Cursor),
cursors_tried: [@typeInfo(enums.MouseCursor).Enum.fields.len]bool =
[_]bool{false} ** @typeInfo(enums.MouseCursor).Enum.fields.len,
// TODO: these can be moved to Core
instance: *gpu.Instance,
adapter: *gpu.Adapter,
last_cursor_position: structs.WindowPos,
linux_gamemode_is_active: bool,
const EventQueue = std.TailQueue(structs.Event);
const EventNode = EventQueue.Node;
const UserPtr = struct {
platform: *Platform,
};
fn getEnvVarOwned(allocator: std.mem.Allocator, key: []const u8) error{ OutOfMemory, InvalidUtf8 }!?[]u8 {
return std.process.getEnvVarOwned(allocator, key) catch |err| switch (err) {
error.EnvironmentVariableNotFound => @as(?[]u8, null),
else => |e| e,
};
}
pub fn init(allocator: std.mem.Allocator, core: *Core) !Platform {
const linux_gamemode_is_active = try initLinuxGamemode(allocator);
const options = core.options;
const backend_type = try util.detectBackendType(allocator);
glfw.setErrorCallback(Platform.errorCallback);
try glfw.init(.{});
// Create the test window and discover adapters using it (esp. for OpenGL)
var hints = util.glfwWindowHintsForBackend(backend_type);
hints.cocoa_retina_framebuffer = true;
const window = try glfw.Window.create(
options.width,
options.height,
options.title,
null,
null,
hints,
);
if (backend_type == .opengl) try glfw.makeContextCurrent(window);
if (backend_type == .opengles) try glfw.makeContextCurrent(window);
const window_size = try window.getSize();
const framebuffer_size = try window.getFramebufferSize();
const instance = gpu.createInstance(null);
if (instance == null) {
std.log.err("mach: failed to create GPU instance\n", .{});
std.process.exit(1);
}
const surface = util.createSurfaceForWindow(instance.?, window, comptime util.detectGLFWOptions());
var response: ?util.RequestAdapterResponse = null;
instance.?.requestAdapter(&gpu.RequestAdapterOptions{
.compatible_surface = surface,
.power_preference = options.power_preference,
.force_fallback_adapter = false,
}, &response, util.requestAdapterCallback);
if (response.?.status != .success) {
std.log.err("mach: failed to create GPU adapter: {?s}\n", .{response.?.message});
std.log.info("-> maybe try MACH_GPU_BACKEND=opengl ?\n", .{});
std.process.exit(1);
}
// Print which adapter we are going to use.
var props: gpu.Adapter.Properties = undefined;
response.?.adapter.getProperties(&props);
if (props.backend_type == .nul) {
std.log.err("no backend found for {s} adapter", .{props.adapter_type.name()});
std.process.exit(1);
}
std.log.info("mach: found {s} backend on {s} adapter: {s}, {s}\n", .{
props.backend_type.name(),
props.adapter_type.name(),
props.name,
props.driver_description,
});
// Create a device with default limits/features.
const device = response.?.adapter.createDevice(&.{
.required_features_count = if (options.required_features) |v| @intCast(u32, v.len) else 0,
.required_features = if (options.required_features) |v| @as(?[*]gpu.FeatureName, v.ptr) else null,
.required_limits = if (options.required_limits) |limits| @as(?*gpu.RequiredLimits, &gpu.RequiredLimits{
.limits = limits,
}) else null,
});
if (device == null) {
std.log.err("mach: failed to create GPU device\n", .{});
std.process.exit(1);
}
core.swap_chain_format = .bgra8_unorm;
const descriptor = gpu.SwapChain.Descriptor{
.label = "main swap chain",
.usage = .{ .render_attachment = true },
.format = core.swap_chain_format,
.width = framebuffer_size.width,
.height = framebuffer_size.height,
.present_mode = switch (options.vsync) {
.none => .immediate,
.double => .fifo,
.triple => .mailbox,
},
};
device.?.setUncapturedErrorCallback({}, util.printUnhandledErrorCallback);
core.device = device.?;
core.backend_type = backend_type;
core.surface = surface;
core.current_desc = descriptor;
core.target_desc = descriptor;
core.swap_chain = null;
const cursor_pos = try window.getCursorPos();
return Platform{
.window = window,
.core = core,
.backend_type = backend_type,
.allocator = core.allocator,
.last_window_size = .{ .width = window_size.width, .height = window_size.height },
.last_framebuffer_size = .{ .width = framebuffer_size.width, .height = framebuffer_size.height },
.last_position = try window.getPos(),
.last_cursor_position = .{
.x = cursor_pos.xpos,
.y = cursor_pos.ypos,
},
.instance = instance.?,
.adapter = response.?.adapter,
.linux_gamemode_is_active = linux_gamemode_is_active,
};
}
pub fn deinit(platform: *Platform) void {
for (platform.cursors) |glfw_cursor| {
if (glfw_cursor) |cur| {
cur.destroy();
}
}
while (platform.events.popFirst()) |ev| {
platform.allocator.destroy(ev);
}
platform.deinitLinuxGamemode();
}
/// Check if gamemode should be activated
fn activateGamemode(allocator: std.mem.Allocator) error{ OutOfMemory, InvalidUtf8 }!bool {
const GAMEMODE_ENV = try getEnvVarOwned(allocator, "GAMEMODE");
if (GAMEMODE_ENV) |env| {
defer allocator.free(env);
return !(std.ascii.eqlIgnoreCase(env, "off") or std.ascii.eqlIgnoreCase(env, "false"));
}
return true;
}
fn initLinuxGamemode(allocator: std.mem.Allocator) error{ OutOfMemory, DLOpenFailed, InvalidUtf8 }!bool {
if (builtin.os.tag == .linux) {
const gamemode = @import("gamemode");
if (try activateGamemode(allocator)) {
gamemode.requestStart() catch |err| {
if (!std.mem.containsAtLeast(u8, gamemode.errorString(), 1, "dlopen failed"))
std.log.err("Gamemode error {} -> {s}", .{ err, gamemode.errorString() });
return false;
};
std.log.info("Gamemode activated", .{});
return true;
}
}
return false;
}
fn deinitLinuxGamemode(platform: *Platform) void {
if (builtin.os.tag == .linux and platform.linux_gamemode_is_active) {
const gamemode = @import("gamemode");
gamemode.requestEnd() catch |err| {
std.log.err("Gamemode error {} -> {s}", .{ err, gamemode.errorString() });
};
}
}
fn pushEvent(platform: *Platform, event: structs.Event) void {
const node = platform.allocator.create(EventNode) catch unreachable;
node.* = .{ .data = event };
platform.events.append(node);
}
pub fn initCallback(platform: *Platform) void {
platform.user_ptr = UserPtr{ .platform = platform };
platform.window.setUserPointer(&platform.user_ptr);
const key_callback = struct {
fn callback(window: glfw.Window, key: glfw.Key, scancode: i32, action: glfw.Action, mods: glfw.Mods) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).platform;
const key_event = structs.KeyEvent{
.key = toMachKey(key),
.mods = toMachMods(mods),
};
switch (action) {
.press => pf.pushEvent(.{ .key_press = key_event }),
.repeat => pf.pushEvent(.{ .key_repeat = key_event }),
.release => pf.pushEvent(.{ .key_release = key_event }),
}
_ = scancode;
}
}.callback;
platform.window.setKeyCallback(key_callback);
const char_callback = struct {
fn callback(window: glfw.Window, codepoint: u21) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).platform;
pf.pushEvent(.{
.char_input = .{
.codepoint = codepoint,
},
});
}
}.callback;
platform.window.setCharCallback(char_callback);
const mouse_motion_callback = struct {
fn callback(window: glfw.Window, xpos: f64, ypos: f64) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).platform;
pf.last_cursor_position = .{
.x = xpos,
.y = ypos,
};
pf.pushEvent(.{
.mouse_motion = .{
.pos = .{
.x = xpos,
.y = ypos,
},
},
});
}
}.callback;
platform.window.setCursorPosCallback(mouse_motion_callback);
const mouse_button_callback = struct {
fn callback(window: glfw.Window, button: glfw.mouse_button.MouseButton, action: glfw.Action, mods: glfw.Mods) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).platform;
const mouse_button_event = structs.MouseButtonEvent{
.button = toMachButton(button),
.pos = pf.last_cursor_position,
.mods = toMachMods(mods),
};
switch (action) {
.press => pf.pushEvent(.{ .mouse_press = mouse_button_event }),
.release => pf.pushEvent(.{
.mouse_release = mouse_button_event,
}),
else => {},
}
}
}.callback;
platform.window.setMouseButtonCallback(mouse_button_callback);
const scroll_callback = struct {
fn callback(window: glfw.Window, xoffset: f64, yoffset: f64) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).platform;
pf.pushEvent(.{
.mouse_scroll = .{
.xoffset = @floatCast(f32, xoffset),
.yoffset = @floatCast(f32, yoffset),
},
});
}
}.callback;
platform.window.setScrollCallback(scroll_callback);
const focus_callback = struct {
fn callback(window: glfw.Window, focused: bool) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).platform;
pf.pushEvent(if (focused) .focus_gained else .focus_lost);
}
}.callback;
platform.window.setFocusCallback(focus_callback);
const close_callback = struct {
fn callback(window: glfw.Window) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).platform;
pf.pushEvent(.closed);
}
}.callback;
platform.window.setCloseCallback(close_callback);
const size_callback = struct {
fn callback(window: glfw.Window, width: i32, height: i32) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).platform;
pf.last_window_size.width = @intCast(u32, width);
pf.last_window_size.height = @intCast(u32, height);
}
}.callback;
platform.window.setSizeCallback(size_callback);
const framebuffer_size_callback = struct {
fn callback(window: glfw.Window, width: u32, height: u32) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).platform;
pf.last_framebuffer_size.width = width;
pf.last_framebuffer_size.height = height;
}
}.callback;
platform.window.setFramebufferSizeCallback(framebuffer_size_callback);
}
pub fn setOptions(platform: *Platform, options: structs.Options) !void {
try platform.window.setSize(.{ .width = options.width, .height = options.height });
try platform.window.setTitle(options.title);
try platform.window.setSizeLimits(
glfwSizeOptional(options.size_min),
glfwSizeOptional(options.size_max),
);
platform.core.target_desc.present_mode = switch (options.vsync) {
.none => .immediate,
.double => .fifo,
.triple => .mailbox,
};
if (options.fullscreen) {
platform.last_position = try platform.window.getPos();
const monitor = glfw.Monitor.getPrimary().?;
const video_mode = try monitor.getVideoMode();
try platform.window.setMonitor(monitor, 0, 0, video_mode.getWidth(), video_mode.getHeight(), null);
} else {
const position = platform.last_position;
try platform.window.setMonitor(null, @intCast(i32, position.x), @intCast(i32, position.y), options.width, options.height, null);
}
if (options.headless) platform.window.hide() catch {};
}
pub fn setShouldClose(platform: *Platform, value: bool) void {
platform.window.setShouldClose(value);
}
pub fn getFramebufferSize(platform: *Platform) structs.Size {
return platform.last_framebuffer_size;
}
pub fn getWindowSize(platform: *Platform) structs.Size {
return platform.last_window_size;
}
pub fn setMouseCursor(platform: *Platform, cursor: enums.MouseCursor) !void {
// Try to create glfw standard cursor, but could fail. In the future
// we hope to provide custom backup images for these.
// See https://github.com/hexops/mach/pull/352 for more info
const enum_int = @enumToInt(cursor);
const tried = platform.cursors_tried[enum_int];
if (!tried) {
platform.cursors_tried[enum_int] = true;
platform.cursors[enum_int] = switch (cursor) {
.arrow => glfw.Cursor.createStandard(.arrow) catch null,
.ibeam => glfw.Cursor.createStandard(.ibeam) catch null,
.crosshair => glfw.Cursor.createStandard(.crosshair) catch null,
.pointing_hand => glfw.Cursor.createStandard(.pointing_hand) catch null,
.resize_ew => glfw.Cursor.createStandard(.resize_ew) catch null,
.resize_ns => glfw.Cursor.createStandard(.resize_ns) catch null,
.resize_nwse => glfw.Cursor.createStandard(.resize_nwse) catch null,
.resize_nesw => glfw.Cursor.createStandard(.resize_nesw) catch null,
.resize_all => glfw.Cursor.createStandard(.resize_all) catch null,
.not_allowed => glfw.Cursor.createStandard(.not_allowed) catch null,
};
}
if (platform.cursors[enum_int]) |cur| {
try platform.window.setCursor(cur);
} else {
// TODO: In the future we shouldn't hit this because we'll provide backup
// custom cursors.
// See https://github.com/hexops/mach/pull/352 for more info
std.log.warn("mach: setMouseCursor: {} not yet supported\n", .{@tagName(cursor)});
}
}
pub fn hasEvent(platform: *Platform) bool {
return platform.events.first != null;
}
pub fn setWaitEvent(platform: *Platform, timeout: f64) void {
platform.wait_event_timeout = timeout;
}
pub fn pollEvent(platform: *Platform) ?structs.Event {
if (platform.events.popFirst()) |n| {
const data = n.data;
platform.allocator.destroy(n);
return data;
}
return null;
}
fn toMachButton(button: glfw.mouse_button.MouseButton) enums.MouseButton {
return switch (button) {
.left => .left,
.right => .right,
.middle => .middle,
.four => .four,
.five => .five,
.six => .six,
.seven => .seven,
.eight => .eight,
};
}
fn toMachKey(key: glfw.Key) enums.Key {
return switch (key) {
.a => .a,
.b => .b,
.c => .c,
.d => .d,
.e => .e,
.f => .f,
.g => .g,
.h => .h,
.i => .i,
.j => .j,
.k => .k,
.l => .l,
.m => .m,
.n => .n,
.o => .o,
.p => .p,
.q => .q,
.r => .r,
.s => .s,
.t => .t,
.u => .u,
.v => .v,
.w => .w,
.x => .x,
.y => .y,
.z => .z,
.zero => .zero,
.one => .one,
.two => .two,
.three => .three,
.four => .four,
.five => .five,
.six => .six,
.seven => .seven,
.eight => .eight,
.nine => .nine,
.F1 => .f1,
.F2 => .f2,
.F3 => .f3,
.F4 => .f4,
.F5 => .f5,
.F6 => .f6,
.F7 => .f7,
.F8 => .f8,
.F9 => .f9,
.F10 => .f10,
.F11 => .f11,
.F12 => .f12,
.F13 => .f13,
.F14 => .f14,
.F15 => .f15,
.F16 => .f16,
.F17 => .f17,
.F18 => .f18,
.F19 => .f19,
.F20 => .f20,
.F21 => .f21,
.F22 => .f22,
.F23 => .f23,
.F24 => .f24,
.F25 => .f25,
.kp_divide => .kp_divide,
.kp_multiply => .kp_multiply,
.kp_subtract => .kp_subtract,
.kp_add => .kp_add,
.kp_0 => .kp_0,
.kp_1 => .kp_1,
.kp_2 => .kp_2,
.kp_3 => .kp_3,
.kp_4 => .kp_4,
.kp_5 => .kp_5,
.kp_6 => .kp_6,
.kp_7 => .kp_7,
.kp_8 => .kp_8,
.kp_9 => .kp_9,
.kp_decimal => .kp_decimal,
.kp_equal => .kp_equal,
.kp_enter => .kp_enter,
.enter => .enter,
.escape => .escape,
.tab => .tab,
.left_shift => .left_shift,
.right_shift => .right_shift,
.left_control => .left_control,
.right_control => .right_control,
.left_alt => .left_alt,
.right_alt => .right_alt,
.left_super => .left_super,
.right_super => .right_super,
.menu => .menu,
.num_lock => .num_lock,
.caps_lock => .caps_lock,
.print_screen => .print,
.scroll_lock => .scroll_lock,
.pause => .pause,
.delete => .delete,
.home => .home,
.end => .end,
.page_up => .page_up,
.page_down => .page_down,
.insert => .insert,
.left => .left,
.right => .right,
.up => .up,
.down => .down,
.backspace => .backspace,
.space => .space,
.minus => .minus,
.equal => .equal,
.left_bracket => .left_bracket,
.right_bracket => .right_bracket,
.backslash => .backslash,
.semicolon => .semicolon,
.apostrophe => .apostrophe,
.comma => .comma,
.period => .period,
.slash => .slash,
.grave_accent => .grave,
.world_1 => .unknown,
.world_2 => .unknown,
.unknown => .unknown,
};
}
fn toMachMods(mods: glfw.Mods) structs.KeyMods {
return .{
.shift = mods.shift,
.control = mods.control,
.alt = mods.alt,
.super = mods.super,
.caps_lock = mods.caps_lock,
.num_lock = mods.num_lock,
};
}
/// Default GLFW error handling callback
fn errorCallback(error_code: glfw.Error, description: [:0]const u8) void {
std.log.err("glfw: {}: {s}\n", .{ error_code, description });
}
};
pub const BackingTimer = std.time.Timer;
var app: App = undefined;
pub const GPUInterface = gpu.dawn.Interface;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
gpu.Impl.init();
var core = try coreInit(allocator);
defer coreDeinit(core, allocator);
try app.init(core);
defer app.deinit(core);
while (!core.internal.window.shouldClose()) {
// On Darwin targets, Dawn requires an NSAutoreleasePool per frame to release
// some resources. See Dawn's CHelloWorld example.
const pool = try util.AutoReleasePool.init();
defer util.AutoReleasePool.release(pool);
try coreUpdate(core, null);
try app.update(core);
}
}
pub fn coreInit(allocator: std.mem.Allocator) !*Core {
const core: *Core = try allocator.create(Core);
errdefer allocator.destroy(core);
try Core.init(allocator, core);
// Glfw specific: initialize the user pointer used in callbacks
core.*.internal.initCallback();
return core;
}
pub fn coreDeinit(core: *Core, allocator: std.mem.Allocator) void {
core.internal.deinit();
allocator.destroy(core);
}
pub const CoreResizeCallback = *const fn (*Core, u32, u32) callconv(.C) void;
pub fn coreUpdate(core: *Core, resize: ?CoreResizeCallback) !void {
if (core.internal.wait_event_timeout > 0.0) {
if (core.internal.wait_event_timeout == std.math.inf(f64)) {
// Wait for an event
try glfw.waitEvents();
} else {
// Wait for an event with a timeout
try glfw.waitEventsTimeout(core.internal.wait_event_timeout);
}
} else {
// Don't wait for events
try glfw.pollEvents();
}
core.delta_time_ns = core.timer.lapPrecise();
core.delta_time = @intToFloat(f32, core.delta_time_ns) / @intToFloat(f32, std.time.ns_per_s);
var framebuffer_size = core.getFramebufferSize();
core.target_desc.width = framebuffer_size.width;
core.target_desc.height = framebuffer_size.height;
if (core.swap_chain == null or !std.meta.eql(core.current_desc, core.target_desc)) {
core.swap_chain = core.device.createSwapChain(core.surface, &core.target_desc);
if (@hasDecl(App, "resize")) {
try app.resize(core, core.target_desc.width, core.target_desc.height);
} else if (resize != null) {
resize.?(core, core.target_desc.width, core.target_desc.height);
}
core.current_desc = core.target_desc;
}
}
fn glfwSizeOptional(size: structs.SizeOptional) glfw.Window.SizeOptional {
return .{
.width = size.width,
.height = size.height,
};
}