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; const Math = @import("math"); const Vec2 = Math.Vec2; const Vec4 = Math.Vec4; const Mat4 = Math.Mat4; const tracy = @import("tracy"); pub const Color = @import("./color.zig"); const shd = @import("shader"); const ImGUI = @import("./imgui.zig"); pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) { SPACE = 32, APOSTROPHE = 39, COMMA = 44, MINUS = 45, PERIOD = 46, SLASH = 47, _0 = 48, _1 = 49, _2 = 50, _3 = 51, _4 = 52, _5 = 53, _6 = 54, _7 = 55, _8 = 56, _9 = 57, SEMICOLON = 59, EQUAL = 61, A = 65, B = 66, C = 67, D = 68, E = 69, F = 70, G = 71, H = 72, I = 73, J = 74, K = 75, L = 76, M = 77, N = 78, O = 79, P = 80, Q = 81, R = 82, S = 83, T = 84, U = 85, V = 86, W = 87, X = 88, Y = 89, Z = 90, LEFT_BRACKET = 91, BACKSLASH = 92, RIGHT_BRACKET = 93, GRAVE_ACCENT = 96, WORLD_1 = 161, WORLD_2 = 162, ESCAPE = 256, ENTER = 257, TAB = 258, BACKSPACE = 259, INSERT = 260, DELETE = 261, RIGHT = 262, LEFT = 263, DOWN = 264, UP = 265, PAGE_UP = 266, PAGE_DOWN = 267, HOME = 268, END = 269, CAPS_LOCK = 280, SCROLL_LOCK = 281, NUM_LOCK = 282, PRINT_SCREEN = 283, PAUSE = 284, F1 = 290, F2 = 291, F3 = 292, F4 = 293, F5 = 294, F6 = 295, F7 = 296, F8 = 297, F9 = 298, F10 = 299, F11 = 300, F12 = 301, F13 = 302, F14 = 303, F15 = 304, F16 = 305, F17 = 306, F18 = 307, F19 = 308, F20 = 309, F21 = 310, F22 = 311, F23 = 312, F24 = 313, F25 = 314, KP_0 = 320, KP_1 = 321, KP_2 = 322, KP_3 = 323, KP_4 = 324, KP_5 = 325, KP_6 = 326, KP_7 = 327, KP_8 = 328, KP_9 = 329, KP_DECIMAL = 330, KP_DIVIDE = 331, KP_MULTIPLY = 332, KP_SUBTRACT = 333, KP_ADD = 334, KP_ENTER = 335, KP_EQUAL = 336, LEFT_SHIFT = 340, LEFT_CONTROL = 341, LEFT_ALT = 342, LEFT_SUPER = 343, RIGHT_SHIFT = 344, RIGHT_CONTROL = 345, RIGHT_ALT = 346, RIGHT_SUPER = 347, MENU = 348, }; pub const MouseButton = enum { left, right, middle, pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?MouseButton { return switch(mouse_button) { .LEFT => .left, .RIGHT => .right, .MIDDLE => .middle, else => null }; } }; pub const Event = union(enum) { mouse_pressed: MouseButton, mouse_released: MouseButton, mouse_move: Vec2, mouse_enter: Vec2, mouse_leave, mouse_scroll: Vec2, key_pressed: KeyCode, key_released: KeyCode, char: u21, window_resize: Vec2, focused, unfocused, }; fn KeyStateType(T: type) type { return struct { pressed: std.EnumSet(T), released: std.EnumSet(T), down: std.EnumSet(T), pub const empty = @This(){ .pressed = .empty, .released = .empty, .down = .empty, }; pub fn press(self: *@This(), key: T) void { self.pressed.insert(key); self.down.insert(key); } pub fn release(self: *@This(), key: T) void { self.released.insert(key); self.down.remove(key); } pub fn releaseAll(self: *@This()) void { var iter = self.down.iterator(); while (iter.next()) |key| { self.release(key); } } }; } fn PlatformType(App: type) type { const GraphicsState = struct { const max_quads = 2048; screen_size: Vec2, pipeline: sg.Pipeline, bindings: sg.Bindings, quads_buffer: [max_quads]Quad, fn init(self: *@This()) void { self.* = @This(){ .quads_buffer = undefined, .pipeline = .{}, .bindings = .{}, .screen_size = .init(sapp.widthf(), sapp.heightf()) }; sg.setup(.{ .environment = sglue.environment(), .logger = .{ .func = sokolLogCallback }, }); self.bindings.vertex_buffers[0] = sg.makeBuffer(.{ .size = @sizeOf(Quad) * max_quads, .usage = .{ .vertex_buffer = true, .dynamic_update = true }, .label = "quad-vertices" }); const index_count = max_quads * 6; var indices: [index_count]u16 = undefined; for (0..max_quads) |i| { indices[6*i + 0] = @intCast(4*i + 0); indices[6*i + 1] = @intCast(4*i + 1); indices[6*i + 2] = @intCast(4*i + 2); indices[6*i + 3] = @intCast(4*i + 1); indices[6*i + 4] = @intCast(4*i + 3); indices[6*i + 5] = @intCast(4*i + 2); } self.bindings.index_buffer = sg.makeBuffer(.{ .data = sg.asRange(&indices), .usage = .{ .index_buffer = true, }, .label = "indices" }); const shader = sg.makeShader(shd.mainShaderDesc(sg.queryBackend())); self.pipeline = sg.makePipeline(.{ .primitive_type = .TRIANGLES, .index_type = .UINT16, .colors = init: { var s: [8]sg.ColorTargetState = [_]sg.ColorTargetState{.{}} ** 8; s[0] = .{ .blend = .{ .enabled = true, .src_factor_rgb = .SRC_ALPHA, .dst_factor_rgb = .ONE_MINUS_SRC_ALPHA, .op_rgb = .ADD, .src_factor_alpha = .ONE, .dst_factor_alpha = .ONE_MINUS_SRC_ALPHA, .op_alpha = .ADD, } }; break :init s; }, .shader = shader, .layout = init: { var l = sg.VertexLayoutState{}; l.attrs[shd.ATTR_main_position].format = .FLOAT2; l.attrs[shd.ATTR_main_color0].format = .FLOAT4; break :init l; }, .label = "main-pipeline" }); } fn deinit(self: @This()) void { _ = self; // autofix sg.shutdown(); } fn getInterface(self: *@This()) Graphics { return .{ .clear_color = .black, .screen_size = self.screen_size, .quads = .initBuffer(&self.quads_buffer) }; } }; return struct { const Self = @This(); gpa: Allocator, io: Io, cli_args: []const [:0]const u8, app: *App, gfx: GraphicsState, imgui: if (build_options.has_imgui) ImGUI.State else void, started_at: Io.Timestamp, last_frame_at: Io.Timestamp, events_buffer: [256]Event, events_overflow: bool, last_key_press_is_repeat: bool, input: Input, next_input: Input, fn sokolInit(user_data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(user_data)); self.started_at = Io.Timestamp.now(self.io, .awake); self.last_frame_at = self.started_at; self.gfx.init(); if (build_options.has_imgui) { self.imgui.init(.{ .func = sokolLogCallback }); } if (@hasDecl(App, "init")) { const plt = Init{ }; if (try self.app.init(plt)) |exit_code| { std.process.exit(exit_code); } } } fn createProjectionMatrix(screen_size: Vec2) Mat4 { return Mat4.initIdentity() // Convert normalized [-1; 1] coordinates to [0; 1] .multiply(Mat4.initTranslate(.{ .x = -1, .y = -1, .z = 0 })) .multiply(Mat4.initScale(.{ .x = 2, .y = 2, .z = 0 })) // Make the top-left corner (0,0) .multiply(Mat4.initTranslate(.{ .x = 0, .y = 1, .z = 0 })) .multiply(Mat4.initScale(.{ .x = 1, .y = -1, .z = 0 })) // Scretch [0; 1] so that they map 1-to-1 on to screen coordinates. .multiply(Mat4.initScale(.{ .x = 1/screen_size.x, .y = 1/screen_size.y, .z = 0 })); } fn frame(self: *Self) !void { tracy.frameMark(); if (!self.events_overflow) { self.input = self.next_input; } if (build_options.has_imgui) { if (self.input.keyboard.pressed.contains(.F4)) { self.imgui.enabled = !self.imgui.enabled; } } const now = Io.Timestamp.now(self.io, .awake); var gfx = self.gfx.getInterface(); var plt = Frame{ .t = self.started_at.durationTo(now), .dt = self.last_frame_at.durationTo(now), .gfx = &gfx, .input = &self.input, .imgui = null }; self.last_frame_at = now; var imgui: ImGUI.Interface = undefined; if (build_options.has_imgui) { imgui = self.imgui.getInterface(); plt.imgui = &imgui; } if (@hasDecl(App, "frame")) { try self.app.frame(&plt); } if (gfx.quads.items.len > 0) { sg.updateBuffer( self.gfx.bindings.vertex_buffers[0], sg.asRange(gfx.quads.items) ); } const screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); var vs_params: shd.VsParams = .{ // TODO: Maybe 'view' matrix is not needed. // We probably should only have a combined 'mvp' matrix .view = .initIdentity(), .projection = createProjectionMatrix(screen_size) }; var pass_action: sg.PassAction = .{}; pass_action.colors[0] = .{ .load_action = .CLEAR, .clear_value = toSokolColor(gfx.clear_color) }; sg.beginPass(.{ .action = pass_action, .swapchain = sglue.swapchain() }); sg.applyPipeline(self.gfx.pipeline); sg.applyBindings(self.gfx.bindings); sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params)); sg.draw(0, @intCast(gfx.quads.items.len * 6), 1); if (build_options.has_imgui) { try self.imgui.render(self.gpa, &imgui); } sg.endPass(); sg.commit(); self.events_overflow = false; inline for (&.{ &self.input, &self.next_input }) |input| { input.events.clearRetainingCapacity(); input.keyboard.pressed = .empty; input.keyboard.released = .empty; input.mouse_buttons.pressed = .empty; input.mouse_buttons.released = .empty; } } 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(); }; } fn processEvent(input: *Input, e: Event) void { switch (e) { .key_pressed => |key| { assert(!input.isKeyDown(key)); input.keyboard.press(key); }, .key_released => |key| { assert(input.isKeyDown(key)); input.keyboard.release(key); }, .mouse_pressed => |button| { assert(input.mouse_position != null); assert(!input.isMouseDown(button)); input.mouse_buttons.press(button); }, .mouse_released => |button| { assert(input.mouse_position != null); assert(input.isMouseDown(button)); input.mouse_buttons.release(button); }, .mouse_enter => |pos| { assert(input.mouse_position == null); input.mouse_position = pos; }, .mouse_move => |pos| { assert(input.mouse_position != null); input.mouse_position = pos; }, .mouse_leave => { assert(input.mouse_position != null); input.mouse_position = null; }, .char => |char| { const last_event = input.events.getLast(); assert(last_event == .key_pressed); input.key_code_mapping.put(last_event.key_pressed, char); }, .mouse_scroll => |offset| { assert(input.mouse_position != null); input.mouse_scroll = input.mouse_scroll.add(offset); }, .window_resize => {}, .focused => {}, .unfocused => { assert(input.mouse_position == null); assert(input.keyboard.down.eql(.empty)); assert(input.mouse_buttons.down.eql(.empty)); } } } fn appendEvent(self: *Self, e: Event) void { const input = &self.next_input; if (input.events.capacity == input.events.items.len) { if (!self.events_overflow) { log.warn("too many events, limit: {}", .{input.events.capacity}); } self.events_overflow = true; return; } Self.processEvent(input, e); input.events.appendAssumeCapacity(e); } fn pushEvent(self: *Self, e: Event) void { const input = &self.next_input; if (input.focused) { if (e == .focused) { return; } if (e == .key_pressed and input.isKeyDown(e.key_pressed)) { return; } if (e == .key_released and !input.isKeyDown(e.key_released)) { return; } if (e == .mouse_move and input.mouse_position.?.eql(e.mouse_move)) { 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 }); } if (input.mouse_position != null) { self.appendEvent(.{ .mouse_leave = {} }); } input.focused = false; } } else { if (e == .unfocused) { return; } if (e != .focused) { self.appendEvent(.focused); if (e == .mouse_move) { self.appendEvent(.{ .mouse_enter = e.mouse_move }); } } input.focused = true; } self.appendEvent(e); } fn event(self: *Self, e: sapp.Event) !bool { if (build_options.has_imgui) { if (self.imgui.event(e)) { self.pushEvent(.{ .unfocused = {} }); return true; } } switch (e.type) { .MOUSE_DOWN => { self.pushEvent(.{ .mouse_pressed = MouseButton.fromSokol(e.mouse_button) orelse return false, }); }, .MOUSE_UP => { self.pushEvent(.{ .mouse_released = MouseButton.fromSokol(e.mouse_button) orelse return false, }); }, .MOUSE_MOVE => { 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 (build_options.has_imgui) { self.imgui.deinit(self.gpa); } self.gfx.deinit(); if (@hasDecl(App, "deinit")) { self.app.deinit(); } self.gpa.destroy(self.app); } }; } pub const RunOptions = struct { init: std.process.Init, window_title: [*c]const u8 = null, width: i32 = 800, height: i32 = 600, }; pub const Vertex = extern struct { position: Vec2, color: Vec4, }; pub const Quad = [4]Vertex; pub const Graphics = struct { clear_color: Color, screen_size: Vec2, quads: std.ArrayList(Quad), pub fn drawQuad(self: *Graphics, quad: Quad) void { self.quads.appendBounded(quad) catch { log.warn("max quads reached", .{}); }; } pub fn drawRectangle(self: *Graphics, pos: Vec2, size: Vec2, color: Color) void { const color_vec4 = Vec4{ .x = color.r, .y = color.g, .z = color.b, .w = color.a, }; self.drawQuad(Quad{ Vertex{ .position = pos, .color = color_vec4 }, Vertex{ .position = pos.add(.{ .x = size.x, .y = 0 }), .color = color_vec4 }, Vertex{ .position = pos.add(.{ .x = 0, .y = size.y }), .color = color_vec4 }, Vertex{ .position = pos.add(size), .color = color_vec4 }, }); } pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { const step = to.sub(from).normalized().multiplyScalar(width/2); const top_left = from.add(step.rotateLeft90()); const bottom_left = from.add(step.rotateRight90()); const top_right = to.add(step.rotateLeft90()); const bottom_right = to.add(step.rotateRight90()); drawQuad(Quad{ Vertex{ .position = top_right, .color = color }, Vertex{ .position = top_left, .color = color }, Vertex{ .position = bottom_right, .color = color }, Vertex{ .position = bottom_left, .color = color }, }); } }; pub const Init = struct { }; pub const Input = struct { key_code_mapping: std.EnumMap(KeyCode, u21), focused: bool, keyboard: KeyStateType(KeyCode), mouse_buttons: KeyStateType(MouseButton), mouse_position: ?Vec2, mouse_scroll: Vec2, events: std.ArrayList(Event), fn init(events_buffer: []Event) Input { return .{ .events = .initBuffer(events_buffer), .key_code_mapping = .{}, .keyboard = .empty, .mouse_buttons = .empty, .mouse_position = null, .mouse_scroll = .init(0, 0), .focused = false }; } pub fn isKeyPressed(self: Input, key: KeyCode) bool { return self.keyboard.pressed.contains(key); } pub fn isKeyReleased(self: Input, key: KeyCode) bool { return self.keyboard.released.contains(key); } pub fn isKeyDown(self: Input, key: KeyCode) bool { return self.keyboard.down.contains(key); } pub fn isMousePressed(self: Input, button: MouseButton) bool { return self.mouse_buttons.pressed.contains(button); } pub fn iMouseReleased(self: Input, button: MouseButton) bool { return self.mouse_buttons.released.contains(button); } pub fn isMouseDown(self: Input, button: MouseButton) bool { return self.mouse_buttons.down.contains(button); } }; pub const Frame = struct { t: Io.Duration, dt: Io.Duration, gfx: *Graphics, input: *Input, imgui: ?*ImGUI.Interface, 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.nanoseconds); } pub fn time(self: Frame) f32 { return nanosecondsToSeconds(self.t.nanoseconds); } }; pub fn run(App: type, opts: RunOptions) void { const arena = opts.init.arena.allocator(); const gpa = opts.init.gpa; const PlatformApp = PlatformType(App); const plt = arena.create(PlatformApp) catch @panic("OOM"); plt.* = PlatformApp{ .gpa = gpa, .io = opts.init.io, .cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"), .app = gpa.create(App) catch @panic("OOM"), .gfx = undefined, .imgui = undefined, .events_buffer = undefined, .events_overflow = false, .input = .init(&[0]Event{}), .next_input = .init(&plt.events_buffer), .started_at = undefined, .last_frame_at = undefined, .last_key_press_is_repeat = false, }; 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 "") } ); } }