mach/src/Core.zig

846 lines
24 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const build_options = @import("build-options");
const mach = @import("main.zig");
const gpu = mach.gpu;
const log = std.log.scoped(.mach);
const Core = @This();
// Whether or not you can drive the main loop in a non-blocking fashion, or if the underlying
// platform must take control and drive the main loop itself.
pub const supports_non_blocking = switch (build_options.core_platform) {
// Platforms that support non-blocking mode.
.linux => true,
.windows => true,
.null => true,
// Platforms which take control of the main loop.
.wasm => false,
.darwin => false,
};
const EventQueue = std.fifo.LinearFifo(Event, .Dynamic);
/// Set this to true if you intend to drive the main loop yourself.
///
/// A panic will occur if `supports_non_blocking == false` for the platform.
pub var non_blocking = false;
pub const mach_module = .mach_core;
pub const mach_systems = .{ .main, .init, .tick, .presentFrame, .deinit };
// Set track_fields to true so that when these field values change, we know about it
// and can update the platform windows.
windows: mach.Objects(
.{ .track_fields = true },
struct {
/// Window title string
// TODO: document how to set this using a format string
// TODO: allocation/free strategy
title: [:0]const u8 = "Mach Window",
/// Texture format of the framebuffer (read-only)
framebuffer_format: gpu.Texture.Format = .bgra8_unorm,
/// Width of the framebuffer in texels (read-only)
/// Will be updated to reflect the actual framebuffer dimensions after window creation.
framebuffer_width: u32 = 1920 / 2,
/// Height of the framebuffer in texels (read-only)
/// Will be updated to reflect the actual framebuffer dimensions after window creation.
framebuffer_height: u32 = 1080 / 2,
/// Vertical sync mode, prevents screen tearing.
vsync_mode: VSyncMode = .none,
/// Window display mode: fullscreen, windowed or borderless fullscreen
display_mode: DisplayMode = .windowed,
/// Cursor
cursor_mode: CursorMode = .normal,
cursor_shape: CursorShape = .arrow,
/// Outer border
border: bool = true,
/// Width of the window in virtual pixels
width: u32 = 1920 / 2,
/// Height of the window in virtual pixels
height: u32 = 1080 / 2,
/// Target frames per second
refresh_rate: u32 = 0,
/// Titlebar/window decorations
decorated: bool = true,
/// Color of the window decorations, i.e. titlebar
/// if null, decoration is the system-determined color
decoration_color: ?gpu.Color = null,
/// Whether the window should be completely transparent
/// or not. On macOS, to achieve a fully transparent window
/// decoration_color must also be set fully transparent.
transparent: bool = false,
// GPU
// When `native` is not null, the rest of the fields have been
// initialized.
device: *gpu.Device = undefined,
instance: *gpu.Instance = undefined,
adapter: *gpu.Adapter = undefined,
queue: *gpu.Queue = undefined,
swap_chain: *gpu.SwapChain = undefined,
swap_chain_descriptor: gpu.SwapChain.Descriptor = undefined,
surface: *gpu.Surface = undefined,
surface_descriptor: gpu.Surface.Descriptor = undefined,
// After window initialization, (when device is not null)
// changing these will have no effect
power_preference: gpu.PowerPreference = .undefined,
required_features: ?[]const gpu.FeatureName = null,
required_limits: ?gpu.Limits = null,
swap_chain_usage: gpu.Texture.UsageFlags = .{
.render_attachment = true,
},
/// Container for native platform-specific information
native: ?Platform.Native = null,
},
),
/// Callback system invoked per tick (e.g. per-frame)
on_tick: ?mach.FunctionID = null,
/// Callback system invoked when application is exiting
on_exit: ?mach.FunctionID = null,
/// Current state of the application
state: enum {
running,
exiting,
deinitializing,
exited,
} = .running,
frame: mach.time.Frequency,
input: mach.time.Frequency,
// Internal module state
allocator: std.mem.Allocator,
events: EventQueue,
input_state: InputState,
oom: std.Thread.ResetEvent = .{},
pub fn init(core: *Core) !void {
const allocator = std.heap.c_allocator;
// TODO: fix all leaks and use options.allocator
try mach.sysgpu.Impl.init(allocator, .{});
var events = EventQueue.init(allocator);
try events.ensureTotalCapacity(8192);
core.* = .{
// Note: since core.windows is initialized for us already, we just copy the pointer.
.windows = core.windows,
.allocator = allocator,
.events = events,
.input_state = .{},
.input = .{ .target = 0 },
.frame = .{ .target = 1 },
};
try core.frame.start();
try core.input.start();
}
pub fn initWindow(core: *Core, window_id: mach.ObjectID) !void {
var core_window = core.windows.getValue(window_id);
defer core.windows.setValueRaw(window_id, core_window);
core_window.instance = gpu.createInstance(null) orelse {
log.err("failed to create GPU instance", .{});
std.process.exit(1);
};
core_window.surface = core_window.instance.createSurface(&core_window.surface_descriptor);
var response: RequestAdapterResponse = undefined;
core_window.instance.requestAdapter(&gpu.RequestAdapterOptions{
.compatible_surface = core_window.surface,
.power_preference = core_window.power_preference,
.force_fallback_adapter = .false,
}, &response, requestAdapterCallback);
if (response.status != .success) {
log.err("failed to create GPU adapter: {?s}", .{response.message});
log.info("-> maybe try MACH_GPU_BACKEND=opengl ?", .{});
std.process.exit(1);
}
// Print which adapter we are going to use.
var props = std.mem.zeroes(gpu.Adapter.Properties);
response.adapter.?.getProperties(&props);
if (props.backend_type == .null) {
log.err("no backend found for {s} adapter", .{props.adapter_type.name()});
std.process.exit(1);
}
log.info("found {s} backend on {s} adapter: {s}, {s}\n", .{
props.backend_type.name(),
props.adapter_type.name(),
props.name,
props.driver_description,
});
core_window.adapter = response.adapter.?;
// Create a device with default limits/features.
core_window.device = response.adapter.?.createDevice(&.{
.required_features_count = if (core_window.required_features) |v| @as(u32, @intCast(v.len)) else 0,
.required_features = if (core_window.required_features) |v| @as(?[*]const gpu.FeatureName, v.ptr) else null,
.required_limits = if (core_window.required_limits) |limits| @as(?*const gpu.RequiredLimits, &gpu.RequiredLimits{
.limits = limits,
}) else null,
.device_lost_callback = &deviceLostCallback,
.device_lost_userdata = null,
}) orelse {
log.err("failed to create GPU device\n", .{});
std.process.exit(1);
};
core_window.device.setUncapturedErrorCallback({}, printUnhandledErrorCallback);
core_window.queue = core_window.device.getQueue();
core_window.swap_chain_descriptor = gpu.SwapChain.Descriptor{
.label = "main swap chain",
.usage = core_window.swap_chain_usage,
.format = .bgra8_unorm,
.width = core_window.framebuffer_width,
.height = core_window.framebuffer_height,
.present_mode = switch (core_window.vsync_mode) {
.none => .immediate,
.double => .fifo,
.triple => .mailbox,
},
};
core_window.swap_chain = core_window.device.createSwapChain(core_window.surface, &core_window.swap_chain_descriptor);
core.pushEvent(.{ .window_open = .{ .window_id = window_id } });
}
pub fn tick(core: *Core, core_mod: mach.Mod(Core)) !void {
// TODO(core)(slimsag): consider execution order of mach.Core (e.g. creating a new window
// during application execution, rendering to multiple windows, etc.) and how
// that relates to Platform.tick being responsible for both handling window updates
// (like title/size changes) and window creation, plus multi-threaded rendering.
try Platform.tick(core);
core_mod.run(core.on_tick.?);
core_mod.call(.presentFrame);
}
pub fn presentFrame(core: *Core, core_mod: mach.Mod(Core)) !void {
var windows = core.windows.slice();
while (windows.next()) |window_id| {
var core_window = core.windows.getValue(window_id);
defer core.windows.setValueRaw(window_id, core_window);
mach.sysgpu.Impl.deviceTick(core_window.device);
core_window.swap_chain.present();
}
// Record to frame rate frequency monitor that a frame was finished.
core.frame.tick();
switch (core.state) {
.running => {},
.exiting => {
core.state = .deinitializing;
core_mod.run(core.on_exit.?);
core_mod.call(.deinit);
},
.deinitializing => {},
.exited => @panic("application not running"),
}
}
pub fn main(core: *Core, core_mod: mach.Mod(Core)) !void {
if (core.on_tick == null) @panic("core.on_tick callback must be set");
if (core.on_exit == null) @panic("core.on_exit callback must be set");
try Platform.tick(core);
core_mod.run(core.on_tick.?);
core_mod.call(.presentFrame);
// If the user doesn't want mach.Core to take control of the main loop, we bail out - the next
// app tick is already scheduled to run in the future and they'll .present_frame to return
// control to us later.
if (non_blocking) {
if (!supports_non_blocking) std.debug.panic(
"mach.Core: platform {s} does not support non_blocking=true mode.",
.{@tagName(build_options.core_platform)},
);
return;
}
// The user wants mach.Core to take control of the main loop.
if (supports_non_blocking) {
while (core.state != .exited) {
try Platform.tick(core);
core_mod.run(core.on_tick.?);
core_mod.call(.presentFrame);
}
// Don't return, because Platform.run wouldn't either (marked noreturn due to underlying
// platform APIs never returning.)
std.process.exit(0);
} else {
// Platform drives the main loop.
Platform.run(platform_update_callback, .{ core, core_mod });
// Platform.run should be marked noreturn, so this shouldn't ever run. But just in case we
// accidentally introduce a different Platform.run in the future, we put an exit here for
// good measure.
std.process.exit(0);
}
}
fn platform_update_callback(core: *Core, core_mod: mach.Mod(Core)) !bool {
// TODO(core)(slimsag): consider execution order of mach.Core (e.g. creating a new window
// during application execution, rendering to multiple windows, etc.) and how
// that relates to Platform.tick being responsible for both handling window updates
// (like title/size changes) and window creation, plus multi-threaded rendering.
try Platform.tick(core);
core_mod.run(core.on_tick.?);
core_mod.call(.presentFrame);
return core.state != .exited;
}
pub fn exit(core: *Core) void {
core.state = .exiting;
}
pub fn deinit(core: *Core) !void {
core.state = .exited;
var windows = core.windows.slice();
while (windows.next()) |window_id| {
var core_window = core.windows.getValue(window_id);
core_window.swap_chain.release();
core_window.queue.release();
core_window.device.release();
core_window.surface.release();
core_window.adapter.release();
core_window.instance.release();
}
core.events.deinit();
}
/// Returns the next event until there are no more available. You should check for events during
/// every on_tick()
pub inline fn nextEvent(core: *@This()) ?Event {
return core.events.readItem();
}
/// Push an event onto the event queue, or set OOM if no space is available.
///
/// Updates the input_state tracker.
pub inline fn pushEvent(core: *@This(), event: Event) void {
// Write event
core.events.writeItem(event) catch {
core.oom.set();
return;
};
// Update input state
switch (event) {
.key_press => |ev| core.input_state.keys.setValue(@intFromEnum(ev.key), true),
.key_release => |ev| core.input_state.keys.setValue(@intFromEnum(ev.key), false),
.mouse_press => |ev| core.input_state.mouse_buttons.setValue(@intFromEnum(ev.button), true),
.mouse_release => |ev| core.input_state.mouse_buttons.setValue(@intFromEnum(ev.button), false),
.mouse_motion => |ev| core.input_state.mouse_position = ev.pos,
.focus_lost => {
// Clear input state that may be 'stuck' when focus is regained.
core.input_state.keys = InputState.KeyBitSet.initEmpty();
core.input_state.mouse_buttons = InputState.MouseButtonSet.initEmpty();
},
else => {},
}
}
/// Reports whether mach.Core ran out of memory, indicating events may have been dropped.
///
/// Once called, the OOM flag is reset and mach.Core will continue operating normally.
pub fn outOfMemory(core: *@This()) bool {
if (!core.oom.isSet()) return false;
core.oom.reset();
return true;
}
pub fn keyPressed(core: *@This(), key: Key) bool {
return core.input_state.isKeyPressed(key);
}
pub fn keyReleased(core: *@This(), key: Key) bool {
return core.input_state.isKeyReleased(key);
}
pub fn mousePressed(core: *@This(), button: MouseButton) bool {
return core.input_state.isMouseButtonPressed(button);
}
pub fn mouseReleased(core: *@This(), button: MouseButton) bool {
return core.input_state.isMouseButtonReleased(button);
}
pub fn mousePosition(core: *@This()) Position {
return core.input_state.mouse_position;
}
inline fn requestAdapterCallback(
context: *RequestAdapterResponse,
status: gpu.RequestAdapterStatus,
adapter: ?*gpu.Adapter,
message: ?[*:0]const u8,
) void {
context.* = RequestAdapterResponse{
.status = status,
.adapter = adapter,
.message = message,
};
}
// TODO(important): expose device loss to users, this can happen especially in the web and on mobile
// devices. Users will need to re-upload all assets to the GPU in this event.
fn deviceLostCallback(reason: gpu.Device.LostReason, msg: [*:0]const u8, userdata: ?*anyopaque) callconv(.C) void {
_ = userdata;
_ = reason;
log.err("mach: device lost: {s}", .{msg});
@panic("mach: device lost");
}
pub inline fn printUnhandledErrorCallback(_: void, ty: gpu.ErrorType, message: [*:0]const u8) void {
switch (ty) {
.validation => std.log.err("gpu: validation error: {s}\n", .{message}),
.out_of_memory => std.log.err("gpu: out of memory: {s}\n", .{message}),
.device_lost => std.log.err("gpu: device lost: {s}\n", .{message}),
.unknown => std.log.err("gpu: unknown error: {s}\n", .{message}),
else => unreachable,
}
std.process.exit(1);
}
pub fn detectBackendType(allocator: std.mem.Allocator) !gpu.BackendType {
const backend = std.process.getEnvVarOwned(
allocator,
"MACH_GPU_BACKEND",
) catch |err| switch (err) {
error.EnvironmentVariableNotFound => {
if (builtin.target.isDarwin()) return .metal;
if (builtin.target.os.tag == .windows) return .d3d12;
return .vulkan;
},
else => return err,
};
defer allocator.free(backend);
if (std.ascii.eqlIgnoreCase(backend, "null")) return .null;
if (std.ascii.eqlIgnoreCase(backend, "d3d11")) return .d3d11;
if (std.ascii.eqlIgnoreCase(backend, "d3d12")) return .d3d12;
if (std.ascii.eqlIgnoreCase(backend, "metal")) return .metal;
if (std.ascii.eqlIgnoreCase(backend, "vulkan")) return .vulkan;
if (std.ascii.eqlIgnoreCase(backend, "opengl")) return .opengl;
if (std.ascii.eqlIgnoreCase(backend, "opengles")) return .opengles;
@panic("unknown MACH_GPU_BACKEND type");
}
const Platform = switch (build_options.core_platform) {
.wasm => @panic("TODO: support mach.Core WASM platform"),
.windows => @import("core/Windows.zig"),
.linux => @import("core/Linux.zig"),
.darwin => @import("core/Darwin.zig"),
.null => @import("core/Null.zig"),
};
pub const InputState = struct {
const KeyBitSet = std.StaticBitSet(@as(u8, @intFromEnum(Key.max)) + 1);
const MouseButtonSet = std.StaticBitSet(@as(u4, @intFromEnum(MouseButton.max)) + 1);
keys: KeyBitSet = KeyBitSet.initEmpty(),
mouse_buttons: MouseButtonSet = MouseButtonSet.initEmpty(),
mouse_position: Position = .{ .x = 0, .y = 0 },
pub inline fn isKeyPressed(input: InputState, key: Key) bool {
return input.keys.isSet(@intFromEnum(key));
}
pub inline fn isKeyReleased(input: InputState, key: Key) bool {
return !input.isKeyPressed(key);
}
pub inline fn isMouseButtonPressed(input: InputState, button: MouseButton) bool {
return input.mouse_buttons.isSet(@intFromEnum(button));
}
pub inline fn isMouseButtonReleased(input: InputState, button: MouseButton) bool {
return !input.isMouseButtonPressed(button);
}
};
pub const WindowColor = union(enum) {
system: void, // Default window colors
transparent: struct {
color: gpu.Color,
// If true, and the OS supports it, the titlebar will also be set to color
titlebar: bool = false,
},
solid: struct {
color: gpu.Color,
// If titlebar is true, and the OS supports it, the titlebar will also be set to color
titlebar: bool = false,
},
};
pub const Event = union(enum) {
key_press: KeyEvent,
key_repeat: KeyEvent,
key_release: KeyEvent,
char_input: struct {
window_id: mach.ObjectID,
codepoint: u21,
},
mouse_motion: struct {
window_id: mach.ObjectID,
pos: Position,
},
mouse_press: MouseButtonEvent,
mouse_release: MouseButtonEvent,
mouse_scroll: struct {
window_id: mach.ObjectID,
xoffset: f32,
yoffset: f32,
},
window_resize: ResizeEvent,
window_open: struct {
window_id: mach.ObjectID,
},
zoom_gesture: ZoomGestureEvent,
focus_gained: struct {
window_id: mach.ObjectID,
},
focus_lost: struct {
window_id: mach.ObjectID,
},
close: struct {
window_id: mach.ObjectID,
},
};
pub const KeyEvent = struct {
window_id: mach.ObjectID,
key: Key,
mods: KeyMods,
};
pub const MouseButtonEvent = struct {
window_id: mach.ObjectID,
button: MouseButton,
pos: Position,
mods: KeyMods,
};
pub const ResizeEvent = struct {
window_id: mach.ObjectID,
size: Size,
};
pub const ZoomGestureEvent = struct {
window_id: mach.ObjectID,
phase: GesturePhase,
zoom: f32,
};
pub const GesturePhase = enum {
began,
ended,
};
pub const MouseButton = enum {
left,
right,
middle,
four,
five,
six,
seven,
eight,
pub const max = MouseButton.eight;
};
pub const Key = enum {
a,
b,
c,
d,
e,
f,
g,
h,
i,
j,
k,
l,
m,
n,
o,
p,
q,
r,
s,
t,
u,
v,
w,
x,
y,
z,
zero,
one,
two,
three,
four,
five,
six,
seven,
eight,
nine,
f1,
f2,
f3,
f4,
f5,
f6,
f7,
f8,
f9,
f10,
f11,
f12,
f13,
f14,
f15,
f16,
f17,
f18,
f19,
f20,
f21,
f22,
f23,
f24,
f25,
kp_divide,
kp_multiply,
kp_subtract,
kp_add,
kp_0,
kp_1,
kp_2,
kp_3,
kp_4,
kp_5,
kp_6,
kp_7,
kp_8,
kp_9,
kp_decimal,
kp_comma,
kp_equal,
kp_enter,
enter,
escape,
tab,
left_shift,
right_shift,
left_control,
right_control,
left_alt,
right_alt,
left_super,
right_super,
menu,
num_lock,
caps_lock,
print,
scroll_lock,
pause,
delete,
home,
end,
page_up,
page_down,
insert,
left,
right,
up,
down,
backspace,
space,
minus,
equal,
left_bracket,
right_bracket,
backslash,
semicolon,
apostrophe,
comma,
period,
slash,
grave,
iso_backslash,
international1,
international2,
international3,
international4,
international5,
lang1,
lang2,
unknown,
pub const max = Key.unknown;
};
pub const KeyMods = packed struct(u8) {
shift: bool,
control: bool,
alt: bool,
super: bool,
caps_lock: bool,
num_lock: bool,
_padding: u2 = 0,
};
pub const DisplayMode = enum {
/// Windowed mode.
windowed,
/// Fullscreen mode, using this option may change the display's video mode.
fullscreen,
/// Borderless fullscreen window.
///
/// Beware that true .fullscreen is also a hint to the OS that is used in various contexts, e.g.
///
/// * macOS: Moving to a virtual space dedicated to fullscreen windows as the user expects
/// * macOS: .borderless windows cannot prevent the system menu bar from being displayed
///
/// Always allow users to choose their preferred display mode.
borderless,
};
pub const VSyncMode = enum {
/// Potential screen tearing.
/// No synchronization with monitor, render frames as fast as possible.
///
/// Not available on WASM, fallback to double
none,
/// No tearing, synchronizes rendering with monitor refresh rate, rendering frames when ready.
///
/// Tries to stay one frame ahead of the monitor, so when it's ready for the next frame it is
/// already prepared.
double,
/// No tearing, synchronizes rendering with monitor refresh rate, rendering frames when ready.
///
/// Tries to stay two frames ahead of the monitor, so when it's ready for the next frame it is
/// already prepared.
///
/// Not available on WASM, fallback to double
triple,
};
pub const Size = struct {
width: u32,
height: u32,
pub inline fn eql(a: Size, b: Size) bool {
return a.width == b.width and a.height == b.height;
}
};
pub const CursorMode = enum {
/// Makes the cursor visible and behaving normally.
normal,
/// Makes the cursor invisible when it is over the content area of the window but does not
/// restrict it from leaving.
hidden,
/// Hides and grabs the cursor, providing virtual and unlimited cursor movement. This is useful
/// for implementing for example 3D camera controls.
disabled,
};
pub const CursorShape = enum {
arrow,
ibeam,
crosshair,
pointing_hand,
resize_ew,
resize_ns,
resize_nwse,
resize_nesw,
resize_all,
not_allowed,
};
pub const Position = struct {
x: f64,
y: f64,
};
const RequestAdapterResponse = struct {
status: gpu.RequestAdapterStatus,
adapter: ?*gpu.Adapter,
message: ?[*:0]const u8,
};
fn assertHasDecl(comptime T: anytype, comptime decl_name: []const u8) void {
if (!@hasDecl(T, decl_name)) @compileError(@typeName(T) ++ " missing declaration: " ++ decl_name);
}
fn assertHasField(comptime T: anytype, comptime field_name: []const u8) void {
if (!@hasField(T, field_name)) @compileError(@typeName(T) ++ " missing field: " ++ field_name);
}
test {
_ = Platform;
@import("std").testing.refAllDeclsRecursive(VSyncMode);
@import("std").testing.refAllDeclsRecursive(Size);
@import("std").testing.refAllDeclsRecursive(Position);
@import("std").testing.refAllDeclsRecursive(Event);
@import("std").testing.refAllDeclsRecursive(KeyEvent);
@import("std").testing.refAllDeclsRecursive(MouseButtonEvent);
@import("std").testing.refAllDeclsRecursive(MouseButton);
@import("std").testing.refAllDeclsRecursive(Key);
@import("std").testing.refAllDeclsRecursive(KeyMods);
@import("std").testing.refAllDeclsRecursive(DisplayMode);
@import("std").testing.refAllDeclsRecursive(CursorMode);
@import("std").testing.refAllDeclsRecursive(CursorShape);
}