From 76668e841dd88469b4433fe12e2f899342795ff6 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Fri, 10 Jul 2026 01:17:46 +0300 Subject: [PATCH] add drawing texture support --- src/app.zig | 20 ++-- src/graphics.zig | 231 +++++++++++++++++++++++++++++++++-------------- src/imgui.zig | 50 +++++++++- src/platform.zig | 12 +-- 4 files changed, 229 insertions(+), 84 deletions(-) diff --git a/src/app.zig b/src/app.zig index ea075ce..3d637ae 100644 --- a/src/app.zig +++ b/src/app.zig @@ -17,14 +17,9 @@ bg: Platform.Color, show_first_window: bool, check: bool, player_pos: Vec2, +tex: Gfx.TextureId, pub fn init(self: *App, plt: Platform.Init) !?u8 { - self.* = App{ - .bg = .black, - .player_pos = .init(100, 100), - .show_first_window = true, - .check = false, - }; const cwd = std.Io.Dir.cwd(); const sokoban_spritesheet = try cwd.readFileAlloc( plt.io, @@ -37,6 +32,17 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { var image = try STBImage.load(sokoban_spritesheet); defer image.deinit(); + const tex = try Gfx.initTexture(); + try Gfx.setTexture(tex, image.rgba8_pixels, image.width, image.height); + + self.* = App{ + .bg = .black, + .player_pos = .init(100, 100), + .show_first_window = true, + .check = false, + .tex = tex + }; + return null; } @@ -62,7 +68,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt)); Gfx.setClearColor(self.bg); - Gfx.drawRectangle(self.player_pos, .init(100, 100), .rgb(200, 0, 0)); + Gfx.drawTexture(self.tex, 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 (ImGUI.beginWindow(.{ diff --git a/src/graphics.zig b/src/graphics.zig index 332ca7b..53afb23 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -16,25 +16,32 @@ const Color = @import("./color.zig"); const shd = @import("shader"); const SlotMapType = @import("slot_map.zig").SlotMapType; - const State = struct { + gpa: std.mem.Allocator, + shader: sg.Shader, pipeline: sg.Pipeline, bindings: sg.Bindings, - default_image: sg.Image, - default_view: sg.View, + default_texture: TextureId, default_sampler: sg.Sampler, quads_buffer: [2048]Quad, quads: std.ArrayList(Quad), clear_color: Color, + + textures_buffer: [max_textures]Texture, + textures_slotmap: TextureSlotMap, }; +const max_textures = 64; +const TextureSlotMap = SlotMapType(std.math.IntFittingRange(0, max_textures-1), u8); +pub const TextureId = TextureSlotMap.Id; + var g_state: State = undefined; -pub fn init(logger: sg.Logger) void { +pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { const self = &g_state; sg.setup(.{ @@ -45,27 +52,6 @@ pub fn init(logger: sg.Logger) void { 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, @@ -109,7 +95,7 @@ pub fn init(logger: sg.Logger) void { bindings.vertex_buffers[0] = sg.makeBuffer(.{ .size = @sizeOf(Quad) * max_quads, - .usage = .{ .vertex_buffer = true, .dynamic_update = true }, + .usage = .{ .vertex_buffer = true, .stream_update = true }, .label = "quad-vertices" }); @@ -133,25 +119,38 @@ pub fn init(logger: sg.Logger) void { } self.* = State{ + .gpa = gpa, .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 + .clear_color = .black, + .textures_buffer = undefined, + .textures_slotmap = .init(try .initCapacity(gpa, max_textures)), + .default_texture = undefined }; + + { + const width = 8; + const height = 8; + + var pixels: [width * height * 4]u8 = undefined; + @memset(&pixels, 0xFF); + + self.default_texture = try initTexture(); + try setTexture(self.default_texture, &pixels, width, height); + } } pub fn deinit() void { const self = &g_state; + self.textures_slotmap.deinit(self.gpa); + 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(); } @@ -197,6 +196,11 @@ pub fn beginFrame() void { .action = pass_action, .swapchain = sglue.swapchain() }); + + assert(self.textures_slotmap.exists(self.default_texture)); + const default_texture = self.textures_buffer[self.default_texture.index]; + self.bindings.views[shd.VIEW_tex] = default_texture.view; + self.bindings.samplers[shd.SMP_smp] = self.default_sampler; } // TODO: This will always have a 1-frame delay. @@ -213,11 +217,17 @@ pub fn flush() void { if (self.quads.items.len == 0) { return; } + defer self.quads.clearRetainingCapacity(); - sg.updateBuffer( - self.bindings.vertex_buffers[0], - sg.asRange(self.quads.items) - ); + const vertex_buffer = self.bindings.vertex_buffers[0]; + + const data = sg.asRange(self.quads.items); + if (sg.queryBufferWillOverflow(vertex_buffer, data.size)) { + log.warn("Vertex buffer overflow", .{}); + return; + } + + self.bindings.vertex_buffer_offsets[0] = sg.appendBuffer(vertex_buffer, data); // TODO: Rectify `window_size` vs `gfx.window_size` const window_size = Vec2.init(sapp.widthf(), sapp.heightf()); @@ -229,15 +239,10 @@ pub fn flush() void { .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 { @@ -258,34 +263,9 @@ pub fn drawQuad(quad: Quad) void { } 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) - }, - }); + const self = &g_state; + + drawTexture(self.default_texture, pos, size, color); } pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { @@ -320,6 +300,121 @@ pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { }); } +pub fn initTexture() !TextureId { + const self = &g_state; + const id = try self.textures_slotmap.insertBounded(); + + const image = sg.allocImage(); + assert(image.id != sg.invalid_id); + + const view = sg.allocView(); + assert(view.id != sg.invalid_id); + + const texture = &self.textures_buffer[id.index]; + texture.* = .{ + .image = image, + .view = view + }; + return id; +} + +pub fn deinitTexture(id: TextureId) void { + const self = &g_state; + if (self.textures_slotmap.remove(id)) { + const texture = self.textures_buffer[id.index]; + const bound_view = &self.bindings.views[shd.VIEW_tex]; + if (texture.view.id == bound_view.id) { + bound_view.* = self.default_view; + } + + sg.destroyView(texture.view); + sg.destroyImage(texture.image); + } +} + +pub fn setTexture(id: TextureId, rgba8_pixels: [*]const u8, width: u32, height: u32) !void { + const self = &g_state; + if (!self.textures_slotmap.exists(id)) { + return error.NotFound; + } + const texture = &self.textures_buffer[id.index]; + + if (sg.queryImageState(texture.image) == .VALID) { + sg.uninitImage(texture.image); + } + + const pixel_count = width*height; + + var image_data: sg.ImageData = .{}; + image_data.mip_levels[0] = sg.asRange(rgba8_pixels[0..(pixel_count*4)]); + sg.initImage(texture.image, .{ + .type = ._2D, + .width = @intCast(width), + .height = @intCast(height), + .num_mipmaps = 1, + .pixel_format = .RGBA8, + .data = image_data, + }); + assert(sg.queryImageState(texture.image) == .VALID); + + if (sg.queryViewState(texture.view) == .ALLOC) { + sg.initView(texture.view, .{ + .texture = .{ .image = texture.image } + }); + assert(sg.queryViewState(texture.view) == .VALID); + } +} + +pub fn drawTexture(id: TextureId, pos: Vec2, size: Vec2, color: Color) void { + const self = &g_state; + if (!self.textures_slotmap.exists(id)) { + // TODO: Draw purple-black texture + return; + } + + const texture = self.textures_buffer[id.index]; + + const bound_view = &self.bindings.views[shd.VIEW_tex]; + if (bound_view.id != texture.view.id) { + flush(); + bound_view.* = texture.view; + } + + 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) + }, + }); +} + +const Texture = struct { + image: sg.Image, + view: sg.View, +}; + // const SlotMap = SlotMapType(u16, u8); // // const TextureId = u64; diff --git a/src/imgui.zig b/src/imgui.zig index 3437ede..cbe58f8 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -18,6 +18,7 @@ const ig = @import("cimgui"); const ImGUI = @This(); const State = struct { + enabled: bool, frame: ?Allocator }; @@ -40,6 +41,7 @@ pub fn init(logger: simgui.Logger) void { } self.* = State{ + .enabled = true, .frame = null }; } @@ -57,10 +59,15 @@ pub fn event(e: sapp.Event) bool { return false; } + const self = &g_state; + if (!self.enabled) { + return false; + } + return simgui.handleEvent(e); } -pub fn beginFrame(frame: Allocator) void { +pub fn beginFrame(enabled: bool, frame: Allocator) void { if (!build_options.has_imgui) { return; } @@ -68,6 +75,11 @@ pub fn beginFrame(frame: Allocator) void { const self = &g_state; self.frame = frame; + self.enabled = enabled; + + if (!self.enabled) { + return; + } simgui.newFrame(.{ .width = sapp.width(), @@ -105,6 +117,10 @@ pub fn endFrame() void { if (!build_options.has_imgui) { return; } + const self = &g_state; + if (!self.enabled) { + return; + } // TODO: // if (ig.igBegin("Game", null, ig.ImGuiWindowFlags_None)) { @@ -140,6 +156,10 @@ pub fn beginWindow(opts: WindowOptions) bool { if (!build_options.has_imgui) { return false; } + const self = &g_state; + if (!self.enabled) { + return false; + } if (opts.open != null and opts.open.?.* == false) { return false; @@ -180,6 +200,10 @@ pub fn endWindow() void { if (!build_options.has_imgui) { return; } + const self = &g_state; + if (!self.enabled) { + return; + } ig.igEnd(); } @@ -196,6 +220,10 @@ pub fn text(comptime fmt: []const u8, args: anytype) void { if (!build_options.has_imgui) { return; } + const self = &g_state; + if (!self.enabled) { + return; + } ig.igTextUnformatted(formatString(fmt, args)); } @@ -204,6 +232,10 @@ pub fn button(label: []const u8) bool { if (!build_options.has_imgui) { return false; } + const self = &g_state; + if (!self.enabled) { + return false; + } return ig.igButton(formatString("{s}", .{label})); } @@ -219,6 +251,10 @@ pub fn slider(opts: SliderOptions) bool { if (!build_options.has_imgui) { return false; } + const self = &g_state; + if (!self.enabled) { + return false; + } return ig.igSliderFloat( formatString("{s}", .{opts.label}), @@ -232,6 +268,10 @@ pub fn checkbox(label: []const u8, value: *bool) bool { if (!build_options.has_imgui) { return false; } + const self = &g_state; + if (!self.enabled) { + return false; + } return ig.igCheckbox( formatString("{s}", .{label}), @@ -243,6 +283,10 @@ pub fn separator() void { if (!build_options.has_imgui) { return; } + const self = &g_state; + if (!self.enabled) { + return; + } ig.igSeparator(); } @@ -257,6 +301,10 @@ pub fn colorEdit(opts: ColorEditOptions) bool { if (!build_options.has_imgui) { return false; } + const self = &g_state; + if (!self.enabled) { + return false; + } const label = formatString("{s}", .{opts.label}); var color: [4]f32 = .{ diff --git a/src/platform.zig b/src/platform.zig index 750224f..ae167d9 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -24,7 +24,7 @@ const shd = @import("shader"); pub const ImGUI = @import("./imgui.zig"); pub const Gfx = @import("./graphics.zig"); -const Input = @import("./input.zig"); +pub const Input = @import("./input.zig"); fn PlatformType(App: type) type { return struct { @@ -57,7 +57,7 @@ fn PlatformType(App: type) type { self.last_frame_at = self.started_at; self.input = Input.init(window_size); - Gfx.init(.{ + try Gfx.init(self.gpa, .{ .func = sokolLogCallback }); ImGUI.init(.{ @@ -103,9 +103,7 @@ fn PlatformType(App: type) type { { Gfx.beginFrame(); - if (self.show_imgui) { - ImGUI.beginFrame(self.frame_arena.allocator()); - } + ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator()); if (@hasDecl(App, "frame")) { const plt = Frame{ @@ -121,9 +119,7 @@ fn PlatformType(App: type) type { } Gfx.flush(); - if (self.show_imgui) { - ImGUI.endFrame(); - } + ImGUI.endFrame(); Gfx.endFrame(); }