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,
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(.{

View File

@ -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;

View File

@ -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 = .{

View File

@ -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();
}