split engine to separate library

This commit is contained in:
Rokas Puzonas 2026-02-07 21:22:14 +02:00
parent 8434ea6a64
commit e5fccc21f2
30 changed files with 639 additions and 495 deletions

299
build.zig
View File

@ -1,161 +1,32 @@
const std = @import("std");
const sokol = @import("sokol");
const builtin = @import("builtin");
const Engine = @import("engine");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const has_imgui = b.option(bool, "imgui", "ImGui integration") orelse (optimize == .Debug);
var has_tracy = b.option(bool, "tracy", "Tracy integration") orelse (optimize == .Debug);
const has_console = b.option(bool, "console", "Show console (Window only)") orelse (optimize == .Debug);
const isWasm = target.result.cpu.arch.isWasm();
if (isWasm) {
has_tracy = false;
}
const mod_main = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true
const mod = b.createModule(.{
.root_source_file = b.path("src/game.zig"),
});
const dep_sokol = b.dependency("sokol", .{
.target = target,
.optimize = optimize,
.with_sokol_imgui = has_imgui,
});
mod_main.linkLibrary(dep_sokol.artifact("sokol_clib"));
mod_main.addImport("sokol", dep_sokol.module("sokol"));
if (has_imgui) {
if (b.lazyDependency("cimgui", .{
.target = target,
.optimize = optimize,
})) |dep_cimgui| {
const cimgui = b.lazyImport(@This(), "cimgui").?;
const cimgui_conf = cimgui.getConfig(false);
mod_main.addImport("cimgui", dep_cimgui.module(cimgui_conf.module_name));
dep_sokol.artifact("sokol_clib").addIncludePath(dep_cimgui.path(cimgui_conf.include_dir));
}
}
const dep_tracy = b.dependency("tracy", .{
.target = target,
.optimize = optimize,
.tracy_enable = has_tracy,
.tracy_only_localhost = true
});
if (has_tracy) {
mod_main.linkLibrary(dep_tracy.artifact("tracy"));
}
mod_main.addImport("tracy", dep_tracy.module("tracy"));
const dep_tiled = b.dependency("tiled", .{});
mod_main.addImport("tiled", dep_tiled.module("tiled"));
mod.addImport("tiled", dep_tiled.module("tiled"));
const dep_stb = b.dependency("stb", .{});
mod_main.addImport("stb_image", dep_stb.module("stb_image"));
mod_main.addImport("stb_vorbis", dep_stb.module("stb_vorbis"));
const dep_fontstash_c = b.dependency("fontstash_c", .{});
mod_main.addIncludePath(dep_fontstash_c.path("src"));
const dep_sokol_c = b.dependency("sokol_c", .{});
{
var cflags_buffer: [64][]const u8 = undefined;
var cflags = std.ArrayListUnmanaged([]const u8).initBuffer(&cflags_buffer);
switch (sokol.resolveSokolBackend(.auto, target.result)) {
.d3d11 => try cflags.appendBounded("-DSOKOL_D3D11"),
.metal => try cflags.appendBounded("-DSOKOL_METAL"),
.gl => try cflags.appendBounded("-DSOKOL_GLCORE"),
.gles3 => try cflags.appendBounded("-DSOKOL_GLES3"),
.wgpu => try cflags.appendBounded("-DSOKOL_WGPU"),
else => @panic("unknown sokol backend"),
}
mod_main.addIncludePath(dep_sokol_c.path("util"));
mod_main.addCSourceFile(.{
.file = b.path("src/engine/fontstash/sokol_fontstash_impl.c"),
.flags = cflags.items
});
}
// TODO:
// const sdl = b.dependency("sdl", .{
// .optimize = optimize,
// .target = target,
// .linkage = .static,
// .default_target_config = !isWasm
// });
// mod_main.linkLibrary(sdl.artifact("SDL3"));
// if (isWasm) {
// // TODO: Define buid config for wasm
// }
var options = b.addOptions();
options.addOption(bool, "has_imgui", has_imgui);
options.addOption(bool, "has_tracy", has_tracy);
mod_main.addOptions("build_options", options);
// from here on different handling for native vs wasm builds
if (target.result.cpu.arch.isWasm()) {
try buildWasm(b, .{
.name = "sokol_template",
.mod_main = mod_main,
.dep_sokol = dep_sokol,
});
} else {
try buildNative(b, "sokol_template", mod_main, has_console);
}
}
fn buildNative(b: *std.Build, name: []const u8, mod: *std.Build.Module, has_console: bool) !void {
const exe = b.addExecutable(.{
.name = name,
.root_module = mod
Engine.init(b, .{
.name = "sokol_template",
.root_module = mod,
.target = target,
.optimize = optimize,
.dep_engine = b.dependency("engine", .{}),
.has_imgui = b.option(bool, "imgui", "ImGui integration"),
.has_tracy = b.option(bool, "tracy", "Tracy integration"),
.win32_has_console = b.option(bool, "console", "Show console (Window only)"),
.win32_png_icon = b.path("src/assets/icon.png")
});
const target = mod.resolved_target.?;
if (target.result.os.tag == .windows) {
exe.subsystem = if (has_console) .Console else .Windows;
const png_to_icon_tool = b.addExecutable(.{
.name = "png-to-icon",
.root_module = b.createModule(.{
.target = b.graph.host,
.root_source_file = b.path("tools/png-to-icon.zig"),
}),
});
const dep_stb_image = b.dependency("stb_image", .{});
png_to_icon_tool.root_module.addImport("stb_image", dep_stb_image.module("stb_image"));
const png_to_icon_step = b.addRunArtifact(png_to_icon_tool);
png_to_icon_step.addFileArg(b.path("src/assets/icon.png"));
const icon_file = png_to_icon_step.addOutputFileArg("icon.ico");
const add_icon_step = AddExecutableIcon.init(exe, icon_file);
exe.step.dependOn(&add_icon_step.step);
}
b.installArtifact(exe);
{
const run_step = b.step("run", "Run game");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
}
{
mod.resolved_target = target;
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
.root_module = mod,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
@ -164,141 +35,3 @@ fn buildNative(b: *std.Build, name: []const u8, mod: *std.Build.Module, has_cons
test_step.dependOn(&run_exe_tests.step);
}
}
const BuildWasmOptions = struct {
name: []const u8,
mod_main: *std.Build.Module,
dep_sokol: *std.Build.Dependency,
};
fn patchWasmIncludeDirs(
module: *std.Build.Module,
path: std.Build.LazyPath,
depend_step: *std.Build.Step
) void {
if (module.link_libc != null and module.link_libc.?) {
// need to inject the Emscripten system header include path into
// the cimgui C library otherwise the C/C++ code won't find
// C stdlib headers
module.addSystemIncludePath(path);
}
for (module.import_table.values()) |imported_module| {
patchWasmIncludeDirs(imported_module, path, depend_step);
}
for (module.link_objects.items) |link_object| {
if (link_object != .other_step) {
continue;
}
const lib = link_object.other_step;
if (&lib.step == depend_step) {
continue;
}
if (lib.root_module.link_libc != null and lib.root_module.link_libc.?) {
// need to inject the Emscripten system header include path into
// the cimgui C library otherwise the C/C++ code won't find
// C stdlib headers
lib.root_module.addSystemIncludePath(path);
// all C libraries need to depend on the sokol library, when building for
// WASM this makes sure that the Emscripten SDK has been setup before
// C compilation is attempted (since the sokol C library depends on the
// Emscripten SDK setup step)
lib.step.dependOn(depend_step);
}
patchWasmIncludeDirs(lib.root_module, path, depend_step);
}
}
fn buildWasm(b: *std.Build, opts: BuildWasmOptions) !void {
opts.mod_main.sanitize_c = .off;
// build the main file into a library, this is because the WASM 'exe'
// needs to be linked in a separate build step with the Emscripten linker
const main_lib = b.addLibrary(.{
.name = "index",
.root_module = opts.mod_main,
});
const dep_emsdk = opts.dep_sokol.builder.dependency("emsdk", .{});
patchWasmIncludeDirs(
opts.mod_main,
dep_emsdk.path("upstream/emscripten/cache/sysroot/include"),
&(opts.dep_sokol.artifact("sokol_clib").step)
);
// create a build step which invokes the Emscripten linker
const link_step = try sokol.emLinkStep(b, .{
.lib_main = main_lib,
.target = opts.mod_main.resolved_target.?,
.optimize = opts.mod_main.optimize.?,
.emsdk = dep_emsdk,
.use_webgl2 = true,
.use_emmalloc = true,
.use_filesystem = false,
.shell_file_path = b.path("src/engine/shell.html"),
});
// attach to default target
b.getInstallStep().dependOn(&link_step.step);
// ...and a special run step to start the web build output via 'emrun'
const run = sokol.emRunStep(b, .{ .name = "index", .emsdk = dep_emsdk });
run.step.dependOn(&link_step.step);
b.step("run", "Run game").dependOn(&run.step);
// TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload
}
const AddExecutableIcon = struct {
obj: *std.Build.Step.Compile,
step: std.Build.Step,
icon_file: std.Build.LazyPath,
resource_file: std.Build.LazyPath,
fn init(obj: *std.Build.Step.Compile, icon_file: std.Build.LazyPath) *AddExecutableIcon {
const b = obj.step.owner;
const self = b.allocator.create(AddExecutableIcon) catch @panic("OOM");
self.obj = obj;
self.step = std.Build.Step.init(.{
.id = .custom,
.name = "add executable icon",
.owner = b,
.makeFn = make
});
self.icon_file = icon_file;
icon_file.addStepDependencies(&self.step);
const write_files = b.addWriteFiles();
self.resource_file = write_files.add("resource-file.rc", "");
self.step.dependOn(&write_files.step);
self.obj.addWin32ResourceFile(.{
.file = self.resource_file,
});
return self;
}
fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
const b = step.owner;
const self: *AddExecutableIcon = @fieldParentPtr("step", step);
const resource_file = try std.fs.cwd().createFile(self.resource_file.getPath(b), .{ });
defer resource_file.close();
const relative_icon_path = try std.fs.path.relative(
b.allocator,
self.resource_file.dirname().getPath(b),
self.icon_file.getPath(b)
);
std.mem.replaceScalar(u8, relative_icon_path, '\\', '/');
try resource_file.writeAll("IDI_ICON ICON \"");
try resource_file.writeAll(relative_icon_path);
try resource_file.writeAll("\"");
}
};

View File

@ -4,37 +4,12 @@
.fingerprint = 0x60a8e079a691c8d9, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.dependencies = .{
.sokol = .{
.url = "git+https://github.com/floooh/sokol-zig.git#aaf291ca2d3d1cedc05d65f5a1cacae0f53d934a",
.hash = "sokol-0.1.0-pb1HK4iDNgCom5dkY66eUBm_bYBHEl8KWFDAiqwWgpEy",
},
.sokol_c = .{
.url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492",
.hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN",
},
.fontstash_c = .{
.url = "git+https://github.com/memononen/fontstash.git#b5ddc9741061343740d85d636d782ed3e07cf7be",
.hash = "N-V-__8AAA9xHgAxdLYPmlNTy6qzv9IYqiIePEHQUOPWYQ_6",
},
.cimgui = .{
.url = "git+https://github.com/floooh/dcimgui.git#33c99ef426b68030412b5a4b11487a23da9d4f13",
.hash = "cimgui-0.1.0-44ClkQRJlABdFMKRqIG8KDD6jy1eQbgPO335NziPYjmL",
.lazy = true,
},
.tracy = .{
.url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669",
.hash = "zig_tracy-0.0.5-aOIqsX1tAACKaRRB-sraMLuNiMASXi_y-4FtRuw4cTpx",
},
.tiled = .{
.path = "./libs/tiled",
},
.stb = .{
.path = "./libs/stb",
.engine = .{
.path = "./engine/",
},
// .sdl = .{
// .url = "git+https://github.com/allyourcodebase/SDL3.git#f85824b0db782b7d01c60aaad8bcb537892394e8",
// .hash = "sdl-0.0.0-i4QD0UuFqADRQysNyJ1OvCOZnq-clcVhq3BfPcBOf9zr",
// },
},
.paths = .{
"build.zig",

385
engine/build.zig Normal file
View File

@ -0,0 +1,385 @@
const std = @import("std");
const sokol = @import("sokol");
pub fn build(b: *std.Build) !void {
_ = b; // autofix
}
const InitOptions = struct {
name: []const u8,
root_module: *std.Build.Module,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
dep_engine: *std.Build.Dependency,
has_imgui: ?bool = null,
has_tracy: ?bool = null,
win32_has_console: ?bool = null,
win32_png_icon: ?std.Build.LazyPath = null
};
pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
const b = opts.dep_engine.builder;
const engine_module = createEngineModule(b, .{
.target = opts.target,
.optimize = opts.optimize,
.has_tracy = opts.has_tracy,
.has_imgui = opts.has_imgui
});
opts.root_module.resolved_target = opts.target;
opts.root_module.optimize = opts.optimize;
const game_lib = b.addLibrary(.{
.name = "game",
.root_module = opts.root_module,
.linkage = .static,
});
opts.root_module.addImport("engine", engine_module.root_module);
const mod_main = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = opts.target,
.optimize = opts.optimize,
.link_libc = true
});
mod_main.addImport("engine", engine_module.root_module);
mod_main.linkLibrary(game_lib);
var run_cmd_step: *std.Build.Step = undefined;
if (opts.target.result.cpu.arch.isWasm()) {
const build_step = buildWasm(b, .{
.name = "index",
.root_module = mod_main,
.dep_sokol = engine_module.dep_sokol,
.outer_builder = outer_builder
});
outer_builder.getInstallStep().dependOn(build_step);
const dep_sokol = engine_module.dep_sokol;
const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
const emrun_step = sokol.emRunStep(b, .{ .name = "index", .emsdk = dep_emsdk });
emrun_step.step.dependOn(build_step);
run_cmd_step = &emrun_step.step;
// TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload
} else {
const exe = buildNative(b, .{
.name = opts.name,
.root_module = mod_main,
.win32_has_console = opts.win32_has_console,
.win32_png_icon = opts.win32_png_icon
});
outer_builder.installArtifact(exe);
{
const run_cmd = outer_builder.addRunArtifact(exe);
if (outer_builder.args) |args| {
run_cmd.addArgs(args);
}
run_cmd_step = &run_cmd.step;
}
}
const run_step = outer_builder.step("run", "Run game");
run_step.dependOn(run_cmd_step);
}
const EngineModule = struct {
root_module: *std.Build.Module,
dep_sokol: *std.Build.Dependency,
const Options = struct {
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
has_imgui: ?bool = null,
has_tracy: ?bool = null,
};
};
fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
const has_imgui = opts.has_imgui orelse (opts.optimize == .Debug);
var has_tracy = opts.has_tracy orelse (opts.optimize == .Debug);
const isWasm = opts.target.result.cpu.arch.isWasm();
if (isWasm) {
has_tracy = false;
}
const mod = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
.target = opts.target,
.optimize = opts.optimize,
.link_libc = true
});
const dep_sokol = b.dependency("sokol", .{
.target = opts.target,
.optimize = opts.optimize,
.with_sokol_imgui = has_imgui,
});
mod.linkLibrary(dep_sokol.artifact("sokol_clib"));
mod.addImport("sokol", dep_sokol.module("sokol"));
if (has_imgui) {
if (b.lazyDependency("cimgui", .{
.target = opts.target,
.optimize = opts.optimize,
})) |dep_cimgui| {
const cimgui = b.lazyImport(@This(), "cimgui").?;
const cimgui_conf = cimgui.getConfig(false);
mod.addImport("cimgui", dep_cimgui.module(cimgui_conf.module_name));
dep_sokol.artifact("sokol_clib").addIncludePath(dep_cimgui.path(cimgui_conf.include_dir));
}
}
const dep_tracy = b.dependency("tracy", .{
.target = opts.target,
.optimize = opts.optimize,
.tracy_enable = has_tracy,
.tracy_only_localhost = true
});
if (has_tracy) {
mod.linkLibrary(dep_tracy.artifact("tracy"));
}
mod.addImport("tracy", dep_tracy.module("tracy"));
const dep_stb = b.dependency("stb", .{});
mod.addImport("stb_image", dep_stb.module("stb_image"));
mod.addImport("stb_vorbis", dep_stb.module("stb_vorbis"));
const dep_fontstash_c = b.dependency("fontstash_c", .{});
mod.addIncludePath(dep_fontstash_c.path("src"));
const dep_sokol_c = b.dependency("sokol_c", .{});
{
var cflags_buffer: [64][]const u8 = undefined;
var cflags = std.ArrayListUnmanaged([]const u8).initBuffer(&cflags_buffer);
switch (sokol.resolveSokolBackend(.auto, opts.target.result)) {
.d3d11 => cflags.appendAssumeCapacity("-DSOKOL_D3D11"),
.metal => cflags.appendAssumeCapacity("-DSOKOL_METAL"),
.gl => cflags.appendAssumeCapacity("-DSOKOL_GLCORE"),
.gles3 => cflags.appendAssumeCapacity("-DSOKOL_GLES3"),
.wgpu => cflags.appendAssumeCapacity("-DSOKOL_WGPU"),
else => @panic("unknown sokol backend"),
}
mod.addIncludePath(dep_sokol_c.path("util"));
mod.addCSourceFile(.{
.file = b.path("src/fontstash/sokol_fontstash_impl.c"),
.flags = cflags.items
});
}
// TODO:
// const sdl = b.dependency("sdl", Run game.{
// .optimize = optimize,
// .target = target,
// .linkage = .static,
// .default_target_config = !isWasm
// });
// mod_main.linkLibrary(sdl.artifact("SDL3"));
// if (isWasm) {
// // TODO: Define buid config for wasm
// }
var options = b.addOptions();
options.addOption(bool, "has_imgui", has_imgui);
options.addOption(bool, "has_tracy", has_tracy);
mod.addOptions("build_options", options);
return EngineModule{
.root_module = mod,
.dep_sokol = dep_sokol
};
}
fn buildPngToIconTool(
b: *std.Build,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
) *std.Build.Step.Compile {
const mod = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("tools/png-to-icon.zig"),
});
const dep_stb = b.dependency("stb", .{});
mod.addImport("stb_image", dep_stb.module("stb_image"));
return b.addExecutable(.{
.name = "png-to-icon",
.root_module = mod,
});
}
const BuildNativeOptions = struct {
name: []const u8,
root_module: *std.Build.Module,
win32_has_console: ?bool = null,
win32_png_icon: ?std.Build.LazyPath = null
};
fn buildNative(b: *std.Build, opts: BuildNativeOptions) *std.Build.Step.Compile {
const exe = b.addExecutable(.{
.name = opts.name,
.root_module = opts.root_module
});
const target = opts.root_module.resolved_target.?;
if (target.result.os.tag == .windows) {
const optimize = opts.root_module.optimize.?;
const has_console = opts.win32_has_console orelse (optimize == .Debug);
exe.subsystem = if (has_console) .Console else .Windows;
if (opts.win32_png_icon) |win32_png_icon| {
const png_to_icon_tool = buildPngToIconTool(b, b.graph.host, .ReleaseSafe);
const png_to_icon_step = b.addRunArtifact(png_to_icon_tool);
png_to_icon_step.addFileArg(win32_png_icon);
const icon_file = png_to_icon_step.addOutputFileArg("icon.ico");
const add_icon_step = AddExecutableIcon.init(exe, icon_file);
exe.step.dependOn(&add_icon_step.step);
}
}
return exe;
}
const BuildWasmOptions = struct {
name: []const u8,
root_module: *std.Build.Module,
dep_sokol: *std.Build.Dependency,
outer_builder: *std.Build
};
fn buildWasm(b: *std.Build, opts: BuildWasmOptions) *std.Build.Step {
opts.root_module.sanitize_c = .off;
// build the main file into a library, this is because the WASM 'exe'
// needs to be linked in a separate build step with the Emscripten linker
const main_lib = b.addLibrary(.{
.name = opts.name,
.root_module = opts.root_module,
});
const dep_sokol = opts.dep_sokol;
const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
patchWasmIncludeDirs(
opts.root_module,
dep_emsdk.path("upstream/emscripten/cache/sysroot/include"),
&(dep_sokol.artifact("sokol_clib").step)
);
const link_step = sokol.emLinkStep(opts.outer_builder, .{
.lib_main = main_lib,
.target = opts.root_module.resolved_target.?,
.optimize = opts.root_module.optimize.?,
.emsdk = dep_emsdk,
.use_webgl2 = true,
.use_emmalloc = true,
.use_filesystem = false,
.shell_file_path = b.path("src/shell.html"),
}) catch unreachable;
return &link_step.step;
}
fn patchWasmIncludeDirs(
module: *std.Build.Module,
path: std.Build.LazyPath,
depend_step: *std.Build.Step
) void {
if (module.link_libc != null and module.link_libc.?) {
// need to inject the Emscripten system header include path into
// the cimgui C library otherwise the C/C++ code won't find
// C stdlib headers
module.addSystemIncludePath(path);
}
for (module.import_table.values()) |imported_module| {
patchWasmIncludeDirs(imported_module, path, depend_step);
}
for (module.link_objects.items) |link_object| {
if (link_object != .other_step) {
continue;
}
const lib = link_object.other_step;
if (&lib.step == depend_step) {
continue;
}
if (lib.root_module.link_libc != null and lib.root_module.link_libc.?) {
// need to inject the Emscripten system header include path into
// the cimgui C library otherwise the C/C++ code won't find
// C stdlib headers
lib.root_module.addSystemIncludePath(path);
// all C libraries need to depend on the sokol library, when building for
// WASM this makes sure that the Emscripten SDK has been setup before
// C compilation is attempted (since the sokol C library depends on the
// Emscripten SDK setup step)
lib.step.dependOn(depend_step);
}
patchWasmIncludeDirs(lib.root_module, path, depend_step);
}
}
const AddExecutableIcon = struct {
obj: *std.Build.Step.Compile,
step: std.Build.Step,
icon_file: std.Build.LazyPath,
resource_file: std.Build.LazyPath,
fn init(obj: *std.Build.Step.Compile, icon_file: std.Build.LazyPath) *AddExecutableIcon {
const b = obj.step.owner;
const self = b.allocator.create(AddExecutableIcon) catch @panic("OOM");
self.obj = obj;
self.step = std.Build.Step.init(.{
.id = .custom,
.name = "add executable icon",
.owner = b,
.makeFn = make
});
self.icon_file = icon_file;
icon_file.addStepDependencies(&self.step);
const write_files = b.addWriteFiles();
self.resource_file = write_files.add("resource-file.rc", "");
self.step.dependOn(&write_files.step);
self.obj.addWin32ResourceFile(.{
.file = self.resource_file,
});
return self;
}
fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
const b = step.owner;
const self: *AddExecutableIcon = @fieldParentPtr("step", step);
const resource_file = try std.fs.cwd().createFile(self.resource_file.getPath(b), .{ });
defer resource_file.close();
const relative_icon_path = try std.fs.path.relative(
b.allocator,
self.resource_file.dirname().getPath(b),
self.icon_file.getPath(b)
);
std.mem.replaceScalar(u8, relative_icon_path, '\\', '/');
try resource_file.writeAll("IDI_ICON ICON \"");
try resource_file.writeAll(relative_icon_path);
try resource_file.writeAll("\"");
}
};

