707 lines
22 KiB
Zig
707 lines
22 KiB
Zig
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 "")
|
|
}
|
|
);
|
|
}
|
|
}
|