mach: fundamental changes

- Core doesn't depend to `App` anymore
 - `setOptions` has replaced with some new functions (`setTitle`,
   `setSize`, etc)
   - and more
This commit is contained in:
Ali Chraghi 2023-01-10 15:53:29 +04:00 committed by Stephen Gutekanst
parent 91a53807ab
commit 1d7cd4be80
26 changed files with 2306 additions and 1999 deletions

View file

@ -1,100 +1,403 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const glfw = @import("glfw");
const std = @import("std");
const gpu = @import("gpu");
const platform = @import("platform.zig");
const structs = @import("structs.zig");
const enums = @import("enums.zig");
const Timer = @import("Timer.zig");
const Core = @This();
pub const Core = @This();
allocator: Allocator,
internal: *platform.Core,
options: structs.Options,
pub const Options = struct {
is_app: bool = false,
title: [*:0]const u8 = "Mach Engine",
size: Size = .{ .width = 640, .height = 640 },
power_preference: gpu.PowerPreference = .undefined,
required_features: ?[]const gpu.FeatureName = null,
required_limits: ?gpu.Limits = null,
};
/// The amount of time (in seconds) that has passed since the last frame was rendered.
///
/// For example, if you are animating a cube which should rotate 360 degrees every second,
/// instead of writing (360.0 / 60.0) and assuming the frame rate is 60hz, write
/// (360.0 * core.delta_time)
delta_time: f32 = 0,
delta_time_ns: u64 = 0,
timer: Timer,
device: *gpu.Device,
backend_type: gpu.BackendType,
swap_chain: ?*gpu.SwapChain,
swap_chain_format: gpu.Texture.Format,
surface: ?*gpu.Surface,
current_desc: gpu.SwapChain.Descriptor,
target_desc: gpu.SwapChain.Descriptor,
internal: platform.Type,
pub fn init(allocator: std.mem.Allocator, core: *Core) !void {
core.allocator = allocator;
core.options = structs.Options{};
core.timer = try Timer.start();
core.internal = try platform.Type.init(allocator, core);
pub fn init(allocator: std.mem.Allocator, options: Options) !Core {
return .{
.internal = try platform.Core.init(allocator, options),
};
}
/// Set runtime options for application, like title, window size etc.
///
/// See mach.Options for details
pub fn setOptions(core: *Core, options: structs.Options) !void {
try core.internal.setOptions(options);
core.options = options;
}
// Signals mach to stop the update loop.
pub fn close(core: *Core) void {
core.internal.close();
}
// Sets seconds to wait for an event with timeout before calling update()
// again.
//
// timeout is in seconds (<= 0.0 disables waiting)
// - pass std.math.inf(f64) to wait with no timeout
//
// update() can be called earlier than timeout if an event happens (key press,
// mouse motion, etc.)
//
// update() can be called a bit later than timeout due to timer precision and
// process scheduling.
pub fn setWaitEvent(core: *Core, timeout: f64) void {
core.internal.setWaitEvent(timeout);
}
// Returns the framebuffer size, in subpixel units.
//
// e.g. returns 1280x960 on macOS for a window that is 640x480
pub fn getFramebufferSize(core: *Core) structs.Size {
return core.internal.getFramebufferSize();
}
// Returns the window size, in pixel units.
//
// e.g. returns 1280x960 on macOS for a window that is 640x480
pub fn getWindowSize(core: *Core) structs.Size {
return core.internal.getWindowSize();
}
pub fn setMouseCursor(core: *Core, cursor: enums.MouseCursor) !void {
try core.internal.setMouseCursor(cursor);
}
pub fn setCursorMode(core: *Core, mode: enums.CursorMode) !void {
try core.internal.setCursorMode(mode);
pub fn deinit(core: *Core) void {
return core.internal.deinit();
}
pub fn hasEvent(core: *Core) bool {
return core.internal.hasEvent();
}
pub fn pollEvent(core: *Core) ?structs.Event {
return core.internal.pollEvent();
pub fn pollEvents(core: *Core) ?Event {
return core.internal.pollEvents();
}
/// Returns the framebuffer size, in subpixel units.
pub fn framebufferSize(core: *Core) Size {
return core.internal.framebufferSize();
}
/// Sets seconds to wait for an event with timeout when calling `Core.update()`
/// again.
///
/// timeout is in seconds (<= `0.0` disables waiting)
/// - pass `std.math.inf(f64)` to wait with no timeout
///
/// `Core.update()` will return earlier than timeout if an event happens (key press,
/// mouse motion, etc.)
///
/// `Core.update()` can return a bit later than timeout due to timer precision and
/// process scheduling.
pub fn setWaitTimeout(core: *Core, timeout: f64) void {
return core.internal.setWaitTimeout(timeout);
}
/// Set the window title
pub fn setTitle(core: *Core, title: [:0]const u8) void {
return core.internal.setTitle(title);
}
/// Set the window mode
pub fn setDisplayMode(core: *Core, mode: DisplayMode, monitor: ?usize) void {
return core.internal.setDisplayMode(mode, monitor);
}
/// Returns the window mode
pub fn displayMode(core: *Core) DisplayMode {
return core.internal.displayMode();
}
pub fn setBorder(core: *Core, value: bool) void {
return core.internal.setBorder(value);
}
pub fn border(core: *Core) bool {
return core.internal.border();
}
pub fn setHeadless(core: *Core, value: bool) void {
return core.internal.setHeadless(value);
}
pub fn headless(core: *Core) bool {
return core.internal.headless();
}
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,
};
/// Set monitor synchronization mode.
pub fn setVSync(core: *Core, mode: VSyncMode) void {
return core.internal.setVSync(mode);
}
/// Returns monitor synchronization mode.
pub fn vsync(core: *Core) VSyncMode {
return core.internal.vsync();
}
/// Set the window size, in subpixel units.
pub fn setSize(core: *Core, value: Size) void {
return core.internal.setSize(value);
}
/// Returns the window size, in subpixel units.
pub fn size(core: *Core) Size {
return core.internal.size();
}
/// Set the minimum and maximum allowed size for the window.
pub fn setSizeLimit(core: *Core, size_limit: SizeLimit) void {
return core.internal.setSizeLimit(size_limit);
}
/// Returns the minimum and maximum allowed size for the window.
pub fn sizeLimit(core: *Core) SizeLimit {
return core.internal.sizeLimit();
}
pub fn setCursorMode(core: *Core, mode: CursorMode) void {
return core.internal.setCursorMode(mode);
}
pub fn cursorMode(core: *Core) CursorMode {
return core.internal.cursorMode();
}
pub fn setCursorShape(core: *Core, cursor: CursorShape) void {
return core.internal.setCursorShape(cursor);
}
pub fn cursorShape(core: *Core) CursorShape {
return core.internal.cursorShape();
}
pub fn adapter(core: *Core) *gpu.Adapter {
return core.internal.adapter();
}
pub fn device(core: *Core) *gpu.Device {
return core.internal.device();
}
pub fn swapChain(core: *Core) *gpu.SwapChain {
return core.internal.swapChain();
}
pub fn descriptor(core: *Core) gpu.SwapChain.Descriptor {
return core.internal.descriptor();
}
pub const Size = struct {
width: u32,
height: u32,
};
pub const SizeOptional = struct {
width: ?u32,
height: ?u32,
};
pub const SizeLimit = struct {
min: SizeOptional,
max: SizeOptional,
};
pub const Position = struct {
x: f64,
y: f64,
};
pub const Event = union(enum) {
key_press: KeyEvent,
key_repeat: KeyEvent,
key_release: KeyEvent,
char_input: struct {
codepoint: u21,
},
mouse_motion: struct {
pos: Position,
},
mouse_press: MouseButtonEvent,
mouse_release: MouseButtonEvent,
mouse_scroll: struct {
xoffset: f32,
yoffset: f32,
},
framebuffer_resize: Size,
focus_gained,
focus_lost,
close,
};
pub const KeyEvent = struct {
key: Key,
mods: KeyMods,
};
pub const MouseButtonEvent = struct {
button: MouseButton,
pos: Position,
mods: KeyMods,
};
pub const MouseButton = enum {
left,
right,
middle,
four,
five,
six,
seven,
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_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,
unknown,
};
pub const KeyMods = packed struct {
shift: bool,
control: bool,
alt: bool,
super: bool,
caps_lock: bool,
num_lock: bool,
_reserved: u2 = 0,
};
pub const DisplayMode = enum {
windowed,
fullscreen,
// TODO: fullscreen_windowed,
};
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,
};

View file

@ -1,20 +1,20 @@
const std = @import("std");
const platform = @import("platform.zig");
const Timer = @This();
pub const Timer = @This();
backing_timer: platform.BackingTimerType = undefined,
internal: platform.Timer,
/// Initialize the timer.
pub fn start() !Timer {
return Timer{
.backing_timer = try platform.BackingTimerType.start(),
.internal = try platform.Timer.start(),
};
}
/// Reads the timer value since start or the last reset in nanoseconds.
pub inline fn readPrecise(timer: *Timer) u64 {
return timer.backing_timer.read();
return timer.internal.read();
}
/// Reads the timer value since start or the last reset in seconds.
@ -24,12 +24,12 @@ pub inline fn read(timer: *Timer) f32 {
/// Resets the timer value to 0/now.
pub inline fn reset(timer: *Timer) void {
timer.backing_timer.reset();
timer.internal.reset();
}
/// Returns the current value of the timer in nanoseconds, then resets it.
pub inline fn lapPrecise(timer: *Timer) u64 {
return timer.backing_timer.lap();
return timer.internal.lap();
}
/// Returns the current value of the timer in seconds, then resets it.

View file

@ -1,3 +1,4 @@
const std = @import("std");
pub const Core = @import("Core.zig");
pub const gpu = @import("gpu");
pub const ecs = @import("ecs");
@ -12,6 +13,9 @@ pub const module = ecs.Module(.{
},
});
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
pub fn App(
comptime modules: anytype,
comptime app_init: anytype, // fn (engine: *ecs.World(modules)) !void
@ -22,26 +26,32 @@ pub fn App(
return struct {
engine: ecs.World(modules),
pub fn init(app: *@This(), core: *Core) !void {
pub fn init(app: *@This()) !void {
app.* = .{
.engine = try ecs.World(modules).init(core.allocator),
.engine = try ecs.World(modules).init(allocator),
};
app.*.engine.set(.mach, .core, core);
app.*.engine.set(.mach, .device, core.device);
var core = try allocator.create(Core);
core.* = try Core.init(allocator, .{});
app.engine.set(.mach, .core, core);
app.engine.set(.mach, .device, core.device());
try app_init(&app.engine);
}
pub fn deinit(app: *@This(), _: *Core) void {
pub fn deinit(app: *@This()) void {
const core = app.engine.get(.mach, .core);
core.deinit();
allocator.destroy(core);
app.engine.deinit();
_ = gpa.deinit();
}
pub fn update(app: *@This(), _: *Core) !void {
pub fn update(app: *@This()) !bool {
app.engine.tick();
return false;
}
pub fn resize(app: *@This(), core: *Core, width: u32, height: u32) !void {
pub fn resize(app: *@This(), width: u32, height: u32) !void {
_ = app;
_ = core;
_ = width;
_ = height;
// TODO: send resize messages to ECS modules

40
src/entry.zig Normal file
View file

@ -0,0 +1,40 @@
const builtin = @import("builtin");
pub usingnamespace @import("platform.zig").entry;
comptime {
if (!builtin.is_test) {
if (!@hasDecl(@import("app"), "App")) {
@compileError("expected e.g. `pub const App = mach.App(modules, init)' (App definition missing in your main Zig file)");
}
const App = @import("app").App;
if (@typeInfo(App) != .Struct) {
@compileError("App must be a struct type. Found:" ++ @typeName(App));
}
if (@hasDecl(App, "init")) {
const InitFn = @TypeOf(@field(App, "init"));
if (InitFn != fn (*App) @typeInfo(@typeInfo(InitFn).Fn.return_type.?).ErrorUnion.error_set!void)
@compileError("expected 'pub fn init(app: *App) !void' found '" ++ @typeName(InitFn) ++ "'");
} else {
@compileError("App must export 'pub fn init(app: *App) !void'");
}
if (@hasDecl(App, "update")) {
const UpdateFn = @TypeOf(@field(App, "update"));
if (UpdateFn != fn (app: *App) @typeInfo(@typeInfo(UpdateFn).Fn.return_type.?).ErrorUnion.error_set!bool)
@compileError("expected 'pub fn update(app: *App) !bool' found '" ++ @typeName(UpdateFn) ++ "'");
} else {
@compileError("App must export 'pub fn update(app: *App) !bool'");
}
if (@hasDecl(App, "deinit")) {
const DeinitFn = @TypeOf(@field(App, "deinit"));
if (DeinitFn != fn (app: *App) void)
@compileError("expected 'pub fn deinit(app: *App) void' found '" ++ @typeName(DeinitFn) ++ "'");
} else {
@compileError("App must export 'pub fn deinit(app: *App) void'");
}
}
}

View file

@ -1,185 +0,0 @@
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 MouseCursor = enum {
arrow,
ibeam,
crosshair,
pointing_hand,
resize_ew,
resize_ns,
resize_nwse,
resize_nesw,
resize_all,
not_allowed,
};
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 MouseButton = enum {
left,
right,
middle,
four,
five,
six,
seven,
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_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,
unknown,
};

View file

@ -1,27 +1,21 @@
pub usingnamespace @import("structs.zig");
pub usingnamespace @import("enums.zig");
pub usingnamespace @import("entry.zig");
pub const Core = @import("Core.zig");
pub const Timer = @import("Timer.zig");
pub const ResourceManager = @import("resource/ResourceManager.zig");
pub const gpu = @import("gpu");
pub const ecs = @import("ecs");
pub const sysaudio = @import("sysaudio");
pub const sysjs = @import("sysjs");
pub const earcut = @import("earcut");
pub const gfx = @import("gfx/util.zig");
pub const ResourceManager = @import("resource/ResourceManager.zig");
// Engine exports
pub const App = @import("engine.zig").App;
pub const module = @import("engine.zig").module;
const std = @import("std");
const testing = std.testing;
test {
// TODO: can't reference because they import "app"
// testing.refAllDeclsRecursive(Core);
// testing.refAllDeclsRecursive(Timer);
testing.refAllDeclsRecursive(ResourceManager);
testing.refAllDeclsRecursive(gfx);
std.testing.refAllDeclsRecursive(ResourceManager);
std.testing.refAllDeclsRecursive(gfx);
}

View file

@ -1,42 +1,62 @@
const builtin = @import("builtin");
const Platform = if (builtin.cpu.arch == .wasm32)
Interface(@import("platform/wasm.zig"))
pub usingnamespace if (builtin.cpu.arch == .wasm32)
@import("platform/wasm.zig")
else
Interface(@import("platform/native.zig"));
@import("platform/native.zig");
pub const Type = Platform.Platform;
pub const BackingTimerType = Platform.BackingTimer;
// Verifies that a platform implementation exposes the expected function declarations.
comptime {
assertHasDecl(@This(), "entry");
assertHasDecl(@This(), "Core");
assertHasDecl(@This(), "Timer");
/// Verifies that a Platform implementation exposes the expected function declarations.
fn Interface(comptime T: type) type {
assertHasDecl(T, "Platform");
assertHasDecl(T, "BackingTimer");
assertHasDecl(T.Platform, "init");
assertHasDecl(T.Platform, "deinit");
assertHasDecl(T.Platform, "setOptions");
assertHasDecl(T.Platform, "close");
assertHasDecl(T.Platform, "setWaitEvent");
assertHasDecl(T.Platform, "getFramebufferSize");
assertHasDecl(T.Platform, "getWindowSize");
assertHasDecl(T.Platform, "setMouseCursor");
assertHasDecl(T.Platform, "setCursorMode");
assertHasDecl(T.Platform, "hasEvent");
assertHasDecl(T.Platform, "pollEvent");
assertHasDecl(T.BackingTimer, "start");
assertHasDecl(T.BackingTimer, "read");
assertHasDecl(T.BackingTimer, "reset");
assertHasDecl(T.BackingTimer, "lap");
// Core
assertHasDecl(@This().Core, "init");
assertHasDecl(@This().Core, "deinit");
assertHasDecl(@This().Core, "hasEvent");
assertHasDecl(@This().Core, "pollEvents");
assertHasDecl(@This().Core, "framebufferSize");
return T;
}
assertHasDecl(@This().Core, "setWaitTimeout");
assertHasDecl(@This().Core, "setTitle");
fn assertDecl(comptime T: anytype, comptime name: []const u8, comptime Decl: type) void {
assertHasDecl(T, name);
const FoundDecl = @TypeOf(@field(T, name));
if (FoundDecl != Decl) @compileError("Platform field '" ++ name ++ "'\n\texpected type: " ++ @typeName(Decl) ++ "\n\t found type: " ++ @typeName(FoundDecl));
assertHasDecl(@This().Core, "setDisplayMode");
assertHasDecl(@This().Core, "displayMode");
assertHasDecl(@This().Core, "setBorder");
assertHasDecl(@This().Core, "border");
assertHasDecl(@This().Core, "setHeadless");
assertHasDecl(@This().Core, "headless");
assertHasDecl(@This().Core, "setVSync");
assertHasDecl(@This().Core, "vsync");
assertHasDecl(@This().Core, "setSize");
assertHasDecl(@This().Core, "size");
assertHasDecl(@This().Core, "setSizeLimit");
assertHasDecl(@This().Core, "sizeLimit");
assertHasDecl(@This().Core, "setCursorMode");
assertHasDecl(@This().Core, "cursorMode");
assertHasDecl(@This().Core, "setCursorShape");
assertHasDecl(@This().Core, "cursorShape");
assertHasDecl(@This().Core, "adapter");
assertHasDecl(@This().Core, "device");
assertHasDecl(@This().Core, "swapChain");
assertHasDecl(@This().Core, "descriptor");
// Timer
assertHasDecl(@This().Timer, "start");
assertHasDecl(@This().Timer, "read");
assertHasDecl(@This().Timer, "reset");
assertHasDecl(@This().Timer, "lap");
}
fn assertHasDecl(comptime T: anytype, comptime name: []const u8) void {
if (!@hasDecl(T, name)) @compileError("Platform missing declaration: " ++ name);
if (!@hasDecl(T, name)) @compileError("Core missing declaration: " ++ name);
}

View file

@ -1,36 +0,0 @@
const Core = @import("../Core.zig");
pub fn checkApplication(comptime app_pkg: type) void {
if (!@hasDecl(app_pkg, "App")) {
@compileError("expected e.g. `pub const App = mach.App(modules, init)' (App definition missing in your main Zig file)");
}
const App = app_pkg.App;
if (@typeInfo(App) != .Struct) {
@compileError("App must be a struct type. Found:" ++ @typeName(App));
}
if (@hasDecl(App, "init")) {
const InitFn = @TypeOf(@field(App, "init"));
if (InitFn != fn (app: *App, core: *Core) @typeInfo(@typeInfo(InitFn).Fn.return_type.?).ErrorUnion.error_set!void)
@compileError("expected 'pub fn init(app: *App, core: *mach.Core) !void' found '" ++ @typeName(InitFn) ++ "'");
} else {
@compileError("App must export 'pub fn init(app: *App, core: *mach.Core) !void'");
}
if (@hasDecl(App, "update")) {
const UpdateFn = @TypeOf(@field(App, "update"));
if (UpdateFn != fn (app: *App, core: *Core) @typeInfo(@typeInfo(UpdateFn).Fn.return_type.?).ErrorUnion.error_set!void)
@compileError("expected 'pub fn update(app: *App, core: *mach.Core) !void' found '" ++ @typeName(UpdateFn) ++ "'");
} else {
@compileError("App must export 'pub fn update(app: *App, core: *mach.Core) !void'");
}
if (@hasDecl(App, "deinit")) {
const DeinitFn = @TypeOf(@field(App, "deinit"));
if (DeinitFn != fn (app: *App, core: *Core) void)
@compileError("expected 'pub fn deinit(app: *App, core: *mach.Core) void' found '" ++ @typeName(DeinitFn) ++ "'");
} else {
@compileError("App must export 'pub fn deinit(app: *App, core: *mach.Core) void'");
}
}

View file

@ -1,8 +1,8 @@
const std = @import("std");
const Core = @import("../Core.zig");
const gpu = @import("gpu");
const ecs = @import("ecs");
const glfw = @import("glfw");
const Core = @import("../Core.zig");
const native = @import("native.zig");
pub const App = @This();
@ -11,54 +11,32 @@ pub const GPUInterface = gpu.dawn.Interface;
const _ = gpu.Export(GPUInterface);
// Dummy init, deinit, and update functions
pub fn init(_: *App, _: *Core) !void {}
pub fn deinit(_: *App, _: *Core) void {}
pub fn update(_: *App, _: *Core) !void {}
// Current Limitations:
// 1. Currently, ecs seems to be using some weird compile-time type trickery, so I'm not exactly sure how
// `engine` should be integrated into the C API
// 2. Core might need to expose more state so more API functions can be exposed (for example, the WebGPU API)
// 3. Be very careful about arguments, types, memory, etc - any mismatch will result in undefined behavior
pub export fn mach_core_close(core: *Core) void {
core.close();
}
pub export fn mach_core_delta_time(core: *Core) f32 {
return core.delta_time;
}
pub export fn mach_core_window_should_close(core: *Core) bool {
return core.internal.window.shouldClose();
}
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// Returns a pointer to a newly allocated Core
// Will return a null pointer if an error occurred while initializing Core
pub export fn mach_core_init() ?*Core {
pub export fn mach_core_init() ?*native.Core {
gpu.Impl.init();
const core = native.coreInit(allocator) catch {
return @intToPtr(?*Core, 0);
const core = native.Core.init(allocator, .{}) catch {
return @intToPtr(?*native.Core, 0);
};
return core;
}
pub export fn mach_core_deinit(core: *Core) void {
native.coreDeinit(core, allocator);
pub export fn mach_core_deinit(core: *native.Core) void {
native.Core.deinit(core);
}
pub export fn mach_core_update(core: *Core, resize: ?native.CoreResizeCallback) MachStatus {
native.coreUpdate(core, resize) catch {
return MachStatus.Error;
};
return MachStatus.Success;
}
// pub export fn mach_core_poll_events(core: *native.Core) Core.Event {
// return native.Core.pollEvents(core);
// }
const MachStatus = enum(c_int) {
Success = 0x00000000,

View file

@ -1,367 +0,0 @@
const original_title = document.title;
const text_decoder = new TextDecoder();
const text_encoder = new TextEncoder();
let log_buf = "";
function convertKeyCode(code) {
const mapKeyCode = {
KeyA: 0,
KeyB: 1,
KeyC: 2,
KeyD: 3,
KeyE: 4,
KeyF: 5,
KeyG: 6,
KeyH: 7,
KeyI: 8,
KeyJ: 9,
KeyK: 10,
KeyL: 11,
KeyM: 12,
KeyN: 13,
KeyO: 14,
KeyP: 15,
KeyQ: 16,
KeyR: 17,
KeyS: 18,
KeyT: 19,
KeyU: 20,
KeyV: 21,
KeyW: 22,
KeyX: 23,
KeyY: 24,
KeyZ: 25,
Digit0: 26,
Digit1: 27,
Digit2: 28,
Digit3: 29,
Digit4: 30,
Digit5: 31,
Digit6: 32,
Digit7: 33,
Digit8: 34,
Digit9: 35,
F1: 36,
F2: 37,
F3: 38,
F4: 39,
F5: 40,
F6: 41,
F7: 42,
F8: 43,
F9: 44,
F10: 45,
F11: 46,
F12: 47,
F13: 48,
F14: 49,
F15: 50,
F16: 51,
F17: 52,
F18: 53,
F19: 54,
F20: 55,
F21: 56,
F22: 57,
F23: 58,
F24: 59,
F25: 60,
NumpadDivide: 61,
NumpadMultiply: 62,
NumpadSubtract: 63,
NumpadAdd: 64,
Numpad0: 65,
Numpad1: 66,
Numpad2: 67,
Numpad3: 68,
Numpad4: 69,
Numpad5: 70,
Numpad6: 71,
Numpad7: 72,
Numpad8: 73,
Numpad9: 74,
NumpadDecimal: 75,
NumpadEqual: 76,
NumpadEnter: 77,
Enter: 78,
Escape: 79,
Tab: 80,
ShiftLeft: 81,
ShiftRight: 82,
ControlLeft: 83,
ControlRight: 84,
AltLeft: 85,
AltRight: 86,
OSLeft: 87,
MetaLeft: 87,
OSRight: 88,
MetaRight: 88,
ContextMenu: 89,
NumLock: 90,
CapsLock: 91,
PrintScreen: 92,
ScrollLock: 93,
Pause: 94,
Delete: 95,
Home: 96,
End: 97,
PageUp: 98,
PageDown: 99,
Insert: 100,
ArrowLeft: 101,
ArrowRight: 102,
ArrowUp: 103,
ArrowDown: 104,
Backspace: 105,
Space: 106,
Minus: 107,
Equal: 108,
BracketLeft: 109,
BracketRight: 110,
Backslash: 111,
Semicolon: 112,
Quote: 113,
Comma: 114,
Period: 115,
Slash: 116,
Backquote: 117,
};
const k = mapKeyCode[code];
if (k != undefined)
return k;
return 118; // Unknown
}
const mach = {
canvases: [],
wasm: undefined,
observer: undefined,
events: [],
changes: [],
wait_event_timeout: 0,
init(wasm) {
this.wasm = wasm;
this.observer = new MutationObserver((mutables) => {
mutables.forEach((mutable) => {
if (mutable.type === 'attributes') {
if (mutable.attributeName === "width" || mutable.attributeName === "height") {
mutable.target.dispatchEvent(new Event("mach-canvas-resize"));
}
}
})
})
},
getString(str, len) {
const memory = mach.wasm.exports.memory.buffer;
return text_decoder.decode(new Uint8Array(memory, str, len));
},
setString(str, buf) {
const memory = this.wasm.exports.memory.buffer;
const strbuf = text_encoder.encode(str);
const outbuf = new Uint8Array(memory, buf, strbuf.length);
for (let i = 0; i < strbuf.length; i += 1) {
outbuf[i] = strbuf[i];
}
},
machLogWrite(str, len) {
log_buf += mach.getString(str, len);
},
machLogFlush() {
console.log(log_buf);
log_buf = "";
},
machPanic(str, len) {
throw Error(mach.getString(str, len));
},
machCanvasInit(id) {
let canvas = document.createElement("canvas");
canvas.id = "#mach-canvas-" + mach.canvases.length;
canvas.style.border = "1px solid";
canvas.style.position = "absolute";
canvas.style.display = "block";
canvas.tabIndex = 1;
mach.observer.observe(canvas, { attributes: true });
mach.setString(canvas.id, id);
canvas.addEventListener("contextmenu", (ev) => ev.preventDefault());
canvas.addEventListener("keydown", (ev) => {
if (ev.repeat) {
mach.events.push(...[2, convertKeyCode(ev.code)]);
} else {
mach.events.push(...[1, convertKeyCode(ev.code)]);
}
});
canvas.addEventListener("keyup", (ev) => {
mach.events.push(...[3, convertKeyCode(ev.code)]);
});
canvas.addEventListener("mousemove", (ev) => {
mach.events.push(...[4, ev.clientX, ev.clientY]);
});
canvas.addEventListener("mousedown", (ev) => {
mach.events.push(...[5, ev.button]);
});
canvas.addEventListener("mouseup", (ev) => {
mach.events.push(...[6, ev.button]);
});
canvas.addEventListener("wheel", (ev) => {
mach.events.push(...[7, ev.deltaX, ev.deltaY]);
});
canvas.addEventListener("focus", (ev) => {
mach.events.push(...[8]);
});
canvas.addEventListener("blur", (ev) => {
mach.events.push(...[9]);
});
canvas.addEventListener("mach-canvas-resize", (ev) => {
const cv_index = mach.canvases.findIndex((el) => el.canvas === ev.currentTarget);
const cv = mach.canvases[cv_index];
mach.changes.push(...[1, cv.canvas.width, cv.canvas.height, window.devicePixelRatio]);
});
document.body.appendChild(canvas);
return mach.canvases.push({ canvas: canvas, title: undefined }) - 1;
},
machCanvasDeinit(canvas) {
if (mach.canvases[canvas] != undefined) {
mach.canvases.splice(canvas, 1);
}
},
machCanvasSetTitle(canvas, title, len) {
const str = len > 0 ?
mach.getString(title, len) :
original_title;
mach.canvases[canvas].title = str;
},
machCanvasSetSize(canvas, width, height) {
const cv = mach.canvases[canvas];
if (width > 0 && height > 0) {
cv.canvas.style.width = width + "px";
cv.canvas.style.height = height + "px";
cv.canvas.width = Math.floor(width * window.devicePixelRatio);
cv.canvas.height = Math.floor(height * window.devicePixelRatio);
}
},
machCanvasSetFullscreen(canvas, value) {
const cv = mach.canvases[canvas];
if (value) {
cv.canvas.style.border = "0px";
cv.canvas.style.width = "100%";
cv.canvas.style.height = "100%";
cv.canvas.style.top = "0";
cv.canvas.style.left = "0";
cv.canvas.style.margin = "0px";
} else {
cv.canvas.style.border = "1px solid;"
cv.canvas.style.top = "2px";
cv.canvas.style.left = "2px";
}
},
machCanvasGetWindowWidth(canvas) {
const cv = mach.canvases[canvas];
return cv.canvas.width / window.devicePixelRatio;
},
machCanvasGetWindowHeight(canvas) {
const cv = mach.canvases[canvas];
return cv.canvas.height / window.devicePixelRatio;
},
machCanvasGetFramebufferWidth(canvas) {
const cv = mach.canvases[canvas];
return cv.canvas.width;
},
machCanvasGetFramebufferHeight(canvas) {
const cv = mach.canvases[canvas];
return cv.canvas.height;
},
machEmitCloseEvent() {
window.dispatchEvent(new Event("mach-close"));
},
machSetMouseCursor(cursor_ptr, len) {
let mach_name = mach.getString(cursor_ptr, len);
if (mach_name === 'arrow') document.body.style.cursor = 'default';
else if (mach_name === 'ibeam') document.body.style.cursor = 'text';
else if (mach_name === 'crosshair') document.body.style.cursor = 'crosshair';
else if (mach_name === 'pointing_hand') document.body.style.cursor = 'pointer';
else if (mach_name === 'resize_ew') document.body.style.cursor = 'ew-resize';
else if (mach_name === 'resize_ns') document.body.style.cursor = 'ns-resize';
else if (mach_name === 'resize_nwse') document.body.style.cursor = 'nwse-resize';
else if (mach_name === 'resize_nesw') document.body.style.cursor = 'nesw-resize';
else if (mach_name === 'resize_all') document.body.style.cursor = 'move';
else if (mach_name === 'not_allowed') document.body.style.cursor = 'not-allowed';
else {
console.log("machSetMouseCursor failed for " + mach_name);
}
},
machSetCursorMode(cursor_ptr, len) {
let mach_name = mach.getString(cursor_ptr, len);
if (mach_name === 'normal') document.body.style.cursor = 'default';
else if (mach_name === 'hidden' || mach_name === 'disabled') document.body.style.cursor = 'none';
else {
console.log("machSetMouseCursor failed for " + mach_name);
}
},
machSetWaitEvent(timeout) {
mach.wait_event_timeout = timeout;
},
machHasEvent() {
return (mach.events.length > 0);
},
machEventShift() {
if (mach.events.length === 0)
return 0;
return mach.events.shift();
},
machEventShiftFloat() {
return mach.machEventShift();
},
machChangeShift() {
if (mach.changes.length === 0)
return 0;
return mach.changes.shift();
},
machPerfNow() {
return performance.now();
},
};
export { mach };

View file

@ -1,714 +1,5 @@
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: ?bool,
const EventQueue = std.TailQueue(structs.Event);
const EventNode = EventQueue.Node;
const UserPtr = struct {
platform: *Platform,
};
pub fn init(allocator: std.mem.Allocator, core: *Core) !Platform {
const options = core.options;
const backend_type = try util.detectBackendType(allocator);
defer glfw.clearError();
glfw.setErrorCallback(Platform.errorCallback);
if (!glfw.init(.{})) try glfw.getErrorCode();
// 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 = glfw.Window.create(
options.width,
options.height,
options.title,
null,
null,
hints,
) orelse return glfw.mustGetErrorCode();
if (backend_type == .opengl) glfw.makeContextCurrent(window);
if (backend_type == .opengles) glfw.makeContextCurrent(window);
const window_size = window.getSize();
const framebuffer_size = window.getFramebufferSize();
try glfw.getErrorCode();
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 == .null) {
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 = window.getCursorPos();
try glfw.getErrorCode();
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 = window.getPos(),
.last_cursor_position = .{
.x = cursor_pos.xpos,
.y = cursor_pos.ypos,
},
.instance = instance.?,
.adapter = response.?.adapter,
.linux_gamemode = null,
};
}
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);
}
if (builtin.os.tag == .linux and
platform.linux_gamemode != null and
platform.linux_gamemode.?)
deinitLinuxGamemode();
}
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;
render(pf.core) catch {};
}
}.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,
};
platform.last_position = try platform.window.getPos();
if (options.borderless_window) {
glfw.Window.setAttrib(platform.window, .decorated, false);
try glfw.getErrorCode();
}
if (options.fullscreen) {
var monitor: ?glfw.Monitor = null;
if (options.monitor) |monitorIndex| {
const monitorList = try glfw.Monitor.getAll(platform.allocator);
defer platform.allocator.free(monitorList);
monitor = monitorList[monitorIndex];
} else {
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 close(platform: *Platform) void {
platform.window.setShouldClose(true);
}
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: {s} not yet supported\n", .{@tagName(cursor)});
}
}
pub fn setCursorMode(platform: *Platform, mode: enums.CursorMode) !void {
const glfw_mode: glfw.Window.InputModeCursor = switch (mode) {
.normal => .normal,
.hidden => .hidden,
.disabled => .disabled,
};
try platform.window.setInputModeCursor(glfw_mode);
}
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.ErrorCode, 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();
_ = gpu.Export(GPUInterface);
var core = try coreInit(allocator);
defer coreDeinit(core, allocator);
try app.init(core);
defer app.deinit(core);
while (!core.internal.window.shouldClose()) {
try render(core);
}
}
fn render(core: *Core) !void {
// 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 (builtin.os.tag == .linux and !core.options.is_app and
core.internal.linux_gamemode == null and try activateGamemode(core.allocator))
core.internal.linux_gamemode = initLinuxGamemode();
if (core.internal.wait_event_timeout > 0.0) {
if (core.internal.wait_event_timeout == std.math.inf(f64)) {
// Wait for an event
glfw.waitEvents();
} else {
// Wait for an event with a timeout
glfw.waitEventsTimeout(core.internal.wait_event_timeout);
}
} else {
// Don't wait for events
glfw.pollEvents();
}
try glfw.getErrorCode();
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)) and !(core.target_desc.width == 0 or core.target_desc.height == 0)) {
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 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,
};
}
fn glfwSizeOptional(size: structs.SizeOptional) glfw.Window.SizeOptional {
return .{
.width = size.width,
.height = size.height,
};
}
/// Check if gamemode should be activated
fn activateGamemode(allocator: std.mem.Allocator) error{ OutOfMemory, InvalidUtf8 }!bool {
if (try getEnvVarOwned(allocator, "MACH_USE_GAMEMODE")) |env| {
defer allocator.free(env);
return !(std.ascii.eqlIgnoreCase(env, "off") or std.ascii.eqlIgnoreCase(env, "false"));
}
return true;
}
fn initLinuxGamemode() bool {
const gamemode = @import("gamemode");
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;
}
fn deinitLinuxGamemode() void {
const gamemode = @import("gamemode");
gamemode.requestEnd() catch |err| {
std.log.err("Gamemode error {} -> {s}", .{ err, gamemode.errorString() });
};
}
pub const entry = @import("native/entry.zig");
pub const Core = @import("native/Core.zig");
pub const Timer = std.time.Timer;

View file

@ -0,0 +1,767 @@
const builtin = @import("builtin");
const std = @import("std");
const gpu = @import("gpu");
const glfw = @import("glfw");
const util = @import("util.zig");
const Options = @import("../../Core.zig").Options;
const Event = @import("../../Core.zig").Event;
const KeyEvent = @import("../../Core.zig").KeyEvent;
const MouseButtonEvent = @import("../../Core.zig").MouseButtonEvent;
const MouseButton = @import("../../Core.zig").MouseButton;
const Size = @import("../../Core.zig").Size;
const DisplayMode = @import("../../Core.zig").DisplayMode;
const SizeLimit = @import("../../Core.zig").SizeLimit;
const CursorShape = @import("../../Core.zig").CursorShape;
const VSyncMode = @import("../../Core.zig").VSyncMode;
const CursorMode = @import("../../Core.zig").CursorMode;
const Key = @import("../../Core.zig").Key;
const KeyMods = @import("../../Core.zig").KeyMods;
pub const Core = @This();
allocator: std.mem.Allocator,
window: glfw.Window,
backend_type: gpu.BackendType,
user_ptr: UserPtr,
instance: *gpu.Instance,
surface: *gpu.Surface,
gpu_adapter: *gpu.Adapter,
gpu_device: *gpu.Device,
swap_chain: *gpu.SwapChain,
swap_chain_desc: gpu.SwapChain.Descriptor,
events: EventQueue,
wait_timeout: f64,
last_size: glfw.Window.Size,
last_pos: glfw.Window.Pos,
size_limit: SizeLimit,
frame_buffer_resized: bool,
current_cursor: CursorShape,
cursors: [@typeInfo(CursorShape).Enum.fields.len]?glfw.Cursor,
cursors_tried: [@typeInfo(CursorShape).Enum.fields.len]bool,
linux_gamemode: ?bool,
const EventQueue = std.TailQueue(Event);
const EventNode = EventQueue.Node;
const UserPtr = struct {
self: *Core,
};
pub fn init(allocator: std.mem.Allocator, options: Options) !*Core {
const backend_type = try util.detectBackendType(allocator);
glfw.setErrorCallback(errorCallback);
if (!glfw.init(.{}))
glfw.getErrorCode() catch |err| switch (err) {
error.PlatformError,
error.PlatformUnavailable,
=> return err,
else => unreachable,
};
// 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 = glfw.Window.create(
options.size.width,
options.size.height,
options.title,
null,
null,
hints,
) orelse switch (glfw.mustGetErrorCode()) {
error.InvalidEnum,
error.InvalidValue,
error.FormatUnavailable,
=> unreachable,
error.APIUnavailable,
error.VersionUnavailable,
error.PlatformError,
=> |err| return err,
else => unreachable,
};
switch (backend_type) {
.opengl, .opengles => {
glfw.makeContextCurrent(window);
glfw.getErrorCode() catch |err| switch (err) {
error.PlatformError => return err,
else => unreachable,
};
},
else => {},
}
const instance = gpu.createInstance(null) orelse {
std.log.err("mach: failed to create GPU instance", .{});
std.process.exit(1);
};
const surface = util.createSurfaceForWindow(instance, window, comptime util.detectGLFWOptions());
var response: util.RequestAdapterResponse = undefined;
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}", .{response.message});
std.log.info("-> maybe try MACH_GPU_BACKEND=opengl ?", .{});
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 == .null) {
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 gpu_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(?[*]const gpu.FeatureName, v.ptr) else null,
.required_limits = if (options.required_limits) |limits| @as(?*gpu.RequiredLimits, &gpu.RequiredLimits{
.limits = limits,
}) else null,
}) orelse {
std.log.err("mach: failed to create GPU device\n", .{});
std.process.exit(1);
};
gpu_device.setUncapturedErrorCallback({}, util.printUnhandledErrorCallback);
const framebuffer_size = window.getFramebufferSize();
const swap_chain_desc = gpu.SwapChain.Descriptor{
.label = "main swap chain",
.usage = .{ .render_attachment = true },
.format = .bgra8_unorm,
.width = framebuffer_size.width,
.height = framebuffer_size.height,
.present_mode = .fifo,
};
const swap_chain = gpu_device.createSwapChain(surface, &swap_chain_desc);
const self: *Core = try allocator.create(Core);
errdefer allocator.destroy(self);
self.* = .{
.allocator = allocator,
.window = window,
.backend_type = backend_type,
.user_ptr = undefined,
.instance = instance,
.surface = surface,
.gpu_adapter = response.adapter,
.gpu_device = gpu_device,
.swap_chain = swap_chain,
.swap_chain_desc = swap_chain_desc,
.events = .{},
.wait_timeout = 0.0,
.last_size = window.getSize(),
.last_pos = window.getPos(),
.size_limit = .{
.min = .{ .width = 350, .height = 350 },
.max = .{ .width = null, .height = null },
},
.frame_buffer_resized = false,
.current_cursor = .arrow,
.cursors = std.mem.zeroes([@typeInfo(CursorShape).Enum.fields.len]?glfw.Cursor),
.cursors_tried = std.mem.zeroes([@typeInfo(CursorShape).Enum.fields.len]bool),
.linux_gamemode = null,
};
self.setSizeLimit(self.size_limit);
self.initCallbacks();
if (builtin.os.tag == .linux and !options.is_app and
self.linux_gamemode == null and try activateGamemode(self.allocator))
self.linux_gamemode = initLinuxGamemode();
return self;
}
fn initCallbacks(self: *Core) void {
self.user_ptr = UserPtr{ .self = self };
self.window.setUserPointer(&self.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).self;
const key_event = 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;
self.window.setKeyCallback(key_callback);
const char_callback = struct {
fn callback(window: glfw.Window, codepoint: u21) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).self;
pf.pushEvent(.{
.char_input = .{
.codepoint = codepoint,
},
});
}
}.callback;
self.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).self;
pf.pushEvent(.{
.mouse_motion = .{
.pos = .{
.x = xpos,
.y = ypos,
},
},
});
}
}.callback;
self.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).self;
const cursor_pos = pf.window.getCursorPos();
const mouse_button_event = MouseButtonEvent{
.button = toMachButton(button),
.pos = .{ .x = cursor_pos.xpos, .y = cursor_pos.ypos },
.mods = toMachMods(mods),
};
switch (action) {
.press => pf.pushEvent(.{ .mouse_press = mouse_button_event }),
.release => pf.pushEvent(.{
.mouse_release = mouse_button_event,
}),
else => {},
}
}
}.callback;
self.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).self;
pf.pushEvent(.{
.mouse_scroll = .{
.xoffset = @floatCast(f32, xoffset),
.yoffset = @floatCast(f32, yoffset),
},
});
}
}.callback;
self.window.setScrollCallback(scroll_callback);
const focus_callback = struct {
fn callback(window: glfw.Window, focused: bool) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).self;
pf.pushEvent(if (focused) .focus_gained else .focus_lost);
}
}.callback;
self.window.setFocusCallback(focus_callback);
const framebuffer_size_callback = struct {
fn callback(window: glfw.Window, _: u32, _: u32) void {
const pf = (window.getUserPointer(UserPtr) orelse unreachable).self;
pf.frame_buffer_resized = true;
}
}.callback;
self.window.setFramebufferSizeCallback(framebuffer_size_callback);
}
fn pushEvent(self: *Core, event: Event) void {
const node = self.allocator.create(EventNode) catch unreachable;
node.* = .{ .data = event };
self.events.append(node);
}
pub fn deinit(self: *Core) void {
for (self.cursors) |glfw_cursor| {
if (glfw_cursor) |cur| {
cur.destroy();
}
}
while (self.events.popFirst()) |ev| {
self.allocator.destroy(ev);
}
if (builtin.os.tag == .linux and
self.linux_gamemode != null and
self.linux_gamemode.?)
deinitLinuxGamemode();
self.allocator.destroy(self);
}
pub fn hasEvent(self: *Core) bool {
return self.events.first != null;
}
pub fn pollEvents(self: *Core) ?Event {
if (self.wait_timeout > 0.0) {
if (self.wait_timeout == std.math.inf(f64)) {
// Wait for an event
glfw.waitEvents();
} else {
// Wait for an event with a timeout
glfw.waitEventsTimeout(self.wait_timeout);
}
} else {
// Don't wait for events
glfw.pollEvents();
}
glfw.getErrorCode() catch |err| switch (err) {
error.PlatformError => std.log.err("glfw: failed to poll events", .{}),
error.InvalidValue => unreachable,
else => unreachable,
};
if (self.frame_buffer_resized) blk: {
self.frame_buffer_resized = false;
const framebuffer_size = self.window.getFramebufferSize();
glfw.getErrorCode() catch break :blk;
if (framebuffer_size.width != 0 and framebuffer_size.height != 0) {
self.swap_chain_desc.width = framebuffer_size.width;
self.swap_chain_desc.height = framebuffer_size.height;
self.swap_chain = self.gpu_device.createSwapChain(self.surface, &self.swap_chain_desc);
self.pushEvent(.{
.framebuffer_resize = .{
.width = framebuffer_size.width,
.height = framebuffer_size.height,
},
});
}
}
if (self.window.shouldClose()) {
self.pushEvent(.close);
}
if (self.events.popFirst()) |n| {
const data = n.data;
self.allocator.destroy(n);
return data;
}
return null;
}
pub fn shouldClose(self: *Core) bool {
return self.window.shouldClose();
}
pub fn framebufferSize(self: *Core) Size {
const framebuffer_size = self.window.getFramebufferSize();
return .{
.width = framebuffer_size.width,
.height = framebuffer_size.height,
};
}
pub fn setWaitTimeout(self: *Core, timeout: f64) void {
self.wait_timeout = timeout;
}
pub fn setTitle(self: *Core, title: [:0]const u8) void {
self.window.setTitle(title);
}
pub fn setDisplayMode(self: *Core, mode: DisplayMode, monitor_index: ?usize) !void {
switch (mode) {
.windowed => {
try self.window.setMonitor(
null,
@intCast(i32, self.last_pos.x),
@intCast(i32, self.last_pos.y),
self.last_size.width,
self.last_size.height,
null,
);
},
.fullscreen => {
if (try self.displayMode() == .windowed) {
self.last_size = try self.window.getSize();
self.last_pos = try self.window.getPos();
}
const monitor = blk: {
if (monitor_index) |i| {
const monitor_list = try glfw.Monitor.getAll(self.allocator);
defer self.allocator.free(monitor_list);
break :blk monitor_list[i];
}
break :blk glfw.Monitor.getPrimary();
};
const video_mode = try monitor.?.getVideoMode();
try self.window.setMonitor(monitor, 0, 0, video_mode.getWidth(), video_mode.getHeight(), null);
},
}
}
pub fn displayMode(self: *Core) DisplayMode {
if (self.window.getMonitor()) |_| {
return .fullscreen;
} else {
return .windowed;
}
}
pub fn setBorder(self: *Core, value: bool) void {
self.window.setAttrib(.decorated, value);
}
pub fn border(self: *Core) !bool {
const decorated = try self.window.getAttrib(.decorated);
return decorated == 1;
}
pub fn setHeadless(self: *Core, value: bool) void {
if (value) {
self.window.hide();
} else {
self.window.show();
}
}
pub fn headless(self: *Core) bool {
const visible = self.window.getAttrib(.visible);
return visible == 0;
}
pub fn setVSync(self: *Core, mode: VSyncMode) void {
self.swap_chain_desc.present_mode = switch (mode) {
.none => .immediate,
.double => .fifo,
.triple => .mailbox,
};
}
pub fn vsync(self: *Core) VSyncMode {
return switch (self.swap_chain_desc.present_mode) {
.immediate => .none,
.fifo => .double,
.mailbox => .triple,
};
}
pub fn setSize(self: *Core, value: Size) void {
self.window.setSize(.{
.width = value.width,
.height = value.height,
});
}
pub fn size(self: *Core) Size {
const window_size = self.window.getSize();
return .{ .width = window_size.width, .height = window_size.height };
}
pub fn setSizeLimit(self: *Core, limit: SizeLimit) void {
self.window.setSizeLimits(
.{ .width = limit.min.width, .height = limit.min.height },
.{ .width = limit.max.width, .height = limit.max.height },
);
self.size_limit = limit;
}
pub fn sizeLimit(self: *Core) SizeLimit {
return self.size_limit;
}
pub fn setCursorMode(self: *Core, mode: CursorMode) void {
const glfw_mode: glfw.Window.InputModeCursor = switch (mode) {
.normal => .normal,
.hidden => .hidden,
.disabled => .disabled,
};
self.window.setInputModeCursor(glfw_mode);
}
pub fn cursorMode(self: *Core) CursorMode {
const glfw_mode = self.window.getInputModeCursor();
return switch (glfw_mode) {
.normal => .normal,
.hidden => .hidden,
.disabled => .disabled,
};
}
pub fn setCursorShape(self: *Core, cursor: CursorShape) 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 = self.cursors_tried[enum_int];
if (!tried) {
self.cursors_tried[enum_int] = true;
self.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 (self.cursors[enum_int]) |cur| {
self.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: setCursorShape: {s} not yet supported\n", .{@tagName(cursor)});
}
self.current_cursor = cursor;
}
pub fn cursorShape(self: *Core) CursorShape {
return self.current_cursor;
}
pub fn adapter(self: *Core) *gpu.Adapter {
return self.gpu_adapter;
}
pub fn device(self: *Core) *gpu.Device {
return self.gpu_device;
}
pub fn swapChain(self: *Core) *gpu.SwapChain {
return self.swap_chain;
}
pub fn descriptor(self: *Core) gpu.SwapChain.Descriptor {
return self.swap_chain_desc;
}
fn toMachButton(button: glfw.mouse_button.MouseButton) 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) 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) 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.ErrorCode, description: [:0]const u8) void {
std.log.err("glfw: {}: {s}\n", .{ error_code, description });
}
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,
};
}
/// Check if gamemode should be activated
fn activateGamemode(allocator: std.mem.Allocator) error{ OutOfMemory, InvalidUtf8 }!bool {
if (try getEnvVarOwned(allocator, "MACH_USE_GAMEMODE")) |env| {
defer allocator.free(env);
return !(std.ascii.eqlIgnoreCase(env, "off") or std.ascii.eqlIgnoreCase(env, "false"));
}
return true;
}
fn initLinuxGamemode() bool {
const gamemode = @import("gamemode");
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;
}
fn deinitLinuxGamemode() void {
const gamemode = @import("gamemode");
gamemode.requestEnd() catch |err| {
std.log.err("Gamemode error {} -> {s}", .{ err, gamemode.errorString() });
};
}

