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 asset_files = [_][]const u8{
"sokoban/sokoban_tilesheet.png",
"sokoban/sokoban_tilesheet@2.png",
"roboto-font/Roboto-Regular.ttf",
"tiled/map.tmx",
"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")
});
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;

View File

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

View File

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

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()) {
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 {

View File

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

View File

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