const builtin = @import("builtin"); const std = @import("std"); const Io = std.Io; const Allocator = std.mem.Allocator; const log = std.log.scoped(.platform); const assert = std.debug.assert; const build_options = @import("build_options"); const sokol = @import("sokol"); const sapp = sokol.app; const sg = sokol.gfx; const sglue = sokol.glue; const sgl = sokol.gl; pub const Math = @import("math"); pub const Vec2 = Math.Vec2; pub const Vec4 = Math.Vec4; pub const Mat4 = Math.Mat4; pub const Rect = Math.Rect; const tracy = @import("tracy"); pub const Color = @import("./color.zig"); const shd = @import("shader"); pub const ImGUI = @import("./imgui.zig"); pub const Gfx = @import("./graphics.zig"); pub const Input = @import("./input.zig"); const EmbeddedAssets = @import("embedded_assets"); fn PlatformType(App: type) type { return struct { const Self = @This(); gpa: Allocator, arena: *std.heap.ArenaAllocator, io: Io, cli_args: []const [:0]const u8, app: App, started_at: Io.Timestamp, last_frame_at: Io.Timestamp, assets: Assets, last_mouse_position: ?Vec2, input: Input, events_buffer: [256]Input.Event, events_overflow: bool, events: std.ArrayList(Input.Event), last_key_press_is_repeat: bool, last_key_pressed: ?Input.KeyCode, frame_arena: std.heap.ArenaAllocator, show_imgui: bool, fn init(self: *Self) !void { const window_size = Vec2.init(sapp.widthf(), sapp.heightf()); self.started_at = Io.Timestamp.now(self.io, .awake); self.last_frame_at = self.started_at; self.input = Input.init(window_size); try Gfx.init(self.gpa, .{ .func = sokolLogCallback }); ImGUI.init(.{ .func = sokolLogCallback }); if (@hasDecl(App, "init")) { const plt = Init{ .gpa = self.gpa, .arena = self.arena.allocator(), .io = self.io, .assets = &self.assets }; if (try self.app.init(plt)) |exit_code| { std.process.exit(exit_code); } } } fn sokolInit(user_data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(user_data)); self.init() catch |err| { log.err("init() error: {}", .{err}); if (@errorReturnTrace()) |trace| { std.debug.dumpErrorReturnTrace(trace); } sapp.quit(); }; } fn frame(self: *Self) !void { tracy.frameMark(); _ = self.frame_arena.reset(.free_all); // TODO: make arena reset mode configurable const now = Io.Timestamp.now(self.io, .awake); const t = self.started_at.durationTo(now); const dt = self.last_frame_at.durationTo(now); self.last_frame_at = now; if (self.input.keyboard.pressed.contains(.F4)) { self.show_imgui = !self.show_imgui; } if (self.last_mouse_position != null and self.input.mouse_position != null) { self.input.mouse_delta = self.input.mouse_position.?.sub(self.last_mouse_position.?); } else { self.input.mouse_delta = .init(0, 0); } self.last_mouse_position = self.input.mouse_position; { Gfx.beginFrame(); ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator()); if (@hasDecl(App, "frame")) { const plt = Frame{ .t = t.nanoseconds, .dt = dt.nanoseconds, .gpa = self.gpa, .arena = self.arena.allocator(), .io = self.io, .input = &self.input, .input_events = self.events.items, .frame = self.frame_arena.allocator(), .assets = &self.assets }; try self.app.frame(plt); } Gfx.flush(.{}); Gfx.showDebug(); ImGUI.endFrame(); Gfx.endFrame(); } self.events_overflow = false; self.events.clearRetainingCapacity(); self.input.keyboard.pressed = .empty; self.input.keyboard.released = .empty; self.input.mouse_buttons.pressed = .empty; self.input.mouse_buttons.released = .empty; self.input.mouse_scroll = .init(0, 0); } fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(user_data)); self.frame() catch |err| { log.err("frame() error: {}", .{err}); if (@errorReturnTrace()) |trace| { std.debug.dumpErrorReturnTrace(trace); } sapp.quit(); }; } // TODO: I don't like the names of these 'event' related functions. // All of them "append an event", but they do different things. fn applyEventToState(self: *Self, e: Input.Event) bool { const input = &self.input; switch (e) { .key_pressed => |key| { if (input.isKeyDown(key)) { return false; } input.keyboard.press(key); self.last_key_pressed = key; }, .key_released => |key| { if (!input.isKeyDown(key)) { return false; } input.keyboard.release(key); }, .mouse_pressed => |button| { if (input.mouse_position == null or input.isMouseDown(button)) { return false; } input.mouse_buttons.press(button); }, .mouse_released => |button| { if (input.mouse_position == null or !input.isMouseDown(button)) { return false; } input.mouse_buttons.release(button); }, .mouse_enter => |pos| { if (input.mouse_position != null) { return false; } input.mouse_position = pos; }, .mouse_move => |pos| { if (input.mouse_position == null) { return false; } input.mouse_position = pos; }, .mouse_leave => { if (input.mouse_position == null) { return false; } input.mouse_position = null; }, .char => |char| { // A 'key_pressed' event always occurs before a 'char' event. // This allows us to differentiate between keycodes and scan codes. // And how to map key codes to character codes. if (self.last_key_pressed == null) { return false; } input.key_code_mapping.put(self.last_key_pressed.?, char); self.last_key_pressed = null; }, .mouse_scroll => |offset| { if (input.mouse_position == null) { return false; } input.mouse_scroll = input.mouse_scroll.add(offset); }, .window_resize => |window_size| { self.input.window_size = window_size; }, .focused => { if (input.focused) { return false; } input.focused = true; }, .unfocused => { if (!input.focused) { return false; } assert(input.mouse_position == null); assert(input.keyboard.down.eql(.empty)); assert(input.mouse_buttons.down.eql(.empty)); input.focused = false; } } return true; } fn appendEvent(self: *Self, e: Input.Event) void { if (self.events.capacity == self.events.items.len) { if (!self.events_overflow) { log.warn("too many events, limit: {}", .{self.events.capacity}); } self.events_overflow = true; return; } if (self.applyEventToState(e)) { self.events.appendAssumeCapacity(e); } } fn pushEvent(self: *Self, e: Input.Event) void { const input = &self.input; if (input.focused) { if (e == .focused) { return; } if (e == .unfocused) { var key_iter = input.keyboard.down.iterator(); while (key_iter.next()) |key| { self.appendEvent(.{ .key_released = key }); } var mouse_buttons_iter = input.mouse_buttons.down.iterator(); while (mouse_buttons_iter.next()) |button| { self.appendEvent(.{ .mouse_released = button }); } self.appendEvent(.{ .mouse_leave = {} }); } } else { if (e == .unfocused) { return; } if (e != .focused) { self.appendEvent(.focused); if (e == .mouse_move) { self.appendEvent(.{ .mouse_enter = e.mouse_move }); } } } self.appendEvent(e); } fn event(self: *Self, e: sapp.Event) !bool { if (self.show_imgui) { if (ImGUI.event(e)) { self.pushEvent(.{ .unfocused = {} }); return true; } } const input = &self.input; switch (e.type) { .MOUSE_DOWN => { if (input.mouse_position == null) { self.pushEvent(.{ .mouse_enter = .init(e.mouse_x, e.mouse_y) }); } self.pushEvent(.{ .mouse_pressed = Input.MouseButton.fromSokol(e.mouse_button) orelse return false, }); }, .MOUSE_UP => { if (input.mouse_position == null) { self.pushEvent(.{ .mouse_enter = .init(e.mouse_x, e.mouse_y) }); } self.pushEvent(.{ .mouse_released = Input.MouseButton.fromSokol(e.mouse_button) orelse return false, }); }, .MOUSE_MOVE => { if (input.mouse_position == null) { self.pushEvent(.{ .mouse_enter = .init(e.mouse_x, e.mouse_y) }); } self.pushEvent(.{ .mouse_move = .init(e.mouse_x, e.mouse_y) }); }, .MOUSE_ENTER => { self.pushEvent(.{ .mouse_enter = .init(e.mouse_x, e.mouse_y) }); }, .MOUSE_LEAVE => { self.pushEvent(.{ .mouse_leave = {} }); }, .MOUSE_SCROLL => { self.pushEvent(.{ .mouse_scroll = .init(e.scroll_x, e.scroll_y) }); }, .KEY_DOWN => { self.last_key_press_is_repeat = e.key_repeat; if (!e.key_repeat) { self.pushEvent(.{ .key_pressed = @enumFromInt(@intFromEnum(e.key_code)) }); } }, .KEY_UP => { self.pushEvent(.{ .key_released = @enumFromInt(@intFromEnum(e.key_code)), }); }, .CHAR => { if (!self.last_key_press_is_repeat) { self.pushEvent(.{ .char = @intCast(e.char_code) }); } }, .FOCUSED => { self.pushEvent(.{ .focused = {} }); }, .UNFOCUSED => { self.pushEvent(.{ .unfocused = {} }); }, else => {} } return true; } fn sokolEvent(ev: [*c]const sapp.Event, user_data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(user_data)); const consumed_event = self.event(ev.*) catch |err| blk: { log.err("event() error: {}", .{err}); if (@errorReturnTrace()) |trace| { std.debug.dumpErrorReturnTrace(trace); } sapp.quit(); break :blk false; }; if (consumed_event) { sapp.consumeEvent(); } } fn sokolCleanup(user_data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(user_data)); if (@hasDecl(App, "deinit")) { const plt = Deinit{ .gpa = self.gpa, .io = self.io }; self.app.deinit(plt); } self.frame_arena.deinit(); self.assets.deinit(); ImGUI.deinit(); Gfx.deinit(); } }; } pub const RunOptions = struct { init: std.process.Init, window_title: [*c]const u8 = null, width: i32 = 800, height: i32 = 600, }; pub const Assets = struct { arena: std.heap.ArenaAllocator, gpa: Allocator, io: Io, dir: ?Io.Dir, files: std.array_hash_map.String(File), fn init(gpa: Allocator, io: Io, dir: ?Io.Dir) Assets { return Assets{ .gpa = gpa, .io = io, .arena = .init(gpa), .dir = dir, .files = .empty }; } fn readFileFromAssetDir(self: *Assets, filename: []const u8) !?[:0]const u8 { const dir = self.dir orelse return null; const arena = self.arena.allocator(); const contents = try dir.readFileAllocOptions( self.io, filename, arena, .unlimited, .of(u8), 0 ); const filename_owned = try arena.dupe(u8, filename); try self.files.put(self.gpa, filename_owned, File{ .contents = contents }); return contents; } pub fn readFile(self: *Assets, filename: []const u8) [:0]const u8 { if (self.files.get(filename)) |file| { return file.contents; } else { if (self.readFileFromAssetDir(filename)) |maybe_contents| { if (maybe_contents) |contents| { log.warn("Read file which isn't defined in assets: '{s}'", .{filename}); return contents; } else { log.err("Attempt to read file '{s}', which doesn't exist", .{filename}); return ""; } } else |err| { log.err("Failed to read file from filesystem: {}", .{err}); return ""; } } } fn insertStaticFile(self: *Assets, filename: []const u8, contents: [:0]const u8) !void { try self.files.put(self.gpa, filename, File{ .contents = contents }); } fn deinit(self: *Assets) void { self.arena.deinit(); self.files.deinit(self.gpa); } const File = struct { contents: [:0]const u8 }; }; pub const Init = struct { gpa: Allocator, arena: Allocator, io: Io, assets: *Assets }; pub const Deinit = struct { gpa: Allocator, io: Io, }; pub const Nanoseconds = i96; pub const Frame = struct { t: Nanoseconds, dt: Nanoseconds, gpa: Allocator, arena: Allocator, frame: Allocator, io: Io, assets: *Assets, input: *Input, input_events: []Input.Event, fn nanosecondsToSeconds(nanoseconds: i96) f32 { return @floatCast(@as(f64, @floatFromInt(nanoseconds)) / std.time.ns_per_s); } pub fn deltaTime(self: Frame) f32 { return nanosecondsToSeconds(self.dt); } pub fn time(self: Frame) f32 { return nanosecondsToSeconds(self.t); } }; pub fn run(App: type, opts: RunOptions) void { const arena = opts.init.arena.allocator(); const gpa = opts.init.gpa; const io = opts.init.io; var assets_dir: ?Io.Dir = null; if (build_options.assets_dir) |assets_dir_path| { const cwd = Io.Dir.cwd(); assets_dir = cwd.openDir(io, assets_dir_path, .{}) catch |e| blk: { log.err("Failed to open assets directory: {}", .{e}); break :blk null; }; } const PlatformApp = PlatformType(App); const plt = arena.create(PlatformApp) catch @panic("OOM"); plt.* = PlatformApp{ .gpa = gpa, .arena = opts.init.arena, .io = io, .cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"), .app = undefined, .input = undefined, .events_buffer = undefined, .events_overflow = false, .events = .initBuffer(&plt.events_buffer), .started_at = undefined, .last_frame_at = undefined, .last_key_press_is_repeat = false, .last_key_pressed = null, .frame_arena = .init(gpa), .show_imgui = builtin.mode == .Debug, .assets = .init(gpa, io, assets_dir), .last_mouse_position = null }; inline for (EmbeddedAssets.files) |file| { plt.assets.insertStaticFile(file.name, file.contents) catch @panic("OOM"); } var icon: sapp.IconDesc = .{}; icon.sokol_default = true; // TODO: Don't hard code icon path, allow changing through options // var icon_data = try STBImage.load(@embedFile("../assets/icon.png")); // defer icon_data.deinit(); // TODO: // icon.images[0] = .{ // .width = @intCast(icon_data.width), // .height = @intCast(icon_data.height), // .pixels = .{ // .ptr = icon_data.rgba8_pixels, // .size = icon_data.width * icon_data.height * 4 // } // }; tracy.setThreadName("Main"); const is_wasm = builtin.cpu.arch.isWasm(); if (builtin.os.tag == .linux and !is_wasm) { var sa: std.posix.Sigaction = .{ .handler = .{ .handler = posixSignalHandler }, .mask = std.posix.sigemptyset(), .flags = std.posix.SA.RESTART, }; std.posix.sigaction(std.posix.SIG.INT, &sa, null); } sapp.run(.{ .init_userdata_cb = PlatformApp.sokolInit, .frame_userdata_cb = PlatformApp.sokolFrame, .cleanup_userdata_cb = PlatformApp.sokolCleanup, .event_userdata_cb = PlatformApp.sokolEvent, .user_data = plt, .window_title = opts.window_title, .width = opts.width, .height = opts.height, .icon = icon, .logger = .{ .func = sokolLogCallback }, }); } fn posixSignalHandler(sig: std.posix.SIG) callconv(.c) void { _ = sig; sapp.requestQuit(); } fn toSokolColor(color: Color) sokol.gfx.Color { // TODO: Assert and do cast return sokol.gfx.Color{ .r = color.r, .g = color.g, .b = color.b, .a = color.a, }; } fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void { if (log_level == 0) { log.err(format, args); } else if (log_level == 1) { log.err(format, args); } else if (log_level == 2) { log.warn(format, args); } else { log.info(format, args); } } fn cStrToZig(c_str: [*c]const u8) [:0]const u8 { return std.mem.span(c_str); } fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void { _ = user_data; if (filename != null) { sokolLogFmt( log_level, "[{s}][id:{}] {s}:{}: {s}", .{ cStrToZig(tag orelse "-"), log_item, std.fs.path.basename(cStrToZig(filename orelse "-")), line_nr, cStrToZig(message orelse "") } ); } else { sokolLogFmt( log_level, "[{s}][id:{}] {s}", .{ cStrToZig(tag orelse "-"), log_item, cStrToZig(message orelse "") } ); } }