View file

@ -0,0 +1,22 @@
const std = @import("std");
const gpu = @import("gpu");
const App = @import("app").App;
const util = @import("util.zig");
pub const GPUInterface = gpu.dawn.Interface;
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 fn main() !void {
gpu.Impl.init();
_ = gpu.Export(GPUInterface);
var app: App = undefined;
try app.init();
defer app.deinit();
while (true) {
const pool = try util.AutoReleasePool.init();
defer util.AutoReleasePool.release(pool);
if (try app.update()) return;
}
}

View file

@ -0,0 +1,7 @@
// Extracted from `zig translate-c tmp.c` with `#include <objc/message.h>` in the file.
pub const SEL = opaque {};
pub const Class = opaque {};
pub extern fn sel_getUid(str: [*c]const u8) ?*SEL;
pub extern fn objc_getClass(name: [*c]const u8) ?*Class;
pub extern fn objc_msgSend() void;

View file

@ -49,7 +49,7 @@ pub const RequestAdapterResponse = struct {
};
pub inline fn requestAdapterCallback(
context: *?RequestAdapterResponse,
context: *RequestAdapterResponse,
status: gpu.RequestAdapterStatus,
adapter: *gpu.Adapter,
message: ?[*:0]const u8,
@ -168,11 +168,11 @@ pub fn msgSend(obj: anytype, sel_name: [:0]const u8, args: anytype, comptime Ret
const args_meta = @typeInfo(@TypeOf(args)).Struct.fields;
const FnType = switch (args_meta.len) {
0 => *const fn (@TypeOf(obj), objc.SEL) callconv(.C) ReturnType,
1 => *const fn (@TypeOf(obj), objc.SEL, args_meta[0].type) callconv(.C) ReturnType,
2 => *const fn (@TypeOf(obj), objc.SEL, args_meta[0].type, args_meta[1].type) callconv(.C) ReturnType,
3 => *const fn (@TypeOf(obj), objc.SEL, args_meta[0].type, args_meta[1].type, args_meta[2].type) callconv(.C) ReturnType,
4 => *const fn (@TypeOf(obj), objc.SEL, args_meta[0].type, args_meta[1].type, args_meta[2].type, args_meta[3].type) callconv(.C) ReturnType,
0 => *const fn (@TypeOf(obj), ?*objc.SEL) callconv(.C) ReturnType,
1 => *const fn (@TypeOf(obj), ?*objc.SEL, args_meta[0].type) callconv(.C) ReturnType,
2 => *const fn (@TypeOf(obj), ?*objc.SEL, args_meta[0].type, args_meta[1].type) callconv(.C) ReturnType,
3 => *const fn (@TypeOf(obj), ?*objc.SEL, args_meta[0].type, args_meta[1].type, args_meta[2].type) callconv(.C) ReturnType,
4 => *const fn (@TypeOf(obj), ?*objc.SEL, args_meta[0].type, args_meta[1].type, args_meta[2].type, args_meta[3].type) callconv(.C) ReturnType,
else => @compileError("Unsupported number of args"),
};

View file

@ -1,9 +0,0 @@
// Extracted from `zig translate-c tmp.c` with `#include <objc/message.h>` in the file.
pub const struct_objc_selector = opaque {};
pub const SEL = ?*struct_objc_selector;
pub const Class = ?*struct_objc_class;
pub const struct_objc_class = opaque {};
pub extern fn sel_getUid(str: [*c]const u8) SEL;
pub extern fn objc_getClass(name: [*c]const u8) Class;
pub extern fn objc_msgSend() void;

View file

@ -1,349 +1,3 @@
const std = @import("std");
const app_pkg = @import("app");
const Core = @import("../Core.zig");
const structs = @import("../structs.zig");
const enums = @import("../enums.zig");
const gpu = @import("gpu");
const js = struct {
extern "mach" fn machCanvasInit(selector_id: *u8) CanvasId;
extern "mach" fn machCanvasDeinit(canvas: CanvasId) void;
extern "mach" fn machCanvasSetTitle(canvas: CanvasId, title: [*]const u8, len: u32) void;
extern "mach" fn machCanvasSetSize(canvas: CanvasId, width: u32, height: u32) void;
extern "mach" fn machCanvasSetFullscreen(canvas: CanvasId, value: bool) void;
extern "mach" fn machCanvasGetWindowWidth(canvas: CanvasId) u32;
extern "mach" fn machCanvasGetWindowHeight(canvas: CanvasId) u32;
extern "mach" fn machCanvasGetFramebufferWidth(canvas: CanvasId) u32;
extern "mach" fn machCanvasGetFramebufferHeight(canvas: CanvasId) u32;
extern "mach" fn machSetMouseCursor(cursor_name: [*]const u8, len: u32) void;
extern "mach" fn machEmitCloseEvent() void;
extern "mach" fn machSetWaitEvent(timeout: f64) void;
extern "mach" fn machHasEvent() bool;
extern "mach" fn machEventShift() i32;
extern "mach" fn machEventShiftFloat() f64;
extern "mach" fn machChangeShift() u32;
extern "mach" fn machPerfNow() f64;
extern "mach" fn machLog(str: [*]const u8, len: u32) void;
extern "mach" fn machLogWrite(str: [*]const u8, len: u32) void;
extern "mach" fn machLogFlush() void;
extern "mach" fn machPanic(str: [*]const u8, len: u32) void;
};
const common = @import("common.zig");
comptime {
common.checkApplication(app_pkg);
}
const App = app_pkg.App;
pub const GPUInterface = gpu.StubInterface;
pub const CanvasId = u32;
pub const Platform = struct {
id: CanvasId,
selector_id: []const u8,
allocator: std.mem.Allocator,
last_window_size: structs.Size,
last_framebuffer_size: structs.Size,
last_cursor_position: structs.WindowPos,
last_key_mods: structs.KeyMods,
pub fn init(allocator: std.mem.Allocator, eng: *Core) !Platform {
var selector = [1]u8{0} ** 15;
const id = js.machCanvasInit(&selector[0]);
var platform = Platform{
.id = id,
.selector_id = try allocator.dupe(u8, selector[0 .. selector.len - @as(u32, if (selector[selector.len - 1] == 0) 1 else 0)]),
.allocator = allocator,
.last_window_size = .{
.width = js.machCanvasGetWindowWidth(id),
.height = js.machCanvasGetWindowHeight(id),
},
.last_framebuffer_size = .{
.width = js.machCanvasGetFramebufferWidth(id),
.height = js.machCanvasGetFramebufferHeight(id),
},
// TODO initialize these properly
.last_cursor_position = .{
.x = 0,
.y = 0,
},
.last_key_mods = .{
.shift = false,
.control = false,
.alt = false,
.super = false,
.caps_lock = false,
.num_lock = false,
},
};
try platform.setOptions(eng.options);
return platform;
}
pub fn deinit(platform: *Platform) void {
js.machCanvasDeinit(platform.id);
platform.allocator.free(platform.selector_id);
}
pub fn setOptions(platform: *Platform, options: structs.Options) !void {
// NOTE: size limits do not exists on wasm
js.machCanvasSetSize(platform.id, options.width, options.height);
const title = std.mem.span(options.title);
js.machCanvasSetTitle(platform.id, title.ptr, title.len);
js.machCanvasSetFullscreen(platform.id, options.fullscreen);
}
pub fn close(_: *Platform) void {
js.machEmitCloseEvent();
}
pub fn setWaitEvent(_: *Platform, timeout: f64) void {
js.machSetWaitEvent(timeout);
}
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, cursor: enums.MouseCursor) !void {
const cursor_name = @tagName(cursor);
js.machSetMouseCursor(cursor_name.ptr, cursor_name.len);
}
pub fn setCursorMode(_: *Platform, mode: enums.CursorMode) !void {
const mode_name = @tagName(mode);
js.machSetCursorMode(mode_name.ptr, mode_name.len);
}
fn pollChanges(platform: *Platform) void {
const change_type = js.machChangeShift();
switch (change_type) {
1 => {
const width = js.machChangeShift();
const height = js.machChangeShift();
const device_pixel_ratio = js.machChangeShift();
platform.last_window_size = .{
.width = @divFloor(width, device_pixel_ratio),
.height = @divFloor(height, device_pixel_ratio),
};
platform.last_framebuffer_size = .{
.width = width,
.height = height,
};
},
else => {},
}
}
pub fn hasEvent(_: *Platform) bool {
return js.machHasEvent();
}
pub fn pollEvent(platform: *Platform) ?structs.Event {
const event_type = js.machEventShift();
return switch (event_type) {
1, 2 => key_down: {
const key = @intToEnum(enums.Key, js.machEventShift());
switch (key) {
.left_shift, .right_shift => platform.last_key_mods.shift = true,
.left_control, .right_control => platform.last_key_mods.control = true,
.left_alt, .right_alt => platform.last_key_mods.alt = true,
.left_super, .right_super => platform.last_key_mods.super = true,
.caps_lock => platform.last_key_mods.caps_lock = true,
.num_lock => platform.last_key_mods.num_lock = true,
else => {},
}
break :key_down switch (event_type) {
1 => structs.Event{
.key_press = .{
.key = key,
.mods = platform.last_key_mods,
},
},
2 => structs.Event{
.key_repeat = .{
.key = key,
.mods = platform.last_key_mods,
},
},
else => unreachable,
};
},
3 => key_release: {
const key = @intToEnum(enums.Key, js.machEventShift());
switch (key) {
.left_shift, .right_shift => platform.last_key_mods.shift = false,
.left_control, .right_control => platform.last_key_mods.control = false,
.left_alt, .right_alt => platform.last_key_mods.alt = false,
.left_super, .right_super => platform.last_key_mods.super = false,
.caps_lock => platform.last_key_mods.caps_lock = false,
.num_lock => platform.last_key_mods.num_lock = false,
else => {},
}
break :key_release structs.Event{
.key_release = .{
.key = key,
.mods = platform.last_key_mods,
},
};
},
4 => mouse_motion: {
const x = @intToFloat(f64, js.machEventShift());
const y = @intToFloat(f64, js.machEventShift());
platform.last_cursor_position = .{
.x = x,
.y = y,
};
break :mouse_motion structs.Event{
.mouse_motion = .{
.pos = .{
.x = x,
.y = y,
},
},
};
},
5 => structs.Event{
.mouse_press = .{
.button = toMachButton(js.machEventShift()),
.pos = platform.last_cursor_position,
.mods = platform.last_key_mods,
},
},
6 => structs.Event{
.mouse_release = .{
.button = toMachButton(js.machEventShift()),
.pos = platform.last_cursor_position,
.mods = platform.last_key_mods,
},
},
7 => structs.Event{
.mouse_scroll = .{
.xoffset = @floatCast(f32, sign(js.machEventShiftFloat())),
.yoffset = @floatCast(f32, sign(js.machEventShiftFloat())),
},
},
8 => structs.Event.focus_gained,
9 => structs.Event.focus_lost,
else => null,
};
}
inline fn sign(val: f64) f64 {
return if (val == 0.0) 0.0 else -val;
}
fn toMachButton(button: i32) enums.MouseButton {
return switch (button) {
0 => .left,
1 => .middle,
2 => .right,
3 => .four,
4 => .five,
else => unreachable,
};
}
};
pub const BackingTimer = struct {
initial: f64 = undefined,
const WasmTimer = @This();
pub fn start() !WasmTimer {
return WasmTimer{ .initial = js.machPerfNow() };
}
pub fn read(timer: *WasmTimer) u64 {
return timeToNs(js.machPerfNow() - timer.initial);
}
pub fn reset(timer: *WasmTimer) void {
timer.initial = js.machPerfNow();
}
pub fn lap(timer: *WasmTimer) u64 {
const now = js.machPerfNow();
const initial = timer.initial;
timer.initial = now;
return timeToNs(now - initial);
}
fn timeToNs(t: f64) u64 {
return @floatToInt(u64, t) * 1000000;
}
};
var app: App = undefined;
var core: Core = undefined;
export fn wasmInit() void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
Core.init(allocator, &core) catch unreachable;
app.init(&core) catch {};
}
export fn wasmUpdate() void {
// Poll internal events, like resize
core.internal.pollChanges();
core.delta_time_ns = core.timer.lapPrecise();
core.delta_time = @intToFloat(f32, core.delta_time_ns) / @intToFloat(f32, std.time.ns_per_s);
app.update(&core) catch core.close();
}
export fn wasmDeinit() void {
app.deinit(&core);
core.internal.deinit();
}
pub const log_level = if (@hasDecl(App, "log_level")) App.log_level else std.log.default_level;
pub const scope_levels = if (@hasDecl(App, "scope_levels")) App.scope_levels else [0]std.log.ScopeLevel{};
const LogError = error{};
const LogWriter = std.io.Writer(void, LogError, writeLog);
fn writeLog(_: void, msg: []const u8) LogError!usize {
js.machLogWrite(msg.ptr, msg.len);
return msg.len;
}
pub fn log(
comptime message_level: std.log.Level,
comptime scope: @Type(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
const writer = LogWriter{ .context = {} };
writer.print(message_level.asText() ++ prefix ++ format ++ "\n", args) catch return;
js.machLogFlush();
}
pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
_ = error_return_trace;
_ = ret_addr;
js.machPanic(msg.ptr, msg.len);
unreachable;
}
pub const Core = @import("wasm/Core.zig");
pub const Timer = @import("wasm/Timer.zig");
pub const entry = @import("wasm/entry.zig");

