const sokol = @import("sokol"); const sg = sokol.gfx; const sglue = sokol.glue; const slog = sokol.log; const sapp = sokol.app; const simgui = sokol.imgui; const sgl = sokol.gl; const Math = @import("./math.zig"); const Vec2 = Math.Vec2; const Vec4 = Math.Vec4; const rgb = Math.rgb; const Rect = Math.Rect; const std = @import("std"); const log = std.log.scoped(.graphics); const assert = std.debug.assert; const imgui = @import("imgui.zig"); const tracy = @import("tracy"); const fontstash = @import("./fontstash/root.zig"); pub const Font = fontstash.Font; // TODO: Seems that there is a vertical jitter bug when resizing a window in OpenGL. Seems like a driver bug. // From other peoples research it seems that disabling vsync when a resize event occurs fixes it. // Maybe a patch for sokol could be made? // More info: // * https://github.com/libsdl-org/SDL/issues/11618 // * https://github.com/nimgl/nimgl/issues/59 const Options = struct { const ImguiFont = struct { ttf_data: []const u8, size: f32 = 16 }; allocator: std.mem.Allocator, logger: sg.Logger = .{}, imgui_font: ?ImguiFont = null }; const DrawFrame = struct { screen_size: Vec2 = Vec2.zero, bg_color: Vec4 = rgb(0, 0, 0), scissor_stack_buffer: [32]Rect = undefined, scissor_stack: std.ArrayListUnmanaged(Rect) = .empty, fn init(self: *DrawFrame) void { self.* = DrawFrame{ .scissor_stack = .initBuffer(&self.scissor_stack_buffer) }; } }; var draw_frame: DrawFrame = undefined; var main_pipeline: sgl.Pipeline = .{}; var linear_sampler: sg.Sampler = .{}; var nearest_sampler: sg.Sampler = .{}; var font_context: fontstash.Context = undefined; pub var font_resolution_scale: f32 = 1; pub fn init(options: Options) !void { draw_frame.init(); sg.setup(.{ .logger = options.logger, .environment = sglue.environment(), }); sgl.setup(.{ .logger = .{ .func = options.logger.func, .user_data = options.logger.user_data } }); main_pipeline = sgl.makePipeline(.{ .colors = init: { var colors: [8]sg.ColorTargetState = @splat(.{}); colors[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 colors; }, }); imgui.setup(options.allocator, .{ .logger = .{ .func = options.logger.func, .user_data = options.logger.user_data }, .no_default_font = options.imgui_font != null, // TODO: Figure out a way to make imgui play nicely with UI // Ideally when mouse is inside a Imgui window, then the imgui cursor should be used. // Otherwise our own cursor should be used. .disable_set_mouse_cursor = true }); if (options.imgui_font) |imgui_font| { imgui.addFont(imgui_font.ttf_data, imgui_font.size); } linear_sampler = sg.makeSampler(.{ .min_filter = .LINEAR, .mag_filter = .LINEAR, .mipmap_filter = .LINEAR, .label = "linear-sampler", }); nearest_sampler = sg.makeSampler(.{ .min_filter = .NEAREST, .mag_filter = .NEAREST, .mipmap_filter = .NEAREST, .label = "nearest-sampler", }); const dpi_scale = sapp.dpiScale(); const atlas_size = 512; const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(atlas_size * dpi_scale)); font_context = try fontstash.Context.init(.{ .width = @intCast(atlas_dim), .height = @intCast(atlas_dim), }); } pub fn deinit() void { imgui.shutdown(); font_context.deinit(); sgl.shutdown(); sg.shutdown(); } pub fn beginFrame() void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); draw_frame.init(); draw_frame.screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); draw_frame.scissor_stack.appendAssumeCapacity(Rect.init(0, 0, sapp.widthf(), sapp.heightf())); imgui.newFrame(.{ .width = @intFromFloat(draw_frame.screen_size.x), .height = @intFromFloat(draw_frame.screen_size.y), .delta_time = sapp.frameDuration(), .dpi_scale = sapp.dpiScale() }); font_context.clearState(); sgl.defaults(); sgl.matrixModeProjection(); sgl.ortho(0, draw_frame.screen_size.x, draw_frame.screen_size.y, 0, -1, 1); sgl.loadPipeline(main_pipeline); } pub fn endFrame() void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); assert(draw_frame.scissor_stack.items.len == 1); var pass_action: sg.PassAction = .{}; pass_action.colors[0] = sg.ColorAttachmentAction{ .load_action = .CLEAR, .clear_value = .{ .r = draw_frame.bg_color.x, .g = draw_frame.bg_color.y, .b = draw_frame.bg_color.z, .a = draw_frame.bg_color.w } }; font_context.flush(); { sg.beginPass(.{ .action = pass_action, .swapchain = sglue.swapchain() }); defer sg.endPass(); sgl.draw(); imgui.render(); } sg.commit(); } pub fn pushTransform(translation: Vec2, scale: f32) void { sgl.pushMatrix(); sgl.translate(translation.x, translation.y, 0); sgl.scale(scale, scale, 1); } pub fn popTransform() void { sgl.popMatrix(); } pub fn drawQuad(quad: [4]Vec2, color: Vec4) void { sgl.beginQuads(); defer sgl.end(); sgl.c4f(color.x, color.y, color.z, color.w); for (quad) |position| { sgl.v2f(position.x, position.y); } } pub fn drawRectangle(pos: Vec2, size: Vec2, color: Vec4) void { drawQuad( .{ // Top left pos, // Top right pos.add(.{ .x = size.x, .y = 0 }), // Bottom right pos.add(size), // Bottom left pos.add(.{ .x = 0, .y = size.y }), }, color ); } pub fn drawTriangle(tri: [3]Vec2, color: Vec4) void { sgl.beginTriangles(); defer sgl.end(); sgl.c4f(color.x, color.y, color.z, color.w); for (tri) |position| { sgl.v2f(position.x, position.y); } } 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( .{ top_right, top_left, bottom_left, bottom_right }, color ); } pub fn drawRectanglOutline(pos: Vec2, size: Vec2, color: Vec4, width: f32) void { // TODO: Don't use line segments drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width); drawLine(pos, pos.add(.{ .x = 0, .y = size.y }), color, width); drawLine(pos.add(.{ .x = 0, .y = size.y }), pos.add(size), color, width); drawLine(pos.add(.{ .x = size.x, .y = 0 }), pos.add(size), color, width); } pub fn pushScissor(rect: Rect) void { draw_frame.scissor_stack.appendAssumeCapacity(rect); sgl.scissorRectf(rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, true); } pub fn popScissor() void { _ = draw_frame.scissor_stack.pop().?; const rect = draw_frame.scissor_stack.getLast(); sgl.scissorRectf(rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, true); } pub fn addFont(name: [*c]const u8, data: []const u8) !Font.Id { return try font_context.addFont(name, data); } pub const DrawTextOptions = struct { font: Font.Id, size: f32 = 16, color: Vec4 = rgb(255, 255, 255), }; pub fn drawText(position: Vec2, text: []const u8, opts: DrawTextOptions) void { pushTransform(.{ .x = 0, .y = 0}, 1/font_resolution_scale); defer popTransform(); font_context.setFont(opts.font); font_context.setSize(opts.size * font_resolution_scale); font_context.setAlign(.{ .x = .left, .y = .top }); font_context.setSpacing(0); const r: u8 = @intFromFloat(opts.color.x * 255); const g: u8 = @intFromFloat(opts.color.y * 255); const b: u8 = @intFromFloat(opts.color.z * 255); const a: u8 = @intFromFloat(opts.color.w * 255); const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24); font_context.setColor(color); font_context.drawText( position.x * font_resolution_scale, position.y * font_resolution_scale, text ); }