diff --git a/assets/sokoban/Spritesheet/sokoban_spritesheet.png b/assets/sokoban/Spritesheet/sokoban_spritesheet.png deleted file mode 100644 index ba40e6c..0000000 Binary files a/assets/sokoban/Spritesheet/sokoban_spritesheet.png and /dev/null differ diff --git a/assets/sokoban/sokoban_tilesheet.png b/assets/sokoban/sokoban_tilesheet.png new file mode 100644 index 0000000..e3d423a Binary files /dev/null and b/assets/sokoban/sokoban_tilesheet.png differ diff --git a/assets/sokoban/sokoban_tilesheet@2.png b/assets/sokoban/sokoban_tilesheet@2.png new file mode 100644 index 0000000..b1833fa Binary files /dev/null and b/assets/sokoban/sokoban_tilesheet@2.png differ diff --git a/build.zig b/build.zig index 23c9af4..e454659 100644 --- a/build.zig +++ b/build.zig @@ -69,6 +69,7 @@ pub fn build(b: *std.Build) !void { const dep_stb = b.dependency("stb", .{}); exe_mod.addImport("stb_image", dep_stb.module("stb_image")); exe_mod.addImport("stb_vorbis", dep_stb.module("stb_vorbis")); + exe_mod.addImport("stb_rect_pack", dep_stb.module("stb_rect_pack")); const dep_tracy = b.dependency("tracy", .{ .target = target, diff --git a/libs/stb/build.zig b/libs/stb/build.zig index 963b76f..7acb90c 100644 --- a/libs/stb/build.zig +++ b/libs/stb/build.zig @@ -22,17 +22,32 @@ pub fn build(b: *std.Build) void { } { - const mod_stb_image = b.addModule("stb_vorbis", .{ + const mod_stb_vorbis = b.addModule("stb_vorbis", .{ .target = target, .optimize = optimize, .root_source_file = b.path("src/stb_vorbis.zig"), .link_libc = true, }); - mod_stb_image.addIncludePath(stb_dependency.path(".")); - mod_stb_image.addCSourceFile(.{ + mod_stb_vorbis.addIncludePath(stb_dependency.path(".")); + mod_stb_vorbis.addCSourceFile(.{ .file = b.path("src/stb_vorbis_impl.c"), .flags = &.{} }); } + + { + const mod_stb_rect_pack = b.addModule("stb_rect_pack", .{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("src/stb_rect_pack.zig"), + .link_libc = true, + }); + + mod_stb_rect_pack.addIncludePath(stb_dependency.path(".")); + mod_stb_rect_pack.addCSourceFile(.{ + .file = b.path("src/stb_rect_pack_impl.c"), + .flags = &.{} + }); + } } diff --git a/libs/stb/src/stb_rect_pack.zig b/libs/stb/src/stb_rect_pack.zig new file mode 100644 index 0000000..18065eb --- /dev/null +++ b/libs/stb/src/stb_rect_pack.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +const c = @cImport({ + @cInclude("stb_rect_pack.h"); +}); + +const STBRectPack = @This(); + +ctx: c.stbrp_context, + +pub fn init(self: *STBRectPack, width: u32, height: u32, nodes: []Node) void { + c.stbrp_init_target(&self.ctx, @intCast(width), @intCast(height), nodes.ptr, @intCast(nodes.len)); +} + +pub fn setupAllowOutOfMem(self: *STBRectPack, enabled: bool) void { + c.stbrp_setup_heuristic(&self.ctx, if (enabled) 1 else 0); +} + +pub fn setupHeuristic(self: *STBRectPack, heuristic: Heuristic) void { + c.stbrp_setup_heuristic(&self.ctx, heuristic); +} + +pub fn packRects(self: *STBRectPack, rects: []Rect) bool { + return c.stbrp_pack_rects(&self.ctx, rects.ptr, @intCast(rects.len)) == 1; +} + +pub const Heuristic = enum(i32) { + Skyline_BL_sortHeight = c.STBRP_HEURISTIC_Skyline_BL_sortHeight, + Skyline_BF_sortHeight = c.STBRP_HEURISTIC_Skyline_BF_sortHeight, + _, + + pub const default = Heuristic.Skyline_BL_sortHeight; +}; + +pub const Node = c.stbrp_node; +pub const Rect = c.stbrp_rect; diff --git a/libs/stb/src/stb_rect_pack_impl.c b/libs/stb/src/stb_rect_pack_impl.c new file mode 100644 index 0000000..31c4106 --- /dev/null +++ b/libs/stb/src/stb_rect_pack_impl.c @@ -0,0 +1,2 @@ +#define STB_RECT_PACK_IMPLEMENTATION +#include "stb_rect_pack.h" diff --git a/src/app.zig b/src/app.zig index 0ff8d22..dae3429 100644 --- a/src/app.zig +++ b/src/app.zig @@ -10,6 +10,7 @@ const STBImage = @import("stb_image"); const Platform = @import("./platform.zig"); const Gfx = Platform.Gfx; const ImGUI = Platform.ImGUI; +const Rect = Platform.Rect; const App = @This(); @@ -17,13 +18,14 @@ bg: Platform.Color, show_first_window: bool, check: bool, player_pos: Vec2, -tex: Gfx.TextureId, +sprite_sprite: Gfx.Sprite.Id, +wall_sprite: Gfx.Sprite.Id, pub fn init(self: *App, plt: Platform.Init) !?u8 { const cwd = std.Io.Dir.cwd(); const sokoban_spritesheet = try cwd.readFileAlloc( plt.io, - "assets/sokoban/Spritesheet/sokoban_spritesheet.png", + "assets/sokoban/sokoban_tilesheet.png", plt.gpa, .unlimited ); @@ -32,15 +34,35 @@ 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); + const spritesheet = Gfx.ImageData{ + .width = image.width, + .height = image.height, + .pixels = .{ .rgba8 = image.rgba8_pixels } + }; + + const tile_size = Vec2.init(64, 64); + + const player_sprite = try Gfx.initSprite(); + Gfx.setSprite(player_sprite, .{ + .image = spritesheet, + .rect = Rect.init(0, 4, 1, 1).multiply(tile_size) + }); + + const wall_sprite = try Gfx.initSprite(); + Gfx.setSprite(wall_sprite, .{ + .image = spritesheet, + .rect = Rect.init(1, 0, 1, 1).multiply(tile_size) + }); + + try Gfx.packSprites(); self.* = App{ .bg = .black, .player_pos = .init(100, 100), .show_first_window = true, .check = false, - .tex = tex + .sprite_sprite = player_sprite, + .wall_sprite = wall_sprite }; return null; @@ -68,8 +90,8 @@ 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.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(255, 255, 255)); + Gfx.drawSprite(self.sprite_sprite, self.player_pos, .init(100, 100), .white); + Gfx.drawSprite(self.wall_sprite, self.player_pos.add(.init(0, 200)), .init(100, 100), .white); if (ImGUI.beginWindow(.{ .name = "Hello", diff --git a/src/graphics.zig b/src/graphics.zig index a8dca2e..61c9c48 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -1,6 +1,7 @@ const std = @import("std"); const log = std.log.scoped(.graphics); const assert = std.debug.assert; +const Allocator = std.mem.Allocator; const sokol = @import("sokol"); const sg = sokol.gfx; @@ -11,7 +12,9 @@ const Math = @import("math"); const Vec2 = Math.Vec2; const Vec4 = Math.Vec4; const Mat4 = Math.Mat4; +const Rect = Math.Rect; +const STBRectPack = @import("stb_rect_pack"); const Color = @import("./color.zig"); const shd = @import("shader"); const SlotMapType = @import("slot_map.zig").SlotMapType; @@ -23,8 +26,8 @@ const State = struct { pipeline: sg.Pipeline, bindings: sg.Bindings, - default_texture: TextureId, - error_texture: TextureId, + default_sprite: Sprite.Id, + error_sprite: Sprite.Id, default_sampler: sg.Sampler, quads_buffer: [2048]Quad, @@ -32,13 +35,15 @@ const State = struct { clear_color: Color, - textures_buffer: [max_textures]Texture, - textures_slotmap: TextureSlotMap, -}; + textures_buffer: [Texture.max_textures]Texture.SlotMap.Slot, + textures: Texture.SlotMap, -const max_textures = 64; -const TextureSlotMap = SlotMapType(std.math.IntFittingRange(0, max_textures-1), u8); -pub const TextureId = TextureSlotMap.Id; + spritesheet_texture: Texture.Id, + spritesheet_size: Vec2, + spritesheet_stale: bool, + sprites_buffer: [Sprite.max_sprites]Sprite.SlotMap.Slot, + sprites: Sprite.SlotMap, +}; var g_state: State = undefined; @@ -129,9 +134,14 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { .quads = .initBuffer(&self.quads_buffer), .clear_color = .black, .textures_buffer = undefined, - .textures_slotmap = .init(try .initCapacity(gpa, max_textures)), - .default_texture = undefined, - .error_texture = undefined + .textures = .init(.initBuffer(&self.textures_buffer)), + .default_sprite = undefined, + .error_sprite = undefined, + .spritesheet_stale = false, + .sprites_buffer = undefined, + .sprites = .init(.initBuffer(&self.sprites_buffer)), + .spritesheet_texture = undefined, + .spritesheet_size = .init(0, 0) }; { @@ -141,8 +151,14 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { var pixels: [width * height * 4]u8 = undefined; @memset(&pixels, 0xFF); - self.default_texture = try initTexture(); - try setTexture(self.default_texture, &pixels, width, height); + self.default_sprite = try initSprite(); + setSprite(self.default_sprite, .{ + .image = ImageData{ + .width = width, + .height = height, + .pixels = .{ .rgba8 = &pixels } + } + }); } { @@ -169,15 +185,29 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { } } - self.error_texture = try initTexture(); - try setTexture(self.error_texture, &pixels, width, height); + self.error_sprite = try initSprite(); + setSprite(self.error_sprite, .{ + .image = .{ + .width = width, + .height = height, + .pixels = .{ .rgba8 = &pixels } + } + }); } + + self.spritesheet_texture = try initTexture(); } pub fn deinit() void { const self = &g_state; - self.textures_slotmap.deinit(self.gpa); + var sprite_iter = self.sprites.iterator(); + while (sprite_iter.next()) |sprite_id| { + const sprite = self.sprites.getAssumeExists(sprite_id); + if (sprite.data) |data| { + data.deinit(self.gpa); + } + } sg.destroyPipeline(self.pipeline); sg.destroyShader(self.shader); @@ -227,9 +257,8 @@ pub fn beginFrame() void { .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; + const spritesheet_texture = self.textures.getAssumeExists(self.spritesheet_texture); + self.bindings.views[shd.VIEW_tex] = spritesheet_texture.view; self.bindings.samplers[shd.SMP_smp] = self.default_sampler; } @@ -285,7 +314,7 @@ pub fn endFrame() void { pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void { const self = &g_state; - drawTexture(self.default_texture, pos, size, color); + drawSprite(self.default_sprite, pos, size, color); } pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { @@ -300,21 +329,21 @@ pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { var quad: Quad = undefined; quad.setColor(color); - quad.setUVRect(.init(0, 0), .init(1, 1)); + quad.setUVRect(.init(0, 0, 1, 1)); quad.vertices[0] = top_right; quad.vertices[1] = top_left; quad.vertices[2] = bottom_right; quad.vertices[3] = bottom_left; draw(.{ - .texture = self.default_texture, + .texture = self.default_sprite, .quad = quad }); } -pub fn initTexture() !TextureId { +pub fn initTexture() !Texture.Id { const self = &g_state; - const id = try self.textures_slotmap.insertBounded(); + const id = try self.textures.insertBounded(); const image = sg.allocImage(); assert(image.id != sg.invalid_id); @@ -322,7 +351,7 @@ pub fn initTexture() !TextureId { const view = sg.allocView(); assert(view.id != sg.invalid_id); - const texture = &self.textures_buffer[id.index]; + const texture = self.textures.getAssumeExists(id); texture.* = .{ .image = image, .view = view @@ -330,14 +359,14 @@ pub fn initTexture() !TextureId { return id; } -pub fn deinitTexture(id: TextureId) void { +pub fn deinitTexture(id: Texture.Id) void { const self = &g_state; - if (self.textures_slotmap.remove(id)) { + if (self.textures.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) { - assert(self.textures_slotmap.exists(self.default_texture)); - bound_view.* = self.textures_buffer[self.default_texture.index].view; + assert(self.textures.exists(self.default_sprite)); + bound_view.* = self.textures_buffer[self.default_sprite.index].view; } sg.destroyView(texture.view); @@ -345,25 +374,26 @@ pub fn deinitTexture(id: TextureId) void { } } -pub fn setTexture(id: TextureId, rgba8_pixels: [*]const u8, width: u32, height: u32) !void { +pub fn setTexture(id: Texture.Id, texture_data: ImageData) void { const self = &g_state; - if (!self.textures_slotmap.exists(id)) { - return error.NotFound; + if (!self.textures.exists(id)) { + log.warn("Attempt to set data for texture that doesn't exist: {}", .{id}); + return; } - const texture = &self.textures_buffer[id.index]; + const texture = self.textures.getAssumeExists(id); if (sg.queryImageState(texture.image) == .VALID) { sg.uninitImage(texture.image); } - const pixel_count = width*height; + const pixel_count = texture_data.width*texture_data.height; var image_data: sg.ImageData = .{}; - image_data.mip_levels[0] = sg.asRange(rgba8_pixels[0..(pixel_count*4)]); + image_data.mip_levels[0] = sg.asRange(texture_data.pixels.rgba8[0..(pixel_count*4)]); sg.initImage(texture.image, .{ .type = ._2D, - .width = @intCast(width), - .height = @intCast(height), + .width = @intCast(texture_data.width), + .height = @intCast(texture_data.height), .num_mipmaps = 1, .pixel_format = .RGBA8, .data = image_data, @@ -378,11 +408,11 @@ pub fn setTexture(id: TextureId, rgba8_pixels: [*]const u8, width: u32, height: } } -pub fn drawTexture(id: TextureId, pos: Vec2, size: Vec2, color: Color) void { +pub fn drawTexture(id: Texture.Id, pos: Vec2, size: Vec2, color: Color) void { var quad: Quad = undefined; quad.setColor(color); - quad.setRect(pos, size); - quad.setUVRect(.init(0, 0), .init(1, 1)); + quad.setRect(.{ .pos = pos, .size = size }); + quad.setUVRect(.init(0, 0, 1, 1)); draw(.{ .texture = id, @@ -391,7 +421,7 @@ pub fn drawTexture(id: TextureId, pos: Vec2, size: Vec2, color: Color) void { } const DrawOptions = struct { - texture: TextureId, + texture: Texture.Id, quad: Quad }; @@ -400,12 +430,18 @@ pub fn draw(opts: DrawOptions) void { var quad = opts.quad; var texture = opts.texture; - if (!self.textures_slotmap.exists(texture)) { - texture = self.error_texture; - quad.setColor(.white); + if (!self.textures.exists(texture)) { + if (getSpriteUVRect(self.error_sprite)) |error_uv| { + texture = self.spritesheet_texture; + quad.setUVRect(error_uv); + quad.setColor(.white); + } else { + log.err("Failed to draw error texture", .{}); + return; + } } - const view = self.textures_buffer[texture.index].view; + const view = self.textures.getAssumeExists(texture).view; const bound_view = &self.bindings.views[shd.VIEW_tex]; if (bound_view.id != view.id) { @@ -420,9 +456,311 @@ pub fn draw(opts: DrawOptions) void { self.quads.appendAssumeCapacity(quad); } -const Texture = struct { +pub fn initSprite() !Sprite.Id { + const self = &g_state; + const id = try self.sprites.insertBounded(); + + const sprite = self.sprites.getAssumeExists(id); + sprite.* = .{ + .data = null, + .position = null + }; + + return id; +} + +pub fn deinitSprite(id: Sprite.Id) void { + const self = &g_state; + if (self.sprites.exists(id)) { + const sprite = self.sprites.getAssumeExists(id); + if (sprite.data) |data| { + data.deinit(self.gpa); + } + self.sprites.removeAssumeExists(id); + self.spritesheet_stale = true; + } +} + +const SetSpriteOptions = struct { + image: ImageData, + rect: ?Rect = null +}; + +pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void { + const self = &g_state; + if (!self.sprites.exists(id)) { + log.warn("Attempt to set data for sprite that doesn't exist: {}", .{id}); + return; + } + + var sprite_data: ImageData = .empty; + errdefer sprite_data.deinit(self.gpa); + + if (opts.rect) |rect| { + sprite_data = ImageData.initRGBA8(self.gpa, @trunc(rect.size.x), @trunc(rect.size.y)) catch |e| { + log.warn("Failed to create texture data: {}", .{e}); + return; + }; + + sprite_data.copyFrom( + opts.image, + 0, 0, + @trunc(rect.pos.x), @trunc(rect.pos.y), + @trunc(rect.size.x), @trunc(rect.size.y) + ); + } else { + sprite_data = opts.image.clone(self.gpa) catch |e| { + log.warn("Failed to clone image: {}", .{e}); + return; + }; + } + + const sprite = self.sprites.getAssumeExists(id); + if (sprite.data) |old_sprite_data| { + old_sprite_data.deinit(self.gpa); + } + + sprite.data = sprite_data; + sprite.position = null; + self.spritesheet_stale = true; +} + +pub fn packSprites() !void { + const self = &g_state; + + // TODO: Add a smarter startegy for picking the initial texture size. + // One idea is to calculate the sum area of all sprites and pick a atlas size based on that. + const texture_sizes = [_]u32{ + 64, 128, 256, 512, 1024, 2048, 4096 + }; + + var nodes: [32]STBRectPack.Node = undefined; + var rects: [Sprite.max_sprites]STBRectPack.Rect = undefined; + + for (&texture_sizes) |texture_size| { + var ctx: STBRectPack = undefined; + ctx.init(texture_size, texture_size, &nodes); + ctx.setupAllowOutOfMem(true); + + @memset(&rects, .{}); + + + { + var i: usize = 0; + var sprite_iter = self.sprites.iterator(); + while (sprite_iter.next()) |sprite_id| : (i += 1) { + const sprite = self.sprites.getAssumeExists(sprite_id); + const sprite_data = sprite.data orelse continue; + rects[i] = .{ + .w = @intCast(sprite_data.width), + .h = @intCast(sprite_data.height), + }; + } + } + + const success = ctx.packRects(&rects); + std.debug.print("success {} {}\n", .{texture_size, success}); + if (!success) { + continue; + } + + const spritesheet = try ImageData.initRGBA8(self.gpa, texture_size, texture_size); + defer spritesheet.deinit(self.gpa); + + { + var i: usize = 0; + var sprite_iter = self.sprites.iterator(); + while (sprite_iter.next()) |sprite_id| : (i += 1) { + const sprite = self.sprites.getAssumeExists(sprite_id); + const sprite_data = sprite.data orelse continue; + + assert(rects[i].was_packed == 1); + const x: u32 = @intCast(rects[i].x); + const y: u32 = @intCast(rects[i].y); + sprite.position = .initFromInt(u32, x, y); + spritesheet.copyFrom( + sprite_data, + x, + y, + 0, + 0, + sprite_data.width, + sprite_data.height + ); + } + } + + setTexture(self.spritesheet_texture, spritesheet); + + self.spritesheet_size = .initFromInt(u32, texture_size, texture_size); + self.spritesheet_stale = false; + return; + } + + + const max_texture_size = texture_sizes[texture_sizes.len-1]; + log.warn("Failed to pack sprites, couldn't create a texture that is large enough. Max size tried: {}x{}", .{max_texture_size, max_texture_size}); +} + +pub fn getSpriteUVRect(id: Sprite.Id) ?Rect { + const self = &g_state; + + if (!self.sprites.exists(id)) { + return null; + } + const sprite = self.sprites.getAssumeExists(id); + if (sprite.data == null) { + return null; + } + + if (self.spritesheet_stale) { + packSprites() catch |e| { + log.warn("Failed to pack sprites: {}", .{e}); + }; + } + + assert(sprite.position != null); + + const sprite_size = Vec2.initFromInt(u32, sprite.data.?.width, sprite.data.?.height); + return Rect{ + .pos = sprite.position.?.divide(self.spritesheet_size), + .size = sprite_size.divide(self.spritesheet_size), + }; +} + +pub fn drawSprite(id: Sprite.Id, pos: Vec2, size: Vec2, color: Color) void { + const self = &g_state; + + var quad: Quad = undefined; + quad.setColor(color); + quad.setRect(.{ .pos = pos, .size = size }); + + if (getSpriteUVRect(id)) |sprite_uv| { + quad.setUVRect(sprite_uv); + } else if (getSpriteUVRect(self.error_sprite)) |error_uv| { + quad.setUVRect(error_uv); + quad.setColor(.white); + } + + draw(.{ + .texture = self.spritesheet_texture, + .quad = quad + }); +} + +pub const ImageData = struct { + const Pixels = union(enum) { + none, + rgba8: [*]u8 + }; + + pixels: Pixels, + width: u32, + height: u32, + + pub const empty = ImageData{ + .pixels = .none, + .width = 0, + .height = 0, + }; + + pub fn initRGBA8(gpa: Allocator, width: u32, height: u32) !ImageData { + const pixel_count = width * height; + const pixels = try gpa.alloc(u8, pixel_count * 4); + + return ImageData{ + .pixels = .{ .rgba8 = pixels.ptr }, + .width = width, + .height = height + }; + } + + pub fn clone(self: ImageData, gpa: Allocator) !ImageData { + const pixel_count = self.width * self.height; + const pixels = switch (self.pixels) { + .none => Pixels{ .none = {} }, + .rgba8 => |pixels| Pixels{ + .rgba8 = (try gpa.dupe(u8, pixels[0..(pixel_count * 4)])).ptr + } + }; + + return ImageData{ + .pixels = pixels, + .width = self.width, + .height = self.height + }; + } + + pub fn deinit(self: ImageData, gpa: Allocator) void { + const pixel_count = self.width * self.height; + switch (self.pixels) { + .none => {}, + .rgba8 => |pixels| gpa.free(pixels[0..(pixel_count*4)]) + } + } + + pub fn copyFrom( + self: ImageData, + source: ImageData, + cx: u32, + cy: u32, + source_cx: u32, + source_cy: u32, + source_width: u32, + source_height: u32, + ) void { + if (source.pixels == .none or self.pixels == .none) { + return; + } + + for (0..source_height) |oy| { + const source_y = source_cy+oy; + const dest_y = cy+oy; + if (dest_y >= self.height) { + continue; + } + if (source_y >= source.height) { + continue; + } + + for (0..source_width) |ox| { + const source_x = source_cx+ox; + const dest_x = cx+ox; + if (dest_x >= self.width) { + continue; + } + if (source_x >= source.width) { + continue; + } + + const source_pixel_index = source_y * source.width + source_x; + const source_pixel = source.pixels.rgba8[(4*source_pixel_index)..][0..4]; + + const dest_pixel_index = dest_y * self.width + dest_x; + const dest_pixel = self.pixels.rgba8[(4*dest_pixel_index)..][0..4]; + + @memcpy(dest_pixel, source_pixel); + } + } + } +}; + +pub const Texture = struct { image: sg.Image, view: sg.View, + + const max_textures = 64; + const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_textures-1), u8, Texture); + pub const Id = SlotMap.Id; +}; + +pub const Sprite = struct { + data: ?ImageData, + position: ?Vec2, + + const max_sprites = 128; + const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_sprites-1), u8, Sprite); + pub const Id = SlotMap.Id; }; pub const Vertex = extern struct { @@ -451,14 +789,18 @@ pub const Quad = extern struct { } } - pub fn setUVRect(self: *Quad, pos: Vec2, size: Vec2) void { + pub fn setUVRect(self: *Quad, rect: Rect) void { + const pos = rect.pos; + const size = rect.size; self.vertices[0].texcoord = pos; self.vertices[1].texcoord = pos.add(.init(size.x, 0)); self.vertices[2].texcoord = pos.add(.init(0, size.y)); self.vertices[3].texcoord = pos.add(size); } - pub fn setRect(self: *Quad, pos: Vec2, size: Vec2) void { + pub fn setRect(self: *Quad, rect: Rect) void { + const pos = rect.pos; + const size = rect.size; self.vertices[0].position = pos; self.vertices[1].position = pos.add(.init(size.x, 0)); self.vertices[2].position = pos.add(.init(0, size.y)); diff --git a/src/platform.zig b/src/platform.zig index ae167d9..d92a049 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -12,10 +12,11 @@ const sg = sokol.gfx; const sglue = sokol.glue; const sgl = sokol.gl; -const Math = @import("math"); -const Vec2 = Math.Vec2; -const Vec4 = Math.Vec4; -const Mat4 = Math.Mat4; +pub const Math = @import("math"); +pub const Vec2 = Math.Vec2; +pub const Vec4 = Math.Vec4; +pub const Mat4 = Math.Mat4; +pub const Rect = Math.Rect; const tracy = @import("tracy"); diff --git a/src/slot_map.zig b/src/slot_map.zig index b270315..5bbb9c2 100644 --- a/src/slot_map.zig +++ b/src/slot_map.zig @@ -13,7 +13,7 @@ const Allocator = std.mem.Allocator; /// /// For more details about this data structure checkout this podcast episode from Wookash: /// * https://www.youtube.com/watch?v=ShSGHb65f3M -pub fn SlotMapType(Index: type, Generation: type) type { +pub fn SlotMapType(Index: type, Generation: type, Value: type) type { assert(@typeInfo(Generation) == .int); assert(@typeInfo(Index) == .int); @@ -28,6 +28,7 @@ pub fn SlotMapType(Index: type, Generation: type) type { // // So in the end idk if it's worth it. pub const Slot = struct { + value: Value, generation: Generation, next_hole: ?Index, used: bool @@ -109,6 +110,7 @@ pub fn SlotMapType(Index: type, Generation: type) type { const index: Index = @intCast(self.slots.items.len); self.slots.items.len += 1; self.slots.items[index] = Slot{ + .value = undefined, .generation = 0, .next_hole = null, .used = false @@ -171,6 +173,11 @@ pub fn SlotMapType(Index: type, Generation: type) type { return slot.used and slot.generation == id.generation; } + pub fn getAssumeExists(self: *Self, id: Id) *Value { + assert(self.exists(id)); + return &self.slots.items[id.index].value; + } + pub fn removeAssumeExists(self: *Self, id: Id) void { assert(self.exists(id)); @@ -209,7 +216,7 @@ pub fn SlotMapType(Index: type, Generation: type) type { }; } -const TestMap = SlotMapType(u24, u8); +const TestMap = SlotMapType(u24, u8, void); // TODO: Add more rigorous test for check if generation usage is nicely distributed.