299
src/platform/wasm/Core.zig Normal file
View file

@ -0,0 +1,299 @@
const std = @import("std");
const gpu = @import("gpu");
const js = @import("js.zig");
const Timer = @import("Timer.zig");
const Options = @import("../../Core.zig").Options;
const Event = @import("../../Core.zig").Event;
const KeyEvent = @import("../../Core.zig").KeyEvent;
const MouseButtonEvent = @import("../../Core.zig").MouseButtonEvent;
const MouseButton = @import("../../Core.zig").MouseButton;
const Size = @import("../../Core.zig").Size;
const Position = @import("../../Core.zig").Position;
const DisplayMode = @import("../../Core.zig").DisplayMode;
const SizeLimit = @import("../../Core.zig").SizeLimit;
const CursorShape = @import("../../Core.zig").CursorShape;
const VSyncMode = @import("../../Core.zig").VSyncMode;
const CursorMode = @import("../../Core.zig").CursorMode;
const Key = @import("../../Core.zig").Key;
const KeyMods = @import("../../Core.zig").KeyMods;
pub const Core = @This();
allocator: std.mem.Allocator,
id: js.CanvasId,
last_cursor_position: Position,
last_key_mods: KeyMods,
pub fn init(allocator: std.mem.Allocator, options: Options) !*Core {
_ = options;
var selector = [1]u8{0} ** 15;
const id = js.machCanvasInit(&selector[0]);
const self: *Core = try allocator.create(Core);
errdefer allocator.destroy(self);
self.* = Core{
.allocator = allocator,
.id = id,
// TODO initialize these properly
.last_cursor_position = .{
.x = 0,
.y = 0,
},
.last_key_mods = .{
.shift = false,
.control = false,
.alt = false,
.super = false,
.caps_lock = false,
.num_lock = false,
},
};
return self;
}
pub fn deinit(self: *Core) void {
js.machCanvasDeinit(self.id);
}
pub fn hasEvent(_: *Core) bool {
return js.machHasEvent();
}
pub fn pollEvents(self: *Core) ?Event {
const event_int = js.machEventShift();
if (event_int == -1) return null;
const event_type = @intToEnum(std.meta.Tag(Event), event_int);
return switch (event_type) {
.key_press, .key_repeat => blk: {
const key = @intToEnum(Key, js.machEventShift());
switch (key) {
.left_shift, .right_shift => self.last_key_mods.shift = true,
.left_control, .right_control => self.last_key_mods.control = true,
.left_alt, .right_alt => self.last_key_mods.alt = true,
.left_super, .right_super => self.last_key_mods.super = true,
.caps_lock => self.last_key_mods.caps_lock = true,
.num_lock => self.last_key_mods.num_lock = true,
else => {},
}
break :blk switch (event_type) {
.key_press => Event{
.key_press = .{
.key = key,
.mods = self.last_key_mods,
},
},
.key_repeat => Event{
.key_repeat = .{
.key = key,
.mods = self.last_key_mods,
},
},
else => unreachable,
};
},
.key_release => blk: {
const key = @intToEnum(Key, js.machEventShift());
switch (key) {
.left_shift, .right_shift => self.last_key_mods.shift = false,
.left_control, .right_control => self.last_key_mods.control = false,
.left_alt, .right_alt => self.last_key_mods.alt = false,
.left_super, .right_super => self.last_key_mods.super = false,
.caps_lock => self.last_key_mods.caps_lock = false,
.num_lock => self.last_key_mods.num_lock = false,
else => {},
}
break :blk Event{
.key_release = .{
.key = key,
.mods = self.last_key_mods,
},
};
},
.mouse_motion => blk: {
const x = @intToFloat(f64, js.machEventShift());
const y = @intToFloat(f64, js.machEventShift());
self.last_cursor_position = .{
.x = x,
.y = y,
};
break :blk Event{
.mouse_motion = .{
.pos = .{
.x = x,
.y = y,
},
},
};
},
.mouse_press => Event{
.mouse_press = .{
.button = toMachButton(js.machEventShift()),
.pos = self.last_cursor_position,
.mods = self.last_key_mods,
},
},
.mouse_release => Event{
.mouse_release = .{
.button = toMachButton(js.machEventShift()),
.pos = self.last_cursor_position,
.mods = self.last_key_mods,
},
},
.mouse_scroll => Event{
.mouse_scroll = .{
.xoffset = @floatCast(f32, std.math.sign(js.machEventShiftFloat())),
.yoffset = @floatCast(f32, std.math.sign(js.machEventShiftFloat())),
},
},
.framebuffer_resize => blk: {
const width = @intCast(u32, js.machEventShift());
const height = @intCast(u32, js.machEventShift());
const pixel_ratio = @intCast(u32, js.machEventShift());
break :blk Event{
.framebuffer_resize = .{
.width = width * pixel_ratio,
.height = height * pixel_ratio,
},
};
},
.focus_gained => Event.focus_gained,
.focus_lost => Event.focus_lost,
else => null,
};
}
pub fn framebufferSize(self: *Core) Size {
return .{
.width = js.machCanvasFramebufferWidth(self.id),
.height = js.machCanvasFramebufferHeight(self.id),
};
}
pub fn setWaitTimeout(_: *Core, timeout: f64) void {
js.machSetWaitTimeout(timeout);
}
pub fn setTitle(self: *Core, title: [:0]const u8) void {
js.machCanvasSetTitle(self.id, title.ptr, title.len);
}
pub fn setDisplayMode(self: *Core, mode: DisplayMode, monitor: ?usize) void {
_ = monitor;
js.machCanvasSetDisplayMode(self.id, @enumToInt(mode));
}
pub fn displayMode(self: *Core) DisplayMode {
return @intToEnum(DisplayMode, js.machDisplayMode(self.id));
}
pub fn setBorder(self: *Core, value: bool) void {
_ = self;
_ = value;
}
pub fn border(self: *Core) bool {
_ = self;
return false;
}
pub fn setHeadless(self: *Core, value: bool) void {
_ = self;
_ = value;
}
pub fn headless(self: *Core) bool {
_ = self;
return false;
}
pub fn setVSync(self: *Core, mode: VSyncMode) void {
_ = self;
_ = mode;
}
// TODO: https://github.com/gpuweb/gpuweb/issues/1224
pub fn vsync(self: *Core) VSyncMode {
_ = self;
return .double;
}
pub fn setSize(self: *Core, value: Size) void {
js.machCanvasSetSize(self.id, value.width, value.height);
}
pub fn size(self: *Core) Size {
return .{
.width = js.machCanvasWidth(self.id),
.height = js.machCanvasHeight(self.id),
};
}
pub fn setSizeLimit(self: *Core, limit: SizeLimit) void {
js.machCanvasSetSizeLimit(
self.id,
if (limit.min.width) |val| @intCast(i32, val) else -1,
if (limit.min.height) |val| @intCast(i32, val) else -1,
if (limit.max.width) |val| @intCast(i32, val) else -1,
if (limit.max.height) |val| @intCast(i32, val) else -1,
);
}
pub fn sizeLimit(self: *Core) SizeLimit {
return .{
.min = .{
.width = js.machCanvasMinWidth(self.id),
.height = js.machCanvasMinHeight(self.id),
},
.max = .{
.width = js.machCanvasMaxWidth(self.id),
.height = js.machCanvasMaxHeight(self.id),
},
};
}
pub fn setCursorMode(self: *Core, mode: CursorMode) void {
js.machSetCursorMode(self.id, @enumToInt(mode));
}
pub fn cursorMode(self: *Core) CursorMode {
return @intToEnum(CursorMode, js.machCursorMode(self.id));
}
pub fn setCursorShape(self: *Core, shape: CursorShape) void {
js.machSetCursorShape(self.id, @enumToInt(shape));
}
pub fn cursorShape(self: *Core) CursorShape {
return @intToEnum(CursorShape, js.machCursorShape(self.id));
}
pub fn adapter(_: *Core) *gpu.Adapter {
unreachable;
}
pub fn device(_: *Core) *gpu.Device {
unreachable;
}
pub fn swapChain(_: *Core) *gpu.SwapChain {
unreachable;
}
pub fn descriptor(_: *Core) gpu.SwapChain.Descriptor {
unreachable;
}
fn toMachButton(button: i32) MouseButton {
return switch (button) {
0 => .left,
1 => .middle,
2 => .right,
3 => .four,
4 => .five,
else => unreachable,
};
}

