const std = @import("std"); const log = std.log.scoped(.engine); const assert = std.debug.assert; const sokol = @import("sokol"); const sapp = sokol.app; pub const Input = @import("./input.zig"); const ScreenScalar = @import("./screen_scaler.zig"); pub const imgui = @import("./imgui.zig"); pub const Graphics = @import("./graphics.zig"); pub const Audio = @import("./audio/root.zig"); const tracy = @import("tracy"); const builtin = @import("builtin"); const STBImage = @import("stb_image"); const Gfx = Graphics; const build_options = @import("build_options"); const Lib = @import("lib"); pub const Math = Lib.Math; pub const Vec2 = Math.Vec2; const rgb = Math.rgb; const GameCallbacks = struct { const Init = *const fn(opts: *const Lib.Init) callconv(.c) void; const Tick = *const fn(frame: *Lib.Frame) callconv(.c) void; const Deinit = *const fn() callconv(.c) void; const Debug = *const fn(imgui: *Lib.ImGui) callconv(.c) void; init: Init, tick: Tick, deinit: Deinit, debug: if (build_options.has_imgui) Debug else void }; const GameLinking = union(enum) { static, dynamic: struct { lib: std.DynLib }, pub fn getCallbacks(self: *GameLinking) !GameCallbacks { switch (self.*) { .static => { if (build_options.hot_reload) { return error.StaticLinkingDisabled; } const game = @import("game"); return GameCallbacks{ .init = game.init, .deinit = game.deinit, .tick = game.tick, .debug = if (build_options.has_imgui) game.debug else {}, }; }, .dynamic => |*opts| { const lib = &opts.lib; return GameCallbacks{ .init = lib.lookup(GameCallbacks.Init, "init") orelse return error.MissingFunction, .tick = lib.lookup(GameCallbacks.Tick, "tick") orelse return error.MissingFunction, .deinit = lib.lookup(GameCallbacks.Deinit, "deinit") orelse return error.MissingFunction, .debug = if (build_options.has_imgui) lib.lookup(GameCallbacks.Debug, "debug") orelse return error.MissingFunction else {}, }; } } } }; const Engine = @This(); allocator: std.mem.Allocator, started_at: std.time.Instant, last_frame_at: Lib.Nanoseconds, graphics: Graphics, input: Input, game_linking: GameLinking, game_callbacks: GameCallbacks, frame: Lib.Frame, previous_tick_canvas_size: ?Vec2 = null, const RunOptions = struct { allocator: std.mem.Allocator, game_library_path: ?[]const u8 = null }; pub fn run(self: *Engine, opts: RunOptions) !void { self.* = Engine{ .allocator = opts.allocator, .started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"), .last_frame_at = 0, .frame = undefined, .graphics = undefined, .game_linking = undefined, .game_callbacks = undefined, .input = .{} }; if (opts.game_library_path) |game_library_path| { self.game_linking = .{ .dynamic = .{ .lib = try std.DynLib.open(game_library_path) } }; } else { self.game_linking = .static; } self.game_callbacks = try self.game_linking.getCallbacks(); self.frame.init(self.allocator); tracy.setThreadName("Main"); if (builtin.os.tag == .linux) { 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); } // 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(); var icon: sapp.IconDesc = .{}; icon.sokol_default = true; // 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 // } // }; sapp.run(.{ .init_userdata_cb = sokolInitCallback, .frame_userdata_cb = sokolFrameCallback, .cleanup_userdata_cb = sokolCleanupCallback, .event_userdata_cb = sokolEventCallback, .user_data = self, .width = 640, .height = 480, .icon = icon, .window_title = "Game", .logger = .{ .func = sokolLogCallback }, .win32 = .{ .console_utf8 = true } }); } fn sokolInit(self: *Engine) !void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); log.debug("Init", .{}); try self.graphics.init(.{ .allocator = self.allocator, .logger = .{ .func = sokolLogCallback }, // TODO: // .imgui_font = .{ // .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"), // } }); try Audio.init(.{ .allocator = self.allocator, .logger = .{ .func = sokolLogCallback }, }); const seed: u64 = @bitCast(std.time.milliTimestamp()); const opts = Lib.Init{ .gpa = self.allocator, .seed = seed, }; self.game_callbacks.init(&opts); } fn sokolCleanup(self: *Engine) void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); self.frame.deinit(); Audio.deinit(); self.game_callbacks.deinit(); self.graphics.deinit(self.allocator); } fn sokolFrame(self: *Engine) !void { tracy.frameMark(); const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); const now = std.time.Instant.now() catch @panic("Instant.now() unsupported"); const time_passed = now.since(self.started_at); defer self.last_frame_at = time_passed; const frame = &self.frame; const screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); var maybe_screen_scaler: ?ScreenScalar = null; if (self.previous_tick_canvas_size) |canvas_size| { maybe_screen_scaler = ScreenScalar.init(screen_size, canvas_size); } { _ = frame.arena.reset(.retain_capacity); const arena = frame.arena.allocator(); const audio_commands_capacity = frame.audio_commands.capacity; frame.audio_commands = .empty; try frame.audio_commands.ensureTotalCapacity(arena, audio_commands_capacity); const graphics_commands_capacity = frame.graphics_commands.capacity; frame.graphics_commands = .empty; try frame.graphics_commands.ensureTotalCapacity(arena, graphics_commands_capacity); frame.screen_size = screen_size; frame.time_ns = time_passed; frame.dt_ns = time_passed - self.last_frame_at; frame.mouse_position = self.input.mouse_position; if (maybe_screen_scaler) |screen_scaler| { screen_scaler.push(frame); if (frame.mouse_position) |mouse_position| { frame.mouse_position = mouse_position.sub(screen_scaler.translation).divideScalar(screen_scaler.scale); } } self.game_callbacks.tick(frame); if (maybe_screen_scaler) |screen_scaler| { screen_scaler.pop(frame, frame.clear_color); } frame.keyboard.pressed = .initEmpty(); frame.keyboard.released = .initEmpty(); frame.mouse_button.pressed = .initEmpty(); frame.mouse_button.released = .initEmpty(); } // Canvas size modification must always be applied a frame later. // So that mouse coordinate transformations are consistent. self.previous_tick_canvas_size = self.frame.canvas_size; sapp.showMouse(!self.frame.hide_cursor); { self.graphics.beginFrame(); defer self.graphics.endFrame(frame.clear_color); self.graphics.drawCommands(frame.graphics_commands.items); if (frame.show_debug and build_options.has_imgui) { try self.showDebugWindow(frame); } } for (frame.audio_commands.items) |command| { try Audio.mixer.commands.push(command); } } fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void { if (!imgui.beginWindow(.{ .name = "Debug", .pos = Vec2.init(20, 20), .size = Vec2.init(200, 200), })) { return; } defer imgui.endWindow(); _ = imgui.beginTabBar("debug"); defer imgui.endTabBar(); if (imgui.beginTabItem("Game")) { defer imgui.endTabItem(); var imgui_ctx = Lib.ImGui.init(self.allocator); defer imgui_ctx.deinit(); self.game_callbacks.debug(&imgui_ctx); for (imgui_ctx.commands.items) |cmd| { switch (cmd) { .text => |str| imgui.text(str) } } } if (imgui.beginTabItem("Engine")) { defer imgui.endTabItem(); imgui.textFmt("Draw commands: {}\n", .{ frame.graphics_commands.items.len, }); imgui.textFmt("Audio instances: {}/{}\n", .{ Audio.mixer.instances.items.len, Audio.mixer.instances.capacity }); } } fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); const e = e_ptr.*; if (imgui.handleEvent(e)) { if (self.input.mouse_position != null) { self.input.processEvent(&self.frame, .{ .mouse_leave = {} }); } self.input.mouse_position = null; return true; } blk: switch (e.type) { .MOUSE_DOWN => { const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk; self.input.processEvent(&self.frame, .{ .mouse_pressed = .{ .button = mouse_button, .position = Vec2.init(e.mouse_x, e.mouse_y) } }); return true; }, .MOUSE_UP => { const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk; self.input.processEvent(&self.frame, .{ .mouse_released = .{ .button = mouse_button, .position = Vec2.init(e.mouse_x, e.mouse_y) } }); return true; }, .MOUSE_MOVE => { if (self.input.mouse_position == null) { self.input.processEvent(&self.frame, .{ .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) }); } else { self.input.processEvent(&self.frame, .{ .mouse_move = Vec2.init(e.mouse_x, e.mouse_y) }); } return true; }, .MOUSE_ENTER => { if (self.input.mouse_position == null) { self.input.processEvent(&self.frame, .{ .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) }); } return true; }, .RESIZED => { if (self.input.mouse_position != null) { self.input.processEvent(&self.frame, .{ .mouse_leave = {} }); } self.input.processEvent(&self.frame, .{ .window_resize = {} }); return true; }, .MOUSE_LEAVE => { if (self.input.mouse_position != null) { self.input.processEvent(&self.frame, .{ .mouse_leave = {} }); } return true; }, .MOUSE_SCROLL => { self.input.processEvent(&self.frame, .{ .mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y) }); return true; }, .KEY_DOWN => { const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk; self.input.processEvent(&self.frame, .{ .key_pressed = .{ .code = key_code, .repeat = e.key_repeat } }); return true; }, .KEY_UP => { const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk; self.input.processEvent(&self.frame, .{ .key_released = key_code }); return true; }, .CHAR => { self.input.processEvent(&self.frame, .{ .char = @intCast(e.char_code) }); return true; }, .QUIT_REQUESTED => { // TODO: handle quit request. Maybe show confirmation window in certain cases. }, else => {} } return false; } fn sokolEventCallback(e_ptr: [*c]const sapp.Event, userdata: ?*anyopaque) callconv(.c) void { const engine: *Engine = @alignCast(@ptrCast(userdata)); const consume_event = engine.sokolEvent(e_ptr) catch |e| blk: { log.err("sokolEvent() failed: {}", .{e}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } break :blk false; }; if (consume_event) { sapp.consumeEvent(); } } fn sokolCleanupCallback(userdata: ?*anyopaque) callconv(.c) void { const engine: *Engine = @alignCast(@ptrCast(userdata)); engine.sokolCleanup(); } fn sokolInitCallback(userdata: ?*anyopaque) callconv(.c) void { const engine: *Engine = @alignCast(@ptrCast(userdata)); engine.sokolInit() catch |e| { log.err("sokolInit() failed: {}", .{e}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } sapp.requestQuit(); }; } fn sokolFrameCallback(userdata: ?*anyopaque) callconv(.c) void { const engine: *Engine = @alignCast(@ptrCast(userdata)); engine.sokolFrame() catch |e| { log.err("sokolFrame() failed: {}", .{e}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } sapp.requestQuit(); }; } fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void { const log_sokol = std.log.scoped(.sokol); if (log_level == 0) { log_sokol.err(format, args); } else if (log_level == 1) { log_sokol.err(format, args); } else if (log_level == 2) { log_sokol.warn(format, args); } else { log_sokol.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 "") } ); } } fn posixSignalHandler(sig: i32) callconv(.c) void { _ = sig; sapp.requestQuit(); }