42
engine/build.zig.zon Normal file
View File

@ -0,0 +1,42 @@
.{
.name = .engine,
.version = "0.0.0",
.fingerprint = 0xe8a81a8d6fb6f0fb,
.minimum_zig_version = "0.15.2",
.dependencies = .{
.sokol = .{
.url = "git+https://github.com/floooh/sokol-zig.git#aaf291ca2d3d1cedc05d65f5a1cacae0f53d934a",
.hash = "sokol-0.1.0-pb1HK4iDNgCom5dkY66eUBm_bYBHEl8KWFDAiqwWgpEy",
},
.sokol_c = .{
.url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492",
.hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN",
},
.fontstash_c = .{
.url = "git+https://github.com/memononen/fontstash.git#b5ddc9741061343740d85d636d782ed3e07cf7be",
.hash = "N-V-__8AAA9xHgAxdLYPmlNTy6qzv9IYqiIePEHQUOPWYQ_6",
},
.cimgui = .{
.url = "git+https://github.com/floooh/dcimgui.git#33c99ef426b68030412b5a4b11487a23da9d4f13",
.hash = "cimgui-0.1.0-44ClkQRJlABdFMKRqIG8KDD6jy1eQbgPO335NziPYjmL",
.lazy = true,
},
.tracy = .{
.url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669",
.hash = "zig_tracy-0.0.5-aOIqsX1tAACKaRRB-sraMLuNiMASXi_y-4FtRuw4cTpx",
},
.stb = .{
.path = "./libs/stb",
},
// .sdl = .{
// .url = "git+https://github.com/allyourcodebase/SDL3.git#f85824b0db782b7d01c60aaad8bcb537892394e8",
// .hash = "sdl-0.0.0-i4QD0UuFqADRQysNyJ1OvCOZnq-clcVhq3BfPcBOf9zr",
// },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"tools",
},
}