View file

@ -0,0 +1,25 @@
const std = @import("std");
const js = @import("js.zig");
pub const Timer = @This();
initial: f64 = undefined,
pub fn start() !Timer {
return Timer{ .initial = js.machPerfNow() };
}
pub fn read(timer: *Timer) u64 {
return (js.machPerfNow() - timer.initial) * std.time.ns_per_ms;
}
pub fn reset(timer: *Timer) void {
timer.initial = js.machPerfNow();
}
pub fn lap(timer: *Timer) u64 {
const now = js.machPerfNow();
const initial = timer.initial;
timer.initial = now;
return @floatToInt(u64, now - initial) * std.time.ns_per_ms;
}

View file

@ -0,0 +1,49 @@
const std = @import("std");
const gpu = @import("gpu");
const App = @import("app").App;
const js = @import("js.zig");
pub const GPUInterface = gpu.StubInterface;
pub const log_level = if (@hasDecl(App, "log_level")) App.log_level else std.log.default_level;
pub const scope_levels = if (@hasDecl(App, "scope_levels")) App.scope_levels else [0]std.log.ScopeLevel{};
var app: App = undefined;
export fn wasmInit() void {
app.init() catch unreachable;
}
export fn wasmUpdate() bool {
return app.update() catch unreachable;
}
export fn wasmDeinit() void {
app.deinit();
}
const LogError = error{};
const LogWriter = std.io.Writer(void, LogError, writeLog);
fn writeLog(_: void, msg: []const u8) LogError!usize {
js.machLogWrite(msg.ptr, msg.len);
return msg.len;
}
pub fn log(
comptime message_level: std.log.Level,
comptime scope: @Type(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
const writer = LogWriter{ .context = {} };
writer.print(message_level.asText() ++ prefix ++ format ++ "\n", args) catch return;
js.machLogFlush();
}
pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
_ = error_return_trace;
_ = ret_addr;
js.machPanic(msg.ptr, msg.len);
unreachable;
}

