implement mipmaps support on spritesheets

This commit is contained in:
Rokas Puzonas 2026-09-17 00:58:33 +03:00
parent f4ca77194e
commit ddb012d3df
7 changed files with 461 additions and 261 deletions

View File

@ -8,6 +8,7 @@ pub fn build(b: *std.Build) !void {
const assets_dir = "assets"; const assets_dir = "assets";
const asset_files = [_][]const u8{ const asset_files = [_][]const u8{
"sokoban/sokoban_tilesheet.png", "sokoban/sokoban_tilesheet.png",
"sokoban/sokoban_tilesheet@2.png",
"roboto-font/Roboto-Regular.ttf", "roboto-font/Roboto-Regular.ttf",
"tiled/map.tmx", "tiled/map.tmx",
"tiled/tileset.tsx", "tiled/tileset.tsx",

View File

@ -188,15 +188,26 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
.ttf_data = plt.assets.readFile("roboto-font/Roboto-Regular.ttf") .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( const tilesheet = try Tilesheet.init(
plt.arena, plt.arena,
Gfx.ImageData{ .init(.{
.width = tilesheet_png.width, .width = tilesheet2_png.width,
.height = tilesheet_png.height, .height = tilesheet2_png.height,
.pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels } .format = .rgba8,
}, .levels = &levels,
.init(64, 64) }),
.init(128, 128),
.{
.padding = 1,
.spritesheet = Gfx.initSpritesheet(.{ .mipmaps = levels.len })
}
); );
var player_sprites: PlayerSprites = undefined; var player_sprites: PlayerSprites = undefined;

View File

@ -492,12 +492,12 @@ pub fn showDebug() void {
} }
if (ImGUI.beginTabItem("Buses")) { if (ImGUI.beginTabItem("Buses")) {
defer ImGUI.endTabItem(); defer ImGUI.endTabItem();
if(ImGUI.beginTable("buses", 3, 0)) { if(ImGUI.beginTable("buses", 3, .{})) {
defer ImGUI.endTable(); defer ImGUI.endTable();
ImGUI.tableSetupColumn("ID (Label)", 0); ImGUI.tableSetupColumn("ID (Label)", .{});
ImGUI.tableSetupColumn("Volume", 0); ImGUI.tableSetupColumn("Volume", .{});
ImGUI.tableSetupColumn("Multiplied volume", 0); ImGUI.tableSetupColumn("Multiplied volume", .{});
ImGUI.tableHeadersRow(); ImGUI.tableHeadersRow();
for (0.., buses.items) |i, *bus| { for (0.., buses.items) |i, *bus| {

View File

@ -193,11 +193,12 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
self.default_sprite = initSprite(.{ .padding = 1 }); self.default_sprite = initSprite(.{ .padding = 1 });
setSprite(self.default_sprite, .{ setSprite(self.default_sprite, .{
.image = ImageData{ .mipchain = .initSingle(Image{
.width = width, .width = width,
.height = height, .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 }); self.error_sprite = initSprite(.{ .padding = 1 });
setSprite(self.error_sprite, .{ setSprite(self.error_sprite, .{
.image = .{ .mipchain = .initSingle(.{
.width = width, .width = width,
.height = height, .height = height,
.pixels = .{ .rgba8 = &pixels } .format = .rgba8,
} .pixels = &pixels,
})
}); });
} }
} }
@ -242,9 +244,7 @@ pub fn deinit() void {
var sprite_iter = self.sprites.iterator(); var sprite_iter = self.sprites.iterator();
while (sprite_iter.next()) |sprite_id| { while (sprite_iter.next()) |sprite_id| {
const sprite = self.sprites.getAssumeExists(sprite_id); const sprite = self.sprites.getAssumeExists(sprite_id);
if (sprite.data) |data| { sprite.arena.deinit();
data.deinit(self.gpa);
}
} }
sg.shutdown(); sg.shutdown();
@ -337,11 +337,11 @@ pub fn showDebug() void {
if (ImGUI.beginTabItem("Spritesheets")) { if (ImGUI.beginTabItem("Spritesheets")) {
defer ImGUI.endTabItem(); defer ImGUI.endTabItem();
if(ImGUI.beginTable("spritesheets", 2, 0)) { if(ImGUI.beginTable("spritesheets", 2, .{})) {
defer ImGUI.endTable(); defer ImGUI.endTable();
ImGUI.tableSetupColumn("ID", 0); ImGUI.tableSetupColumn("ID", .{});
ImGUI.tableSetupColumn("Size", 0); ImGUI.tableSetupColumn("Size", .{});
ImGUI.tableHeadersRow(); ImGUI.tableHeadersRow();
var spritesheet_iter = self.spritesheets.iterator(); var spritesheet_iter = self.spritesheets.iterator();
@ -365,11 +365,11 @@ pub fn showDebug() void {
if (ImGUI.beginTabItem("Sprites")) { if (ImGUI.beginTabItem("Sprites")) {
defer ImGUI.endTabItem(); defer ImGUI.endTabItem();
if(ImGUI.beginTable("sprites", 2, 0)) { if(ImGUI.beginTable("sprites", 2, .{})) {
defer ImGUI.endTable(); defer ImGUI.endTable();
ImGUI.tableSetupColumn("ID", 0); ImGUI.tableSetupColumn("ID", .{});
ImGUI.tableSetupColumn("Position", 0); ImGUI.tableSetupColumn("Position", .{});
ImGUI.tableHeadersRow(); ImGUI.tableHeadersRow();
var sprite_iter = self.sprites.iterator(); var sprite_iter = self.sprites.iterator();
@ -649,7 +649,7 @@ pub fn deinitTexture(id: Texture.Id) void {
} }
const SetTextureOptions = struct { const SetTextureOptions = struct {
data: ImageData, mipchain: Mipchain,
premultiplied: bool = false premultiplied: bool = false
}; };
@ -660,34 +660,27 @@ pub fn setTexture(id: Texture.Id, opts: SetTextureOptions) void {
return; 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 // TODO: Apply premultiply, will need scratch buffer
assert(opts.premultiplied); 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{ var new_image_desc = sg.ImageDesc{
.type = ._2D, .type = ._2D,
.width = @intCast(texture_data.width), .width = @intCast(mipchain.width),
.height = @intCast(texture_data.height), .height = @intCast(mipchain.height),
.num_mipmaps = 1, .num_mipmaps = @intCast(@max(1, mipchain.length())),
.pixel_format = pixel_format, .pixel_format = switch (mipchain.format) {
.rgba8 => .RGBA8,
.r8 => .R8,
}
}; };
if (texture.dynamic) { if (texture.dynamic) {
new_image_desc.usage.dynamic_update = true; new_image_desc.usage.dynamic_update = true;
@ -841,7 +834,7 @@ pub fn draw(opts: DrawOptions) void {
self.frame.quads.appendAssumeCapacity(quad); self.frame.quads.appendAssumeCapacity(quad);
} }
const SpriteOptions = struct { pub const SpriteOptions = struct {
spritesheet: ?Spritesheet.Id = null, spritesheet: ?Spritesheet.Id = null,
padding: u32 = 0, padding: u32 = 0,
}; };
@ -855,10 +848,11 @@ pub fn initSprite(opts: SpriteOptions) Sprite.Id {
const sprite = self.sprites.getAssumeExists(id); const sprite = self.sprites.getAssumeExists(id);
sprite.* = .{ sprite.* = .{
.arena = .init(self.gpa),
.spritesheet = opts.spritesheet orelse self.default_spritesheet, .spritesheet = opts.spritesheet orelse self.default_spritesheet,
.padding = opts.padding, .padding = opts.padding,
.data = null, .mipchain = null,
.position = null .position = null,
}; };
return id; return id;
@ -870,11 +864,8 @@ pub fn deinitSprite(id: Sprite.Id) void {
return; return;
} }
if (self.sprites.exists(id)) { if (self.sprites.get(id)) |sprite| {
const sprite = self.sprites.getAssumeExists(id); sprite.arena.deinit();
if (sprite.data) |data| {
data.deinit(self.gpa);
}
self.sprites.removeAssumeExists(id); self.sprites.removeAssumeExists(id);
if (self.spritesheets.get(sprite.spritesheet)) |spritesheet| { if (self.spritesheets.get(sprite.spritesheet)) |spritesheet| {
@ -884,7 +875,7 @@ pub fn deinitSprite(id: Sprite.Id) void {
} }
const SetSpriteOptions = struct { const SetSpriteOptions = struct {
image: ImageData, mipchain: Mipchain,
rect: ?Rect = null, rect: ?Rect = null,
premultiplied: bool = false premultiplied: bool = false
}; };
@ -902,57 +893,62 @@ pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void {
return; return;
}; };
var sprite_data: ImageData = .none; if (opts.mipchain.format != spritesheet.format) {
errdefer sprite_data.deinit(self.gpa); log.warn("Attempt to use image of format '{}' on a spritesheet with '{}' format", .{opts.mipchain.format, spritesheet.format});
return;
if (opts.image.pixels) |pixels| { }
const format = @as(ImageData.Format, pixels); if (opts.mipchain.length() != spritesheet.mipmaps) {
if (format != spritesheet.format) { log.warn("Attempt to use image with '{}' mipmaps on a spritesheet with '{}' mipmaps", .{opts.mipchain.length(), spritesheet.mipmaps});
log.warn("Attempt to use image of format '{}' on a spritesheet with '{}' format", .{format, spritesheet.format}); return;
return;
}
} }
if (opts.rect) |rect| { var next_mipchain: Mipchain = .none;
var format: ?ImageData.Format = null; var next_arena = std.heap.ArenaAllocator.init(self.gpa);
if (opts.image.pixels) |pixels| { errdefer next_arena.deinit();
format = @as(ImageData.Format, pixels);
}
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}); log.warn("Failed to create texture data: {}", .{e});
return; return;
}; };
sprite_data.copyFrom( next_mipchain.copyFromRect(
opts.image, opts.mipchain,
0, 0, 0, 0,
@trunc(rect.pos.x), @trunc(rect.pos.y), @trunc(rect.pos.x), @trunc(rect.pos.y),
@trunc(rect.size.x), @trunc(rect.size.y) @trunc(rect.size.x), @trunc(rect.size.y)
); );
} else { } else {
sprite_data = opts.image.clone(self.gpa) catch |e| { next_mipchain = opts.mipchain.clone(next_arena.allocator()) catch |e| {
log.warn("Failed to clone image: {}", .{e}); log.warn("Failed to clone mipchain: {}", .{e});
return; return;
}; };
} }
if (!opts.premultiplied) { if (!opts.premultiplied) {
sprite_data.premultiplyAlpha(); for (0..next_mipchain.length()) |index| {
next_mipchain.getLevel(index).premultiplyAlpha();
}
} }
var old_size: ?Vec2 = null; var prev_size: ?Vec2 = null;
if (sprite.data) |old_sprite_data| { if (sprite.mipchain) |prev_mipchain| {
old_size = Vec2.initFromInt(u32, old_sprite_data.width, old_sprite_data.height); prev_size = Vec2.initFromInt(u32, prev_mipchain.width, prev_mipchain.height);
old_sprite_data.deinit(self.gpa);
} }
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}); 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; sprite.position = null;
spritesheet.needs_repack = true; spritesheet.needs_repack = true;
} }
@ -984,7 +980,7 @@ fn repackSpritesheet(id: Spritesheet.Id) void {
var sprite_iter = self.sprites.iterator(); var sprite_iter = self.sprites.iterator();
while (sprite_iter.next()) |sprite_id| { while (sprite_iter.next()) |sprite_id| {
const sprite = self.sprites.getAssumeExists(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); sprites.appendAssumeCapacity(sprite);
} }
} }
@ -992,10 +988,11 @@ fn repackSpritesheet(id: Spritesheet.Id) void {
var rects: [Sprite.max_sprites]STBRectPack.Rect = undefined; var rects: [Sprite.max_sprites]STBRectPack.Rect = undefined;
@memset(&rects, .{}); @memset(&rects, .{});
for (0.., sprites.items) |i, sprite| { 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] = .{ rects[i] = .{
.w = @intCast(sprite_data.width + sprite.padding * 2), .w = @intCast(sprite_mipchain.width + padding * 2),
.h = @intCast(sprite_data.height + sprite.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| { for (0.., sprites.items) |i, sprite| {
assert(rects[i].was_packed == 1); assert(rects[i].was_packed == 1);
const x: u32 = @as(u32, @intCast(rects[i].x)) + sprite.padding; const padding = sprite.padding << @intCast(sprite.mipchain.?.length()-1);
const y: u32 = @as(u32, @intCast(rects[i].y)) + sprite.padding; 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); 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}); log.debug("[{}] Rebuild spritesheet texture: {f}", .{self.frame.index, id});
const image_data = ImageData.init( var mipchain_arena = std.heap.ArenaAllocator.init(self.gpa);
self.gpa, defer mipchain_arena.deinit();
@intFromFloat(spritesheet.size.x), const mipchain = Mipchain.initAlloc(
@intFromFloat(spritesheet.size.y), mipchain_arena.allocator(),
spritesheet.format .{
.width = @intFromFloat(spritesheet.size.x),
.height = @intFromFloat(spritesheet.size.y),
.format = spritesheet.format
},
spritesheet.mipmaps
) catch |e| { ) catch |e| {
log.err("ImageData allocation failed: {}", .{e}); log.err("Mipchain allocation failed: {}", .{e});
return; return;
}; };
defer image_data.deinit(self.gpa);
var sprite_iter = self.sprites.iterator(); var sprite_iter = self.sprites.iterator();
while (sprite_iter.next()) |sprite_id| { while (sprite_iter.next()) |sprite_id| {
@ -1071,60 +1073,64 @@ fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void {
continue; continue;
} }
const sprite_data = sprite.data orelse continue; const sprite_mipchain = sprite.mipchain orelse continue;
const sprite_position = sprite.position orelse continue; const sprite_position = sprite.position orelse continue;
const x: u32 = @intFromFloat(sprite_position.x); assert(sprite_mipchain.length() == spritesheet.mipmaps);
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);
}
}
for (0..sprite_data.width) |ox| { for (0..spritesheet.mipmaps) |index| {
const pixel = image_data.getPixel(x+ox, y+sprite_data.height-1); const level = mipchain.getLevel(index);
for (0..sprite.padding) |i| {
@memcpy(image_data.getPixel(x+ox, y+sprite_data.height+i), pixel);
}
}
for (0..sprite_data.height) |oy| { const sprite_level = sprite_mipchain.getLevel(index);
const pixel = image_data.getPixel(x, y+oy); const padding = sprite.padding << @intCast(sprite_mipchain.length()-1 - index);
for (0..sprite.padding) |i| { var x: u32 = @intFromFloat(sprite_position.x);
@memcpy(image_data.getPixel(x-(i+1), y+oy), pixel); x = x >> @intCast(index);
} var y: u32 = @intFromFloat(sprite_position.y);
} y = y >> @intCast(index);
for (0..sprite_data.height) |oy| { level.setRectZero(
const pixel = image_data.getPixel(x+sprite_data.width-1, y+oy); x - padding,
for (0..sprite.padding) |i| { y - padding,
@memcpy(image_data.getPixel(x+sprite_data.width+i, y+oy), pixel); 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, .{ setTexture(spritesheet.texture, .{
.data = image_data, .mipchain = mipchain,
.premultiplied = true .premultiplied = true
}); });
@ -1137,20 +1143,23 @@ fn ensureSpritePacked(id: Sprite.Id) bool {
if (sprite.position == null) { if (sprite.position == null) {
const spritesheet = self.spritesheets.get(sprite.spritesheet) orelse return false; 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| { if (spritesheet.packer) |*packer| {
assert(sprite_mipchain.length() > 0);
const padding = sprite.padding << @intCast(sprite_mipchain.length()-1);
var rects: [1]STBRectPack.Rect = .{ var rects: [1]STBRectPack.Rect = .{
.{ .{
.w = @intCast(sprite_data.width + sprite.padding * 2), .w = @intCast(sprite_mipchain.width + padding * 2),
.h = @intCast(sprite_data.height + sprite.padding * 2), .h = @intCast(sprite_mipchain.height + padding * 2),
} }
}; };
if (packer.packRects(&rects)) { if (packer.packRects(&rects)) {
// Suboptimal sprite packing, will repack at end of frame // Suboptimal sprite packing, will repack at end of frame
assert(rects[0].was_packed == 1); assert(rects[0].was_packed == 1);
const x: u32 = @as(u32, @intCast(rects[0].x)) + sprite.padding; const x: u32 = @as(u32, @intCast(rects[0].x)) + padding;
const y: u32 = @as(u32, @intCast(rects[0].y)) + sprite.padding; const y: u32 = @as(u32, @intCast(rects[0].y)) + padding;
sprite.position = .initFromInt(u32, x, y); sprite.position = .initFromInt(u32, x, y);
spritesheet.needs_repack = true; spritesheet.needs_repack = true;
} else { } else {
@ -1167,7 +1176,7 @@ fn ensureSpritePacked(id: Sprite.Id) bool {
pub fn getSpriteUVRect(id: Sprite.Id) ?Rect { pub fn getSpriteUVRect(id: Sprite.Id) ?Rect {
const self = &g_state; const self = &g_state;
const sprite = self.sprites.get(id) orelse return null; 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; 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); 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{ return Rect{
.pos = sprite.position.?.divide(spritesheet.size), .pos = sprite.position.?.divide(spritesheet.size),
.size = sprite_size.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 { pub const SpriteSheetOptions = struct {
format: ImageData.Format = .rgba8 mipmaps: u32 = 1,
format: Image.Format = .rgba8
}; };
pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id { pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id {
@ -1225,11 +1235,15 @@ pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id {
return .nil; return .nil;
}; };
assert(opts.mipmaps <= sg.max_mipmaps);
assert(opts.mipmaps > 0);
const spritesheet = self.spritesheets.getAssumeExists(id); const spritesheet = self.spritesheets.getAssumeExists(id);
spritesheet.* = .{ spritesheet.* = .{
.texture = initTexture(.{ .dynamic = true }), .texture = initTexture(.{ .dynamic = true }),
.size = .init(0, 0), .size = .init(0, 0),
.format = opts.format, .format = opts.format,
.mipmaps = opts.mipmaps,
.needs_repack = false, .needs_repack = false,
.needs_texture_rebuild = false, .needs_texture_rebuild = false,
.nodes = undefined, .nodes = undefined,
@ -1247,105 +1261,86 @@ pub fn deinitSpritesheet(id: Spritesheet.Id) void {
} }
} }
pub const ImageData = struct { pub const Image = struct {
const Format = enum { format: Image.Format,
rgba8, pixels: [*]u8,
r8
};
const Pixels = union(Format) {
rgba8: [*]u8,
r8: [*]u8,
};
pixels: ?Pixels,
width: u32, width: u32,
height: u32, height: u32,
pub const none = ImageData{ pub const Format = enum {
.pixels = null, 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, .width = 0,
.height = 0, .height = 0,
}; };
pub fn init(gpa: Allocator, width: u32, height: u32, format: ?Format) !ImageData { const InitOptions = struct {
const pixel_count = width * height; format: Image.Format,
var pixels: ?Pixels = null; width: u32,
if (format != null) { height: u32,
pixels = switch (format.?) { };
.rgba8 => Pixels{
.rgba8 = (try gpa.alloc(u8, pixel_count * 4)).ptr
},
.r8 => Pixels{
.r8 = (try gpa.alloc(u8, pixel_count)).ptr
}
};
}
return ImageData{ pub fn initAlloc(gpa: Allocator, opts: InitOptions) !Image {
.pixels = pixels, const pixel_count = opts.width * opts.height;
.width = width, const pixel_size = opts.format.pixelSize();
.height = height
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 { fn pixelCount(self: Image) usize {
return ImageData.init(gpa, width, height, .rgba8); return self.width * self.height;
} }
pub fn initR8(gpa: Allocator, width: u32, height: u32) !ImageData { fn pixelsSlice(self: Image) []u8 {
return ImageData.init(gpa, width, height, .r8); return self.pixels[0..(self.pixelCount() * self.format.pixelSize())];
} }
pub fn clone(self: ImageData, gpa: Allocator) !ImageData { pub fn clone(self: Image, arena: Allocator) !Image {
const pixel_count = self.width * self.height; return Image{
.pixels = (try arena.dupe(u8, self.pixelsSlice())).ptr,
var pixels: ?Pixels = null; .format = self.format,
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,
.width = self.width, .width = self.width,
.height = self.height .height = self.height
}; };
} }
pub fn deinit(self: ImageData, gpa: Allocator) void { pub fn deinit(self: Image, gpa: Allocator) void {
const pixel_count = self.width * self.height; gpa.free(self.pixelsSlice());
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 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; const pixel_index = y * self.width + x;
if (self.pixels) |pixels| { const pixel_size = self.format.pixelSize();
return switch (pixels) { return self.pixels[(pixel_size*pixel_index)..][0..pixel_size];
.rgba8 => |rgba8| rgba8[(4*pixel_index)..][0..4],
.r8 => |r8| r8[pixel_index..][0..1]
};
} else {
return &.{};
}
} }
pub fn setRect(self: ImageData, x: u32, y: u32, width: u32, height: u32, pixel: []const u8) void { pub fn setRect(self: Image, x: u32, y: u32, width: u32, height: u32, pixel: []const u8) void {
if (self.pixels == null) {
return;
}
for (0..height) |oy| { for (0..height) |oy| {
const iy = y + oy; const iy = y + oy;
if (iy >= self.height) { 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 { pub fn setRectZero(self: Image, x: u32, y: u32, width: u32, height: u32) void {
if (self.pixels == null) { const zero_pixel_buff: [Image.Format.maxPixelSize()]u8 = @splat(0);
return; const zero_pixel = zero_pixel_buff[0..self.format.pixelSize()];
} self.setRect(x, y, width, height, zero_pixel);
const pixel = switch (self.pixels.?) {
.rgba8 => &[_]u8{ 0, 0, 0, 0 },
.r8 => &[_]u8{ 0 }
};
self.setRect(x, y, width, height, pixel);
} }
pub fn premultiplyAlpha(self: ImageData) void { pub fn premultiplyAlpha(self: Image) void {
const pixels = self.pixels orelse return; if (self.format != .rgba8) return;
if (pixels != .rgba8) return;
const rgba8_pixels = pixels.rgba8;
for (0..self.height) |y| { for (0..self.height) |y| {
for (0..self.width) |x| { for (0..self.width) |x| {
const pixel_index = y * 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| { inline for (0..3) |i| {
rgba8_pixel[i] = @intCast(@as(u16, rgba8_pixel[i]) * rgba8_pixel[3] / 255); rgba8_pixel[i] = @intCast(@as(u16, rgba8_pixel[i]) * rgba8_pixel[3] / 255);
@ -1393,9 +1380,9 @@ pub const ImageData = struct {
} }
} }
pub fn copyFrom( pub fn copyFromRect(
self: ImageData, self: Image,
source: ImageData, source: Image,
cx: u32, cx: u32,
cy: u32, cy: u32,
source_cx: u32, source_cx: u32,
@ -1403,9 +1390,7 @@ pub const ImageData = struct {
source_width: u32, source_width: u32,
source_height: u32, source_height: u32,
) void { ) void {
if (source.pixels == null or self.pixels == null) { assert(source.format == self.format);
return;
}
for (0..source_height) |oy| { for (0..source_height) |oy| {
const source_y = source_cy+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 { pub const Texture = struct {
@ -1448,8 +1597,9 @@ pub const Texture = struct {
}; };
pub const Sprite = struct { pub const Sprite = struct {
arena: std.heap.ArenaAllocator,
spritesheet: Spritesheet.Id, spritesheet: Spritesheet.Id,
data: ?ImageData, mipchain: ?Mipchain,
position: ?Vec2, position: ?Vec2,
padding: u32, padding: u32,
@ -1460,8 +1610,9 @@ pub const Sprite = struct {
pub const Spritesheet = struct { pub const Spritesheet = struct {
texture: Texture.Id, texture: Texture.Id,
format: ImageData.Format, format: Image.Format,
size: Vec2, size: Vec2,
mipmaps: u32,
needs_repack: bool, needs_repack: bool,
needs_texture_rebuild: bool, needs_texture_rebuild: bool,

View File

@ -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()) { if (isDisabled()) {
return false; return false;
} }
return ig.igBeginTable(id, @intCast(columns), flags); return ig.igBeginTable(id, @intCast(columns), flags.toInt());
} }
pub fn endTable() void { pub fn endTable() void {
@ -544,12 +556,22 @@ pub fn tableSetColumnIndex(index: usize) void {
_ = ig.igTableSetColumnIndex(@intCast(index)); _ = 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()) { if (isDisabled()) {
return; return;
} }
ig.igTableSetupColumn(label, flags); ig.igTableSetupColumn(label, flags.toInt());
} }
pub fn tableHeadersRow() void { pub fn tableHeadersRow() void {

View File

@ -149,6 +149,11 @@ fn PlatformType(App: type) type {
_ = ImGUI.beginTabBar("platform tab bar"); _ = ImGUI.beginTabBar("platform tab bar");
defer ImGUI.endTabBar(); defer ImGUI.endTabBar();
if (ImGUI.beginTabItem("General")) {
defer ImGUI.endTabItem();
ImGUI.text("Embedded assets: {B}", .{self.assets.totalSize()});
}
if (ImGUI.beginTabItem("Graphics")) { if (ImGUI.beginTabItem("Graphics")) {
defer ImGUI.endTabItem(); defer ImGUI.endTabItem();
Gfx.showDebug(); 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 { fn insertStaticFile(self: *Assets, filename: []const u8, contents: [:0]const u8) !void {
try self.files.put(self.gpa, filename, File{ try self.files.put(self.gpa, filename, File{
.contents = contents .contents = contents

View File

@ -9,25 +9,27 @@ const Rect = Platform.Rect;
const Tilesheet = @This(); const Tilesheet = @This();
image: Gfx.ImageData, sprite_options: Gfx.SpriteOptions,
mipchain: Gfx.Mipchain,
sprites: []Gfx.Sprite.Id, sprites: []Gfx.Sprite.Id,
tile_size: Vec2, tile_size: Vec2,
width: u32, width: u32,
height: u32, height: u32,
pub fn init(gpa: Allocator, image: Gfx.ImageData, tile_size: Vec2) !Tilesheet { pub fn init(gpa: Allocator, mipchain: Gfx.Mipchain, tile_size: Vec2, sprite_options: Gfx.SpriteOptions) !Tilesheet {
const width = @divFloor(image.width, @as(u32, @intFromFloat(tile_size.x))); const width = @divFloor(mipchain.width, @as(u32, @intFromFloat(tile_size.x)));
const height = @divFloor(image.height, @as(u32, @intFromFloat(tile_size.y))); const height = @divFloor(mipchain.height, @as(u32, @intFromFloat(tile_size.y)));
const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height); const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height);
errdefer gpa.free(sprites); errdefer gpa.free(sprites);
@memset(sprites, .nil); @memset(sprites, .nil);
return Tilesheet{ return Tilesheet{
.sprites = sprites, .sprites = sprites,
.image = image, .mipchain = mipchain,
.width = width, .width = width,
.height = height, .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; const sprite_index = y * self.width + x;
if (self.sprites[sprite_index] == Gfx.Sprite.Id.nil) { 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, .{ Gfx.setSprite(sprite, .{
.image = self.image, .mipchain = self.mipchain,
.rect = Rect.init(@floatFromInt(x), @floatFromInt(y), 1, 1).multiply(self.tile_size) .rect = Rect.init(@floatFromInt(x), @floatFromInt(y), 1, 1).multiply(self.tile_size)
}); });
self.sprites[sprite_index] = sprite; self.sprites[sprite_index] = sprite;