diff --git a/build.zig b/build.zig index 66bc6a3d..9b4458a6 100644 --- a/build.zig +++ b/build.zig @@ -127,12 +127,7 @@ fn testStep(b: *Builder, mode: std.builtin.Mode, target: CrossTarget) *std.build for (pkg.dependencies.?) |dependency| { main_tests.addPackage(dependency); } - - main_tests.addPackage(freetype.pkg); - freetype.link(b, main_tests, .{}); - main_tests.install(); - return main_tests.run(); } @@ -226,16 +221,12 @@ pub const App = struct { const step = blk: { if (platform == .web) { - const lib = b.addSharedLibrary(options.name, sdkPath("/src/platform/wasm.zig"), .unversioned); - lib.addPackage(gpu.pkg); - lib.addPackage(sysaudio.pkg); + const lib = b.addSharedLibrary(options.name, sdkPath("/src/main.zig"), .unversioned); lib.addPackage(sysjs.pkg); break :blk lib; } else { - const exe = b.addExecutable(options.name, sdkPath("/src/platform/native.zig")); - exe.addPackage(gpu.pkg); - exe.addPackage(sysaudio.pkg); + const exe = b.addExecutable(options.name, sdkPath("/src/main.zig")); exe.addPackage(glfw.pkg); if (target.os.tag == .linux) @@ -246,6 +237,9 @@ pub const App = struct { }; step.main_pkg_path = sdkPath("/src"); + step.addPackage(ecs.pkg); + step.addPackage(sysaudio.pkg); + step.addPackage(gpu.pkg); step.addPackage(app_pkg); step.setTarget(options.target); step.setBuildMode(options.mode); @@ -285,7 +279,7 @@ pub const App = struct { // Set install directory to '{prefix}/www' app.getInstallStep().?.dest_dir = web_install_dir; - inline for (.{ "/src/platform/mach.js", "/libs/sysjs/src/mach-sysjs.js" }) |js| { + inline for (.{ "/src/platform/wasm/mach.js", "/libs/sysjs/src/mach-sysjs.js" }) |js| { const install_js = app.b.addInstallFileWithDir( .{ .path = sdkPath(js) }, web_install_dir, diff --git a/shaderexp/main.zig b/shaderexp/main.zig index 0866d893..0b71c6ea 100755 --- a/shaderexp/main.zig +++ b/shaderexp/main.zig @@ -10,8 +10,11 @@ const UniformBufferObject = struct { time: f32, }; -var timer: std.time.Timer = undefined; +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +const allocator = gpa.allocator(); +core: mach.Core, +timer: mach.Timer, pipeline: *gpu.RenderPipeline, queue: *gpu.Queue, uniform_buffer: *gpu.Buffer, @@ -21,8 +24,8 @@ fragment_shader_file: std.fs.File, fragment_shader_code: [:0]const u8, last_mtime: i128, -pub fn init(app: *App, core: *mach.Core) !void { - timer = try std.time.Timer.start(); +pub fn init(app: *App) !void { + app.core = try mach.Core.init(allocator, .{ .title = "shaderexp" }); var fragment_file: std.fs.File = undefined; var last_mtime: i128 = undefined; @@ -40,21 +43,21 @@ pub fn init(app: *App, core: *mach.Core) !void { std.debug.print("Something went wrong when attempting to open file: {}\n", .{e}); return; } - var code = try fragment_file.readToEndAllocOptions(core.allocator, std.math.maxInt(u16), null, 1, 0); + var code = try fragment_file.readToEndAllocOptions(allocator, std.math.maxInt(u16), null, 1, 0); - const queue = core.device.getQueue(); + const queue = app.core.device().getQueue(); // We need a bgl to bind the UniformBufferObject, but it is also needed for creating // the RenderPipeline, so we pass it to recreatePipeline as a pointer var bgl: *gpu.BindGroupLayout = undefined; - const pipeline = recreatePipeline(core, code, &bgl); + const pipeline = recreatePipeline(&app.core, code, &bgl); - const uniform_buffer = core.device.createBuffer(&.{ + const uniform_buffer = app.core.device().createBuffer(&.{ .usage = .{ .copy_dst = true, .uniform = true }, .size = @sizeOf(UniformBufferObject), .mapped_at_creation = false, }); - const bind_group = core.device.createBindGroup( + const bind_group = app.core.device().createBindGroup( &gpu.BindGroup.Descriptor.init(.{ .layout = bgl, .entries = &.{ @@ -63,6 +66,8 @@ pub fn init(app: *App, core: *mach.Core) !void { }), ); + app.timer = try mach.Timer.start(); + app.pipeline = pipeline; app.queue = queue; app.uniform_buffer = uniform_buffer; @@ -75,21 +80,24 @@ pub fn init(app: *App, core: *mach.Core) !void { bgl.release(); } -pub fn deinit(app: *App, core: *mach.Core) void { +pub fn deinit(app: *App) void { + defer _ = gpa.deinit(); + defer app.core.deinit(); + app.fragment_shader_file.close(); - core.allocator.free(app.fragment_shader_code); + allocator.free(app.fragment_shader_code); app.uniform_buffer.release(); app.bind_group.release(); } -pub fn update(app: *App, core: *mach.Core) !void { - while (core.pollEvent()) |event| { +pub fn update(app: *App) !bool { + while (app.core.pollEvents()) |event| { switch (event) { .key_press => |ev| { - if (ev.key == .space) - core.close(); + if (ev.key == .space) return true; }, + .close => return true, else => {}, } } @@ -99,17 +107,17 @@ pub fn update(app: *App, core: *mach.Core) !void { std.log.info("The fragment shader has been changed", .{}); app.last_mtime = stat.mtime; app.fragment_shader_file.seekTo(0) catch unreachable; - app.fragment_shader_code = app.fragment_shader_file.readToEndAllocOptions(core.allocator, std.math.maxInt(u32), null, 1, 0) catch |err| { + app.fragment_shader_code = app.fragment_shader_file.readToEndAllocOptions(allocator, std.math.maxInt(u32), null, 1, 0) catch |err| { std.log.err("Err: {}", .{err}); - return core.close(); + return true; }; - app.pipeline = recreatePipeline(core, app.fragment_shader_code, null); + app.pipeline = recreatePipeline(&app.core, app.fragment_shader_code, null); } } else |err| { std.log.err("Something went wrong when attempting to stat file: {}\n", .{err}); } - const back_buffer_view = core.swap_chain.?.getCurrentTextureView(); + const back_buffer_view = app.core.swapChain().getCurrentTextureView(); const color_attachment = gpu.RenderPassColorAttachment{ .view = back_buffer_view, .clear_value = std.mem.zeroes(gpu.Color), @@ -117,14 +125,14 @@ pub fn update(app: *App, core: *mach.Core) !void { .store_op = .store, }; - const encoder = core.device.createCommandEncoder(null); + const encoder = app.core.device().createCommandEncoder(null); const render_pass_info = gpu.RenderPassDescriptor.init(.{ .color_attachments = &.{color_attachment}, }); - const time = @intToFloat(f32, timer.read()) / @as(f32, std.time.ns_per_s); + const time = app.timer.read() / @as(f32, std.time.ns_per_s); const ubo = UniformBufferObject{ - .resolution = .{ @intToFloat(f32, core.current_desc.width), @intToFloat(f32, core.current_desc.height) }, + .resolution = .{ @intToFloat(f32, app.core.descriptor().width), @intToFloat(f32, app.core.descriptor().height) }, .time = time, }; encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo}); @@ -141,21 +149,23 @@ pub fn update(app: *App, core: *mach.Core) !void { app.queue.submit(&[_]*gpu.CommandBuffer{command}); command.release(); - core.swap_chain.?.present(); + app.core.swapChain().present(); back_buffer_view.release(); + + return false; } fn recreatePipeline(core: *mach.Core, fragment_shader_code: [:0]const u8, bgl: ?**gpu.BindGroupLayout) *gpu.RenderPipeline { - const vs_module = core.device.createShaderModuleWGSL("vert.wgsl", @embedFile("vert.wgsl")); + const vs_module = core.device().createShaderModuleWGSL("vert.wgsl", @embedFile("vert.wgsl")); defer vs_module.release(); // Check wether the fragment shader code compiled successfully, if not // print the validation layer error and show a black screen - core.device.pushErrorScope(.validation); - var fs_module = core.device.createShaderModuleWGSL("fragment shader", fragment_shader_code); + core.device().pushErrorScope(.validation); + var fs_module = core.device().createShaderModuleWGSL("fragment shader", fragment_shader_code); var error_occurred: bool = false; // popErrorScope() returns always true, (unless maybe it fails to capture the error scope?) - _ = core.device.popErrorScope(&error_occurred, struct { + _ = core.device().popErrorScope(&error_occurred, struct { inline fn callback(ctx: *bool, typ: gpu.ErrorType, message: [*:0]const u8) void { if (typ != .no_error) { std.debug.print("🔴🔴🔴🔴:\n{s}\n", .{message}); @@ -164,7 +174,7 @@ fn recreatePipeline(core: *mach.Core, fragment_shader_code: [:0]const u8, bgl: ? } }.callback); if (error_occurred) { - fs_module = core.device.createShaderModuleWGSL( + fs_module = core.device().createShaderModuleWGSL( "black_screen_frag.wgsl", @embedFile("black_screen_frag.wgsl"), ); @@ -173,7 +183,7 @@ fn recreatePipeline(core: *mach.Core, fragment_shader_code: [:0]const u8, bgl: ? const blend = gpu.BlendState{}; const color_target = gpu.ColorTargetState{ - .format = core.swap_chain_format, + .format = core.descriptor().format, .blend = &blend, .write_mask = gpu.ColorWriteMaskFlags.all, }; @@ -185,7 +195,7 @@ fn recreatePipeline(core: *mach.Core, fragment_shader_code: [:0]const u8, bgl: ? const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .fragment = true }, .uniform, true, 0); // bgl is needed outside, for the creation of the uniform_buffer in main - const bgl_tmp = core.device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{ + const bgl_tmp = core.device().createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{ .entries = &.{bgle}, })); defer { @@ -198,7 +208,7 @@ fn recreatePipeline(core: *mach.Core, fragment_shader_code: [:0]const u8, bgl: ? } const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl_tmp}; - const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ + const pipeline_layout = core.device().createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{ .bind_group_layouts = &bind_group_layouts, })); defer pipeline_layout.release(); @@ -214,10 +224,10 @@ fn recreatePipeline(core: *mach.Core, fragment_shader_code: [:0]const u8, bgl: ? // Create the render pipeline. Even if the shader compilation succeeded, this could fail if the // shader is missing a `main` entrypoint. - core.device.pushErrorScope(.validation); - const pipeline = core.device.createRenderPipeline(&pipeline_descriptor); + core.device().pushErrorScope(.validation); + const pipeline = core.device().createRenderPipeline(&pipeline_descriptor); // popErrorScope() returns always true, (unless maybe it fails to capture the error scope?) - _ = core.device.popErrorScope(&error_occurred, struct { + _ = core.device().popErrorScope(&error_occurred, struct { inline fn callback(ctx: *bool, typ: gpu.ErrorType, message: [*:0]const u8) void { if (typ != .no_error) { std.debug.print("🔴🔴🔴🔴:\n{s}\n", .{message}); diff --git a/src/Core.zig b/src/Core.zig index 391277a7..008675a2 100644 --- a/src/Core.zig +++ b/src/Core.zig @@ -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, +}; diff --git a/src/Timer.zig b/src/Timer.zig index 28b3b99a..852d26eb 100644 --- a/src/Timer.zig +++ b/src/Timer.zig @@ -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. diff --git a/src/engine.zig b/src/engine.zig index ad09381f..a0da54a3 100644 --- a/src/engine.zig +++ b/src/engine.zig @@ -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 diff --git a/src/entry.zig b/src/entry.zig new file mode 100644 index 00000000..1dba7509 --- /dev/null +++ b/src/entry.zig @@ -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'"); + } + } +} diff --git a/src/enums.zig b/src/enums.zig deleted file mode 100644 index 668dc026..00000000 --- a/src/enums.zig +++ /dev/null @@ -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, -}; diff --git a/src/main.zig b/src/main.zig index aa92b073..2d84c499 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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); } diff --git a/src/platform.zig b/src/platform.zig index d3cc3ccb..0c85cd27 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -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); } diff --git a/src/platform/common.zig b/src/platform/common.zig deleted file mode 100644 index 62ed49ac..00000000 --- a/src/platform/common.zig +++ /dev/null @@ -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'"); - } -} diff --git a/src/platform/libmach.zig b/src/platform/libmach.zig index 478b96c6..686c46f1 100644 --- a/src/platform/libmach.zig +++ b/src/platform/libmach.zig @@ -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, diff --git a/src/platform/mach.js b/src/platform/mach.js deleted file mode 100644 index 3f609a9c..00000000 --- a/src/platform/mach.js +++ /dev/null @@ -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 }; diff --git a/src/platform/native.zig b/src/platform/native.zig index 4f29bb0d..f1fd4522 100644 --- a/src/platform/native.zig +++ b/src/platform/native.zig @@ -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; diff --git a/src/platform/native/Core.zig b/src/platform/native/Core.zig new file mode 100644 index 00000000..0f4fa02b --- /dev/null +++ b/src/platform/native/Core.zig @@ -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() }); + }; +} diff --git a/src/platform/native/entry.zig b/src/platform/native/entry.zig new file mode 100644 index 00000000..0eb922fe --- /dev/null +++ b/src/platform/native/entry.zig @@ -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; + } +} diff --git a/src/platform/native/objc_message.zig b/src/platform/native/objc_message.zig new file mode 100644 index 00000000..e039b4a9 --- /dev/null +++ b/src/platform/native/objc_message.zig @@ -0,0 +1,7 @@ +// Extracted from `zig translate-c tmp.c` with `#include ` 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; diff --git a/src/platform/util.zig b/src/platform/native/util.zig similarity index 91% rename from src/platform/util.zig rename to src/platform/native/util.zig index d8878268..f514a5cd 100644 --- a/src/platform/util.zig +++ b/src/platform/native/util.zig @@ -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"), }; diff --git a/src/platform/objc_message.zig b/src/platform/objc_message.zig deleted file mode 100644 index 3b14412b..00000000 --- a/src/platform/objc_message.zig +++ /dev/null @@ -1,9 +0,0 @@ -// Extracted from `zig translate-c tmp.c` with `#include ` 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; diff --git a/src/platform/wasm.zig b/src/platform/wasm.zig index 1b755198..df35ada7 100644 --- a/src/platform/wasm.zig +++ b/src/platform/wasm.zig @@ -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"); diff --git a/src/platform/wasm/Core.zig b/src/platform/wasm/Core.zig new file mode 100644 index 00000000..35ef6209 --- /dev/null +++ b/src/platform/wasm/Core.zig @@ -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, + }; +} diff --git a/src/platform/wasm/Timer.zig b/src/platform/wasm/Timer.zig new file mode 100644 index 00000000..74d76a07 --- /dev/null +++ b/src/platform/wasm/Timer.zig @@ -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; +} diff --git a/src/platform/wasm/entry.zig b/src/platform/wasm/entry.zig new file mode 100644 index 00000000..3a03c8fc --- /dev/null +++ b/src/platform/wasm/entry.zig @@ -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; +} diff --git a/src/platform/wasm/js.zig b/src/platform/wasm/js.zig new file mode 100644 index 00000000..0971485c --- /dev/null +++ b/src/platform/wasm/js.zig @@ -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; diff --git a/src/platform/wasm/mach.js b/src/platform/wasm/mach.js new file mode 100644 index 00000000..87e8db45 --- /dev/null +++ b/src/platform/wasm/mach.js @@ -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 }; diff --git a/src/structs.zig b/src/structs.zig deleted file mode 100644 index 95f487eb..00000000 --- a/src/structs.zig +++ /dev/null @@ -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, -}; diff --git a/tools/html-generator/template.html b/tools/html-generator/template.html index becd9139..3a34470d 100644 --- a/tools/html-generator/template.html +++ b/tools/html-generator/template.html @@ -31,21 +31,21 @@ let frame = true; let last_update_time = performance.now(); let update = function () { - if (!frame) return; - if (mach.machHasEvent() || - (last_update_time + (mach.wait_event_timeout * 1000)) <= performance.now()) { - instance.exports.wasmUpdate(); + if (!frame) { + instance.exports.wasmDeinit(); + return; + } + if (mach.machHasEvent() || + last_update_time + mach.wait_timeout * 1000 <= performance.now()) { + if (instance.exports.wasmUpdate()) { + instance.exports.wasmDeinit(); + return; + } last_update_time = performance.now(); } window.requestAnimationFrame(update); }; - window.requestAnimationFrame(update); - - window.addEventListener("mach-close", () => { - instance.exports.wasmDeinit(); - frame = false; - }); }) .catch(err => console.error(err));