40
src/platform/wasm/js.zig Normal file
View file

@ -0,0 +1,40 @@
pub const CanvasId = u32;
pub extern "mach" fn machLogWrite(str: [*]const u8, len: u32) void;
pub extern "mach" fn machLogFlush() void;
pub extern "mach" fn machPanic(str: [*]const u8, len: u32) void;
pub extern "mach" fn machCanvasInit(selector_id: *u8) CanvasId;
pub extern "mach" fn machCanvasDeinit(canvas: CanvasId) void;
pub extern "mach" fn machCanvasFramebufferWidth(canvas: CanvasId) u32;
pub extern "mach" fn machCanvasFramebufferHeight(canvas: CanvasId) u32;
pub extern "mach" fn machCanvasSetTitle(canvas: CanvasId, title: [*]const u8, len: u32) void;
pub extern "mach" fn machCanvasSetDisplayMode(canvas: CanvasId, mode: u32) void;
pub extern "mach" fn machCanvasDisplayMode(canvas: CanvasId) u32;
pub extern "mach" fn machCanvasSetBorder(canvas: CanvasId, value: bool) void;
pub extern "mach" fn machCanvasBorder(canvas: CanvasId) bool;
pub extern "mach" fn machCanvasSetHeadless(canvas: CanvasId, value: bool) void;
pub extern "mach" fn machCanvasHeadless(canvas: CanvasId) bool;
pub extern "mach" fn machCanvasSetVsync(canvas: CanvasId, mode: u32) void;
pub extern "mach" fn machCanvasVsync(canvas: CanvasId) u32;
pub extern "mach" fn machCanvasSetSize(canvas: CanvasId, width: u32, height: u32) void;
pub extern "mach" fn machCanvasWidth(canvas: CanvasId) u32;
pub extern "mach" fn machCanvasHeight(canvas: CanvasId) u32;
pub extern "mach" fn machCanvasSetSizeLimit(canvas: CanvasId, min_width: i32, min_height: i32, max_width: i32, max_height: i32) void;
pub extern "mach" fn machCanvasMinWidth(canvas: CanvasId) u32;
pub extern "mach" fn machCanvasMinHeight(canvas: CanvasId) u32;
pub extern "mach" fn machCanvasMaxWidth(canvas: CanvasId) u32;
pub extern "mach" fn machCanvasMaxHeight(canvas: CanvasId) u32;
pub extern "mach" fn machSetCursorMode(canvas: CanvasId, mode: u32) void;
pub extern "mach" fn machCursorMode(canvas: CanvasId) u32;
pub extern "mach" fn machSetCursorShape(canvas: CanvasId, shape: u32) void;
pub extern "mach" fn machCursorShape(canvas: CanvasId) u32;
pub extern "mach" fn machShouldClose() bool;
pub extern "mach" fn machHasEvent() bool;
pub extern "mach" fn machSetWaitTimeout(timeout: f64) void;
pub extern "mach" fn machEventShift() i32;
pub extern "mach" fn machEventShiftFloat() f64;
pub extern "mach" fn machChangeShift() u32;
pub extern "mach" fn machPerfNow() f64;

