add drawing texture support

This commit is contained in:
Rokas Puzonas 2026-07-10 01:17:46 +03:00
parent 47d3dad1c3
commit 76668e841d
4 changed files with 229 additions and 84 deletions

View File

@ -17,14 +17,9 @@ bg: Platform.Color,
show_first_window: bool, show_first_window: bool,
check: bool, check: bool,
player_pos: Vec2, player_pos: Vec2,
tex: Gfx.TextureId,
pub fn init(self: *App, plt: Platform.Init) !?u8 { 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 cwd = std.Io.Dir.cwd();
const sokoban_spritesheet = try cwd.readFileAlloc( const sokoban_spritesheet = try cwd.readFileAlloc(
plt.io, plt.io,
@ -37,6 +32,17 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
var image = try STBImage.load(sokoban_spritesheet); var image = try STBImage.load(sokoban_spritesheet);
defer image.deinit(); 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; 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)); self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt));
Gfx.setClearColor(self.bg); 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)); Gfx.drawRectangle(self.player_pos.add(.init(0, 200)), .init(100, 100), .rgb(200, 200, 0));
if (ImGUI.beginWindow(.{ if (ImGUI.beginWindow(.{

View File

@ -16,25 +16,32 @@ const Color = @import("./color.zig");
const shd = @import("shader"); const shd = @import("shader");
const SlotMapType = @import("slot_map.zig").SlotMapType; const SlotMapType = @import("slot_map.zig").SlotMapType;
const State = struct { const State = struct {
gpa: std.mem.Allocator,
shader: sg.Shader, shader: sg.Shader,
pipeline: sg.Pipeline, pipeline: sg.Pipeline,
bindings: sg.Bindings, bindings: sg.Bindings,
default_image: sg.Image, default_texture: TextureId,
default_view: sg.View,
default_sampler: sg.Sampler, default_sampler: sg.Sampler,
quads_buffer: [2048]Quad, quads_buffer: [2048]Quad,
quads: std.ArrayList(Quad), quads: std.ArrayList(Quad),
clear_color: Color, 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; 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; const self = &g_state;
sg.setup(.{ sg.setup(.{
@ -45,27 +52,6 @@ pub fn init(logger: sg.Logger) void {
const shader = sg.makeShader(shd.mainShaderDesc(sg.queryBackend())); const shader = sg.makeShader(shd.mainShaderDesc(sg.queryBackend()));
assert(shader.id != sg.invalid_id); 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(.{ const default_sampler = sg.makeSampler(.{
.min_filter = .NEAREST, .min_filter = .NEAREST,
.mag_filter = .NEAREST, .mag_filter = .NEAREST,
@ -109,7 +95,7 @@ pub fn init(logger: sg.Logger) void {
bindings.vertex_buffers[0] = sg.makeBuffer(.{ bindings.vertex_buffers[0] = sg.makeBuffer(.{
.size = @sizeOf(Quad) * max_quads, .size = @sizeOf(Quad) * max_quads,
.usage = .{ .vertex_buffer = true, .dynamic_update = true }, .usage = .{ .vertex_buffer = true, .stream_update = true },
.label = "quad-vertices" .label = "quad-vertices"
}); });
@ -133,25 +119,38 @@ pub fn init(logger: sg.Logger) void {
} }
self.* = State{ self.* = State{
.gpa = gpa,
.shader = shader, .shader = shader,
.pipeline = pipeline, .pipeline = pipeline,
.bindings = bindings, .bindings = bindings,
.default_image = default_image,
.default_view = default_view,
.default_sampler = default_sampler, .default_sampler = default_sampler,
.quads_buffer = undefined, .quads_buffer = undefined,
.quads = .initBuffer(&self.quads_buffer), .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 { pub fn deinit() void {
const self = &g_state; const self = &g_state;
self.textures_slotmap.deinit(self.gpa);
sg.destroyPipeline(self.pipeline); sg.destroyPipeline(self.pipeline);
sg.destroyShader(self.shader); sg.destroyShader(self.shader);
sg.destroyImage(self.default_image);
sg.destroyView(self.default_view);
sg.destroySampler(self.default_sampler); sg.destroySampler(self.default_sampler);
sg.shutdown(); sg.shutdown();
} }
@ -197,6 +196,11 @@ pub fn beginFrame() void {
.action = pass_action, .action = pass_action,
.swapchain = sglue.swapchain() .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. // TODO: This will always have a 1-frame delay.
@ -213,11 +217,17 @@ pub fn flush() void {
if (self.quads.items.len == 0) { if (self.quads.items.len == 0) {
return; return;
} }
defer self.quads.clearRetainingCapacity();
sg.updateBuffer( const vertex_buffer = self.bindings.vertex_buffers[0];
self.bindings.vertex_buffers[0],
sg.asRange(self.quads.items) 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` // TODO: Rectify `window_size` vs `gfx.window_size`
const window_size = Vec2.init(sapp.widthf(), sapp.heightf()); const window_size = Vec2.init(sapp.widthf(), sapp.heightf());
@ -229,15 +239,10 @@ pub fn flush() void {
.projection = createProjectionMatrix(window_size) .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.applyPipeline(self.pipeline);
sg.applyBindings(self.bindings); sg.applyBindings(self.bindings);
sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params)); sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params));
sg.draw(0, @intCast(self.quads.items.len * 6), 1); sg.draw(0, @intCast(self.quads.items.len * 6), 1);
self.quads.clearRetainingCapacity();
} }
pub fn endFrame() void { pub fn endFrame() void {
@ -258,34 +263,9 @@ pub fn drawQuad(quad: Quad) void {
} }
pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void { pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void {
const color_vec4 = Vec4{ const self = &g_state;
.x = color.r,
.y = color.g, drawTexture(self.default_texture, pos, size, color);
.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 { 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 SlotMap = SlotMapType(u16, u8);
// //
// const TextureId = u64; // const TextureId = u64;

View File

@ -18,6 +18,7 @@ const ig = @import("cimgui");
const ImGUI = @This(); const ImGUI = @This();
const State = struct { const State = struct {
enabled: bool,
frame: ?Allocator frame: ?Allocator
}; };
@ -40,6 +41,7 @@ pub fn init(logger: simgui.Logger) void {
} }
self.* = State{ self.* = State{
.enabled = true,
.frame = null .frame = null
}; };
} }
@ -57,10 +59,15 @@ pub fn event(e: sapp.Event) bool {
return false; return false;
} }
const self = &g_state;
if (!self.enabled) {
return false;
}
return simgui.handleEvent(e); return simgui.handleEvent(e);
} }
pub fn beginFrame(frame: Allocator) void { pub fn beginFrame(enabled: bool, frame: Allocator) void {
if (!build_options.has_imgui) { if (!build_options.has_imgui) {
return; return;
} }
@ -68,6 +75,11 @@ pub fn beginFrame(frame: Allocator) void {
const self = &g_state; const self = &g_state;
self.frame = frame; self.frame = frame;
self.enabled = enabled;
if (!self.enabled) {
return;
}
simgui.newFrame(.{ simgui.newFrame(.{
.width = sapp.width(), .width = sapp.width(),
@ -105,6 +117,10 @@ pub fn endFrame() void {
if (!build_options.has_imgui) { if (!build_options.has_imgui) {
return; return;
} }
const self = &g_state;
if (!self.enabled) {
return;
}
// TODO: // TODO:
// if (ig.igBegin("Game", null, ig.ImGuiWindowFlags_None)) { // if (ig.igBegin("Game", null, ig.ImGuiWindowFlags_None)) {
@ -140,6 +156,10 @@ pub fn beginWindow(opts: WindowOptions) bool {
if (!build_options.has_imgui) { if (!build_options.has_imgui) {
return false; return false;
} }
const self = &g_state;
if (!self.enabled) {
return false;
}
if (opts.open != null and opts.open.?.* == false) { if (opts.open != null and opts.open.?.* == false) {
return false; return false;
@ -180,6 +200,10 @@ pub fn endWindow() void {
if (!build_options.has_imgui) { if (!build_options.has_imgui) {
return; return;
} }
const self = &g_state;
if (!self.enabled) {
return;
}
ig.igEnd(); ig.igEnd();
} }
@ -196,6 +220,10 @@ pub fn text(comptime fmt: []const u8, args: anytype) void {
if (!build_options.has_imgui) { if (!build_options.has_imgui) {
return; return;
} }
const self = &g_state;
if (!self.enabled) {
return;
}
ig.igTextUnformatted(formatString(fmt, args)); ig.igTextUnformatted(formatString(fmt, args));
} }
@ -204,6 +232,10 @@ pub fn button(label: []const u8) bool {
if (!build_options.has_imgui) { if (!build_options.has_imgui) {
return false; return false;
} }
const self = &g_state;
if (!self.enabled) {
return false;
}
return ig.igButton(formatString("{s}", .{label})); return ig.igButton(formatString("{s}", .{label}));
} }
@ -219,6 +251,10 @@ pub fn slider(opts: SliderOptions) bool {
if (!build_options.has_imgui) { if (!build_options.has_imgui) {
return false; return false;
} }
const self = &g_state;
if (!self.enabled) {
return false;
}
return ig.igSliderFloat( return ig.igSliderFloat(
formatString("{s}", .{opts.label}), formatString("{s}", .{opts.label}),
@ -232,6 +268,10 @@ pub fn checkbox(label: []const u8, value: *bool) bool {
if (!build_options.has_imgui) { if (!build_options.has_imgui) {
return false; return false;
} }
const self = &g_state;
if (!self.enabled) {
return false;
}
return ig.igCheckbox( return ig.igCheckbox(
formatString("{s}", .{label}), formatString("{s}", .{label}),
@ -243,6 +283,10 @@ pub fn separator() void {
if (!build_options.has_imgui) { if (!build_options.has_imgui) {
return; return;
} }
const self = &g_state;
if (!self.enabled) {
return;
}
ig.igSeparator(); ig.igSeparator();
} }
@ -257,6 +301,10 @@ pub fn colorEdit(opts: ColorEditOptions) bool {
if (!build_options.has_imgui) { if (!build_options.has_imgui) {
return false; return false;
} }
const self = &g_state;
if (!self.enabled) {
return false;
}
const label = formatString("{s}", .{opts.label}); const label = formatString("{s}", .{opts.label});
var color: [4]f32 = .{ var color: [4]f32 = .{

View File

@ -24,7 +24,7 @@ const shd = @import("shader");
pub const ImGUI = @import("./imgui.zig"); pub const ImGUI = @import("./imgui.zig");
pub const Gfx = @import("./graphics.zig"); pub const Gfx = @import("./graphics.zig");
const Input = @import("./input.zig"); pub const Input = @import("./input.zig");
fn PlatformType(App: type) type { fn PlatformType(App: type) type {
return struct { return struct {
@ -57,7 +57,7 @@ fn PlatformType(App: type) type {
self.last_frame_at = self.started_at; self.last_frame_at = self.started_at;
self.input = Input.init(window_size); self.input = Input.init(window_size);
Gfx.init(.{ try Gfx.init(self.gpa, .{
.func = sokolLogCallback .func = sokolLogCallback
}); });
ImGUI.init(.{ ImGUI.init(.{
@ -103,9 +103,7 @@ fn PlatformType(App: type) type {
{ {
Gfx.beginFrame(); Gfx.beginFrame();
if (self.show_imgui) { ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator());
ImGUI.beginFrame(self.frame_arena.allocator());
}
if (@hasDecl(App, "frame")) { if (@hasDecl(App, "frame")) {
const plt = Frame{ const plt = Frame{
@ -121,9 +119,7 @@ fn PlatformType(App: type) type {
} }
Gfx.flush(); Gfx.flush();
if (self.show_imgui) {
ImGUI.endFrame(); ImGUI.endFrame();
}
Gfx.endFrame(); Gfx.endFrame();
} }