diff --git a/src/app.zig b/src/app.zig index 64b1842..ea075ce 100644 --- a/src/app.zig +++ b/src/app.zig @@ -1,11 +1,16 @@ const std = @import("std"); const CLI = @import("./cli.zig"); -const Platform = @import("./platform.zig"); const log = std.log.scoped(.app); const Math = @import("math"); const Vec2 = Math.Vec2; +const STBImage = @import("stb_image"); + +const Platform = @import("./platform.zig"); +const Gfx = Platform.Gfx; +const ImGUI = Platform.ImGUI; + const App = @This(); bg: Platform.Color, @@ -20,12 +25,22 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { .show_first_window = true, .check = false, }; - _ = plt; // autofix + const cwd = std.Io.Dir.cwd(); + const sokoban_spritesheet = try cwd.readFileAlloc( + plt.io, + "assets/sokoban/Spritesheet/sokoban_spritesheet.png", + plt.gpa, + .unlimited + ); + defer plt.gpa.free(sokoban_spritesheet); + + var image = try STBImage.load(sokoban_spritesheet); + defer image.deinit(); + return null; } -pub fn frame(self: *App, plt: *Platform.Frame) !void { - const gfx = plt.gfx; +pub fn frame(self: *App, plt: Platform.Frame) !void { const input = plt.input; const dt = plt.deltaTime(); @@ -46,36 +61,35 @@ pub fn frame(self: *App, plt: *Platform.Frame) !void { dir = dir.normalized(); self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt)); - gfx.drawRectangle(self.player_pos, .init(100, 100), .rgb(200, 0, 0)); + Gfx.setClearColor(self.bg); + Gfx.drawRectangle(self.player_pos, .init(100, 100), .rgb(200, 0, 0)); + Gfx.drawRectangle(self.player_pos.add(.init(0, 200)), .init(100, 100), .rgb(200, 200, 0)); - if (plt.imgui) |imgui| { - if (imgui.beginWindow(.{ - .name = "Hello", - .size = .init(400, 200), - .open = &self.show_first_window + if (ImGUI.beginWindow(.{ + .name = "Hello", + .size = .init(400, 200), + .open = &self.show_first_window + })) { + defer ImGUI.endWindow(); + + ImGUI.text("Player pos: {}", .{self.player_pos}); + if (ImGUI.button("foo")) { + std.debug.print("pressed\n", .{}); + } + if (ImGUI.checkbox("Hello", &self.check)) { + std.debug.print("checkbox changed\n", .{}); + } + ImGUI.separator(); + if (ImGUI.colorEdit(.{ + .label = "Color", + .value = &self.bg, })) { - defer imgui.endWindow(); - - imgui.text("Player pos: {}", .{self.player_pos}); - if (imgui.button("foo")) { - std.debug.print("pressed\n", .{}); - } - if (imgui.checkbox("Hello", &self.check)) { - std.debug.print("checkbox changed\n", .{}); - } - imgui.separator(); - if (imgui.colorEdit(.{ - .label = "Color", - .value = &self.bg, - })) { - std.debug.print("color changed\n", .{}); - } + std.debug.print("color changed\n", .{}); } } - - gfx.clear_color = self.bg; } -pub fn deinit(self: *App) void { +pub fn deinit(self: *App, plt: Platform.Deinit) void { + _ = plt; // autofix _ = self; // autofix } diff --git a/src/graphics.zig b/src/graphics.zig index 9966ab2..332ca7b 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -14,10 +14,10 @@ const Mat4 = Math.Mat4; const Color = @import("./color.zig"); const shd = @import("shader"); +const SlotMapType = @import("slot_map.zig").SlotMapType; -const max_quads = 2048; -pub const System = struct { +const State = struct { shader: sg.Shader, pipeline: sg.Pipeline, bindings: sg.Bindings, @@ -26,253 +26,343 @@ pub const System = struct { default_view: sg.View, default_sampler: sg.Sampler, - pub fn init(self: *System, logger: sg.Logger) void { - sg.setup(.{ - .environment = sglue.environment(), - .logger = logger - }); - - const shader = sg.makeShader(shd.mainShaderDesc(sg.queryBackend())); - assert(shader.id != sg.invalid_id); - - var pixels: [8 * 8 * 4]u8 = undefined; - @memset(&pixels, 0xFF); - var image_data: sg.ImageData = .{}; - image_data.mip_levels[0] = sg.asRange(&pixels); - const default_image = sg.makeImage(.{ - .type = ._2D, - .width = 8, - .height = 8, - .num_mipmaps = 1, - .pixel_format = .RGBA8, - .data = image_data, - .label = "default-texture" - }); - assert(default_image.id != sg.invalid_id); - - const default_view = sg.makeView(.{ - .texture = .{ .image = default_image }, - .label = "default-view" - }); - assert(default_view.id != sg.invalid_id); - - const default_sampler = sg.makeSampler(.{ - .min_filter = .NEAREST, - .mag_filter = .NEAREST, - .label = "default-sampler" - }); - assert(default_sampler.id != sg.invalid_id); - - const 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_texcoord0].format = .FLOAT2; - l.attrs[shd.ATTR_main_color0].format = .FLOAT4; - break :init l; - }, - .label = "main-pipeline" - }); - assert(pipeline.id != sg.invalid_id); - - var bindings: sg.Bindings = .{}; - { - 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); - } - - bindings.index_buffer = sg.makeBuffer(.{ - .data = sg.asRange(&indices), - .usage = .{ .index_buffer = true, }, - .label = "indices" - }); - - bindings.views[shd.VIEW_tex] = default_view; - bindings.samplers[shd.SMP_smp] = default_sampler; - } - - self.* = @This(){ - .shader = shader, - .pipeline = pipeline, - .bindings = bindings, - .default_image = default_image, - .default_view = default_view, - .default_sampler = default_sampler - }; - } - - pub fn deinit(self: System) void { - sg.destroyPipeline(self.pipeline); - sg.destroyShader(self.shader); - sg.destroyImage(self.default_image); - sg.destroyView(self.default_view); - sg.destroySampler(self.default_sampler); - 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_buffer: [2048]Quad, 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, - .texcoord = .init(0, 0) - }, - Vertex{ - .position = pos.add(.{ .x = size.x, .y = 0 }), - .color = color_vec4, - .texcoord = .init(1, 0) - }, - Vertex{ - .position = pos.add(.{ .x = 0, .y = size.y }), - .color = color_vec4, - .texcoord = .init(0, 1) - }, - Vertex{ - .position = pos.add(size), - .color = color_vec4, - .texcoord = .init(1, 1) - }, - }); - } - - 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, - .texcoord = .init(0, 0) - }, - Vertex{ - .position = top_left, - .color = color, - .texcoord = .init(1, 0) - }, - Vertex{ - .position = bottom_right, - .color = color, - .texcoord = .init(0, 1) - }, - Vertex{ - .position = bottom_left, - .color = color, - .texcoord = .init(1, 1) - }, - }); - } + clear_color: Color, }; +var g_state: State = undefined; + +pub fn init(logger: sg.Logger) void { + const self = &g_state; + + sg.setup(.{ + .environment = sglue.environment(), + .logger = logger + }); + + const shader = sg.makeShader(shd.mainShaderDesc(sg.queryBackend())); + assert(shader.id != sg.invalid_id); + + var pixels: [8 * 8 * 4]u8 = undefined; + @memset(&pixels, 0xFF); + var image_data: sg.ImageData = .{}; + image_data.mip_levels[0] = sg.asRange(&pixels); + const default_image = sg.makeImage(.{ + .type = ._2D, + .width = 8, + .height = 8, + .num_mipmaps = 1, + .pixel_format = .RGBA8, + .data = image_data, + .label = "default-texture" + }); + assert(default_image.id != sg.invalid_id); + + const default_view = sg.makeView(.{ + .texture = .{ .image = default_image }, + .label = "default-view" + }); + assert(default_view.id != sg.invalid_id); + + const default_sampler = sg.makeSampler(.{ + .min_filter = .NEAREST, + .mag_filter = .NEAREST, + .label = "default-sampler" + }); + assert(default_sampler.id != sg.invalid_id); + + const 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_texcoord0].format = .FLOAT2; + l.attrs[shd.ATTR_main_color0].format = .FLOAT4; + break :init l; + }, + .label = "main-pipeline" + }); + assert(pipeline.id != sg.invalid_id); + + var bindings: sg.Bindings = .{}; + { + const max_quads = self.quads_buffer.len; + + 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); + } + + bindings.index_buffer = sg.makeBuffer(.{ + .data = sg.asRange(&indices), + .usage = .{ .index_buffer = true, }, + .label = "indices" + }); + } + + self.* = State{ + .shader = shader, + .pipeline = pipeline, + .bindings = bindings, + .default_image = default_image, + .default_view = default_view, + .default_sampler = default_sampler, + .quads_buffer = undefined, + .quads = .initBuffer(&self.quads_buffer), + .clear_color = .black + }; +} + +pub fn deinit() void { + const self = &g_state; + + sg.destroyPipeline(self.pipeline); + sg.destroyShader(self.shader); + sg.destroyImage(self.default_image); + sg.destroyView(self.default_view); + sg.destroySampler(self.default_sampler); + 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 + })); +} + +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, + }; +} + +pub fn beginFrame() void { + const self = &g_state; + + var pass_action: sg.PassAction = .{}; + pass_action.colors[0] = .{ + .load_action = .CLEAR, + .clear_value = toSokolColor(self.clear_color) + }; + + sg.beginPass(.{ + .action = pass_action, + .swapchain = sglue.swapchain() + }); +} + +// TODO: This will always have a 1-frame delay. +// fix this. +pub fn setClearColor(color: Color) void { + const self = &g_state; + + self.clear_color = color; +} + +pub fn flush() void { + const self = &g_state; + + if (self.quads.items.len == 0) { + return; + } + + sg.updateBuffer( + self.bindings.vertex_buffers[0], + sg.asRange(self.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) + }; + + self.bindings.views[shd.VIEW_tex] = self.default_view; + self.bindings.samplers[shd.SMP_smp] = self.default_sampler; + + sg.applyPipeline(self.pipeline); + sg.applyBindings(self.bindings); + sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params)); + sg.draw(0, @intCast(self.quads.items.len * 6), 1); + + self.quads.clearRetainingCapacity(); +} + +pub fn endFrame() void { + flush(); + + sg.endPass(); + sg.commit(); +} + +pub fn drawQuad(quad: Quad) void { + const self = &g_state; + + if (self.quads.items.len == self.quads.capacity) { + flush(); + } + + self.quads.appendAssumeCapacity(quad); +} + +pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void { + const color_vec4 = Vec4{ + .x = color.r, + .y = color.g, + .z = color.b, + .w = color.a, + }; + drawQuad(Quad{ + Vertex{ + .position = pos, + .color = color_vec4, + .texcoord = .init(0, 0) + }, + Vertex{ + .position = pos.add(.{ .x = size.x, .y = 0 }), + .color = color_vec4, + .texcoord = .init(1, 0) + }, + Vertex{ + .position = pos.add(.{ .x = 0, .y = size.y }), + .color = color_vec4, + .texcoord = .init(0, 1) + }, + Vertex{ + .position = pos.add(size), + .color = color_vec4, + .texcoord = .init(1, 1) + }, + }); +} + +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, + .texcoord = .init(0, 0) + }, + Vertex{ + .position = top_left, + .color = color, + .texcoord = .init(1, 0) + }, + Vertex{ + .position = bottom_right, + .color = color, + .texcoord = .init(0, 1) + }, + Vertex{ + .position = bottom_left, + .color = color, + .texcoord = .init(1, 1) + }, + }); +} + +// const SlotMap = SlotMapType(u16, u8); +// +// const TextureId = u64; +// +// pub const Interface = struct { +// clear_color: Color, +// +// quads: std.ArrayList(Quad), +// quads_overflow: bool, +// +// commands: std.ArrayList(Command), +// commands_overflow: bool, +// +// // blob: std.ArrayList(u8), +// // blob_overflow: bool, +// +// fn pushCommand(self: *Interface, cmd: Command) void { +// self.commands.appendBounded(cmd) catch { +// if (!self.commands_overflow) { +// log.err("max commands reached, limit: {}", .{self.commands.capacity}); +// } +// self.commands_overflow = true; +// }; +// } +// +// pub fn createTexture(self: *Interface) void { +// _ = self; // autofix +// } +// +// pub fn destroyTexture(self: *Interface) void { +// _ = self; // autofix +// } +// +// pub fn setTextureData(self: *Interface, rgba8_pixels: [*]u8, width: u32, height: u32) void { +// _ = rgba8_pixels; // autofix +// _ = width; // autofix +// _ = height; // autofix +// _ = self; // autofix +// } +// +// } +// }; + pub const Vertex = extern struct { position: Vec2, texcoord: Vec2, diff --git a/src/imgui.zig b/src/imgui.zig index 7e9f300..3437ede 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -4,591 +4,288 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const builtin = @import("builtin"); +const build_options = @import("build_options"); const Color = @import("./color.zig"); const Math = @import("math"); const Vec2 = Math.Vec2; const sokol = @import("sokol"); const sapp = sokol.app; +const simgui = sokol.imgui; -pub const System = struct { - const simgui = sokol.imgui; - const ig = @import("cimgui"); +const ig = @import("cimgui"); - enabled: bool, +const ImGUI = @This(); - pub fn init(self: *System, logger: simgui.Logger) void { - simgui.setup(.{ - .logger = logger - }); +const State = struct { + frame: ?Allocator +}; - if (@hasDecl(ig, "ImGuiConfigFlags_DockingEnable")) { - ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_DockingEnable; - ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_ViewportsEnable; - } +var g_state: State = undefined; - const is_debug = builtin.mode == .Debug; - self.* = @This(){ - .enabled = is_debug, - }; +pub fn init(logger: simgui.Logger) void { + if (!build_options.has_imgui) { + return; } - pub fn deinit(self: *System, gpa: Allocator) void { - _ = self; // autofix - _ = gpa; // autofix - simgui.shutdown(); + const self = &g_state; + + simgui.setup(.{ + .logger = logger + }); + + if (@hasDecl(ig, "ImGuiConfigFlags_DockingEnable")) { + ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_DockingEnable; + ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_ViewportsEnable; } - pub fn event(self: System, e: sapp.Event) bool { - if (self.enabled) { - return simgui.handleEvent(e); - } + self.* = State{ + .frame = null + }; +} +pub fn deinit() void { + if (!build_options.has_imgui) { + return; + } + + simgui.shutdown(); +} + +pub fn event(e: sapp.Event) bool { + if (!build_options.has_imgui) { return false; } - pub fn render(self: *System, gpa: Allocator, imgui: Interface) !std.ArrayList(WidgetResult.WithId) { - var result: std.ArrayList(WidgetResult.WithId) = .empty; - errdefer result.deinit(gpa); + return simgui.handleEvent(e); +} - if (!self.enabled) { - return result; - } +pub fn beginFrame(frame: Allocator) void { + if (!build_options.has_imgui) { + return; + } - simgui.newFrame(.{ - .width = sapp.width(), - .height = sapp.height(), - .delta_time = sapp.frameDuration(), - .dpi_scale = sapp.dpiScale(), - }); + const self = &g_state; - const viewport = ig.igGetMainViewport(); - ig.igSetNextWindowPos(viewport[0].WorkPos, ig.ImGuiCond_Always); - ig.igSetNextWindowSize(viewport[0].WorkSize, ig.ImGuiCond_Always); - ig.igSetNextWindowViewport(viewport[0].ID); + self.frame = frame; - const window_flags = - ig.ImGuiWindowFlags_NoDocking | - ig.ImGuiWindowFlags_NoTitleBar | - ig.ImGuiWindowFlags_NoCollapse | - ig.ImGuiWindowFlags_NoResize | - ig.ImGuiWindowFlags_NoMove | - ig.ImGuiWindowFlags_NoBackground | - ig.ImGuiWindowFlags_NoBringToFrontOnFocus | - ig.ImGuiWindowFlags_NoNavFocus; + simgui.newFrame(.{ + .width = sapp.width(), + .height = sapp.height(), + .delta_time = sapp.frameDuration(), + .dpi_scale = sapp.dpiScale(), + }); - ig.igPushStyleVar(ig.ImGuiStyleVar_WindowRounding, 0.0); - ig.igPushStyleVar(ig.ImGuiStyleVar_WindowBorderSize, 0.0); - ig.igPushStyleVarImVec2(ig.ImGuiStyleVar_WindowPadding, .{ .x = 0, .y = 0 }); - _ = ig.igBegin("DockSpace", null, window_flags); - ig.igPopStyleVarEx(3); + const viewport = ig.igGetMainViewport(); + ig.igSetNextWindowPos(viewport[0].WorkPos, ig.ImGuiCond_Always); + ig.igSetNextWindowSize(viewport[0].WorkSize, ig.ImGuiCond_Always); + ig.igSetNextWindowViewport(viewport[0].ID); - const dockspace_id = ig.igGetID("MyDockSpace"); - _ = ig.igDockSpaceEx(dockspace_id, ig.ImVec2{ .x = 0, .y = 0 }, ig.ImGuiDockNodeFlags_PassthruCentralNode, null); + const window_flags = + ig.ImGuiWindowFlags_NoDocking | + ig.ImGuiWindowFlags_NoTitleBar | + ig.ImGuiWindowFlags_NoCollapse | + ig.ImGuiWindowFlags_NoResize | + ig.ImGuiWindowFlags_NoMove | + ig.ImGuiWindowFlags_NoBackground | + ig.ImGuiWindowFlags_NoBringToFrontOnFocus | + ig.ImGuiWindowFlags_NoNavFocus; - if (!imgui.overflowOccured()) { - for (imgui.commands.items) |cmd| { - switch (cmd) { - .begin_window => |opts| { - if (opts.pos) |pos| { - ig.igSetNextWindowPos(.{ .x = pos.x, .y = pos.y }, ig.ImGuiCond_Once); - } - if (opts.size) |size| { - ig.igSetNextWindowSize(.{ .x = size.x, .y = size.y }, ig.ImGuiCond_Once); - } + ig.igPushStyleVar(ig.ImGuiStyleVar_WindowRounding, 0.0); + ig.igPushStyleVar(ig.ImGuiStyleVar_WindowBorderSize, 0.0); + ig.igPushStyleVarImVec2(ig.ImGuiStyleVar_WindowPadding, .{ .x = 0, .y = 0 }); + _ = ig.igBegin("DockSpace", null, window_flags); + ig.igPopStyleVarEx(3); - ig.igSetNextWindowBgAlpha(1); + const dockspace_id = ig.igGetID("MyDockSpace"); + _ = ig.igDockSpaceEx(dockspace_id, ig.ImVec2{ .x = 0, .y = 0 }, ig.ImGuiDockNodeFlags_PassthruCentralNode, null); +} - const name = imgui.getString(opts.name); +pub fn endFrame() void { + if (!build_options.has_imgui) { + return; + } - var open: bool = true; - var p_open: ?*bool = null; - if (opts.open != null and opts.open == true) { - p_open = &open; - } - _ = ig.igBegin(name, p_open, ig.ImGuiWindowFlags_None); - if (open == false) { - try result.append(gpa, .{ - .id = opts.id, - .result = .{ .window_changed = open } - }); - } + // TODO: + // if (ig.igBegin("Game", null, ig.ImGuiWindowFlags_None)) { + // // std.debug.print("{}\n", .{ig.igGetContentRegionAvail()}); + // } + // ig.igEnd(); - if (opts.open != null and opts.open == false) { - ig.igEnd(); - } - }, - .end => { - ig.igEnd(); - }, - .text => |str_index| { - ig.igTextUnformatted(imgui.getString(str_index)); - }, - .button => |opts| { - const pressed = ig.igButton(imgui.getString(opts.label)); - if (pressed) { - try result.append(gpa, .{ - .id = opts.id, - .result = .button_true - }); - } - }, - .slider => |opts| { - var value = opts.value; - const changed = ig.igSliderFloat( - imgui.getString(opts.label), - &value, - opts.min, - opts.max, - ); - if (changed) { - try result.append(gpa, .{ - .id = opts.id, - .result = .{ .slider_changed = value } - }); - } - }, - .checkbox => |opts| { - var value = opts.value; - const changed = ig.igCheckbox( - imgui.getString(opts.label), - &value - ); - if (changed) { - try result.append(gpa, .{ - .id = opts.id, - .result = .{ .checkbox_changed = value } - }); - } - }, - .separator => { - ig.igSeparator(); - }, - .color_edit => |opts| { - var color: [4]f32 = .{ - opts.value.r, - opts.value.g, - opts.value.b, - opts.value.a, - }; - var changed = false; - if (opts.has_alpha) { - changed = ig.igColorEdit4( - imgui.getString(opts.label), - &color, - ig.ImGuiColorEditFlags_None - ); - } else { - changed = ig.igColorEdit3( - imgui.getString(opts.label), - &color, - ig.ImGuiColorEditFlags_None - ); - } - if (changed) { - try result.append(gpa, .{ - .id = opts.id, - .result = .{ - .color_edit_changed = .{ - .r = color[0], - .g = color[1], - .b = color[2], - .a = color[3], - } - } - }); - } - } - } + ig.igEnd(); + + if (ig.igBeginMainMenuBar()) { + defer ig.igEndMainMenuBar(); + + if (ig.igBeginMenu("foo")) { + defer ig.igEndMenu(); + + if (ig.igMenuItem("bar")) { } } - - // TODO: - // if (ig.igBegin("Game", null, ig.ImGuiWindowFlags_None)) { - // // std.debug.print("{}\n", .{ig.igGetContentRegionAvail()}); - // } - // ig.igEnd(); - - ig.igEnd(); - - if (ig.igBeginMainMenuBar()) { - defer ig.igEndMainMenuBar(); - - if (ig.igBeginMenu("foo")) { - defer ig.igEndMenu(); - - if (ig.igMenuItem("bar")) { - } - } - } - - simgui.render(); - - return result; } + + simgui.render(); +} + +pub const WindowOptions = struct { + name: []const u8, + pos: ?Vec2 = null, + size: ?Vec2 = null, + collapsed: ?bool = null, + open: ?*bool = null, }; -const WidgetId = enum(u64) { - _, - - const StringHasher = std.hash.Wyhash; - const CombineHasher = std.hash.Fnv1a_64; - - pub fn init(hash: u64) WidgetId { - return @enumFromInt(hash); +pub fn beginWindow(opts: WindowOptions) bool { + if (!build_options.has_imgui) { + return false; } - pub fn initPtr(ptr: anytype) WidgetId { - return initUsize(@intFromPtr(ptr)); + if (opts.open != null and opts.open.?.* == false) { + return false; } - pub fn initUsize(num: usize) WidgetId { - return init(@truncate(num)); + if (opts.pos) |pos| { + ig.igSetNextWindowPos(toImVec2(pos), ig.ImGuiCond_Once); + } + if (opts.size) |size| { + ig.igSetNextWindowSize(toImVec2(size), ig.ImGuiCond_Once); + } + if (opts.collapsed) |collapsed| { + ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once); } - pub fn initString(seed: u64, str: []const u8) WidgetId { - return init(StringHasher.hash(seed, str)); - } + ig.igSetNextWindowBgAlpha(1); - pub fn combine(self: WidgetId, other: WidgetId) WidgetId { - var hasher = CombineHasher.init(); - hasher.update(std.mem.asBytes(&@intFromEnum(self))); - hasher.update(std.mem.asBytes(&@intFromEnum(other))); - - return init(hasher.final()); - } -}; - -const StringIndex = u16; -const Command = union(enum) { - begin_window: struct { - id: WidgetId, - name: StringIndex, - open: ?bool, - pos: ?Vec2, - size: ?Vec2, - }, - text: StringIndex, - button: struct { - id: WidgetId, - label: StringIndex - }, - slider: struct { - id: WidgetId, - label: StringIndex, - value: f32, - min: f32, - max: f32, - }, - checkbox: struct { - id: WidgetId, - label: StringIndex, - value: bool, - }, - separator, - color_edit: struct { - id: WidgetId, - label: StringIndex, - value: Color, - has_alpha: bool - }, - end, -}; - -const WidgetResult = union(enum){ - button_true, - slider_changed: f32, - checkbox_changed: bool, - color_edit_changed: Color, - 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 { - commands: std.ArrayList(Command), - commands_overflow: bool, - - strings: std.ArrayList(u8), - strings_overflow: bool, - - id_stack: std.ArrayList(WidgetId), - id_stack_overflow: bool, - - widget_results: WidgetResult.ArrayHashMap, - - fn overflowOccured(self: Interface) bool { - return self.commands_overflow or self.strings_overflow or self.id_stack_overflow; - } - - fn pushCommand(self: *Interface, cmd: Command) void { - self.commands.appendBounded(cmd) catch { - if (!self.commands_overflow) { - log.warn("commands overflow, limit: {}", .{self.commands.capacity}); - } - self.commands_overflow = true; - }; - } - - fn pushId(self: *Interface, id: WidgetId) void { - self.id_stack.appendBounded(id) catch { - if (!self.id_stack_overflow) { - log.warn("imgui id stack overflow, limit: {}", .{self.id_stack.capacity}); - } - self.id_stack_overflow = true; - }; - } - - fn popId(self: *Interface) void { - _ = self.id_stack.pop(); - } - - fn tryAllocString(self: *Interface, str: []const u8) !StringIndex { - const result = self.strings.items.len; - try self.strings.appendSliceBounded(str); - try self.strings.appendBounded(0); - assert(result <= std.math.maxInt(StringIndex)); - return @intCast(result); - } - - fn tryFormatString(self: *Interface, comptime fmt: []const u8, args: anytype) !StringIndex { - const result = self.strings.items.len; - const formatted = try std.fmt.bufPrintSentinel( - self.strings.unusedCapacitySlice(), - fmt, - args, - 0 - ); - self.strings.items.len += formatted.len + 1; - assert(result <= std.math.maxInt(StringIndex)); - return @intCast(result); - } - - fn allocString(self: *Interface, str: []const u8) ?StringIndex { - return self.tryAllocString(str) catch { - if (!self.strings_overflow) { - log.warn("imgui strings overflow, limit: {}", .{self.strings.capacity}); - } - self.strings_overflow = true; - return null; - }; - } - - fn formatString(self: *Interface, comptime fmt: []const u8, args: anytype) ?StringIndex { - return self.tryFormatString(fmt, args) catch { - if (!self.strings_overflow) { - log.warn("imgui strings overflow, limit: {}", .{self.strings.capacity}); - } - self.strings_overflow = true; - return null; - }; - } - - fn getString(self: *const Interface, index: StringIndex) [*:0]const u8 { - return std.mem.sliceTo( - @as([*:0]const u8, @ptrCast(&self.strings.items[index])), - 0 - ); - } - - fn getParentId(self: *Interface) WidgetId { - if (self.id_stack.getLastOrNull()) |top| { - return top; - } else { - return WidgetId.init(0); - } - } - - fn initWidgetIdFromLabel(self: *Interface, label: []const u8) WidgetId { - return WidgetId.initString(@intFromEnum(self.getParentId()), label); - } - - pub const WindowOptions = struct { - name: []const u8, - pos: ?Vec2 = null, - size: ?Vec2 = null, - open: ?*bool = null, + const frame = g_state.frame.?; + const namez = frame.dupeSentinel(u8, opts.name, 0) catch blk: { + log.err("dupeSentinel() failed",.{}); + break :blk ""; }; - pub fn beginWindow(self: *Interface, opts: WindowOptions) bool { - const id = self.initWidgetIdFromLabel(opts.name); - - if (self.widget_results.get(id)) |result| { - if (result == .window_changed and opts.open != null) { - opts.open.?.* = result.window_changed; - } + var open = ig.igBegin(namez, opts.open, ig.ImGuiWindowFlags_None); + if (opts.open) |opts_open| { + if (opts_open.* == false) { + open = false; } - - if (opts.open != null and opts.open.?.* == false) { - return false; - } - - self.pushCommand(.{ - .begin_window = .{ - .id = id, - .name = self.allocString(opts.name) orelse return true, - .size = opts.size, - .pos = opts.pos, - .open = if (opts.open != null) opts.open.?.* else null - } - }); - self.pushId(id); - - return true; + } + if (!open) { + endWindow(); } - pub fn endWindow(self: *Interface) void { - self.pushCommand(.end); - self.popId(); + return open; +} + +pub fn endWindow() void { + if (!build_options.has_imgui) { + return; } - pub fn text(self: *Interface, comptime fmt: []const u8, args: anytype) void { - self.pushCommand(.{ - .text = self.formatString(fmt, args) orelse return - }); - } + ig.igEnd(); +} - pub fn button(self: *Interface, label: []const u8) bool { - const id = self.initWidgetIdFromLabel(label); - - self.pushCommand(.{ - .button = .{ - .id = id, - .label = self.allocString(label) orelse return false - } - }); - - if (self.widget_results.get(id)) |result| { - if (result == .button_true) { - return true; - } - } - return false; - } - - pub const SliderOptions = struct { - label: []const u8, - value: *f32, - min: f32, - max: f32, +fn formatString(comptime fmt: []const u8, args: anytype) [:0]const u8 { + const frame = g_state.frame.?; + return std.fmt.allocPrintSentinel(frame, fmt, args, 0) catch blk: { + log.err("allocPrintSentinel() failed", .{}); + break :blk ""; }; +} - pub fn slider(self: *Interface, opts: SliderOptions) bool { - const id = self.initWidgetIdFromLabel(opts.label); +pub fn text(comptime fmt: []const u8, args: anytype) void { + if (!build_options.has_imgui) { + return; + } - self.pushCommand(.{ - .slider = .{ - .id = id, - .label = self.allocString(opts.label) orelse return false, - .value = opts.value.*, - .min = opts.min, - .max = opts.max - } - }); + ig.igTextUnformatted(formatString(fmt, args)); +} - if (self.widget_results.get(id)) |result| { - if (result == .slider_changed) { - opts.value.* = result.slider_changed; - return true; - } - } +pub fn button(label: []const u8) bool { + if (!build_options.has_imgui) { return false; } - pub fn checkbox(self: *Interface, label: []const u8, value: *bool) bool { - const id = self.initWidgetIdFromLabel(label); + return ig.igButton(formatString("{s}", .{label})); +} - self.pushCommand(.{ - .checkbox = .{ - .id = id, - .label = self.allocString(label) orelse return false, - .value = value.* - } - }); - - if (self.widget_results.get(id)) |result| { - if (result == .checkbox_changed) { - value.* = result.checkbox_changed; - return true; - } - } - - return false; - } - - pub fn separator(self: *Interface) void { - self.pushCommand(.separator); - } - - const ColorEditOptions = struct { - label: []const u8, - value: *Color, - has_alpha: bool = false - }; - - pub fn colorEdit(self: *Interface, opts: ColorEditOptions) bool { - const id = self.initWidgetIdFromLabel(opts.label); - - self.pushCommand(.{ - .color_edit = .{ - .id = id, - .label = self.allocString(opts.label) orelse return false, - .value = opts.value.*, - .has_alpha = opts.has_alpha - } - }); - - if (self.widget_results.get(id)) |result| { - if (result == .color_edit_changed) { - opts.value.* = result.color_edit_changed; - return true; - } - } - - return false; - } +pub const SliderOptions = struct { + label: []const u8, + value: *f32, + min: f32, + max: f32, }; + +pub fn slider(opts: SliderOptions) bool { + if (!build_options.has_imgui) { + return false; + } + + return ig.igSliderFloat( + formatString("{s}", .{opts.label}), + opts.value, + opts.min, + opts.max + ); +} + +pub fn checkbox(label: []const u8, value: *bool) bool { + if (!build_options.has_imgui) { + return false; + } + + return ig.igCheckbox( + formatString("{s}", .{label}), + value, + ); +} + +pub fn separator() void { + if (!build_options.has_imgui) { + return; + } + + ig.igSeparator(); +} + +const ColorEditOptions = struct { + label: []const u8, + value: *Color, + has_alpha: bool = false +}; + +pub fn colorEdit(opts: ColorEditOptions) bool { + if (!build_options.has_imgui) { + return false; + } + + const label = formatString("{s}", .{opts.label}); + var color: [4]f32 = .{ + opts.value.r, + opts.value.g, + opts.value.b, + opts.value.a, + }; + + var changed: bool = undefined; + if (opts.has_alpha) { + changed = ig.igColorEdit4(label, &color, ig.ImGuiColorEditFlags_None); + } else { + changed = ig.igColorEdit3(label, &color, ig.ImGuiColorEditFlags_None); + } + + if (changed) { + opts.value.r = color[0]; + opts.value.g = color[1]; + opts.value.b = color[2]; + opts.value.a = color[3]; + } + + return changed; +} + +fn toImVec2(vec2: Vec2) ig.ImVec2 { + return ig.ImVec2{ + .x = vec2.x, + .y = vec2.y, + }; +} diff --git a/src/input.zig b/src/input.zig index 03ba4a5..dcd58c4 100644 --- a/src/input.zig +++ b/src/input.zig @@ -6,6 +6,53 @@ const sokol = @import("sokol"); const Math = @import("math"); const Vec2 = Math.Vec2; +const Input = @This(); + +key_code_mapping: std.EnumMap(KeyCode, u21), + +window_size: Vec2, +focused: bool, +keyboard: KeyStateType(KeyCode), +mouse_buttons: KeyStateType(MouseButton), +mouse_position: ?Vec2, +mouse_scroll: Vec2, + +pub fn init(window_size: Vec2) Input { + return Input{ + .key_code_mapping = .{}, + .focused = false, + .keyboard = .empty, + .mouse_buttons = .empty, + .mouse_position = null, + .mouse_scroll = .init(0, 0), + .window_size = window_size + }; +} + +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 KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) { SPACE = 32, APOSTROPHE = 39, @@ -189,114 +236,3 @@ fn KeyStateType(T: type) type { } }; } - -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/main.zig b/src/main.zig index 2959ea6..e8f15ee 100644 --- a/src/main.zig +++ b/src/main.zig @@ -15,4 +15,5 @@ pub fn main(init: std.process.Init) void { test { std.testing.refAllDecls(@This()); + _ = @import("./slot_map.zig"); } diff --git a/src/platform.zig b/src/platform.zig index a1d7a90..750224f 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -22,108 +22,11 @@ const tracy = @import("tracy"); pub const Color = @import("./color.zig"); const shd = @import("shader"); -const ImGUI = @import("./imgui.zig"); -const Graphics = @import("./graphics.zig"); +pub const ImGUI = @import("./imgui.zig"); +pub const Gfx = @import("./graphics.zig"); const Input = @import("./input.zig"); fn PlatformType(App: type) type { - const Runtime = struct { - app: App, - - time: Io.Duration, - input: Input.State, - gfx: Graphics.Instance, - imgui: if (build_options.has_imgui) ImGUI.Instance else void, - last_key_pressed: ?Input.KeyCode, - - const InitOptions = struct { - window_size: Vec2, - }; - - pub fn init(self: *@This(), opts: InitOptions) !?u8 { - self.* = @This(){ - .app = undefined, - .last_key_pressed = null, - .gfx = .{ - .window_size = opts.window_size, - .quads_buffer = undefined, - }, - .imgui = undefined, - .input = .empty, - .time = .zero - }; - - if (build_options.has_imgui) { - self.imgui.init(); - } - - if (@hasDecl(App, "init")) { - const plt = Init{ }; - return try self.app.init(plt); - } else { - return null; - } - } - - pub fn deinit(self: *@This(), gpa: Allocator) void { - if (@hasDecl(App, "deinit")) { - self.app.deinit(); - } - - if (build_options.has_imgui) { - self.imgui.deinit(gpa); - } - } - - 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; - } - }; - return struct { const Self = @This(); @@ -131,96 +34,105 @@ fn PlatformType(App: type) type { io: Io, cli_args: []const [:0]const u8, - runtime: Runtime, - - gfx: Graphics.System, - imgui: if (build_options.has_imgui) ImGUI.System else void, + app: App, started_at: Io.Timestamp, last_frame_at: Io.Timestamp, + + 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, - event_validation: struct { - last_key_pressed: ?Input.KeyCode, - window_size: Vec2, - input: Input.State, - }, + 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); + + Gfx.init(.{ + .func = sokolLogCallback + }); + ImGUI.init(.{ + .func = sokolLogCallback + }); + + if (@hasDecl(App, "init")) { + const plt = Init{ + .gpa = self.gpa, + .io = self.io + }; + 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.started_at = Io.Timestamp.now(self.io, .awake); - self.last_frame_at = self.started_at; - - self.gfx.init(.{ - .func = sokolLogCallback - }); - if (build_options.has_imgui) { - self.imgui.init(.{ - .func = sokolLogCallback - }); - } - - 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); - } - - self.event_validation = .{ - .input = self.runtime.input, - .window_size = self.runtime.gfx.window_size, - .last_key_pressed = self.runtime.last_key_pressed + 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; - var events: []Input.Event = &.{}; - if (!self.events_overflow) { - events = self.events.items; + if (self.input.keyboard.pressed.contains(.F4)) { + self.show_imgui = !self.show_imgui; + } + + { + Gfx.beginFrame(); + if (self.show_imgui) { + ImGUI.beginFrame(self.frame_arena.allocator()); + } + + if (@hasDecl(App, "frame")) { + const plt = Frame{ + .t = t, + .dt = dt, + .gpa = self.gpa, + .io = self.io, + .input = &self.input, + .input_events = self.events.items, + .frame = self.frame_arena.allocator() + }; + try self.app.frame(plt); + } + + Gfx.flush(); + if (self.show_imgui) { + ImGUI.endFrame(); + } + Gfx.endFrame(); } - 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.runtime.input.keyboard.pressed.contains(.F4)) { - self.imgui.enabled = !self.imgui.enabled; - } - } - - var pass_action: sg.PassAction = .{}; - pass_action.colors[0] = .{ - .load_action = .CLEAR, - .clear_value = toSokolColor(frame_result.gfx.clear_color) - }; - - sg.beginPass(.{ .action = pass_action, .swapchain = sglue.swapchain() }); - self.gfx.render(frame_result.gfx); - if (build_options.has_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.input.keyboard.pressed = .empty; + self.input.keyboard.released = .empty; + self.input.mouse_buttons.pressed = .empty; + self.input.mouse_buttons.released = .empty; } fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void { @@ -234,6 +146,72 @@ fn PlatformType(App: type) type { }; } + // 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) 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| { + // 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. + 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.input.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; + } + } + } + fn appendEvent(self: *Self, e: Input.Event) void { if (self.events.capacity == self.events.items.len) { if (!self.events_overflow) { @@ -244,17 +222,11 @@ fn PlatformType(App: type) type { } 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); + self.applyEventToState(e); } fn pushEvent(self: *Self, e: Input.Event) void { - const input = &self.event_validation.input; + const input = &self.input; if (input.focused) { if (e == .focused) { @@ -303,8 +275,8 @@ fn PlatformType(App: type) type { } fn event(self: *Self, e: sapp.Event) !bool { - if (build_options.has_imgui) { - if (self.imgui.event(e)) { + if (self.show_imgui) { + if (ImGUI.event(e)) { self.pushEvent(.{ .unfocused = {} }); @@ -398,11 +370,17 @@ 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); + if (@hasDecl(App, "deinit")) { + const plt = Deinit{ + .gpa = self.gpa, + .io = self.io + }; + self.app.deinit(plt); } - self.gfx.deinit(); + self.frame_arena.deinit(); + + ImGUI.deinit(); + Gfx.deinit(); } }; } @@ -416,16 +394,24 @@ pub const RunOptions = struct { }; pub const Init = struct { + gpa: Allocator, + io: Io, +}; + +pub const Deinit = struct { + gpa: Allocator, + io: Io, }; pub const Frame = struct { t: Io.Duration, dt: Io.Duration, - gfx: *Graphics.Interface, - input: *Input.State, + frame: Allocator, + gpa: Allocator, + io: Io, + input: *Input, input_events: []Input.Event, - imgui: ?*ImGUI.Interface, fn nanosecondsToSeconds(nanoseconds: i96) f32 { return @floatCast(@as(f64, @floatFromInt(nanoseconds)) / std.time.ns_per_s); @@ -450,16 +436,17 @@ 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"), - .runtime = undefined, - .gfx = undefined, - .imgui = undefined, + .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, - .event_validation = undefined, + .last_key_pressed = null, + .frame_arena = .init(gpa), + .show_imgui = builtin.mode == .Debug }; var icon: sapp.IconDesc = .{}; diff --git a/src/slot_map.zig b/src/slot_map.zig new file mode 100644 index 0000000..b270315 --- /dev/null +++ b/src/slot_map.zig @@ -0,0 +1,291 @@ +const std = @import("std"); +const tracy = @import("tracy"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +/// This array provides: +/// - O(1) insertion +/// - O(1) removal +/// - O(1) lookup +/// - Pointers and IDs stay stable when inserting and removing. +/// - Unique IDs for every inserted item (assuming that generation doesn't overflow) +/// +/// For more details about this data structure checkout this podcast episode from Wookash: +/// * https://www.youtube.com/watch?v=ShSGHb65f3M +pub fn SlotMapType(Index: type, Generation: type) type { + assert(@typeInfo(Generation) == .int); + assert(@typeInfo(Index) == .int); + + return struct { + const Self = @This(); + + // TODO: This struct will probably have paddings bytes inserted. + // It's not ideal for a data structure like this which will be commonly used. + // + // The `used` field could be packed into a bitset. + // But that would introduce a second `std.ArrayList` which would make managing the memory a PITA. + // + // So in the end idk if it's worth it. + pub const Slot = struct { + generation: Generation, + next_hole: ?Index, + used: bool + }; + + slots: std.ArrayList(Slot), + first_hole: ?Index, + last_hole: ?Index, + hole_count: usize, + + const empty = Self{ + .slots = .empty, + .first_hole = null, + .last_hole = null, + .hole_count = 0, + }; + + pub const Id = packed struct { + generation: Generation, + index: Index, + + pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void { + try writer.print("Id{{ {}, {} }}", .{ self.index, self.generation }); + } + }; + + pub const Iterator = struct { + slot_map: *Self, + index: Index, + + pub fn next(self: *Iterator) ?Id { + while (self.index < self.slot_map.slots.items.len) { + const index = self.index; + const slot = self.slot_map.slots.items[index]; + self.index += 1; + + if (slot.used) { + return Id{ + .index = @intCast(index), + .generation = slot.generation + }; + } + } + + return null; + } + }; + + pub fn clearRetainingCapacity(self: *Self) void { + const slots = self.slots; + slots.clearRetainingCapacity(); + + self.* = .init(slots); + } + + fn insertHole(self: *Self, index: Index) void { + if (self.last_hole) |last_hole| { + self.slots.items[index].next_hole = last_hole; + self.last_hole = index; + } else { + self.first_hole = index; + self.last_hole = index; + } + self.hole_count += 1; + } + + fn removeUnused(self: *Self) ?Index { + if (self.first_hole) |first_hole| { + self.first_hole = self.slots.items[first_hole].next_hole; + if (self.first_hole == null) { + self.last_hole = null; + } + self.hole_count -= 1; + return first_hole; + } + + const capacity = @min(self.slots.capacity, std.math.maxInt(Index)); + if (self.slots.items.len < capacity) { + const index: Index = @intCast(self.slots.items.len); + self.slots.items.len += 1; + self.slots.items[index] = Slot{ + .generation = 0, + .next_hole = null, + .used = false + }; + return index; + } + + return null; + } + + pub fn ensureUnusedCapacity(self: *Self, gpa: Allocator, additional_count: usize) Allocator.Error!void { + if (additional_count > self.hole_count) { + const new_capacity, const overflow = @addWithOverflow( + self.slots.capacity, + additional_count - self.hole_count + ); + if (overflow != 0) return error.OutOfMemory; + if (new_capacity >= std.math.maxInt(Index)) return error.OutOfMemory; + + try self.slots.ensureTotalCapacity(gpa, new_capacity); + } + } + + pub fn unusedCapacity(self: *Self) usize { + const capacity = @min(self.slots.capacity, std.math.maxInt(Index)); + return capacity - self.slots.items.len + self.hole_count; + } + + pub fn insertAssumeCapacity(self: *Self) Id { + const index = self.removeUnused().?; + const slot = &self.slots.items[index]; + assert(!slot.used); + slot.used = true; + + return Id{ + .index = @intCast(index), + .generation = slot.generation + }; + } + + pub fn insert(self: *Self, gpa: Allocator) Allocator.Error!Id { + try self.ensureUnusedCapacity(gpa, 1); + return self.insertAssumeCapacity(); + } + + pub fn insertBounded(self: *Self) Allocator.Error!Id { + if (self.unusedCapacity() == 0) { + return error.OutOfMemory; + } + return self.insertAssumeCapacity(); + } + + pub fn exists(self: *Self, id: Id) bool { + if (id.index >= self.slots.items.len) { + return false; + } + + const slot = self.slots.items[id.index]; + + return slot.used and slot.generation == id.generation; + } + + pub fn removeAssumeExists(self: *Self, id: Id) void { + assert(self.exists(id)); + + const slot = &self.slots.items[id.index]; + + slot.used = false; + slot.generation +%= 1; + self.insertHole(id.index); + } + + pub fn remove(self: *Self, id: Id) bool { + if (!self.exists(id)) { + return false; + } + + self.removeAssumeExists(id); + return true; + } + + pub fn iterator(self: *Self) Iterator { + return Iterator{ + .slot_map = self, + .index = 0 + }; + } + + pub fn init(slots: std.ArrayList(Slot)) Self { + var self: Self = .empty; + self.slots = slots; + return self; + } + + pub fn deinit(self: *Self, gpa: Allocator) void { + self.slots.deinit(gpa); + } + }; +} + +const TestMap = SlotMapType(u24, u8); + +// TODO: Add more rigorous test for check if generation usage is nicely distributed. + +test "insert & remove" { + const expect = std.testing.expect; + const gpa = std.testing.allocator; + + var map: TestMap = .empty; + defer map.deinit(gpa); + + const id1 = try map.insert(gpa); + try expect(map.exists(id1)); + try expect(map.remove(id1)); + try expect(!map.exists(id1)); + try expect(!map.remove(id1)); + + const id2 = try map.insert(gpa); + try expect(map.exists(id2)); + try expect(!map.exists(id1)); +} + +test "generation wrap around" { + const expectEqual = std.testing.expectEqual; + const gpa = std.testing.allocator; + + var map: TestMap = .empty; + defer map.deinit(gpa); + + // Grow array list so that at least 1 slot exists + const id1 = try map.insert(gpa); + map.removeAssumeExists(id1); + + // Artificially increase generation count + map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation")); + + // Check if generation wraps around + const id2 = try map.insert(gpa); + map.removeAssumeExists(id2); + try expectEqual(id1.index, id2.index); + try expectEqual(0, map.slots.items[id1.index].generation); +} + +test "iterator" { + const expectEqual = std.testing.expectEqual; + const gpa = std.testing.allocator; + + var map: TestMap = .empty; + defer map.deinit(gpa); + + // Create array which has a hole + const id1 = try map.insert(gpa); + const id2 = try map.insert(gpa); + const id3 = try map.insert(gpa); + + map.removeAssumeExists(id2); + + var iter = map.iterator(); + try expectEqual(id1, iter.next().?); + try expectEqual(id3, iter.next().?); + try expectEqual(null, iter.next()); +} + +test "clear retaining capacity" { + const expect = std.testing.expect; + const expectEqual = std.testing.expectEqual; + const gpa = std.testing.allocator; + + var map: TestMap = .empty; + defer map.deinit(gpa); + + const id1 = try map.insert(gpa); + try expect(map.exists(id1)); + map.clearRetainingCapacity(); + + const id2 = try map.insert(gpa); + try expect(map.exists(id2)); + + try expectEqual(id1, id2); +}