sokoban-v1/engine/src/runtime/root.zig

540 lines
15 KiB
Zig

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();
}