511
src/platform/wasm/mach.js Normal file
View file

@ -0,0 +1,511 @@
const text_decoder = new TextDecoder();
const text_encoder = new TextEncoder();
const mach = {
canvases: [],
wasm: undefined,
observer: undefined,
events: [],
changes: [],
wait_timeout: 0,
log_buf: "",
init(wasm) {
mach.wasm = wasm;
mach.observer = new MutationObserver((mutables) => {
mutables.forEach((mutable) => {
mach.canvases.forEach((canvas) => {
if (mutable.target == canvas) {
if (mutable.attributeName === "width" ||
mutable.attributeName === "height" ||
mutable.attributeName === "style") {
mutable.target.dispatchEvent(new Event("mach-canvas-resize"));
}
}
});
})
})
},
getString(str, len) {
const memory = mach.wasm.exports.memory.buffer;
return text_decoder.decode(new Uint8Array(memory, str, len));
},
setString(str, buf) {
const memory = mach.wasm.exports.memory.buffer;
const strbuf = text_encoder.encode(str);
const outbuf = new Uint8Array(memory, buf, strbuf.length);
for (let i = 0; i < strbuf.length; i += 1) {
outbuf[i] = strbuf[i];
}
},
machLogWrite(str, len) {
mach.log_buf += mach.getString(str, len);
},
machLogFlush() {
console.log(log_buf);
mach.log_buf = "";
},
machPanic(str, len) {
throw Error(mach.getString(str, len));
},
machCanvasInit(id) {
let canvas = document.createElement("canvas");
canvas.id = "#mach-canvas-" + mach.canvases.length;
canvas.style.border = "1px solid";
canvas.style.position = "absolute";
canvas.style.display = "block";
canvas.tabIndex = 1;
mach.observer.observe(canvas, { attributes: true });
mach.setString(canvas.id, id);
canvas.addEventListener("contextmenu", (ev) => ev.preventDefault());
canvas.addEventListener("keydown", (ev) => {
if (ev.repeat) {
mach.events.push(...[EventCode.key_repeat, convertKeyCode(ev.code)]);
} else {
mach.events.push(...[EventCode.key_press, convertKeyCode(ev.code)]);
}
});
canvas.addEventListener("keyup", (ev) => {
mach.events.push(...[EventCode.key_release, convertKeyCode(ev.code)]);
});
canvas.addEventListener("mousemove", (ev) => {
mach.events.push(...[EventCode.mouse_motion, ev.clientX, ev.clientY]);
});
canvas.addEventListener("mousedown", (ev) => {
mach.events.push(...[EventCode.mouse_press, ev.button]);
});
canvas.addEventListener("mouseup", (ev) => {
mach.events.push(...[EventCode.mouse_release, ev.button]);
});
canvas.addEventListener("wheel", (ev) => {
mach.events.push(...[EventCode.mouse_scroll, ev.deltaX, ev.deltaY]);
});
canvas.addEventListener("mach-canvas-resize", (ev) => {
const cv_index = mach.canvases.findIndex((el) => el === ev.currentTarget);
const cv = mach.canvases[cv_index];
mach.events.push(...[EventCode.framebuffer_resize, cv.width, cv.height, window.devicePixelRatio]);
});
canvas.addEventListener("focus", (ev) => {
mach.events.push(...[EventCode.focus_gained]);
});
canvas.addEventListener("blur", (ev) => {
mach.events.push(...[EventCode.focus_lost]);
});
document.body.appendChild(canvas);
return mach.canvases.push(canvas) - 1;
},
machCanvasDeinit(canvas) {
if (mach.canvases[canvas] != undefined) {
mach.canvases.splice(canvas, 1);
}
},
machCanvasFramebufferWidth(canvas) {
const cv = mach.canvases[canvas];
return cv.width;
},
machCanvasFramebufferHeight(canvas) {
const cv = mach.canvases[canvas];
return cv.height;
},
machCanvasSetTitle(canvas, title, len) {
// TODO
},
machCanvasSetDisplayMode(canvas, mode) {
const cv = mach.canvases[canvas];
switch (mode) {
case DisplayMode.windowed:
document.exitFullscreen();
break;
case DisplayMode.fullscreen:
cv.requestFullscreen();
break;
}
},
machCanvasDisplayMode(canvas) {
if (mach.canvases[canvas].fullscreenElement == null) {
return DisplayMode.windowed;
} else {
return DisplayMode.fullscreen;
}
},
machCanvasSetBorder(canvas, value) {
// TODO
},
machCanvasBorder(canvas) {
// TODO
},
machCanvasSetHeadless(canvas, value) {
// TODO
},
machCanvasHeadless(canvas) {
// TODO
},
machCanvasSetVSync(canvas, mode) {
// TODO
},
machCanvasVSync(canvas) {
// TODO
},
machCanvasSetSize(canvas, width, height) {
const cv = mach.canvases[canvas];
if (width > 0 && height > 0) {
cv.style.width = width + "px";
cv.style.height = height + "px";
cv.width = Math.floor(width * window.devicePixelRatio);
cv.height = Math.floor(height * window.devicePixelRatio);
}
},
machCanvasWidth(canvas) {
const cv = mach.canvases[canvas];
return cv.width / window.devicePixelRatio;
},
machCanvasHeight(canvas) {
const cv = mach.canvases[canvas];
return cv.height / window.devicePixelRatio;
},
machCanvasSetSizeLimit(canvas, min_width, min_height, max_width, max_height) {
const cv = mach.canvases[canvas];
if (min_width == -1) {
cv.style.minWidth = "inherit"
} else {
cv.style.minWidth = min_width + "px";
}
if (min_width == -1) {
cv.style.minHeight = "inherit"
} else {
cv.style.minHeight = min_height + "px";
}
if (min_width == -1) {
cv.style.maxWidth = "inherit"
} else {
cv.style.maxWidth = max_width + "px";
}
if (min_width == -1) {
cv.style.maxHeight = "inherit"
} else {
cv.style.maxHeight = max_height + "px";
}
},
machCanvasMinWidth(canvas) {
const cv = mach.canvases[canvas];
return cv.style.minWidth;
},
machCanvasMinHeight(canvas) {
const cv = mach.canvases[canvas];
return cv.style.minHeight;
},
machCanvasMaxWidth(canvas) {
const cv = mach.canvases[canvas];
return cv.style.maxWidth;
},
machCanvasMaxHeight(canvas) {
const cv = mach.canvases[canvas];
return cv.style.maxHeight;
},
machSetCursorMode(canvas, mode) {
const cv = mach.canvases[canvas];
switch (mode) {
case CursorMode.normal:
cv.style.cursor = 'default';
break;
case CursorMode.hidden:
cv.style.cursor = 'none';
break;
case CursorMode.hidden:
cv.style.cursor = 'none';
break;
}
},
machCursorMode(canvas) {
switch (mach.canvases[canvas].style.cursor) {
case 'none': return CursorMode.hidden;
default: return CursorMode.normal;
}
},
machSetCursorShape(canvas, shape) {
const cv = mach.canvases[canvas];
switch (shape) {
case CursorShape.arrow:
cv.style.cursor = 'default';
break;
case CursorShape.ibeam:
cv.style.cursor = 'text';
break;
case CursorShape.crosshair:
cv.style.cursor = 'crosshair';
break;
case CursorShape.pointing_hand:
cv.style.cursor = 'pointer';
break;
case CursorShape.resize_ew:
cv.style.cursor = 'ew-resize';
break;
case CursorShape.resize_ns:
cv.style.cursor = 'ns-resize';
break;
case CursorShape.resize_nwse:
cv.style.cursor = 'nwse-resize';
break;
case CursorShape.resize_nesw:
cv.style.cursor = 'nesw-resize';
break;
case CursorShape.resize_all:
cv.style.cursor = 'move';
break;
case CursorShape.not_allowed:
cv.style.cursor = 'not-allowed';
break;
}
},
machCursorShape(canvas) {
switch (mach.canvases[canvas].style.cursor) {
case 'default': return CursorShape.arrow;
case 'text': return CursorShape.ibeam;
case 'crosshair': return CursorShape.crosshair;
case 'pointer': return CursorShape.pointing_hand;
case 'ew-resize': return CursorShape.resize_ew;
case 'ns-resize': return CursorShape.resize_ns;
case 'nwse-resize': return CursorShape.resize_nwse;
case 'nesw-resize': return CursorShape.resize_nesw;
case 'move': return CursorShape.resize_all;
case 'not-allowed': return CursorShape.not_allowed;
}
},
machSetWaitTimeout(timeout) {
mach.wait_timeout = timeout;
},
machHasEvent() {
return mach.events.length > 0;
},
machEventShift() {
if (mach.machHasEvent())
return mach.events.shift();
return -1;
},
machEventShiftFloat() {
return mach.machEventShift();
},
machPerfNow() {
return performance.now();
},
};
function convertKeyCode(code) {
const k = Key[code];
if (k != undefined)
return k;
return 118; // Unknown
}
const Key = {
KeyA: 0,
KeyB: 1,
KeyC: 2,
KeyD: 3,
KeyE: 4,
KeyF: 5,
KeyG: 6,
KeyH: 7,
KeyI: 8,
KeyJ: 9,
KeyK: 10,
KeyL: 11,
KeyM: 12,
KeyN: 13,
KeyO: 14,
KeyP: 15,
KeyQ: 16,
KeyR: 17,
KeyS: 18,
KeyT: 19,
KeyU: 20,
KeyV: 21,
KeyW: 22,
KeyX: 23,
KeyY: 24,
KeyZ: 25,
Digit0: 26,
Digit1: 27,
Digit2: 28,
Digit3: 29,
Digit4: 30,
Digit5: 31,
Digit6: 32,
Digit7: 33,
Digit8: 34,
Digit9: 35,
F1: 36,
F2: 37,
F3: 38,
F4: 39,
F5: 40,
F6: 41,
F7: 42,
F8: 43,
F9: 44,
F10: 45,
F11: 46,
F12: 47,
F13: 48,
F14: 49,
F15: 50,
F16: 51,
F17: 52,
F18: 53,
F19: 54,
F20: 55,
F21: 56,
F22: 57,
F23: 58,
F24: 59,
F25: 60,
NumpadDivide: 61,
NumpadMultiply: 62,
NumpadSubtract: 63,
NumpadAdd: 64,
Numpad0: 65,
Numpad1: 66,
Numpad2: 67,
Numpad3: 68,
Numpad4: 69,
Numpad5: 70,
Numpad6: 71,
Numpad7: 72,
Numpad8: 73,
Numpad9: 74,
NumpadDecimal: 75,
NumpadEqual: 76,
NumpadEnter: 77,
Enter: 78,
Escape: 79,
Tab: 80,
ShiftLeft: 81,
ShiftRight: 82,
ControlLeft: 83,
ControlRight: 84,
AltLeft: 85,
AltRight: 86,
OSLeft: 87,
MetaLeft: 87,
OSRight: 88,
MetaRight: 88,
ContextMenu: 89,
NumLock: 90,
CapsLock: 91,
PrintScreen: 92,
ScrollLock: 93,
Pause: 94,
Delete: 95,
Home: 96,
End: 97,
PageUp: 98,
PageDown: 99,
Insert: 100,
ArrowLeft: 101,
ArrowRight: 102,
ArrowUp: 103,
ArrowDown: 104,
Backspace: 105,
Space: 106,
Minus: 107,
Equal: 108,
BracketLeft: 109,
BracketRight: 110,
Backslash: 111,
Semicolon: 112,
Quote: 113,
Comma: 114,
Period: 115,
Slash: 116,
Backquote: 117,
};
const EventCode = {
key_press: 0,
key_repeat: 1,
key_release: 2,
char_input: 3,
mouse_motion: 4,
mouse_press: 5,
mouse_release: 6,
mouse_scroll: 7,
framebuffer_resize: 8,
focus_gained: 9,
focus_lost: 10,
close: 11,
};
const DisplayMode = {
windowed: 0,
fullscreen: 1,
};
const CursorMode = {
normal: 0,
hidden: 1,
disabled: 2,
};
const CursorShape = {
arrow: 0,
ibeam: 1,
crosshair: 2,
pointing_hand: 3,
resize_ew: 4,
resize_ns: 5,
resize_nwse: 6,
resize_nesw: 7,
resize_all: 8,
not_allowed: 9,
};
export { mach };