View File

@ -23,6 +23,8 @@ pub const Font = fontstash.Font;
const GraphicsFrame = @import("./frame.zig").Graphics;
const Graphics = @This();
// TODO: Seems that there is a vertical jitter bug when resizing a window in OpenGL. Seems like a driver bug.
// From other peoples research it seems that disabling vsync when a resize event occurs fixes it.
// Maybe a patch for sokol could be made?
@ -97,20 +99,16 @@ pub const Sprite = struct {
uv: Rect,
};
var gpa: std.mem.Allocator = undefined;
main_pipeline: sgl.Pipeline,
linear_sampler: sg.Sampler,
nearest_sampler: sg.Sampler,
font_context: fontstash.Context,
textures: std.ArrayList(Texture) = .empty,
var main_pipeline: sgl.Pipeline = .{};
var linear_sampler: sg.Sampler = .{};
var nearest_sampler: sg.Sampler = .{};
var font_context: fontstash.Context = undefined;
var textures: std.ArrayList(Texture) = .empty;
var scale_stack_buffer: [32]Vec2 = undefined;
var scale_stack: std.ArrayList(Vec2) = .empty;
pub fn init(options: Options) !void {
gpa = options.allocator;
scale_stack_buffer: [32]Vec2 = undefined,
scale_stack: std.ArrayList(Vec2) = .empty,
pub fn init(self: *Graphics, options: Options) !void {
sg.setup(.{
.logger = options.logger,
.environment = sglue.environment(),
@ -123,7 +121,7 @@ pub fn init(options: Options) !void {
}
});
main_pipeline = sgl.makePipeline(.{
const main_pipeline = sgl.makePipeline(.{
.colors = init: {
var colors: [8]sg.ColorTargetState = @splat(.{});
colors[0] = .{
@ -158,14 +156,14 @@ pub fn init(options: Options) !void {
imgui.addFont(imgui_font.ttf_data, imgui_font.size);
}
linear_sampler = sg.makeSampler(.{
const linear_sampler = sg.makeSampler(.{
.min_filter = .LINEAR,
.mag_filter = .LINEAR,
.mipmap_filter = .LINEAR,
.label = "linear-sampler",
});
nearest_sampler = sg.makeSampler(.{
const nearest_sampler = sg.makeSampler(.{
.min_filter = .NEAREST,
.mag_filter = .NEAREST,
.mipmap_filter = .NEAREST,
@ -175,31 +173,38 @@ pub fn init(options: Options) !void {
const dpi_scale = sapp.dpiScale();
const atlas_size = 512;
const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(atlas_size * dpi_scale));
font_context = try fontstash.Context.init(.{
const font_context = try fontstash.Context.init(.{
.width = @intCast(atlas_dim),
.height = @intCast(atlas_dim),
});
self.* = Graphics{
.main_pipeline = main_pipeline,
.linear_sampler = linear_sampler,
.nearest_sampler = nearest_sampler,
.font_context = font_context,
};
}
pub fn deinit() void {
textures.deinit(gpa);
pub fn deinit(self: *Graphics, gpa: std.mem.Allocator) void {
self.textures.deinit(gpa);
imgui.shutdown();
font_context.deinit();
self.font_context.deinit();
sgl.shutdown();
sg.shutdown();
}
pub fn drawCommand(command: Command) void {
pub fn drawCommand(self: *Graphics, command: Command) void {
switch(command) {
.push_transformation => |opts| {
pushTransform(opts.translation, opts.scale);
self.pushTransform(opts.translation, opts.scale);
// font_resolution_scale = font_resolution_scale.multiply(opts.scale);
},
.pop_transformation => {
popTransform();
self.popTransform();
},
.draw_rectangle => |opts| {
drawRectangle(opts);
self.drawRectangle(opts);
},
.set_scissor => |opts| {
sgl.scissorRectf(
@ -219,25 +224,25 @@ pub fn drawCommand(command: Command) void {
);
},
.draw_text => |opts| {
const font_resolution_scale = scale_stack.getLast();
const font_resolution_scale = self.scale_stack.getLast();
sgl.pushMatrix();
defer sgl.popMatrix();
sgl.scale(1/font_resolution_scale.x, 1/font_resolution_scale.y, 1);
font_context.setFont(opts.font);
font_context.setSize(opts.size * font_resolution_scale.y);
font_context.setAlign(.{ .x = .left, .y = .top });
font_context.setSpacing(0);
self.font_context.setFont(opts.font);
self.font_context.setSize(opts.size * font_resolution_scale.y);
self.font_context.setAlign(.{ .x = .left, .y = .top });
self.font_context.setSpacing(0);
const r: u8 = @intFromFloat(opts.color.x * 255);
const g: u8 = @intFromFloat(opts.color.y * 255);
const b: u8 = @intFromFloat(opts.color.z * 255);
const a: u8 = @intFromFloat(opts.color.w * 255);
const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24);
font_context.setColor(color);
font_context.drawText(
self.font_context.setColor(color);
self.font_context.drawText(
opts.pos.x * font_resolution_scale.x,
opts.pos.y * font_resolution_scale.y,
opts.text
@ -246,13 +251,13 @@ pub fn drawCommand(command: Command) void {
}
}
pub fn drawCommands(commands: []const Command) void {
pub fn drawCommands(self: *Graphics, commands: []const Command) void {
for (commands) |command| {
drawCommand(command);
self.drawCommand(command);
}
}
pub fn beginFrame() void {
pub fn beginFrame(self: *Graphics) void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
@ -263,17 +268,17 @@ pub fn beginFrame() void {
.dpi_scale = sapp.dpiScale()
});
scale_stack = .initBuffer(&scale_stack_buffer);
scale_stack.appendAssumeCapacity(.init(1, 1));
self.scale_stack = .initBuffer(&self.scale_stack_buffer);
self.scale_stack.appendAssumeCapacity(.init(1, 1));
font_context.clearState();
self.font_context.clearState();
sgl.defaults();
sgl.matrixModeProjection();
sgl.ortho(0, sapp.widthf(), sapp.heightf(), 0, -1, 1);
sgl.loadPipeline(main_pipeline);
sgl.loadPipeline(self.main_pipeline);
}
pub fn endFrame(clear_color: Vec4) void {
pub fn endFrame(self: *Graphics, clear_color: Vec4) void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
@ -289,7 +294,7 @@ pub fn endFrame(clear_color: Vec4) void {
}
};
font_context.flush();
self.font_context.flush();
{
sg.beginPass(.{
@ -305,17 +310,17 @@ pub fn endFrame(clear_color: Vec4) void {
sg.commit();
}
fn pushTransform(translation: Vec2, scale: Vec2) void {
fn pushTransform(self: *Graphics, translation: Vec2, scale: Vec2) void {
sgl.pushMatrix();
sgl.translate(translation.x, translation.y, 0);
sgl.scale(scale.x, scale.y, 1);
scale_stack.appendAssumeCapacity(scale_stack.getLast().multiply(scale));
self.scale_stack.appendAssumeCapacity(self.scale_stack.getLast().multiply(scale));
}
fn popTransform() void {
fn popTransform(self: *Graphics) void {
sgl.popMatrix();
_ = scale_stack.pop().?;
_ = self.scale_stack.pop().?;
}
const Vertex = struct {
@ -323,13 +328,13 @@ const Vertex = struct {
uv: Vec2
};
fn drawQuad(quad: [4]Vertex, color: Vec4, texture_id: TextureId) void {
fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: TextureId) void {
sgl.enableTexture();
defer sgl.disableTexture();
const view = textures.items[@intFromEnum(texture_id)].view;
const view = self.textures.items[@intFromEnum(texture_id)].view;
// TODO: Make sampler configurable
sgl.texture(view, nearest_sampler);
sgl.texture(view, self.nearest_sampler);
sgl.beginQuads();
defer sgl.end();
@ -352,7 +357,7 @@ fn drawQuadNoUVs(quad: [4]Vec2, color: Vec4) void {
}
}
fn drawRectangle(opts: Command.DrawRectangle) void {
fn drawRectangle(self: *Graphics, opts: Command.DrawRectangle) void {
const pos = opts.rect.pos;
const size = opts.rect.size;
@ -381,7 +386,7 @@ fn drawRectangle(opts: Command.DrawRectangle) void {
.uv = .init(uv.left(), uv.bottom())
}
};
drawQuad(quad, opts.color, sprite.texture);
self.drawQuad(quad, opts.color, sprite.texture);
} else {
const quad = .{
pos.add(top_left),
@ -407,8 +412,8 @@ fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
);
}
pub fn addFont(name: [*c]const u8, data: []const u8) !Font.Id {
return try font_context.addFont(name, data);
fn addFont(self: *Graphics, name: [*c]const u8, data: []const u8) !Font.Id {
return try self.font_context.addFont(name, data);
}
fn makeView(image: sg.Image) !sg.View {
@ -453,7 +458,7 @@ fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
return image;
}
pub fn addTexture(mipmaps: []const Texture.Data) !TextureId {
pub fn addTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Texture.Data) !TextureId {
const image = try makeImageWithMipMaps(mipmaps);
errdefer sg.deallocImage(image);
@ -461,8 +466,8 @@ pub fn addTexture(mipmaps: []const Texture.Data) !TextureId {
errdefer sg.deallocView(view);
assert(mipmaps.len > 0);
const index = textures.items.len;
try textures.append(gpa, .{
const index = self.textures.items.len;
try self.textures.append(gpa, .{
.image = image,
.view = view,
.info = .{
@ -474,7 +479,7 @@ pub fn addTexture(mipmaps: []const Texture.Data) !TextureId {
return @enumFromInt(index);
}
pub fn getTextureInfo(id: TextureId) TextureInfo {
const texture = textures.items[@intFromEnum(id)];
pub fn getTextureInfo(self: *Graphics, id: TextureId) TextureInfo {
const texture = self.textures.items[@intFromEnum(id)];
return texture.info;
}

View File

@ -1,4 +1,4 @@
const Engine = @import("./engine/root.zig");
const Engine = @import("engine");
var engine: Engine = undefined;

View File

@ -23,20 +23,34 @@ const STBImage = @import("stb_image");
const Gfx = Graphics;
const Game = @import("../game.zig");
const Assets = @import("../assets.zig");
const GameCallbacks = struct {
init: *const fn(opts: *const Engine.InitOptions) callconv(.c) void,
tick: *const fn(frame: *Frame) callconv(.c) void,
deinit: *const fn() callconv(.c) void,
debug: *const fn() callconv(.c) void,
};
extern fn init(opts: *const Engine.InitOptions) void;
extern fn tick(frame: *Frame) void;
extern fn deinit() void;
extern fn debug() void;
const Engine = @This();
pub const Nanoseconds = u64;
pub const InitOptions = struct {
gpa: std.mem.Allocator,
seed: u64
};
allocator: std.mem.Allocator,
started_at: std.time.Instant,
mouse_inside: bool,
last_frame_at: Nanoseconds,
graphics: Graphics,
game_callbacks: GameCallbacks,
game: Game,
assets: Assets,
frame: Frame,
canvas_size: ?Vec2 = null,
@ -53,9 +67,14 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
.started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"),
.mouse_inside = false,
.last_frame_at = 0,
.assets = undefined,
.game = undefined,
.frame = undefined
.frame = undefined,
.graphics = undefined,
.game_callbacks = .{
.init = init,
.deinit = deinit,
.tick = tick,
.debug = debug,
}
};
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
@ -84,19 +103,20 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
}
// TODO: Don't hard code icon path, allow changing through options
var icon_data = try STBImage.load(@embedFile("../assets/icon.png"));
defer icon_data.deinit();
// var icon_data = try STBImage.load(@embedFile("../assets/icon.png"));
// defer icon_data.deinit();
var icon: sapp.IconDesc = .{};
icon.sokol_default = false;
icon.images[0] = .{
.width = @intCast(icon_data.width),
.height = @intCast(icon_data.height),
.pixels = .{
.ptr = icon_data.rgba8_pixels,
.size = icon_data.width * icon_data.height * 4
}
};
icon.sokol_default = true;
// TODO:
// icon.images[0] = .{
// .width = @intCast(icon_data.width),
// .height = @intCast(icon_data.height),
// .pixels = .{
// .ptr = icon_data.rgba8_pixels,
// .size = icon_data.width * icon_data.height * 4
// }
// };
sapp.run(.{
.init_userdata_cb = sokolInitCallback,
@ -119,12 +139,15 @@ fn sokolInit(self: *Engine) !void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
try Gfx.init(.{
log.debug("Init", .{});
try self.graphics.init(.{
.allocator = self.allocator,
.logger = .{ .func = sokolLogCallback },
.imgui_font = .{
.ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"),
}
// TODO:
// .imgui_font = .{
// .ttf_data = @embedFile("../assets/roboto-font/Roboto-Regular.ttf"),
// }
});
try Audio.init(.{
@ -134,8 +157,11 @@ fn sokolInit(self: *Engine) !void {
const seed: u64 = @bitCast(std.time.milliTimestamp());
self.assets = try Assets.init(self.allocator);
self.game = try Game.init(self.allocator, seed, &self.assets);
const opts = InitOptions{
.gpa = self.allocator,
.seed = seed,
};
self.game_callbacks.init(&opts);
}
fn sokolCleanup(self: *Engine) void {
@ -144,9 +170,8 @@ fn sokolCleanup(self: *Engine) void {
self.frame.deinit();
Audio.deinit();
self.game.deinit();
self.assets.deinit(self.allocator);
Gfx.deinit();
self.game_callbacks.deinit();
self.graphics.deinit(self.allocator);
}
fn sokolFrame(self: *Engine) !void {
@ -193,7 +218,7 @@ fn sokolFrame(self: *Engine) !void {
frame.dt_ns = time_passed - self.last_frame_at;
frame.hide_cursor = false;
try self.game.tick(&self.frame);
self.game_callbacks.tick(&self.frame);
frame.input.keyboard.pressed = .initEmpty();
frame.input.keyboard.released = .initEmpty();
@ -220,14 +245,13 @@ fn sokolFrame(self: *Engine) !void {
self.canvas_size = self.frame.graphics.canvas_size;
{
Gfx.beginFrame();
defer Gfx.endFrame(frame.graphics.clear_color);
self.graphics.beginFrame();
defer self.graphics.endFrame(frame.graphics.clear_color);
Gfx.drawCommands(frame.graphics.commands.items);
self.graphics.drawCommands(frame.graphics.commands.items);
if (frame.show_debug) {
try self.game.debug();
try showDebugWindow(&self.frame);
try self.showDebugWindow(&self.frame);
}
}
@ -240,23 +264,34 @@ fn sokolFrame(self: *Engine) !void {
}
}
fn showDebugWindow(frame: *Frame) !void {
fn showDebugWindow(self: *Engine, frame: *Frame) !void {
if (!imgui.beginWindow(.{
.name = "Engine",
.pos = Vec2.init(240, 20),
.name = "Debug",
.pos = Vec2.init(20, 20),
.size = Vec2.init(200, 200),
})) {
return;
}
defer imgui.endWindow();
imgui.textFmt("Draw commands: {}\n", .{
frame.graphics.commands.items.len,
});
imgui.textFmt("Audio instances: {}/{}\n", .{
Audio.mixer.instances.items.len,
Audio.mixer.instances.capacity
});
if (imgui.beginTabBar("debug")) {
defer imgui.endTabBar();
if (imgui.beginTabItem("Game")) {
defer imgui.endTabItem();
self.game_callbacks.debug();
}
if (imgui.beginTabItem("Engine")) {
defer imgui.endTabItem();
imgui.textFmt("Draw commands: {}\n", .{
frame.graphics.commands.items.len,
});
imgui.textFmt("Audio instances: {}/{}\n", .{
Audio.mixer.instances.items.len,
Audio.mixer.instances.capacity
});
}
}
}
fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool {

View File

@ -1,44 +0,0 @@
const std = @import("std");
const Math = @import("./engine/math.zig");
const Engine = @import("./engine/root.zig");
const Gfx = Engine.Graphics;
const Audio = Engine.Audio;
const Assets = @This();
const FontName = enum {
regular,
bold,
italic,
const EnumArray = std.EnumArray(FontName, Gfx.Font.Id);
};
font_id: FontName.EnumArray,
wood01: Audio.Data.Id,
pub fn init(gpa: std.mem.Allocator) !Assets {
_ = gpa; // autofix
const font_id_array: FontName.EnumArray = .init(.{
.regular = try Gfx.addFont("regular", @embedFile("assets/roboto-font/Roboto-Regular.ttf")),
.bold = try Gfx.addFont("bold", @embedFile("assets/roboto-font/Roboto-Bold.ttf")),
.italic = try Gfx.addFont("italic", @embedFile("assets/roboto-font/Roboto-Italic.ttf")),
});
const wood01 = try Audio.load(.{
.format = .vorbis,
.data = @embedFile("assets/wood01.ogg"),
});
return Assets{
.font_id = font_id_array,
.wood01 = wood01
};
}
pub fn deinit(self: *Assets, gpa: std.mem.Allocator) void {
_ = self; // autofix
_ = gpa; // autofix
}

View File

@ -2,9 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const Assets = @import("./assets.zig");
const Engine = @import("./engine/root.zig");
const Engine = @import("engine");
const imgui = Engine.imgui;
const Vec2 = Engine.Vec2;
const rgb = Engine.Math.rgb;
@ -13,25 +11,55 @@ const Audio = Engine.Audio;
const Game = @This();
gpa: Allocator,
assets: *Assets,
const FontName = enum {
regular,
bold,
italic,
player: Vec2,
const EnumArray = std.EnumArray(FontName, Gfx.Font.Id);
};
pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game {
_ = seed; // autofix
return Game{
const State = struct {
gpa: Allocator,
player: Vec2,
// font_id: FontName.EnumArray,
// wood01: Audio.Data.Id,
};
var g_state: *State = undefined;
pub export fn init(opts: *const Engine.InitOptions) void {
const gpa = opts.gpa;
// const font_id_array: FontName.EnumArray = .init(.{
// .regular = try Gfx.addFont("regular", @embedFile("assets/roboto-font/Roboto-Regular.ttf")),
// .bold = try Gfx.addFont("bold", @embedFile("assets/roboto-font/Roboto-Bold.ttf")),
// .italic = try Gfx.addFont("italic", @embedFile("assets/roboto-font/Roboto-Italic.ttf")),
// });
// const wood01 = try Audio.load(.{
// .format = .vorbis,
// .data = @embedFile("assets/wood01.ogg"),
// });
g_state = gpa.create(State) catch @panic("OOM");
g_state.* = State{
.gpa = gpa,
.assets = assets,
.player = .init(50, 50)
.player = .init(50, 50),
// .font_id = font_id_array,
// .wood01 = wood01
};
}
pub fn deinit(self: *Game) void {
_ = self; // autofix
pub export fn deinit() void {
const gpa = g_state.gpa;
gpa.destroy(g_state);
g_state = undefined;
}
pub fn tick(self: *Game, frame: *Engine.Frame) !void {
pub export fn tick(frame: *Engine.Frame) void {
const dt = frame.deltaTime();
const canvas_size = Vec2.init(100, 100);
@ -57,15 +85,13 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void {
dir = dir.normalized();
if (dir.x != 0 or dir.y != 0) {
frame.playAudio(.{
.id = self.assets.wood01,
.volume = 0.1
});
// frame.playAudio(.{
// .id = g_state.wood01,
// .volume = 0.1
// });
}
self.player = self.player.add(dir.multiplyScalar(50 * dt));
const regular_font = self.assets.font_id.get(.regular);
g_state.player = g_state.player.add(dir.multiplyScalar(50 * dt));
frame.drawRectangle(.{
.rect = .{ .pos = .init(0, 0), .size = canvas_size },
@ -74,35 +100,22 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void {
const size = Vec2.init(20, 20);
frame.drawRectangle(.{
.rect = .{
.pos = self.player.sub(size.divideScalar(2)),
.pos = g_state.player.sub(size.divideScalar(2)),
.size = size
},
.color = rgb(200, 20, 20)
});
if (dir.x != 0 or dir.y != 0) {
frame.drawRectanglOutline(self.player.sub(size.divideScalar(2)), size, rgb(20, 200, 20), 3);
frame.drawRectanglOutline(g_state.player.sub(size.divideScalar(2)), size, rgb(20, 200, 20), 3);
}
frame.drawText(self.player, "Player", .{
.font = regular_font,
.size = 10
});
// const regular_font = g_state.font_id.get(.regular);
// frame.drawText(g_state.player, "Player", .{
// .font = regular_font,
// .size = 10
// });
}
pub fn debug(self: *Game) !void {
_ = self; // autofix
if (!imgui.beginWindow(.{
.name = "Debug",
.pos = Vec2.init(20, 20),
.size = Vec2.init(400, 200),
})) {
return;
}
defer imgui.endWindow();
pub export fn debug() void {
imgui.text("Hello World!\n");
imgui.textFmt("Audio: {}/{}\n", .{
Audio.mixer.instances.items.len,
Audio.mixer.instances.capacity
});
}