diff --git a/src/graphics.zig b/src/graphics.zig new file mode 100644 index 0000000..bef4c71 --- /dev/null +++ b/src/graphics.zig @@ -0,0 +1,222 @@ +const std = @import("std"); +const log = std.log.scoped(.graphics); +const assert = std.debug.assert; + +const sokol = @import("sokol"); +const sg = sokol.gfx; +const sapp = sokol.app; +const sglue = sokol.glue; + +const Math = @import("math"); +const Vec2 = Math.Vec2; +const Vec4 = Math.Vec4; +const Mat4 = Math.Mat4; + +const Color = @import("./color.zig"); +const shd = @import("shader"); + +const max_quads = 2048; + +pub const System = struct { + pipeline: sg.Pipeline, + bindings: sg.Bindings, + + pub fn init(self: *System, logger: sg.Logger) void { + self.* = @This(){ + .pipeline = .{}, + .bindings = .{}, + }; + + sg.setup(.{ + .environment = sglue.environment(), + .logger = logger + }); + + 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" + }); + } + + pub fn deinit(self: System) void { + _ = self; // autofix + sg.shutdown(); + } + + 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 + })); + } + + pub fn render(self: *System, instance: Interface) void { + assert(instance.quads.items.len < max_quads); + + if (instance.quads.items.len > 0) { + sg.updateBuffer( + self.bindings.vertex_buffers[0], + sg.asRange(instance.quads.items) + ); + } + + // TODO: Rectify `window_size` vs `gfx.window_size` + const window_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(window_size) + }; + + sg.applyPipeline(self.pipeline); + sg.applyBindings(self.bindings); + sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params)); + sg.draw(0, @intCast(instance.quads.items.len * 6), 1); + } +}; + +pub const Instance = struct { + window_size: Vec2, + quads_buffer: [max_quads]Quad, + + pub fn getInterface(self: *Instance) Interface { + return .{ + .clear_color = .black, + .quads = .initBuffer(&self.quads_buffer) + }; + } +}; + +pub const Interface = struct { + clear_color: Color, + quads: std.ArrayList(Quad), + + pub fn drawQuad(self: *Interface, quad: Quad) void { + self.quads.appendBounded(quad) catch { + log.warn("max quads reached, limit: {}", .{self.quads.capacity}); + }; + } + + pub fn drawRectangle(self: *Interface, 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 Vertex = extern struct { + position: Vec2, + color: Vec4, +}; + +pub const Quad = [4]Vertex; diff --git a/src/imgui.zig b/src/imgui.zig index 117eed8..7e9f300 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -2,6 +2,7 @@ const std = @import("std"); const log = std.log.scoped(.imgui); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const builtin = @import("builtin"); const Color = @import("./color.zig"); const Math = @import("math"); @@ -10,19 +11,13 @@ const Vec2 = Math.Vec2; const sokol = @import("sokol"); const sapp = sokol.app; -pub const State = struct { +pub const System = struct { const simgui = sokol.imgui; const ig = @import("cimgui"); enabled: bool, - commands_buffer: [128]Command, - strings_buffer: [Math.bytes_per_kib * 16]u8, - id_stack_buffer: [64]WidgetId, - - widget_results: WidgetResult.ArrayHashMap, - - pub fn init(self: *State, logger: simgui.Logger) void { + pub fn init(self: *System, logger: simgui.Logger) void { simgui.setup(.{ .logger = logger }); @@ -32,21 +27,19 @@ pub const State = struct { ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_ViewportsEnable; } + const is_debug = builtin.mode == .Debug; self.* = @This(){ - .enabled = true, - .widget_results = .{}, - .commands_buffer = undefined, - .strings_buffer = undefined, - .id_stack_buffer = undefined, + .enabled = is_debug, }; } - pub fn deinit(self: *State, gpa: Allocator) void { + pub fn deinit(self: *System, gpa: Allocator) void { + _ = self; // autofix + _ = gpa; // autofix simgui.shutdown(); - self.widget_results.deinit(gpa); } - pub fn event(self: State, e: sapp.Event) bool { + pub fn event(self: System, e: sapp.Event) bool { if (self.enabled) { return simgui.handleEvent(e); } @@ -54,9 +47,12 @@ pub const State = struct { return false; } - pub fn render(self: *State, gpa: Allocator, imgui: *Interface) !void { + pub fn render(self: *System, gpa: Allocator, imgui: Interface) !std.ArrayList(WidgetResult.WithId) { + var result: std.ArrayList(WidgetResult.WithId) = .empty; + errdefer result.deinit(gpa); + if (!self.enabled) { - return; + return result; } simgui.newFrame(.{ @@ -90,7 +86,6 @@ pub const State = struct { const dockspace_id = ig.igGetID("MyDockSpace"); _ = ig.igDockSpaceEx(dockspace_id, ig.ImVec2{ .x = 0, .y = 0 }, ig.ImGuiDockNodeFlags_PassthruCentralNode, null); - self.widget_results.clearRetainingCapacity(); if (!imgui.overflowOccured()) { for (imgui.commands.items) |cmd| { switch (cmd) { @@ -113,7 +108,10 @@ pub const State = struct { } _ = ig.igBegin(name, p_open, ig.ImGuiWindowFlags_None); if (open == false) { - try self.widget_results.put(gpa, opts.id, .{ .window_changed = open }); + try result.append(gpa, .{ + .id = opts.id, + .result = .{ .window_changed = open } + }); } if (opts.open != null and opts.open == false) { @@ -129,7 +127,10 @@ pub const State = struct { .button => |opts| { const pressed = ig.igButton(imgui.getString(opts.label)); if (pressed) { - try self.widget_results.put(gpa, opts.id, .button_true); + try result.append(gpa, .{ + .id = opts.id, + .result = .button_true + }); } }, .slider => |opts| { @@ -141,7 +142,10 @@ pub const State = struct { opts.max, ); if (changed) { - try self.widget_results.put(gpa, opts.id, .{ .slider_changed = value }); + try result.append(gpa, .{ + .id = opts.id, + .result = .{ .slider_changed = value } + }); } }, .checkbox => |opts| { @@ -151,7 +155,10 @@ pub const State = struct { &value ); if (changed) { - try self.widget_results.put(gpa, opts.id, .{ .checkbox_changed = value }); + try result.append(gpa, .{ + .id = opts.id, + .result = .{ .checkbox_changed = value } + }); } }, .separator => { @@ -179,12 +186,15 @@ pub const State = struct { ); } if (changed) { - try self.widget_results.put(gpa, opts.id, .{ - .color_edit_changed = .{ - .r = color[0], - .g = color[1], - .b = color[2], - .a = color[3], + try result.append(gpa, .{ + .id = opts.id, + .result = .{ + .color_edit_changed = .{ + .r = color[0], + .g = color[1], + .b = color[2], + .a = color[3], + } } }); } @@ -213,18 +223,8 @@ pub const State = struct { } simgui.render(); - } - pub fn getInterface(self: *State) Interface { - return .{ - .widget_results = self.widget_results, - .id_stack = .initBuffer(&self.id_stack_buffer), - .id_stack_overflow = false, - .commands = .initBuffer(&self.commands_buffer), - .commands_overflow = false, - .strings = .initBuffer(&self.strings_buffer), - .strings_overflow = false - }; + return result; } }; @@ -303,6 +303,50 @@ const WidgetResult = union(enum){ window_changed: bool, const ArrayHashMap = std.array_hash_map.Auto(WidgetId, WidgetResult); + + const WithId = struct { + id: WidgetId, + result: WidgetResult + }; +}; + +pub const Instance = struct { + commands_buffer: [128]Command, + strings_buffer: [Math.bytes_per_kib * 16]u8, + id_stack_buffer: [64]WidgetId, + widget_results: WidgetResult.ArrayHashMap, + + pub fn init(self: *Instance) void { + self.* = Instance{ + .widget_results = .{}, + .commands_buffer = undefined, + .strings_buffer = undefined, + .id_stack_buffer = undefined, + }; + } + + pub fn deinit(self: *Instance, gpa: Allocator) void { + self.widget_results.deinit(gpa); + } + + pub fn getInterface(self: *Instance) Interface { + return .{ + .widget_results = self.widget_results, + .id_stack = .initBuffer(&self.id_stack_buffer), + .id_stack_overflow = false, + .commands = .initBuffer(&self.commands_buffer), + .commands_overflow = false, + .strings = .initBuffer(&self.strings_buffer), + .strings_overflow = false + }; + } + + pub fn applyResults(self: *Instance, gpa: Allocator, widget_results: []WidgetResult.WithId) !void { + self.widget_results.clearRetainingCapacity(); + for (widget_results) |result| { + try self.widget_results.put(gpa, result.id, result.result); + } + } }; pub const Interface = struct { @@ -384,9 +428,9 @@ pub const Interface = struct { }; } - fn getString(self: *Interface, index: StringIndex) [*:0]u8 { + fn getString(self: *const Interface, index: StringIndex) [*:0]const u8 { return std.mem.sliceTo( - @as([*:0]u8, @ptrCast(&self.strings.items[index])), + @as([*:0]const u8, @ptrCast(&self.strings.items[index])), 0 ); } diff --git a/src/input.zig b/src/input.zig new file mode 100644 index 0000000..03ba4a5 --- /dev/null +++ b/src/input.zig @@ -0,0 +1,302 @@ +const std = @import("std"); +const assert = std.debug.assert; + +const sokol = @import("sokol"); + +const Math = @import("math"); +const Vec2 = Math.Vec2; + +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); + } + } + }; +} + +pub const State = struct { + key_code_mapping: std.EnumMap(KeyCode, u21), + + focused: bool, + keyboard: KeyStateType(KeyCode), + mouse_buttons: KeyStateType(MouseButton), + mouse_position: ?Vec2, + mouse_scroll: Vec2, + + pub const empty = State{ + .key_code_mapping = .{}, + .focused = false, + .keyboard = .empty, + .mouse_buttons = .empty, + .mouse_position = null, + .mouse_scroll = .init(0, 0), + }; + + pub fn isKeyPressed(self: State, key: KeyCode) bool { + return self.keyboard.pressed.contains(key); + } + + pub fn isKeyReleased(self: State, key: KeyCode) bool { + return self.keyboard.released.contains(key); + } + + pub fn isKeyDown(self: State, key: KeyCode) bool { + return self.keyboard.down.contains(key); + } + + pub fn isMousePressed(self: State, button: MouseButton) bool { + return self.mouse_buttons.pressed.contains(button); + } + + pub fn iMouseReleased(self: State, button: MouseButton) bool { + return self.mouse_buttons.released.contains(button); + } + + pub fn isMouseDown(self: State, button: MouseButton) bool { + return self.mouse_buttons.down.contains(button); + } +}; + +// TODO: I don't like this struct, it's clunky to use and will become more annoying to use over time. +pub const ProcessEventsContext = struct { + input: *State, + last_key_pressed: *?KeyCode, + window_size: *Vec2, + + pub fn process(self: ProcessEventsContext, e: Event) void { + const input = self.input; + + switch (e) { + .key_pressed => |key| { + assert(!input.isKeyDown(key)); + input.keyboard.press(key); + self.last_key_pressed.* = 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| { + assert(self.last_key_pressed.* != null); + input.key_code_mapping.put(self.last_key_pressed.*.?, char); + self.last_key_pressed.* = null; + }, + .mouse_scroll => |offset| { + assert(input.mouse_position != null); + input.mouse_scroll = input.mouse_scroll.add(offset); + }, + .window_resize => |window_size| { + self.window_size.* = window_size; + }, + .focused => { + assert(!input.focused); + input.focused = true; + }, + .unfocused => { + assert(input.focused); + assert(input.mouse_position == null); + assert(input.keyboard.down.eql(.empty)); + assert(input.mouse_buttons.down.eql(.empty)); + input.focused = false; + } + } + } +}; diff --git a/src/platform.zig b/src/platform.zig index fc3fff8..a1d7a90 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -23,280 +23,104 @@ 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); - } - } - }; -} +const Graphics = @import("./graphics.zig"); +const Input = @import("./input.zig"); fn PlatformType(App: type) type { - const GraphicsState = struct { - const max_quads = 2048; + const Runtime = struct { + app: App, - screen_size: Vec2, + time: Io.Duration, + input: Input.State, + gfx: Graphics.Instance, + imgui: if (build_options.has_imgui) ImGUI.Instance else void, + last_key_pressed: ?Input.KeyCode, - pipeline: sg.Pipeline, - bindings: sg.Bindings, + const InitOptions = struct { + window_size: Vec2, + }; - quads_buffer: [max_quads]Quad, - - fn init(self: *@This()) void { + pub fn init(self: *@This(), opts: InitOptions) !?u8 { self.* = @This(){ - .quads_buffer = undefined, - .pipeline = .{}, - .bindings = .{}, - .screen_size = .init(sapp.widthf(), sapp.heightf()) + .app = undefined, + .last_key_pressed = null, + .gfx = .{ + .window_size = opts.window_size, + .quads_buffer = undefined, + }, + .imgui = undefined, + .input = .empty, + .time = .zero }; - 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); + if (build_options.has_imgui) { + self.imgui.init(); } - 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" - }); + if (@hasDecl(App, "init")) { + const plt = Init{ }; + return try self.app.init(plt); + } else { + return null; + } } - fn deinit(self: @This()) void { - _ = self; // autofix - sg.shutdown(); + pub fn deinit(self: *@This(), gpa: Allocator) void { + if (@hasDecl(App, "deinit")) { + self.app.deinit(); + } + + if (build_options.has_imgui) { + self.imgui.deinit(gpa); + } } - fn getInterface(self: *@This()) Graphics { - return .{ - .clear_color = .black, - .screen_size = self.screen_size, - .quads = .initBuffer(&self.quads_buffer) + const FrameResult = struct { + gfx: Graphics.Interface, + imgui: ?ImGUI.Interface, + }; + + pub fn frame(self: *@This(), dt: Io.Duration, events: []Input.Event) !FrameResult { + assert(dt.nanoseconds > 0); + self.time.nanoseconds += dt.nanoseconds; + + self.input.keyboard.pressed = .empty; + self.input.keyboard.released = .empty; + self.input.mouse_buttons.pressed = .empty; + self.input.mouse_buttons.released = .empty; + + const process_events_ctx = Input.ProcessEventsContext{ + .input = &self.input, + .last_key_pressed = &self.last_key_pressed, + .window_size = &self.gfx.window_size }; + for (events) |e| { + process_events_ctx.process(e); + } + + var result = FrameResult{ + .gfx = self.gfx.getInterface(), + .imgui = null + }; + + var plt = Frame{ + .t = self.time, + .dt = dt, + .gfx = &result.gfx, + .input = &self.input, + .input_events = events, + .imgui = null + }; + + if (build_options.has_imgui) { + result.imgui = self.imgui.getInterface(); + plt.imgui = &result.imgui.?; + } + if (@hasDecl(App, "frame")) { + try self.app.frame(&plt); + } + + return result; } }; @@ -307,134 +131,96 @@ fn PlatformType(App: type) type { io: Io, cli_args: []const [:0]const u8, - app: *App, + runtime: Runtime, - gfx: GraphicsState, - imgui: if (build_options.has_imgui) ImGUI.State else void, + gfx: Graphics.System, + imgui: if (build_options.has_imgui) ImGUI.System else void, started_at: Io.Timestamp, last_frame_at: Io.Timestamp, - events_buffer: [256]Event, + events_buffer: [256]Input.Event, events_overflow: bool, + events: std.ArrayList(Input.Event), last_key_press_is_repeat: bool, - input: Input, - next_input: Input, + event_validation: struct { + last_key_pressed: ?Input.KeyCode, + window_size: Vec2, + input: Input.State, + }, fn sokolInit(user_data: ?*anyopaque) callconv(.c) void { - const self: *Self = @ptrCast(@alignCast(user_data)); + 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(); + self.gfx.init(.{ + .func = sokolLogCallback + }); 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); - } + const init_opts = Runtime.InitOptions{ + .window_size = Vec2.init(sapp.widthf(), sapp.heightf()) + }; + if (try self.runtime.init(init_opts)) |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 - })); + self.event_validation = .{ + .input = self.runtime.input, + .window_size = self.runtime.gfx.window_size, + .last_key_pressed = self.runtime.last_key_pressed + }; } fn frame(self: *Self) !void { tracy.frameMark(); + const now = Io.Timestamp.now(self.io, .awake); + const dt = self.last_frame_at.durationTo(now); + self.last_frame_at = now; + + var events: []Input.Event = &.{}; if (!self.events_overflow) { - self.input = self.next_input; + events = self.events.items; } + const frame_result = try self.runtime.frame(dt, events); + + self.events_overflow = false; + self.events.clearRetainingCapacity(); + + self.event_validation = .{ + .last_key_pressed = self.runtime.last_key_pressed, + .window_size = self.runtime.gfx.window_size, + .input = self.runtime.input + }; if (build_options.has_imgui) { - if (self.input.keyboard.pressed.contains(.F4)) { + if (self.runtime.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) + .clear_value = toSokolColor(frame_result.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); + self.gfx.render(frame_result.gfx); if (build_options.has_imgui) { - try self.imgui.render(self.gpa, &imgui); + var results = try self.imgui.render(self.gpa, frame_result.imgui.?); + defer results.deinit(self.gpa); + try self.runtime.imgui.applyResults(self.gpa, results.items); } 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 { @@ -448,74 +234,27 @@ fn PlatformType(App: type) type { }; } - 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) { + 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: {}", .{input.events.capacity}); + log.warn("too many events, limit: {}", .{self.events.capacity}); } self.events_overflow = true; return; } - Self.processEvent(input, e); - input.events.appendAssumeCapacity(e); + self.events.appendAssumeCapacity(e); + + const process_events_ctx = Input.ProcessEventsContext{ + .input = &self.event_validation.input, + .last_key_pressed = &self.event_validation.last_key_pressed, + .window_size = &self.event_validation.window_size + }; + process_events_ctx.process(e); } - fn pushEvent(self: *Self, e: Event) void { - const input = &self.next_input; + fn pushEvent(self: *Self, e: Input.Event) void { + const input = &self.event_validation.input; if (input.focused) { if (e == .focused) { @@ -544,8 +283,6 @@ fn PlatformType(App: type) type { if (input.mouse_position != null) { self.appendEvent(.{ .mouse_leave = {} }); } - - input.focused = false; } } else { @@ -560,7 +297,6 @@ fn PlatformType(App: type) type { self.appendEvent(.{ .mouse_enter = e.mouse_move }); } } - input.focused = true; } self.appendEvent(e); @@ -579,12 +315,12 @@ fn PlatformType(App: type) type { switch (e.type) { .MOUSE_DOWN => { self.pushEvent(.{ - .mouse_pressed = MouseButton.fromSokol(e.mouse_button) orelse return false, + .mouse_pressed = Input.MouseButton.fromSokol(e.mouse_button) orelse return false, }); }, .MOUSE_UP => { self.pushEvent(.{ - .mouse_released = MouseButton.fromSokol(e.mouse_button) orelse return false, + .mouse_released = Input.MouseButton.fromSokol(e.mouse_button) orelse return false, }); }, .MOUSE_MOVE => { @@ -662,15 +398,11 @@ fn PlatformType(App: type) type { fn sokolCleanup(user_data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(user_data)); + self.runtime.deinit(self.gpa); 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); } }; } @@ -683,138 +415,16 @@ pub const RunOptions = struct { 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, + gfx: *Graphics.Interface, + input: *Input.State, + input_events: []Input.Event, imgui: ?*ImGUI.Interface, fn nanosecondsToSeconds(nanoseconds: i96) f32 { @@ -840,16 +450,16 @@ pub fn run(App: type, opts: RunOptions) void { .gpa = gpa, .io = opts.init.io, .cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"), - .app = gpa.create(App) catch @panic("OOM"), + .runtime = undefined, .gfx = undefined, .imgui = undefined, .events_buffer = undefined, .events_overflow = false, - .input = .init(&[0]Event{}), - .next_input = .init(&plt.events_buffer), + .events = .initBuffer(&plt.events_buffer), .started_at = undefined, .last_frame_at = undefined, .last_key_press_is_repeat = false, + .event_validation = undefined, }; var icon: sapp.IconDesc = .{};