diff --git a/build.zig b/build.zig index 8e2997b..096274c 100644 --- a/build.zig +++ b/build.zig @@ -8,6 +8,7 @@ pub fn build(b: *std.Build) !void { const assets_dir = "assets"; const asset_files = [_][]const u8{ "sokoban/sokoban_tilesheet.png", + "sokoban/sokoban_tilesheet@2.png", "roboto-font/Roboto-Regular.ttf", "tiled/map.tmx", "tiled/tileset.tsx", diff --git a/src/app.zig b/src/app.zig index b09f86c..f000ad3 100644 --- a/src/app.zig +++ b/src/app.zig @@ -188,15 +188,26 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { .ttf_data = plt.assets.readFile("roboto-font/Roboto-Regular.ttf") }); - const tilesheet_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet.png")); + const tilesheet1_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet.png")); + const tilesheet2_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet@2.png")); + assert(tilesheet2_png.width == tilesheet1_png.width*2); + assert(tilesheet2_png.height == tilesheet1_png.height*2); + + var levels = [_][*]u8{ tilesheet2_png.rgba8_pixels, tilesheet1_png.rgba8_pixels }; + const tilesheet = try Tilesheet.init( plt.arena, - Gfx.ImageData{ - .width = tilesheet_png.width, - .height = tilesheet_png.height, - .pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels } - }, - .init(64, 64) + .init(.{ + .width = tilesheet2_png.width, + .height = tilesheet2_png.height, + .format = .rgba8, + .levels = &levels, + }), + .init(128, 128), + .{ + .padding = 1, + .spritesheet = Gfx.initSpritesheet(.{ .mipmaps = levels.len }) + } ); var player_sprites: PlayerSprites = undefined; diff --git a/src/platform/audio.zig b/src/platform/audio.zig index b157ed4..1257f9c 100644 --- a/src/platform/audio.zig +++ b/src/platform/audio.zig @@ -492,12 +492,12 @@ pub fn showDebug() void { } if (ImGUI.beginTabItem("Buses")) { defer ImGUI.endTabItem(); - if(ImGUI.beginTable("buses", 3, 0)) { + if(ImGUI.beginTable("buses", 3, .{})) { defer ImGUI.endTable(); - ImGUI.tableSetupColumn("ID (Label)", 0); - ImGUI.tableSetupColumn("Volume", 0); - ImGUI.tableSetupColumn("Multiplied volume", 0); + ImGUI.tableSetupColumn("ID (Label)", .{}); + ImGUI.tableSetupColumn("Volume", .{}); + ImGUI.tableSetupColumn("Multiplied volume", .{}); ImGUI.tableHeadersRow(); for (0.., buses.items) |i, *bus| { diff --git a/src/platform/graphics.zig b/src/platform/graphics.zig index 2d66527..3951463 100644 --- a/src/platform/graphics.zig +++ b/src/platform/graphics.zig @@ -193,11 +193,12 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { self.default_sprite = initSprite(.{ .padding = 1 }); setSprite(self.default_sprite, .{ - .image = ImageData{ + .mipchain = .initSingle(Image{ .width = width, .height = height, - .pixels = .{ .rgba8 = &pixels } - } + .format = .rgba8, + .pixels = &pixels, + }) }); } @@ -227,11 +228,12 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { self.error_sprite = initSprite(.{ .padding = 1 }); setSprite(self.error_sprite, .{ - .image = .{ + .mipchain = .initSingle(.{ .width = width, .height = height, - .pixels = .{ .rgba8 = &pixels } - } + .format = .rgba8, + .pixels = &pixels, + }) }); } } @@ -242,9 +244,7 @@ pub fn deinit() void { 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); - } + sprite.arena.deinit(); } sg.shutdown(); @@ -337,11 +337,11 @@ pub fn showDebug() void { if (ImGUI.beginTabItem("Spritesheets")) { defer ImGUI.endTabItem(); - if(ImGUI.beginTable("spritesheets", 2, 0)) { + if(ImGUI.beginTable("spritesheets", 2, .{})) { defer ImGUI.endTable(); - ImGUI.tableSetupColumn("ID", 0); - ImGUI.tableSetupColumn("Size", 0); + ImGUI.tableSetupColumn("ID", .{}); + ImGUI.tableSetupColumn("Size", .{}); ImGUI.tableHeadersRow(); var spritesheet_iter = self.spritesheets.iterator(); @@ -365,11 +365,11 @@ pub fn showDebug() void { if (ImGUI.beginTabItem("Sprites")) { defer ImGUI.endTabItem(); - if(ImGUI.beginTable("sprites", 2, 0)) { + if(ImGUI.beginTable("sprites", 2, .{})) { defer ImGUI.endTable(); - ImGUI.tableSetupColumn("ID", 0); - ImGUI.tableSetupColumn("Position", 0); + ImGUI.tableSetupColumn("ID", .{}); + ImGUI.tableSetupColumn("Position", .{}); ImGUI.tableHeadersRow(); var sprite_iter = self.sprites.iterator(); @@ -649,7 +649,7 @@ pub fn deinitTexture(id: Texture.Id) void { } const SetTextureOptions = struct { - data: ImageData, + mipchain: Mipchain, premultiplied: bool = false }; @@ -660,34 +660,27 @@ pub fn setTexture(id: Texture.Id, opts: SetTextureOptions) void { return; }; - const texture_data = opts.data; - const pixel_count = texture_data.width*texture_data.height; - - var image_data: sg.ImageData = .{}; - var pixel_format: sg.PixelFormat = .DEFAULT; - - if (texture_data.pixels) |pixels| { - switch (pixels) { - .rgba8 => |rgba8| { - image_data.mip_levels[0] = sg.asRange(rgba8[0..(pixel_count*4)]); - pixel_format = .RGBA8; - }, - .r8 => |r8| { - image_data.mip_levels[0] = sg.asRange(r8[0..pixel_count]); - pixel_format = .R8; - } - } - } - // TODO: Apply premultiply, will need scratch buffer assert(opts.premultiplied); + var mipchain = opts.mipchain; + + var image_data: sg.ImageData = .{}; + assert(mipchain.length() <= image_data.mip_levels.len); + for (0..mipchain.length()) |index| { + const image = mipchain.getLevel(@intCast(index)); + image_data.mip_levels[index] = sg.asRange(image.pixelsSlice()); + } + var new_image_desc = sg.ImageDesc{ .type = ._2D, - .width = @intCast(texture_data.width), - .height = @intCast(texture_data.height), - .num_mipmaps = 1, - .pixel_format = pixel_format, + .width = @intCast(mipchain.width), + .height = @intCast(mipchain.height), + .num_mipmaps = @intCast(@max(1, mipchain.length())), + .pixel_format = switch (mipchain.format) { + .rgba8 => .RGBA8, + .r8 => .R8, + } }; if (texture.dynamic) { new_image_desc.usage.dynamic_update = true; @@ -841,7 +834,7 @@ pub fn draw(opts: DrawOptions) void { self.frame.quads.appendAssumeCapacity(quad); } -const SpriteOptions = struct { +pub const SpriteOptions = struct { spritesheet: ?Spritesheet.Id = null, padding: u32 = 0, }; @@ -855,10 +848,11 @@ pub fn initSprite(opts: SpriteOptions) Sprite.Id { const sprite = self.sprites.getAssumeExists(id); sprite.* = .{ + .arena = .init(self.gpa), .spritesheet = opts.spritesheet orelse self.default_spritesheet, .padding = opts.padding, - .data = null, - .position = null + .mipchain = null, + .position = null, }; return id; @@ -870,11 +864,8 @@ pub fn deinitSprite(id: Sprite.Id) void { return; } - if (self.sprites.exists(id)) { - const sprite = self.sprites.getAssumeExists(id); - if (sprite.data) |data| { - data.deinit(self.gpa); - } + if (self.sprites.get(id)) |sprite| { + sprite.arena.deinit(); self.sprites.removeAssumeExists(id); if (self.spritesheets.get(sprite.spritesheet)) |spritesheet| { @@ -884,7 +875,7 @@ pub fn deinitSprite(id: Sprite.Id) void { } const SetSpriteOptions = struct { - image: ImageData, + mipchain: Mipchain, rect: ?Rect = null, premultiplied: bool = false }; @@ -902,57 +893,62 @@ pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void { return; }; - var sprite_data: ImageData = .none; - errdefer sprite_data.deinit(self.gpa); - - if (opts.image.pixels) |pixels| { - const format = @as(ImageData.Format, pixels); - if (format != spritesheet.format) { - log.warn("Attempt to use image of format '{}' on a spritesheet with '{}' format", .{format, spritesheet.format}); - return; - } + if (opts.mipchain.format != spritesheet.format) { + log.warn("Attempt to use image of format '{}' on a spritesheet with '{}' format", .{opts.mipchain.format, spritesheet.format}); + return; + } + if (opts.mipchain.length() != spritesheet.mipmaps) { + log.warn("Attempt to use image with '{}' mipmaps on a spritesheet with '{}' mipmaps", .{opts.mipchain.length(), spritesheet.mipmaps}); + return; } - if (opts.rect) |rect| { - var format: ?ImageData.Format = null; - if (opts.image.pixels) |pixels| { - format = @as(ImageData.Format, pixels); - } + var next_mipchain: Mipchain = .none; + var next_arena = std.heap.ArenaAllocator.init(self.gpa); + errdefer next_arena.deinit(); - sprite_data = ImageData.init(self.gpa, @trunc(rect.size.x), @trunc(rect.size.y), format) catch |e| { + if (opts.rect) |rect| { + const image_opts = Image.InitOptions{ + .width = @trunc(rect.size.x), + .height = @trunc(rect.size.y), + .format = opts.mipchain.format + }; + next_mipchain = Mipchain.initAlloc(next_arena.allocator(), image_opts, opts.mipchain.length()) catch |e| { log.warn("Failed to create texture data: {}", .{e}); return; }; - sprite_data.copyFrom( - opts.image, + next_mipchain.copyFromRect( + opts.mipchain, 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}); + next_mipchain = opts.mipchain.clone(next_arena.allocator()) catch |e| { + log.warn("Failed to clone mipchain: {}", .{e}); return; }; } if (!opts.premultiplied) { - sprite_data.premultiplyAlpha(); + for (0..next_mipchain.length()) |index| { + next_mipchain.getLevel(index).premultiplyAlpha(); + } } - var old_size: ?Vec2 = null; - if (sprite.data) |old_sprite_data| { - old_size = Vec2.initFromInt(u32, old_sprite_data.width, old_sprite_data.height); - old_sprite_data.deinit(self.gpa); + var prev_size: ?Vec2 = null; + if (sprite.mipchain) |prev_mipchain| { + prev_size = Vec2.initFromInt(u32, prev_mipchain.width, prev_mipchain.height); } - sprite.data = sprite_data; + sprite.arena.deinit(); + sprite.mipchain = next_mipchain; + sprite.arena = next_arena; - const new_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height); + const new_size = Vec2.initFromInt(u32, next_mipchain.width, next_mipchain.height); log.debug("set sprite on {f}", .{sprite.spritesheet}); - if (old_size == null or !old_size.?.eql(new_size) or sprite.position == null) { + if (prev_size == null or !prev_size.?.eql(new_size) or sprite.position == null) { sprite.position = null; spritesheet.needs_repack = true; } @@ -984,7 +980,7 @@ fn repackSpritesheet(id: Spritesheet.Id) void { var sprite_iter = self.sprites.iterator(); while (sprite_iter.next()) |sprite_id| { const sprite = self.sprites.getAssumeExists(sprite_id); - if (sprite.data != null and sprite.spritesheet == id) { + if (sprite.mipchain != null and sprite.spritesheet == id) { sprites.appendAssumeCapacity(sprite); } } @@ -992,10 +988,11 @@ fn repackSpritesheet(id: Spritesheet.Id) void { var rects: [Sprite.max_sprites]STBRectPack.Rect = undefined; @memset(&rects, .{}); for (0.., sprites.items) |i, sprite| { - const sprite_data = sprite.data.?; + const sprite_mipchain = sprite.mipchain.?; + const padding = sprite.padding << @intCast(sprite_mipchain.length()-1); rects[i] = .{ - .w = @intCast(sprite_data.width + sprite.padding * 2), - .h = @intCast(sprite_data.height + sprite.padding * 2), + .w = @intCast(sprite_mipchain.width + padding * 2), + .h = @intCast(sprite_mipchain.height + padding * 2), }; } @@ -1012,8 +1009,9 @@ fn repackSpritesheet(id: Spritesheet.Id) void { for (0.., sprites.items) |i, sprite| { assert(rects[i].was_packed == 1); - const x: u32 = @as(u32, @intCast(rects[i].x)) + sprite.padding; - const y: u32 = @as(u32, @intCast(rects[i].y)) + sprite.padding; + const padding = sprite.padding << @intCast(sprite.mipchain.?.length()-1); + const x: u32 = @as(u32, @intCast(rects[i].x)) + padding; + const y: u32 = @as(u32, @intCast(rects[i].y)) + padding; sprite.position = .initFromInt(u32, x, y); } @@ -1053,16 +1051,20 @@ fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void { log.debug("[{}] Rebuild spritesheet texture: {f}", .{self.frame.index, id}); - const image_data = ImageData.init( - self.gpa, - @intFromFloat(spritesheet.size.x), - @intFromFloat(spritesheet.size.y), - spritesheet.format + var mipchain_arena = std.heap.ArenaAllocator.init(self.gpa); + defer mipchain_arena.deinit(); + const mipchain = Mipchain.initAlloc( + mipchain_arena.allocator(), + .{ + .width = @intFromFloat(spritesheet.size.x), + .height = @intFromFloat(spritesheet.size.y), + .format = spritesheet.format + }, + spritesheet.mipmaps ) catch |e| { - log.err("ImageData allocation failed: {}", .{e}); + log.err("Mipchain allocation failed: {}", .{e}); return; }; - defer image_data.deinit(self.gpa); var sprite_iter = self.sprites.iterator(); while (sprite_iter.next()) |sprite_id| { @@ -1071,60 +1073,64 @@ fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void { continue; } - const sprite_data = sprite.data orelse continue; + const sprite_mipchain = sprite.mipchain orelse continue; const sprite_position = sprite.position orelse continue; - const x: u32 = @intFromFloat(sprite_position.x); - const y: u32 = @intFromFloat(sprite_position.y); - image_data.setRectZero( - x - sprite.padding, - y - sprite.padding, - sprite_data.width + sprite.padding * 2, - sprite_data.height + sprite.padding * 2 - ); - image_data.copyFrom( - sprite_data, - x, - y, - 0, - 0, - sprite_data.width, - sprite_data.height - ); - if (sprite.padding > 0) { - for (0..sprite_data.width) |ox| { - const pixel = image_data.getPixel(x+ox, y); - for (0..sprite.padding) |i| { - @memcpy(image_data.getPixel(x+ox, y-(i+1)), pixel); - } - } + assert(sprite_mipchain.length() == spritesheet.mipmaps); - for (0..sprite_data.width) |ox| { - const pixel = image_data.getPixel(x+ox, y+sprite_data.height-1); - for (0..sprite.padding) |i| { - @memcpy(image_data.getPixel(x+ox, y+sprite_data.height+i), pixel); - } - } + for (0..spritesheet.mipmaps) |index| { + const level = mipchain.getLevel(index); - for (0..sprite_data.height) |oy| { - const pixel = image_data.getPixel(x, y+oy); - for (0..sprite.padding) |i| { - @memcpy(image_data.getPixel(x-(i+1), y+oy), pixel); - } - } + const sprite_level = sprite_mipchain.getLevel(index); + const padding = sprite.padding << @intCast(sprite_mipchain.length()-1 - index); + var x: u32 = @intFromFloat(sprite_position.x); + x = x >> @intCast(index); + var y: u32 = @intFromFloat(sprite_position.y); + y = y >> @intCast(index); - for (0..sprite_data.height) |oy| { - const pixel = image_data.getPixel(x+sprite_data.width-1, y+oy); - for (0..sprite.padding) |i| { - @memcpy(image_data.getPixel(x+sprite_data.width+i, y+oy), pixel); - } - } + level.setRectZero( + x - padding, + y - padding, + sprite_level.width + padding * 2, + sprite_level.height + padding * 2 + ); + level.copyFrom(sprite_level, x, y); - // TODO: Add padding to corners. + if (padding > 0) { + for (0..sprite_level.width) |ox| { + const pixel = level.getPixel(x+ox, y); + for (0..padding) |i| { + @memcpy(level.getPixel(x+ox, y-(i+1)), pixel); + } + } + + for (0..sprite_level.width) |ox| { + const pixel = level.getPixel(x+ox, y+sprite_level.height-1); + for (0..padding) |i| { + @memcpy(level.getPixel(x+ox, y+sprite_level.height+i), pixel); + } + } + + for (0..sprite_level.height) |oy| { + const pixel = level.getPixel(x, y+oy); + for (0..padding) |i| { + @memcpy(level.getPixel(x-(i+1), y+oy), pixel); + } + } + + for (0..sprite_level.height) |oy| { + const pixel = level.getPixel(x+sprite_level.width-1, y+oy); + for (0..padding) |i| { + @memcpy(level.getPixel(x+sprite_level.width+i, y+oy), pixel); + } + } + + // TODO: Add padding to corners. + } } } setTexture(spritesheet.texture, .{ - .data = image_data, + .mipchain = mipchain, .premultiplied = true }); @@ -1137,20 +1143,23 @@ fn ensureSpritePacked(id: Sprite.Id) bool { if (sprite.position == null) { const spritesheet = self.spritesheets.get(sprite.spritesheet) orelse return false; - const sprite_data = sprite.data orelse return false; + const sprite_mipchain = sprite.mipchain orelse return false; if (spritesheet.packer) |*packer| { + assert(sprite_mipchain.length() > 0); + const padding = sprite.padding << @intCast(sprite_mipchain.length()-1); + var rects: [1]STBRectPack.Rect = .{ .{ - .w = @intCast(sprite_data.width + sprite.padding * 2), - .h = @intCast(sprite_data.height + sprite.padding * 2), + .w = @intCast(sprite_mipchain.width + padding * 2), + .h = @intCast(sprite_mipchain.height + padding * 2), } }; if (packer.packRects(&rects)) { // Suboptimal sprite packing, will repack at end of frame assert(rects[0].was_packed == 1); - const x: u32 = @as(u32, @intCast(rects[0].x)) + sprite.padding; - const y: u32 = @as(u32, @intCast(rects[0].y)) + sprite.padding; + const x: u32 = @as(u32, @intCast(rects[0].x)) + padding; + const y: u32 = @as(u32, @intCast(rects[0].y)) + padding; sprite.position = .initFromInt(u32, x, y); spritesheet.needs_repack = true; } else { @@ -1167,7 +1176,7 @@ fn ensureSpritePacked(id: Sprite.Id) bool { pub fn getSpriteUVRect(id: Sprite.Id) ?Rect { const self = &g_state; const sprite = self.sprites.get(id) orelse return null; - const sprite_data = sprite.data orelse return null; + const sprite_mipchain = sprite.mipchain orelse return null; const spritesheet = self.spritesheets.get(sprite.spritesheet) orelse return null; @@ -1177,7 +1186,7 @@ pub fn getSpriteUVRect(id: Sprite.Id) ?Rect { assert(sprite.position != null); - const sprite_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height); + const sprite_size = Vec2.initFromInt(u32, sprite_mipchain.width, sprite_mipchain.height); return Rect{ .pos = sprite.position.?.divide(spritesheet.size), .size = sprite_size.divide(spritesheet.size), @@ -1215,7 +1224,8 @@ pub fn drawSprite(id: Sprite.Id, pos: Vec2, size: Vec2, color: Color) void { } pub const SpriteSheetOptions = struct { - format: ImageData.Format = .rgba8 + mipmaps: u32 = 1, + format: Image.Format = .rgba8 }; pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id { @@ -1225,11 +1235,15 @@ pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id { return .nil; }; + assert(opts.mipmaps <= sg.max_mipmaps); + assert(opts.mipmaps > 0); + const spritesheet = self.spritesheets.getAssumeExists(id); spritesheet.* = .{ .texture = initTexture(.{ .dynamic = true }), .size = .init(0, 0), .format = opts.format, + .mipmaps = opts.mipmaps, .needs_repack = false, .needs_texture_rebuild = false, .nodes = undefined, @@ -1247,105 +1261,86 @@ pub fn deinitSpritesheet(id: Spritesheet.Id) void { } } -pub const ImageData = struct { - const Format = enum { - rgba8, - r8 - }; - - const Pixels = union(Format) { - rgba8: [*]u8, - r8: [*]u8, - }; - - pixels: ?Pixels, +pub const Image = struct { + format: Image.Format, + pixels: [*]u8, width: u32, height: u32, - pub const none = ImageData{ - .pixels = null, + pub const Format = enum { + rgba8, + r8, + + pub fn pixelSize(self: Image.Format) usize { + return switch(self) { + .rgba8 => 4, + .r8 => 1, + }; + } + + pub fn maxPixelSize() usize { + var result: usize = 0; + for (@typeInfo(Image.Format).@"enum".fields) |field| { + const pixel_size = Image.Format.pixelSize(@enumFromInt(field.value)); + result = @max(result, pixel_size); + } + return result; + } + }; + + pub const none = Image{ + .format = .rgba8, + .pixels = &.{}, .width = 0, .height = 0, }; - pub fn init(gpa: Allocator, width: u32, height: u32, format: ?Format) !ImageData { - const pixel_count = width * height; - var pixels: ?Pixels = null; - if (format != null) { - pixels = switch (format.?) { - .rgba8 => Pixels{ - .rgba8 = (try gpa.alloc(u8, pixel_count * 4)).ptr - }, - .r8 => Pixels{ - .r8 = (try gpa.alloc(u8, pixel_count)).ptr - } - }; - } + const InitOptions = struct { + format: Image.Format, + width: u32, + height: u32, + }; - return ImageData{ - .pixels = pixels, - .width = width, - .height = height + pub fn initAlloc(gpa: Allocator, opts: InitOptions) !Image { + const pixel_count = opts.width * opts.height; + const pixel_size = opts.format.pixelSize(); + + return Image{ + .format = opts.format, + .pixels = (try gpa.alloc(u8, pixel_count * pixel_size)).ptr, + .width = opts.width, + .height = opts.height }; } - pub fn initRGBA8(gpa: Allocator, width: u32, height: u32) !ImageData { - return ImageData.init(gpa, width, height, .rgba8); + fn pixelCount(self: Image) usize { + return self.width * self.height; } - pub fn initR8(gpa: Allocator, width: u32, height: u32) !ImageData { - return ImageData.init(gpa, width, height, .r8); + fn pixelsSlice(self: Image) []u8 { + return self.pixels[0..(self.pixelCount() * self.format.pixelSize())]; } - pub fn clone(self: ImageData, gpa: Allocator) !ImageData { - const pixel_count = self.width * self.height; - - var pixels: ?Pixels = null; - if (self.pixels) |self_pixels| { - pixels = switch (self_pixels) { - .rgba8 => |rgba8| Pixels{ - .rgba8 = (try gpa.dupe(u8, rgba8[0..(pixel_count * 4)])).ptr - }, - .r8 => |r8| Pixels{ - .r8 = (try gpa.dupe(u8, r8[0..pixel_count])).ptr - } - }; - } - - return ImageData{ - .pixels = pixels, + pub fn clone(self: Image, arena: Allocator) !Image { + return Image{ + .pixels = (try arena.dupe(u8, self.pixelsSlice())).ptr, + .format = self.format, .width = self.width, .height = self.height }; } - pub fn deinit(self: ImageData, gpa: Allocator) void { - const pixel_count = self.width * self.height; - if (self.pixels) |pixels| { - switch (pixels) { - .rgba8 => |rgba8| gpa.free(rgba8[0..(pixel_count*4)]), - .r8 => |r8| gpa.free(r8[0..pixel_count]) - } - } + pub fn deinit(self: Image, gpa: Allocator) void { + gpa.free(self.pixelsSlice()); } - pub fn getPixel(self: ImageData, x: usize, y: usize) []u8 { + pub fn getPixel(self: Image, x: usize, y: usize) []u8 { const pixel_index = y * self.width + x; - if (self.pixels) |pixels| { - return switch (pixels) { - .rgba8 => |rgba8| rgba8[(4*pixel_index)..][0..4], - .r8 => |r8| r8[pixel_index..][0..1] - }; - } else { - return &.{}; - } + const pixel_size = self.format.pixelSize(); + return self.pixels[(pixel_size*pixel_index)..][0..pixel_size]; } - pub fn setRect(self: ImageData, x: u32, y: u32, width: u32, height: u32, pixel: []const u8) void { - if (self.pixels == null) { - return; - } - + pub fn setRect(self: Image, x: u32, y: u32, width: u32, height: u32, pixel: []const u8) void { for (0..height) |oy| { const iy = y + oy; if (iy >= self.height) { @@ -1364,27 +1359,19 @@ pub const ImageData = struct { } } - pub fn setRectZero(self: ImageData, x: u32, y: u32, width: u32, height: u32) void { - if (self.pixels == null) { - return; - } - - const pixel = switch (self.pixels.?) { - .rgba8 => &[_]u8{ 0, 0, 0, 0 }, - .r8 => &[_]u8{ 0 } - }; - self.setRect(x, y, width, height, pixel); + pub fn setRectZero(self: Image, x: u32, y: u32, width: u32, height: u32) void { + const zero_pixel_buff: [Image.Format.maxPixelSize()]u8 = @splat(0); + const zero_pixel = zero_pixel_buff[0..self.format.pixelSize()]; + self.setRect(x, y, width, height, zero_pixel); } - pub fn premultiplyAlpha(self: ImageData) void { - const pixels = self.pixels orelse return; - if (pixels != .rgba8) return; - const rgba8_pixels = pixels.rgba8; + pub fn premultiplyAlpha(self: Image) void { + if (self.format != .rgba8) return; for (0..self.height) |y| { for (0..self.width) |x| { const pixel_index = y * self.width + x; - const rgba8_pixel = rgba8_pixels[(4*pixel_index)..][0..4]; + const rgba8_pixel = self.pixels[(4*pixel_index)..][0..4]; inline for (0..3) |i| { rgba8_pixel[i] = @intCast(@as(u16, rgba8_pixel[i]) * rgba8_pixel[3] / 255); @@ -1393,9 +1380,9 @@ pub const ImageData = struct { } } - pub fn copyFrom( - self: ImageData, - source: ImageData, + pub fn copyFromRect( + self: Image, + source: Image, cx: u32, cy: u32, source_cx: u32, @@ -1403,9 +1390,7 @@ pub const ImageData = struct { source_width: u32, source_height: u32, ) void { - if (source.pixels == null or self.pixels == null) { - return; - } + assert(source.format == self.format); for (0..source_height) |oy| { const source_y = source_cy+oy; @@ -1434,6 +1419,170 @@ pub const ImageData = struct { } } } + + pub fn copyFrom( + self: Image, + source: Image, + cx: u32, + cy: u32, + ) void { + self.copyFromRect(source, cx, cy, 0, 0, source.width, source.height); + } +}; + +pub const Mipchain = struct { + format: Image.Format, + level0: [*]u8, // level0 is stored inline, so that we could cast a `Image` into a `Mipchain` without allocations + next_levels: [][*]u8, // Each level after `level0` must be half the size + width: u32, + height: u32, + + pub const none = Mipchain{ + .format = .rgba8, + .level0 = &.{}, + .next_levels = &.{}, + .width = 0, + .height = 0, + }; + + pub const InitOptions = struct { + format: Image.Format, + levels: [][*]u8, + width: u32, + height: u32, + }; + + pub fn init(opts: InitOptions) Mipchain { + var mipchain: Mipchain = .{ + .format = opts.format, + .level0 = &.{}, + .next_levels = &.{}, + .width = opts.width, + .height = opts.height, + }; + if (opts.levels.len > 0) { + mipchain.level0 = opts.levels[0]; + mipchain.next_levels = opts.levels[1..]; + } + return mipchain; + } + + pub fn initSingle(image: Image) Mipchain { + return Mipchain{ + .format = image.format, + .level0 = image.pixels, + .next_levels = &.{}, + .width = image.width, + .height = image.height, + }; + } + + pub fn clone(self: Mipchain, arena: Allocator) !Mipchain { + var mipchain = Mipchain{ + .format = self.format, + .width = self.width, + .height = self.height, + .level0 = &.{}, + .next_levels = &.{} + }; + + if (self.length() >= 2) { + mipchain.next_levels = try arena.alloc([*]u8, self.length()-1); + } + + for (0..mipchain.length()) |index| { + var level = self.getLevel(index); + const pixels = try arena.dupe(u8, level.pixelsSlice()); + mipchain.getLevelPixelsPtr(index).* = pixels.ptr; + } + + return mipchain; + } + + pub fn initAlloc(arena: Allocator, image_opts: Image.InitOptions, levels: usize) !Mipchain { + var mipchain = Mipchain{ + .width = image_opts.width, + .height = image_opts.height, + .format = image_opts.format, + .level0 = &.{}, + .next_levels = &.{} + }; + + if (levels >= 2) { + mipchain.next_levels = try arena.alloc([*]u8, levels-1); + } + + for (0..mipchain.length()) |index| { + var level = mipchain.getLevel(index); + const pixels = try arena.alloc(u8, level.pixelCount() * level.format.pixelSize()); + mipchain.getLevelPixelsPtr(index).* = pixels.ptr; + } + + assert(mipchain.length() == levels); + + return mipchain; + } + + pub fn length(self: Mipchain) usize { + if (self.width == 0 or self.height == 0) { + return 0; + } + + return 1 + self.next_levels.len; + } + + pub fn getLevelPixels(self: Mipchain, index: usize) [*]u8 { + if (index == 0) { + return self.level0; + } else { + return self.next_levels[index-1]; + } + } + + // TODO: Don't like that this is redundant with `getLevelPixels` + pub fn getLevelPixelsPtr(self: *Mipchain, index: usize) *[*]u8 { + if (index == 0) { + return &self.level0; + } else { + return &self.next_levels[index-1]; + } + } + + pub fn getLevel(self: Mipchain, index: usize) Image { + return Image{ + .format = self.format, + .pixels = self.getLevelPixels(index), + .width = @max(self.width >> @intCast(index), 1), + .height = @max(self.height >> @intCast(index), 1), + }; + } + + pub fn copyFromRect( + self: Mipchain, + source: Mipchain, + cx: u32, + cy: u32, + source_cx: u32, + source_cy: u32, + source_width: u32, + source_height: u32, + ) void { + assert(self.length() == source.length()); + + for (0..self.length()) |index| { + const self_level = self.getLevel(index); + const source_level = source.getLevel(index); + self_level.copyFromRect( + source_level, + cx >> @intCast(index), + cy >> @intCast(index), + source_cx >> @intCast(index), + source_cy >> @intCast(index), + source_width >> @intCast(index), + source_height >> @intCast(index), + ); + } + } }; pub const Texture = struct { @@ -1448,8 +1597,9 @@ pub const Texture = struct { }; pub const Sprite = struct { + arena: std.heap.ArenaAllocator, spritesheet: Spritesheet.Id, - data: ?ImageData, + mipchain: ?Mipchain, position: ?Vec2, padding: u32, @@ -1460,8 +1610,9 @@ pub const Sprite = struct { pub const Spritesheet = struct { texture: Texture.Id, - format: ImageData.Format, + format: Image.Format, size: Vec2, + mipmaps: u32, needs_repack: bool, needs_texture_rebuild: bool, diff --git a/src/platform/imgui.zig b/src/platform/imgui.zig index 54ebc08..3e6e819 100644 --- a/src/platform/imgui.zig +++ b/src/platform/imgui.zig @@ -504,12 +504,24 @@ pub fn collapsingHeader(label: []const u8) bool { ); } -pub fn beginTable(id: [*c]const u8, columns: u32, flags: ig.ImGuiTableFlags) bool { +pub const TableFlags = struct { + sortable: bool = false, + + fn toInt(self: TableFlags) ig.ImGuiTableFlags { + var result: ig.ImGuiTableFlags = 0; + if (self.sortable) { + result |= ig.ImGuiTableFlags_Sortable; + } + return result; + } +}; + +pub fn beginTable(id: [*c]const u8, columns: u32, flags: TableFlags) bool { if (isDisabled()) { return false; } - return ig.igBeginTable(id, @intCast(columns), flags); + return ig.igBeginTable(id, @intCast(columns), flags.toInt()); } pub fn endTable() void { @@ -544,12 +556,22 @@ pub fn tableSetColumnIndex(index: usize) void { _ = ig.igTableSetColumnIndex(@intCast(index)); } -pub fn tableSetupColumn(label: [*c]const u8, flags: ig.ImGuiTableColumnFlags) void { +pub const TableColumnFlags = struct { + + fn toInt(self: TableColumnFlags) ig.ImGuiTableColumnFlags { + _ = self; // autofix + const result: ig.ImGuiTableColumnFlags = 0; + + return result; + } +}; + +pub fn tableSetupColumn(label: [*c]const u8, flags: TableColumnFlags) void { if (isDisabled()) { return; } - ig.igTableSetupColumn(label, flags); + ig.igTableSetupColumn(label, flags.toInt()); } pub fn tableHeadersRow() void { diff --git a/src/platform/root.zig b/src/platform/root.zig index 9ed0c5e..9ddf6e4 100644 --- a/src/platform/root.zig +++ b/src/platform/root.zig @@ -149,6 +149,11 @@ fn PlatformType(App: type) type { _ = ImGUI.beginTabBar("platform tab bar"); defer ImGUI.endTabBar(); + if (ImGUI.beginTabItem("General")) { + defer ImGUI.endTabItem(); + ImGUI.text("Embedded assets: {B}", .{self.assets.totalSize()}); + } + if (ImGUI.beginTabItem("Graphics")) { defer ImGUI.endTabItem(); Gfx.showDebug(); @@ -543,6 +548,14 @@ pub const Assets = struct { } } + pub fn totalSize(self: *Assets) usize { + var total: usize = 0; + for (self.files.values()) |file| { + total += file.contents.len; + } + return total; + } + fn insertStaticFile(self: *Assets, filename: []const u8, contents: [:0]const u8) !void { try self.files.put(self.gpa, filename, File{ .contents = contents diff --git a/src/tilesheet.zig b/src/tilesheet.zig index a7d02bd..138abe2 100644 --- a/src/tilesheet.zig +++ b/src/tilesheet.zig @@ -9,25 +9,27 @@ const Rect = Platform.Rect; const Tilesheet = @This(); -image: Gfx.ImageData, +sprite_options: Gfx.SpriteOptions, +mipchain: Gfx.Mipchain, sprites: []Gfx.Sprite.Id, tile_size: Vec2, width: u32, height: u32, -pub fn init(gpa: Allocator, image: Gfx.ImageData, tile_size: Vec2) !Tilesheet { - const width = @divFloor(image.width, @as(u32, @intFromFloat(tile_size.x))); - const height = @divFloor(image.height, @as(u32, @intFromFloat(tile_size.y))); +pub fn init(gpa: Allocator, mipchain: Gfx.Mipchain, tile_size: Vec2, sprite_options: Gfx.SpriteOptions) !Tilesheet { + const width = @divFloor(mipchain.width, @as(u32, @intFromFloat(tile_size.x))); + const height = @divFloor(mipchain.height, @as(u32, @intFromFloat(tile_size.y))); const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height); errdefer gpa.free(sprites); @memset(sprites, .nil); return Tilesheet{ .sprites = sprites, - .image = image, + .mipchain = mipchain, .width = width, .height = height, - .tile_size = tile_size + .tile_size = tile_size, + .sprite_options = sprite_options }; } @@ -45,9 +47,9 @@ pub fn get(self: Tilesheet, x: usize, y: usize) Gfx.Sprite.Id { const sprite_index = y * self.width + x; if (self.sprites[sprite_index] == Gfx.Sprite.Id.nil) { - const sprite = Gfx.initSprite(.{ .padding = 1 }); + const sprite = Gfx.initSprite(self.sprite_options); Gfx.setSprite(sprite, .{ - .image = self.image, + .mipchain = self.mipchain, .rect = Rect.init(@floatFromInt(x), @floatFromInt(y), 1, 1).multiply(self.tile_size) }); self.sprites[sprite_index] = sprite;