View file

@ -1,110 +0,0 @@
const gpu = @import("gpu");
const enums = @import("enums.zig");
pub const Size = struct {
width: u32,
height: u32,
};
pub const SizeOptional = struct {
width: ?u32,
height: ?u32,
};
/// Application options that can be configured at init time.
pub const StartupOptions = struct {};
/// Application options that can be configured at run time.
pub const Options = struct {
/// The title of the window.
title: [*:0]const u8 = "Mach core",
/// The width of the window.
width: u32 = 640,
/// The height of the window.
height: u32 = 480,
/// The minimum allowed size for the window. On Linux, if we don't set a minimum size,
/// you can squish the window to 0 width and height with strange effects, so it's better to leave
/// a minimum size to avoid that. This doesn't prevent you from minimizing the window.
size_min: SizeOptional = .{ .width = 350, .height = 350 },
/// The maximum allowed size for the window.
size_max: SizeOptional = .{ .width = null, .height = null },
/// Fullscreen window.
fullscreen: bool = false,
/// Fullscreen monitor index
monitor: ?u32 = null,
/// Headless mode.
headless: bool = false,
/// Borderless window
borderless_window: bool = false,
/// Monitor synchronization modes.
vsync: enums.VSyncMode = .double,
/// GPU features required by the application.
required_features: ?[]gpu.FeatureName = null,
/// GPU limits required by the application.
required_limits: ?gpu.Limits = null,
/// Whether the application has a preference for low power or high performance GPU.
power_preference: gpu.PowerPreference = .undefined,
/// If set, optimize for regular applications rather than games. e.g. disable Linux gamemode / process priority, prefer low-power GPU (if preference is .undefined), etc.
is_app: bool = false,
};
pub const Event = union(enum) {
key_press: KeyEvent,
key_repeat: KeyEvent,
key_release: KeyEvent,
char_input: struct {
codepoint: u21,
},
mouse_motion: struct {
pos: WindowPos,
},
mouse_press: MouseButtonEvent,
mouse_release: MouseButtonEvent,
mouse_scroll: struct {
xoffset: f32,
yoffset: f32,
},
focus_gained,
focus_lost,
closed,
};
pub const KeyEvent = struct {
key: enums.Key,
mods: KeyMods,
};
pub const MouseButtonEvent = struct {
button: enums.MouseButton,
pos: WindowPos,
mods: KeyMods,
};
pub const KeyMods = packed struct {
shift: bool,
control: bool,
alt: bool,
super: bool,
caps_lock: bool,
num_lock: bool,
_reserved: u2 = 0,
};
pub const WindowPos = struct {
// These are in window coordinates (not framebuffer coords)
x: f64,
y: f64,
};