From 98f59a30bdd0a03742e5ed927aae0432dad11acc Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Mon, 6 Jul 2026 16:40:24 +0300 Subject: [PATCH] integrate imgui --- build.zig | 23 ++- src/app.zig | 87 +++++--- src/imgui.zig | 526 +++++++++++++++++++++++++++++++++++++++++++++++ src/platform.zig | 430 ++++++++++++++++++++++++++------------ src/shader.glsl | 32 +++ 5 files changed, 934 insertions(+), 164 deletions(-) create mode 100644 src/imgui.zig create mode 100644 src/shader.glsl diff --git a/build.zig b/build.zig index a8aa2f7..23c9af4 100644 --- a/build.zig +++ b/build.zig @@ -25,13 +25,34 @@ pub fn build(b: *std.Build) !void { build_options.addOption(bool, "has_imgui", has_imgui); exe_mod.addOptions("build_options", build_options); + const math_mod = b.createModule(.{ + .root_source_file = b.path("src/math.zig") + }); + exe_mod.addImport("math", math_mod); + const dep_sokol = b.dependency("sokol", .{ .target = target, .optimize = optimize, .with_sokol_imgui = has_imgui }); + const mod_sokol = dep_sokol.module("sokol"); exe_mod.linkLibrary(dep_sokol.artifact("sokol_clib")); - exe_mod.addImport("sokol", dep_sokol.module("sokol")); + exe_mod.addImport("sokol", mod_sokol); + + const dep_shdc = dep_sokol.builder.dependency("shdc", .{}); + const mod_shader = try sokol.shdc.createModule(b, "shader", mod_sokol, .{ + .shdc_dep = dep_shdc, + .input = "src/shader.glsl", + .output = "src/shader.zig", + .slang = .{ + .glsl410 = true, + .hlsl4 = true, + .metal_macos = true, + .glsl300es = true, + }, + }); + mod_shader.addImport("math", math_mod); + exe_mod.addImport("shader", mod_shader); if (has_imgui) { if (b.lazyDependency("cimgui", .{ diff --git a/src/app.zig b/src/app.zig index 57a8fdf..1beddcc 100644 --- a/src/app.zig +++ b/src/app.zig @@ -1,40 +1,65 @@ +const std = @import("std"); const CLI = @import("./cli.zig"); +const Platform = @import("./platform.zig"); +const log = std.log.scoped(.app); const App = @This(); -pub fn init(self: *App) !?u8 { - _ = self; // autofix - // var cli: CLI = undefined; - // cli.init(plt.io, plt.gpa); - // defer cli.deinit(); - // - // const opt_plugin_path = try cli.addOption(.{ - // .kind = .single_argument, - // .long_name = "plugin-path" - // }); - // - // var result = cli.parse(plt.gpa, opts.cli_args) catch { - // return 1; - // }; - // defer result.deinit(); - // - // var cwd = Io.Dir.cwd(); - // const root_dir = try cwd.realPathFileAlloc(plt.io, ".", plt.gpa); - // defer plt.gpa.free(root_dir); - // - // var plugin_path: ?[]const u8 = null; - // if (result.getOptionArgument(opt_plugin_path)) |absolute_plugin_path| { - // plugin_path = FileWatcher.toRelativePath(root_dir, absolute_plugin_path); - // if (plugin_path == null) { - // return error.PluginPathOutsideOfProject; - // } - // - // log.debug("Loaded dynamic plugin from: '{s}'", .{plugin_path.?}); - // } +bg: Platform.Color, +show_first_window: bool, +check: bool, +player_x: f32, + +pub fn init(self: *App, plt: Platform.Init) !?u8 { + self.* = App{ + .bg = .black, + .player_x = 0, + .show_first_window = true, + .check = false, + }; + _ = plt; // autofix + return null; } -pub fn frame(self: *App) !void { - _ = self; // autofix +pub fn frame(self: *App, plt: *Platform.Frame) !void { + const gfx = plt.gfx; + + gfx.drawRectangle(.init(self.player_x + 100, 100), .init(100, 100), .rgb(200, 0, 0)); + + if (plt.imgui) |imgui| { + if (imgui.beginWindow(.{ + .name = "Hello", + .size = .init(400, 200), + .open = &self.show_first_window + })) { + defer imgui.endWindow(); + + imgui.text("Hello", .{}); + if (imgui.button("foo")) { + std.debug.print("pressed\n", .{}); + } + if (imgui.slider(.{ + .label = "player x", + .value = &self.player_x, + .min = 0, + .max = 100 + })) { + std.debug.print("player x changed\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", .{}); + } + } + } + + gfx.clear_color = self.bg; } pub fn deinit(self: *App) void { diff --git a/src/imgui.zig b/src/imgui.zig new file mode 100644 index 0000000..0001183 --- /dev/null +++ b/src/imgui.zig @@ -0,0 +1,526 @@ +const std = @import("std"); +const log = std.log.scoped(.imgui); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const Color = @import("./color.zig"); +const Math = @import("math"); +const Vec2 = Math.Vec2; + +const sokol = @import("sokol"); +const sapp = sokol.app; + +pub const State = struct { + const simgui = sokol.imgui; + const ig = @import("cimgui"); + + 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 { + simgui.setup(.{ + .logger = logger + }); + + if (@hasDecl(ig, "ImGuiConfigFlags_DockingEnable")) { + ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_DockingEnable; + ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_ViewportsEnable; + } + + self.* = @This(){ + .widget_results = .{}, + .commands_buffer = undefined, + .strings_buffer = undefined, + .id_stack_buffer = undefined, + }; + } + + pub fn deinit(self: *State, gpa: Allocator) void { + simgui.shutdown(); + self.widget_results.deinit(gpa); + } + + pub fn event(self: State, e: sapp.Event) bool { + _ = self; // autofix + return simgui.handleEvent(e); + } + + pub fn render(self: *State, gpa: Allocator, imgui: *Interface) !void { + simgui.newFrame(.{ + .width = sapp.width(), + .height = sapp.height(), + .delta_time = sapp.frameDuration(), + .dpi_scale = sapp.dpiScale(), + }); + + const viewport = ig.igGetMainViewport(); + ig.igSetNextWindowPos(viewport[0].Pos, ig.ImGuiCond_Always); + ig.igSetNextWindowSize(viewport[0].Size, ig.ImGuiCond_Always); + ig.igSetNextWindowViewport(viewport[0].ID); + + 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; + + 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 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) { + .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); + } + + const name = imgui.getString(opts.name); + + 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 self.widget_results.put(gpa, opts.id, .{ .window_changed = open }); + } + + 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 self.widget_results.put(gpa, opts.id, .button_true); + } + }, + .slider => |opts| { + var value = opts.value; + const changed = ig.igSliderFloat( + imgui.getString(opts.label), + &value, + opts.min, + opts.max, + ); + if (changed) { + try self.widget_results.put(gpa, opts.id, .{ .slider_changed = value }); + } + }, + .checkbox => |opts| { + var value = opts.value; + const changed = ig.igCheckbox( + imgui.getString(opts.label), + &value + ); + if (changed) { + try self.widget_results.put(gpa, opts.id, .{ .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 self.widget_results.put(gpa, opts.id, .{ + .color_edit_changed = .{ + .r = color[0], + .g = color[1], + .b = color[2], + .a = color[3], + } + }); + } + } + } + } + } + + ig.igEnd(); + + // TODO: + // if (ig.igBeginMainMenuBar()) { + // ig.igEndMainMenuBar(); + // } + + 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 + }; + } +}; + +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 initPtr(ptr: anytype) WidgetId { + return initUsize(@intFromPtr(ptr)); + } + + pub fn initUsize(num: usize) WidgetId { + return init(@truncate(num)); + } + + pub fn initString(seed: u64, str: []const u8) WidgetId { + return init(StringHasher.hash(seed, str)); + } + + 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); +}; + +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: *Interface, index: StringIndex) [*:0]u8 { + return std.mem.sliceTo( + @as([*:0]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, + }; + + 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; + } + } + + 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; + } + + pub fn endWindow(self: *Interface) void { + self.pushCommand(.end); + self.popId(); + } + + pub fn text(self: *Interface, comptime fmt: []const u8, args: anytype) void { + self.pushCommand(.{ + .text = self.formatString(fmt, args) orelse return + }); + } + + 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, + }; + + pub fn slider(self: *Interface, opts: SliderOptions) bool { + const id = self.initWidgetIdFromLabel(opts.label); + + self.pushCommand(.{ + .slider = .{ + .id = id, + .label = self.allocString(opts.label) orelse return false, + .value = opts.value.*, + .min = opts.min, + .max = opts.max + } + }); + + if (self.widget_results.get(id)) |result| { + if (result == .slider_changed) { + opts.value.* = result.slider_changed; + return true; + } + } + return false; + } + + pub fn checkbox(self: *Interface, label: []const u8, value: *bool) bool { + const id = self.initWidgetIdFromLabel(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; + } +}; diff --git a/src/platform.zig b/src/platform.zig index 3715e41..cf0ef48 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -4,149 +4,245 @@ const Io = std.Io; const Allocator = std.mem.Allocator; const log = std.log.scoped(.platform); const assert = std.debug.assert; +const build_options = @import("build_options"); const sokol = @import("sokol"); const sapp = sokol.app; const sg = sokol.gfx; const sglue = sokol.glue; const sgl = sokol.gl; -const simgui = sokol.imgui; -const Math = @import("./math.zig"); +const Math = @import("math"); const Vec2 = Math.Vec2; +const Vec4 = Math.Vec4; +const Mat4 = Math.Mat4; const tracy = @import("tracy"); -const ig = @import("cimgui"); +pub const Color = @import("./color.zig"); +const shd = @import("shader"); -const Color = @import("./color.zig"); +const ImGUI = @import("./imgui.zig"); fn PlatformType(App: type) type { - return struct { - const Self = @This(); + const GraphicsState = struct { + const max_quads = 2048; - pass_action: sg.PassAction = .{}, - cli_args: []const [:0]const u8, + pipeline: sg.Pipeline = .{}, + bindings: sg.Bindings = .{}, - show_first_window: bool = true, + quads_buffer: [max_quads]Quad, - app: App, - - fn sokolInit(user_data: ?*anyopaque) callconv(.c) void { - const self: *Self = @ptrCast(@alignCast(user_data)); + fn init(self: *@This()) void { sg.setup(.{ .environment = sglue.environment(), .logger = .{ .func = sokolLogCallback }, }); - simgui.setup(.{ - .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" }); - if (@hasDecl(ig, "ImGuiConfigFlags_DockingEnable")) { - ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_DockingEnable; - ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_ViewportsEnable; + 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.pass_action.colors[0] = .{ - .load_action = .CLEAR, - .clear_value = toSokolColor(.black) - }; + self.bindings.index_buffer = sg.makeBuffer(.{ + .data = sg.asRange(&indices), + .usage = .{ .index_buffer = true, }, + .label = "indices" + }); + + const shader = sg.makeShader(shd.mainShaderDesc(sg.queryBackend())); + self.pipeline = sg.makePipeline(.{ + .primitive_type = .TRIANGLES, + .index_type = .UINT16, + .colors = init: { + var s: [8]sg.ColorTargetState = [_]sg.ColorTargetState{.{}} ** 8; + s[0] = .{ + .blend = .{ + .enabled = true, + .src_factor_rgb = .SRC_ALPHA, + .dst_factor_rgb = .ONE_MINUS_SRC_ALPHA, + .op_rgb = .ADD, + .src_factor_alpha = .ONE, + .dst_factor_alpha = .ONE_MINUS_SRC_ALPHA, + .op_alpha = .ADD, + } + }; + break :init s; + }, + .shader = shader, + .layout = init: { + var l = sg.VertexLayoutState{}; + l.attrs[shd.ATTR_main_position].format = .FLOAT2; + l.attrs[shd.ATTR_main_color0].format = .FLOAT4; + break :init l; + }, + .label = "main-pipeline" + }); } - fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void { + fn deinit(self: @This()) void { + _ = self; // autofix + sg.shutdown(); + } + + fn getInterface(self: *@This()) Graphics { + return .{ + .clear_color = .black, + .canvas_size = .init(320, 180), + .quads = .initBuffer(&self.quads_buffer) + }; + } + }; + + return struct { + const Self = @This(); + + gpa: Allocator, + cli_args: []const [:0]const u8, + + app: *App, + + gfx: GraphicsState, + imgui: if (build_options.has_imgui) ImGUI.State else void, + + fn sokolInit(user_data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(user_data)); - tracy.frameMark(); - - simgui.newFrame(.{ - .width = sapp.width(), - .height = sapp.height(), - .delta_time = sapp.frameDuration(), - .dpi_scale = sapp.dpiScale(), - }); - - //=== UI CODE STARTS HERE - { - const viewport = ig.igGetMainViewport(); - ig.igSetNextWindowPos(viewport[0].Pos, ig.ImGuiCond_Always); - ig.igSetNextWindowSize(viewport[0].Size, ig.ImGuiCond_Always); - ig.igSetNextWindowViewport(viewport[0].ID); - - const window_flags = - ig.ImGuiWindowFlags_NoDocking | - ig.ImGuiWindowFlags_NoTitleBar | - ig.ImGuiWindowFlags_NoCollapse | - ig.ImGuiWindowFlags_NoResize | - ig.ImGuiWindowFlags_NoMove | - ig.ImGuiWindowFlags_NoBringToFrontOnFocus | - ig.ImGuiWindowFlags_NoNavFocus; - - 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 dockspace_id = ig.igGetID("MyDockSpace"); - _ = ig.igDockSpaceEx(dockspace_id, ig.ImVec2{ .x = 0, .y = 0 }, ig.ImGuiDockNodeFlags_PassthruCentralNode, null); - - ig.igSetNextWindowPos(.{ .x = 10, .y = 30 }, ig.ImGuiCond_Once); - ig.igSetNextWindowSize(.{ .x = 400, .y = 100 }, ig.ImGuiCond_Once); - if (ig.igBegin("Hello Dear ImGui!", &self.show_first_window, ig.ImGuiWindowFlags_None)) { - _ = ig.igColorEdit3("Background", &self.pass_action.colors[0].clear_value.r, ig.ImGuiColorEditFlags_None); - _ = ig.igText("Dear ImGui Version: %s", ig.IMGUI_VERSION); - } - ig.igEnd(); - - ig.igEnd(); + self.gfx.init(); + if (build_options.has_imgui) { + self.imgui.init(.{ + .func = sokolLogCallback + }); } - // TODO: - // if (ig.igBeginMainMenuBar()) { - // ig.igEndMainMenuBar(); - // } - //=== UI CODE ENDS HERE + if (@hasDecl(App, "init")) { + const plt = Init{ }; + if (try self.app.init(plt)) |exit_code| { + std.process.exit(exit_code); + } + } + } - // var frame = Platform.Frame{ - // .platform = &self.exposed, - // .input = .{}, - // .output = .{}, - // }; - // if (self.frame(&frame)) { - // // ? - // } else |e| { - // log.err("frame() failed: {}", .{e}); - // if (@errorReturnTrace()) |trace| { - // std.debug.dumpErrorReturnTrace(trace); - // } - // self.quit(); - // } + fn createProjectionMatrix(screen_size: Vec2, canvas_size: Vec2) Mat4 { + const scale_x = screen_size.x/canvas_size.x; + const scale_y = screen_size.y/canvas_size.y; + const min_scale = @min(scale_x, scale_y); - // var pass_action: sg.PassAction = .{}; - // pass_action.colors[0] = .{ - // .load_action = .CLEAR, - // .clear_value = toSokolColor(.black) - // }; + const offscreen_canvas_size = canvas_size.multiply(.{ + .x = scale_x / min_scale, + .y = scale_y / min_scale + }); - sg.beginPass(.{ .action = self.pass_action, .swapchain = sglue.swapchain() }); - simgui.render(); + return Mat4.initIdentity() + .multiply(Mat4.initTranslate(.{ .x = -1, .y = -1, .z = 0 })) + .multiply(Mat4.initScale(.{ .x = 2, .y = 2, .z = 0 })) + + .multiply(Mat4.initTranslate(.{ .x = 0, .y = 1, .z = 0 })) + .multiply(Mat4.initScale(.{ .x = 1, .y = -1, .z = 0 })) + + .multiply(Mat4.initScale(.{ + .x = 1/offscreen_canvas_size.x, + .y = 1/offscreen_canvas_size.y, + .z = 0 + })) + .multiply(Mat4.initTranslate(.{ + .x = (offscreen_canvas_size.x - canvas_size.x)/2, + .y = (offscreen_canvas_size.y - canvas_size.y)/2, + .z = 0 + })); + } + + fn frame(self: *Self) !void { + tracy.frameMark(); + + var gfx = self.gfx.getInterface(); + var plt = Frame{ + .gfx = &gfx, + .imgui = undefined + }; + 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 = .{ + .view = .initIdentity(), + .projection = createProjectionMatrix(screen_size, gfx.canvas_size) + }; + + var pass_action: sg.PassAction = .{}; + pass_action.colors[0] = .{ + .load_action = .CLEAR, + .clear_value = toSokolColor(gfx.clear_color) + }; + + sg.beginPass(.{ .action = pass_action, .swapchain = sglue.swapchain() }); + sg.applyPipeline(self.gfx.pipeline); + sg.applyBindings(self.gfx.bindings); + sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params)); + sg.draw(0, 6, 1); + if (build_options.has_imgui) { + try self.imgui.render(self.gpa, &imgui); + } sg.endPass(); sg.commit(); } + fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void { + const self: *Self = @ptrCast(@alignCast(user_data)); + self.frame() catch |err| { + log.err("frame() error: {}", .{err}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpErrorReturnTrace(trace); + } + sapp.quit(); + }; + } + fn sokolEvent(ev: [*c]const sapp.Event, user_data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(user_data)); - _ = self; // autofix - - _ = simgui.handleEvent(ev.*); + if (build_options.has_imgui) { + _ = self.imgui.event(ev.*); + } } fn sokolCleanup(user_data: ?*anyopaque) callconv(.c) void { const self: *Self = @ptrCast(@alignCast(user_data)); - _ = self; // autofix - simgui.shutdown(); - sg.shutdown(); + 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); } }; } @@ -159,43 +255,103 @@ 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, + + canvas_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 Frame = struct { + gfx: *Graphics, + imgui: ?*ImGUI.Interface, +}; + pub fn run(App: type, opts: RunOptions) void { const arena = opts.init.arena.allocator(); const gpa = opts.init.gpa; - _ = gpa; // autofix - const Platform = PlatformType(App); - const plt = arena.create(Platform) catch @panic("OOM"); - plt.* = Platform{ + const PlatformApp = PlatformType(App); + const plt = arena.create(PlatformApp) catch @panic("OOM"); + plt.* = PlatformApp{ + .gpa = gpa, .cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"), - .app = undefined + .app = gpa.create(App) catch @panic("OOM"), + .gfx = undefined, + .imgui = undefined }; - // var init = Platform.Init{ - // .cli_args = self.cli_args - // }; - // if (self.init(&self.exposed, &init)) |maybe_error_code| { - // if (maybe_error_code) |error_code| { - // std.process.exit(error_code); - // } - // } else |e| { - // log.err("init() failed: {}", .{e}); - // if (@errorReturnTrace()) |trace| { - // std.debug.dumpErrorReturnTrace(trace); - // } - // std.process.exit(1); - // } - - const is_wasm = builtin.cpu.arch.isWasm(); - if (builtin.os.tag == .linux and !is_wasm) { - var sa: std.posix.Sigaction = .{ - .handler = .{ .handler = posixSignalHandler }, - .mask = std.posix.sigemptyset(), - .flags = std.posix.SA.RESTART, - }; - std.posix.sigaction(std.posix.SIG.INT, &sa, null); - } - var icon: sapp.IconDesc = .{}; icon.sokol_default = true; // TODO: Don't hard code icon path, allow changing through options @@ -213,11 +369,21 @@ pub fn run(App: type, opts: RunOptions) void { tracy.setThreadName("Main"); + const is_wasm = builtin.cpu.arch.isWasm(); + if (builtin.os.tag == .linux and !is_wasm) { + var sa: std.posix.Sigaction = .{ + .handler = .{ .handler = posixSignalHandler }, + .mask = std.posix.sigemptyset(), + .flags = std.posix.SA.RESTART, + }; + std.posix.sigaction(std.posix.SIG.INT, &sa, null); + } + sapp.run(.{ - .init_userdata_cb = Platform.sokolInit, - .frame_userdata_cb = Platform.sokolFrame, - .cleanup_userdata_cb = Platform.sokolCleanup, - .event_userdata_cb = Platform.sokolEvent, + .init_userdata_cb = PlatformApp.sokolInit, + .frame_userdata_cb = PlatformApp.sokolFrame, + .cleanup_userdata_cb = PlatformApp.sokolCleanup, + .event_userdata_cb = PlatformApp.sokolEvent, .user_data = plt, .window_title = opts.window_title, .width = opts.width, diff --git a/src/shader.glsl b/src/shader.glsl new file mode 100644 index 0000000..d2be216 --- /dev/null +++ b/src/shader.glsl @@ -0,0 +1,32 @@ +@header const m = @import("math") +@ctype mat4 m.Mat4 + +/* main vertex shader */ +@vs vs +in vec2 position; +in vec4 color0; +out vec4 color; + +layout(binding = 0) uniform vs_params { + mat4 view; + mat4 projection; +}; + +void main() { + gl_Position = projection * view * vec4(position, 0, 1); + color = color0; +} +@end + +/* main fragment shader */ +@fs fs +in vec4 color; +out vec4 frag_color; + +void main() { + frag_color = color; +} +@end + +/* main shader program */ +@program main vs fs