diff --git a/assets/roboto-font/LICENSE.txt b/assets/roboto-font/LICENSE.txt new file mode 100644 index 0000000..9c48e05 --- /dev/null +++ b/assets/roboto-font/LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/roboto-font/Roboto-Bold.ttf b/assets/roboto-font/Roboto-Bold.ttf new file mode 100644 index 0000000..9d7cf22 Binary files /dev/null and b/assets/roboto-font/Roboto-Bold.ttf differ diff --git a/assets/roboto-font/Roboto-Italic.ttf b/assets/roboto-font/Roboto-Italic.ttf new file mode 100644 index 0000000..c3abaef Binary files /dev/null and b/assets/roboto-font/Roboto-Italic.ttf differ diff --git a/assets/roboto-font/Roboto-Regular.ttf b/assets/roboto-font/Roboto-Regular.ttf new file mode 100644 index 0000000..7e3bb2f Binary files /dev/null and b/assets/roboto-font/Roboto-Regular.ttf differ diff --git a/build.zig b/build.zig index 86a0a0e..27c8766 100644 --- a/build.zig +++ b/build.zig @@ -7,7 +7,8 @@ pub fn build(b: *std.Build) !void { const assets_dir = "assets"; const asset_files = [_][]const u8{ - "sokoban/sokoban_tilesheet.png" + "sokoban/sokoban_tilesheet.png", + "roboto-font/Roboto-Regular.ttf" }; const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir }); @@ -81,6 +82,7 @@ pub fn build(b: *std.Build) !void { exe_mod.addImport("stb_image", dep_stb.module("stb_image")); exe_mod.addImport("stb_vorbis", dep_stb.module("stb_vorbis")); exe_mod.addImport("stb_rect_pack", dep_stb.module("stb_rect_pack")); + exe_mod.addImport("stb_truetype", dep_stb.module("stb_truetype")); const dep_tracy = b.dependency("tracy", .{ .target = target, diff --git a/build.zig.zon b/build.zig.zon index 04326e3..80d956e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -17,6 +17,7 @@ .cimgui = .{ .url = "git+https://github.com/floooh/dcimgui.git#3c2b41a9333b1195e2c2f8ce6a2d3e50660a73bf", .hash = "cimgui-0.1.0-44ClkR8dmgDqqnfloyLE1bUAtl4zSKTT1wxaHSEC0ikV", + .lazy = true }, }, .paths = .{ diff --git a/libs/stb/build.zig b/libs/stb/build.zig index 7acb90c..a601475 100644 --- a/libs/stb/build.zig +++ b/libs/stb/build.zig @@ -1,53 +1,37 @@ const std = @import("std"); +const Build = std.Build; +const LazyPath = Build.LazyPath; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + buildSTBModule(b, target, optimize, "stb_image", b.path("src/stb_image.zig"), b.path("src/stb_image_impl.c")); + buildSTBModule(b, target, optimize, "stb_vorbis", b.path("src/stb_vorbis.zig"), b.path("src/stb_vorbis_impl.c")); + buildSTBModule(b, target, optimize, "stb_rect_pack", b.path("src/stb_rect_pack.zig"), b.path("src/stb_rect_pack_impl.c")); + buildSTBModule(b, target, optimize, "stb_truetype", b.path("src/stb_truetype.zig"), b.path("src/stb_truetype_impl.c")); +} + +fn buildSTBModule( + b: *std.Build, + target: Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + name: []const u8, + zig_file: LazyPath, + impl_file: LazyPath +) void { const stb_dependency = b.dependency("stb", .{}); - { - const mod_stb_image = b.addModule("stb_image", .{ - .target = target, - .optimize = optimize, - .root_source_file = b.path("src/stb_image.zig"), - .link_libc = true, - }); + const mod = b.addModule(name, .{ + .target = target, + .optimize = optimize, + .root_source_file = zig_file, + .link_libc = true, + }); - mod_stb_image.addIncludePath(stb_dependency.path(".")); - mod_stb_image.addCSourceFile(.{ - .file = b.path("src/stb_image_impl.c"), - .flags = &.{} - }); - } - - { - const mod_stb_vorbis = b.addModule("stb_vorbis", .{ - .target = target, - .optimize = optimize, - .root_source_file = b.path("src/stb_vorbis.zig"), - .link_libc = true, - }); - - mod_stb_vorbis.addIncludePath(stb_dependency.path(".")); - mod_stb_vorbis.addCSourceFile(.{ - .file = b.path("src/stb_vorbis_impl.c"), - .flags = &.{} - }); - } - - { - const mod_stb_rect_pack = b.addModule("stb_rect_pack", .{ - .target = target, - .optimize = optimize, - .root_source_file = b.path("src/stb_rect_pack.zig"), - .link_libc = true, - }); - - mod_stb_rect_pack.addIncludePath(stb_dependency.path(".")); - mod_stb_rect_pack.addCSourceFile(.{ - .file = b.path("src/stb_rect_pack_impl.c"), - .flags = &.{} - }); - } + mod.addIncludePath(stb_dependency.path(".")); + mod.addCSourceFile(.{ + .file = impl_file, + .flags = &.{} + }); } diff --git a/libs/stb/src/stb_truetype.zig b/libs/stb/src/stb_truetype.zig new file mode 100644 index 0000000..97621c4 --- /dev/null +++ b/libs/stb/src/stb_truetype.zig @@ -0,0 +1,116 @@ +const std = @import("std"); + +const c = @cImport({ + @cInclude("stb_truetype.h"); +}); + +pub const Box = struct { + x0: i32, + y0: i32, + x1: i32, + y1: i32, +}; + +pub const HMetrics = struct { + advance_width: i32, + left_side_bearing: i32 +}; + +pub const VMetrics = struct { + ascent: i32, + descent: i32, + line_gap: i32 +}; + +pub const Font = struct { + info: c.stbtt_fontinfo, + + pub fn getNumberOfFonts(data: [*c]const u8) !u32 { + const result = c.stbtt_GetNumberOfFonts(data); + if (result == -1) { + return error.stbtt_GetNumberOfFonts; + } + return @intCast(result); + } + + pub fn getFontOffsetForIndex(data: [*c]const u8, index: u32) ?u32 { + const result = c.stbtt_GetFontOffsetForIndex(data, @intCast(index)); + if (result == -1) { + return null; + } + return @intCast(result); + } + + pub fn init(data: [*c]const u8, offset: u32) !Font { + var font: Font = .{ + .info = undefined + }; + if (c.stbtt_InitFont(&font.info, data, @intCast(offset)) == 0) { + return error.stbtt_InitFont; + } + return font; + } + + pub fn findGlyphIndex(self: Font, codepoint: u21) ?u32 { + const result = c.stbtt_FindGlyphIndex(&self.info, codepoint); + if (result == 0) { + return null; + } + return @intCast(result); + } + + pub fn scaleForPixelHeight(self: Font, pixel_height: f32) f32 { + return c.stbtt_ScaleForPixelHeight(&self.info, pixel_height); + } + + pub fn getGlyphBitmapBox(self: Font, glyph: u32, scale_x: f32, scale_y: f32) Box { + var result: Box = undefined; + c.stbtt_GetGlyphBitmapBox( + &self.info, + @intCast(glyph), + scale_x, scale_y, + &result.x0, &result.y0, + &result.x1, &result.y1, + ); + return result; + } + + pub fn makeGlyphBitmap( + self: Font, + output: [*]u8, + out_w: u32, + out_h: u32, + out_stride: u32, + scale_x: f32, + scale_y: f32, + glyph: u32 + ) void { + c.stbtt_MakeGlyphBitmap( + &self.info, + output, + @intCast(out_w), + @intCast(out_h), + @intCast(out_stride), + scale_x, + scale_y, + @intCast(glyph) + ); + } + + pub fn getGlyphHMetrics(self: Font, glyph: u32) HMetrics { + var result: HMetrics = undefined; + c.stbtt_GetGlyphHMetrics(&self.info, @intCast(glyph), &result.advance_width, &result.left_side_bearing); + return result; + } + + pub fn getFontVMetrics(self: Font) VMetrics { + var result: VMetrics = undefined; + c.stbtt_GetFontVMetrics(&self.info, &result.ascent, &result.descent, &result.line_gap); + return result; + } + + pub fn getGlyphKernAdvance(self: Font, prev_glyph: u32, next_glyph: u21) i32 { + return c.stbtt_GetGlyphKernAdvance(&self.info, prev_glyph, next_glyph); + } +}; + diff --git a/libs/stb/src/stb_truetype_impl.c b/libs/stb/src/stb_truetype_impl.c new file mode 100644 index 0000000..9f0bf61 --- /dev/null +++ b/libs/stb/src/stb_truetype_impl.c @@ -0,0 +1,4 @@ +#define STB_TRUETYPE_IMPLEMENTATION +// TODO: #define STBTT_STATIC +#include "stb_truetype.h" + diff --git a/src/app.zig b/src/app.zig index b288001..b9ee9c6 100644 --- a/src/app.zig +++ b/src/app.zig @@ -6,6 +6,7 @@ const Math = @import("math"); const Vec2 = Math.Vec2; const STBImage = @import("stb_image"); +const STBTrueType = @import("stb_truetype"); const Platform = @import("./platform.zig"); const Gfx = Platform.Gfx; @@ -20,10 +21,24 @@ check: bool, player_pos: Vec2, sprite_sprite: Gfx.Sprite.Id, wall_sprite: Gfx.Sprite.Id, +roboto_font: Gfx.Font.Id, pub fn init(self: *App, plt: Platform.Init) !?u8 { const sokoban_spritesheet = plt.assets.readFile("sokoban/sokoban_tilesheet.png"); + // const font = try STBTrueType.Font.init(robot_font, STBTrueType.Font.getFontOffsetForIndex(robot_font, 0).?); + + // const glyph = font.findGlyphIndex('A').?; + // const scale = font.scaleForPixelHeight(16); + // const box = font.getGlyphBitmapBox(glyph, scale, scale); + // const width = box.x1 - box.x0; + // const height = box.y1 - box.y0; + // font.makeGlyphBitmap + // std.debug.print("{}\n", .{box}); + + const robot_font = Gfx.initFont(); + Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf")); + var image = try STBImage.load(sokoban_spritesheet); defer image.deinit(); @@ -35,13 +50,13 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { const tile_size = Vec2.init(64, 64); - const player_sprite = Gfx.initSprite(); + const player_sprite = Gfx.initSprite(.{}); Gfx.setSprite(player_sprite, .{ .image = spritesheet, .rect = Rect.init(0, 4, 1, 1).multiply(tile_size) }); - const wall_sprite = Gfx.initSprite(); + const wall_sprite = Gfx.initSprite(.{}); Gfx.setSprite(wall_sprite, .{ .image = spritesheet, .rect = Rect.init(1, 0, 1, 1).multiply(tile_size) @@ -53,7 +68,8 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { .show_first_window = true, .check = false, .sprite_sprite = player_sprite, - .wall_sprite = wall_sprite + .wall_sprite = wall_sprite, + .roboto_font = robot_font }; return null; @@ -83,6 +99,11 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { Gfx.setClearColor(self.bg); Gfx.drawSprite(self.sprite_sprite, self.player_pos, .init(100, 100), .white); Gfx.drawSprite(self.wall_sprite, self.player_pos.add(.init(0, 200)), .init(100, 100), .white); + Gfx.drawText(self.roboto_font, .{ + .pos = .init(300, 100), + .text = "Hello, World!", + .height = 32, + }); if (ImGUI.beginWindow(.{ .name = "Hello", diff --git a/src/graphics.zig b/src/graphics.zig index c15757d..ad2e385 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -15,6 +15,8 @@ const Mat4 = Math.Mat4; const Rect = Math.Rect; const STBRectPack = @import("stb_rect_pack"); +const STBTrueType = @import("stb_truetype"); +const ImGUI = @import("imgui.zig"); const Color = @import("./color.zig"); const shd = @import("shader"); const SlotMapType = @import("slot_map.zig").SlotMapType; @@ -22,6 +24,8 @@ const SlotMapType = @import("slot_map.zig").SlotMapType; const State = struct { gpa: std.mem.Allocator, + frame_index: u64, + shader: sg.Shader, pipeline: sg.Pipeline, bindings: sg.Bindings, @@ -29,6 +33,7 @@ const State = struct { default_sprite: Sprite.Id, error_sprite: Sprite.Id, default_spritesheet: Spritesheet.Id, + default_font_spritesheet: Spritesheet.Id, default_sampler: sg.Sampler, quads_buffer: [2048]Quad, @@ -47,6 +52,10 @@ const State = struct { sprites_buffer: [Sprite.max_sprites]Sprite.SlotMap.Slot, sprites: Sprite.SlotMap, nil_sprite: Sprite.Id, + + fonts_buffer: [Font.max_fonts]Font.SlotMap.Slot, + fonts: Font.SlotMap, + nil_font: Font.Id, }; var g_state: State = undefined; @@ -130,6 +139,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { self.* = State{ .gpa = gpa, + .frame_index = 0, .shader = shader, .pipeline = pipeline, .bindings = bindings, @@ -140,6 +150,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { .default_sprite = undefined, .error_sprite = undefined, .default_spritesheet = undefined, + .default_font_spritesheet = undefined, .textures_buffer = undefined, .textures = .init(.initBuffer(&self.textures_buffer)), @@ -152,12 +163,18 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { .spritesheets_buffer = undefined, .spritesheets = .init(.initBuffer(&self.spritesheets_buffer)), .nil_spritesheet = undefined, + + .fonts_buffer = undefined, + .fonts = .init(.initBuffer(&self.fonts_buffer)), + .nil_font = undefined, }; self.nil_texture = self.textures.insertAssumeCapacity(); { const nil_texture = self.textures.getAssumeExists(self.nil_texture); nil_texture.* = Texture{ + .dynamic = false, + .updated_at = null, .image = .{ .id = sg.invalid_id }, .view = .{ .id = sg.invalid_id }, }; @@ -168,8 +185,12 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { const nil_spritesheet = self.spritesheets.getAssumeExists(self.nil_spritesheet); nil_spritesheet.* = Spritesheet{ .texture = self.nil_texture, + .format = .rgba8, .size = .init(0, 0), - .stale = false + .needs_repack = false, + .needs_texture_rebuild = false, + .nodes = undefined, + .packer = null, }; } @@ -183,6 +204,19 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { }; } + self.nil_font = self.fonts.insertAssumeCapacity(); + { + const nil_font = self.fonts.getAssumeExists(self.nil_font); + nil_font.* = Font{ + .spritesheet = self.nil_spritesheet, + .stb = null, + .cache = .init(), + }; + } + + self.default_spritesheet = initSpritesheet(.{}); + self.default_font_spritesheet = initSpritesheet(.{ .format = .r8 }); + { const width = 8; const height = 8; @@ -190,7 +224,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { var pixels: [width * height * 4]u8 = undefined; @memset(&pixels, 0xFF); - self.default_sprite = initSprite(); + self.default_sprite = initSprite(.{}); setSprite(self.default_sprite, .{ .image = ImageData{ .width = width, @@ -224,7 +258,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { } } - self.error_sprite = initSprite(); + self.error_sprite = initSprite(.{}); setSprite(self.error_sprite, .{ .image = .{ .width = width, @@ -234,7 +268,8 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { }); } - self.default_spritesheet = initSpritesheet(); + repackSpritesheetIfNeeded(self.default_spritesheet); + rebuildSpritesheetTextureIfNeeded(self.default_spritesheet); } pub fn deinit() void { @@ -302,6 +337,41 @@ pub fn beginFrame() void { self.bindings.samplers[shd.SMP_smp] = self.default_sampler; } +pub fn showDebug() void { + const self = &g_state; + _ = self; // autofix + + // TODO: Improve this by using tables or somekind of filtering + + // if (ImGUI.beginWindow(.{ + // .name = "graphics", + // .size = .init(200, 200) + // })) { + // defer ImGUI.endWindow(); + // + // { + // ImGUI.text("Spritesheets:", .{}); + // var spritesheet_iter = self.spritesheets.iterator(); + // while (spritesheet_iter.next()) |spritesheet_id| { + // const spritesheet = self.spritesheets.getAssumeExists(spritesheet_id); + // const texture = self.textures.get(spritesheet.texture); + // ImGUI.text("{f}:", .{spritesheet_id}); + // ImGUI.text(" - needs_texture_rebuild:{}", .{spritesheet.needs_texture_rebuild}); + // ImGUI.text(" - texture:{?}", .{texture}); + // } + // } + // + // { + // ImGUI.text("Sprites:", .{}); + // var sprite_iter = self.sprites.iterator(); + // while (sprite_iter.next()) |sprite_id| { + // const sprite = self.sprites.getAssumeExists(sprite_id); + // ImGUI.text("{f} - {?}", .{sprite_id, sprite.position}); + // } + // } + // } +} + // TODO: This will always have a 1-frame delay. // fix this. pub fn setClearColor(color: Color) void { @@ -310,7 +380,11 @@ pub fn setClearColor(color: Color) void { self.clear_color = color; } -pub fn flush() void { +const FlushOptions = struct { + rebuild_spritesheets: bool = true +}; + +pub fn flush(opts: FlushOptions) void { const self = &g_state; if (self.quads.items.len == 0) { @@ -318,6 +392,17 @@ pub fn flush() void { } defer self.quads.clearRetainingCapacity(); + if (opts.rebuild_spritesheets) { + var spritesheet_iter = self.spritesheets.iterator(); + while (spritesheet_iter.next()) |spritesheet_id| { + const spritesheet = self.spritesheets.getAssumeExists(spritesheet_id); + const texture = self.textures.get(spritesheet.texture) orelse continue; + if (isViewBound(texture.view)) { + rebuildSpritesheetTextureIfNeeded(spritesheet_id); + } + } + } + const vertex_buffer = self.bindings.vertex_buffers[0]; const data = sg.asRange(self.quads.items); @@ -338,17 +423,34 @@ pub fn flush() void { .projection = createProjectionMatrix(window_size) }; + var fs_params: shd.FsParams = .{ + .texture_mode = 0, + }; + const image = sg.queryViewImage(self.bindings.views[shd.VIEW_tex]); + if (sg.queryImagePixelformat(image) == .R8) { + fs_params.texture_mode = 1; + } + sg.applyPipeline(self.pipeline); sg.applyBindings(self.bindings); sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params)); + sg.applyUniforms(shd.UB_fs_params, sg.asRange(&fs_params)); sg.draw(0, @intCast(self.quads.items.len * 6), 1); } pub fn endFrame() void { - flush(); + const self = &g_state; + + flush(.{}); sg.endPass(); sg.commit(); + self.frame_index += 1; + + var spritesheet_iter = self.spritesheets.iterator(); + while (spritesheet_iter.next()) |spritesheet_id| { + repackSpritesheetIfNeeded(spritesheet_id); + } } pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void { @@ -381,49 +483,62 @@ pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { }); } -pub fn initTexture() Texture.Id { +const TextureOptions = struct { + dynamic: bool = false +}; + +pub fn initTexture(opts: TextureOptions) Texture.Id { const self = &g_state; const id = self.textures.insertBounded() catch { log.warn("Failed to create texture, limit reached! limit: {}", .{self.textures.slots.capacity}); return self.nil_texture; }; - const image = sg.allocImage(); - if (image.id == sg.invalid_id) { - log.warn("allocImage() failed!", .{}); - return self.nil_texture; - } - const view = sg.allocView(); - if (view.id == sg.invalid_id) { - log.warn("allocView() failed!", .{}); + if (sg.queryViewState(view) != .ALLOC) { + log.warn("Failed to alloc texture view", .{}); return self.nil_texture; } const texture = self.textures.getAssumeExists(id); texture.* = .{ - .image = image, - .view = view + .dynamic = opts.dynamic, + .updated_at = null, + .image = .{ .id = sg.invalid_id }, + .view = view, }; return id; } +fn isViewBound(view: sg.View) bool { + const self = &g_state; + const bound_view = &self.bindings.views[shd.VIEW_tex]; + return bound_view.id == view.id; +} + +fn deinitTextureResources(texture: *Texture) void { + const self = &g_state; + + if (isViewBound(texture.view)) { + const default_sprite = self.sprites.getAssumeExists(self.default_sprite); + const default_spritesheet = self.spritesheets.getAssumeExists(default_sprite.spritesheet); + const default_texture = self.textures.getAssumeExists(default_spritesheet.texture); + bindView(default_texture.view, .{ .rebuild_spritesheets = false }); + } + + sg.destroyImage(texture.image); +} + pub fn deinitTexture(id: Texture.Id) void { const self = &g_state; if (id == self.nil_texture) { return; } - if (self.textures.remove(id)) { - const texture = self.textures_buffer[id.index]; - const bound_view = &self.bindings.views[shd.VIEW_tex]; - if (texture.view.id == bound_view.id) { - assert(self.textures.exists(self.default_sprite)); - bound_view.* = self.textures_buffer[self.default_sprite.index].view; - } - + if (self.textures.get(id)) |texture| { + deinitTextureResources(texture); sg.destroyView(texture.view); - sg.destroyImage(texture.image); + self.textures.removeAssumeExists(id); } } @@ -438,35 +553,87 @@ pub fn setTexture(id: Texture.Id, texture_data: ImageData) void { return; }; - if (sg.queryImageState(texture.image) == .VALID) { - sg.uninitImage(texture.image); - } - const pixel_count = texture_data.width*texture_data.height; var image_data: sg.ImageData = .{}; - image_data.mip_levels[0] = sg.asRange(texture_data.pixels.rgba8[0..(pixel_count*4)]); - sg.initImage(texture.image, .{ + 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; + } + } + } + + var new_image_desc = sg.ImageDesc{ .type = ._2D, .width = @intCast(texture_data.width), .height = @intCast(texture_data.height), .num_mipmaps = 1, - .pixel_format = .RGBA8, - .data = image_data, - }); - if (sg.queryImageState(texture.image) != .VALID) { - log.warn("initImage() failed", .{}); - return; + .pixel_format = pixel_format, + }; + if (texture.dynamic) { + new_image_desc.usage.dynamic_update = true; + } else { + new_image_desc.data = image_data; } - if (sg.queryViewState(texture.view) == .ALLOC) { + var texture_updated: bool = false; + if (sg.queryImageState(texture.image) == .VALID) { + // sg.queryImageInfo + const image_desc = sg.queryImageDesc(texture.image); + if (image_desc.width == new_image_desc.width and + image_desc.height == new_image_desc.height and + image_desc.num_mipmaps == new_image_desc.num_mipmaps and + image_desc.pixel_format == new_image_desc.pixel_format and + image_desc.usage.dynamic_update == new_image_desc.usage.dynamic_update and + image_desc.usage.immutable == new_image_desc.usage.immutable and + image_desc.usage.dynamic_update + ) { + log.debug("Update texture '{f}'", .{id}); + if (texture.updated_at == null or texture.updated_at.? != self.frame_index) { + sg.updateImage(texture.image, image_data); + texture_updated = true; + texture.updated_at = self.frame_index; + } else { + log.warn("Attempt to update the same texture multiple times per frame", .{}); + } + } + } + + if (!texture_updated) { + log.debug("Create image-view, texture={f}", .{id}); + const new_image = sg.makeImage(new_image_desc); + if (sg.queryImageState(new_image) != .VALID) { + log.warn("makeImage() failed", .{}); + return; + } + + if (sg.queryViewState(texture.view) == .VALID) { + sg.uninitView(texture.view); + } sg.initView(texture.view, .{ - .texture = .{ .image = texture.image } + .texture = .{ .image = new_image } }); if (sg.queryViewState(texture.view) != .VALID) { - log.warn("initView() failed", .{}); + log.warn("makeView() failed", .{}); return; } + + deinitTextureResources(texture); + texture.image = new_image; + texture.updated_at = null; + + if (texture.dynamic) { + sg.updateImage(new_image, image_data); + texture.updated_at = self.frame_index; + } } } @@ -482,6 +649,22 @@ pub fn drawTexture(id: Texture.Id, pos: Vec2, size: Vec2, color: Color) void { }); } +fn bindView(view: sg.View, opts: FlushOptions) void { + const self = &g_state; + + const view_state = sg.queryViewState(view); + if (view_state != .VALID and view_state != .ALLOC) { + log.warn("Attempt to bind non-valid view: state={}", .{view_state}); + return; + } + + const bound_view = &self.bindings.views[shd.VIEW_tex]; + if (bound_view.id != view.id) { + flush(opts); + bound_view.* = view; + } +} + const DrawOptions = struct { texture: Texture.Id, quad: Quad @@ -498,7 +681,7 @@ pub fn draw(opts: DrawOptions) void { view = texture.view; } - if (sg.queryViewState(view) != .VALID) { + if (sg.queryViewState(view) != .VALID and sg.queryViewState(view) != .ALLOC) { if (self.sprites.get(self.error_sprite)) |sprite| { if (self.spritesheets.get(sprite.spritesheet)) |spritesheet| { if (self.textures.get(spritesheet.texture)) |texture| { @@ -512,25 +695,25 @@ pub fn draw(opts: DrawOptions) void { } } - if (sg.queryViewState(view) != .VALID) { - log.warn("Failed to draw error image", .{}); + if (sg.queryViewState(view) != .VALID and sg.queryViewState(view) != .ALLOC) { + log.warn("Failed to bind view", .{}); return; } - const bound_view = &self.bindings.views[shd.VIEW_tex]; - if (bound_view.id != view.id) { - flush(); - bound_view.* = view; - } + bindView(view, .{}); if (self.quads.items.len == self.quads.capacity) { - flush(); + flush(.{}); } self.quads.appendAssumeCapacity(quad); } -pub fn initSprite() Sprite.Id { +const SpriteOptions = struct { + spritesheet: ?Spritesheet.Id = null +}; + +pub fn initSprite(opts: SpriteOptions) Sprite.Id { const self = &g_state; const id = self.sprites.insertBounded() catch { log.warn("Failed to create sprite, limit reached! limit: {}", .{self.sprites.slots.capacity}); @@ -539,7 +722,7 @@ pub fn initSprite() Sprite.Id { const sprite = self.sprites.getAssumeExists(id); sprite.* = .{ - .spritesheet = self.default_spritesheet, + .spritesheet = opts.spritesheet orelse self.default_spritesheet, .data = null, .position = null }; @@ -561,7 +744,7 @@ pub fn deinitSprite(id: Sprite.Id) void { self.sprites.removeAssumeExists(id); if (self.spritesheets.get(sprite.spritesheet)) |spritesheet| { - spritesheet.stale = true; + spritesheet.needs_repack = true; } } } @@ -582,11 +765,29 @@ pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void { return; }; + const spritesheet = self.spritesheets.get(sprite.spritesheet) orelse { + log.warn("Attempt to set sprite which doesn't have spritesheet", .{}); + 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.rect) |rect| { - sprite_data = ImageData.initRGBA8(self.gpa, @trunc(rect.size.x), @trunc(rect.size.y)) catch |e| { + var format: ?ImageData.Format = null; + if (opts.image.pixels) |pixels| { + format = @as(ImageData.Format, pixels); + } + + sprite_data = ImageData.init(self.gpa, @trunc(rect.size.x), @trunc(rect.size.y), format) catch |e| { log.warn("Failed to create texture data: {}", .{e}); return; }; @@ -604,40 +805,41 @@ pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void { }; } + 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); } - sprite.data = sprite_data; - sprite.position = null; - if (self.spritesheets.get(sprite.spritesheet)) |spritesheet| { - spritesheet.stale = true; + + const new_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height); + + if (old_size != null and old_size.?.eql(new_size) and sprite.position != null) { + spritesheet.needs_texture_rebuild = true; + } else { + sprite.position = null; + spritesheet.needs_repack = true; } } -pub fn packSpritesheet(id: Spritesheet.Id) !void { +// WARNING: This will change the UV coordinates of all sprites. +// It's only safe to do this operation between frames where it's guarenteed that sprites aren't being used. +fn repackSpritesheet(id: Spritesheet.Id) void { const self = &g_state; - if (id == self.nil_spritesheet) { - return; - } const spritesheet = self.spritesheets.get(id) orelse { - log.warn("Attempt to pack spritesheet that doesn't exist: {}", .{id}); + log.warn("Attempt to repack spritesheet which doesn't exist", .{}); return; }; - if (!spritesheet.stale) { - return; - } + log.debug("Repack spritesheet: {f}", .{id}); // TODO: Add a smarter startegy for picking the initial texture size. // One idea is to calculate the sum area of all sprites and pick a atlas size based on that. const texture_sizes = [_]u32{ - 64, 128, 256, 512, 1024, 2048, 4096 + 256, 512, 1024, 2048, 4096 }; - var nodes: [32]STBRectPack.Node = undefined; - var sprites_buffer: [Sprite.max_sprites]*Sprite = undefined; var sprites = std.ArrayList(*Sprite).initBuffer(&sprites_buffer); @@ -659,47 +861,131 @@ pub fn packSpritesheet(id: Spritesheet.Id) !void { }; } - for (&texture_sizes) |texture_size| { - var ctx: STBRectPack = undefined; - ctx.init(texture_size, texture_size, &nodes); - ctx.setupAllowOutOfMem(true); + spritesheet.packer = undefined; + const packer = &spritesheet.packer.?; - const success = ctx.packRects(&rects); - std.debug.print("success {} {}\n", .{texture_size, success}); - if (!success) { + for (&texture_sizes) |texture_size| { + packer.init(texture_size, texture_size, &spritesheet.nodes); + packer.setupAllowOutOfMem(true); + + if (!packer.packRects(&rects)) { continue; } - const image_data = try ImageData.initRGBA8(self.gpa, texture_size, texture_size); - defer image_data.deinit(self.gpa); - for (0.., sprites.items) |i, sprite| { - const sprite_data = sprite.data.?; assert(rects[i].was_packed == 1); const x: u32 = @intCast(rects[i].x); const y: u32 = @intCast(rects[i].y); sprite.position = .initFromInt(u32, x, y); - image_data.copyFrom( - sprite_data, - x, - y, - 0, - 0, - sprite_data.width, - sprite_data.height - ); } - setTexture(spritesheet.texture, image_data); - spritesheet.size = .initFromInt(u32, texture_size, texture_size); - spritesheet.stale = false; + spritesheet.needs_texture_rebuild = true; + spritesheet.needs_repack = false; return; } const max_texture_size = texture_sizes[texture_sizes.len-1]; - log.warn("Failed to pack sprites, couldn't create a texture that is large enough. Max size tried: {}x{}", .{max_texture_size, max_texture_size}); - return error.TooManySprites; + log.err("Failed to pack sprites, couldn't create a texture that is large enough. Max size tried: {}x{}", .{max_texture_size, max_texture_size}); + return; +} + +fn repackSpritesheetIfNeeded(id: Spritesheet.Id) void { + const self = &g_state; + if (id == self.nil_spritesheet) { + return; + } + + const spritesheet = self.spritesheets.get(id) orelse { + log.warn("Attempt to pack spritesheet that doesn't exist: {f}", .{id}); + return; + }; + + if (!spritesheet.needs_repack) { + return; + } + + repackSpritesheet(id); +} + +fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void { + const self = &g_state; + if (id == self.nil_spritesheet) { + return; + } + + const spritesheet = self.spritesheets.get(id) orelse return; + if (!spritesheet.needs_texture_rebuild) { + return; + } + + log.debug("Rebuild spritesheet texture: {f}", .{id}); + + const image_data = ImageData.init( + self.gpa, + @intFromFloat(spritesheet.size.x), + @intFromFloat(spritesheet.size.y), + spritesheet.format + ) catch |e| { + log.err("ImageData allocation failed: {}", .{e}); + return; + }; + defer image_data.deinit(self.gpa); + + var sprite_iter = self.sprites.iterator(); + while (sprite_iter.next()) |sprite_id| { + const sprite = self.sprites.getAssumeExists(sprite_id); + if (sprite.spritesheet != id) { + continue; + } + + const sprite_data = sprite.data orelse continue; + const sprite_position = sprite.position orelse continue; + image_data.copyFrom( + sprite_data, + @intFromFloat(sprite_position.x), + @intFromFloat(sprite_position.y), + 0, + 0, + sprite_data.width, + sprite_data.height + ); + } + + setTexture(spritesheet.texture, image_data); + + spritesheet.needs_texture_rebuild = false; +} + +fn ensureSpritePacked(id: Sprite.Id) bool { + const self = &g_state; + const sprite = self.sprites.get(id) orelse return false; + + if (sprite.position == null) { + const spritesheet = self.spritesheets.get(sprite.spritesheet) orelse return false; + const sprite_data = sprite.data orelse return false; + + if (spritesheet.packer) |*packer| { + var rects: [1]STBRectPack.Rect = .{ + .{ + .w = @intCast(sprite_data.width), + .h = @intCast(sprite_data.height), + } + }; + if (packer.packRects(&rects)) { + // Suboptimal sprite packing, will repack at end of frame + assert(rects[0].was_packed == 1); + sprite.position = .initFromInt(i32, rects[0].x, rects[0].y); + spritesheet.needs_repack = true; + } else { + log.debug("Failed to pack a single sprite to spritesheet", .{}); + } + } else { + repackSpritesheet(sprite.spritesheet); + } + } + + return sprite.position != null; } pub fn getSpriteUVRect(id: Sprite.Id) ?Rect { @@ -713,10 +999,9 @@ pub fn getSpriteUVRect(id: Sprite.Id) ?Rect { const spritesheet = self.spritesheets.get(sprite.spritesheet) orelse return null; - packSpritesheet(sprite.spritesheet) catch |e| { - log.warn("Failed to pack sprites: {}", .{e}); + if (!ensureSpritePacked(id)) { return null; - }; + } assert(sprite.position != null); @@ -752,7 +1037,11 @@ pub fn drawSprite(id: Sprite.Id, pos: Vec2, size: Vec2, color: Color) void { }); } -pub fn initSpritesheet() Spritesheet.Id { +pub const SpriteSheetOptions = struct { + format: ImageData.Format = .rgba8 +}; + +pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id { const self = &g_state; const id = self.spritesheets.insertBounded() catch { log.warn("Failed to create spritesheet, limit reached! limit: {}", .{self.spritesheets.slots.capacity}); @@ -761,9 +1050,13 @@ pub fn initSpritesheet() Spritesheet.Id { const spritesheet = self.spritesheets.getAssumeExists(id); spritesheet.* = .{ - .texture = initTexture(), + .texture = initTexture(.{ .dynamic = true }), .size = .init(0, 0), - .stale = false, + .format = opts.format, + .needs_repack = false, + .needs_texture_rebuild = false, + .nodes = undefined, + .packer = null }; return id; @@ -782,41 +1075,225 @@ pub fn deinitSpritesheet(id: Spritesheet.Id) void { } } -pub const ImageData = struct { - const Pixels = union(enum) { - none, - rgba8: [*]u8 +pub fn initFont() Font.Id { + const self = &g_state; + const id = self.fonts.insertBounded() catch { + log.warn("Failed to create font, limit reached! limit: {}", .{self.fonts.slots.capacity}); + return self.nil_font; }; - pixels: Pixels, + const font = self.fonts.getAssumeExists(id); + font.* = .{ + .spritesheet = self.default_font_spritesheet, + .stb = null, + .cache = .init(), + }; + + return id; +} + +pub fn deinitFont(id: Font.Id) void { + const self = &g_state; + if (id == self.nil_font) { + return; + } + + if (self.fonts.get(id)) { + // TODO: deinit sprites + self.fonts.removeAssumeExists(id); + } +} + +pub fn setFont(id: Font.Id, ttf_data: [:0]const u8) void { + const self = &g_state; + if (id == self.nil_font) { + return; + } + + const font = self.fonts.get(id) orelse { + log.warn("Attempt to set font that doesn't exist: {}", .{id}); + return; + }; + + const offset = STBTrueType.Font.getFontOffsetForIndex(ttf_data, 0) orelse { + log.err("Failed to get offset for font at index 0", .{}); + return; + }; + + const stb_font = STBTrueType.Font.init(ttf_data, offset) catch { + log.err("Failed to initialize font info", .{}); + return; + }; + + // TODO: deinit previous glyphs + + font.stb = stb_font; + font.cache = .init(); +} + +const DrawTextOptions = struct { + text: []const u8, + pos: Vec2, + height: f32, + color: Color = .white +}; + +pub fn drawText(id: Font.Id, opts: DrawTextOptions) void { + const self = &g_state; + if (id == self.nil_font) { + return; + } + + const font = self.fonts.get(id) orelse { + log.warn("Attempt to draw text with font that doesn't exist: {}", .{id}); + return; + }; + const stb_font = font.stb orelse { + log.warn("Attempt to draw text with font that isn't set: {}", .{id}); + return; + }; + + const fallback_glyph = stb_font.findGlyphIndex('?') orelse { + log.warn("Failed to find fallback glyph '?'", .{}); + return; + }; + + const scale = stb_font.scaleForPixelHeight(opts.height); + const scale_x = scale; + const scale_y = scale; + + const vmetrics = stb_font.getFontVMetrics(); + const baseline = @as(f32, @floatFromInt(vmetrics.ascent)) * scale; + + var current_point = opts.pos; + current_point.y += baseline; + + var cursor: usize = 0; + while (cursor < opts.text.len) { + const codepoint_len = std.unicode.utf8ByteSequenceLength(opts.text[cursor]) catch |e| { + log.err("Failed to draw text, invalid codepoint at {} in '{s}': {}", .{cursor, opts.text, e}); + return; + }; + const codepoint_bytes = opts.text[cursor..][0..codepoint_len]; + const codepoint = std.unicode.utf8Decode(codepoint_bytes) catch |e| { + log.err("Failed to draw text, invalid codepoint at {} in '{s}': {}", .{cursor, opts.text, e}); + return; + }; + cursor += codepoint_len; + + const glyph_index = stb_font.findGlyphIndex(codepoint) orelse fallback_glyph; + const glyph_key = Font.Glyph.Key{ .index = glyph_index, .scale_x = scale_x, .scale_y = scale_y }; + + var glyph: *Font.Glyph = undefined; + if (font.cache.lookup(glyph_key)) |glyph_id| { + glyph = font.cache.get(glyph_id); + } else { + const glyph_id = font.cache.insert(glyph_key) orelse { + @panic("TODO: render glyph lru_tail"); + }; + glyph = font.cache.get(glyph_id); + glyph.box = stb_font.getGlyphBitmapBox(glyph_key.index, glyph_key.scale_x, glyph_key.scale_y); + + assert(glyph.sprite == self.nil_sprite); + glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet }); + + const width: u32 = @intCast(glyph.box.x1 - glyph.box.x0); + const height: u32 = @intCast(glyph.box.y1 - glyph.box.y0); + + assert(width < Font.Glyph.max_width); + assert(height < Font.Glyph.max_height); + + var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; + const bitmap = ImageData{ + .pixels = .{ .r8 = (&bitmap_buffer).ptr }, + .width = width, + .height = height + }; + stb_font.makeGlyphBitmap( + bitmap.pixels.?.r8, + width, height, + width, + glyph_key.scale_x, glyph_key.scale_y, + glyph_key.index + ); + setSprite(glyph.sprite, .{ .image = bitmap }); + } + glyph.used_this_frame = true; + + const hmetrics = stb_font.getGlyphHMetrics(glyph_index); + + const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0); + const glyph_pos = current_point.sub(.initFromInt(i32, glyph.box.x0, -glyph.box.y0)); + drawSprite(glyph.sprite, glyph_pos, glyph_size, opts.color); + + current_point.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * scale; + } +} + +pub const ImageData = struct { + const Format = enum { + rgba8, + r8 + }; + + const Pixels = union(Format) { + rgba8: [*]u8, + r8: [*]u8, + }; + + pixels: ?Pixels, width: u32, height: u32, pub const none = ImageData{ - .pixels = .none, + .pixels = null, .width = 0, .height = 0, }; - pub fn initRGBA8(gpa: Allocator, width: u32, height: u32) !ImageData { + pub fn init(gpa: Allocator, width: u32, height: u32, format: ?Format) !ImageData { const pixel_count = width * height; - const pixels = try gpa.alloc(u8, pixel_count * 4); + 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 + } + }; + } return ImageData{ - .pixels = .{ .rgba8 = pixels.ptr }, + .pixels = pixels, .width = width, .height = height }; } + pub fn initRGBA8(gpa: Allocator, width: u32, height: u32) !ImageData { + return ImageData.init(gpa, width, height, .rgba8); + } + + pub fn initR8(gpa: Allocator, width: u32, height: u32) !ImageData { + return ImageData.init(gpa, width, height, .r8); + } + pub fn clone(self: ImageData, gpa: Allocator) !ImageData { const pixel_count = self.width * self.height; - const pixels = switch (self.pixels) { - .none => Pixels{ .none = {} }, - .rgba8 => |pixels| Pixels{ - .rgba8 = (try gpa.dupe(u8, pixels[0..(pixel_count * 4)])).ptr - } - }; + + 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, @@ -827,9 +1304,23 @@ pub const ImageData = struct { pub fn deinit(self: ImageData, gpa: Allocator) void { const pixel_count = self.width * self.height; - switch (self.pixels) { - .none => {}, - .rgba8 => |pixels| gpa.free(pixels[0..(pixel_count*4)]) + if (self.pixels) |pixels| { + switch (pixels) { + .rgba8 => |rgba8| gpa.free(rgba8[0..(pixel_count*4)]), + .r8 => |r8| gpa.free(r8[0..pixel_count]) + } + } + } + + fn getPixel(self: ImageData, x: u32, y: u32) []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 &.{}; } } @@ -843,7 +1334,7 @@ pub const ImageData = struct { source_width: u32, source_height: u32, ) void { - if (source.pixels == .none or self.pixels == .none) { + if (source.pixels == null or self.pixels == null) { return; } @@ -867,11 +1358,8 @@ pub const ImageData = struct { continue; } - const source_pixel_index = source_y * source.width + source_x; - const source_pixel = source.pixels.rgba8[(4*source_pixel_index)..][0..4]; - - const dest_pixel_index = dest_y * self.width + dest_x; - const dest_pixel = self.pixels.rgba8[(4*dest_pixel_index)..][0..4]; + const source_pixel = source.getPixel(@intCast(source_x), @intCast(source_y)); + const dest_pixel = self.getPixel(@intCast(dest_x), @intCast(dest_y)); @memcpy(dest_pixel, source_pixel); } @@ -880,6 +1368,8 @@ pub const ImageData = struct { }; pub const Texture = struct { + dynamic: bool, + updated_at: ?u64, image: sg.Image, view: sg.View, @@ -900,14 +1390,450 @@ pub const Sprite = struct { pub const Spritesheet = struct { texture: Texture.Id, + format: ImageData.Format, size: Vec2, - stale: bool, + + needs_repack: bool, + needs_texture_rebuild: bool, + + packer: ?STBRectPack, + nodes: [256]STBRectPack.Node, const max_spritesheets = 64; const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_spritesheets-1), u8, Spritesheet); pub const Id = SlotMap.Id; }; +pub const Font = struct { + spritesheet: Spritesheet.Id, + stb: ?STBTrueType.Font, + + cache: GlyphCache, + + const max_fonts = 16; + const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_fonts-1), u8, Font); + pub const Id = SlotMap.Id; + + const GlyphCache = struct { + glyphs: [GlyphSlot.limit]GlyphSlot, + glyphs_hashmap: [GlyphSlot.limit*2]GlyphSlot.Id, + lru_head: GlyphSlot.Id, + lru_tail: GlyphSlot.Id, + free_head: GlyphSlot.Id, + + fn init() GlyphCache { + var self: GlyphCache = .{ + .glyphs = undefined, + .glyphs_hashmap = undefined, + .lru_head = GlyphSlot.nil_id, + .lru_tail = GlyphSlot.nil_id, + .free_head = undefined + }; + @memset(&self.glyphs_hashmap, GlyphSlot.nil_id); + @memset(&self.glyphs, .{ + .glyph = undefined, + .lru_prev = GlyphSlot.nil_id, + .lru_next = GlyphSlot.nil_id, + .next_free = GlyphSlot.nil_id, + }); + + assert(self.glyphs_hashmap.len > self.glyphs.len); + + self.free_head = 1; + for (1.., self.glyphs[1..(self.glyphs.len-1)]) |i, *glyph| { + glyph.next_free = @intCast(i + 1); + } + + return self; + } + + fn get(self: *GlyphCache, id: GlyphSlot.Id) *Glyph { + return &self.glyphs[id].glyph; + } + + fn lookup(self: *GlyphCache, key: Glyph.Key) ?GlyphSlot.Id { + const home_bucket = key.hash() % self.glyphs_hashmap.len; + for (0..self.glyphs_hashmap.len) |offset| { + const i = (home_bucket + offset) % self.glyphs_hashmap.len; + const id = self.glyphs_hashmap[i]; + if (id == GlyphSlot.nil_id) { + continue; + } + if (self.glyphs[id].glyph.key == key) { + self.removeFromLRU(id); + self.appendToLRUHead(id); + return id; + } + } + + return null; + } + + fn appendToLRUHead(self: *GlyphCache, id: GlyphSlot.Id) void { + if (self.lru_head != GlyphSlot.nil_id) { + self.glyphs[self.lru_head].lru_prev = id; + self.glyphs[id].lru_next = self.lru_head; + } else { + assert(self.lru_tail == GlyphSlot.nil_id); + self.lru_tail = id; + } + + self.glyphs[id].lru_prev = GlyphSlot.nil_id; + self.lru_head = id; + } + + fn removeFromLRU(self: *GlyphCache, id: GlyphSlot.Id) void { + assert(id != GlyphSlot.nil_id); + + const lru_prev = self.glyphs[id].lru_prev; + const lru_next = self.glyphs[id].lru_next; + + if (lru_prev != GlyphSlot.nil_id) { + self.glyphs[lru_prev].lru_next = lru_next; + } + + if (lru_next != GlyphSlot.nil_id) { + self.glyphs[lru_next].lru_prev = lru_prev; + } + + if (id == self.lru_head) { + self.lru_head = lru_next; + } + if (id == self.lru_tail) { + self.lru_tail = lru_prev; + } + } + + fn popFreeId(self: *GlyphCache) ?GlyphSlot.Id { + if (self.free_head != GlyphSlot.nil_id) { + const result = self.free_head; + self.free_head = self.glyphs[result].next_free; + return result; + } + return null; + } + + fn pushFreeId(self: *GlyphCache, id: GlyphSlot.Id) void { + assert(id != GlyphSlot.nil_id); + self.glyphs[id].next_free = self.free_head; + self.free_head = id; + } + + fn insert(self: *GlyphCache, key: Glyph.Key) ?GlyphSlot.Id { + const key_hash = key.hash(); + + for (0..self.glyphs_hashmap.len) |offset| { + const i = (key_hash + offset) % self.glyphs_hashmap.len; + const hashmap_id = self.glyphs_hashmap[i]; + if (hashmap_id == GlyphSlot.nil_id) { + const id = self.popFreeId() orelse return null; + self.glyphs_hashmap[i] = id; + + self.glyphs[id].glyph = .init(key); + self.glyphs[id].lru_next = GlyphSlot.nil_id; + self.glyphs[id].lru_prev = GlyphSlot.nil_id; + self.appendToLRUHead(id); + return id; + } else { + const glyph = &self.glyphs[hashmap_id].glyph; + if (glyph.key == key) { + // An entry for this key already exists + self.removeFromLRU(hashmap_id); + self.appendToLRUHead(hashmap_id); + return hashmap_id; + } + } + } + + // This can't be reached because the hasmap is guarenteed to have enough slots to store all of the glyph ids + unreachable; + } + + fn removeFromHashmap(self: *GlyphCache, id: GlyphSlot.Id) void { + var bucket = self.glyphs[id].glyph.key.hash() % self.glyphs_hashmap.len; + for (0..self.glyphs_hashmap.len) |_| { + assert(self.glyphs_hashmap[bucket] != GlyphSlot.nil_id); + if (self.glyphs_hashmap[bucket] == id) { + break; + } + + bucket = (bucket + 1) % self.glyphs_hashmap.len; + } + + assert(self.glyphs_hashmap[bucket] == id); + + var empty = bucket; + var current = (bucket + 1) % self.glyphs_hashmap.len; + + while (self.glyphs_hashmap[current] != GlyphSlot.nil_id) { + const current_glyph_id = self.glyphs_hashmap[current]; + const home = self.glyphs[current_glyph_id].glyph.key.hash() % self.glyphs_hashmap.len; + + if (self.hashmapDistance(home, current) > self.hashmapDistance(home, empty)) { + self.glyphs_hashmap[empty] = self.glyphs_hashmap[current]; + empty = current; + } + + current = (current + 1) % self.glyphs_hashmap.len; + } + + assert(self.glyphs_hashmap[empty] != GlyphSlot.nil_id); + self.glyphs_hashmap[empty] = GlyphSlot.nil_id; + } + + fn hashmapDistance(self: GlyphCache, from: usize, to: usize) usize { + if (to >= from) { + return to - from; + } else { + return self.glyphs_hashmap.len - from + to; + } + } + + fn remove(self: *GlyphCache, id: GlyphSlot.Id) void { + assert(id != GlyphSlot.nil_id); + self.pushFreeId(id); + self.removeFromHashmap(id); + self.removeFromLRU(id); + + const glyph = self.glyphs[id].glyph; + glyph.deinit(); + } + + fn getLRUItems(self: *GlyphCache, gpa: Allocator) ![]GlyphSlot.Id { + var lru: std.ArrayList(GlyphSlot.Id) = .empty; + defer lru.deinit(gpa); + + var lru_iter = self.lru_head; + while (lru_iter != GlyphSlot.nil_id) { + try lru.append(gpa, lru_iter); + lru_iter = self.glyphs[lru_iter].lru_next; + } + + return try lru.toOwnedSlice(gpa); + } + + test { + var cache = GlyphCache.init(); + const key = Glyph.Key{ .index = 10, .scale_x = 1, .scale_y = 1 }; + const id1 = cache.insert(key).?; + const id2 = cache.insert(key).?; + try std.testing.expectEqual(id1, id2); + } + + test { + var cache = GlyphCache.init(); + const key = Glyph.Key{ .index = 10, .scale_x = 1, .scale_y = 1 }; + const id1 = cache.insert(key).?; + cache.remove(id1); + const id2 = cache.insert(key).?; + try std.testing.expectEqual(id1, id2); + } + + test { + var cache = GlyphCache.init(); + const id1 = cache.insert(Glyph.Key{ .index = 10, .scale_x = 1, .scale_y = 1 }).?; + const id2 = cache.insert(Glyph.Key{ .index = 11, .scale_x = 1, .scale_y = 1 }).?; + try std.testing.expect(id1 != id2); + } + + test { + var cache = GlyphCache.init(); + for (0..(GlyphSlot.limit-1)) |i| { + const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = 1, .scale_y = 1 }); + try std.testing.expect(id != null); + } + const id = cache.insert(Glyph.Key{ .index = GlyphSlot.limit, .scale_x = 1, .scale_y = 1 }); + try std.testing.expect(id == null); + } + + test { + const gpa = std.testing.allocator; + var cache = GlyphCache.init(); + for (0..10) |_| { + var ids: std.ArrayList(GlyphSlot.Id) = .empty; + defer ids.deinit(gpa); + + for (0..(GlyphSlot.limit-1)) |i| { + const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = 1, .scale_y = 1 }); + try std.testing.expect(id != null); + try ids.append(gpa, id.?); + } + assert(cache.free_head == GlyphSlot.nil_id); + + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + + const expected_lru = try gpa.dupe(GlyphSlot.Id, ids.items); + defer gpa.free(expected_lru); + std.mem.reverse(GlyphSlot.Id, expected_lru); + + try std.testing.expectEqualSlices(GlyphSlot.Id, expected_lru, lru); + + for (ids.items) |id| { + cache.remove(id); + } + assert(cache.lru_head == GlyphSlot.nil_id); + assert(cache.lru_tail == GlyphSlot.nil_id); + } + } + + test { + var cache = GlyphCache.init(); + const id1 = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; + const id2 = cache.insert(.{ .index = 1, .scale_x = 1, .scale_y = 1 }).?; + const id3 = cache.insert(.{ .index = 2, .scale_x = 1, .scale_y = 1 }).?; + + const gpa = std.testing.allocator; + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); + } + + test { + var cache = GlyphCache.init(); + const id1 = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; + const id2 = cache.insert(.{ .index = 1, .scale_x = 1, .scale_y = 1 }).?; + const id3 = cache.insert(.{ .index = 2, .scale_x = 1, .scale_y = 1 }).?; + + const gpa = std.testing.allocator; + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); + } + + test { + var cache = GlyphCache.init(); + const id = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; + try std.testing.expect(cache.lru_head != GlyphSlot.nil_id); + try std.testing.expect(cache.lru_tail != GlyphSlot.nil_id); + cache.remove(id); + try std.testing.expect(cache.lru_head == GlyphSlot.nil_id); + try std.testing.expect(cache.lru_tail == GlyphSlot.nil_id); + } + + test { + const gpa = std.testing.allocator; + var cache = GlyphCache.init(); + const key1 = Glyph.Key{ .index = 0, .scale_x = 1, .scale_y = 1 }; + const id1 = cache.insert(key1).?; + const key2 = Glyph.Key{ .index = 1, .scale_x = 1, .scale_y = 1 }; + const id2 = cache.insert(key2).?; + const key3 = Glyph.Key{ .index = 2, .scale_x = 1, .scale_y = 1 }; + const id3 = cache.insert(key3).?; + + _ = cache.lookup(key2); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id2, id3, id1 }, lru); + } + + _ = cache.lookup(key1); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1, id2, id3 }, lru); + } + } + + test { + const gpa = std.testing.allocator; + var cache = GlyphCache.init(); + const id1 = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; + const id2 = cache.insert(.{ .index = 1, .scale_x = 1, .scale_y = 1 }).?; + const id3 = cache.insert(.{ .index = 2, .scale_x = 1, .scale_y = 1 }).?; + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); + } + + _ = cache.remove(id2); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id1 }, lru); + } + + _ = cache.remove(id3); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1 }, lru); + } + + _ = cache.remove(id1); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ }, lru); + } + } + }; + + const GlyphSlot = struct { + glyph: Glyph, + + lru_next: GlyphSlot.Id, + lru_prev: GlyphSlot.Id, + + next_free: GlyphSlot.Id, + + const limit = 256; + const Id = std.math.IntFittingRange(0, limit-1); + const nil_id: GlyphSlot.Id = 0; + }; + + const Glyph = struct { + key: Key, + + box: STBTrueType.Box, + sprite: Sprite.Id, + used_this_frame: bool, + + const max_width: u32 = 512; + const max_height: u32 = 512; + + const Key = packed struct { + index: u32, + scale_x: f32, + scale_y: f32, + + pub fn hash(self: Key) u64 { + var hasher = std.hash.Wyhash.init(0); + hasher.update(std.mem.asBytes(&self)); + return hasher.final(); + } + }; + + fn init(key: Key) Glyph { + return Glyph{ + .key = key, + .box = .{ + .x0 = 0, + .y0 = 0, + .x1 = 0, + .y1 = 0, + }, + .sprite = g_state.nil_sprite, + .used_this_frame = false + }; + } + + fn deinit(self: Glyph) void { + deinitSprite(self.sprite); + } + }; +}; + pub const Vertex = extern struct { position: Vec2, texcoord: Vec2, diff --git a/src/platform.zig b/src/platform.zig index 2157113..cbde187 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -124,7 +124,9 @@ fn PlatformType(App: type) type { try self.app.frame(plt); } - Gfx.flush(); + Gfx.flush(.{}); + Gfx.showDebug(); + ImGUI.endFrame(); Gfx.endFrame(); } @@ -434,15 +436,17 @@ pub const Assets = struct { }; } - fn readFileFromAssetDir(self: *Assets, filename: []const u8) !?[]const u8 { + fn readFileFromAssetDir(self: *Assets, filename: []const u8) !?[:0]const u8 { const dir = self.dir orelse return null; const arena = self.arena.allocator(); - const contents = try dir.readFileAlloc( + const contents = try dir.readFileAllocOptions( self.io, filename, arena, - .unlimited + .unlimited, + .of(u8), + 0 ); const filename_owned = try arena.dupe(u8, filename); @@ -453,13 +457,13 @@ pub const Assets = struct { return contents; } - pub fn readFile(self: *Assets, filename: []const u8) []const u8 { + pub fn readFile(self: *Assets, filename: []const u8) [:0]const u8 { if (self.files.get(filename)) |file| { return file.contents; } else { if (self.readFileFromAssetDir(filename)) |maybe_contents| { if (maybe_contents) |contents| { - log.warn("Read file from which isn't defined in assets: '{s}'", .{filename}); + log.warn("Read file which isn't defined in assets: '{s}'", .{filename}); return contents; } else { log.err("Attempt to read file '{s}', which doesn't exist", .{filename}); @@ -472,7 +476,7 @@ pub const Assets = struct { } } - fn insertStaticFile(self: *Assets, filename: []const u8, contents: []const u8) !void { + fn insertStaticFile(self: *Assets, filename: []const u8, contents: [:0]const u8) !void { try self.files.put(self.gpa, filename, File{ .contents = contents }); @@ -484,7 +488,7 @@ pub const Assets = struct { } const File = struct { - contents: []const u8 + contents: [:0]const u8 }; }; diff --git a/src/shader.glsl b/src/shader.glsl index 5e966ee..3dc847b 100644 --- a/src/shader.glsl +++ b/src/shader.glsl @@ -29,8 +29,20 @@ in vec4 color; in vec4 uv; out vec4 frag_color; +layout(binding = 1) uniform fs_params { + // Textures modes: + // 0 - rgba8 + // 1 - r8 - use red channel as opacity, used for fonts. + int texture_mode; +}; + void main() { - frag_color = texture(sampler2D(tex, smp), uv.xy) * color; + vec4 tex_color = texture(sampler2D(tex, smp), uv.xy); + if (texture_mode == 0) { + frag_color = tex_color * color; + } else if (texture_mode == 1) { + frag_color = vec4(1, 1, 1, tex_color.r) * color; + } } @end diff --git a/src/slot_map.zig b/src/slot_map.zig index 400ff6f..5eb1cd2 100644 --- a/src/slot_map.zig +++ b/src/slot_map.zig @@ -78,7 +78,7 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { }; pub fn clearRetainingCapacity(self: *Self) void { - const slots = self.slots; + var slots = self.slots; slots.clearRetainingCapacity(); self.* = .init(slots);